@@ -3373,6 +3373,79 @@ const handlers = {
33733373 }
33743374 } ,
33753375
3376+ list_all_plugins ( ) {
3377+ const cfg = readOpenclawConfigOptional ( )
3378+ const entries = cfg . plugins ?. entries || { }
3379+ const allowArr = cfg . plugins ?. allow || [ ]
3380+ const extDir = path . join ( OPENCLAW_DIR , 'extensions' )
3381+ const plugins = [ ]
3382+ const seen = new Set ( )
3383+
3384+ // Scan extensions directory
3385+ if ( fs . existsSync ( extDir ) ) {
3386+ for ( const name of fs . readdirSync ( extDir ) ) {
3387+ if ( name . startsWith ( '.' ) ) continue
3388+ const p = path . join ( extDir , name )
3389+ if ( ! fs . statSync ( p ) . isDirectory ( ) ) continue
3390+ const hasMarker = fs . existsSync ( path . join ( p , 'package.json' ) ) || fs . existsSync ( path . join ( p , 'plugin.ts' ) ) || fs . existsSync ( path . join ( p , 'index.js' ) )
3391+ if ( ! hasMarker ) continue
3392+ seen . add ( name )
3393+ const entryCfg = entries [ name ]
3394+ const enabled = ! ! entryCfg ?. enabled
3395+ const allowed = allowArr . includes ( name )
3396+ let version = null , description = null
3397+ try {
3398+ const pkg = JSON . parse ( fs . readFileSync ( path . join ( p , 'package.json' ) , 'utf8' ) )
3399+ version = pkg . version || null
3400+ description = pkg . description || null
3401+ } catch { }
3402+ plugins . push ( { id : name , installed : true , builtin : false , enabled, allowed, version, description, config : entryCfg ?. config || null } )
3403+ }
3404+ }
3405+
3406+ // Include entries from config not found in extensions dir
3407+ for ( const [ pid , val ] of Object . entries ( entries ) ) {
3408+ if ( seen . has ( pid ) ) continue
3409+ seen . add ( pid )
3410+ plugins . push ( { id : pid , installed : false , builtin : false , enabled : ! ! val ?. enabled , allowed : allowArr . includes ( pid ) , version : null , description : null , config : val ?. config || null } )
3411+ }
3412+
3413+ plugins . sort ( ( a , b ) => ( b . enabled ? 1 : 0 ) - ( a . enabled ? 1 : 0 ) || a . id . localeCompare ( b . id ) )
3414+ return { plugins }
3415+ } ,
3416+
3417+ toggle_plugin ( { pluginId, enabled } ) {
3418+ if ( ! pluginId || ! pluginId . trim ( ) ) throw new Error ( 'pluginId 不能为空' )
3419+ const pid = pluginId . trim ( )
3420+ const cfg = readOpenclawConfigOptional ( )
3421+ if ( ! cfg . plugins ) cfg . plugins = { }
3422+ if ( ! cfg . plugins . entries ) cfg . plugins . entries = { }
3423+ if ( ! cfg . plugins . allow ) cfg . plugins . allow = [ ]
3424+
3425+ if ( enabled ) {
3426+ if ( ! cfg . plugins . allow . includes ( pid ) ) cfg . plugins . allow . push ( pid )
3427+ if ( ! cfg . plugins . entries [ pid ] ) cfg . plugins . entries [ pid ] = { }
3428+ cfg . plugins . entries [ pid ] . enabled = true
3429+ } else {
3430+ cfg . plugins . allow = cfg . plugins . allow . filter ( v => v !== pid )
3431+ if ( cfg . plugins . entries [ pid ] ) cfg . plugins . entries [ pid ] . enabled = false
3432+ }
3433+
3434+ fs . writeFileSync ( CONFIG_PATH , JSON . stringify ( cfg , null , 2 ) , 'utf8' )
3435+ return { ok : true , enabled, pluginId : pid }
3436+ } ,
3437+
3438+ install_plugin ( { packageName } ) {
3439+ if ( ! packageName || ! packageName . trim ( ) ) throw new Error ( '包名不能为空' )
3440+ const spec = packageName . trim ( )
3441+ try {
3442+ execOpenclawSync ( [ 'plugins' , 'install' , spec ] , { timeout : 120000 , cwd : homedir ( ) , windowsHide : true } , `插件 ${ spec } 安装失败` )
3443+ return { ok : true , output : '安装成功' }
3444+ } catch ( e ) {
3445+ throw new Error ( `插件安装失败: ${ e . message || e } ` )
3446+ }
3447+ } ,
3448+
33763449 get_channel_plugin_status ( { pluginId } ) {
33773450 if ( ! pluginId || ! pluginId . trim ( ) ) throw new Error ( 'pluginId 不能为空' )
33783451 const pid = pluginId . trim ( )
@@ -4480,6 +4553,129 @@ const handlers = {
44804553 }
44814554 } ,
44824555
4556+ async probe_gateway_port ( ) {
4557+ const port = readGatewayPort ( )
4558+ return new Promise ( resolve => {
4559+ const net = require ( 'net' )
4560+ const sock = net . createConnection ( { host : '127.0.0.1' , port, timeout : 3000 } )
4561+ sock . on ( 'connect' , ( ) => { sock . destroy ( ) ; resolve ( true ) } )
4562+ sock . on ( 'error' , ( ) => resolve ( false ) )
4563+ sock . on ( 'timeout' , ( ) => { sock . destroy ( ) ; resolve ( false ) } )
4564+ } )
4565+ } ,
4566+
4567+ async diagnose_gateway_connection ( ) {
4568+ const steps = [ ]
4569+ const ocDir = openclawDir ( )
4570+ const configPath = path . join ( ocDir , 'openclaw.json' )
4571+ const port = readGatewayPort ( )
4572+
4573+ // 1. 配置文件
4574+ const t1 = Date . now ( )
4575+ try {
4576+ const content = fs . readFileSync ( configPath , 'utf-8' )
4577+ const val = JSON . parse ( content )
4578+ steps . push ( { name : 'config' , ok : ! ! val . gateway , message : val . gateway ? '配置文件有效,含 gateway 配置' : '配置文件缺少 gateway 段' , durationMs : Date . now ( ) - t1 } )
4579+ } catch ( e ) {
4580+ steps . push ( { name : 'config' , ok : false , message : `配置文件异常: ${ e . message } ` , durationMs : Date . now ( ) - t1 } )
4581+ }
4582+
4583+ // 2. 设备密钥
4584+ const t2 = Date . now ( )
4585+ const keyPath = path . join ( ocDir , 'clawpanel-device-key.json' )
4586+ const keyExists = fs . existsSync ( keyPath )
4587+ steps . push ( { name : 'device_key' , ok : keyExists , message : keyExists ? '设备密钥存在' : '设备密钥不存在' , durationMs : Date . now ( ) - t2 } )
4588+
4589+ // 3. allowedOrigins
4590+ const t3 = Date . now ( )
4591+ try {
4592+ const val = JSON . parse ( fs . readFileSync ( configPath , 'utf-8' ) )
4593+ const origins = val ?. gateway ?. controlUi ?. allowedOrigins
4594+ if ( Array . isArray ( origins ) && origins . length > 0 ) {
4595+ steps . push ( { name : 'allowed_origins' , ok : true , message : `allowedOrigins: ${ JSON . stringify ( origins ) } ` , durationMs : Date . now ( ) - t3 } )
4596+ } else {
4597+ steps . push ( { name : 'allowed_origins' , ok : false , message : '未配置 allowedOrigins' , durationMs : Date . now ( ) - t3 } )
4598+ }
4599+ } catch {
4600+ steps . push ( { name : 'allowed_origins' , ok : false , message : '配置文件不可读' , durationMs : Date . now ( ) - t3 } )
4601+ }
4602+
4603+ // 4. TCP 端口
4604+ const t4 = Date . now ( )
4605+ const tcpOk = await new Promise ( resolve => {
4606+ const net = require ( 'net' )
4607+ const sock = net . createConnection ( { host : '127.0.0.1' , port, timeout : 3000 } )
4608+ sock . on ( 'connect' , ( ) => { sock . destroy ( ) ; resolve ( true ) } )
4609+ sock . on ( 'error' , ( ) => resolve ( false ) )
4610+ sock . on ( 'timeout' , ( ) => { sock . destroy ( ) ; resolve ( false ) } )
4611+ } )
4612+ steps . push ( { name : 'tcp_port' , ok : tcpOk , message : tcpOk ? `端口 ${ port } 可达` : `端口 ${ port } 不可达` , durationMs : Date . now ( ) - t4 } )
4613+
4614+ // 5. HTTP /health
4615+ const t5 = Date . now ( )
4616+ let httpOk = false
4617+ let httpMsg = ''
4618+ try {
4619+ const resp = await fetch ( `http://127.0.0.1:${ port } /health` , { signal : AbortSignal . timeout ( 5000 ) } )
4620+ httpOk = resp . ok
4621+ httpMsg = `HTTP /health 返回 ${ resp . status } `
4622+ } catch ( e ) {
4623+ httpMsg = `HTTP /health 请求失败: ${ e . message } `
4624+ }
4625+ steps . push ( { name : 'http_health' , ok : httpOk , message : httpMsg , durationMs : Date . now ( ) - t5 } )
4626+
4627+ // 6. 错误日志
4628+ const t6 = Date . now ( )
4629+ const errLogPath = path . join ( ocDir , 'logs' , 'gateway.err.log' )
4630+ if ( fs . existsSync ( errLogPath ) ) {
4631+ const stat = fs . statSync ( errLogPath )
4632+ if ( stat . size === 0 ) {
4633+ steps . push ( { name : 'err_log' , ok : true , message : '错误日志为空(正常)' , durationMs : Date . now ( ) - t6 } )
4634+ } else {
4635+ const buf = Buffer . alloc ( Math . min ( 1024 , stat . size ) )
4636+ const fd = fs . openSync ( errLogPath , 'r' )
4637+ fs . readSync ( fd , buf , 0 , buf . length , Math . max ( 0 , stat . size - buf . length ) )
4638+ fs . closeSync ( fd )
4639+ const tail = buf . toString ( 'utf-8' ) . toLowerCase ( )
4640+ const hasFatal = tail . includes ( 'fatal' ) || tail . includes ( 'eaddrinuse' ) || tail . includes ( 'config invalid' )
4641+ steps . push ( { name : 'err_log' , ok : ! hasFatal , message : hasFatal ? `错误日志含关键错误 (${ stat . size } bytes)` : `错误日志存在但无致命错误 (${ stat . size } bytes)` , durationMs : Date . now ( ) - t6 } )
4642+ }
4643+ } else {
4644+ steps . push ( { name : 'err_log' , ok : true , message : '无错误日志(正常)' , durationMs : Date . now ( ) - t6 } )
4645+ }
4646+
4647+ // env
4648+ let authMode = 'none'
4649+ try {
4650+ const val = JSON . parse ( fs . readFileSync ( configPath , 'utf-8' ) )
4651+ const auth = val ?. gateway ?. auth
4652+ if ( auth ?. token ) authMode = 'token'
4653+ else if ( auth ?. password ) authMode = 'password'
4654+ } catch { }
4655+ let errLogExcerpt = ''
4656+ try {
4657+ const buf = fs . readFileSync ( errLogPath )
4658+ errLogExcerpt = buf . slice ( Math . max ( 0 , buf . length - 2048 ) ) . toString ( 'utf-8' )
4659+ } catch { }
4660+
4661+ const overallOk = steps . every ( s => s . ok )
4662+ const failed = steps . filter ( s => ! s . ok ) . map ( s => s . name )
4663+ return {
4664+ steps,
4665+ env : {
4666+ openclawDir : ocDir ,
4667+ configExists : fs . existsSync ( configPath ) ,
4668+ port,
4669+ authMode,
4670+ deviceKeyExists : keyExists ,
4671+ gatewayOwner : null ,
4672+ errLogExcerpt,
4673+ } ,
4674+ overallOk,
4675+ summary : overallOk ? '所有检查项通过' : `以下检查未通过: ${ failed . join ( ', ' ) } ` ,
4676+ }
4677+ } ,
4678+
44834679 guardian_status ( ) {
44844680 // Web 模式没有 Guardian 守护进程
44854681 return { enabled : false , giveUp : false }
0 commit comments