claude-opencode-viewer 2.6.5 → 2.6.7
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/bin/cov.js +2 -0
- package/index-pc.html +75 -9
- package/package.json +1 -1
- package/server.js +130 -5
package/bin/cov.js
CHANGED
|
@@ -42,6 +42,7 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
42
42
|
║ 用法: ║
|
|
43
43
|
║ cov 在当前目录启动服务(端口7008) ║
|
|
44
44
|
║ cov --pc PC端随机端口启动(19200-19220) ║
|
|
45
|
+
║ cov --pc --https PC端HTTPS模式启动 ║
|
|
45
46
|
║ cov --port <端口号> 指定端口启动 ║
|
|
46
47
|
║ cov [路径] 在指定目录启动服务 ║
|
|
47
48
|
║ cov --help, -h 显示此帮助信息 ║
|
|
@@ -85,6 +86,7 @@ for (const arg of args) {
|
|
|
85
86
|
const port = getPort();
|
|
86
87
|
const serverArgs = [SERVER_PATH, String(port)];
|
|
87
88
|
if (args.includes('--pc')) serverArgs.push('--pc');
|
|
89
|
+
if (args.includes('--https')) serverArgs.push('--https');
|
|
88
90
|
const server = spawn(process.execPath, serverArgs, {
|
|
89
91
|
stdio: 'inherit',
|
|
90
92
|
cwd: process.cwd(),
|
package/index-pc.html
CHANGED
|
@@ -559,6 +559,59 @@
|
|
|
559
559
|
display: block;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
#copy-popup {
|
|
563
|
+
display: none;
|
|
564
|
+
position: fixed;
|
|
565
|
+
top: 50%;
|
|
566
|
+
left: 50%;
|
|
567
|
+
transform: translate(-50%, -50%);
|
|
568
|
+
background: #1a1a1a;
|
|
569
|
+
border: 1px solid #444;
|
|
570
|
+
border-radius: 8px;
|
|
571
|
+
padding: 16px;
|
|
572
|
+
z-index: 10000;
|
|
573
|
+
max-width: 80vw;
|
|
574
|
+
max-height: 60vh;
|
|
575
|
+
}
|
|
576
|
+
#copy-popup.show {
|
|
577
|
+
display: flex;
|
|
578
|
+
flex-direction: column;
|
|
579
|
+
gap: 10px;
|
|
580
|
+
}
|
|
581
|
+
#copy-popup textarea {
|
|
582
|
+
width: 500px;
|
|
583
|
+
max-width: 70vw;
|
|
584
|
+
height: 120px;
|
|
585
|
+
background: #0a0a0a;
|
|
586
|
+
color: #ccc;
|
|
587
|
+
border: 1px solid #333;
|
|
588
|
+
border-radius: 4px;
|
|
589
|
+
padding: 8px;
|
|
590
|
+
font-family: Menlo, Monaco, monospace;
|
|
591
|
+
font-size: 12px;
|
|
592
|
+
resize: vertical;
|
|
593
|
+
}
|
|
594
|
+
#copy-popup .popup-actions {
|
|
595
|
+
display: flex;
|
|
596
|
+
justify-content: flex-end;
|
|
597
|
+
gap: 8px;
|
|
598
|
+
}
|
|
599
|
+
#copy-popup button {
|
|
600
|
+
padding: 6px 16px;
|
|
601
|
+
border: none;
|
|
602
|
+
border-radius: 4px;
|
|
603
|
+
cursor: pointer;
|
|
604
|
+
font-size: 13px;
|
|
605
|
+
}
|
|
606
|
+
#copy-popup .btn-copy {
|
|
607
|
+
background: #2563eb;
|
|
608
|
+
color: #fff;
|
|
609
|
+
}
|
|
610
|
+
#copy-popup .btn-close {
|
|
611
|
+
background: #333;
|
|
612
|
+
color: #ccc;
|
|
613
|
+
}
|
|
614
|
+
|
|
562
615
|
/* Git Diff 面板 */
|
|
563
616
|
#git-diff-bar {
|
|
564
617
|
display: none;
|
|
@@ -929,6 +982,14 @@
|
|
|
929
982
|
</div>
|
|
930
983
|
|
|
931
984
|
<div id="copy-toast">已复制</div>
|
|
985
|
+
<div id="copy-popup">
|
|
986
|
+
<div style="color:#ccc;font-size:13px;">剪贴板写入需要 HTTPS,请手动复制:</div>
|
|
987
|
+
<textarea id="copy-popup-text" readonly></textarea>
|
|
988
|
+
<div class="popup-actions">
|
|
989
|
+
<button class="btn-copy" onclick="document.getElementById('copy-popup-text').select();document.execCommand('copy');document.getElementById('copy-popup').classList.remove('show');">选中并复制</button>
|
|
990
|
+
<button class="btn-close" onclick="document.getElementById('copy-popup').classList.remove('show');">关闭</button>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
932
993
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
933
994
|
<script>
|
|
934
995
|
(function() {
|
|
@@ -973,21 +1034,17 @@
|
|
|
973
1034
|
if (idx === -1) return false;
|
|
974
1035
|
var b64 = data.substring(idx + 1);
|
|
975
1036
|
if (!b64 || b64 === '?') return false;
|
|
1037
|
+
if (isBufferReplay) return true;
|
|
976
1038
|
try {
|
|
977
1039
|
var text = atob(b64);
|
|
978
1040
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
979
1041
|
navigator.clipboard.writeText(text).then(function() {
|
|
980
|
-
|
|
1042
|
+
showCopyToast();
|
|
1043
|
+
}).catch(function() {
|
|
1044
|
+
showCopyPopup(text);
|
|
981
1045
|
});
|
|
982
1046
|
} else {
|
|
983
|
-
|
|
984
|
-
ta.value = text;
|
|
985
|
-
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
986
|
-
document.body.appendChild(ta);
|
|
987
|
-
ta.select();
|
|
988
|
-
document.execCommand('copy');
|
|
989
|
-
document.body.removeChild(ta);
|
|
990
|
-
if (!isBufferReplay) showCopyToast();
|
|
1047
|
+
showCopyPopup(text);
|
|
991
1048
|
}
|
|
992
1049
|
} catch (e) {}
|
|
993
1050
|
return true;
|
|
@@ -1905,6 +1962,15 @@
|
|
|
1905
1962
|
showCopyToast();
|
|
1906
1963
|
}
|
|
1907
1964
|
|
|
1965
|
+
function showCopyPopup(text) {
|
|
1966
|
+
var popup = document.getElementById('copy-popup');
|
|
1967
|
+
var ta = document.getElementById('copy-popup-text');
|
|
1968
|
+
ta.value = text;
|
|
1969
|
+
popup.classList.add('show');
|
|
1970
|
+
ta.focus();
|
|
1971
|
+
ta.select();
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1908
1974
|
function showCopyToast() {
|
|
1909
1975
|
var toast = document.getElementById('copy-toast');
|
|
1910
1976
|
toast.classList.add('show');
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
|
-
import {
|
|
3
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
4
|
+
import { existsSync, createReadStream, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
5
|
import { join, dirname } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { networkInterfaces, platform, arch, homedir } from 'node:os';
|
|
7
8
|
import { chmodSync, statSync } from 'node:fs';
|
|
8
9
|
import { execSync, execFile } from 'child_process';
|
|
9
10
|
import { promisify } from 'node:util';
|
|
11
|
+
import { generateKeyPairSync, createSign, randomBytes } from 'node:crypto';
|
|
10
12
|
import { WebSocketServer } from 'ws';
|
|
11
13
|
import Database from 'better-sqlite3';
|
|
12
14
|
|
|
@@ -16,6 +18,123 @@ process.title = 'claude-opencode-viewer';
|
|
|
16
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
19
|
const PORT = parseInt(process.argv[2]) || 7008;
|
|
18
20
|
const IS_PC = process.argv.includes('--pc');
|
|
21
|
+
const USE_HTTPS = process.argv.includes('--https');
|
|
22
|
+
|
|
23
|
+
// 自签名证书生成(纯 Node.js,不依赖 openssl)
|
|
24
|
+
function getOrCreateCert() {
|
|
25
|
+
const certDir = join(homedir(), '.cov-certs');
|
|
26
|
+
const keyPath = join(certDir, 'key.pem');
|
|
27
|
+
const certPath = join(certDir, 'cert.pem');
|
|
28
|
+
|
|
29
|
+
if (existsSync(keyPath) && existsSync(certPath)) {
|
|
30
|
+
return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('🔐 首次使用 HTTPS,生成自签名证书...');
|
|
34
|
+
mkdirSync(certDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
|
37
|
+
modulusLength: 2048,
|
|
38
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
39
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 构建自签名 X.509 v3 证书(DER 编码)
|
|
43
|
+
const serial = randomBytes(8);
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const expire = new Date(now.getTime() + 365 * 24 * 3600 * 1000);
|
|
46
|
+
|
|
47
|
+
function encodeLength(len) {
|
|
48
|
+
if (len < 128) return Buffer.from([len]);
|
|
49
|
+
if (len < 256) return Buffer.from([0x81, len]);
|
|
50
|
+
return Buffer.from([0x82, (len >> 8) & 0xff, len & 0xff]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function derSequence(buffers) {
|
|
54
|
+
const body = Buffer.concat(buffers);
|
|
55
|
+
return Buffer.concat([Buffer.from([0x30]), encodeLength(body.length), body]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function derSet(buffers) {
|
|
59
|
+
const body = Buffer.concat(buffers);
|
|
60
|
+
return Buffer.concat([Buffer.from([0x31]), encodeLength(body.length), body]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function derOid(oid) {
|
|
64
|
+
const parts = oid.split('.').map(Number);
|
|
65
|
+
const bytes = [40 * parts[0] + parts[1]];
|
|
66
|
+
for (let i = 2; i < parts.length; i++) {
|
|
67
|
+
let v = parts[i];
|
|
68
|
+
if (v < 128) { bytes.push(v); }
|
|
69
|
+
else {
|
|
70
|
+
const enc = [];
|
|
71
|
+
enc.push(v & 0x7f); v >>= 7;
|
|
72
|
+
while (v > 0) { enc.push(0x80 | (v & 0x7f)); v >>= 7; }
|
|
73
|
+
bytes.push(...enc.reverse());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const buf = Buffer.from(bytes);
|
|
77
|
+
return Buffer.concat([Buffer.from([0x06]), encodeLength(buf.length), buf]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function derUtf8(str) {
|
|
81
|
+
const buf = Buffer.from(str, 'utf8');
|
|
82
|
+
return Buffer.concat([Buffer.from([0x0c]), encodeLength(buf.length), buf]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function derInt(buf) {
|
|
86
|
+
if (buf[0] & 0x80) buf = Buffer.concat([Buffer.from([0x00]), buf]);
|
|
87
|
+
return Buffer.concat([Buffer.from([0x02]), encodeLength(buf.length), buf]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function derBitString(buf) {
|
|
91
|
+
const body = Buffer.concat([Buffer.from([0x00]), buf]);
|
|
92
|
+
return Buffer.concat([Buffer.from([0x03]), encodeLength(body.length), body]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function derTime(date) {
|
|
96
|
+
const s = date.toISOString().replace(/[-:T]/g, '').slice(0, 14) + 'Z';
|
|
97
|
+
const buf = Buffer.from(s, 'ascii');
|
|
98
|
+
return Buffer.concat([Buffer.from([0x17]), encodeLength(buf.length), buf]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function derExplicit(tag, content) {
|
|
102
|
+
return Buffer.concat([Buffer.from([0xa0 | tag]), encodeLength(content.length), content]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const issuer = derSequence([derSet([derSequence([derOid('2.5.4.3'), derUtf8('cov-self-signed')])])]);
|
|
106
|
+
const validity = derSequence([derTime(now), derTime(expire)]);
|
|
107
|
+
const spki = Buffer.from(publicKey);
|
|
108
|
+
|
|
109
|
+
const tbs = derSequence([
|
|
110
|
+
derExplicit(0, derInt(Buffer.from([0x02]))), // version v3
|
|
111
|
+
derInt(serial),
|
|
112
|
+
derSequence([derOid('1.2.840.113549.1.1.11'), Buffer.from([0x05, 0x00])]), // sha256WithRSA
|
|
113
|
+
issuer,
|
|
114
|
+
validity,
|
|
115
|
+
issuer, // subject = issuer (self-signed)
|
|
116
|
+
spki,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const sign = createSign('SHA256');
|
|
120
|
+
sign.update(tbs);
|
|
121
|
+
const signature = sign.sign(privateKey);
|
|
122
|
+
|
|
123
|
+
const cert = derSequence([
|
|
124
|
+
tbs,
|
|
125
|
+
derSequence([derOid('1.2.840.113549.1.1.11'), Buffer.from([0x05, 0x00])]),
|
|
126
|
+
derBitString(signature),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const certPem = '-----BEGIN CERTIFICATE-----\n' +
|
|
130
|
+
cert.toString('base64').match(/.{1,64}/g).join('\n') +
|
|
131
|
+
'\n-----END CERTIFICATE-----\n';
|
|
132
|
+
|
|
133
|
+
writeFileSync(keyPath, privateKey);
|
|
134
|
+
writeFileSync(certPath, certPem);
|
|
135
|
+
console.log(`📁 证书已保存到 ${certDir}`);
|
|
136
|
+
return { key: privateKey, cert: certPem };
|
|
137
|
+
}
|
|
19
138
|
|
|
20
139
|
// OpenCode 数据库路径(支持环境变量覆盖,自动检测 /halo 环境)
|
|
21
140
|
const OPENCODE_DB_PATH = process.env.OPENCODE_DB_PATH || join(
|
|
@@ -407,7 +526,7 @@ function getSessionMessages(sessionId) {
|
|
|
407
526
|
}
|
|
408
527
|
}
|
|
409
528
|
|
|
410
|
-
const
|
|
529
|
+
const requestHandler = async (req, res) => {
|
|
411
530
|
if (req.url === '/' || req.url === '/index.html') {
|
|
412
531
|
res.writeHead(200, {
|
|
413
532
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -557,7 +676,11 @@ const server = createServer(async (req, res) => {
|
|
|
557
676
|
|
|
558
677
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
559
678
|
res.end('Not Found');
|
|
560
|
-
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const server = USE_HTTPS
|
|
682
|
+
? createHttpsServer(getOrCreateCert(), requestHandler)
|
|
683
|
+
: createServer(requestHandler);
|
|
561
684
|
|
|
562
685
|
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
563
686
|
|
|
@@ -740,11 +863,13 @@ wss.on('connection', (ws, req) => {
|
|
|
740
863
|
|
|
741
864
|
server.listen(PORT, '0.0.0.0', async () => {
|
|
742
865
|
const ip = getLocalIp();
|
|
866
|
+
const proto = USE_HTTPS ? 'https' : 'http';
|
|
743
867
|
console.log('\n' + '='.repeat(50));
|
|
744
868
|
console.log('✅ Claude OpenCode Viewer 已启动');
|
|
745
869
|
console.log('='.repeat(50));
|
|
746
|
-
console.log(`🖥️
|
|
747
|
-
console.log(`📱
|
|
870
|
+
console.log(`🖥️ 本地访问:${proto}://127.0.0.1:${PORT}`);
|
|
871
|
+
console.log(`📱 手机访问:${proto}://${ip}:${PORT}`);
|
|
872
|
+
if (USE_HTTPS) console.log('🔐 HTTPS 模式(首次访问需信任自签名证书)');
|
|
748
873
|
console.log('='.repeat(50));
|
|
749
874
|
console.log('\n按 Ctrl+C 停止服务\n');
|
|
750
875
|
|