claude-opencode-viewer 2.6.23 → 2.6.25
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/package.json +1 -1
- package/server.js +63 -48
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const IS_PC = process.argv.includes('--pc');
|
|
|
19
19
|
const PORT = parseInt(process.argv[2]) || 7008;
|
|
20
20
|
const USE_HTTPS = process.argv.includes('--https');
|
|
21
21
|
const JSON_OUTPUT = process.argv.includes('--json');
|
|
22
|
+
const LOG = JSON_OUTPUT ? console.log.bind(console) : () => {};
|
|
22
23
|
|
|
23
24
|
// 自签名证书生成(使用 selfsigned 库,不依赖 openssl)
|
|
24
25
|
async function getOrCreateCert() {
|
|
@@ -30,7 +31,7 @@ async function getOrCreateCert() {
|
|
|
30
31
|
return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
if (!JSON_OUTPUT)
|
|
34
|
+
if (!JSON_OUTPUT) LOG('🔐 首次使用 HTTPS,生成自签名证书...');
|
|
34
35
|
mkdirSync(certDir, { recursive: true });
|
|
35
36
|
|
|
36
37
|
const { default: selfsigned } = await import('selfsigned');
|
|
@@ -48,7 +49,7 @@ async function getOrCreateCert() {
|
|
|
48
49
|
|
|
49
50
|
writeFileSync(keyPath, pems.private);
|
|
50
51
|
writeFileSync(certPath, pems.cert);
|
|
51
|
-
if (!JSON_OUTPUT)
|
|
52
|
+
if (!JSON_OUTPUT) LOG(`📁 证书已保存到 ${certDir}`);
|
|
52
53
|
return { key: pems.private, cert: pems.cert };
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -200,7 +201,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
200
201
|
// 如果提供了 sessionId,添加 --session 参数
|
|
201
202
|
if (sessionId) {
|
|
202
203
|
args = ['--session', sessionId];
|
|
203
|
-
|
|
204
|
+
LOG(`[opencode] 恢复会话: ${sessionId}`);
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
|
|
@@ -219,10 +220,10 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
219
220
|
db.close();
|
|
220
221
|
if (session && session.directory && existsSync(session.directory)) {
|
|
221
222
|
spawnCwd = session.directory;
|
|
222
|
-
|
|
223
|
+
LOG(`[opencode] 使用会话目录: ${spawnCwd}`);
|
|
223
224
|
}
|
|
224
225
|
} catch (e) {
|
|
225
|
-
|
|
226
|
+
LOG('[opencode] 查询会话目录失败:', e.message);
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
|
|
@@ -245,7 +246,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
245
246
|
});
|
|
246
247
|
|
|
247
248
|
proc.onExit(({ exitCode }) => {
|
|
248
|
-
|
|
249
|
+
LOG(`[onExit] 进程退出, PID: ${proc.pid}, exitCode: ${exitCode}`);
|
|
249
250
|
if (currentProcess === proc) {
|
|
250
251
|
currentProcess = null;
|
|
251
252
|
}
|
|
@@ -264,7 +265,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
264
265
|
if (mode === 'claude') {
|
|
265
266
|
if (claudeProcess && claudeProcess !== proc && claudeProcess.pid) {
|
|
266
267
|
try {
|
|
267
|
-
|
|
268
|
+
LOG(`[spawnProcess] 清理旧 claude 进程 PID: ${claudeProcess.pid}`);
|
|
268
269
|
killProcessTree(claudeProcess);
|
|
269
270
|
} catch {}
|
|
270
271
|
}
|
|
@@ -272,7 +273,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
272
273
|
} else {
|
|
273
274
|
if (opencodeProcess && opencodeProcess !== proc && opencodeProcess.pid) {
|
|
274
275
|
try {
|
|
275
|
-
|
|
276
|
+
LOG(`[spawnProcess] 清理旧 opencode 进程 PID: ${opencodeProcess.pid}`);
|
|
276
277
|
killProcessTree(opencodeProcess);
|
|
277
278
|
} catch {}
|
|
278
279
|
}
|
|
@@ -281,7 +282,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
281
282
|
// 追踪当前会话 ID
|
|
282
283
|
if (sessionId) {
|
|
283
284
|
currentSessionId = sessionId;
|
|
284
|
-
|
|
285
|
+
LOG(`[session] 当前会话 ID: ${currentSessionId}`);
|
|
285
286
|
} else {
|
|
286
287
|
// 新建会话:保持 null,前端点击复制时不显示内容
|
|
287
288
|
currentSessionId = null;
|
|
@@ -289,14 +290,14 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
currentProcess = proc;
|
|
292
|
-
|
|
293
|
+
LOG(`[claude-opencode-viewer] ${mode === 'claude' ? 'Claude Code' : 'OpenCode'} 已启动 (PID: ${proc.pid})`);
|
|
293
294
|
return proc;
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
async function switchMode(newMode) {
|
|
297
298
|
if (newMode === currentMode) return;
|
|
298
299
|
|
|
299
|
-
|
|
300
|
+
LOG(`[switchMode] 从 ${currentMode} 切换到 ${newMode}`);
|
|
300
301
|
|
|
301
302
|
isSwitching = true;
|
|
302
303
|
|
|
@@ -306,18 +307,18 @@ async function switchMode(newMode) {
|
|
|
306
307
|
// 杀死旧进程
|
|
307
308
|
if (currentMode === 'claude' && claudeProcess) {
|
|
308
309
|
try {
|
|
309
|
-
|
|
310
|
+
LOG(`[switchMode] 杀死 claude 进程 PID: ${claudeProcess.pid}`);
|
|
310
311
|
killProcessTree(claudeProcess);
|
|
311
312
|
} catch (e) {
|
|
312
|
-
|
|
313
|
+
LOG('[switchMode] 杀死 claude 进程失败:', e.message);
|
|
313
314
|
}
|
|
314
315
|
claudeProcess = null;
|
|
315
316
|
} else if (currentMode === 'opencode' && opencodeProcess) {
|
|
316
317
|
try {
|
|
317
|
-
|
|
318
|
+
LOG(`[switchMode] 杀死 opencode 进程 PID: ${opencodeProcess.pid}`);
|
|
318
319
|
killProcessTree(opencodeProcess);
|
|
319
320
|
} catch (e) {
|
|
320
|
-
|
|
321
|
+
LOG('[switchMode] 杀死 opencode 进程失败:', e.message);
|
|
321
322
|
}
|
|
322
323
|
opencodeProcess = null;
|
|
323
324
|
}
|
|
@@ -334,9 +335,9 @@ async function switchMode(newMode) {
|
|
|
334
335
|
if (currentProcess && lastPtyCols && lastPtyRows) {
|
|
335
336
|
try { currentProcess.resize(lastPtyCols, lastPtyRows); } catch {}
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
+
LOG(`[switchMode] 切换到 ${newMode} 成功`);
|
|
338
339
|
} catch (e) {
|
|
339
|
-
|
|
340
|
+
LOG('[switchMode] 启动新进程失败:', e.message);
|
|
340
341
|
}
|
|
341
342
|
isSwitching = false;
|
|
342
343
|
}
|
|
@@ -361,7 +362,7 @@ function resizePty(cols, rows) {
|
|
|
361
362
|
function getOpenCodeSessions() {
|
|
362
363
|
try {
|
|
363
364
|
if (!existsSync(OPENCODE_DB_PATH)) {
|
|
364
|
-
|
|
365
|
+
LOG('[DB] OpenCode 数据库不存在:', OPENCODE_DB_PATH);
|
|
365
366
|
return [];
|
|
366
367
|
}
|
|
367
368
|
|
|
@@ -416,7 +417,7 @@ function getOpenCodeSessions() {
|
|
|
416
417
|
: partData.text;
|
|
417
418
|
}
|
|
418
419
|
} catch (e) {
|
|
419
|
-
|
|
420
|
+
LOG('[DB] 解析 part 失败:', e.message);
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
}
|
|
@@ -430,7 +431,7 @@ function getOpenCodeSessions() {
|
|
|
430
431
|
db.close();
|
|
431
432
|
return result;
|
|
432
433
|
} catch (err) {
|
|
433
|
-
|
|
434
|
+
LOG('[DB] 读取会话失败:', err.message);
|
|
434
435
|
return [];
|
|
435
436
|
}
|
|
436
437
|
}
|
|
@@ -501,7 +502,7 @@ function getSessionMessages(sessionId) {
|
|
|
501
502
|
db.close();
|
|
502
503
|
return result;
|
|
503
504
|
} catch (err) {
|
|
504
|
-
|
|
505
|
+
LOG('[DB] 读取消息失败:', err.message);
|
|
505
506
|
return [];
|
|
506
507
|
}
|
|
507
508
|
}
|
|
@@ -661,7 +662,7 @@ const requestHandler = async (req, res) => {
|
|
|
661
662
|
db.close();
|
|
662
663
|
res.end(JSON.stringify({ ok: true, changes: result.changes }));
|
|
663
664
|
} catch (err) {
|
|
664
|
-
|
|
665
|
+
LOG('[DB] 软删除会话失败:', err.message);
|
|
665
666
|
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
666
667
|
}
|
|
667
668
|
return;
|
|
@@ -706,7 +707,9 @@ function createServerAndWss() {
|
|
|
706
707
|
|
|
707
708
|
function setupWss(wssInst) {
|
|
708
709
|
wssInst.on('connection', (ws, req) => {
|
|
709
|
-
|
|
710
|
+
LOG('[WS] 客户端连接 from', req.socket.remoteAddress);
|
|
711
|
+
ws.isAlive = true;
|
|
712
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
710
713
|
|
|
711
714
|
ws.send(JSON.stringify({
|
|
712
715
|
type: 'state',
|
|
@@ -732,7 +735,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
732
735
|
await spawnProcess('opencode', sid);
|
|
733
736
|
ws.send(JSON.stringify({ type: 'started', sessionId: sid }));
|
|
734
737
|
} catch (e) {
|
|
735
|
-
|
|
738
|
+
LOG('[reconnect] 重启失败:', e.message);
|
|
736
739
|
}
|
|
737
740
|
isSwitching = false;
|
|
738
741
|
}, 200);
|
|
@@ -758,19 +761,19 @@ wssInst.on('connection', (ws, req) => {
|
|
|
758
761
|
ws.on('message', async (raw) => {
|
|
759
762
|
try {
|
|
760
763
|
const msg = JSON.parse(raw);
|
|
761
|
-
|
|
764
|
+
LOG(`[WS msg] type=${msg.type}, currentProcess=${!!currentProcess}, currentMode=${currentMode}`);
|
|
762
765
|
|
|
763
766
|
if (msg.type === 'input') {
|
|
764
767
|
// 进程已退出时,自动重新启动(参考 cc-viewer 逻辑)
|
|
765
768
|
// 模式切换/新建会话期间不触发 respawn
|
|
766
769
|
if (!currentProcess && !isSwitching) {
|
|
767
770
|
try {
|
|
768
|
-
|
|
771
|
+
LOG(`[respawn] 进程已退出,自动重新启动 ${currentMode}`);
|
|
769
772
|
outputBuffer = '';
|
|
770
773
|
await spawnProcess(currentMode);
|
|
771
|
-
|
|
774
|
+
LOG(`[respawn] 重新启动成功, currentProcess=${!!currentProcess}`);
|
|
772
775
|
} catch (e) {
|
|
773
|
-
|
|
776
|
+
LOG(`[respawn] 重新启动失败: ${e.message}`);
|
|
774
777
|
}
|
|
775
778
|
}
|
|
776
779
|
if (activeWs !== ws) {
|
|
@@ -809,17 +812,17 @@ wssInst.on('connection', (ws, req) => {
|
|
|
809
812
|
} else if (msg.type === 'restore') {
|
|
810
813
|
// 恢复会话
|
|
811
814
|
if (msg.sessionId && currentMode === 'opencode') {
|
|
812
|
-
|
|
815
|
+
LOG(`[restore] 恢复会话: ${msg.sessionId}`);
|
|
813
816
|
|
|
814
817
|
isSwitching = true;
|
|
815
818
|
|
|
816
819
|
// 杀死当前 opencode 进程
|
|
817
820
|
if (opencodeProcess) {
|
|
818
821
|
try {
|
|
819
|
-
|
|
822
|
+
LOG(`[restore] 杀死当前进程 PID: ${opencodeProcess.pid}`);
|
|
820
823
|
killProcessTree(opencodeProcess);
|
|
821
824
|
} catch (e) {
|
|
822
|
-
|
|
825
|
+
LOG('[restore] 杀死进程失败:', e.message);
|
|
823
826
|
}
|
|
824
827
|
opencodeProcess = null;
|
|
825
828
|
currentProcess = null;
|
|
@@ -836,7 +839,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
836
839
|
await spawnProcess('opencode', msg.sessionId);
|
|
837
840
|
ws.send(JSON.stringify({ type: 'restored', sessionId: msg.sessionId }));
|
|
838
841
|
} catch (e) {
|
|
839
|
-
|
|
842
|
+
LOG('[restore] 启动进程失败:', e.message);
|
|
840
843
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
841
844
|
}
|
|
842
845
|
isSwitching = false;
|
|
@@ -844,19 +847,19 @@ wssInst.on('connection', (ws, req) => {
|
|
|
844
847
|
} else if (msg.type === 'start') {
|
|
845
848
|
// 前端启动指令:可选带 sessionId 恢复会话
|
|
846
849
|
const mode = currentMode;
|
|
847
|
-
|
|
850
|
+
LOG(`[start] 启动 ${mode}, sessionId=${msg.sessionId || '(新会话)'}`);
|
|
848
851
|
outputBuffer = '';
|
|
849
852
|
try {
|
|
850
853
|
await spawnProcess(mode, msg.sessionId || null);
|
|
851
854
|
ws.send(JSON.stringify({ type: 'started', sessionId: msg.sessionId || null }));
|
|
852
855
|
} catch (e) {
|
|
853
|
-
|
|
856
|
+
LOG('[start] 启动失败:', e.message);
|
|
854
857
|
ws.send(JSON.stringify({ type: 'start-error', error: e.message }));
|
|
855
858
|
}
|
|
856
859
|
} else if (msg.type === 'new-session') {
|
|
857
860
|
// 开启新会话:杀掉当前进程,重新启动不带 session 参数的 opencode
|
|
858
861
|
const mode = currentMode;
|
|
859
|
-
|
|
862
|
+
LOG(`[new-session] 开启新会话, mode=${mode}`);
|
|
860
863
|
|
|
861
864
|
isSwitching = true;
|
|
862
865
|
previousSessionId = currentSessionId; // 记住旧会话,用于判断新会话是否已写入 DB
|
|
@@ -880,7 +883,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
880
883
|
try {
|
|
881
884
|
await spawnProcess(mode);
|
|
882
885
|
} catch (e) {
|
|
883
|
-
|
|
886
|
+
LOG('[new-session] 启动失败:', e.message);
|
|
884
887
|
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
885
888
|
}
|
|
886
889
|
isSwitching = false;
|
|
@@ -898,7 +901,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
898
901
|
db.close();
|
|
899
902
|
if (row && row.id !== prevSid) {
|
|
900
903
|
currentSessionId = row.id;
|
|
901
|
-
|
|
904
|
+
LOG(`[new-session] 检测到新会话 ID: ${currentSessionId}`);
|
|
902
905
|
return;
|
|
903
906
|
}
|
|
904
907
|
}
|
|
@@ -908,7 +911,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
908
911
|
setTimeout(() => checkNewSession(0), 3000);
|
|
909
912
|
}
|
|
910
913
|
} catch (err) {
|
|
911
|
-
|
|
914
|
+
LOG('[WS] Error:', err.message);
|
|
912
915
|
}
|
|
913
916
|
});
|
|
914
917
|
|
|
@@ -936,6 +939,18 @@ wssInst.on('connection', (ws, req) => {
|
|
|
936
939
|
}
|
|
937
940
|
});
|
|
938
941
|
});
|
|
942
|
+
|
|
943
|
+
// WebSocket 心跳保活,防止中间网络设备断开空闲连接
|
|
944
|
+
const HEARTBEAT_INTERVAL = 15000;
|
|
945
|
+
const heartbeat = setInterval(() => {
|
|
946
|
+
wssInst.clients.forEach((ws) => {
|
|
947
|
+
if (ws.isAlive === false) return ws.terminate();
|
|
948
|
+
ws.isAlive = false;
|
|
949
|
+
ws.ping();
|
|
950
|
+
});
|
|
951
|
+
}, HEARTBEAT_INTERVAL);
|
|
952
|
+
|
|
953
|
+
wssInst.on('close', () => clearInterval(heartbeat));
|
|
939
954
|
} // end setupWss
|
|
940
955
|
|
|
941
956
|
createServerAndWss();
|
|
@@ -954,16 +969,16 @@ function startServer() {
|
|
|
954
969
|
const proto = USE_HTTPS ? 'https' : 'http';
|
|
955
970
|
|
|
956
971
|
if (JSON_OUTPUT) {
|
|
957
|
-
|
|
972
|
+
LOG(JSON.stringify({ port: PORT, url: `${proto}://127.0.0.1:${PORT}`, ip, proto, pid: process.pid }));
|
|
958
973
|
} else {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (USE_HTTPS)
|
|
965
|
-
|
|
966
|
-
|
|
974
|
+
LOG('\n' + '='.repeat(50));
|
|
975
|
+
LOG('✅ Claude OpenCode Viewer 已启动');
|
|
976
|
+
LOG('='.repeat(50));
|
|
977
|
+
LOG(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
|
|
978
|
+
LOG(`📱 手机访问:${proto}://${ip}:${PORT}`);
|
|
979
|
+
if (USE_HTTPS) LOG('🔐 HTTPS 模式(首次访问需信任自签名证书)');
|
|
980
|
+
LOG('='.repeat(50));
|
|
981
|
+
LOG('\n按 Ctrl+C 停止服务\n');
|
|
967
982
|
}
|
|
968
983
|
|
|
969
984
|
// 尝试恢复最近的会话,如果没有则新建
|
|
@@ -978,7 +993,7 @@ function startServer() {
|
|
|
978
993
|
} catch (e) {}
|
|
979
994
|
|
|
980
995
|
if (lastSessionId) {
|
|
981
|
-
|
|
996
|
+
LOG(`[startup] 恢复最近会话: ${lastSessionId}`);
|
|
982
997
|
await spawnProcess('opencode', lastSessionId);
|
|
983
998
|
} else {
|
|
984
999
|
await spawnProcess('opencode');
|