11import { createServer } from 'node:http' ;
2+ import { createServer as createHttpsServer } from 'node:https' ;
23import { createConnection } from 'node:net' ;
34import { randomBytes } from 'node:crypto' ;
45import { 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
113114let clients = [ ] ;
114115let server ;
115- let actualPort = START_PORT ;
116+ let actualPort = 0 ;
117+ let serverProtocol = 'http' ;
116118// 跟踪所有被 watch 的日志文件
117119const 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+
245257async 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
16501652export 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+
19892022let _stoppingPromise = null ;
19902023export function stopViewer ( ) {
19912024 if ( _stoppingPromise ) return _stoppingPromise ;
0 commit comments