claude-opencode-viewer 2.6.23 → 2.6.24
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 +49 -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,7 @@ function createServerAndWss() {
|
|
|
706
707
|
|
|
707
708
|
function setupWss(wssInst) {
|
|
708
709
|
wssInst.on('connection', (ws, req) => {
|
|
709
|
-
|
|
710
|
+
LOG('[WS] 客户端连接 from', req.socket.remoteAddress);
|
|
710
711
|
|
|
711
712
|
ws.send(JSON.stringify({
|
|
712
713
|
type: 'state',
|
|
@@ -732,7 +733,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
732
733
|
await spawnProcess('opencode', sid);
|
|
733
734
|
ws.send(JSON.stringify({ type: 'started', sessionId: sid }));
|
|
734
735
|
} catch (e) {
|
|
735
|
-
|
|
736
|
+
LOG('[reconnect] 重启失败:', e.message);
|
|
736
737
|
}
|
|
737
738
|
isSwitching = false;
|
|
738
739
|
}, 200);
|
|
@@ -758,19 +759,19 @@ wssInst.on('connection', (ws, req) => {
|
|
|
758
759
|
ws.on('message', async (raw) => {
|
|
759
760
|
try {
|
|
760
761
|
const msg = JSON.parse(raw);
|
|
761
|
-
|
|
762
|
+
LOG(`[WS msg] type=${msg.type}, currentProcess=${!!currentProcess}, currentMode=${currentMode}`);
|
|
762
763
|
|
|
763
764
|
if (msg.type === 'input') {
|
|
764
765
|
// 进程已退出时,自动重新启动(参考 cc-viewer 逻辑)
|
|
765
766
|
// 模式切换/新建会话期间不触发 respawn
|
|
766
767
|
if (!currentProcess && !isSwitching) {
|
|
767
768
|
try {
|
|
768
|
-
|
|
769
|
+
LOG(`[respawn] 进程已退出,自动重新启动 ${currentMode}`);
|
|
769
770
|
outputBuffer = '';
|
|
770
771
|
await spawnProcess(currentMode);
|
|
771
|
-
|
|
772
|
+
LOG(`[respawn] 重新启动成功, currentProcess=${!!currentProcess}`);
|
|
772
773
|
} catch (e) {
|
|
773
|
-
|
|
774
|
+
LOG(`[respawn] 重新启动失败: ${e.message}`);
|
|
774
775
|
}
|
|
775
776
|
}
|
|
776
777
|
if (activeWs !== ws) {
|
|
@@ -809,17 +810,17 @@ wssInst.on('connection', (ws, req) => {
|
|
|
809
810
|
} else if (msg.type === 'restore') {
|
|
810
811
|
// 恢复会话
|
|
811
812
|
if (msg.sessionId && currentMode === 'opencode') {
|
|
812
|
-
|
|
813
|
+
LOG(`[restore] 恢复会话: ${msg.sessionId}`);
|
|
813
814
|
|
|
814
815
|
isSwitching = true;
|
|
815
816
|
|
|
816
817
|
// 杀死当前 opencode 进程
|
|
817
818
|
if (opencodeProcess) {
|
|
818
819
|
try {
|
|
819
|
-
|
|
820
|
+
LOG(`[restore] 杀死当前进程 PID: ${opencodeProcess.pid}`);
|
|
820
821
|
killProcessTree(opencodeProcess);
|
|
821
822
|
} catch (e) {
|
|
822
|
-
|
|
823
|
+
LOG('[restore] 杀死进程失败:', e.message);
|
|
823
824
|
}
|
|
824
825
|
opencodeProcess = null;
|
|
825
826
|
currentProcess = null;
|
|
@@ -836,7 +837,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
836
837
|
await spawnProcess('opencode', msg.sessionId);
|
|
837
838
|
ws.send(JSON.stringify({ type: 'restored', sessionId: msg.sessionId }));
|
|
838
839
|
} catch (e) {
|
|
839
|
-
|
|
840
|
+
LOG('[restore] 启动进程失败:', e.message);
|
|
840
841
|
ws.send(JSON.stringify({ type: 'restore-error', error: e.message }));
|
|
841
842
|
}
|
|
842
843
|
isSwitching = false;
|
|
@@ -844,19 +845,19 @@ wssInst.on('connection', (ws, req) => {
|
|
|
844
845
|
} else if (msg.type === 'start') {
|
|
845
846
|
// 前端启动指令:可选带 sessionId 恢复会话
|
|
846
847
|
const mode = currentMode;
|
|
847
|
-
|
|
848
|
+
LOG(`[start] 启动 ${mode}, sessionId=${msg.sessionId || '(新会话)'}`);
|
|
848
849
|
outputBuffer = '';
|
|
849
850
|
try {
|
|
850
851
|
await spawnProcess(mode, msg.sessionId || null);
|
|
851
852
|
ws.send(JSON.stringify({ type: 'started', sessionId: msg.sessionId || null }));
|
|
852
853
|
} catch (e) {
|
|
853
|
-
|
|
854
|
+
LOG('[start] 启动失败:', e.message);
|
|
854
855
|
ws.send(JSON.stringify({ type: 'start-error', error: e.message }));
|
|
855
856
|
}
|
|
856
857
|
} else if (msg.type === 'new-session') {
|
|
857
858
|
// 开启新会话:杀掉当前进程,重新启动不带 session 参数的 opencode
|
|
858
859
|
const mode = currentMode;
|
|
859
|
-
|
|
860
|
+
LOG(`[new-session] 开启新会话, mode=${mode}`);
|
|
860
861
|
|
|
861
862
|
isSwitching = true;
|
|
862
863
|
previousSessionId = currentSessionId; // 记住旧会话,用于判断新会话是否已写入 DB
|
|
@@ -880,7 +881,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
880
881
|
try {
|
|
881
882
|
await spawnProcess(mode);
|
|
882
883
|
} catch (e) {
|
|
883
|
-
|
|
884
|
+
LOG('[new-session] 启动失败:', e.message);
|
|
884
885
|
ws.send(JSON.stringify({ type: 'new-session-error', error: e.message }));
|
|
885
886
|
}
|
|
886
887
|
isSwitching = false;
|
|
@@ -898,7 +899,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
898
899
|
db.close();
|
|
899
900
|
if (row && row.id !== prevSid) {
|
|
900
901
|
currentSessionId = row.id;
|
|
901
|
-
|
|
902
|
+
LOG(`[new-session] 检测到新会话 ID: ${currentSessionId}`);
|
|
902
903
|
return;
|
|
903
904
|
}
|
|
904
905
|
}
|
|
@@ -908,7 +909,7 @@ wssInst.on('connection', (ws, req) => {
|
|
|
908
909
|
setTimeout(() => checkNewSession(0), 3000);
|
|
909
910
|
}
|
|
910
911
|
} catch (err) {
|
|
911
|
-
|
|
912
|
+
LOG('[WS] Error:', err.message);
|
|
912
913
|
}
|
|
913
914
|
});
|
|
914
915
|
|
|
@@ -954,16 +955,16 @@ function startServer() {
|
|
|
954
955
|
const proto = USE_HTTPS ? 'https' : 'http';
|
|
955
956
|
|
|
956
957
|
if (JSON_OUTPUT) {
|
|
957
|
-
|
|
958
|
+
LOG(JSON.stringify({ port: PORT, url: `${proto}://127.0.0.1:${PORT}`, ip, proto, pid: process.pid }));
|
|
958
959
|
} else {
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (USE_HTTPS)
|
|
965
|
-
|
|
966
|
-
|
|
960
|
+
LOG('\n' + '='.repeat(50));
|
|
961
|
+
LOG('✅ Claude OpenCode Viewer 已启动');
|
|
962
|
+
LOG('='.repeat(50));
|
|
963
|
+
LOG(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
|
|
964
|
+
LOG(`📱 手机访问:${proto}://${ip}:${PORT}`);
|
|
965
|
+
if (USE_HTTPS) LOG('🔐 HTTPS 模式(首次访问需信任自签名证书)');
|
|
966
|
+
LOG('='.repeat(50));
|
|
967
|
+
LOG('\n按 Ctrl+C 停止服务\n');
|
|
967
968
|
}
|
|
968
969
|
|
|
969
970
|
// 尝试恢复最近的会话,如果没有则新建
|
|
@@ -978,7 +979,7 @@ function startServer() {
|
|
|
978
979
|
} catch (e) {}
|
|
979
980
|
|
|
980
981
|
if (lastSessionId) {
|
|
981
|
-
|
|
982
|
+
LOG(`[startup] 恢复最近会话: ${lastSessionId}`);
|
|
982
983
|
await spawnProcess('opencode', lastSessionId);
|
|
983
984
|
} else {
|
|
984
985
|
await spawnProcess('opencode');
|