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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +63 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.23",
3
+ "version": "2.6.25",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",
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) console.log('🔐 首次使用 HTTPS,生成自签名证书...');
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) console.log(`📁 证书已保存到 ${certDir}`);
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
- console.log(`[opencode] 恢复会话: ${sessionId}`);
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
- console.log(`[opencode] 使用会话目录: ${spawnCwd}`);
223
+ LOG(`[opencode] 使用会话目录: ${spawnCwd}`);
223
224
  }
224
225
  } catch (e) {
225
- console.log('[opencode] 查询会话目录失败:', e.message);
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
- console.log(`[onExit] 进程退出, PID: ${proc.pid}, exitCode: ${exitCode}`);
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
- console.log(`[spawnProcess] 清理旧 claude 进程 PID: ${claudeProcess.pid}`);
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
- console.log(`[spawnProcess] 清理旧 opencode 进程 PID: ${opencodeProcess.pid}`);
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
- console.log(`[session] 当前会话 ID: ${currentSessionId}`);
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
- console.log(`[claude-opencode-viewer] ${mode === 'claude' ? 'Claude Code' : 'OpenCode'} 已启动 (PID: ${proc.pid})`);
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
- console.log(`[switchMode] 从 ${currentMode} 切换到 ${newMode}`);
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
- console.log(`[switchMode] 杀死 claude 进程 PID: ${claudeProcess.pid}`);
310
+ LOG(`[switchMode] 杀死 claude 进程 PID: ${claudeProcess.pid}`);
310
311
  killProcessTree(claudeProcess);
311
312
  } catch (e) {
312
- console.log('[switchMode] 杀死 claude 进程失败:', e.message);
313
+ LOG('[switchMode] 杀死 claude 进程失败:', e.message);
313
314
  }
314
315
  claudeProcess = null;
315
316
  } else if (currentMode === 'opencode' && opencodeProcess) {
316
317
  try {
317
- console.log(`[switchMode] 杀死 opencode 进程 PID: ${opencodeProcess.pid}`);
318
+ LOG(`[switchMode] 杀死 opencode 进程 PID: ${opencodeProcess.pid}`);
318
319
  killProcessTree(opencodeProcess);
319
320
  } catch (e) {
320
- console.log('[switchMode] 杀死 opencode 进程失败:', e.message);
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
- console.log(`[switchMode] 切换到 ${newMode} 成功`);
338
+ LOG(`[switchMode] 切换到 ${newMode} 成功`);
338
339
  } catch (e) {
339
- console.error('[switchMode] 启动新进程失败:', e.message);
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
- console.log('[DB] OpenCode 数据库不存在:', OPENCODE_DB_PATH);
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
- console.error('[DB] 解析 part 失败:', e.message);
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
- console.error('[DB] 读取会话失败:', err.message);
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
- console.error('[DB] 读取消息失败:', err.message);
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
- console.error('[DB] 软删除会话失败:', err.message);
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
- console.log('[WS] 客户端连接 from', req.socket.remoteAddress);
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
- console.error('[reconnect] 重启失败:', e.message);
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
- console.log(`[WS msg] type=${msg.type}, currentProcess=${!!currentProcess}, currentMode=${currentMode}`);
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
- console.log(`[respawn] 进程已退出,自动重新启动 ${currentMode}`);
771
+ LOG(`[respawn] 进程已退出,自动重新启动 ${currentMode}`);
769
772
  outputBuffer = '';
770
773
  await spawnProcess(currentMode);
771
- console.log(`[respawn] 重新启动成功, currentProcess=${!!currentProcess}`);
774
+ LOG(`[respawn] 重新启动成功, currentProcess=${!!currentProcess}`);
772
775
  } catch (e) {
773
- console.log(`[respawn] 重新启动失败: ${e.message}`);
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
- console.log(`[restore] 恢复会话: ${msg.sessionId}`);
815
+ LOG(`[restore] 恢复会话: ${msg.sessionId}`);
813
816
 
814
817
  isSwitching = true;
815
818
 
816
819
  // 杀死当前 opencode 进程
817
820
  if (opencodeProcess) {
818
821
  try {
819
- console.log(`[restore] 杀死当前进程 PID: ${opencodeProcess.pid}`);
822
+ LOG(`[restore] 杀死当前进程 PID: ${opencodeProcess.pid}`);
820
823
  killProcessTree(opencodeProcess);
821
824
  } catch (e) {
822
- console.log('[restore] 杀死进程失败:', e.message);
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
- console.error('[restore] 启动进程失败:', e.message);
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
- console.log(`[start] 启动 ${mode}, sessionId=${msg.sessionId || '(新会话)'}`);
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
- console.error('[start] 启动失败:', e.message);
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
- console.log(`[new-session] 开启新会话, mode=${mode}`);
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
- console.error('[new-session] 启动失败:', e.message);
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
- console.log(`[new-session] 检测到新会话 ID: ${currentSessionId}`);
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
- console.error('[WS] Error:', err.message);
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
- console.log(JSON.stringify({ port: PORT, url: `${proto}://127.0.0.1:${PORT}`, ip, proto, pid: process.pid }));
972
+ LOG(JSON.stringify({ port: PORT, url: `${proto}://127.0.0.1:${PORT}`, ip, proto, pid: process.pid }));
958
973
  } else {
959
- console.log('\n' + '='.repeat(50));
960
- console.log('✅ Claude OpenCode Viewer 已启动');
961
- console.log('='.repeat(50));
962
- console.log(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
963
- console.log(`📱 手机访问:${proto}://${ip}:${PORT}`);
964
- if (USE_HTTPS) console.log('🔐 HTTPS 模式(首次访问需信任自签名证书)');
965
- console.log('='.repeat(50));
966
- console.log('\n按 Ctrl+C 停止服务\n');
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
- console.log(`[startup] 恢复最近会话: ${lastSessionId}`);
996
+ LOG(`[startup] 恢复最近会话: ${lastSessionId}`);
982
997
  await spawnProcess('opencode', lastSessionId);
983
998
  } else {
984
999
  await spawnProcess('opencode');