claude-opencode-viewer 2.6.29 → 2.6.31
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 +52 -30
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -199,6 +199,20 @@ function killProcessTree(proc) {
|
|
|
199
199
|
}, 1000);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// 清理孤儿进程(PPID=1 的 opencode/claude)
|
|
203
|
+
function cleanupOrphanProcesses() {
|
|
204
|
+
try {
|
|
205
|
+
const orphans = execSync(
|
|
206
|
+
"ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && (/opencode/||/claude/) {print $1}'",
|
|
207
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
208
|
+
).trim().split(/\s+/).filter(Boolean).map(Number);
|
|
209
|
+
for (const pid of orphans) {
|
|
210
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
211
|
+
}
|
|
212
|
+
if (orphans.length > 0) LOG(`[cleanup] 清理孤儿进程: ${orphans.join(', ')}`);
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
|
|
202
216
|
async function spawnProcess(mode, sessionId = null) {
|
|
203
217
|
const pty = await getPty();
|
|
204
218
|
fixSpawnHelperPermissions();
|
|
@@ -336,8 +350,14 @@ async function switchMode(newMode) {
|
|
|
336
350
|
}
|
|
337
351
|
currentProcess = null;
|
|
338
352
|
|
|
339
|
-
//
|
|
340
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
353
|
+
// 等待旧进程完全退出(Bun crash dump 可能延迟输出)
|
|
354
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
355
|
+
|
|
356
|
+
// 清理可能的孤儿进程(Bun segfault 导致子进程未被回收)
|
|
357
|
+
cleanupOrphanProcesses();
|
|
358
|
+
|
|
359
|
+
// 清空旧进程残留输出(包括 Bun crash dump)
|
|
360
|
+
outputBuffer = '';
|
|
341
361
|
|
|
342
362
|
// 切换到新模式
|
|
343
363
|
currentMode = newMode;
|
|
@@ -673,22 +693,32 @@ const requestHandler = async (req, res) => {
|
|
|
673
693
|
return;
|
|
674
694
|
}
|
|
675
695
|
|
|
676
|
-
// API:
|
|
696
|
+
// API: 获取文档列表(扫描 /halo 或 cwd 下的 md 文件,最多两层目录)
|
|
677
697
|
if (req.url === '/api/docs') {
|
|
678
698
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
679
699
|
try {
|
|
680
|
-
const
|
|
700
|
+
const docsRoot = existsSync('/halo') ? '/halo' : process.cwd();
|
|
681
701
|
const EXCLUDE = new Set(['readme.md', 'changelog.md', 'license.md', 'claude.md', 'agents.md', 'contributing.md', 'security.md', 'context.md']);
|
|
682
|
-
const files =
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
702
|
+
const files = [];
|
|
703
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.opencode', '.claude', '.idea', '.vscode']);
|
|
704
|
+
const scanDir = (dir, prefix, depth) => {
|
|
705
|
+
if (depth > 5) return; // 最多 5 层,避免过深
|
|
706
|
+
try {
|
|
707
|
+
for (const f of readdirSync(dir, { withFileTypes: true })) {
|
|
708
|
+
if (f.isFile() && f.name.endsWith('.md') && !EXCLUDE.has(f.name.toLowerCase())) {
|
|
709
|
+
try {
|
|
710
|
+
const fullPath = join(dir, f.name);
|
|
711
|
+
const st = statSync(fullPath);
|
|
712
|
+
files.push({ name: join(dir, f.name), mtime: st.mtimeMs });
|
|
713
|
+
} catch {}
|
|
714
|
+
} else if (f.isDirectory() && !f.name.startsWith('.') && !SKIP_DIRS.has(f.name)) {
|
|
715
|
+
scanDir(join(dir, f.name), prefix ? prefix + '/' + f.name : f.name, depth + 1);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} catch {}
|
|
719
|
+
};
|
|
720
|
+
scanDir(docsRoot, '', 0);
|
|
721
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
692
722
|
res.end(JSON.stringify({ docs: files }));
|
|
693
723
|
} catch (err) {
|
|
694
724
|
res.end(JSON.stringify({ docs: [], error: err.message }));
|
|
@@ -701,13 +731,12 @@ const requestHandler = async (req, res) => {
|
|
|
701
731
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
702
732
|
const file = url.searchParams.get('file');
|
|
703
733
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
704
|
-
if (!file || file.includes('
|
|
734
|
+
if (!file || file.includes('..') || !file.endsWith('.md')) {
|
|
705
735
|
res.end(JSON.stringify({ error: '无效文件名' }));
|
|
706
736
|
return;
|
|
707
737
|
}
|
|
708
738
|
try {
|
|
709
|
-
const
|
|
710
|
-
const filePath = join(docsCwd, file);
|
|
739
|
+
const filePath = file;
|
|
711
740
|
if (!existsSync(filePath)) {
|
|
712
741
|
res.end(JSON.stringify({ error: '文件不存在' }));
|
|
713
742
|
return;
|
|
@@ -933,7 +962,8 @@ wssInst.on('connection', (ws, req) => {
|
|
|
933
962
|
outputBuffer = '';
|
|
934
963
|
|
|
935
964
|
// 等待进程完全退出
|
|
936
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
965
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
966
|
+
cleanupOrphanProcesses();
|
|
937
967
|
|
|
938
968
|
// 启动新的 opencode 进程,传入 session ID
|
|
939
969
|
try {
|
|
@@ -978,7 +1008,8 @@ wssInst.on('connection', (ws, req) => {
|
|
|
978
1008
|
}
|
|
979
1009
|
|
|
980
1010
|
outputBuffer = '';
|
|
981
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1011
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1012
|
+
cleanupOrphanProcesses();
|
|
982
1013
|
|
|
983
1014
|
// 先通知前端准备好,再启动新进程
|
|
984
1015
|
ws.send(JSON.stringify({ type: 'new-session-ok', mode }));
|
|
@@ -1089,17 +1120,8 @@ function startServer() {
|
|
|
1089
1120
|
LOG('\n按 Ctrl+C 停止服务\n');
|
|
1090
1121
|
}
|
|
1091
1122
|
|
|
1092
|
-
// 清理上次 cov
|
|
1093
|
-
|
|
1094
|
-
const orphans = execSync(
|
|
1095
|
-
"ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && (/opencode/||/claude/) {print $1}'",
|
|
1096
|
-
{ encoding: 'utf-8', timeout: 5000 }
|
|
1097
|
-
).trim().split(/\s+/).filter(Boolean).map(Number);
|
|
1098
|
-
for (const pid of orphans) {
|
|
1099
|
-
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1100
|
-
}
|
|
1101
|
-
if (orphans.length > 0) LOG(`[startup] 清理孤儿进程: ${orphans.join(', ')}`);
|
|
1102
|
-
} catch {}
|
|
1123
|
+
// 清理上次 cov 崩溃残留的孤儿进程
|
|
1124
|
+
cleanupOrphanProcesses();
|
|
1103
1125
|
|
|
1104
1126
|
// 尝试恢复最近的会话,如果没有则新建
|
|
1105
1127
|
let lastSessionId = null;
|