Skip to content

Commit c83ca74

Browse files
authored
Merge pull request #33 from zhaozhiqiangjianming-ship-it/https
支持 httpsOptions 的插件 hooks
2 parents 97f45bc + b0d8d3b commit c83ca74

10 files changed

Lines changed: 239 additions & 57 deletions

File tree

cli.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ async function runCliMode(extraClaudeArgs = [], cwd) {
304304
}
305305

306306
// 4. 自动打开浏览器
307-
const url = `http://127.0.0.1:${port}`;
307+
const protocol = serverMod.getProtocol();
308+
const url = `${protocol}://127.0.0.1:${port}`;
308309
try {
309310
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
310311
const { execSync } = await import('node:child_process');
@@ -359,7 +360,8 @@ async function runCliModeWorkspaceSelector(extraClaudeArgs = []) {
359360
serverMod.setWorkspaceClaudePath(claudePath, isNpmVersion);
360361

361362
// 自动打开浏览器
362-
const url = `http://127.0.0.1:${port}`;
363+
const wsProtocol = serverMod.getProtocol();
364+
const url = `${wsProtocol}://127.0.0.1:${port}`;
363365
try {
364366
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
365367
const { execSync } = await import('node:child_process');

docs/plugins.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,40 @@ export default {
6161

6262
## Available Hooks
6363

64+
### `httpsOptions` — Waterfall
65+
66+
Triggered at server startup to obtain HTTPS certificate options. If the returned object contains `pfx` or `cert`, the server starts in HTTPS mode; otherwise it falls back to HTTP.
67+
68+
| Property | Description |
69+
|----------|-------------|
70+
| **Type** | Waterfall (serial pipeline) |
71+
| **Parameters** | `{}` (empty object) |
72+
| **Returns** | `{ pfx, passphrase }` or `{ cert, key }` — TLS options passed to `https.createServer()` |
73+
| **Timing** | Before the HTTP/HTTPS server is created |
74+
75+
```javascript
76+
hooks: {
77+
async httpsOptions() {
78+
// Example: load PFX certificate from an internal package
79+
const { getDevPfxBuffer, getDevPassphrase } = await import('@al/xxx');
80+
return { pfx: await getDevPfxBuffer(), passphrase: await getDevPassphrase() };
81+
},
82+
}
83+
```
84+
85+
```javascript
86+
hooks: {
87+
async httpsOptions() {
88+
// Example: load PEM cert/key from files
89+
const { readFileSync } = await import('node:fs');
90+
return {
91+
cert: readFileSync('/path/to/cert.pem'),
92+
key: readFileSync('/path/to/key.pem'),
93+
};
94+
},
95+
}
96+
```
97+
6498
### `localUrl` — Waterfall
6599

66100
Triggered when `/api/local-url` is requested (used by the QR code feature).
@@ -88,14 +122,14 @@ Triggered after the HTTP server starts successfully.
88122
| Property | Description |
89123
|----------|-------------|
90124
| **Type** | Parallel (concurrent notification) |
91-
| **Parameters** | `{ port, host }` |
125+
| **Parameters** | `{ port, host, url, ip, token, protocol }` |
92126
| **Returns** | Ignored |
93127
| **Timing** | After server binds to a port |
94128

95129
```javascript
96130
hooks: {
97-
async serverStarted({ port, host }) {
98-
console.error(`[my-plugin] Server is running on ${host}:${port}`);
131+
async serverStarted({ port, host, url, ip, token, protocol }) {
132+
console.error(`[my-plugin] Server is running at ${url}`);
99133
},
100134
}
101135
```

docs/plugins.zh.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,53 @@ export default {
6868

6969
| Hook 名称 | 类型 | 参数 | 返回值 | 触发时机 |
7070
|-----------|------|------|--------|---------|
71+
| `httpsOptions` | waterfall | `{}` | `{ pfx, passphrase }``{ cert, key }` | 服务器创建前 |
7172
| `localUrl` | waterfall | `{ url, ip, port, token }` | `{ url }` | 客户端请求局域网地址时 |
72-
| `serverStarted` | parallel | `{ port, host }` | 忽略 | 服务器启动成功后 |
73+
| `serverStarted` | parallel | `{ port, host, url, ip, token, protocol }` | 忽略 | 服务器启动成功后 |
7374
| `serverStopping` | parallel | `{}` | 忽略 | 服务器关闭前 |
7475
| `onNewEntry` | parallel | `entry` (JSONL 日志条目对象) | 忽略 | 检测到新的 JSONL 日志条目时 |
7576

7677
---
7778

7879
## Hook 详解
7980

81+
### `httpsOptions` — 提供 HTTPS 证书
82+
83+
**类型:Waterfall(串行管道)**
84+
85+
服务器启动时触发,用于获取 HTTPS 证书选项。如果返回的对象包含 `pfx``cert`,服务器将以 HTTPS 模式启动;否则回退到 HTTP。
86+
87+
**参数说明:**
88+
89+
| 参数 | 类型 | 说明 |
90+
|------|------|------|
91+
| (空对象) | `object` | 初始空对象,插件可以向其中添加 TLS 选项 |
92+
93+
**返回值:** 返回 `{ pfx, passphrase }``{ cert, key }`,传给 `https.createServer()`
94+
95+
```javascript
96+
hooks: {
97+
async httpsOptions() {
98+
// 示例 1:从内网包加载 PFX 证书
99+
const { getDevPfxBuffer, getDevPassphrase } = await import('@al/xxx');
100+
return { pfx: await getDevPfxBuffer(), passphrase: await getDevPassphrase() };
101+
},
102+
}
103+
```
104+
105+
```javascript
106+
hooks: {
107+
async httpsOptions() {
108+
// 示例 2:从文件加载 PEM 证书
109+
const { readFileSync } = await import('node:fs');
110+
return {
111+
cert: readFileSync('/path/to/cert.pem'),
112+
key: readFileSync('/path/to/key.pem'),
113+
};
114+
},
115+
}
116+
```
117+
80118
### `localUrl` — 修改局域网访问地址
81119

82120
**类型:Waterfall(串行管道)**
@@ -130,14 +168,14 @@ hooks: {
130168

131169
```javascript
132170
hooks: {
133-
async serverStarted({ port, host }) {
134-
console.error(`[my-plugin] 服务器运行在 ${host}:${port}`);
171+
async serverStarted({ port, host, url, ip, token, protocol }) {
172+
console.error(`[my-plugin] 服务器运行在 ${url}`);
135173

136174
// 示例:通知企业监控系统
137175
fetch('https://monitor.company.com/api/register', {
138176
method: 'POST',
139177
headers: { 'Content-Type': 'application/json' },
140-
body: JSON.stringify({ service: 'cc-viewer', port, host }),
178+
body: JSON.stringify({ service: 'cc-viewer', port, host, url }),
141179
}).catch(() => {});
142180
},
143181
}

history.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 1.5.21 (2026-03-12)
4+
5+
- Refactor: replace hardcoded HTTPS cert with plugin hook `httpsOptions` (waterfall)
6+
- Enhancement: `serverStarted` hook now receives `{ port, host, url, ip, token }` (added `url`, `ip`, `token`)
7+
- Fix: `/api/local-url` now respects actual server protocol (HTTP/HTTPS) instead of hardcoded `http://`
8+
39
## 1.5.20 (2026-03-12)
410

511
- Fix: `proxy-errors.js` missing from npm package, causing `ERR_MODULE_NOT_FOUND` when running `ccv -logger`

i18n.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,24 +367,24 @@ const i18nData = {
367367
"uk": "\n🗑️ Інтеграцію CC Viewer видалено (Запустіть 'npm uninstall -g cc-viewer', щоб видалити команду)"
368368
},
369369
"server.started": {
370-
"zh": "\nClaude 请求监控服务已启动: http://{host}:{port}\n",
371-
"en": "\nCC Viewer started: http://{host}:{port}\n",
372-
"zh-TW": "\nClaude 請求監控服務已啟動: http://{host}:{port}\n",
373-
"ko": "\nCC Viewer 시작됨: http://{host}:{port}\n",
374-
"ja": "\nCC Viewer 起動: http://{host}:{port}\n",
375-
"de": "\nCC Viewer gestartet: http://{host}:{port}\n",
376-
"es": "\nCC Viewer iniciado: http://{host}:{port}\n",
377-
"fr": "\nCC Viewer démarré : http://{host}:{port}\n",
378-
"it": "\nCC Viewer avviato: http://{host}:{port}\n",
379-
"da": "\nCC Viewer startet: http://{host}:{port}\n",
380-
"pl": "\nCC Viewer uruchomiony: http://{host}:{port}\n",
381-
"ru": "\nCC Viewer запущен: http://{host}:{port}\n",
382-
"ar": "\nتم تشغيل CC Viewer: http://{host}:{port}\n",
383-
"no": "\nCC Viewer startet: http://{host}:{port}\n",
384-
"pt-BR": "\nCC Viewer iniciado: http://{host}:{port}\n",
385-
"th": "\nCC Viewer เริ่มทำงาน: http://{host}:{port}\n",
386-
"tr": "\nCC Viewer başlatıldı: http://{host}:{port}\n",
387-
"uk": "\nCC Viewer запущено: http://{host}:{port}\n"
370+
"zh": "\nClaude 请求监控服务已启动: {protocol}://{host}:{port}\n",
371+
"en": "\nCC Viewer started: {protocol}://{host}:{port}\n",
372+
"zh-TW": "\nClaude 請求監控服務已啟動: {protocol}://{host}:{port}\n",
373+
"ko": "\nCC Viewer 시작됨: {protocol}://{host}:{port}\n",
374+
"ja": "\nCC Viewer 起動: {protocol}://{host}:{port}\n",
375+
"de": "\nCC Viewer gestartet: {protocol}://{host}:{port}\n",
376+
"es": "\nCC Viewer iniciado: {protocol}://{host}:{port}\n",
377+
"fr": "\nCC Viewer démarré : {protocol}://{host}:{port}\n",
378+
"it": "\nCC Viewer avviato: {protocol}://{host}:{port}\n",
379+
"da": "\nCC Viewer startet: {protocol}://{host}:{port}\n",
380+
"pl": "\nCC Viewer uruchomiony: {protocol}://{host}:{port}\n",
381+
"ru": "\nCC Viewer запущен: {protocol}://{host}:{port}\n",
382+
"ar": "\nتم تشغيل CC Viewer: {protocol}://{host}:{port}\n",
383+
"no": "\nCC Viewer startet: {protocol}://{host}:{port}\n",
384+
"pt-BR": "\nCC Viewer iniciado: {protocol}://{host}:{port}\n",
385+
"th": "\nCC Viewer เริ่มทำงาน: {protocol}://{host}:{port}\n",
386+
"tr": "\nCC Viewer başlatıldı: {protocol}://{host}:{port}\n",
387+
"uk": "\nCC Viewer запущено: {protocol}://{host}:{port}\n"
388388
},
389389
"update.updating": {
390390
"zh": "正在更新到 v{version}...",

lib/plugin-loader.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const PREFS_FILE = join(LOG_DIR, 'preferences.json');
77

88
// Hook 类型定义
99
const HOOK_TYPES = {
10+
httpsOptions: 'waterfall',
1011
localUrl: 'waterfall',
1112
serverStarted: 'parallel',
1213
serverStopping: 'parallel',

server.js

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createServer } from 'node:http';
2+
import { createServer as createHttpsServer } from 'node:https';
23
import { createConnection } from 'node:net';
34
import { randomBytes } from 'node:crypto';
45
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync, realpathSync, mkdirSync, createReadStream } from 'node:fs';
@@ -112,7 +113,8 @@ const ACCESS_TOKEN = randomBytes(16).toString('hex');
112113

113114
let clients = [];
114115
let server;
115-
let actualPort = START_PORT;
116+
let actualPort = 0;
117+
let serverProtocol = 'http';
116118
// 跟踪所有被 watch 的日志文件
117119
const watchedFiles = new Map();
118120
// Stats Worker 实例
@@ -242,6 +244,16 @@ function startWatching() {
242244
watchLogFile(LOG_FILE);
243245
}
244246

247+
function getLocalIp() {
248+
const nets = networkInterfaces();
249+
for (const name of Object.keys(nets)) {
250+
for (const net of nets[name]) {
251+
if (net.family === 'IPv4' && !net.internal) return net.address;
252+
}
253+
}
254+
return '127.0.0.1';
255+
}
256+
245257
async function handleRequest(req, res) {
246258
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
247259
const url = parsedUrl.pathname;
@@ -1210,18 +1222,8 @@ async function handleRequest(req, res) {
12101222

12111223
// 返回局域网访问地址
12121224
if (url === '/api/local-url' && method === 'GET') {
1213-
const nets = networkInterfaces();
1214-
let localIp = '127.0.0.1';
1215-
for (const name of Object.keys(nets)) {
1216-
for (const net of nets[name]) {
1217-
if (net.family === 'IPv4' && !net.internal) {
1218-
localIp = net.address;
1219-
break;
1220-
}
1221-
}
1222-
if (localIp !== '127.0.0.1') break;
1223-
}
1224-
const defaultUrl = `http://${localIp}:${actualPort}?token=${ACCESS_TOKEN}`;
1225+
const localIp = getLocalIp();
1226+
const defaultUrl = `${serverProtocol}://${localIp}:${actualPort}?token=${ACCESS_TOKEN}`;
12251227
const hookResult = await runWaterfallHook('localUrl', { url: defaultUrl, ip: localIp, port: actualPort, token: ACCESS_TOKEN });
12261228
res.writeHead(200, { 'Content-Type': 'application/json' });
12271229
res.end(JSON.stringify({ url: hookResult.url }));
@@ -1648,6 +1650,23 @@ async function handleRequest(req, res) {
16481650
}
16491651

16501652
export async function startViewer() {
1653+
// 加载插件(需要在创建服务器之前,以便通过 hook 获取 HTTPS 证书)
1654+
await loadPlugins();
1655+
1656+
// 通过插件 hook 获取 HTTPS 证书选项
1657+
let httpsOptions = null;
1658+
try {
1659+
const httpsResult = await runWaterfallHook('httpsOptions', {});
1660+
httpsOptions = (httpsResult.pfx || httpsResult.cert) ? httpsResult : null;
1661+
} catch (err) {
1662+
console.error('[CC Viewer] httpsOptions hook error:', err.message);
1663+
}
1664+
1665+
const useHttps = !!httpsOptions;
1666+
const protocol = useHttps ? 'https' : 'http';
1667+
serverProtocol = protocol;
1668+
if (useHttps) console.error('[CC Viewer] HTTPS mode enabled via plugin hook');
1669+
16511670
return new Promise((resolve, reject) => {
16521671
function tryListen(port) {
16531672
if (port > MAX_PORT) {
@@ -1664,14 +1683,25 @@ export async function startViewer() {
16641683
});
16651684
probe.on('error', () => {
16661685
probe.destroy();
1667-
// 端口空闲,绑定 0.0.0.0
1668-
const currentServer = createServer(handleRequest);
1686+
// 端口空闲,绑定
1687+
let currentServer;
1688+
if (useHttps) {
1689+
try {
1690+
currentServer = createHttpsServer(httpsOptions, handleRequest);
1691+
} catch (err) {
1692+
console.error('[CC Viewer] HTTPS server creation failed, falling back to HTTP:', err.message);
1693+
currentServer = createServer(handleRequest);
1694+
serverProtocol = 'http';
1695+
}
1696+
} else {
1697+
currentServer = createServer(handleRequest);
1698+
}
16691699

16701700
currentServer.listen(port, HOST, () => {
16711701
server = currentServer;
16721702
actualPort = port;
1673-
const url = `http://127.0.0.1:${port}`;
1674-
console.error(t('server.started', { host: '127.0.0.1', port }));
1703+
const url = `${serverProtocol}://127.0.0.1:${port}`;
1704+
console.error(t('server.started', { host: '127.0.0.1', port, protocol: serverProtocol }));
16751705
// v2.0.69 之前的版本会清空控制台,自动打开浏览器确保用户能看到界面
16761706
try {
16771707
const ccPkgPath = join(__dirname, '..', '@anthropic-ai', 'claude-code', 'package.json');
@@ -1691,10 +1721,9 @@ export async function startViewer() {
16911721
if (isCliMode) {
16921722
setupTerminalWebSocket(currentServer);
16931723
}
1694-
// 加载插件并通知服务器已启动
1695-
loadPlugins()
1696-
.then(() => runParallelHook('serverStarted', { port, host: HOST }))
1697-
.catch(err => console.error('[CC Viewer] Plugin init error:', err.message));
1724+
// 通知插件服务器已启动
1725+
runParallelHook('serverStarted', { port, host: HOST, url, ip: getLocalIp(), token: ACCESS_TOKEN, protocol: serverProtocol })
1726+
.catch(err => console.error('[CC Viewer] Plugin serverStarted hook error:', err.message));
16981727
resolve(server);
16991728
});
17001729

@@ -1986,6 +2015,10 @@ export function getPort() {
19862015
return actualPort;
19872016
}
19882017

2018+
export function getProtocol() {
2019+
return serverProtocol;
2020+
}
2021+
19892022
let _stoppingPromise = null;
19902023
export function stopViewer() {
19912024
if (_stoppingPromise) return _stoppingPromise;

0 commit comments

Comments
 (0)