CLI 与 MBE Desktop 集成方案

概述

本文档描述如何将 MBE PowerShell CLI 工具集成到 MBE Desktop Electron 应用中,实现命令行功能在桌面应用中的无缝使用。

架构设计

当前状态

  • CLI工具 (cli/mbe.ps1): PowerShell 统一命令行工具,提供完整管理功能
  • MBE Desktop: Electron 桌面应用,React 前端界面
  • 通信方式: 前端直接通过 HTTP API 与后端通信

CLI 特性

特性 说明
全局标志 --json--verbose--quiet--force--dry-run--help
结构化输出 --json 模式输出标准 JSON,适合程序解析
审计日志 所有操作自动记录到 logs/cli_audit.log
Tab 补全 加载 lib/completion.ps1 后支持命令/参数补全
可配置化 通过 cli/config/defaults.json 配置默认值

集成目标

  1. 在 Electron 主进程中执行 CLI 命令
  2. 通过 IPC 将 CLI 功能暴露给渲染进程
  3. 前端可以选择使用 CLI 或直接 API 调用
  4. 支持 CLI 输出流式传输到前端
  5. 利用 --json 全局标志获取结构化数据

实现方案

方案一:主进程执行 CLI(推荐)

1. 主进程添加 CLI 执行器

src/main/index.ts 中添加:

import { spawn, ChildProcess } from 'child_process'
import { ipcMain, BrowserWindow } from 'electron'
import path from 'path'

interface CLICommand {
  command: string    // 'db', 'user', 'health', 'ops', 'monitoring' 等
  subCommand?: string // 'backup', 'list', 'status', 'dashboard' 等
  args?: string[]
  globalFlags?: {
    json?: boolean     // --json 结构化输出
    verbose?: boolean  // --verbose 详细输出
    quiet?: boolean    // --quiet 静默模式
    force?: boolean    // --force 跳过确认
    dryRun?: boolean   // --dry-run 预览模式
  }
}

interface CLIResult {
  requestId: string
  exitCode: number | null
  jsonData?: object  // --json 模式下的解析数据
}

class CLIManager {
  private mbeScriptPath: string
  private processes: Map<string, ChildProcess> = new Map()

  constructor() {
    // PowerShell CLI 脚本路径
    this.mbeScriptPath = path.join(
      __dirname,
      '../../cli/mbe.ps1'
    )
  }

  /**
   * 构建 PowerShell 命令参数
   */
  private buildArgs(command: CLICommand): string[] {
    const args = [
      '-NoProfile',
      '-ExecutionPolicy', 'Bypass',
      '-File', this.mbeScriptPath,
      command.command
    ]

    if (command.subCommand) {
      args.push(command.subCommand)
    }

    // 添加全局标志
    if (command.globalFlags) {
      if (command.globalFlags.json)    args.push('--json')
      if (command.globalFlags.verbose) args.push('--verbose')
      if (command.globalFlags.quiet)   args.push('--quiet')
      if (command.globalFlags.force)   args.push('--force')
      if (command.globalFlags.dryRun)  args.push('--dry-run')
    }

    // 添加额外参数
    if (command.args) {
      args.push(...command.args)
    }

    return args
  }

  async executeCommand(
    command: CLICommand,
    requestId: string
  ): Promise<void> {
    const args = this.buildArgs(command)

    // 使用 powershell.exe 执行(Windows)
    const shell = process.platform === 'win32' ? 'powershell.exe' : 'pwsh'

    const childProcess = spawn(shell, args, {
      cwd: path.dirname(this.mbeScriptPath),
      env: {
        ...process.env,
        // 确保 UTF-8 输出
        PYTHONIOENCODING: 'utf-8',
      },
      stdio: ['pipe', 'pipe', 'pipe'],
    })

    this.processes.set(requestId, childProcess)

    let stdoutBuffer = ''

    // 处理标准输出
    childProcess.stdout?.on('data', (data) => {
      const output = data.toString('utf8')
      stdoutBuffer += output
      const mainWindow = BrowserWindow.getAllWindows()[0]
      if (mainWindow) {
        mainWindow.webContents.send('cli:output', {
          requestId,
          type: 'stdout',
          data: output,
        })
      }
    })

    // 处理错误输出
    childProcess.stderr?.on('data', (data) => {
      const output = data.toString('utf8')
      const mainWindow = BrowserWindow.getAllWindows()[0]
      if (mainWindow) {
        mainWindow.webContents.send('cli:output', {
          requestId,
          type: 'stderr',
          data: output,
        })
      }
    })

    // 处理进程退出
    childProcess.on('close', (code) => {
      this.processes.delete(requestId)
      const mainWindow = BrowserWindow.getAllWindows()[0]
      if (mainWindow) {
        // 如果是 JSON 模式,尝试解析输出
        let jsonData: object | undefined
        if (command.globalFlags?.json) {
          try {
            jsonData = JSON.parse(stdoutBuffer)
          } catch {
            // JSON 解析失败,忽略
          }
        }
        mainWindow.webContents.send('cli:complete', {
          requestId,
          exitCode: code,
          jsonData,
        })
      }
    })

    // 处理错误
    childProcess.on('error', (error) => {
      this.processes.delete(requestId)
      const mainWindow = BrowserWindow.getAllWindows()[0]
      if (mainWindow) {
        mainWindow.webContents.send('cli:error', {
          requestId,
          error: error.message,
        })
      }
    })
  }

  killCommand(requestId: string): boolean {
    const proc = this.processes.get(requestId)
    if (proc) {
      proc.kill()
      this.processes.delete(requestId)
      return true
    }
    return false
  }

  /**
   * 便捷方法:执行命令并等待 JSON 结果
   */
  async executeJson(command: Omit<CLICommand, 'globalFlags'>): Promise<object | null> {
    return new Promise((resolve) => {
      const requestId = `json-${Date.now()}`
      const fullCommand: CLICommand = {
        ...command,
        globalFlags: { json: true, quiet: true }
      }

      const args = this.buildArgs(fullCommand)
      const shell = process.platform === 'win32' ? 'powershell.exe' : 'pwsh'

      const child = spawn(shell, args, {
        cwd: path.dirname(this.mbeScriptPath),
        env: process.env,
      })

      let output = ''
      child.stdout?.on('data', (d) => { output += d.toString('utf8') })
      child.on('close', () => {
        try {
          resolve(JSON.parse(output))
        } catch {
          resolve(null)
        }
      })
      child.on('error', () => resolve(null))
    })
  }
}

const cliManager = new CLIManager()

// IPC 处理器
ipcMain.handle('cli:execute', async (_event, command: CLICommand) => {
  const requestId = `${Date.now()}-${Math.random()}`
  await cliManager.executeCommand(command, requestId)
  return { requestId }
})

ipcMain.handle('cli:kill', async (_event, requestId: string) => {
  return cliManager.killCommand(requestId)
})

// 便捷 JSON 查询
ipcMain.handle('cli:query', async (_event, command: Omit<CLICommand, 'globalFlags'>) => {
  return await cliManager.executeJson(command)
})

2. 更新 preload.ts

import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
  // ... 现有 API ...

  // CLI 功能
  cli: {
    execute: (command: {
      command: string
      subCommand?: string
      args?: string[]
      globalFlags?: {
        json?: boolean
        verbose?: boolean
        quiet?: boolean
        force?: boolean
        dryRun?: boolean
      }
    }) => ipcRenderer.invoke('cli:execute', command),

    kill: (requestId: string) => ipcRenderer.invoke('cli:kill', requestId),

    // 便捷 JSON 查询(同步等待结果)
    query: (command: {
      command: string
      subCommand?: string
      args?: string[]
    }) => ipcRenderer.invoke('cli:query', command),

    onOutput: (callback: (data: {
      requestId: string
      type: 'stdout' | 'stderr'
      data: string
    }) => void) => {
      ipcRenderer.on('cli:output', (_event, data) => callback(data))
    },

    onComplete: (callback: (data: {
      requestId: string
      exitCode: number | null
      jsonData?: object
    }) => void) => {
      ipcRenderer.on('cli:complete', (_event, data) => callback(data))
    },

    onError: (callback: (data: {
      requestId: string
      error: string
    }) => void) => {
      ipcRenderer.on('cli:error', (_event, data) => callback(data))
    },
  },
})

3. 前端 CLI 服务

创建 src/services/cliService.ts

interface CLICommand {
  command: string
  subCommand?: string
  args?: string[]
  globalFlags?: {
    json?: boolean
    verbose?: boolean
    quiet?: boolean
    force?: boolean
    dryRun?: boolean
  }
}

interface CLIOutput {
  requestId: string
  type: 'stdout' | 'stderr'
  data: string
}

class CLIService {
  private outputCallbacks: Map<string, (output: CLIOutput) => void> = new Map()
  private completeCallbacks: Map<string, (result: { exitCode: number | null; jsonData?: object }) => void> = new Map()
  private errorCallbacks: Map<string, (error: string) => void> = new Map()

  constructor() {
    if (window.electronAPI?.cli) {
      window.electronAPI.cli.onOutput((data) => {
        const callback = this.outputCallbacks.get(data.requestId)
        callback?.(data)
      })

      window.electronAPI.cli.onComplete((data) => {
        const callback = this.completeCallbacks.get(data.requestId)
        callback?.({ exitCode: data.exitCode, jsonData: data.jsonData })
        this.cleanup(data.requestId)
      })

      window.electronAPI.cli.onError((data) => {
        const callback = this.errorCallbacks.get(data.requestId)
        callback?.(data.error)
        this.cleanup(data.requestId)
      })
    }
  }

  private cleanup(requestId: string) {
    this.outputCallbacks.delete(requestId)
    this.completeCallbacks.delete(requestId)
    this.errorCallbacks.delete(requestId)
  }

  /**
   * 执行 CLI 命令(流式输出)
   */
  async execute(
    command: CLICommand,
    callbacks?: {
      onOutput?: (output: CLIOutput) => void
      onComplete?: (result: { exitCode: number | null; jsonData?: object }) => void
      onError?: (error: string) => void
    }
  ): Promise<string> {
    if (!window.electronAPI?.cli) {
      throw new Error('CLI 功能不可用')
    }

    const { requestId } = await window.electronAPI.cli.execute(command)

    if (callbacks) {
      if (callbacks.onOutput) this.outputCallbacks.set(requestId, callbacks.onOutput)
      if (callbacks.onComplete) this.completeCallbacks.set(requestId, callbacks.onComplete)
      if (callbacks.onError) this.errorCallbacks.set(requestId, callbacks.onError)
    }

    return requestId
  }

  /**
   * 便捷方法:执行命令并获取 JSON 结果
   */
  async query<T = object>(command: string, subCommand?: string, args?: string[]): Promise<T | null> {
    if (!window.electronAPI?.cli) {
      throw new Error('CLI 功能不可用')
    }
    return await window.electronAPI.cli.query({ command, subCommand, args }) as T | null
  }

  async kill(requestId: string): Promise<boolean> {
    if (!window.electronAPI?.cli) return false
    return await window.electronAPI.cli.kill(requestId)
  }
}

export const cliService = new CLIService()

4. React 组件示例

创建 src/components/cli/Terminal.tsx

import React, { useState, useRef, useEffect, useCallback } from 'react'
import { cliService } from '@/services/cliService'

// 预定义的常用命令
const QUICK_COMMANDS = [
  { label: '健康检查', command: 'health', icon: '🩺' },
  { label: '数据库状态', command: 'db', subCommand: 'status', icon: '🗄️' },
  { label: '服务状态', command: 'deploy', subCommand: 'status', icon: '🚀' },
  { label: '运维仪表盘', command: 'ops', subCommand: 'dashboard', icon: '📊' },
  { label: '运维巡检', command: 'ops', subCommand: 'check', icon: '✅' },
  { label: '监控状态', command: 'monitoring', subCommand: 'status', icon: '📈' },
  { label: '用户列表', command: 'user', subCommand: 'list', icon: '👥' },
  { label: '缓存状态', command: 'cache', subCommand: 'status', icon: '💾' },
]

export const CLITerminal: React.FC = () => {
  const [output, setOutput] = useState<string[]>([])
  const [isRunning, setIsRunning] = useState(false)
  const [currentRequestId, setCurrentRequestId] = useState<string | null>(null)
  const [inputValue, setInputValue] = useState('')
  const outputEndRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    outputEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [output])

  const executeCommand = useCallback(async (
    command: string,
    subCommand?: string,
    args?: string[]
  ) => {
    setIsRunning(true)
    const cmdLine = `mbe ${command}${subCommand ? ' ' + subCommand : ''}${args ? ' ' + args.join(' ') : ''}`
    setOutput(prev => [...prev, `$ ${cmdLine}`])

    try {
      const requestId = await cliService.execute(
        { command, subCommand, args },
        {
          onOutput: (data) => setOutput(prev => [...prev, data.data]),
          onComplete: (result) => {
            setIsRunning(false)
            setCurrentRequestId(null)
            setOutput(prev => [...prev, `\n[完成,退出码: ${result.exitCode}]`])
          },
          onError: (error) => {
            setIsRunning(false)
            setCurrentRequestId(null)
            setOutput(prev => [...prev, `[错误] ${error}`])
          },
        }
      )
      setCurrentRequestId(requestId)
    } catch (error) {
      setIsRunning(false)
      setOutput(prev => [...prev, `[错误] ${error}`])
    }
  }, [])

  const handleInputSubmit = useCallback(() => {
    if (!inputValue.trim() || isRunning) return
    const parts = inputValue.trim().split(/\s+/)
    // 跳过 "mbe" 前缀(如果有的话)
    const start = parts[0] === 'mbe' ? 1 : 0
    const command = parts[start] || ''
    const subCommand = parts[start + 1]
    const args = parts.slice(start + 2)
    if (command) {
      executeCommand(command, subCommand, args.length > 0 ? args : undefined)
    }
    setInputValue('')
  }, [inputValue, isRunning, executeCommand])

  const handleKill = useCallback(async () => {
    if (currentRequestId) {
      await cliService.kill(currentRequestId)
      setIsRunning(false)
      setCurrentRequestId(null)
    }
  }, [currentRequestId])

  return (
    <div className="cli-terminal bg-gray-900 rounded-lg overflow-hidden shadow-xl">
      <div className="terminal-header bg-gray-800 px-4 py-2 flex justify-between items-center">
        <span className="text-gray-300 font-mono text-sm">MBE CLI (PowerShell)</span>
        <div className="flex gap-2">
          {isRunning && (
            <button
              onClick={handleKill}
              className="text-red-400 hover:text-red-300 text-sm px-2 py-1 border border-red-500 rounded"
            >
              停止
            </button>
          )}
          <button
            onClick={() => setOutput([])}
            className="text-gray-400 hover:text-gray-300 text-sm px-2 py-1"
          >
            清空
          </button>
        </div>
      </div>

      {/* 快捷命令 */}
      <div className="quick-commands bg-gray-850 px-4 py-2 flex flex-wrap gap-2 border-b border-gray-700">
        {QUICK_COMMANDS.map(({ label, command, subCommand, icon }) => (
          <button
            key={`${command}-${subCommand}`}
            onClick={() => executeCommand(command, subCommand)}
            disabled={isRunning}
            className="text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 px-3 py-1 rounded-full disabled:opacity-50"
          >
            {icon} {label}
          </button>
        ))}
      </div>

      {/* 输出区域 */}
      <div className="terminal-output h-96 overflow-y-auto p-4 font-mono text-sm text-green-400">
        {output.map((line, index) => (
          <div key={index} className="terminal-line whitespace-pre-wrap">
            {line}
          </div>
        ))}
        <div ref={outputEndRef} />
      </div>

      {/* 输入区域 */}
      <div className="terminal-input bg-gray-800 px-4 py-2 flex gap-2">
        <span className="text-green-400 font-mono">$</span>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleInputSubmit()}
          placeholder="输入命令,例如: mbe health 或 db status --json"
          disabled={isRunning}
          className="flex-1 bg-transparent text-green-400 font-mono text-sm outline-none placeholder-gray-600"
        />
      </div>
    </div>
  )
}

方案二:直接 API 调用(当前方式)

如果不需要 CLI 的特定功能(如格式化输出、交互式体验),可以直接使用 HTTP API:

// src/services/api.ts
export const api = {
  health: () => axios.get('/api/health'),
  users: () => axios.get('/api/users'),
  experts: () => axios.get('/api/experts'),
  knowledgeBases: () => axios.get('/api/knowledge/bases'),
  // ...
}

方案三:混合模式(推荐生产环境)

结合 CLI JSON 输出和直接 API 调用,选择最优方式:

// src/services/mbeService.ts
class MBEService {
  /**
   * 优先 API 调用,回退到 CLI
   */
  async getHealth(): Promise<HealthData> {
    try {
      // 优先直接 API
      const { data } = await axios.get('/api/health')
      return data
    } catch {
      // 回退到 CLI JSON 输出
      const result = await cliService.query<HealthData>('health')
      if (result) return result
      throw new Error('无法获取健康状态')
    }
  }

  /**
   * CLI 独有功能(无 API 对应)
   */
  async runDiagnose(): Promise<DiagnoseData | null> {
    return await cliService.query('diagnose')
  }

  async getOpsReport(): Promise<OpsReport | null> {
    return await cliService.query('ops', 'report')
  }

  async runMaintenance(type: string, dryRun = true): Promise<string> {
    return await cliService.execute({
      command: 'ops',
      subCommand: 'maintenance',
      args: ['--type', type],
      globalFlags: { dryRun, force: true, json: true }
    })
  }
}

export const mbeService = new MBEService()

使用场景

场景 1:系统状态监控

// JSON 模式获取结构化数据
const healthData = await cliService.query('health')
// 返回: { command: "health", status: "ok", data: {...}, messages: [...] }

场景 2:运维仪表盘

// 获取运维仪表盘数据
const dashboard = await cliService.query('ops', 'dashboard')

场景 3:数据库备份

// 带进度显示的备份
await cliService.execute(
  { command: 'db', subCommand: 'backup', globalFlags: { force: true } },
  {
    onOutput: (data) => {
      // 实时显示备份进度
      updateProgress(data.data)
    },
    onComplete: (result) => {
      showNotification(result.exitCode === 0 ? '备份成功' : '备份失败')
    }
  }
)

场景 4:预览模式操作

// 先预览再执行
const preview = await cliService.query('ops', 'maintenance', ['--type', 'vacuum', '--dry-run'])
if (userConfirms(preview)) {
  await cliService.execute({
    command: 'ops',
    subCommand: 'maintenance',
    args: ['--type', 'vacuum'],
    globalFlags: { force: true }
  })
}

可用的 CLI 命令

完整命令列表请运行 .\mbe.ps1 help

分类 命令 子命令
诊断 health (整体健康检查)
诊断 diagnose (深度诊断)
诊断 doctor (环境检查)
运维 ops dashboard, check, maintenance, report, schedule
监控 monitoring status, prometheus, grafana, alerts, metrics, system, check
日志 logs view, tail, search, analyze, level, stats, export, rotate
数据库 db backup, restore, status, migrate, backup-verify, backup-schedule
部署 deploy status, dev, prod, restart, rollback, services
缓存 cache status, flush
用户 user list, info, stats, freeze, create, update
配置 config show, set, diff, validate, export
订阅 subscription plans, list, stats, changes, coupons, invoices
IoT iot status, terminals, health, config, types
文档 document list, generate, status, templates, industries
集群 cluster moe-stats, moe-health, moe-config, gpu-nodes, gpu-stats, ab-tests, ab-analyze
知识库 kb list, info, search, stats, create, delete, batch-import, batch-upload, quality-check
市场 market stats, experts, toggle, expert-optimize, expert-train, expert-cleanup, expert-test
Worker worker start, stop, status, scale, beat-status, task-result, task-stats, active
自测 self-test run, summary
仓库 repo status, sync, build, info, init

配置要求

1. PowerShell 环境

2. 路径配置

在 Electron 打包时,需要确保:

  • PowerShell 可执行文件在 PATH 中
  • cli/mbe.ps1 脚本路径正确(开发/生产环境不同)
// 开发环境
const mbeScript = path.join(__dirname, '../../cli/mbe.ps1')

// 生产环境(打包后)
const mbeScript = path.join(process.resourcesPath, 'cli/mbe.ps1')

3. 环境变量

CLI 通过 .env 文件和环境变量自动配置连接参数,通常无需额外设置。

4. CLI 配置文件

可通过修改 cli/config/defaults.json 调整 CLI 默认行为:

{
  "rate_limits": { ... },
  "revenue_split": { ... },
  "subscription_plans": { ... },
  "backup": { "retention_count": 10 },
  "schedule": { ... }
}

也可通过环境变量 MBE_DEFAULTS_FILE 指定自定义配置路径。

安全考虑

  1. 输入验证: 验证所有 CLI 命令和参数,防止命令注入
  2. 路径限制: 限制 CLI 可访问的文件系统路径
  3. 权限控制: 根据用户角色限制可执行的 CLI 命令
  4. 输出过滤: 过滤敏感信息(如密码、token)
  5. --force 控制: 破坏性操作需 --force 标志,防止误操作
  6. --dry-run 预览: 支持预览模式,先查看再执行

优势

  1. 复用现有 CLI 代码: 无需重写功能
  2. 统一体验: 桌面应用和命令行工具功能一致
  3. 结构化输出: --json 模式直接返回可解析的 JSON 数据
  4. 审计追踪: 所有操作自动记录审计日志
  5. 无 Python 依赖: PowerShell CLI 在 Windows 上开箱即用
  6. 灵活选择: 前端可以选择 CLI 或直接 API 调用

劣势

  1. 进程开销: 每次命令需启动 PowerShell 进程
  2. 跨平台: macOS/Linux 需额外安装 PowerShell Core
  3. 异步模型: 流式输出需要 IPC 事件管理

实施步骤

  1. ✅ 创建 CLI 管理器类(主进程)
  2. ✅ 添加 IPC 处理器
  3. ✅ 更新 preload.ts 暴露 CLI API
  4. ✅ 创建前端 CLI 服务
  5. ✅ 创建 CLI 终端组件
  6. ✅ 支持 JSON 输出模式
  7. ⬜ 添加错误处理和重试机制
  8. ⬜ 添加命令历史记录
  9. ⬜ 添加命令自动补全
  10. ⬜ 测试和优化

快速开始

1. 确保 PowerShell 可用

# 检查 PowerShell 版本
$PSVersionTable.PSVersion

# 测试 CLI
.\cli\mbe.ps1 version
.\cli\mbe.ps1 health --json

2. 在应用中使用 CLI 终端组件

import { CLITerminal } from '@/components/cli/Terminal'

function MyPage() {
  return (
    <div className="h-screen">
      <CLITerminal />
    </div>
  )
}

3. 在代码中调用 CLI 服务

import { cliService } from '@/services/cliService'

// 流式执行命令
await cliService.execute(
  { command: 'ops', subCommand: 'dashboard' },
  {
    onOutput: (data) => console.log('输出:', data.data),
    onComplete: (result) => console.log('完成:', result.exitCode),
    onError: (error) => console.error('错误:', error),
  }
)

// 便捷 JSON 查询
const health = await cliService.query('health')
console.log('健康状态:', health)

const users = await cliService.query('user', 'list')
console.log('用户:', users)

已实现的功能

  • ✅ 主进程 CLI 管理器(PowerShell 执行器)
  • ✅ IPC 通信(execute / kill / query)
  • ✅ 前端 CLI 服务
  • ✅ CLI 终端组件(带快捷命令和手动输入)
  • ✅ 支持全局标志(--json / --verbose / --quiet / --force / --dry-run / --help)
  • ✅ 实时输出流式传输
  • ✅ JSON 结构化输出支持
  • ✅ 命令终止功能
  • ✅ 错误处理

后续优化

  1. 进程池: 复用 PowerShell 进程降低启动开销
  2. 命令历史: 保存和搜索命令历史
  3. 自动补全: 基于 completion.ps1 的命令补全
  4. 主题支持: 支持深色/浅色终端主题
  5. 性能监控: 追踪 CLI 命令执行耗时