Files
DeskClaw/ClawX-项目架构与版本大纲.md
Haze ecb36f0ed8 fix(build): update OpenClaw submodule URL and enhance gateway token management
- Changed the OpenClaw submodule URL to the official GitHub repository for proper integration.
- Implemented automatic gateway token generation on first launch, ensuring seamless authentication for users.
- Updated the gateway manager to utilize the generated token for WebSocket connections.
2026-02-06 05:03:53 +08:00

144 KiB
Raw Blame History

ClawX 项目架构与版本大纲

基于 OpenClaw 的图形化 AI 助手应用
技术栈Electron + React + TypeScript 代码规范:全部英文注释、 开发规范:每一个模块开发完成后,写好完整单测,在{project}/build_process/目录中更新proecess.md, 增加当前feature.md文档(保持commit_X_feat.md格式)提交commit。 图形支持语言与openClaw保持一致 如果有疑问请重新参考当前文档


一、项目概述

1.1 项目定位

ClawX 是 OpenClaw 的图形化封装层,旨在提供:

  • 🎯 零命令行体验 - 通过 GUI 完成所有安装、配置和使用
  • 🎨 现代化 UI - 美观、直观的桌面应用界面
  • 📦 开箱即用 - 预装精选技能包,即刻可用
  • 🖥️ 跨平台 - macOS / Windows / Linux 统一体验
  • 🔄 无缝集成 - 与 OpenClaw 生态完全兼容

1.2 目标用户

用户群体 痛点 ClawX 解决方案
非技术用户 命令行恐惧 可视化安装向导
效率追求者 配置繁琐 一键预设技能包
跨平台用户 体验不一致 统一 UI 设计语言
AI 尝鲜者 门槛高 引导式 onboarding

1.3 与 OpenClaw 的关系

┌─────────────────────────────────────────────────────────┐
│                     ClawX App                           │
│  ┌─────────────────────────────────────────────────┐   │
│  │  Electron Main Process                          │   │
│  │  - 窗口管理、系统托盘、自动更新                    │   │
│  │  - Gateway 进程管理                              │   │
│  │  - Node.js 环境检测/安装                         │   │
│  └─────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────┐   │
│  │  React Renderer Process                         │   │
│  │  - 现代化 UI 界面                                │   │
│  │  - WebSocket 通信层                             │   │
│  └─────────────────────────────────────────────────┘   │
└────────────────────────┬────────────────────────────────┘
                         │ WebSocket (JSON-RPC)
                         ▼
┌─────────────────────────────────────────────────────────┐
│              OpenClaw Gateway (上游)                     │
│  - 消息通道管理 (WhatsApp/Telegram/Discord...)          │
│  - AI Agent 运行时                                      │
│  - 技能/插件系统                                        │
└─────────────────────────────────────────────────────────┘

核心原则

  • 封装而非 Fork - 通过 npm 依赖引入 openclaw
  • 不修改上游 - 所有定制通过配置、插件实现
  • 版本绑定 - 每个 ClawX 版本绑定特定 openclaw 版本
  • CLI 兼容 - 命令行保持 openclaw 命令,不引入 clawx CLI

openclaw project remote: https://github.com/openclaw/openclaw

1.4 CLI 兼容性设计

ClawX 是 OpenClaw 的图形化增强层,而非替代品。用户可以同时使用 GUI 和 CLI

┌─────────────────────────────────────────────────────────────────┐
│                    ClawX + OpenClaw 共存模式                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   用户交互方式                                                   │
│   ┌─────────────────────┐    ┌─────────────────────┐           │
│   │    ClawX GUI        │    │   openclaw CLI      │           │
│   │    (图形界面)        │    │   (命令行)          │           │
│   │                     │    │                     │           │
│   │  • 点击操作          │    │  • openclaw doctor  │           │
│   │  • 可视化配置        │    │  • openclaw plugins │           │
│   │  • 安装向导          │    │  • openclaw config  │           │
│   │  • 普通用户首选      │    │  • 高级用户/脚本    │           │
│   └──────────┬──────────┘    └──────────┬──────────┘           │
│              │                          │                       │
│              └────────────┬─────────────┘                       │
│                           ▼                                     │
│              ┌─────────────────────────┐                       │
│              │   OpenClaw Gateway      │                       │
│              │   (共享同一实例)         │                       │
│              └─────────────────────────┘                       │
│                           │                                     │
│              ┌────────────┴────────────┐                       │
│              ▼                         ▼                       │
│   ┌─────────────────────┐   ┌─────────────────────┐           │
│   │  ~/.openclaw/       │   │  OpenClaw 配置/数据  │           │
│   │  (共享配置目录)      │   │  (技能/插件/会话)    │           │
│   └─────────────────────┘   └─────────────────────┘           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

CLI 兼容性原则

原则 说明
命令一致 使用 openclaw 命令,不引入 clawx CLI
配置共享 GUI 和 CLI 共享 ~/.openclaw/ 配置目录
Gateway 共享 GUI 和 CLI 连接同一个 Gateway 实例
功能互补 GUI 简化常用操作CLI 支持高级/自动化场景

用户使用场景

场景 A: 纯 GUI 用户 (新手)

1. 安装 ClawX.app
2. 通过安装向导完成配置
3. 日常使用 GUI 界面
4. 无需接触命令行

场景 B: GUI + CLI 混合用户 (进阶)

1. 安装 ClawX.app (自动包含 openclaw CLI)
2. 日常使用 GUI 界面
3. 需要时打开终端使用 CLI:
   - openclaw doctor        # 健康检查
   - openclaw plugins list  # 查看插件
   - openclaw config get    # 查看配置

场景 C: CLI 优先用户 (开发者)

1. 安装 ClawX.app 或单独安装 openclaw CLI
2. 主要使用命令行操作
3. 偶尔打开 GUI 查看状态或配置复杂选项

ClawX 安装时的 CLI 处理

// electron/installer/cli-setup.ts

/**
 * ClawX 安装时确保 openclaw CLI 可用
 * 不创建 clawx 命令,保持与上游一致
 */
export async function ensureOpenClawCli(): Promise<void> {
  const isInstalled = await checkCliInstalled('openclaw');
  
  if (!isInstalled) {
    // 通过 npm 全局安装 openclaw
    await privilegeManager.execAsAdmin(
      'npm install -g openclaw',
      { reason: '安装 OpenClaw 命令行工具' }
    );
  }
  
  // 确保 PATH 包含 npm 全局目录
  await pathManager.ensureNpmGlobalPath();
  
  // 验证安装
  const version = await exec('openclaw --version');
  console.log(`OpenClaw CLI installed: ${version}`);
}

/**
 * ClawX 不创建自己的 CLI 命令
 * 所有命令行操作都通过 openclaw 完成
 */
// ❌ 不会有 clawx CLI
// ✅ 只有 openclaw CLI

GUI 与 CLI 的功能映射

操作 ClawX GUI openclaw CLI
健康检查 设置 → 诊断 → 运行检查 openclaw doctor
安装插件 技能市场 → 安装 openclaw plugins install <name>
查看配置 设置 → 高级 openclaw config get
修改配置 设置页面表单 openclaw config set <key> <value>
查看状态 Dashboard openclaw status
查看日志 设置 → 日志 openclaw logs
连接通道 通道 → 添加 → 扫码 openclaw channels add whatsapp
发送消息 对话界面 openclaw message send <target> <msg>

开发者模式: 终端集成

// src/pages/Settings/DeveloperSettings.tsx

export function DeveloperSettings() {
  const openTerminal = async () => {
    // 在内置终端或系统终端中打开,预设好环境
    await window.electron.ipcRenderer.invoke('terminal:open', {
      cwd: '~/.openclaw',
      env: {
        // 确保 openclaw 命令可用
        PATH: `${process.env.PATH}:${npmGlobalBinPath}`,
      },
    });
  };
  
  return (
    <Card>
      <CardHeader>
        <CardTitle>开发者工具</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {/* 快捷命令按钮 */}
        <div className="grid grid-cols-2 gap-2">
          <Button variant="outline" onClick={() => runCommand('openclaw doctor')}>
            <Terminal className="w-4 h-4 mr-2" />
            运行健康检查
          </Button>
          <Button variant="outline" onClick={() => runCommand('openclaw status --all')}>
            <Activity className="w-4 h-4 mr-2" />
            查看完整状态
          </Button>
          <Button variant="outline" onClick={() => runCommand('openclaw plugins list')}>
            <Puzzle className="w-4 h-4 mr-2" />
            列出插件
          </Button>
          <Button variant="outline" onClick={() => runCommand('openclaw config get')}>
            <Settings className="w-4 h-4 mr-2" />
            查看配置
          </Button>
        </div>
        
        <Separator />
        
        {/* 打开终端 */}
        <Button variant="outline" className="w-full" onClick={openTerminal}>
          <Terminal className="w-4 h-4 mr-2" />
          打开终端 (已配置 openclaw 环境)
        </Button>
        
        <p className="text-xs text-muted-foreground">
          ClawX  OpenClaw CLI 完全兼容,您可以在终端中使用 <code>openclaw</code> 命令进行高级操作。
        </p>
      </CardContent>
    </Card>
  );
}

配置同步机制

// electron/config/sync.ts

/**
 * ClawX 和 openclaw CLI 共享同一配置文件
 * 任何一方的修改都会实时反映到另一方
 */
export class ConfigSync {
  private configPath = join(homedir(), '.openclaw', 'config.json');
  private watcher: FSWatcher | null = null;
  
  /**
   * 监听配置文件变化 (CLI 修改时同步到 GUI)
   */
  startWatching(): void {
    this.watcher = watch(this.configPath, async () => {
      const config = await this.readConfig();
      // 通知渲染进程配置已更新
      mainWindow?.webContents.send('config:updated', config);
    });
  }
  
  /**
   * GUI 修改配置时,写入共享配置文件 (CLI 可读取)
   */
  async updateConfig(updates: Partial<OpenClawConfig>): Promise<void> {
    const current = await this.readConfig();
    const merged = deepMerge(current, updates);
    await writeFile(this.configPath, JSON.stringify(merged, null, 2));
  }
  
  /**
   * 配置文件位置说明
   * 
   * ~/.openclaw/
   * ├── config.json      # 主配置 (GUI 和 CLI 共享)
   * ├── credentials/     # 凭证存储
   * ├── sessions/        # 会话数据
   * └── skills/          # 用户技能
   * 
   * ~/.clawx/
   * ├── presets.json     # ClawX 专属配置 (技能包选择等)
   * └── ui-state.json    # GUI 状态 (窗口位置等)
   */
}

二、技术架构

2.1 技术栈选型

层级 技术 版本 选型理由
运行时 Electron 33+ 跨平台、嵌入 Node.js
前端框架 React 19 生态丰富、hooks 模式
UI 组件 shadcn/ui latest 可定制、现代设计
样式 Tailwind CSS 4.x 原子化、快速开发
状态管理 Zustand 5.x 轻量、TypeScript 友好
路由 React Router 7.x 声明式、嵌套路由
构建工具 Vite 6.x 极速 HMR
打包工具 electron-builder latest 多平台打包、自动更新
测试 Vitest + Playwright latest 单元测试 + E2E
语言 TypeScript 5.x 类型安全

2.2 双端口架构与开发者模式

ClawX 采用双端口分层架构,区分普通用户界面与开发者管理后台:

┌─────────────────────────────────────────────────────────────────┐
│                        用户访问入口                              │
├─────────────────────────────┬───────────────────────────────────┤
│                             │                                   │
│   ┌─────────────────────┐   │   ┌─────────────────────────┐    │
│   │   ClawX GUI         │   │   │   OpenClaw Control UI   │    │
│   │   Port: 23333       │   │   │   Port: 18789           │    │
│   │                     │   │   │                         │    │
│   │   🎨 现代化界面      │   │   │   ⚙️ 原生管理后台        │    │
│   │   📦 预装技能包      │──▶│   │   🔧 高级配置           │    │
│   │   🚀 一键安装向导    │   │   │   📊 调试日志           │    │
│   │   👤 普通用户        │   │   │   👨‍💻 开发者/高级用户    │    │
│   └─────────────────────┘   │   └─────────────────────────┘    │
│                             │                                   │
│        [开发者模式] ────────┼──────────────▶                    │
│                             │                                   │
└─────────────────────────────┴───────────────────────────────────┘
                              │
                              │ WebSocket (JSON-RPC)
                              ▼
                 ┌────────────────────────┐
                 │   OpenClaw Gateway     │
                 │   内部服务端口: 18789   │
                 └────────────────────────┘

端口分配

端口 服务 用途 目标用户
23333 ClawX GUI 默认图形化界面 所有用户
18789 OpenClaw Control UI Gateway 管理后台 开发者/高级用户

端口选择说明

  • 23333 - ClawX 专属端口,易记且不与常见服务冲突
  • 18789 - OpenClaw 原生端口,保持上游兼容

开发者模式入口

// src/components/layout/Sidebar.tsx
export function Sidebar() {
  const [devModeClicks, setDevModeClicks] = useState(0);
  
  // 连续点击 5 次版本号解锁开发者模式
  const handleVersionClick = () => {
    const clicks = devModeClicks + 1;
    setDevModeClicks(clicks);
    
    if (clicks >= 5) {
      toast.success('开发者模式已解锁');
      setDevModeClicks(0);
    }
  };
  
  return (
    <aside className="w-64 border-r flex flex-col h-full">
      {/* 导航菜单 */}
      <nav className="flex-1 p-4 space-y-1">
        <NavItem href="/" icon={<Home />} label="概览" />
        <NavItem href="/chat" icon={<MessageSquare />} label="对话" />
        <NavItem href="/channels" icon={<Radio />} label="通道" />
        <NavItem href="/skills" icon={<Puzzle />} label="技能" />
        <NavItem href="/cron" icon={<Clock />} label="定时任务" />
        <NavItem href="/settings" icon={<Settings />} label="设置" />
      </nav>
      
      {/* 底部版本信息 */}
      <footer className="p-4 border-t">
        <button onClick={handleVersionClick} className="text-xs text-muted">
          ClawX v1.0.0
        </button>
        
        {/* 开发者模式入口 - 解锁后显示 */}
        {isDevModeUnlocked && (
          <Button
            variant="ghost"
            size="sm"
            onClick={() => window.electron.openExternal('http://localhost:18789')}
          >
            <Terminal className="w-4 h-4 mr-2" />
            开发者模式
          </Button>
        )}
      </footer>
    </aside>
  );
}

设置页面快捷入口

// src/pages/Settings/AdvancedSettings.tsx
export function AdvancedSettings() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>高级设置</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        {/* 其他设置项 */}
        
        <Separator />
        
        {/* 开发者工具区域 */}
        <div className="space-y-2">
          <Label className="text-sm font-medium">开发者工具</Label>
          <p className="text-xs text-muted-foreground">
            访问 OpenClaw 原生管理后台,进行高级配置和调试
          </p>
          <Button
            variant="outline"
            onClick={() => window.electron.openExternal('http://localhost:18789')}
          >
            <ExternalLink className="w-4 h-4 mr-2" />
            打开 OpenClaw 管理后台
          </Button>
          <p className="text-xs text-muted-foreground">
            将在浏览器中打开 http://localhost:18789
          </p>
        </div>
      </CardContent>
    </Card>
  );
}

配置文件

// electron/utils/config.ts
export const PORTS = {
  /** ClawX 默认 GUI 端口 */
  CLAWX_GUI: 23333,
  
  /** OpenClaw Gateway 端口 (上游默认) */
  OPENCLAW_GATEWAY: 18789,
} as const;

// 环境变量覆盖
export function getPort(key: keyof typeof PORTS): number {
  const envKey = `CLAWX_PORT_${key}`;
  const envValue = process.env[envKey];
  return envValue ? parseInt(envValue, 10) : PORTS[key];
}

使用场景对比

场景 ClawX GUI (23333) OpenClaw 后台 (18789)
日常对话
查看消息记录
添加/管理通道 (简化版) (完整版)
安装技能包
编辑技能配置
查看原始日志
插件管理
JSON 配置编辑
WebSocket 调试
Cron 任务管理

2.3 目录结构

clawx/
├── .github/
│   ├── workflows/
│   │   ├── ci.yml              # 持续集成
│   │   ├── release.yml         # 自动发布
│   │   └── test.yml            # 测试流水线
│   └── ISSUE_TEMPLATE/
│
├── electron/                    # Electron 主进程
│   ├── main/
│   │   ├── index.ts            # 主入口
│   │   ├── window.ts           # 窗口管理
│   │   ├── tray.ts             # 系统托盘
│   │   ├── menu.ts             # 原生菜单
│   │   └── ipc-handlers.ts     # IPC 处理器
│   │
│   ├── updater/                 # 自动更新模块
│   │   ├── index.ts            # 更新管理器入口
│   │   ├── checker.ts          # 版本检查器
│   │   ├── downloader.ts       # 下载管理
│   │   ├── notifier.ts         # 更新通知
│   │   └── channels.ts         # 更新通道配置
│   │
│   ├── gateway/
│   │   ├── manager.ts          # Gateway 进程生命周期
│   │   ├── client.ts           # WebSocket 客户端
│   │   ├── protocol.ts         # JSON-RPC 协议定义
│   │   └── health.ts           # 健康检查
│   │
│   ├── installer/
│   │   ├── node-installer.ts   # Node.js 自动安装
│   │   ├── openclaw-installer.ts # openclaw npm 安装
│   │   ├── skill-installer.ts  # 技能包安装
│   │   └── platform/
│   │       ├── darwin.ts       # macOS 特定逻辑
│   │       ├── win32.ts        # Windows 特定逻辑
│   │       └── linux.ts        # Linux 特定逻辑
│   │
│   ├── privilege/               # 权限提升模块
│   │   ├── index.ts            # 统一权限管理器
│   │   ├── darwin-admin.ts     # macOS 管理员权限 (osascript)
│   │   ├── win32-admin.ts      # Windows UAC 提升
│   │   └── linux-admin.ts      # Linux pkexec/polkit
│   │
│   ├── env-config/              # 环境配置模块
│   │   ├── index.ts            # 环境配置管理器
│   │   ├── path-manager.ts     # PATH 环境变量管理
│   │   ├── api-keys.ts         # API Key 安全存储
│   │   └── shell-profile.ts    # Shell 配置文件管理
│   │
│   ├── preload/
│   │   └── index.ts            # 预加载脚本 (contextBridge)
│   │
│   └── utils/
│       ├── logger.ts           # 日志
│       ├── paths.ts            # 路径管理
│       └── store.ts            # 持久化存储 (electron-store)
│
├── src/                         # React 渲染进程
│   ├── main.tsx                # React 入口
│   ├── App.tsx                 # 根组件
│   │
│   ├── pages/                  # 页面组件
│   │   ├── Dashboard/
│   │   │   ├── index.tsx
│   │   │   ├── StatusCard.tsx
│   │   │   └── QuickActions.tsx
│   │   ├── Chat/
│   │   │   ├── index.tsx
│   │   │   ├── MessageList.tsx
│   │   │   ├── InputArea.tsx
│   │   │   └── ToolCallCard.tsx
│   │   ├── Channels/
│   │   │   ├── index.tsx
│   │   │   ├── ChannelCard.tsx
│   │   │   └── QRScanner.tsx
│   │   ├── Skills/
│   │   │   ├── index.tsx
│   │   │   ├── SkillCard.tsx
│   │   │   ├── SkillMarket.tsx
│   │   │   └── BundleSelector.tsx
  │   │   ├── Cron/                  # 定时任务
│   │   │   ├── index.tsx         # 任务列表页
│   │   │   ├── CronJobCard.tsx   # 任务卡片组件
│   │   │   ├── CronEditor.tsx    # 任务编辑器
│   │   │   ├── CronSchedulePicker.tsx # Cron 表达式选择器
│   │   │   └── CronHistory.tsx   # 执行历史
│   │   ├── Settings/
│   │   │   ├── index.tsx
│   │   │   ├── GeneralSettings.tsx
│   │   │   ├── ProviderSettings.tsx
│   │   │   ├── ChannelsSettings.tsx  # 通道连接配置 (从安装向导移出)
│   │   │   └── AdvancedSettings.tsx
│   │   └── Setup/              # 安装向导 (简化版,不含通道连接)
│   │       ├── index.tsx
│   │       ├── WelcomeStep.tsx
│   │       ├── RuntimeStep.tsx
│   │       ├── ProviderStep.tsx
│   │       └── SkillStep.tsx
│   │
│   ├── components/             # 通用组件
│   │   ├── ui/                 # shadcn/ui 组件
│   │   │   ├── button.tsx
│   │   │   ├── card.tsx
│   │   │   ├── dialog.tsx
│   │   │   └── ...
│   │   ├── layout/
│   │   │   ├── Sidebar.tsx
│   │   │   ├── Header.tsx
│   │   │   └── MainLayout.tsx
│   │   └── common/
│   │       ├── LoadingSpinner.tsx
│   │       ├── ErrorBoundary.tsx
│   │       └── StatusBadge.tsx
│   │
│   ├── hooks/                  # 自定义 Hooks
│   │   ├── useGateway.ts       # Gateway 连接状态
│   │   ├── useChannels.ts      # 通道数据
│   │   ├── useSkills.ts        # 技能数据
│   │   ├── useChat.ts          # 聊天会话
│   │   ├── useCron.ts          # 定时任务数据
│   │   └── useElectron.ts      # IPC 通信
│   │
│   ├── stores/                 # Zustand 状态管理
│   │   ├── gateway.ts          # Gateway 状态
│   │   ├── channels.ts         # 通道状态
│   │   ├── chat.ts             # 聊天状态
│   │   ├── skills.ts           # 技能状态
│   │   ├── cron.ts             # 定时任务状态
│   │   └── settings.ts         # 设置状态
│   │
│   ├── services/               # 服务层
│   │   ├── gateway-rpc.ts      # Gateway RPC 调用封装
│   │   ├── skill-service.ts    # 技能服务
│   │   ├── cron-service.ts     # 定时任务服务
│   │   └── update-service.ts   # 更新服务
│   │
│   ├── types/                  # TypeScript 类型定义
│   │   ├── gateway.ts          # Gateway 协议类型
│   │   ├── channel.ts          # 通道类型
│   │   ├── skill.ts            # 技能类型
│   │   ├── cron.ts             # 定时任务类型
│   │   └── electron.d.ts       # Electron API 类型
│   │
│   ├── utils/                  # 工具函数
│   │   ├── format.ts
│   │   └── platform.ts
│   │
│   └── styles/
│       ├── globals.css         # 全局样式
│       └── themes/             # 主题定义
│           ├── light.css
│           └── dark.css
│
├── resources/                   # 静态资源
│   ├── icons/                  # 应用图标
│   │   ├── icon.icns           # macOS
│   │   ├── icon.ico            # Windows
│   │   └── icon.png            # Linux
│   ├── skills/                 # 预装技能包
│   │   ├── productivity.json
│   │   ├── developer.json
│   │   └── smart-home.json
│   └── locales/                # 国际化 (可选)
│       ├── en.json
│       └── zh-CN.json
│
├── scripts/                     # 构建/工具脚本
│   ├── build.ts                # 构建脚本
│   ├── release.ts              # 发布脚本
│   ├── notarize.ts             # macOS 公证
│   └── dev.ts                  # 开发脚本
│
├── tests/                       # 测试
│   ├── unit/                   # 单元测试
│   ├── integration/            # 集成测试
│   └── e2e/                    # E2E 测试
│       ├── setup.spec.ts       # 安装向导测试
│       ├── chat.spec.ts        # 聊天功能测试
│       └── channels.spec.ts    # 通道配置测试
│
├── .env.example                # 环境变量示例
├── .eslintrc.cjs               # ESLint 配置
├── .prettierrc                 # Prettier 配置
├── electron-builder.yml        # 打包配置
├── package.json
├── tsconfig.json               # TypeScript 配置 (主)
├── tsconfig.node.json          # TypeScript 配置 (Node)
├── vite.config.ts              # Vite 配置
├── tailwind.config.js          # Tailwind 配置
├── CHANGELOG.md
├── LICENSE
└── README.md

2.4 核心模块设计

2.4.1 Gateway 管理器

// electron/gateway/manager.ts
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import WebSocket from 'ws';

export interface GatewayStatus {
  state: 'stopped' | 'starting' | 'running' | 'error';
  port: number;
  pid?: number;
  uptime?: number;
  error?: string;
}

export class GatewayManager extends EventEmitter {
  private process: ChildProcess | null = null;
  private ws: WebSocket | null = null;
  private status: GatewayStatus = { state: 'stopped', port: 18789 };
  
  // 启动 Gateway
  async start(): Promise<void> {
    if (this.status.state === 'running') return;
    
    this.setStatus({ state: 'starting' });
    
    try {
      // 检查已有进程
      const existing = await this.findExisting();
      if (existing) {
        await this.connect(existing.port);
        return;
      }
      
      // 启动新进程
      this.process = spawn('openclaw', [
        'gateway', 'run',
        '--port', String(this.status.port),
        '--force'
      ], {
        stdio: ['ignore', 'pipe', 'pipe'],
        detached: true,
      });
      
      this.process.on('exit', (code) => {
        this.setStatus({ state: 'stopped' });
        this.emit('exit', code);
      });
      
      // 等待就绪并连接
      await this.waitForReady();
      await this.connect(this.status.port);
      
    } catch (error) {
      this.setStatus({ state: 'error', error: String(error) });
      throw error;
    }
  }
  
  // 停止 Gateway
  async stop(): Promise<void> {
    this.ws?.close();
    this.process?.kill();
    this.setStatus({ state: 'stopped' });
  }
  
  // RPC 调用
  async rpc<T>(method: string, params?: unknown): Promise<T> {
    return new Promise((resolve, reject) => {
      if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
        reject(new Error('Gateway not connected'));
        return;
      }
      
      const id = crypto.randomUUID();
      const handler = (data: WebSocket.Data) => {
        const msg = JSON.parse(data.toString());
        if (msg.id === id) {
          this.ws?.off('message', handler);
          if (msg.error) reject(msg.error);
          else resolve(msg.result as T);
        }
      };
      
      this.ws.on('message', handler);
      this.ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
    });
  }
  
  private setStatus(update: Partial<GatewayStatus>): void {
    this.status = { ...this.status, ...update };
    this.emit('status', this.status);
  }
}

2.4.2 安装向导流程

// src/pages/Setup/index.tsx
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

interface SetupStep {
  id: string;
  title: string;
  description: string;
  component: React.ComponentType<StepProps>;
}

const steps: SetupStep[] = [
  {
    id: 'welcome',
    title: '欢迎使用 ClawX',
    description: '您的 AI 助手,即将启程',
    component: WelcomeStep,
  },
  {
    id: 'runtime',
    title: '环境检测',
    description: '检测并安装必要运行环境',
    component: RuntimeStep,
  },
  {
    id: 'provider',
    title: '选择 AI 模型',
    description: '配置您的 AI 服务提供商',
    component: ProviderStep,
  },
  // NOTE: Channel step removed - 通道连接移至 Settings > Channels 页面
  // 用户可在完成初始设置后自行配置消息通道
  // NOTE: Skills selection step removed - 自动安装必要组件
  // 用户无需手动选择,核心组件自动安装
  {
    id: 'installing',
    title: '安装组件',
    description: '正在安装必要的 AI 组件',
    component: InstallingStep,
  },
  {
    id: 'complete',
    title: '设置完成',
    description: '一切就绪,开始使用吧!',
    component: CompleteStep,
  },
];

export function SetupWizard() {
  const [currentStep, setCurrentStep] = useState(0);
  const [setupData, setSetupData] = useState<SetupData>({});
  
  const step = steps[currentStep];
  const StepComponent = step.component;
  
  const handleNext = (data: Partial<SetupData>) => {
    setSetupData({ ...setupData, ...data });
    setCurrentStep((i) => Math.min(i + 1, steps.length - 1));
  };
  
  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
      {/* 进度指示器 */}
      <div className="flex justify-center pt-8">
        {steps.map((s, i) => (
          <div key={s.id} className="flex items-center">
            <div className={cn(
              'w-3 h-3 rounded-full transition-colors',
              i <= currentStep ? 'bg-blue-500' : 'bg-slate-600'
            )} />
            {i < steps.length - 1 && (
              <div className={cn(
                'w-12 h-0.5 transition-colors',
                i < currentStep ? 'bg-blue-500' : 'bg-slate-600'
              )} />
            )}
          </div>
        ))}
      </div>
      
      {/* 步骤内容 */}
      <AnimatePresence mode="wait">
        <motion.div
          key={step.id}
          initial={{ opacity: 0, x: 20 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: -20 }}
          className="max-w-2xl mx-auto p-8"
        >
          <h1 className="text-3xl font-bold text-white mb-2">{step.title}</h1>
          <p className="text-slate-400 mb-8">{step.description}</p>
          
          <StepComponent
            data={setupData}
            onNext={handleNext}
            onBack={() => setCurrentStep((i) => Math.max(i - 1, 0))}
          />
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

2.4.3 预装技能包定义

// resources/skills/bundles.ts
export interface SkillBundle {
  id: string;
  name: string;
  nameZh: string;
  description: string;
  descriptionZh: string;
  icon: string;
  skills: string[];
  recommended?: boolean;
}

export const skillBundles: SkillBundle[] = [
  {
    id: 'productivity',
    name: 'Productivity',
    nameZh: '效率办公',
    description: 'Calendar, reminders, notes, email management',
    descriptionZh: '日历、提醒、笔记、邮件管理',
    icon: '📋',
    skills: [
      'apple-reminders',
      'apple-notes',
      'himalaya',
      'notion',
      'obsidian',
      'trello',
    ],
    recommended: true,
  },
  {
    id: 'developer',
    name: 'Developer Tools',
    nameZh: '开发者工具',
    description: 'GitHub, coding assistant, terminal management',
    descriptionZh: 'GitHub、代码助手、终端管理',
    icon: '💻',
    skills: [
      'github',
      'coding-agent',
      'tmux',
    ],
    recommended: true,
  },
  {
    id: 'smart-home',
    name: 'Smart Home',
    nameZh: '智能家居',
    description: 'Lights, music, device control',
    descriptionZh: '灯光、音乐、设备控制',
    icon: '🏠',
    skills: [
      'openhue',
      'sonoscli',
      'spotify-player',
    ],
  },
  {
    id: 'media',
    name: 'Media & Creative',
    nameZh: '多媒体创作',
    description: 'Image generation, video processing, audio transcription',
    descriptionZh: '图片生成、视频处理、音频转写',
    icon: '🎨',
    skills: [
      'openai-image-gen',
      'nano-banana-pro',
      'video-frames',
      'openai-whisper-api',
    ],
  },
  {
    id: 'communication',
    name: 'Communication',
    nameZh: '通讯增强',
    description: 'Messaging, voice calls, notifications',
    descriptionZh: '消息管理、语音通话、通知',
    icon: '💬',
    skills: [
      'discord',
      'slack',
      'voice-call',
      'imsg',
    ],
  },
  {
    id: 'security',
    name: 'Security & Privacy',
    nameZh: '安全隐私',
    description: 'Password management, secrets',
    descriptionZh: '密码管理、密钥存储',
    icon: '🔐',
    skills: [
      '1password',
    ],
  },
  {
    id: 'information',
    name: 'Information',
    nameZh: '信息获取',
    description: 'Weather, news, web browsing',
    descriptionZh: '天气、新闻、网页浏览',
    icon: '🌐',
    skills: [
      'weather',
      'blogwatcher',
      'summarize',
    ],
  },
];

2.5 默认预装机制设计

ClawX 需要在代码层面实现默认技能默认扩展的预装机制,确保用户开箱即用。

2.5.1 预装配置定义

// electron/presets/defaults.ts

/**
 * ClawX 预装配置
 * 定义首次安装时默认启用的技能和扩展
 */
export interface ClawXPresets {
  /** 默认启用的技能 ID 列表 */
  skills: string[];
  
  /** 默认启用的扩展 ID 列表 */
  extensions: string[];
  
  /** 默认技能包 (用户可在安装向导中选择) */
  defaultBundles: string[];
  
  /** 核心技能 (始终启用,用户不可禁用) */
  coreSkills: string[];
  
  /** 核心扩展 (始终启用,用户不可禁用) */
  coreExtensions: string[];
}

/**
 * ClawX 默认预装配置
 */
export const CLAWX_PRESETS: ClawXPresets = {
  // 默认启用的技能 (首次安装自动启用)
  skills: [
    // Tier 1: 核心体验技能
    'coding-agent',      // 代码助手 (类似 opencode)
    'canvas',            // Canvas UI
    'summarize',         // 内容摘要
    
    // Tier 2: 常用工具技能
    'weather',           // 天气查询
    'github',            // GitHub 集成
    'clawhub',           // 技能市场
  ],
  
  // 默认启用的扩展
  extensions: [
    'lobster',           // UI 美化
    'memory-core',       // 记忆系统
  ],
  
  // 默认推荐的技能包 (安装向导中预选)
  defaultBundles: [
    'productivity',
    'developer',
  ],
  
  // 核心技能 (不可禁用)
  coreSkills: [
    'coding-agent',      // 代码能力是核心体验
  ],
  
  // 核心扩展 (不可禁用)
  coreExtensions: [
    'memory-core',       // 记忆是核心功能
  ],
};

2.5.2 预装加载器

// electron/installer/preset-loader.ts

import { CLAWX_PRESETS } from '../presets/defaults';
import { GatewayManager } from '../gateway/manager';

export interface PresetLoadResult {
  skills: { id: string; status: 'loaded' | 'failed'; error?: string }[];
  extensions: { id: string; status: 'loaded' | 'failed'; error?: string }[];
}

export class PresetLoader {
  constructor(private gateway: GatewayManager) {}
  
  /**
   * 首次安装时加载所有预装项
   */
  async loadInitialPresets(): Promise<PresetLoadResult> {
    const result: PresetLoadResult = { skills: [], extensions: [] };
    
    // 1. 加载核心扩展 (优先级最高)
    for (const extId of CLAWX_PRESETS.coreExtensions) {
      const status = await this.loadExtension(extId, { required: true });
      result.extensions.push({ id: extId, ...status });
    }
    
    // 2. 加载默认扩展
    for (const extId of CLAWX_PRESETS.extensions) {
      if (!CLAWX_PRESETS.coreExtensions.includes(extId)) {
        const status = await this.loadExtension(extId, { required: false });
        result.extensions.push({ id: extId, ...status });
      }
    }
    
    // 3. 加载核心技能
    for (const skillId of CLAWX_PRESETS.coreSkills) {
      const status = await this.loadSkill(skillId, { required: true });
      result.skills.push({ id: skillId, ...status });
    }
    
    // 4. 加载默认技能
    for (const skillId of CLAWX_PRESETS.skills) {
      if (!CLAWX_PRESETS.coreSkills.includes(skillId)) {
        const status = await this.loadSkill(skillId, { required: false });
        result.skills.push({ id: skillId, ...status });
      }
    }
    
    return result;
  }
  
  /**
   * 加载用户选择的技能包
   */
  async loadBundles(bundleIds: string[]): Promise<void> {
    const { skillBundles } = await import('../../resources/skills/bundles');
    
    for (const bundleId of bundleIds) {
      const bundle = skillBundles.find(b => b.id === bundleId);
      if (bundle) {
        for (const skillId of bundle.skills) {
          await this.loadSkill(skillId, { required: false });
        }
      }
    }
  }
  
  private async loadSkill(
    skillId: string, 
    opts: { required: boolean }
  ): Promise<{ status: 'loaded' | 'failed'; error?: string }> {
    try {
      // 通过 Gateway RPC 启用技能
      await this.gateway.rpc('skills.enable', { skillId });
      return { status: 'loaded' };
    } catch (error) {
      if (opts.required) {
        throw new Error(`Failed to load required skill: ${skillId}`);
      }
      return { status: 'failed', error: String(error) };
    }
  }
  
  private async loadExtension(
    extId: string,
    opts: { required: boolean }
  ): Promise<{ status: 'loaded' | 'failed'; error?: string }> {
    try {
      // 通过 Gateway RPC 启用扩展
      await this.gateway.rpc('plugins.enable', { pluginId: extId });
      return { status: 'loaded' };
    } catch (error) {
      if (opts.required) {
        throw new Error(`Failed to load required extension: ${extId}`);
      }
      return { status: 'failed', error: String(error) };
    }
  }
}

2.5.3 预装状态管理

// src/stores/presets.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface PresetState {
  /** 是否已完成首次预装 */
  initialized: boolean;
  
  /** 用户选择的技能包 */
  selectedBundles: string[];
  
  /** 用户额外启用的技能 */
  enabledSkills: string[];
  
  /** 用户禁用的默认技能 (不包括核心技能) */
  disabledSkills: string[];
  
  /** 用户额外启用的扩展 */
  enabledExtensions: string[];
  
  /** 用户禁用的默认扩展 (不包括核心扩展) */
  disabledExtensions: string[];
  
  // Actions
  setInitialized: (value: boolean) => void;
  setSelectedBundles: (bundles: string[]) => void;
  toggleSkill: (skillId: string, enabled: boolean) => void;
  toggleExtension: (extId: string, enabled: boolean) => void;
  
  // Computed
  getEffectiveSkills: () => string[];
  getEffectiveExtensions: () => string[];
}

export const usePresetStore = create<PresetState>()(
  persist(
    (set, get) => ({
      initialized: false,
      selectedBundles: [],
      enabledSkills: [],
      disabledSkills: [],
      enabledExtensions: [],
      disabledExtensions: [],
      
      setInitialized: (value) => set({ initialized: value }),
      
      setSelectedBundles: (bundles) => set({ selectedBundles: bundles }),
      
      toggleSkill: (skillId, enabled) => {
        const { enabledSkills, disabledSkills } = get();
        if (enabled) {
          set({
            enabledSkills: [...enabledSkills, skillId],
            disabledSkills: disabledSkills.filter(id => id !== skillId),
          });
        } else {
          set({
            enabledSkills: enabledSkills.filter(id => id !== skillId),
            disabledSkills: [...disabledSkills, skillId],
          });
        }
      },
      
      toggleExtension: (extId, enabled) => {
        const { enabledExtensions, disabledExtensions } = get();
        if (enabled) {
          set({
            enabledExtensions: [...enabledExtensions, extId],
            disabledExtensions: disabledExtensions.filter(id => id !== extId),
          });
        } else {
          set({
            enabledExtensions: enabledExtensions.filter(id => id !== extId),
            disabledExtensions: [...disabledExtensions, extId],
          });
        }
      },
      
      // 计算实际生效的技能列表
      getEffectiveSkills: () => {
        const { selectedBundles, enabledSkills, disabledSkills } = get();
        const { CLAWX_PRESETS } = require('../../electron/presets/defaults');
        const { skillBundles } = require('../../resources/skills/bundles');
        
        // 1. 核心技能 (始终包含)
        const skills = new Set(CLAWX_PRESETS.coreSkills);
        
        // 2. 默认技能
        CLAWX_PRESETS.skills.forEach((id: string) => skills.add(id));
        
        // 3. 选中的技能包
        for (const bundleId of selectedBundles) {
          const bundle = skillBundles.find((b: any) => b.id === bundleId);
          bundle?.skills.forEach((id: string) => skills.add(id));
        }
        
        // 4. 用户额外启用的
        enabledSkills.forEach(id => skills.add(id));
        
        // 5. 移除用户禁用的 (但不能移除核心技能)
        disabledSkills.forEach(id => {
          if (!CLAWX_PRESETS.coreSkills.includes(id)) {
            skills.delete(id);
          }
        });
        
        return Array.from(skills);
      },
      
      getEffectiveExtensions: () => {
        // 类似逻辑...
        const { enabledExtensions, disabledExtensions } = get();
        const { CLAWX_PRESETS } = require('../../electron/presets/defaults');
        
        const extensions = new Set(CLAWX_PRESETS.coreExtensions);
        CLAWX_PRESETS.extensions.forEach((id: string) => extensions.add(id));
        enabledExtensions.forEach(id => extensions.add(id));
        disabledExtensions.forEach(id => {
          if (!CLAWX_PRESETS.coreExtensions.includes(id)) {
            extensions.delete(id);
          }
        });
        
        return Array.from(extensions);
      },
    }),
    {
      name: 'clawx-presets',
    }
  )
);

2.5.4 安装向导集成

// src/pages/Setup/SkillStep.tsx

import { usePresetStore } from '@/stores/presets';
import { skillBundles } from '@/resources/skills/bundles';
import { CLAWX_PRESETS } from '@electron/presets/defaults';

export function SkillStep({ onNext, onBack }: StepProps) {
  const { selectedBundles, setSelectedBundles } = usePresetStore();
  
  // 默认预选推荐的技能包
  const [selected, setSelected] = useState<string[]>(
    selectedBundles.length > 0 
      ? selectedBundles 
      : CLAWX_PRESETS.defaultBundles
  );
  
  const handleToggle = (bundleId: string) => {
    setSelected(prev => 
      prev.includes(bundleId)
        ? prev.filter(id => id !== bundleId)
        : [...prev, bundleId]
    );
  };
  
  const handleNext = () => {
    setSelectedBundles(selected);
    onNext({ bundles: selected });
  };
  
  return (
    <div className="space-y-6">
      <div className="grid grid-cols-2 gap-4">
        {skillBundles.map(bundle => (
          <Card
            key={bundle.id}
            className={cn(
              'cursor-pointer transition-all',
              selected.includes(bundle.id) && 'ring-2 ring-primary'
            )}
            onClick={() => handleToggle(bundle.id)}
          >
            <CardHeader>
              <div className="flex items-center gap-3">
                <span className="text-3xl">{bundle.icon}</span>
                <div>
                  <CardTitle className="text-lg">{bundle.nameZh}</CardTitle>
                  <CardDescription>{bundle.descriptionZh}</CardDescription>
                </div>
              </div>
            </CardHeader>
            <CardContent>
              <div className="flex flex-wrap gap-1">
                {bundle.skills.slice(0, 4).map(skill => (
                  <Badge key={skill} variant="secondary" className="text-xs">
                    {skill}
                  </Badge>
                ))}
                {bundle.skills.length > 4 && (
                  <Badge variant="outline" className="text-xs">
                    +{bundle.skills.length - 4}
                  </Badge>
                )}
              </div>
            </CardContent>
            {bundle.recommended && (
              <div className="absolute top-2 right-2">
                <Badge>推荐</Badge>
              </div>
            )}
          </Card>
        ))}
      </div>
      
      {/* 核心技能提示 */}
      <Alert>
        <Info className="h-4 w-4" />
        <AlertDescription>
          以下核心技能将始终启用:
          {CLAWX_PRESETS.coreSkills.map(id => (
            <Badge key={id} variant="outline" className="ml-1">{id}</Badge>
          ))}
        </AlertDescription>
      </Alert>
      
      <div className="flex justify-between">
        <Button variant="ghost" onClick={onBack}>上一步</Button>
        <Button onClick={handleNext}>下一步</Button>
      </div>
    </div>
  );
}

2.5.5 预装层级与优先级

┌─────────────────────────────────────────────────────────────────┐
│                     ClawX 预装层级架构                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 0: 核心层 (Core) - 不可禁用                              │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Skills: coding-agent                                   │   │
│  │  Extensions: memory-core                                │   │
│  └─────────────────────────────────────────────────────────┘   │
│                          ▲                                      │
│                          │ 始终启用                              │
│                                                                 │
│  Layer 1: 默认层 (Default) - 可禁用                             │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Skills: canvas, summarize, weather, github, clawhub    │   │
│  │  Extensions: lobster                                    │   │
│  └─────────────────────────────────────────────────────────┘   │
│                          ▲                                      │
│                          │ 首次安装自动启用                       │
│                                                                 │
│  Layer 2: 技能包层 (Bundle) - 用户选择                          │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  productivity: apple-reminders, notion, obsidian...     │   │
│  │  developer: github, coding-agent, tmux...               │   │
│  │  smart-home: openhue, sonoscli, spotify-player...       │   │
│  └─────────────────────────────────────────────────────────┘   │
│                          ▲                                      │
│                          │ 安装向导中选择                        │
│                                                                 │
│  Layer 3: 用户层 (User) - 完全自定义                            │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  用户手动启用/禁用的技能和扩展                            │   │
│  │  (通过设置页面或技能市场)                                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

优先级: Layer 3 > Layer 2 > Layer 1 > Layer 0 (不可覆盖)

2.5.6 配置文件结构

// ~/.clawx/presets.json (用户本地配置)
{
  "version": 1,
  "initialized": true,
  "initializedAt": "2026-02-05T12:00:00Z",
  
  // 用户选择的技能包
  "bundles": ["productivity", "developer"],
  
  // 用户自定义覆盖
  "overrides": {
    "skills": {
      "enabled": ["custom-skill-1"],     // 额外启用
      "disabled": ["weather"]            // 禁用默认
    },
    "extensions": {
      "enabled": ["custom-ext-1"],
      "disabled": []
    }
  },
  
  // 同步到 OpenClaw 的配置 (生成后写入 ~/.openclaw/config.json)
  "syncedConfig": {
    "skills": {
      "enabled": ["coding-agent", "canvas", "github", "notion", "custom-skill-1"]
    },
    "plugins": {
      "entries": {
        "memory-core": { "enabled": true },
        "lobster": { "enabled": true }
      }
    }
  }
}

2.6 跨平台自动更新系统

ClawX 需要实现主动式自动更新机制,在新版本发布后主动通知用户,同时支持用户手动检查更新。

2.6.1 更新策略设计

┌─────────────────────────────────────────────────────────────────┐
│                    ClawX 自动更新流程                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│  │   启动检查    │    │   定时检查    │    │   手动检查    │      │
│  │  (App Start) │    │  (每6小时)    │    │ (用户触发)   │      │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘      │
│         │                   │                   │              │
│         └───────────────────┼───────────────────┘              │
│                             ▼                                   │
│                  ┌─────────────────────┐                       │
│                  │   Version Checker   │                       │
│                  │   检查 GitHub/CDN   │                       │
│                  └──────────┬──────────┘                       │
│                             │                                   │
│              ┌──────────────┴──────────────┐                   │
│              ▼                             ▼                   │
│     ┌────────────────┐           ┌────────────────┐           │
│     │   无新版本      │           │   有新版本      │           │
│     │   静默结束      │           │                │           │
│     └────────────────┘           └───────┬────────┘           │
│                                          │                     │
│                              ┌───────────┴───────────┐        │
│                              ▼                       ▼        │
│                    ┌──────────────┐       ┌──────────────┐    │
│                    │  主动通知弹窗  │       │  静默下载     │    │
│                    │  (询问用户)   │       │  (后台进行)   │    │
│                    └──────┬───────┘       └──────┬───────┘    │
│                           │                      │            │
│              ┌────────────┴────────────┐         │            │
│              ▼                         ▼         │            │
│     ┌────────────────┐      ┌────────────────┐   │            │
│     │   立即更新      │      │   稍后提醒      │   │            │
│     │   (下载安装)    │      │   (记录时间)    │   │            │
│     └───────┬────────┘      └────────────────┘   │            │
│             │                                    │            │
│             └────────────────────────────────────┘            │
│                             │                                  │
│                             ▼                                  │
│                  ┌─────────────────────┐                      │
│                  │   下载完成通知       │                      │
│                  │   "重启以完成更新"   │                      │
│                  └─────────────────────┘                      │
│                                                                │
└─────────────────────────────────────────────────────────────────┘

2.6.2 更新管理器实现

// electron/updater/index.ts

import { autoUpdater, UpdateInfo } from 'electron-updater';
import { app, BrowserWindow, dialog, Notification } from 'electron';
import { EventEmitter } from 'events';
import log from 'electron-log';

export interface UpdateConfig {
  /** 更新通道: stable | beta | dev */
  channel: 'stable' | 'beta' | 'dev';
  
  /** 是否自动下载 */
  autoDownload: boolean;
  
  /** 是否允许降级 */
  allowDowngrade: boolean;
  
  /** 检查间隔 (毫秒) */
  checkInterval: number;
  
  /** 是否显示主动通知 */
  showNotification: boolean;
}

export interface UpdateStatus {
  state: 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'error';
  currentVersion: string;
  latestVersion?: string;
  releaseNotes?: string;
  downloadProgress?: number;
  error?: string;
}

const DEFAULT_CONFIG: UpdateConfig = {
  channel: 'stable',
  autoDownload: false,  // 默认不自动下载,先询问用户
  allowDowngrade: false,
  checkInterval: 6 * 60 * 60 * 1000, // 6小时
  showNotification: true,
};

export class UpdateManager extends EventEmitter {
  private config: UpdateConfig;
  private status: UpdateStatus;
  private checkTimer: NodeJS.Timeout | null = null;
  private mainWindow: BrowserWindow | null = null;
  
  constructor(config: Partial<UpdateConfig> = {}) {
    super();
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.status = {
      state: 'idle',
      currentVersion: app.getVersion(),
    };
    
    this.setupAutoUpdater();
  }
  
  private setupAutoUpdater(): void {
    // 配置 electron-updater
    autoUpdater.logger = log;
    autoUpdater.autoDownload = this.config.autoDownload;
    autoUpdater.allowDowngrade = this.config.allowDowngrade;
    
    // 设置更新通道
    autoUpdater.channel = this.config.channel;
    
    // GitHub Releases 作为更新源
    autoUpdater.setFeedURL({
      provider: 'github',
      owner: 'clawx',
      repo: 'clawx',
    });
    
    // 事件监听
    autoUpdater.on('checking-for-update', () => {
      this.setStatus({ state: 'checking' });
    });
    
    autoUpdater.on('update-available', (info: UpdateInfo) => {
      this.setStatus({
        state: 'available',
        latestVersion: info.version,
        releaseNotes: this.formatReleaseNotes(info.releaseNotes),
      });
      
      // 主动通知用户
      if (this.config.showNotification) {
        this.showUpdateNotification(info);
      }
    });
    
    autoUpdater.on('update-not-available', () => {
      this.setStatus({ state: 'idle' });
    });
    
    autoUpdater.on('download-progress', (progress) => {
      this.setStatus({
        state: 'downloading',
        downloadProgress: Math.round(progress.percent),
      });
    });
    
    autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
      this.setStatus({ state: 'downloaded' });
      this.showDownloadedNotification(info);
    });
    
    autoUpdater.on('error', (error) => {
      this.setStatus({ state: 'error', error: error.message });
    });
  }
  
  /**
   * 初始化更新器 (应用启动时调用)
   */
  initialize(mainWindow: BrowserWindow): void {
    this.mainWindow = mainWindow;
    
    // 启动时检查更新 (延迟5秒避免影响启动速度)
    setTimeout(() => this.checkForUpdates(true), 5000);
    
    // 定时检查
    this.startPeriodicCheck();
  }
  
  /**
   * 启动定时检查
   */
  private startPeriodicCheck(): void {
    if (this.checkTimer) {
      clearInterval(this.checkTimer);
    }
    
    this.checkTimer = setInterval(() => {
      this.checkForUpdates(true);
    }, this.config.checkInterval);
  }
  
  /**
   * 检查更新
   * @param silent 是否静默 (不显示"已是最新版本"提示)
   */
  async checkForUpdates(silent: boolean = false): Promise<void> {
    try {
      const result = await autoUpdater.checkForUpdates();
      
      if (!result?.updateInfo && !silent) {
        // 手动检查且无更新时,显示提示
        this.showNoUpdateDialog();
      }
    } catch (error) {
      if (!silent) {
        this.showErrorDialog(error as Error);
      }
    }
  }
  
  /**
   * 下载更新
   */
  async downloadUpdate(): Promise<void> {
    if (this.status.state !== 'available') return;
    await autoUpdater.downloadUpdate();
  }
  
  /**
   * 安装更新并重启
   */
  quitAndInstall(): void {
    autoUpdater.quitAndInstall(false, true);
  }
  
  /**
   * 显示更新可用通知
   */
  private showUpdateNotification(info: UpdateInfo): void {
    // 系统通知
    if (Notification.isSupported()) {
      const notification = new Notification({
        title: 'ClawX 有新版本可用',
        body: `版本 ${info.version} 已发布,点击查看详情`,
        icon: app.isPackaged 
          ? undefined 
          : 'resources/icons/icon.png',
      });
      
      notification.on('click', () => {
        this.showUpdateDialog(info);
      });
      
      notification.show();
    }
    
    // 同时发送到渲染进程
    this.mainWindow?.webContents.send('update:available', {
      version: info.version,
      releaseNotes: this.formatReleaseNotes(info.releaseNotes),
    });
  }
  
  /**
   * 显示更新对话框 (主动询问用户)
   */
  private async showUpdateDialog(info: UpdateInfo): Promise<void> {
    const releaseNotes = this.formatReleaseNotes(info.releaseNotes);
    
    const result = await dialog.showMessageBox(this.mainWindow!, {
      type: 'info',
      title: '发现新版本',
      message: `ClawX ${info.version} 已发布`,
      detail: `当前版本: ${this.status.currentVersion}\n\n更新内容:\n${releaseNotes}`,
      buttons: ['立即更新', '稍后提醒', '跳过此版本'],
      defaultId: 0,
      cancelId: 1,
    });
    
    switch (result.response) {
      case 0: // 立即更新
        this.downloadUpdate();
        break;
      case 1: // 稍后提醒
        // 30分钟后再次提醒
        setTimeout(() => this.showUpdateNotification(info), 30 * 60 * 1000);
        break;
      case 2: // 跳过此版本
        this.skipVersion(info.version);
        break;
    }
  }
  
  /**
   * 显示下载完成通知
   */
  private showDownloadedNotification(info: UpdateInfo): void {
    if (Notification.isSupported()) {
      const notification = new Notification({
        title: '更新已就绪',
        body: `ClawX ${info.version} 已下载完成,点击重启应用以完成更新`,
      });
      
      notification.on('click', () => {
        this.showRestartDialog();
      });
      
      notification.show();
    }
    
    this.mainWindow?.webContents.send('update:downloaded', {
      version: info.version,
    });
  }
  
  /**
   * 显示重启对话框
   */
  private async showRestartDialog(): Promise<void> {
    const result = await dialog.showMessageBox(this.mainWindow!, {
      type: 'info',
      title: '更新已就绪',
      message: '重启应用以完成更新',
      detail: '更新已下载完成,是否立即重启应用?',
      buttons: ['立即重启', '稍后'],
      defaultId: 0,
    });
    
    if (result.response === 0) {
      this.quitAndInstall();
    }
  }
  
  /**
   * 显示无更新对话框 (手动检查时)
   */
  private showNoUpdateDialog(): void {
    dialog.showMessageBox(this.mainWindow!, {
      type: 'info',
      title: '检查更新',
      message: '已是最新版本',
      detail: `当前版本 ${this.status.currentVersion} 已是最新`,
      buttons: ['确定'],
    });
  }
  
  private showErrorDialog(error: Error): void {
    dialog.showMessageBox(this.mainWindow!, {
      type: 'error',
      title: '更新检查失败',
      message: '无法检查更新',
      detail: error.message,
      buttons: ['确定'],
    });
  }
  
  private formatReleaseNotes(notes: string | any): string {
    if (typeof notes === 'string') return notes;
    if (Array.isArray(notes)) {
      return notes.map(n => n.note || n).join('\n');
    }
    return '';
  }
  
  private skipVersion(version: string): void {
    // 存储跳过的版本
    const store = require('electron-store');
    const settings = new store();
    const skipped = settings.get('update.skippedVersions', []) as string[];
    if (!skipped.includes(version)) {
      settings.set('update.skippedVersions', [...skipped, version]);
    }
  }
  
  private setStatus(update: Partial<UpdateStatus>): void {
    this.status = { ...this.status, ...update };
    this.emit('status', this.status);
    this.mainWindow?.webContents.send('update:status', this.status);
  }
  
  getStatus(): UpdateStatus {
    return this.status;
  }
  
  setConfig(config: Partial<UpdateConfig>): void {
    this.config = { ...this.config, ...config };
    autoUpdater.channel = this.config.channel;
    autoUpdater.autoDownload = this.config.autoDownload;
    
    // 重启定时检查
    this.startPeriodicCheck();
  }
}

// 导出单例
export const updateManager = new UpdateManager();

2.6.3 渲染进程更新 UI

// src/hooks/useUpdate.ts

import { useState, useEffect } from 'react';

interface UpdateStatus {
  state: 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'error';
  currentVersion: string;
  latestVersion?: string;
  releaseNotes?: string;
  downloadProgress?: number;
  error?: string;
}

export function useUpdate() {
  const [status, setStatus] = useState<UpdateStatus>({
    state: 'idle',
    currentVersion: '0.0.0',
  });
  
  useEffect(() => {
    // 监听主进程更新状态
    const handleStatus = (_: any, newStatus: UpdateStatus) => {
      setStatus(newStatus);
    };
    
    window.electron.ipcRenderer.on('update:status', handleStatus);
    
    // 获取初始状态
    window.electron.ipcRenderer.invoke('update:getStatus').then(setStatus);
    
    return () => {
      window.electron.ipcRenderer.off('update:status', handleStatus);
    };
  }, []);
  
  const checkForUpdates = () => {
    window.electron.ipcRenderer.invoke('update:check');
  };
  
  const downloadUpdate = () => {
    window.electron.ipcRenderer.invoke('update:download');
  };
  
  const installUpdate = () => {
    window.electron.ipcRenderer.invoke('update:install');
  };
  
  return {
    status,
    checkForUpdates,
    downloadUpdate,
    installUpdate,
  };
}
// src/components/UpdateNotification.tsx

import { useUpdate } from '@/hooks/useUpdate';
import { motion, AnimatePresence } from 'framer-motion';
import { Download, RefreshCw, X, Loader2 } from 'lucide-react';

export function UpdateNotification() {
  const { status, downloadUpdate, installUpdate } = useUpdate();
  const [dismissed, setDismissed] = useState(false);
  
  // 只在有更新可用或已下载时显示
  const shouldShow = 
    !dismissed && 
    (status.state === 'available' || status.state === 'downloaded');
  
  return (
    <AnimatePresence>
      {shouldShow && (
        <motion.div
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          className="fixed top-4 right-4 z-50 max-w-sm"
        >
          <Card className="border-primary/50 shadow-lg">
            <CardHeader className="pb-2">
              <div className="flex items-center justify-between">
                <CardTitle className="text-sm font-medium flex items-center gap-2">
                  {status.state === 'available' ? (
                    <>
                      <Download className="w-4 h-4 text-primary" />
                      新版本可用
                    </>
                  ) : (
                    <>
                      <RefreshCw className="w-4 h-4 text-green-500" />
                      更新已就绪
                    </>
                  )}
                </CardTitle>
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-6 w-6"
                  onClick={() => setDismissed(true)}
                >
                  <X className="w-4 h-4" />
                </Button>
              </div>
            </CardHeader>
            <CardContent className="pt-0">
              <p className="text-sm text-muted-foreground mb-3">
                ClawX {status.latestVersion} 已发布
              </p>
              
              {status.state === 'downloading' && (
                <div className="mb-3">
                  <Progress value={status.downloadProgress} className="h-2" />
                  <p className="text-xs text-muted-foreground mt-1">
                    下载中 {status.downloadProgress}%
                  </p>
                </div>
              )}
              
              <div className="flex gap-2">
                {status.state === 'available' && (
                  <Button size="sm" onClick={downloadUpdate}>
                    立即下载
                  </Button>
                )}
                {status.state === 'downloaded' && (
                  <Button size="sm" onClick={installUpdate}>
                    重启更新
                  </Button>
                )}
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => setDismissed(true)}
                >
                  稍后
                </Button>
              </div>
            </CardContent>
          </Card>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

2.6.4 设置页面手动检查入口

// src/pages/Settings/GeneralSettings.tsx

import { useUpdate } from '@/hooks/useUpdate';

export function GeneralSettings() {
  const { status, checkForUpdates } = useUpdate();
  
  return (
    <Card>
      <CardHeader>
        <CardTitle>通用设置</CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* 版本与更新 */}
        <div className="space-y-4">
          <Label className="text-sm font-medium">版本与更新</Label>
          
          <div className="flex items-center justify-between p-4 rounded-lg border">
            <div>
              <p className="font-medium">ClawX</p>
              <p className="text-sm text-muted-foreground">
                当前版本: {status.currentVersion}
              </p>
              {status.latestVersion && status.state === 'available' && (
                <p className="text-sm text-primary">
                  新版本可用: {status.latestVersion}
                </p>
              )}
            </div>
            <Button
              variant="outline"
              onClick={checkForUpdates}
              disabled={status.state === 'checking'}
            >
              {status.state === 'checking' ? (
                <>
                  <Loader2 className="w-4 h-4 mr-2 animate-spin" />
                  检查中...
                </>
              ) : (
                '检查更新'
              )}
            </Button>
          </div>
          
          {/* 更新设置 */}
          <div className="space-y-3 pt-2">
            <div className="flex items-center justify-between">
              <div>
                <p className="text-sm font-medium">自动检查更新</p>
                <p className="text-xs text-muted-foreground">
                  启动时和每6小时自动检查
                </p>
              </div>
              <Switch defaultChecked />
            </div>
            
            <div className="flex items-center justify-between">
              <div>
                <p className="text-sm font-medium">自动下载更新</p>
                <p className="text-xs text-muted-foreground">
                  发现新版本后在后台自动下载
                </p>
              </div>
              <Switch />
            </div>
            
            <div className="flex items-center justify-between">
              <div>
                <p className="text-sm font-medium">更新通道</p>
                <p className="text-xs text-muted-foreground">
                  选择接收更新的类型
                </p>
              </div>
              <Select defaultValue="stable">
                <SelectTrigger className="w-32">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="stable">稳定版</SelectItem>
                  <SelectItem value="beta">测试版</SelectItem>
                </SelectContent>
              </Select>
            </div>
          </div>
        </div>
        
        <Separator />
        
        {/* 其他设置... */}
      </CardContent>
    </Card>
  );
}

2.6.5 跨平台配置

// electron/updater/channels.ts

import { Platform } from 'electron-builder';

export interface PlatformUpdateConfig {
  /** 更新源 URL */
  feedUrl: string;
  
  /** 是否支持差量更新 */
  supportsDelta: boolean;
  
  /** 安装方式 */
  installMethod: 'nsis' | 'dmg' | 'appimage' | 'deb' | 'rpm';
}

export const PLATFORM_CONFIGS: Record<NodeJS.Platform, PlatformUpdateConfig> = {
  darwin: {
    feedUrl: 'https://releases.clawx.app/mac',
    supportsDelta: true,  // macOS 支持 Sparkle 差量更新
    installMethod: 'dmg',
  },
  win32: {
    feedUrl: 'https://releases.clawx.app/win',
    supportsDelta: true,  // Windows NSIS 支持差量
    installMethod: 'nsis',
  },
  linux: {
    feedUrl: 'https://releases.clawx.app/linux',
    supportsDelta: false, // AppImage 不支持差量
    installMethod: 'appimage',
  },
} as Record<NodeJS.Platform, PlatformUpdateConfig>;

// electron-builder.yml 配置示例
export const ELECTRON_BUILDER_CONFIG = `
appId: app.clawx.desktop
productName: ClawX
copyright: Copyright © 2026 ClawX

publish:
  - provider: github
    owner: clawx
    repo: clawx
    releaseType: release

mac:
  category: public.app-category.productivity
  target:
    - target: dmg
      arch: [universal]
    - target: zip
      arch: [universal]
  notarize:
    teamId: \${env.APPLE_TEAM_ID}

win:
  target:
    - target: nsis
      arch: [x64, arm64]
  publisherName: ClawX Inc.
  
nsis:
  oneClick: false
  allowToChangeInstallationDirectory: true
  differentialPackage: true  # 启用差量更新

linux:
  target:
    - target: AppImage
      arch: [x64, arm64]
    - target: deb
      arch: [x64]
  category: Utility
`;

2.6.6 更新时间线与用户交互

用户视角的更新流程:

┌─────────────────────────────────────────────────────────────────┐
│ 场景 A: 有新版本时主动通知                                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 用户正在使用 ClawX                                          │
│     │                                                           │
│  2. 系统检测到新版本 (启动时/定时检查)                            │
│     │                                                           │
│  3. 右上角弹出通知卡片 ────────────────────┐                    │
│     │                                    │                     │
│     │  ┌─────────────────────────────┐   │                     │
│     │  │ 🔔 新版本可用                │   │                     │
│     │  │ ClawX 1.2.0 已发布          │   │                     │
│     │  │ [立即下载] [稍后]            │   │                     │
│     │  └─────────────────────────────┘   │                     │
│     │                                    │                     │
│  4. 用户点击"立即下载"                                           │
│     │                                                           │
│  5. 后台下载,通知卡片显示进度                                    │
│     │                                                           │
│  6. 下载完成,通知"重启以完成更新"                                │
│     │                                                           │
│  7. 用户选择"立即重启"或继续工作后重启                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 场景 B: 用户手动检查更新                                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 用户打开 设置 → 通用 → 检查更新                               │
│     │                                                           │
│  2. 显示"检查中..."                                              │
│     │                                                           │
│  3a. 无更新 → 弹窗提示"已是最新版本 (1.1.0)"                      │
│     │                                                           │
│  3b. 有更新 → 弹窗显示更新详情                                    │
│     │                                                           │
│     │  ┌─────────────────────────────────────┐                 │
│     │  │ 发现新版本                           │                 │
│     │  │                                     │                 │
│     │  │ ClawX 1.2.0 已发布                  │                 │
│     │  │ 当前版本: 1.1.0                     │                 │
│     │  │                                     │                 │
│     │  │ 更新内容:                           │                 │
│     │  │ • 新增技能市场                      │                 │
│     │  │ • 修复 Windows 启动问题             │                 │
│     │  │ • 性能优化                          │                 │
│     │  │                                     │                 │
│     │  │ [立即更新] [稍后提醒] [跳过此版本]   │                 │
│     │  └─────────────────────────────────────┘                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.7 GUI 权限提升机制

ClawX 需要在某些操作时获取管理员权限(如全局安装 Node.js、修改系统 PATH必须通过图形化密码弹窗而非命令行 sudo

2.7.1 需要管理员权限的场景

场景 操作 原因
Node.js 安装 写入 /usr/local/bin (macOS/Linux) 系统目录需要 root
全局 npm 安装 npm install -g openclaw 系统 node_modules
PATH 配置 修改 /etc/paths.d/ 系统级环境变量
端口 < 1024 Gateway 使用低端口 特权端口
系统服务注册 LaunchDaemon / systemd 开机自启

2.7.2 跨平台权限提升架构

┌─────────────────────────────────────────────────────────────────┐
│                    ClawX 权限提升流程                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────┐                                          │
│  │  需要管理员权限    │                                          │
│  │  的操作触发       │                                          │
│  └────────┬─────────┘                                          │
│           │                                                     │
│           ▼                                                     │
│  ┌──────────────────┐                                          │
│  │  PrivilegeManager │                                          │
│  │  检测当前平台      │                                          │
│  └────────┬─────────┘                                          │
│           │                                                     │
│     ┌─────┴─────┬─────────────┐                                │
│     ▼           ▼             ▼                                │
│  ┌──────┐   ┌──────┐    ┌──────┐                              │
│  │macOS │   │Windows│    │Linux │                              │
│  └──┬───┘   └──┬───┘    └──┬───┘                              │
│     │          │           │                                    │
│     ▼          ▼           ▼                                    │
│  ┌────────┐ ┌────────┐ ┌────────┐                             │
│  │osascript│ │  UAC   │ │pkexec │                             │
│  │密码弹窗 │ │提升弹窗│ │密码弹窗│                             │
│  └────────┘ └────────┘ └────────┘                             │
│     │          │           │                                    │
│     └──────────┴───────────┘                                   │
│                │                                                │
│                ▼                                                │
│  ┌──────────────────────────┐                                  │
│  │  以管理员身份执行命令      │                                  │
│  │  (无需终端 sudo 输入)     │                                  │
│  └──────────────────────────┘                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.7.3 权限管理器实现

// electron/privilege/index.ts

import { dialog } from 'electron';
import { platform } from 'os';
import { DarwinAdmin } from './darwin-admin';
import { Win32Admin } from './win32-admin';
import { LinuxAdmin } from './linux-admin';

export interface PrivilegeResult {
  success: boolean;
  stdout?: string;
  stderr?: string;
  error?: string;
}

export interface PrivilegeOptions {
  /** 向用户展示的操作说明 */
  reason: string;
  
  /** 图标路径 */
  icon?: string;
  
  /** 超时时间 (毫秒) */
  timeout?: number;
}

/**
 * 跨平台权限提升管理器
 * 通过 GUI 弹窗获取管理员权限,而非命令行 sudo
 */
export class PrivilegeManager {
  private admin: DarwinAdmin | Win32Admin | LinuxAdmin;
  
  constructor() {
    switch (platform()) {
      case 'darwin':
        this.admin = new DarwinAdmin();
        break;
      case 'win32':
        this.admin = new Win32Admin();
        break;
      case 'linux':
        this.admin = new LinuxAdmin();
        break;
      default:
        throw new Error(`Unsupported platform: ${platform()}`);
    }
  }
  
  /**
   * 检查是否已有管理员权限
   */
  async hasAdminPrivilege(): Promise<boolean> {
    return this.admin.hasAdminPrivilege();
  }
  
  /**
   * 以管理员权限执行命令 (会弹出密码框)
   */
  async execAsAdmin(
    command: string,
    options: PrivilegeOptions
  ): Promise<PrivilegeResult> {
    // 先检查是否真的需要提权
    if (await this.hasAdminPrivilege()) {
      // 已经是 admin直接执行
      return this.admin.exec(command);
    }
    
    // 显示确认对话框
    const confirmed = await this.showConfirmDialog(options.reason);
    if (!confirmed) {
      return { success: false, error: 'User cancelled' };
    }
    
    // 通过平台特定方式获取权限并执行
    return this.admin.execWithPrivilege(command, options);
  }
  
  /**
   * 显示权限请求确认对话框
   */
  private async showConfirmDialog(reason: string): Promise<boolean> {
    const result = await dialog.showMessageBox({
      type: 'warning',
      title: '需要管理员权限',
      message: 'ClawX 需要管理员权限来完成此操作',
      detail: `${reason}\n\n点击"继续"后将弹出系统密码输入框。`,
      buttons: ['继续', '取消'],
      defaultId: 0,
      cancelId: 1,
    });
    
    return result.response === 0;
  }
}

export const privilegeManager = new PrivilegeManager();

2.7.4 macOS 实现 (osascript)

// electron/privilege/darwin-admin.ts

import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export class DarwinAdmin {
  /**
   * 检查是否以 root 运行
   */
  async hasAdminPrivilege(): Promise<boolean> {
    try {
      await execAsync('test -w /usr/local/bin');
      return true;
    } catch {
      return false;
    }
  }
  
  /**
   * 直接执行命令
   */
  async exec(command: string): Promise<PrivilegeResult> {
    try {
      const { stdout, stderr } = await execAsync(command);
      return { success: true, stdout, stderr };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  }
  
  /**
   * 通过 osascript 弹出系统密码框执行命令
   * 这会显示 macOS 原生的授权对话框
   */
  async execWithPrivilege(
    command: string,
    options: PrivilegeOptions
  ): Promise<PrivilegeResult> {
    // 使用 osascript 调用 Authorization Services
    // 这会弹出 macOS 原生密码输入框
    const escapedCommand = command.replace(/"/g, '\\"');
    const escapedReason = options.reason.replace(/"/g, '\\"');
    
    const appleScript = `
      do shell script "${escapedCommand}" \\
        with administrator privileges \\
        with prompt "${escapedReason}"
    `;
    
    try {
      const { stdout, stderr } = await execAsync(
        `osascript -e '${appleScript.replace(/'/g, "'\"'\"'")}'`,
        { timeout: options.timeout || 60000 }
      );
      return { success: true, stdout, stderr };
    } catch (error: any) {
      // 用户取消会抛出错误
      if (error.message.includes('User canceled')) {
        return { success: false, error: 'User cancelled' };
      }
      return { success: false, error: error.message };
    }
  }
}

2.7.5 Windows 实现 (UAC)

// electron/privilege/win32-admin.ts

import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import { writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

const execAsync = promisify(exec);

export class Win32Admin {
  /**
   * 检查是否以管理员运行
   */
  async hasAdminPrivilege(): Promise<boolean> {
    try {
      // 尝试写入 System32如果成功说明有管理员权限
      await execAsync('fsutil dirty query %systemdrive%');
      return true;
    } catch {
      return false;
    }
  }
  
  async exec(command: string): Promise<PrivilegeResult> {
    try {
      const { stdout, stderr } = await execAsync(command, { shell: 'powershell.exe' });
      return { success: true, stdout, stderr };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  }
  
  /**
   * 通过 UAC 提升权限执行命令
   * 这会弹出 Windows UAC 确认对话框
   */
  async execWithPrivilege(
    command: string,
    options: PrivilegeOptions
  ): Promise<PrivilegeResult> {
    return new Promise((resolve) => {
      // 创建临时 PowerShell 脚本
      const scriptPath = join(tmpdir(), `clawx-admin-${Date.now()}.ps1`);
      const outputPath = join(tmpdir(), `clawx-admin-${Date.now()}.txt`);
      
      // PowerShell 脚本内容
      const script = `
        try {
          ${command}
          "SUCCESS" | Out-File -FilePath "${outputPath}"
        } catch {
          $_.Exception.Message | Out-File -FilePath "${outputPath}"
          exit 1
        }
      `;
      
      writeFileSync(scriptPath, script, 'utf-8');
      
      // 使用 PowerShell 的 Start-Process 触发 UAC
      const elevateCommand = `
        Start-Process powershell.exe \
          -ArgumentList '-ExecutionPolicy Bypass -File "${scriptPath}"' \
          -Verb RunAs \
          -Wait
      `;
      
      const child = spawn('powershell.exe', ['-Command', elevateCommand], {
        stdio: 'pipe',
      });
      
      child.on('close', (code) => {
        try {
          const output = require('fs').readFileSync(outputPath, 'utf-8').trim();
          unlinkSync(scriptPath);
          unlinkSync(outputPath);
          
          if (output === 'SUCCESS') {
            resolve({ success: true });
          } else {
            resolve({ success: false, error: output });
          }
        } catch (error: any) {
          resolve({ success: false, error: 'UAC cancelled or failed' });
        }
      });
    });
  }
}

2.7.6 Linux 实现 (pkexec/polkit)

// electron/privilege/linux-admin.ts

import { exec, spawn } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export class LinuxAdmin {
  async hasAdminPrivilege(): Promise<boolean> {
    return process.getuid?.() === 0;
  }
  
  async exec(command: string): Promise<PrivilegeResult> {
    try {
      const { stdout, stderr } = await execAsync(command);
      return { success: true, stdout, stderr };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  }
  
  /**
   * 通过 pkexec 弹出图形化密码框
   * pkexec 是 polkit 的一部分,大多数 Linux 桌面都支持
   */
  async execWithPrivilege(
    command: string,
    options: PrivilegeOptions
  ): Promise<PrivilegeResult> {
    return new Promise((resolve) => {
      // 检查 pkexec 是否可用
      exec('which pkexec', (error) => {
        if (error) {
          // 回退到 gksudo 或 kdesudo
          this.execWithFallback(command, options).then(resolve);
          return;
        }
        
        // 使用 pkexec (会弹出 polkit 密码框)
        const child = spawn('pkexec', ['bash', '-c', command], {
          stdio: ['ignore', 'pipe', 'pipe'],
          env: {
            ...process.env,
            // 设置 polkit 显示的描述
            POLKIT_AGENT_HELPER_NAME: 'ClawX',
          },
        });
        
        let stdout = '';
        let stderr = '';
        
        child.stdout.on('data', (data) => { stdout += data; });
        child.stderr.on('data', (data) => { stderr += data; });
        
        child.on('close', (code) => {
          if (code === 0) {
            resolve({ success: true, stdout, stderr });
          } else if (code === 126) {
            // 用户取消
            resolve({ success: false, error: 'User cancelled' });
          } else {
            resolve({ success: false, error: stderr || `Exit code: ${code}` });
          }
        });
      });
    });
  }
  
  /**
   * pkexec 不可用时的回退方案
   */
  private async execWithFallback(
    command: string,
    options: PrivilegeOptions
  ): Promise<PrivilegeResult> {
    // 尝试 gksudo (GNOME) 或 kdesudo (KDE)
    const fallbacks = ['gksudo', 'kdesudo', 'sudo -A'];
    
    for (const fallback of fallbacks) {
      try {
        const { stdout, stderr } = await execAsync(`${fallback} ${command}`);
        return { success: true, stdout, stderr };
      } catch {
        continue;
      }
    }
    
    return { 
      success: false, 
      error: 'No GUI sudo helper available. Please run ClawX as root.' 
    };
  }
}

2.7.7 使用示例

// electron/installer/node-installer.ts

import { privilegeManager } from '../privilege';

export async function installNodeGlobally(): Promise<void> {
  const isNodeInstalled = await checkNodeInstalled();
  
  if (!isNodeInstalled) {
    // 通过 GUI 弹窗获取管理员权限安装 Node.js
    const result = await privilegeManager.execAsAdmin(
      // macOS: 使用 Homebrew
      process.platform === 'darwin'
        ? '/opt/homebrew/bin/brew install node@22'
        // Windows: 使用 winget
        : process.platform === 'win32'
        ? 'winget install OpenJS.NodeJS.LTS'
        // Linux: 使用包管理器
        : 'apt-get install -y nodejs',
      {
        reason: '安装 Node.js 运行时环境,这是 ClawX 运行的必要组件。',
      }
    );
    
    if (!result.success) {
      throw new Error(`Node.js 安装失败: ${result.error}`);
    }
  }
}

2.8 GUI 环境变量配置

ClawX 需要管理多种环境变量API Keys、PATH、代理设置等必须通过图形界面配置,而非要求用户编辑 .env 文件或终端。

2.8.1 环境变量分类

类型 示例 存储位置 安全级别
API Keys ANTHROPIC_API_KEY 系统密钥链 🔐 加密存储
PATH Node.js/npm 路径 Shell Profile 普通
代理 HTTP_PROXY 应用配置 普通
应用配置 CLAWX_PORT 应用配置 普通

2.8.2 环境配置管理器

// electron/env-config/index.ts

import { safeStorage, app } from 'electron';
import Store from 'electron-store';
import { PathManager } from './path-manager';
import { ApiKeyManager } from './api-keys';
import { ShellProfileManager } from './shell-profile';

export interface EnvConfig {
  // API Keys (加密存储)
  apiKeys: {
    anthropic?: string;
    openai?: string;
    google?: string;
    [key: string]: string | undefined;
  };
  
  // 代理设置
  proxy: {
    http?: string;
    https?: string;
    noProxy?: string[];
  };
  
  // 应用配置
  app: {
    port?: number;
    logLevel?: string;
    dataDir?: string;
  };
}

export class EnvConfigManager {
  private store: Store;
  private pathManager: PathManager;
  private apiKeyManager: ApiKeyManager;
  private shellProfileManager: ShellProfileManager;
  
  constructor() {
    this.store = new Store({ name: 'env-config' });
    this.pathManager = new PathManager();
    this.apiKeyManager = new ApiKeyManager();
    this.shellProfileManager = new ShellProfileManager();
  }
  
  /**
   * 获取所有环境配置 (API Keys 脱敏)
   */
  getConfig(): EnvConfig {
    return {
      apiKeys: this.apiKeyManager.getMaskedKeys(),
      proxy: this.store.get('proxy', {}) as EnvConfig['proxy'],
      app: this.store.get('app', {}) as EnvConfig['app'],
    };
  }
  
  /**
   * 设置 API Key (自动加密存储)
   */
  async setApiKey(provider: string, key: string): Promise<void> {
    await this.apiKeyManager.setKey(provider, key);
    
    // 同步到 OpenClaw 配置
    await this.syncToOpenClaw();
  }
  
  /**
   * 设置代理
   */
  setProxy(proxy: EnvConfig['proxy']): void {
    this.store.set('proxy', proxy);
    this.applyProxy(proxy);
  }
  
  /**
   * 确保 PATH 包含必要的路径
   */
  async ensurePath(): Promise<void> {
    const requiredPaths = [
      '/usr/local/bin',
      '/opt/homebrew/bin',        // macOS ARM
      `${app.getPath('home')}/.nvm/versions/node/*/bin`, // nvm
      `${app.getPath('home')}/.local/bin`, // pip install --user
    ];
    
    for (const p of requiredPaths) {
      await this.pathManager.addToPath(p);
    }
  }
  
  /**
   * 同步配置到 OpenClaw
   */
  private async syncToOpenClaw(): Promise<void> {
    const openclawConfigPath = `${app.getPath('home')}/.openclaw/config.json`;
    // 读取现有配置,合并 API keys写回
    // ...
  }
  
  private applyProxy(proxy: EnvConfig['proxy']): void {
    if (proxy.http) process.env.HTTP_PROXY = proxy.http;
    if (proxy.https) process.env.HTTPS_PROXY = proxy.https;
    if (proxy.noProxy) process.env.NO_PROXY = proxy.noProxy.join(',');
  }
}

export const envConfigManager = new EnvConfigManager();

2.8.3 API Key 安全存储

// electron/env-config/api-keys.ts

import { safeStorage } from 'electron';
import Store from 'electron-store';
import keytar from 'keytar';

const SERVICE_NAME = 'ClawX';

/**
 * API Key 管理器
 * 使用系统密钥链安全存储敏感信息
 */
export class ApiKeyManager {
  private store: Store;
  
  constructor() {
    this.store = new Store({ name: 'api-keys-meta' });
  }
  
  /**
   * 安全存储 API Key
   * - macOS: 使用 Keychain
   * - Windows: 使用 Credential Manager
   * - Linux: 使用 libsecret (GNOME Keyring / KWallet)
   */
  async setKey(provider: string, key: string): Promise<void> {
    try {
      // 首选: 系统密钥链 (通过 keytar)
      await keytar.setPassword(SERVICE_NAME, provider, key);
      this.store.set(`providers.${provider}`, { stored: true, method: 'keychain' });
    } catch {
      // 回退: Electron safeStorage (本地加密)
      if (safeStorage.isEncryptionAvailable()) {
        const encrypted = safeStorage.encryptString(key);
        this.store.set(`providers.${provider}`, { 
          stored: true, 
          method: 'safeStorage',
          encrypted: encrypted.toString('base64'),
        });
      } else {
        throw new Error('No secure storage available');
      }
    }
  }
  
  /**
   * 获取 API Key
   */
  async getKey(provider: string): Promise<string | null> {
    const meta = this.store.get(`providers.${provider}`) as any;
    if (!meta?.stored) return null;
    
    if (meta.method === 'keychain') {
      return keytar.getPassword(SERVICE_NAME, provider);
    } else if (meta.method === 'safeStorage' && meta.encrypted) {
      const buffer = Buffer.from(meta.encrypted, 'base64');
      return safeStorage.decryptString(buffer);
    }
    
    return null;
  }
  
  /**
   * 删除 API Key
   */
  async deleteKey(provider: string): Promise<void> {
    const meta = this.store.get(`providers.${provider}`) as any;
    if (!meta?.stored) return;
    
    if (meta.method === 'keychain') {
      await keytar.deletePassword(SERVICE_NAME, provider);
    }
    
    this.store.delete(`providers.${provider}`);
  }
  
  /**
   * 获取所有已配置的 Key (脱敏显示)
   */
  getMaskedKeys(): Record<string, string | undefined> {
    const result: Record<string, string | undefined> = {};
    const providers = this.store.get('providers', {}) as Record<string, any>;
    
    for (const [provider, meta] of Object.entries(providers)) {
      if (meta?.stored) {
        result[provider] = '••••••••••••'; // 脱敏显示
      }
    }
    
    return result;
  }
  
  /**
   * 检查 Key 是否已配置
   */
  hasKey(provider: string): boolean {
    const meta = this.store.get(`providers.${provider}`) as any;
    return meta?.stored === true;
  }
}

2.8.4 PATH 环境变量管理

// electron/env-config/path-manager.ts

import { exec } from 'child_process';
import { promisify } from 'util';
import { existsSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir, platform } from 'os';
import { privilegeManager } from '../privilege';

const execAsync = promisify(exec);

export class PathManager {
  /**
   * 添加路径到 PATH 环境变量
   * 通过 GUI 完成,用户无需手动编辑 shell 配置文件
   */
  async addToPath(newPath: string): Promise<void> {
    // 检查路径是否已存在
    const currentPath = process.env.PATH || '';
    if (currentPath.includes(newPath)) return;
    
    // 检查路径是否有效
    if (!existsSync(newPath) && !newPath.includes('*')) return;
    
    switch (platform()) {
      case 'darwin':
        await this.addToPathMacOS(newPath);
        break;
      case 'win32':
        await this.addToPathWindows(newPath);
        break;
      case 'linux':
        await this.addToPathLinux(newPath);
        break;
    }
  }
  
  /**
   * macOS: 添加到 /etc/paths.d/ (系统级) 或 shell profile (用户级)
   */
  private async addToPathMacOS(newPath: string): Promise<void> {
    // 优先使用用户级配置 (~/.zshrc 或 ~/.bash_profile)
    const shellProfile = this.getShellProfile();
    
    if (shellProfile) {
      const content = existsSync(shellProfile) 
        ? readFileSync(shellProfile, 'utf-8')
        : '';
      
      if (!content.includes(newPath)) {
        const exportLine = `\nexport PATH="${newPath}:$PATH"\n`;
        writeFileSync(shellProfile, content + exportLine);
      }
    } else {
      // 回退到系统级 (需要管理员权限)
      const pathFile = `/etc/paths.d/clawx`;
      await privilegeManager.execAsAdmin(
        `echo "${newPath}" >> ${pathFile}`,
        { reason: '配置系统 PATH 环境变量' }
      );
    }
  }
  
  /**
   * Windows: 修改用户环境变量 (通过注册表)
   */
  private async addToPathWindows(newPath: string): Promise<void> {
    // 使用 PowerShell 修改用户级 PATH (无需管理员权限)
    const command = `
      $currentPath = [Environment]::GetEnvironmentVariable("Path", "User")
      if ($currentPath -notlike "*${newPath}*") {
        [Environment]::SetEnvironmentVariable("Path", "${newPath};$currentPath", "User")
      }
    `;
    
    await execAsync(`powershell -Command "${command}"`);
    
    // 通知系统环境变量已更改
    await execAsync('setx CLAWX_PATH_UPDATED 1');
  }
  
  /**
   * Linux: 添加到 shell profile
   */
  private async addToPathLinux(newPath: string): Promise<void> {
    const shellProfile = this.getShellProfile();
    
    if (shellProfile) {
      const content = existsSync(shellProfile)
        ? readFileSync(shellProfile, 'utf-8')
        : '';
      
      if (!content.includes(newPath)) {
        const exportLine = `\nexport PATH="${newPath}:$PATH"\n`;
        writeFileSync(shellProfile, content + exportLine);
      }
    }
  }
  
  /**
   * 获取当前用户的 shell profile 文件
   */
  private getShellProfile(): string | null {
    const home = homedir();
    const shell = process.env.SHELL || '';
    
    if (shell.includes('zsh')) {
      return join(home, '.zshrc');
    } else if (shell.includes('bash')) {
      const bashProfile = join(home, '.bash_profile');
      const bashrc = join(home, '.bashrc');
      return existsSync(bashProfile) ? bashProfile : bashrc;
    } else if (shell.includes('fish')) {
      return join(home, '.config/fish/config.fish');
    }
    
    // 默认
    return join(home, '.profile');
  }
}

2.8.5 设置页面 UI

// src/pages/Settings/ProviderSettings.tsx

import { useState, useEffect } from 'react';
import { Eye, EyeOff, Check, AlertCircle } from 'lucide-react';

interface ApiKeyInputProps {
  provider: string;
  label: string;
  placeholder: string;
  hasKey: boolean;
  onSave: (key: string) => Promise<void>;
  onDelete: () => Promise<void>;
}

function ApiKeyInput({ provider, label, placeholder, hasKey, onSave, onDelete }: ApiKeyInputProps) {
  const [value, setValue] = useState('');
  const [showKey, setShowKey] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const handleSave = async () => {
    if (!value.trim()) return;
    
    setSaving(true);
    setError(null);
    
    try {
      await onSave(value);
      setValue(''); // 清空输入,显示"已配置"状态
    } catch (e: any) {
      setError(e.message);
    } finally {
      setSaving(false);
    }
  };
  
  return (
    <div className="space-y-2">
      <Label className="text-sm font-medium">{label}</Label>
      
      {hasKey ? (
        // 已配置状态
        <div className="flex items-center gap-2 p-3 rounded-lg border bg-green-50 dark:bg-green-950 border-green-200">
          <Check className="w-4 h-4 text-green-600" />
          <span className="text-sm text-green-700 dark:text-green-400">已配置</span>
          <span className="text-sm text-muted-foreground">••••••••••••</span>
          <div className="flex-1" />
          <Button variant="ghost" size="sm" onClick={onDelete}>
            删除
          </Button>
          <Button variant="ghost" size="sm" onClick={() => setShowKey(true)}>
            重新配置
          </Button>
        </div>
      ) : (
        // 未配置状态
        <div className="flex gap-2">
          <div className="relative flex-1">
            <Input
              type={showKey ? 'text' : 'password'}
              value={value}
              onChange={(e) => setValue(e.target.value)}
              placeholder={placeholder}
              className="pr-10"
            />
            <Button
              type="button"
              variant="ghost"
              size="icon"
              className="absolute right-0 top-0 h-full"
              onClick={() => setShowKey(!showKey)}
            >
              {showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
            </Button>
          </div>
          <Button onClick={handleSave} disabled={saving || !value.trim()}>
            {saving ? '保存中...' : '保存'}
          </Button>
        </div>
      )}
      
      {error && (
        <div className="flex items-center gap-2 text-sm text-red-600">
          <AlertCircle className="w-4 h-4" />
          {error}
        </div>
      )}
      
      <p className="text-xs text-muted-foreground">
        API Key 将安全存储在系统密钥链中,不会以明文保存。
      </p>
    </div>
  );
}

export function ProviderSettings() {
  const [config, setConfig] = useState<any>(null);
  
  useEffect(() => {
    // 从主进程获取配置
    window.electron.ipcRenderer.invoke('env:getConfig').then(setConfig);
  }, []);
  
  const handleSaveApiKey = async (provider: string, key: string) => {
    await window.electron.ipcRenderer.invoke('env:setApiKey', provider, key);
    // 刷新配置
    const newConfig = await window.electron.ipcRenderer.invoke('env:getConfig');
    setConfig(newConfig);
  };
  
  const handleDeleteApiKey = async (provider: string) => {
    await window.electron.ipcRenderer.invoke('env:deleteApiKey', provider);
    const newConfig = await window.electron.ipcRenderer.invoke('env:getConfig');
    setConfig(newConfig);
  };
  
  if (!config) return <LoadingSpinner />;
  
  return (
    <Card>
      <CardHeader>
        <CardTitle>AI 模型配置</CardTitle>
        <CardDescription>
          配置您的 AI 服务提供商 API Key
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-6">
        <ApiKeyInput
          provider="anthropic"
          label="Anthropic (Claude)"
          placeholder="sk-ant-api03-..."
          hasKey={!!config.apiKeys.anthropic}
          onSave={(key) => handleSaveApiKey('anthropic', key)}
          onDelete={() => handleDeleteApiKey('anthropic')}
        />
        
        <Separator />
        
        <ApiKeyInput
          provider="openai"
          label="OpenAI (GPT)"
          placeholder="sk-..."
          hasKey={!!config.apiKeys.openai}
          onSave={(key) => handleSaveApiKey('openai', key)}
          onDelete={() => handleDeleteApiKey('openai')}
        />
        
        <Separator />
        
        <ApiKeyInput
          provider="google"
          label="Google (Gemini)"
          placeholder="AIza..."
          hasKey={!!config.apiKeys.google}
          onSave={(key) => handleSaveApiKey('google', key)}
          onDelete={() => handleDeleteApiKey('google')}
        />
      </CardContent>
    </Card>
  );
}

2.8.6 安装向导集成

// src/pages/Setup/ProviderStep.tsx

export function ProviderStep({ onNext, onBack }: StepProps) {
  const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');
  const [apiKey, setApiKey] = useState('');
  const [validating, setValidating] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const providers = [
    { id: 'anthropic', name: 'Anthropic', model: 'Claude', icon: '🤖' },
    { id: 'openai', name: 'OpenAI', model: 'GPT-4', icon: '💚' },
    { id: 'google', name: 'Google', model: 'Gemini', icon: '🔷' },
  ];
  
  const handleNext = async () => {
    if (!apiKey.trim()) {
      setError('请输入 API Key');
      return;
    }
    
    setValidating(true);
    setError(null);
    
    try {
      // 验证 API Key 是否有效
      const valid = await window.electron.ipcRenderer.invoke(
        'provider:validateKey',
        selectedProvider,
        apiKey
      );
      
      if (!valid) {
        setError('API Key 无效,请检查后重试');
        return;
      }
      
      // 安全存储 API Key
      await window.electron.ipcRenderer.invoke(
        'env:setApiKey',
        selectedProvider,
        apiKey
      );
      
      onNext({ provider: selectedProvider });
    } catch (e: any) {
      setError(e.message);
    } finally {
      setValidating(false);
    }
  };
  
  return (
    <div className="space-y-6">
      <div className="space-y-4">
        <Label>选择 AI 服务提供商</Label>
        <div className="grid grid-cols-3 gap-4">
          {providers.map((p) => (
            <Card
              key={p.id}
              className={cn(
                'cursor-pointer transition-all p-4',
                selectedProvider === p.id && 'ring-2 ring-primary'
              )}
              onClick={() => setSelectedProvider(p.id)}
            >
              <div className="text-center">
                <span className="text-3xl">{p.icon}</span>
                <p className="font-medium mt-2">{p.name}</p>
                <p className="text-sm text-muted-foreground">{p.model}</p>
              </div>
            </Card>
          ))}
        </div>
      </div>
      
      <div className="space-y-2">
        <Label>API Key</Label>
        <Input
          type="password"
          value={apiKey}
          onChange={(e) => setApiKey(e.target.value)}
          placeholder={`输入您的 ${providers.find(p => p.id === selectedProvider)?.name} API Key`}
        />
        {error && (
          <p className="text-sm text-red-600">{error}</p>
        )}
        <p className="text-xs text-muted-foreground">
          您的 API Key 将安全存储在系统密钥链中,不会上传到任何服务器。
        </p>
      </div>
      
      <div className="flex justify-between">
        <Button variant="ghost" onClick={onBack}>上一步</Button>
        <Button onClick={handleNext} disabled={validating}>
          {validating ? '验证中...' : '下一步'}
        </Button>
      </div>
    </div>
  );
}

三、UI/UX 设计规范

3.1 设计原则

原则 描述
简洁优先 隐藏复杂性,暴露必要功能
渐进式披露 基础功能易达,高级功能可探索
一致性 跨页面/组件保持视觉和交互一致
响应式反馈 所有操作有即时视觉反馈
原生感 遵循各平台设计语言

3.2 色彩系统

/* src/styles/themes/tokens.css */
:root {
  /* Primary */
  --color-primary-50: #eff6ff;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  
  /* Semantic */
  --color-success: #22c55e;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  --color-info: #3b82f6;
  
  /* Neutral (Light) */
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f8fafc;
  --color-text-primary: #0f172a;
  --color-text-secondary: #64748b;
  --color-border: #e2e8f0;
}

[data-theme="dark"] {
  --color-bg-primary: #0f172a;
  --color-bg-secondary: #1e293b;
  --color-text-primary: #f8fafc;
  --color-text-secondary: #94a3b8;
  --color-border: #334155;
}

3.3 核心页面线框图

┌─────────────────────────────────────────────────────────┐
│  ClawX                              ─ □ ×  │
├──────────┬──────────────────────────────────────────────┤
│          │                                              │
│  ● 概览  │  ┌─────────────┐  ┌─────────────┐           │
│  ○ 对话  │  │   Gateway   │  │   Channels  │           │
│  ○ 通道  │  │   ● 运行中   │  │   3 已连接   │           │
│  ○ 技能  │  └─────────────┘  └─────────────┘           │
│  ○ 设置  │                                              │
│ ○ 定时任务│  ┌───────────────────────────────────────┐   │
│          │  │            最近对话                    │   │
│          │  ├───────────────────────────────────────┤   │
│          │  │ 📱 WhatsApp · 用户A · 2分钟前          │   │
│          │  │ 💬 Telegram · 用户B · 15分钟前         │   │
│          │  │ 💬 Discord · 用户C · 1小时前           │   │
│          │  └───────────────────────────────────────┘   │
│          │                                              │
│          │  ┌───────────────────────────────────────┐   │
│          │  │          快捷操作                      │   │
│          │  │  [添加通道]  [浏览技能]  [查看日志]     │   │
│          │  └───────────────────────────────────────┘   │
│          │                                              │
├──────────┴──────────────────────────────────────────────┤
│  Gateway: 运行中 · 3 通道 · 12 技能 · v2026.2.3        │
└─────────────────────────────────────────────────────────┘

3.4 定时任务页面设计

┌─────────────────────────────────────────────────────────┐
│  ClawX                              ─ □ ×  │
├──────────┬──────────────────────────────────────────────┤
│          │                                              │
│  ○ 概览  │  定时任务                    [+ 新建任务]    │
│  ○ 对话  │                                              │
│  ○ 通道  │  ┌───────────────────────────────────────┐   │
│  ○ 技能  │  │ 📋 每日天气播报              ● 已启用  │   │
│  ● 定时  │  │ ⏰ 每天 08:00                          │   │
│  ○ 设置  │  │ 📍 发送到: WhatsApp - 家庭群           │   │
│          │  │ 上次执行: 今天 08:00 ✓                 │   │
│          │  │                    [编辑] [暂停] [删除] │   │
│          │  └───────────────────────────────────────┘   │
│          │                                              │
│          │  ┌───────────────────────────────────────┐   │
│          │  │ 📊 周报汇总                   ○ 已暂停  │   │
│          │  │ ⏰ 每周五 18:00                        │   │
│          │  │ 📍 发送到: Telegram - 工作频道         │   │
│          │  │ 上次执行: 上周五 18:00 ✓               │   │
│          │  │                    [编辑] [启用] [删除] │   │
│          │  └───────────────────────────────────────┘   │
│          │                                              │
│          │  ┌───────────────────────────────────────┐   │
│          │  │ 🔔 服务器健康检查             ● 已启用  │   │
│          │  │ ⏰ 每 30 分钟                          │   │
│          │  │ 📍 发送到: Discord - 运维通知          │   │
│          │  │ 上次执行: 10分钟前 ✓                   │   │
│          │  │                    [编辑] [暂停] [删除] │   │
│          │  └───────────────────────────────────────┘   │
│          │                                              │
├──────────┴──────────────────────────────────────────────┤
│  3 个任务 · 2 个运行中 · 1 个暂停                       │
└─────────────────────────────────────────────────────────┘

定时任务编辑器弹窗

┌─────────────────────────────────────────────────────────┐
│  新建定时任务                                    [×]   │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  任务名称                                               │
│  ┌─────────────────────────────────────────────────┐   │
│  │ 每日天气播报                                     │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  执行内容 (发送给 AI 的消息)                            │
│  ┌─────────────────────────────────────────────────┐   │
│  │ 请查询北京今天的天气,并生成一条适合发送到群里   │   │
│  │ 的天气播报消息,包含穿衣建议。                   │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  执行时间                                               │
│  ┌──────────────┐  ┌────────────────────────────┐     │
│  │ ○ 每天       │  │ 08 : 00                    │     │
│  │ ○ 每周       │  └────────────────────────────┘     │
│  │ ○ 每月       │                                     │
│  │ ○ 自定义Cron │  Cron: 0 8 * * *                   │
│  └──────────────┘                                      │
│                                                         │
│  发送到                                                 │
│  ┌─────────────────────────────────────────────────┐   │
│  │ WhatsApp - 家庭群                           ▼   │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  ☑ 启用任务                                            │
│                                                         │
│                              [取消]    [保存]          │
└─────────────────────────────────────────────────────────┘

定时任务类型定义

// src/types/cron.ts

export interface CronJob {
  /** 任务 ID */
  id: string;
  
  /** 任务名称 */
  name: string;
  
  /** 发送给 AI 的消息内容 */
  message: string;
  
  /** Cron 表达式 */
  schedule: string;
  
  /** 目标通道 */
  target: {
    channelType: 'whatsapp' | 'telegram' | 'discord' | 'slack';
    channelId: string;
    channelName: string;
  };
  
  /** 是否启用 */
  enabled: boolean;
  
  /** 创建时间 */
  createdAt: string;
  
  /** 更新时间 */
  updatedAt: string;
  
  /** 上次执行信息 */
  lastRun?: {
    time: string;
    success: boolean;
    error?: string;
  };
  
  /** 下次执行时间 */
  nextRun?: string;
}

export interface CronJobCreateInput {
  name: string;
  message: string;
  schedule: string;
  target: CronJob['target'];
  enabled?: boolean;
}

export interface CronJobUpdateInput {
  name?: string;
  message?: string;
  schedule?: string;
  target?: CronJob['target'];
  enabled?: boolean;
}

定时任务 Hook

// src/hooks/useCron.ts

import { useState, useEffect, useCallback } from 'react';
import type { CronJob, CronJobCreateInput, CronJobUpdateInput } from '@/types/cron';

export function useCron() {
  const [jobs, setJobs] = useState<CronJob[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  // 加载任务列表
  const fetchJobs = useCallback(async () => {
    try {
      setLoading(true);
      const result = await window.electron.ipcRenderer.invoke('cron:list');
      setJobs(result);
      setError(null);
    } catch (e: any) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }, []);
  
  useEffect(() => {
    fetchJobs();
    
    // 监听任务更新事件
    const handleUpdate = (_: any, updatedJobs: CronJob[]) => {
      setJobs(updatedJobs);
    };
    
    window.electron.ipcRenderer.on('cron:updated', handleUpdate);
    
    return () => {
      window.electron.ipcRenderer.off('cron:updated', handleUpdate);
    };
  }, [fetchJobs]);
  
  // 创建任务
  const createJob = async (input: CronJobCreateInput): Promise<CronJob> => {
    const job = await window.electron.ipcRenderer.invoke('cron:create', input);
    await fetchJobs();
    return job;
  };
  
  // 更新任务
  const updateJob = async (id: string, input: CronJobUpdateInput): Promise<void> => {
    await window.electron.ipcRenderer.invoke('cron:update', id, input);
    await fetchJobs();
  };
  
  // 删除任务
  const deleteJob = async (id: string): Promise<void> => {
    await window.electron.ipcRenderer.invoke('cron:delete', id);
    await fetchJobs();
  };
  
  // 启用/禁用任务
  const toggleJob = async (id: string, enabled: boolean): Promise<void> => {
    await window.electron.ipcRenderer.invoke('cron:toggle', id, enabled);
    await fetchJobs();
  };
  
  // 手动触发任务
  const triggerJob = async (id: string): Promise<void> => {
    await window.electron.ipcRenderer.invoke('cron:trigger', id);
  };
  
  return {
    jobs,
    loading,
    error,
    createJob,
    updateJob,
    deleteJob,
    toggleJob,
    triggerJob,
    refresh: fetchJobs,
  };
}

定时任务页面组件

// src/pages/Cron/index.tsx

import { useState } from 'react';
import { useCron } from '@/hooks/useCron';
import { CronJobCard } from './CronJobCard';
import { CronEditor } from './CronEditor';
import { Plus, Clock } from 'lucide-react';

export function CronPage() {
  const { jobs, loading, createJob, updateJob, deleteJob, toggleJob } = useCron();
  const [editorOpen, setEditorOpen] = useState(false);
  const [editingJob, setEditingJob] = useState<CronJob | null>(null);
  
  const activeJobs = jobs.filter(j => j.enabled);
  const pausedJobs = jobs.filter(j => !j.enabled);
  
  const handleCreate = () => {
    setEditingJob(null);
    setEditorOpen(true);
  };
  
  const handleEdit = (job: CronJob) => {
    setEditingJob(job);
    setEditorOpen(true);
  };
  
  const handleSave = async (input: CronJobCreateInput) => {
    if (editingJob) {
      await updateJob(editingJob.id, input);
    } else {
      await createJob(input);
    }
    setEditorOpen(false);
  };
  
  if (loading) return <LoadingSpinner />;
  
  return (
    <div className="p-6 space-y-6">
      {/* 页头 */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold">定时任务</h1>
          <p className="text-muted-foreground">
            设置自动执行的 AI 任务,按计划发送消息
          </p>
        </div>
        <Button onClick={handleCreate}>
          <Plus className="w-4 h-4 mr-2" />
          新建任务
        </Button>
      </div>
      
      {/* 统计卡片 */}
      <div className="grid grid-cols-3 gap-4">
        <Card>
          <CardContent className="pt-6">
            <div className="flex items-center gap-4">
              <div className="p-3 rounded-full bg-primary/10">
                <Clock className="w-6 h-6 text-primary" />
              </div>
              <div>
                <p className="text-2xl font-bold">{jobs.length}</p>
                <p className="text-sm text-muted-foreground">总任务数</p>
              </div>
            </div>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="pt-6">
            <div className="flex items-center gap-4">
              <div className="p-3 rounded-full bg-green-100">
                <Play className="w-6 h-6 text-green-600" />
              </div>
              <div>
                <p className="text-2xl font-bold">{activeJobs.length}</p>
                <p className="text-sm text-muted-foreground">运行中</p>
              </div>
            </div>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="pt-6">
            <div className="flex items-center gap-4">
              <div className="p-3 rounded-full bg-yellow-100">
                <Pause className="w-6 h-6 text-yellow-600" />
              </div>
              <div>
                <p className="text-2xl font-bold">{pausedJobs.length}</p>
                <p className="text-sm text-muted-foreground">已暂停</p>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
      
      {/* 任务列表 */}
      {jobs.length === 0 ? (
        <Card className="p-12 text-center">
          <Clock className="w-12 h-12 mx-auto text-muted-foreground mb-4" />
          <h3 className="text-lg font-medium mb-2">暂无定时任务</h3>
          <p className="text-muted-foreground mb-4">
            创建您的第一个定时任务,让 AI 自动为您工作
          </p>
          <Button onClick={handleCreate}>
            <Plus className="w-4 h-4 mr-2" />
            新建任务
          </Button>
        </Card>
      ) : (
        <div className="space-y-4">
          {jobs.map(job => (
            <CronJobCard
              key={job.id}
              job={job}
              onEdit={() => handleEdit(job)}
              onDelete={() => deleteJob(job.id)}
              onToggle={(enabled) => toggleJob(job.id, enabled)}
            />
          ))}
        </div>
      )}
      
      {/* 编辑器弹窗 */}
      <CronEditor
        open={editorOpen}
        onClose={() => setEditorOpen(false)}
        onSave={handleSave}
        initialData={editingJob}
      />
    </div>
  );
}

Cron 表达式选择器

// src/pages/Cron/CronSchedulePicker.tsx

import { useState, useEffect } from 'react';

interface CronSchedulePickerProps {
  value: string;
  onChange: (cron: string) => void;
}

type ScheduleType = 'daily' | 'weekly' | 'monthly' | 'interval' | 'custom';

export function CronSchedulePicker({ value, onChange }: CronSchedulePickerProps) {
  const [type, setType] = useState<ScheduleType>('daily');
  const [time, setTime] = useState('08:00');
  const [weekday, setWeekday] = useState(1); // 周一
  const [monthday, setMonthday] = useState(1);
  const [interval, setInterval] = useState(30); // 分钟
  const [customCron, setCustomCron] = useState(value);
  
  // 解析现有 cron 表达式
  useEffect(() => {
    // 简单解析,实际可用 cron-parser 库
    if (value.match(/^\d+ \d+ \* \* \*$/)) {
      setType('daily');
      const [min, hour] = value.split(' ');
      setTime(`${hour.padStart(2, '0')}:${min.padStart(2, '0')}`);
    }
    // ... 其他模式解析
  }, []);
  
  // 生成 cron 表达式
  useEffect(() => {
    let cron = '';
    const [hour, min] = time.split(':');
    
    switch (type) {
      case 'daily':
        cron = `${parseInt(min)} ${parseInt(hour)} * * *`;
        break;
      case 'weekly':
        cron = `${parseInt(min)} ${parseInt(hour)} * * ${weekday}`;
        break;
      case 'monthly':
        cron = `${parseInt(min)} ${parseInt(hour)} ${monthday} * *`;
        break;
      case 'interval':
        cron = `*/${interval} * * * *`;
        break;
      case 'custom':
        cron = customCron;
        break;
    }
    
    if (cron !== value) {
      onChange(cron);
    }
  }, [type, time, weekday, monthday, interval, customCron]);
  
  const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
  
  return (
    <div className="space-y-4">
      {/* 类型选择 */}
      <RadioGroup value={type} onValueChange={(v) => setType(v as ScheduleType)}>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="daily" id="daily" />
          <Label htmlFor="daily">每天</Label>
        </div>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="weekly" id="weekly" />
          <Label htmlFor="weekly">每周</Label>
        </div>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="monthly" id="monthly" />
          <Label htmlFor="monthly">每月</Label>
        </div>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="interval" id="interval" />
          <Label htmlFor="interval">间隔执行</Label>
        </div>
        <div className="flex items-center space-x-2">
          <RadioGroupItem value="custom" id="custom" />
          <Label htmlFor="custom">自定义 Cron</Label>
        </div>
      </RadioGroup>
      
      {/* 时间选择 */}
      {(type === 'daily' || type === 'weekly' || type === 'monthly') && (
        <div className="flex items-center gap-2">
          <Label>执行时间:</Label>
          <Input
            type="time"
            value={time}
            onChange={(e) => setTime(e.target.value)}
            className="w-32"
          />
        </div>
      )}
      
      {/* 周几选择 */}
      {type === 'weekly' && (
        <div className="flex items-center gap-2">
          <Label>星期:</Label>
          <Select value={String(weekday)} onValueChange={(v) => setWeekday(parseInt(v))}>
            <SelectTrigger className="w-32">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {weekdays.map((day, i) => (
                <SelectItem key={i} value={String(i)}>{day}</SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      )}
      
      {/* 日期选择 */}
      {type === 'monthly' && (
        <div className="flex items-center gap-2">
          <Label>日期:</Label>
          <Select value={String(monthday)} onValueChange={(v) => setMonthday(parseInt(v))}>
            <SelectTrigger className="w-32">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
                <SelectItem key={day} value={String(day)}>{day} </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      )}
      
      {/* 间隔选择 */}
      {type === 'interval' && (
        <div className="flex items-center gap-2">
          <Label>每隔:</Label>
          <Input
            type="number"
            min={1}
            max={1440}
            value={interval}
            onChange={(e) => setInterval(parseInt(e.target.value))}
            className="w-20"
          />
          <span>分钟</span>
        </div>
      )}
      
      {/* 自定义 Cron */}
      {type === 'custom' && (
        <div className="space-y-2">
          <Input
            value={customCron}
            onChange={(e) => setCustomCron(e.target.value)}
            placeholder="分 时 日 月 周 (如: 0 8 * * 1-5)"
          />
          <p className="text-xs text-muted-foreground">
            Cron 表达式格式: 分钟(0-59) 小时(0-23) (1-31) (1-12) (0-6)
          </p>
        </div>
      )}
      
      {/* 预览 */}
      <div className="p-3 rounded-lg bg-muted">
        <p className="text-sm">
          <span className="text-muted-foreground">Cron 表达式: </span>
          <code className="font-mono">{value}</code>
        </p>
        <p className="text-sm">
          <span className="text-muted-foreground">下次执行: </span>
          {getNextRunTime(value)}
        </p>
      </div>
    </div>
  );
}

function getNextRunTime(cron: string): string {
  // 使用 croner 或类似库计算下次执行时间
  // 这里简化处理
  return '今天 08:00';
}

四、版本规划

4.1 版本号策略

ClawX 版本: X.Y.Z[-prerelease]
            │ │ │
            │ │ └── Patch: Bug 修复、性能优化
            │ └──── Minor: 新功能、新技能包、UI 改进
            └────── Major: 重大变更、不兼容更新

示例:
- 1.0.0        首个稳定版
- 1.1.0        新增技能市场功能
- 1.1.1        修复 Windows 安装问题
- 2.0.0        UI 重构、新架构
- 1.2.0-beta.1 下一版本测试版

4.2 OpenClaw 兼容性矩阵

ClawX 版本 OpenClaw 版本 Node.js 版本 备注
1.0.x 2026.2.x 22.x 首发版本
1.1.x 2026.3.x 22.x 功能增强
2.0.x 2026.6.x 24.x 大版本升级

4.3 里程碑规划

🚀 v0.1.0 - Alpha (内部测试)

目标: 核心架构验证

任务 优先级 状态
Electron + React 项目骨架 P0
Gateway 进程管理 P0
基础 UI 框架 (侧边栏/布局) P0
WebSocket 通信层 P0
状态管理 (Zustand) P1

交付物:

  • 可运行的桌面应用
  • 能启动/停止 Gateway
  • 基础 Dashboard 页面

🎯 v0.5.0 - Beta (公开测试)

目标: 安装向导 MVP

任务 优先级 状态
安装向导 UI P0
Node.js 自动检测/安装 P0
openclaw npm 安装 P0
Provider 配置 (API Key) P0
错误处理与提示 P1

注意: 通道连接功能 (WhatsApp/Telegram 等) 已从安装向导移至 Settings > Channels 页面。 用户可在完成初始设置后,根据需要自行配置消息通道,降低首次使用门槛。

交付物:

  • 简化版安装向导流程 (不含通道连接)
  • 支持 macOS (Apple Silicon + Intel)
  • 可配置 Anthropic/OpenAI/OpenRouter

📦 v1.0.0 - Stable (首个正式版)

目标: 生产可用

任务 优先级 状态
完整 Dashboard P0
通道管理页面 P0
对话界面 P0
技能浏览/启用 P0
设置页面 P0
预装技能包选择 P1
自动更新 (Sparkle/electron-updater) P1
Windows 支持 P1
深色模式 P2
崩溃报告 P2

交付物:

  • macOS + Windows 安装包
  • 自动更新能力
  • 用户文档

🌟 v1.1.0 - 功能增强

目标: 技能生态

任务 优先级 状态
技能市场 UI P0
在线技能安装 P0
技能配置界面 P1
技能使用统计 P2
Linux 支持 P2

🚀 v2.0.0 - 重大升级

目标: 多 Agent / 高级功能

任务 优先级 状态
多 Agent 支持 P0
工作流编排 P1
插件 SDK P1
自定义主题 P2
性能监控面板 P2

五、开发规范

5.1 代码风格

// ✅ 命名约定
const MY_CONSTANT = 'value';        // 常量: SCREAMING_SNAKE_CASE
function getUserData() {}           // 函数: camelCase
class GatewayManager {}             // 类: PascalCase
interface ChannelConfig {}          // 接口: PascalCase
type StatusType = 'running';        // 类型: PascalCase

// ✅ 文件命名
// 组件: PascalCase.tsx
// Dashboard.tsx, SkillCard.tsx

// 工具/hooks: kebab-case.ts
// gateway-manager.ts, use-gateway.ts

// ✅ 目录命名
// kebab-case
// src/pages/skill-market/

// ✅ React 组件
export function SkillCard({ skill, onSelect }: SkillCardProps) {
  // hooks 在顶部
  const [loading, setLoading] = useState(false);
  
  // handlers
  const handleClick = () => { /* ... */ };
  
  // render
  return (
    <Card onClick={handleClick}>
      {/* ... */}
    </Card>
  );
}

5.2 Git 提交规范

<type>(<scope>): <subject>

<body>

<footer>

Type:

  • feat: 新功能
  • fix: Bug 修复
  • docs: 文档
  • style: 格式调整
  • refactor: 重构
  • perf: 性能优化
  • test: 测试
  • chore: 构建/工具

示例:

feat(setup): add Node.js auto-installer for Windows

- Detect existing Node.js installation
- Download and install via winget if missing
- Show progress indicator during installation

Closes #123

5.3 测试策略

         /\
        /  \  E2E 测试 (10%)
       /    \  - Playwright
      /──────\  - 完整用户流程
     /        \ - CI 慢速运行
    /──────────\  集成测试 (20%)
   /            \  - Gateway 通信
  /              \ - IPC 调用
 /────────────────\  单元测试 (70%)
/                  \  - Vitest
                    \  - 组件/函数

六、发布流程

6.1 发布清单

# 1. 版本更新
pnpm version <major|minor|patch>

# 2. 更新 Changelog
# 手动编辑 CHANGELOG.md

# 3. 构建检查
pnpm lint && pnpm test && pnpm build

# 4. 打包各平台
pnpm package:mac    # macOS
pnpm package:win    # Windows
pnpm package:linux  # Linux

# 5. 签名 & 公证 (macOS)
pnpm notarize

# 6. 创建 Release
git tag v1.0.0
git push origin v1.0.0
# GitHub Actions 自动发布

6.2 更新通道

通道 用途 更新频率
stable 正式版 每月/按需
beta 测试版 每周
dev 开发版 每次提交

七、附录

7.1 参考资源

资源 链接
OpenClaw 仓库 https://github.com/openclaw/openclaw
OpenClaw 文档 https://docs.openclaw.ai
Electron 文档 https://www.electronjs.org/docs
shadcn/ui https://ui.shadcn.com
Tailwind CSS https://tailwindcss.com

7.2 环境变量

# .env.example

# OpenClaw 配置
OPENCLAW_GATEWAY_PORT=18789

# 开发配置
VITE_DEV_SERVER_PORT=5173

# 发布配置 (CI)
APPLE_ID=your@email.com
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
CSC_LINK=path/to/certificate.p12
CSC_KEY_PASSWORD=certificate_password
GH_TOKEN=github_personal_access_token

7.3 常用命令

# 开发
pnpm dev           # 启动开发服务器
pnpm dev:main      # 仅启动 Electron 主进程
pnpm dev:renderer  # 仅启动 React 开发服务器

# 构建
pnpm build         # 构建生产版本
pnpm build:main    # 构建主进程
pnpm build:renderer # 构建渲染进程

# 打包
pnpm package       # 打包当前平台
pnpm package:mac   # 打包 macOS
pnpm package:win   # 打包 Windows
pnpm package:linux # 打包 Linux
pnpm package:all   # 打包所有平台

# 测试
pnpm test          # 运行单元测试
pnpm test:e2e      # 运行 E2E 测试
pnpm test:coverage # 生成覆盖率报告

# 代码质量
pnpm lint          # 检查代码规范
pnpm lint:fix      # 自动修复
pnpm typecheck     # TypeScript 类型检查