claude-opencode-viewer 2.6.47 → 2.6.49
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/.agents/skills/code-review-expert/README.md +74 -0
- package/.agents/skills/code-review-expert/SKILL.md +156 -0
- package/.agents/skills/code-review-expert/agents/agent.yaml +7 -0
- package/.agents/skills/code-review-expert/references/code-quality-checklist.md +130 -0
- package/.agents/skills/code-review-expert/references/removal-plan.md +52 -0
- package/.agents/skills/code-review-expert/references/security-checklist.md +118 -0
- package/.agents/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/index-pc.html +135 -45
- package/index.html +88 -39
- package/package.json +1 -1
- package/server.js +98 -7
- package/skills-lock.json +10 -0
- package/test-doc.md +86 -0
package/server.js
CHANGED
|
@@ -201,6 +201,7 @@ function killProcessTree(proc) {
|
|
|
201
201
|
|
|
202
202
|
// 清理孤儿进程(PPID=1 的 opencode/claude)
|
|
203
203
|
function cleanupOrphanProcesses() {
|
|
204
|
+
const tc = Date.now();
|
|
204
205
|
try {
|
|
205
206
|
const myPid = process.pid;
|
|
206
207
|
const orphans = execSync(
|
|
@@ -212,15 +213,23 @@ function cleanupOrphanProcesses() {
|
|
|
212
213
|
}
|
|
213
214
|
if (orphans.length > 0) LOG(`[cleanup] 清理孤儿进程: ${orphans.join(', ')}`);
|
|
214
215
|
} catch {}
|
|
216
|
+
console.log(`[perf] cleanupOrphanProcesses: ${Date.now() - tc}ms`);
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
async function spawnProcess(mode, sessionId = null) {
|
|
220
|
+
const t0 = Date.now();
|
|
218
221
|
const pty = await getPty();
|
|
222
|
+
console.log(`[perf] getPty: ${Date.now() - t0}ms`);
|
|
223
|
+
|
|
224
|
+
const t1 = Date.now();
|
|
219
225
|
fixSpawnHelperPermissions();
|
|
226
|
+
console.log(`[perf] fixSpawnHelperPermissions: ${Date.now() - t1}ms`);
|
|
220
227
|
|
|
221
228
|
let command, args = [];
|
|
222
229
|
if (mode === 'claude') {
|
|
230
|
+
const t2 = Date.now();
|
|
223
231
|
const claudePath = findCommand('claude');
|
|
232
|
+
console.log(`[perf] findCommand(claude): ${Date.now() - t2}ms`);
|
|
224
233
|
if (claudePath.endsWith('.js')) {
|
|
225
234
|
command = process.execPath;
|
|
226
235
|
args = [claudePath];
|
|
@@ -233,7 +242,9 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
233
242
|
LOG(`[claude] 恢复会话: ${sessionId}`);
|
|
234
243
|
}
|
|
235
244
|
} else {
|
|
245
|
+
const t2 = Date.now();
|
|
236
246
|
command = findCommand('opencode');
|
|
247
|
+
console.log(`[perf] findCommand(opencode): ${Date.now() - t2}ms`);
|
|
237
248
|
// 如果提供了 sessionId,添加 --session 参数
|
|
238
249
|
if (sessionId) {
|
|
239
250
|
args = ['--session', sessionId];
|
|
@@ -244,6 +255,7 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
244
255
|
const spawnEnv = { ...process.env };
|
|
245
256
|
|
|
246
257
|
// 恢复会话时,使用会话记录的工作目录
|
|
258
|
+
const t3 = Date.now();
|
|
247
259
|
let spawnCwd = process.cwd();
|
|
248
260
|
if (sessionId && mode === 'opencode') {
|
|
249
261
|
try {
|
|
@@ -286,6 +298,9 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
286
298
|
}
|
|
287
299
|
}
|
|
288
300
|
|
|
301
|
+
console.log(`[perf] cwdLookup: ${Date.now() - t3}ms`);
|
|
302
|
+
|
|
303
|
+
const t4 = Date.now();
|
|
289
304
|
const proc = pty.spawn(command, args, {
|
|
290
305
|
name: 'xterm-256color',
|
|
291
306
|
cols: lastPtyCols,
|
|
@@ -294,6 +309,9 @@ async function spawnProcess(mode, sessionId = null) {
|
|
|
294
309
|
env: spawnEnv,
|
|
295
310
|
});
|
|
296
311
|
|
|
312
|
+
console.log(`[perf] pty.spawn: ${Date.now() - t4}ms`);
|
|
313
|
+
console.log(`[perf] spawnProcess total: ${Date.now() - t0}ms`);
|
|
314
|
+
|
|
297
315
|
proc.onData((data) => {
|
|
298
316
|
// 忽略已被替换的旧进程输出
|
|
299
317
|
if (currentProcess !== proc) return;
|
|
@@ -637,7 +655,7 @@ const requestHandler = async (req, res) => {
|
|
|
637
655
|
return;
|
|
638
656
|
}
|
|
639
657
|
|
|
640
|
-
// API: 获取 git status
|
|
658
|
+
// API: 获取 git status(含每个文件的 unified_diff,批量获取优化)
|
|
641
659
|
if (req.url === '/api/git-status') {
|
|
642
660
|
res.writeHead(200, {
|
|
643
661
|
'Content-Type': 'application/json',
|
|
@@ -645,13 +663,76 @@ const requestHandler = async (req, res) => {
|
|
|
645
663
|
});
|
|
646
664
|
try {
|
|
647
665
|
const gitCwd = process.env.PROJECT_DIR || process.cwd();
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
666
|
+
|
|
667
|
+
// 并行执行: git status + git diff --numstat + git diff (批量获取)
|
|
668
|
+
const [statusResult, numstatResult, diffResult] = await Promise.all([
|
|
669
|
+
execFileAsync('git', ['-c', 'safe.directory=*', 'status', '--porcelain'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }),
|
|
670
|
+
execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--numstat', 'HEAD'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000 }).catch(() => ({ stdout: '' })),
|
|
671
|
+
execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', 'HEAD'], { cwd: gitCwd, encoding: 'utf-8', timeout: 60000, maxBuffer: 10 * 1024 * 1024 }).catch(e => ({ stdout: e.stdout || '' })),
|
|
672
|
+
]);
|
|
673
|
+
|
|
674
|
+
const changes = statusResult.stdout.split('\n').filter(Boolean).map(line => ({
|
|
652
675
|
status: line.substring(0, 2).trim(),
|
|
653
676
|
file: line.substring(3),
|
|
654
677
|
})).filter(c => !/^core-/.test(c.file));
|
|
678
|
+
|
|
679
|
+
// 解析 numstat 识别二进制文件
|
|
680
|
+
const binaryFiles = new Set();
|
|
681
|
+
numstatResult.stdout.split('\n').filter(Boolean).forEach(line => {
|
|
682
|
+
if (line.startsWith('-\t-\t')) binaryFiles.add(line.split('\t')[2]);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// 将批量 diff 输出按文件拆分
|
|
686
|
+
const diffMap = {};
|
|
687
|
+
const diffParts = diffResult.stdout.split(/^diff --git /m);
|
|
688
|
+
for (let i = 1; i < diffParts.length; i++) {
|
|
689
|
+
const part = diffParts[i];
|
|
690
|
+
// 提取文件名: "a/path b/path\n..."
|
|
691
|
+
const firstLine = part.substring(0, part.indexOf('\n'));
|
|
692
|
+
const bMatch = firstLine.match(/ b\/(.+)$/);
|
|
693
|
+
if (bMatch) diffMap[bMatch[1]] = 'diff --git ' + part;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 填充每个文件的 diff 信息
|
|
697
|
+
const untrackedFiles = [];
|
|
698
|
+
for (const c of changes) {
|
|
699
|
+
if (c.file.includes('..') || c.file.startsWith('/')) continue;
|
|
700
|
+
c.is_new = c.status === 'A' || c.status === '??';
|
|
701
|
+
c.is_deleted = c.status === 'D';
|
|
702
|
+
c.is_binary = binaryFiles.has(c.file);
|
|
703
|
+
if (c.is_binary) continue;
|
|
704
|
+
|
|
705
|
+
// 检查大文件
|
|
706
|
+
if (!c.is_deleted) {
|
|
707
|
+
try {
|
|
708
|
+
const filePath = join(gitCwd, c.file);
|
|
709
|
+
if (existsSync(filePath)) {
|
|
710
|
+
const stat = statSync(filePath);
|
|
711
|
+
if (stat.size > 5 * 1024 * 1024) { c.is_large = true; c.size = stat.size; continue; }
|
|
712
|
+
}
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (c.status === '??') {
|
|
717
|
+
// untracked 文件需要单独处理
|
|
718
|
+
untrackedFiles.push(c);
|
|
719
|
+
} else {
|
|
720
|
+
c.unified_diff = diffMap[c.file] || '';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// 对 untracked 文件并行获取 diff
|
|
725
|
+
if (untrackedFiles.length > 0) {
|
|
726
|
+
await Promise.all(untrackedFiles.map(async (c) => {
|
|
727
|
+
try {
|
|
728
|
+
const { stdout: diffOut } = await execFileAsync('git', ['-c', 'safe.directory=*', 'diff', '--no-color', '-U3', '--no-index', '/dev/null', c.file], { cwd: gitCwd, encoding: 'utf-8', timeout: 30000, maxBuffer: 5 * 1024 * 1024 });
|
|
729
|
+
c.unified_diff = diffOut;
|
|
730
|
+
} catch (e) {
|
|
731
|
+
c.unified_diff = e.stdout || '';
|
|
732
|
+
}
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
|
|
655
736
|
res.end(JSON.stringify({ changes, cwd: gitCwd }));
|
|
656
737
|
} catch (err) {
|
|
657
738
|
res.end(JSON.stringify({ changes: [], cwd: process.env.PROJECT_DIR || process.cwd(), error: err.message }));
|
|
@@ -786,6 +867,7 @@ const requestHandler = async (req, res) => {
|
|
|
786
867
|
|
|
787
868
|
// API: 获取最近的 OpenCode 和 Claude 会话(用于启动对话框)
|
|
788
869
|
if (req.url === '/api/last-sessions') {
|
|
870
|
+
const tls = Date.now();
|
|
789
871
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
790
872
|
let opencode = null, claude = null;
|
|
791
873
|
// OpenCode: 从 SQLite 查最近会话
|
|
@@ -861,6 +943,7 @@ const requestHandler = async (req, res) => {
|
|
|
861
943
|
}
|
|
862
944
|
}
|
|
863
945
|
} catch {}
|
|
946
|
+
console.log(`[perf] last-sessions: ${Date.now() - tls}ms`);
|
|
864
947
|
res.end(JSON.stringify({ opencode, claude }));
|
|
865
948
|
return;
|
|
866
949
|
}
|
|
@@ -1099,8 +1182,16 @@ wssInst.on('connection', (ws, req) => {
|
|
|
1099
1182
|
}
|
|
1100
1183
|
isSwitching = false;
|
|
1101
1184
|
}, 200);
|
|
1102
|
-
} else if (
|
|
1103
|
-
|
|
1185
|
+
} else if (currentProcess) {
|
|
1186
|
+
// TUI 程序使用 alternate screen buffer,直接回放 raw buffer 容易转义序列错乱。
|
|
1187
|
+
// 改为发送 resize 信号让 TUI 重绘当前画面。
|
|
1188
|
+
// 先改为不同尺寸再改回,强制触发 SIGWINCH(相同尺寸不触发)。
|
|
1189
|
+
try {
|
|
1190
|
+
currentProcess.resize(Math.max(2, lastPtyCols - 1), lastPtyRows);
|
|
1191
|
+
setTimeout(() => {
|
|
1192
|
+
try { currentProcess.resize(lastPtyCols, lastPtyRows); } catch {}
|
|
1193
|
+
}, 50);
|
|
1194
|
+
} catch {}
|
|
1104
1195
|
}
|
|
1105
1196
|
|
|
1106
1197
|
|
package/skills-lock.json
ADDED
package/test-doc.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Claude OpenCode Viewer 使用指南
|
|
2
|
+
|
|
3
|
+
## 简介
|
|
4
|
+
|
|
5
|
+
Claude OpenCode Viewer(COV)是一个统一的终端查看器,支持在浏览器中远程查看和操作 Claude Code 与 OpenCode 的终端会话。适用于需要在手机或其他设备上查看 AI 编程助手工作进度的场景。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g claude-opencode-viewer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
安装完成后,可以通过以下命令启动:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cov
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
默认监听端口 7008,PC 模式使用 `--pc` 参数启动。
|
|
20
|
+
|
|
21
|
+
## 功能特性
|
|
22
|
+
|
|
23
|
+
### 终端查看
|
|
24
|
+
|
|
25
|
+
支持实时查看终端输出,基于 xterm.js 实现完整的终端模拟,包括:
|
|
26
|
+
|
|
27
|
+
- 颜色渲染
|
|
28
|
+
- Unicode 字符支持
|
|
29
|
+
- WebGL 加速渲染
|
|
30
|
+
- 移动端触摸滚动
|
|
31
|
+
|
|
32
|
+
### 会话管理
|
|
33
|
+
|
|
34
|
+
可以管理多个会话,支持以下操作:
|
|
35
|
+
|
|
36
|
+
1. 查看历史会话列表
|
|
37
|
+
2. 恢复已有会话
|
|
38
|
+
3. 创建新会话
|
|
39
|
+
4. 在 Claude 和 OpenCode 之间切换
|
|
40
|
+
|
|
41
|
+
### Git 变更查看
|
|
42
|
+
|
|
43
|
+
集成了 Git 状态查看功能,可以直接在页面上查看:
|
|
44
|
+
|
|
45
|
+
| 状态 | 含义 | 颜色 |
|
|
46
|
+
|------|------|------|
|
|
47
|
+
| M | 已修改 | 橙色 |
|
|
48
|
+
| A | 新增 | 绿色 |
|
|
49
|
+
| D | 已删除 | 红色 |
|
|
50
|
+
| ?? | 未跟踪 | 灰色 |
|
|
51
|
+
|
|
52
|
+
### 文档浏览
|
|
53
|
+
|
|
54
|
+
支持浏览项目中的 Markdown 文档,自动扫描项目目录下的 `.md` 文件并以富文本格式展示。
|
|
55
|
+
|
|
56
|
+
## 配置说明
|
|
57
|
+
|
|
58
|
+
> 注意:以下配置需要在项目根目录下操作,确保 `PROJECT_DIR` 环境变量指向正确的项目路径。
|
|
59
|
+
|
|
60
|
+
常用环境变量:
|
|
61
|
+
|
|
62
|
+
- `PROJECT_DIR` — 指定项目工作目录
|
|
63
|
+
- `PORT` — 自定义端口号
|
|
64
|
+
- `COV_MODE` — 默认启动模式(claude / opencode)
|
|
65
|
+
|
|
66
|
+
## 常见问题
|
|
67
|
+
|
|
68
|
+
### 连接断开怎么办?
|
|
69
|
+
|
|
70
|
+
页面会自动显示重连提示并尝试重新连接。如果持续无法连接,请检查:
|
|
71
|
+
|
|
72
|
+
1. 服务进程是否仍在运行
|
|
73
|
+
2. 网络是否可达
|
|
74
|
+
3. 端口是否被占用
|
|
75
|
+
|
|
76
|
+
### 移动端键盘遮挡问题
|
|
77
|
+
|
|
78
|
+
在 iOS 设备上,系统会自动调整终端高度以适应键盘弹出。如果遇到显示异常,可以尝试旋转屏幕后再旋转回来。
|
|
79
|
+
|
|
80
|
+
## 更新日志
|
|
81
|
+
|
|
82
|
+
**v2.6.48** — 修复重连内容重复、模式切换黑屏问题
|
|
83
|
+
|
|
84
|
+
**v2.6.47** — 添加 PC 端重连覆盖层
|
|
85
|
+
|
|
86
|
+
**v2.6.46** — 移动端键盘交互优化
|