@@ -290,6 +290,35 @@ function clearLoginAttempts(ip) {
290290 _loginAttempts . delete ( ip )
291291}
292292
293+ // 从 CLI 输出中提取 JSON(跳过 Node 警告、npm 更新提示等非 JSON 行)
294+ function extractCliJson ( text ) {
295+ // 快速路径:整个文本就是合法 JSON
296+ try { return JSON . parse ( text ) } catch { }
297+ // 找到第一个 { 或 [ 开始尝试解析
298+ for ( let i = 0 ; i < text . length ; i ++ ) {
299+ const ch = text [ i ]
300+ if ( ch === '{' || ch === '[' ) {
301+ // 找到匹配的闭合位置
302+ let depth = 0 , end = - 1
303+ const close = ch === '{' ? '}' : ']'
304+ let inStr = false , esc = false
305+ for ( let j = i ; j < text . length ; j ++ ) {
306+ const c = text [ j ]
307+ if ( esc ) { esc = false ; continue }
308+ if ( c === '\\' && inStr ) { esc = true ; continue }
309+ if ( c === '"' && ! esc ) { inStr = ! inStr ; continue }
310+ if ( inStr ) continue
311+ if ( c === ch ) depth ++
312+ else if ( c === close ) { depth -- ; if ( depth === 0 ) { end = j ; break } }
313+ }
314+ if ( end > i ) {
315+ try { return JSON . parse ( text . slice ( i , end + 1 ) ) } catch { }
316+ }
317+ }
318+ }
319+ throw new Error ( '解析失败: 输出中未找到有效 JSON' )
320+ }
321+
293322// 配置缓存:避免每次请求同步读磁盘(TTL 2秒,写入时立即失效)
294323let _panelConfigCache = null
295324let _panelConfigCacheTime = 0
@@ -1334,7 +1363,7 @@ const handlers = {
13341363 return { exists : true , values : form }
13351364 } ,
13361365
1337- save_messaging_platform ( { platform, form } ) {
1366+ save_messaging_platform ( { platform, form, accountId } ) {
13381367 if ( ! fs . existsSync ( CONFIG_PATH ) ) throw new Error ( 'openclaw.json 不存在' )
13391368 const cfg = JSON . parse ( fs . readFileSync ( CONFIG_PATH , 'utf8' ) )
13401369 if ( ! cfg . channels ) cfg . channels = { }
@@ -1356,6 +1385,14 @@ const handlers = {
13561385 entry . appSecret = form . appSecret
13571386 entry . connectionMode = 'websocket'
13581387 if ( form . domain ) entry . domain = form . domain
1388+ // 多账号模式:写入 channels.feishu.accounts.<accountId>
1389+ if ( accountId ) {
1390+ if ( ! cfg . channels . feishu ) cfg . channels . feishu = { enabled : true }
1391+ if ( ! cfg . channels . feishu . accounts ) cfg . channels . feishu . accounts = { }
1392+ cfg . channels . feishu . accounts [ accountId ] = entry
1393+ fs . writeFileSync ( CONFIG_PATH , JSON . stringify ( cfg , null , 2 ) )
1394+ return { ok : true }
1395+ }
13591396 } else {
13601397 Object . assign ( entry , form )
13611398 }
@@ -3006,8 +3043,8 @@ const handlers = {
30063043 skills_list ( ) {
30073044 // 尝试真实 CLI
30083045 try {
3009- const out = execSync ( 'npx -y openclaw skills list --json --verbose ' , { encoding : 'utf8' , timeout : 30000 } )
3010- return JSON . parse ( out )
3046+ const out = execSync ( 'npx -y openclaw skills list --json' , { encoding : 'utf8' , timeout : 30000 } )
3047+ return extractCliJson ( out )
30113048 } catch {
30123049 // CLI 不可用时返回 mock 数据
30133050 return {
@@ -3026,15 +3063,15 @@ const handlers = {
30263063 skills_info ( { name } ) {
30273064 try {
30283065 const out = execSync ( `npx -y openclaw skills info ${ JSON . stringify ( name ) } --json` , { encoding : 'utf8' , timeout : 30000 } )
3029- return JSON . parse ( out )
3066+ return extractCliJson ( out )
30303067 } catch ( e ) {
30313068 throw new Error ( '查看详情失败: ' + ( e . message || e ) )
30323069 }
30333070 } ,
30343071 skills_check ( ) {
30353072 try {
30363073 const out = execSync ( 'npx -y openclaw skills check --json' , { encoding : 'utf8' , timeout : 30000 } )
3037- return JSON . parse ( out )
3074+ return extractCliJson ( out )
30383075 } catch {
30393076 return { summary : { total : 0 , eligible : 0 , disabled : 0 , blocked : 0 , missingRequirements : 0 } , eligible : [ ] , disabled : [ ] , blocked : [ ] , missingRequirements : [ ] }
30403077 }
@@ -3055,6 +3092,76 @@ const handlers = {
30553092 throw new Error ( `安装失败: ${ e . message || e } ` )
30563093 }
30573094 } ,
3095+ skills_skillhub_check ( ) {
3096+ try {
3097+ const out = execSync ( 'skillhub --version' , { encoding : 'utf8' , timeout : 5000 } )
3098+ return { installed : true , version : out . trim ( ) }
3099+ } catch {
3100+ return { installed : false }
3101+ }
3102+ } ,
3103+ skills_skillhub_setup ( { cliOnly } ) {
3104+ const flag = cliOnly ? '--cli-only' : '--no-skills'
3105+ try {
3106+ const out = execSync (
3107+ `curl -fsSL https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/install/install.sh | bash -s -- ${ flag } ` ,
3108+ { encoding : 'utf8' , timeout : 120000 }
3109+ )
3110+ return { success : true , output : out . trim ( ) }
3111+ } catch ( e ) {
3112+ throw new Error ( 'SkillHub 安装失败: ' + ( e . message || e ) )
3113+ }
3114+ } ,
3115+ skills_skillhub_search ( { query } ) {
3116+ const q = String ( query || '' ) . trim ( )
3117+ if ( ! q ) return [ ]
3118+ try {
3119+ const out = execSync ( `skillhub search ${ JSON . stringify ( q ) } ` , { encoding : 'utf8' , timeout : 30000 } )
3120+ // 解析格式: [N] owner/repo/name 状态\n 统计 描述...
3121+ const lines = out . split ( '\n' )
3122+ const items = [ ]
3123+ for ( let i = 0 ; i < lines . length ; i ++ ) {
3124+ const trimmed = lines [ i ] . trim ( )
3125+ if ( ! trimmed . startsWith ( '[' ) ) continue
3126+ const bracketEnd = trimmed . indexOf ( ']' )
3127+ if ( bracketEnd < 0 ) continue
3128+ const afterBracket = trimmed . slice ( bracketEnd + 1 ) . trim ( )
3129+ const slug = ( afterBracket . split ( / \s / ) [ 0 ] || '' ) . trim ( )
3130+ if ( ! slug . includes ( '/' ) ) continue
3131+ let desc = ''
3132+ if ( i + 1 < lines . length ) {
3133+ const next = lines [ i + 1 ] . trim ( )
3134+ const starIdx = next . indexOf ( '⭐' )
3135+ if ( starIdx >= 0 ) {
3136+ const afterStar = next . slice ( starIdx + 2 ) . trim ( )
3137+ desc = afterStar . replace ( / ^ [ \d . ] + [ k K m M ] ? \s * / , '' ) . trim ( )
3138+ }
3139+ }
3140+ items . push ( { slug, description : desc , source : 'skillhub' } )
3141+ }
3142+ return items
3143+ } catch ( e ) {
3144+ throw new Error ( '搜索失败: ' + ( e . message || e ) + '。请先安装 SkillHub CLI' )
3145+ }
3146+ } ,
3147+ skills_skillhub_install ( { slug } ) {
3148+ const skillsDir = path . join ( OPENCLAW_DIR , 'skills' )
3149+ if ( ! fs . existsSync ( skillsDir ) ) fs . mkdirSync ( skillsDir , { recursive : true } )
3150+ try {
3151+ const out = execSync ( `skillhub install ${ JSON . stringify ( slug ) } --force` , { cwd : homedir ( ) , encoding : 'utf8' , timeout : 120000 } )
3152+ return { success : true , slug, output : out . trim ( ) }
3153+ } catch ( e ) {
3154+ throw new Error ( '安装失败: ' + ( e . message || e ) + '。请先安装 SkillHub CLI' )
3155+ }
3156+ } ,
3157+
3158+ skills_uninstall ( { name } ) {
3159+ if ( ! name || name . includes ( '..' ) || name . includes ( '/' ) || name . includes ( '\\' ) ) throw new Error ( '无效的 Skill 名称' )
3160+ const skillDir = path . join ( OPENCLAW_DIR , 'skills' , name )
3161+ if ( ! fs . existsSync ( skillDir ) ) throw new Error ( `Skill「${ name } 」不存在` )
3162+ fs . rmSync ( skillDir , { recursive : true , force : true } )
3163+ return { success : true , name }
3164+ } ,
30583165 skills_clawhub_search ( { query } ) {
30593166 const q = String ( query || '' ) . trim ( )
30603167 if ( ! q ) return [ ]
0 commit comments