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 配置默认值 |
集成目标
- 在 Electron 主进程中执行 CLI 命令
- 通过 IPC 将 CLI 功能暴露给渲染进程
- 前端可以选择使用 CLI 或直接 API 调用
- 支持 CLI 输出流式传输到前端
- 利用
--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 环境
- Windows: 系统自带 PowerShell 5.1+(默认可用)
- macOS/Linux: 安装 PowerShell Core (pwsh)
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 指定自定义配置路径。
安全考虑
- 输入验证: 验证所有 CLI 命令和参数,防止命令注入
- 路径限制: 限制 CLI 可访问的文件系统路径
- 权限控制: 根据用户角色限制可执行的 CLI 命令
- 输出过滤: 过滤敏感信息(如密码、token)
--force控制: 破坏性操作需--force标志,防止误操作--dry-run预览: 支持预览模式,先查看再执行
优势
- 复用现有 CLI 代码: 无需重写功能
- 统一体验: 桌面应用和命令行工具功能一致
- 结构化输出:
--json模式直接返回可解析的 JSON 数据 - 审计追踪: 所有操作自动记录审计日志
- 无 Python 依赖: PowerShell CLI 在 Windows 上开箱即用
- 灵活选择: 前端可以选择 CLI 或直接 API 调用
劣势
- 进程开销: 每次命令需启动 PowerShell 进程
- 跨平台: macOS/Linux 需额外安装 PowerShell Core
- 异步模型: 流式输出需要 IPC 事件管理
实施步骤
- ✅ 创建 CLI 管理器类(主进程)
- ✅ 添加 IPC 处理器
- ✅ 更新 preload.ts 暴露 CLI API
- ✅ 创建前端 CLI 服务
- ✅ 创建 CLI 终端组件
- ✅ 支持 JSON 输出模式
- ⬜ 添加错误处理和重试机制
- ⬜ 添加命令历史记录
- ⬜ 添加命令自动补全
- ⬜ 测试和优化
快速开始
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 结构化输出支持
- ✅ 命令终止功能
- ✅ 错误处理
后续优化
- 进程池: 复用 PowerShell 进程降低启动开销
- 命令历史: 保存和搜索命令历史
- 自动补全: 基于
completion.ps1的命令补全 - 主题支持: 支持深色/浅色终端主题
- 性能监控: 追踪 CLI 命令执行耗时