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 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
- if (!isBufferReplay) showCopyToast();
1042
+ showCopyToast();
1043
+ }).catch(function() {
1044
+ showCopyPopup(text);
981
1045
  });
982
1046
  } else {
983
- var ta = document.createElement('textarea');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.5",
3
+ "version": "2.6.7",
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
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { createServer } from 'node:http';
3
- import { existsSync, createReadStream, readFileSync } from 'node:fs';
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 server = createServer(async (req, res) => {
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(`🖥️ 本地访问:http://127.0.0.1:${PORT}`);
747
- console.log(`📱 手机访问:http://${ip}:${PORT}`);
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