OpenClaw Uninstall 技术分析

概述

OpenClaw 的卸载功能分为两个独立的子系统:

  1. 主程序卸载 (openclaw uninstall) - 卸载 Gateway 服务、本地状态、工作区等
  2. 插件卸载 (openclaw plugins uninstall) - 卸载已安装的插件

本文档聚焦于 主程序卸载 的完整执行链路分析。


执行链路总览

用户执行命令
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. CLI 入口层                                                    │
│    源码目录: src/cli/program/                                   │
│    关键文件: register.maintenance.ts → uninstallCommand()      │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. 参数解析与范围确定                                            │
│    源码目录: src/commands/                                      │
│    关键文件: uninstall.ts → buildScopeSelection()              │
│    功能: 确定卸载范围 (service/state/workspace/app)             │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. 交互式确认 (非 --yes 模式)                                    │
│    源码目录: src/commands/                                      │
│    关键文件: uninstall.ts → multiselectStyled() + confirm()    │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. 清理计划解析                                                  │
│    源码目录: src/commands/                                      │
│    关键文件: cleanup-plan.ts → resolveCleanupPlanFromDisk()    │
│    获取路径:                                                    │
│      - 状态目录: ~/.openclaw                                    │
│      - 工作区目录: ~/.openclaw/workspace                        │
│      - 配置文件: ~/.openclaw/config.json                        │
│      - 凭证目录: ~/.openclaw/credentials                        │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. 分阶段执行卸载                                                │
│    源码目录: src/commands/                                      │
│    ┌─────────────┬─────────────┬─────────────┬─────────────┐   │
│    │  service    │   state     │  workspace  │    app      │   │
│    │  (服务)     │  (状态)     │  (工作区)   │  (macOS应用)│   │
│    └─────────────┴─────────────┴─────────────┴─────────────┘   │
│         │              │              │              │          │
│         ▼              ▼              ▼              ▼          │
│    删除目录:       删除目录:        删除目录:       删除目录:   │
│    ──────────     ~/.openclaw      ~/.openclaw/    /Applications│
│    macOS:         (全部内容)       workspace       /OpenClaw.app│
│    ~/Library/     ├─ agents/       (Agent工作区)               │
│    LaunchAgents/  ├─ sessions/     ├─ AGENTS.md                │
│    ai.openclaw.   ├─ credentials/  ├─ IDENTITY.md              │
│    gateway.plist  ├─ config.json   ├─ SOUL.md                  │
│                   └─ ...           ├─ TOOLS.md                 │
│    Linux:                           ├─ USER.md                  │
│    ~/.config/                       ├─ memory.md                │
│    systemd/user/                    ├─ knowledge/               │
│    openclaw-gateway.service         └─ skills/                  │
│                                                               │
│    Windows:                                                   │
│    1. 计划任务: "OpenClaw Gateway" (schtasks /Delete)         │
│    2. 启动项: %APPDATA%\Microsoft\Windows\Start               │
│       Menu\Programs\Startup\OpenClaw Gateway.cmd             │
│    3. 任务脚本: %USERPROFILE%\.openclaw\gateway.cmd           │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. 平台特定服务卸载                                              │
│    源码目录: src/daemon/                                         │
│    关键文件: service.ts (resolveGatewayService)                │
│    平台实现:                                                     │
│      - macOS: launchd.ts → ~/Library/LaunchAgents/ai.openclaw.gateway.plist │
│      - Linux: systemd.ts → ~/.config/systemd/user/openclaw-gateway.service │
│      - Windows: schtasks.ts                                     │
│        · 计划任务: "OpenClaw Gateway" (schtasks /Delete)        │
│        · 启动项: %APPDATA%\Microsoft\Windows\Start             │
│          Menu\Programs\Startup\OpenClaw Gateway.cmd            │
│        · 任务脚本: %USERPROFILE%\.openclaw\gateway.cmd         │
└─────────────────────────────────────────────────────────────────┘
     │
     ▼
完成

各阶段删除内容详细表格

路径说明:

  • ~ 表示用户主目录
    • macOS/Linux: /Users/<用户名>//home/<用户名>/
    • Windows: %USERPROFILE% (通常为 C:\Users\<用户名>)
  • %USERPROFILE% 是 Windows 环境变量,表示当前登录用户的主目录
  • %APPDATA% 是 Windows 环境变量,表示 Roaming AppData 目录 (如 C:\Users\<用户名>\AppData\Roaming)
  • 计划任务 是 Windows 操作系统内置的任务调度服务 (Task Scheduler),可以定时或按条件自动执行程序
阶段 范围 删除内容 目录路径 (Windows 示例)
5 service Gateway 服务 (launchd/systemd/schtasks) macOS: ~/Library/LaunchAgents/ai.openclaw.gateway.plist
Linux: ~/.config/systemd/user/openclaw-gateway.service
Windows: 计划任务 “OpenClaw Gateway”
删除方式: schtasks /Delete /F /TN "OpenClaw Gateway"
5 service 启动项 (Windows fallback) Windows: %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\OpenClaw Gateway.cmd
删除方式: fs.unlink() (文件删除)
5 service 任务脚本 (Windows) Windows: %USERPROFILE%\.openclaw\gateway.cmd
删除方式: fs.unlink() (文件删除)
5 state 状态目录 (全部内容) ~/.openclaw (即 %USERPROFILE%\.openclaw)
5 state ├─ agents/ (Agent 数据) ~/.openclaw/agents
5 state ├─ sessions/ (会话记录) ~/.openclaw/sessions
5 state ├─ credentials/ (通道凭据) ~/.openclaw/credentials
5 state └─ config.json (配置文件) ~/.openclaw/config.json
5 workspace 工作区目录 (全部内容) ~/.openclaw/workspace
5 workspace ├─ AGENTS.md (Agent 配置) ~/.openclaw/workspace/AGENTS.md
5 workspace ├─ IDENTITY.md (Agent 身份) ~/.openclaw/workspace/IDENTITY.md
5 workspace ├─ SOUL.md (Agent 性格) ~/.openclaw/workspace/SOUL.md
5 workspace ├─ TOOLS.md (工具配置) ~/.openclaw/workspace/TOOLS.md
5 workspace ├─ USER.md (用户信息) ~/.openclaw/workspace/USER.md
5 workspace ├─ memory.md (记忆文件) ~/.openclaw/workspace/memory.md
5 workspace ├─ knowledge/ (知识库) ~/.openclaw/workspace/knowledge/
5 workspace └─ skills/ (技能目录) ~/.openclaw/workspace/skills/
5 app macOS 应用程序 macOS: /Applications/OpenClaw.app

各卸载范围清理内容

范围 清理内容
service (服务) Gateway 服务 (launchd/systemd/schtasks)
state (状态) ~/.openclaw 目录: agents/, sessions/, credentials/, config.json
workspace (工作区) ~/.openclaw/workspace 目录: Agent 文件, memory/, knowledge/
app (macOS应用) /Applications/OpenClaw.app

详细说明

  • service: 停止并卸载系统服务 (macOS LaunchAgent → 移至废纸篓, Linux systemd → 删除 unit 文件, Windows → 删除计划任务)
  • state: 删除所有运行时数据,包括 Agent 数据、会话记录、通道凭据、配置文件
  • workspace: 删除工作区目录中的 Agent 文件、memory、knowledge 等用户数据
  • app: 仅在 macOS 平台删除应用程序

工作区 (workspace) 说明

工作区 是 Agent 的工作目录,用于存放 Agent 运行过程中产生的用户数据文件。

默认路径

  • ~/.openclaw/workspace (默认 profile)
  • ~/.openclaw/workspace-{profile} (自定义 profile)

工作区包含的文件

文件/目录 说明
AGENTS.md Agent 配置文件
IDENTITY.md Agent 身份定义
SOUL.md Agent 灵魂/性格定义
TOOLS.md Agent 工具配置
USER.md 用户信息
memory.md Agent 记忆文件
knowledge/ 知识库目录
skills/ 技能目录
.git/ Git 仓库 (如果初始化)

与 state 的区别

范围 路径 内容
state ~/.openclaw 运行时数据 (sessions, credentials, config)
workspace ~/.openclaw/workspace Agent 用户数据 (AGENTS.md, memory, knowledge 等)

注意: workspace 是 state 的子目录,删除 state 会同时删除 workspace。


详细执行流程

阶段 1: CLI 入口层

文件: src/cli/program/register.maintenance.ts

// 注册 uninstall 命令,定义命令行选项
program
  .command("uninstall")  // 命令名称
  .description("Uninstall the gateway service + local data (CLI remains)")  // 命令描述

  // 定义各种选项
  .option("--service", "Remove the gateway service", false)      // 卸载 Gateway 服务
  .option("--state", "Remove state + config", false)              // 卸载状态和配置
  .option("--workspace", "Remove workspace dirs", false)          // 卸载工作区目录
  .option("--app", "Remove the macOS app", false)                 // 卸载 macOS 应用
  .option("--all", "Remove service + state + workspace + app", false)  // 全部卸载
  .option("--yes", "Skip confirmation prompts", false)            // 跳过确认提示
  .option("--non-interactive", "Disable prompts (requires --yes)", false)  // 非交互模式
  .option("--dry-run", "Print actions without removing files", false)  // 预览模式

  // 命令执行入口
  .action(async (opts) => {
    // 使用默认运行时环境执行命令
    await runCommandWithRuntime(defaultRuntime, async () => {
      // 调用核心卸载函数,传递解析后的选项
      await uninstallCommand(defaultRuntime, {
        service: Boolean(opts.service),      // 转换为布尔值
        state: Boolean(opts.state),
        workspace: Boolean(opts.workspace),
        app: Boolean(opts.app),
        all: Boolean(opts.all),
        yes: Boolean(opts.yes),
        nonInteractive: Boolean(opts.nonInteractive),
        dryRun: Boolean(opts.dryRun),
      });
    });
  });

输入: 用户命令行参数 (--service, --state, --workspace, --app, --all, --yes, --non-interactive, --dry-run)

输出: 调用 uninstallCommand() 并传递解析后的选项


阶段 2: 参数解析与范围确定

文件: src/commands/uninstall.ts

/**
 * 根据用户选项确定要卸载的范围
 * @param opts - 用户提供的命令行选项
 * @returns scopes: 要卸载的范围集合; hadExplicit: 是否有显式指定范围
 */
function buildScopeSelection(opts: UninstallOptions): {
  scopes: Set<UninstallScope>;  // 存储要卸载的范围
  hadExplicit: boolean;         // 标记是否有显式指定
} {
  // 检查是否有任何显式指定 (--all 或具体选项)
  const hadExplicit = Boolean(opts.all || opts.service || opts.state || opts.workspace || opts.app);
  
  // 创建范围集合
  const scopes = new Set<UninstallScope>();
  
  // 如果指定了 --all 或 --service,添加 "service" 范围
  if (opts.all || opts.service) scopes.add("service");
  
  // 如果指定了 --all 或 --state,添加 "state" 范围
  if (opts.all || opts.state) scopes.add("state");
  
  // 如果指定了 --all 或 --workspace,添加 "workspace" 范围
  if (opts.all || opts.workspace) scopes.add("workspace");
  
  // 如果指定了 --all 或 --app,添加 "app" 范围
  if (opts.all || opts.app) scopes.add("app");
  
  // 返回解析结果
  return { scopes, hadExplicit };
}

决策逻辑:

用户输入 确定的 scopes
无参数 进入交互模式
--all service + state + workspace + app
--service service
--service --state service + state
--non-interactive 无显式 scope 报错退出

阶段 3: 交互式确认

文件: src/commands/uninstall.ts

当用户 未指定显式 scope 时,进入交互式选择流程:

// 如果用户没有显式指定任何范围,进入交互模式
if (!hadExplicit) {
  // 3.1 弹出多选菜单,让用户选择要卸载的组件
  const selection = await multiselectStyled<UninstallScope>({
    message: "Uninstall which components?",  // 提示消息
    options: [
      // 可选组件列表
      { value: "service", label: "Gateway service", hint: "launchd / systemd / schtasks" },
      { value: "state", label: "State + config", hint: "~/.openclaw" },
      { value: "workspace", label: "Workspace", hint: "agent files" },
      { value: "app", label: "macOS app", hint: "/Applications/OpenClaw.app" },
    ],
    // 默认选中前三项 (service, state, workspace)
    initialValues: ["service", "state", "workspace"],
  });
  
  // 3.2 将用户选择的值添加到 scopes 集合中
  for (const value of selection) {
    scopes.add(value);
  }
}

// 3.3 如果是交互模式且用户没有使用 --yes,显示确认提示
if (interactive && !opts.yes) {
  // 弹出确认对话框
  const ok = await confirm({
    message: stylePromptMessage("Proceed with uninstall?"),  // 格式化提示消息
  });
  
  // 如果用户取消或选择否
  if (!ok) {
    cancel("Uninstall cancelled.");  // 显示取消消息
    runtime.exit(0);                 // 正常退出
    return;                          // 结束函数执行
  }
}

流程图:

用户无显式参数?
     │
     ├── 是 ──→ multiselectStyled() → 用户选择 scopes
     │                    │
     │                    ▼
     │            confirm() → 用户确认?
     │                    │
     │            是 ──→ 继续执行
     │            否 ──→ 取消退出
     │
     └── 否 ──→ 直接执行 (--yes 模式跳过确认)

阶段 4: 清理计划解析

文件: src/commands/uninstall.ts

// 获取 dry-run 模式标志
const dryRun = Boolean(opts.dryRun);

// 从磁盘解析清理计划,获取需要清理的路径信息
const { 
  stateDir,              // 状态目录 (~/.openclaw)
  configPath,            // 配置文件路径
  oauthDir,              // 凭证目录 (credentials, 存放 OAuth token、通道凭据等)
  configInsideState,     // 配置是否在状态目录内
  oauthInsideState,      // OAuth 是否在状态目录内
  workspaceDirs          // 工作区目录列表
} = resolveCleanupPlanFromDisk();

文件: src/commands/cleanup-plan.ts

/**
 * 从磁盘解析清理计划
 * 解析出所有需要清理的路径信息
 */
export function resolveCleanupPlanFromDisk(): CleanupResolvedPaths & { workspaceDirs: string[] } {
  // 1. 解析状态目录路径 (默认 ~/.openclaw)
  const stateDir = resolveStateDir(process.env, os.homedir());
  
  // 2. 解析配置文件路径
  const configPath = resolveConfigPath();
  
  // 3. 解析 OAuth 目录路径
  const oauthDir = resolveOAuthDir(process.env, os.homedir());
  
  // 4. 加载当前配置 (用于获取工作区路径)
  const cfg = loadConfig();
  
  // 5. 返回所有路径信息
  return {
    stateDir,      // 状态目录
    configPath,    // 配置文件路径
    oauthDir,      // 凭证目录 (credentials)
    // 展开清理计划 (包含配置/OAuth是否在状态目录内,以及工作区目录列表)
    ...buildCleanupPlan({ cfg, stateDir, configPath, oauthDir }),
  };
}

获取的路径信息:

路径变量 说明 示例
stateDir 状态目录 ~/.openclaw
configPath 配置文件路径 ~/.openclaw/config.json
oauthDir 凭证目录 (credentials) ~/.openclaw/credentials
configInsideState 配置是否在状态目录内 true/false
oauthInsideState OAuth 是否在状态目录内 true/false
workspaceDirs 工作区目录列表 ["~/OpenClaw"]

阶段 5: 分阶段执行卸载

文件: src/commands/uninstall.ts

按照以下顺序执行卸载:

// 5.1 如果涉及 state 或 workspace,提示用户先备份
if (scopes.has("state") || scopes.has("workspace")) {
  // 打印备份建议: "Recommended first: openclaw backup create"
  logBackupRecommendation(runtime);
}

// 5.2 如果用户选择了 service 范围,卸载 Gateway 服务
if (scopes.has("service")) {
  // 如果是 dry-run 模式,只打印日志不实际执行
  if (dryRun) {
    runtime.log("[dry-run] remove gateway service");
  } else {
    // 执行服务停止和卸载
    await stopAndUninstallService(runtime);
  }
}

// 5.3 如果用户选择了 state 范围,清理状态目录和配置
if (scopes.has("state")) {
  // 删除状态目录及相关路径
  await removeStateAndLinkedPaths(
    { 
      stateDir,              // 状态目录
      configPath,            // 配置文件路径
      oauthDir,              // OAuth 目录
      configInsideState,     // 配置是否在状态目录内
      oauthInsideState       // OAuth 是否在状态目录内
    },
    runtime,                 // 运行时环境
    { dryRun },              // 传递 dry-run 选项
  );
}

// 5.4 如果用户选择了 workspace 范围,清理工作区目录
if (scopes.has("workspace")) {
  // 删除所有工作区目录
  await removeWorkspaceDirs(workspaceDirs, runtime, { dryRun });
}

// 5.5 如果用户选择了 app 范围 (仅 macOS),清理应用
if (scopes.has("app")) {
  // 删除 /Applications/OpenClaw.app
  await removeMacApp(runtime, dryRun);
}

执行顺序设计理由:

  1. 先建议备份: 提醒用户数据可恢复
  2. 先服务后数据: 确保服务已停止后再清理数据
  3. 服务优先: 如果服务卸载失败,可以及时终止后续操作

阶段 6: 服务卸载详细流程

文件: src/commands/uninstall.ts

/**
 * 停止并卸载 Gateway 服务
 * @param runtime - 运行时环境
 * @returns 是否成功
 */
async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
  // 6.1 检查是否为 Nix 模式 (Nix 模式下不允许卸载服务)
  if (isNixMode) {
    runtime.error("Nix mode detected; service uninstall is disabled.");
    return false;  // 返回失败
  }

  // 6.2 根据当前平台获取对应的服务抽象实例
  // darwin -> launchd, linux -> systemd, win32 -> schtasks
  const service = resolveGatewayService();

  // 6.3 检查服务是否已加载/运行
  let loaded = false;
  try {
    // 调用平台的 isLoaded 方法检查服务状态
    loaded = await service.isLoaded({ env: process.env });
  } catch (err) {
    // 检查失败,记录错误并返回
    runtime.error(`Gateway service check failed: ${String(err)}`);
    return false;
  }

  // 6.4 如果服务未加载,直接返回成功
  if (!loaded) {
    // 打印服务未加载信息 (not loaded / disabled / missing)
    runtime.log(`Gateway service ${service.notLoadedText}.`);
    return true;
  }

  // 6.5 停止服务 (如果正在运行)
  try {
    // 调用平台的 stop 方法停止服务
    await service.stop({ env: process.env, stdout: process.stdout });
  } catch (err) {
    // 停止失败,记录错误但继续尝试卸载
    runtime.error(`Gateway stop failed: ${String(err)}`);
  }

  // 6.6 卸载服务
  try {
    // 调用平台的 uninstall 方法卸载服务
    await service.uninstall({ env: process.env, stdout: process.stdout });
    return true;  // 卸载成功
  } catch (err) {
    // 卸载失败,记录错误并返回失败
    runtime.error(`Gateway uninstall failed: ${String(err)}`);
    return false;
  }
}

阶段 7: 平台特定服务卸载

文件: src/daemon/service.ts

根据当前平台解析对应的服务实现:

/**
 * 获取当前平台对应的 Gateway 服务实现
 * 根据 process.platform 返回 launchd/systemd/schtasks 之一
 */
export function resolveGatewayService(): GatewayService {
  // 检查当前平台是否支持
  if (isSupportedGatewayServicePlatform(process.platform)) {
    // 从注册表中返回对应平台的服务实现
    return GATEWAY_SERVICE_REGISTRY[process.platform];
  }
  // 不支持的平台抛出错误
  throw new Error(`Gateway service install not supported on ${process.platform}`);
}
7.1 macOS (launchd)

文件: src/daemon/launchd.ts

/**
 * 卸载 macOS LaunchAgent 服务
 * @param env - 环境变量
 * @param stdout - 标准输出流
 */
export async function uninstallLaunchAgent({
  env,
  stdout,
}: GatewayServiceManageArgs): Promise<void> {
  // 步骤 1: 停止并移除 LaunchAgent
  
  // 解析 GUI 域 (通常是当前用户的 UID)
  const domain = resolveGuiDomain();
  
  // 解析 LaunchAgent 标签 (如 com.openclaw.gateway)
  const label = resolveLaunchAgentLabel({ env });
  
  // 解析 plist 文件路径
  const plistPath = resolveLaunchAgentPlistPath(env);
  
  // 使用 launchctl bootout 停止并移除服务
  await execLaunchctl(["bootout", domain, plistPath]);
  
  // 使用 launchctl unload 卸载 plist
  await execLaunchctl(["unload", plistPath]);

  // 步骤 2: 将 plist 移至废纸篓 (而非直接删除,保证可恢复)
  
  // 解析用户主目录并转换为 POSIX 路径格式
  const home = toPosixPath(resolveHomeDir(env));
  
  // 废纸篓目录路径
  const trashDir = path.posix.join(home, ".Trash");
  
  // 目标文件路径 (在废纸篓中)
  const dest = path.join(trashDir, `${label}.plist`);
  
  // 尝试移动文件到废纸篓
  try {
    // 确保废纸篓目录存在
    await fs.mkdir(trashDir, { recursive: true });
    
    // 将 plist 移动到废纸篓
    await fs.rename(plistPath, dest);
    
    // 输出成功消息
    stdout.write(`${formatLine("Moved LaunchAgent to Trash", dest)}\n`);
  } catch {
    // 如果移动失败,保留原文件
    stdout.write(`LaunchAgent remains at ${plistPath} (could not move)\n`);
  }
}

执行命令:

  1. launchctl bootout gui/UID/ai.openclaw.gateway - 停止并移除服务
  2. launchctl unload /Users/USER/Library/LaunchAgents/ai.openclaw.gateway.plist - 卸载配置
  3. 将 plist 移动到 ~/.Trash/ - 移至废纸篓 (可恢复)
7.2 Linux (systemd)

文件: src/daemon/systemd.ts

/**
 * 卸载 Linux systemd 服务
 * @param env - 环境变量
 * @param stdout - 标准输出流
 */
export async function uninstallSystemdService({
  env,
  stdout,
}: GatewayServiceManageArgs): Promise<void> {
  // 步骤 1: 禁用并停止服务
  
  // 解析服务名称 (支持多配置文件)
  const serviceName = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
  
  // 完整的 unit 名称
  const unitName = `${serviceName}.service`;
  
  // 执行 systemctl disable --now 禁用并立即停止服务
  await execSystemctlUser(env, ["disable", "--now", unitName]);

  // 步骤 2: 删除 unit 文件
  
  // 解析 unit 文件路径
  const unitPath = resolveSystemdUnitPath(env);
  
  // 尝试删除 unit 文件
  try {
    await fs.unlink(unitPath);  // 删除文件
    stdout.write(`${formatLine("Removed systemd service", unitPath)}\n`);
  } catch {
    // 文件不存在时输出提示
    stdout.write(`Systemd service not found at ${unitPath}\n`);
  }
}

执行命令:

  1. systemctl --user disable --now openclaw-gateway.service - 禁用并停止服务
  2. 删除 ~/.config/systemd/user/openclaw-gateway.service - 删除 unit 文件
7.3 Windows (schtasks)

文件: src/daemon/schtasks.ts

/**
 * 卸载 Windows 计划任务服务
 * @param env - 环境变量
 * @param stdout - 标准输出流
 */
export async function uninstallScheduledTask({
  env,
  stdout,
}: GatewayServiceManageArgs): Promise<void> {
  // 步骤 1: 删除计划任务
  
  // 解析任务名称
  const taskName = resolveTaskName(env);
  
  // 检查任务是否已注册 (捕获异常)
  const taskInstalled = await isRegisteredScheduledTask(env).catch(() => false);
  
  // 如果任务已注册,则删除
  if (taskInstalled) {
    // /Delete: 删除任务; /F: 强制删除; /TN: 任务名
    await execSchtasks(["/Delete", "/F", "/TN", taskName]);
  }

  // 步骤 2: 删除启动项 (Windows 登录时自动启动)
  
  // 解析启动项文件路径
  const startupEntryPath = resolveStartupEntryPath(env);
  
  // 尝试删除启动项文件
  try {
    await fs.unlink(startupEntryPath);
    stdout.write(`${formatLine("Removed Windows login item", startupEntryPath)}\n`);
  } catch {
    // 忽略文件不存在的错误
  }

  // 步骤 3: 删除任务脚本 (如果存在)
  
  // 解析任务脚本路径
  const scriptPath = resolveTaskScriptPath(env);
  
  // 尝试删除脚本文件
  try {
    await fs.unlink(scriptPath);
    stdout.write(`${formatLine("Removed task script", scriptPath)}\n`);
  } catch {
    // 忽略文件不存在的错误
  }
}

执行操作:

  1. schtasks /Delete /F /TN "OpenClaw Gateway" - 删除计划任务
  2. 删除启动项文件 - 删除 Windows 登录启动项
  3. 删除任务脚本 - 清理相关脚本文件

阶段 8: 状态和工作区清理

文件: src/commands/cleanup-utils.ts

8.1 安全检查

在删除任何路径前进行安全检查,防止误删系统关键目录:

/**
 * 检查路径是否为不安全的删除目标
 * 防止意外删除系统关键目录
 */
function isUnsafeRemovalTarget(target: string): boolean {
  // 1. 空路径检查 - 防止空字符串导致的问题
  if (!target.trim()) return true;
  
  // 2. 根目录检查 - 防止删除整个磁盘
  const resolved = path.resolve(target);  // 解析为绝对路径
  const root = path.parse(resolved).root;  // 获取根目录
  if (resolved === root) return true;      // 如果是根目录,拒绝删除
  
  // 3. 用户主目录检查 - 防止删除整个 home 目录
  const home = resolveHomeDir();
  if (home && resolved === path.resolve(home)) return true;
  
  // 路径安全,允许删除
  return false;
}
8.2 状态目录清理
/**
 * 删除状态目录及相关路径
 * @param cleanup - 包含所有路径信息的对象
 * @param runtime - 运行时环境
 * @param opts - 可选配置 (如 dry-run)
 */
export async function removeStateAndLinkedPaths(
  cleanup: CleanupResolvedPaths,
  runtime: RuntimeEnv,
  opts?: { dryRun?: boolean },
): Promise<void> {
  // 1. 删除主状态目录 (~/.openclaw)
  // 包含所有运行时数据: sessions, agents, credentials 等
  await removePath(cleanup.stateDir, runtime, { dryRun: opts?.dryRun });
  
  // 2. 如果配置文件不在状态目录内,单独删除
  // 某些配置可能存放在 ~/.openclaw 之外的位置
  if (!cleanup.configInsideState) {
    await removePath(cleanup.configPath, runtime, { dryRun: opts?.dryRun });
  }
  
  // 3. 如果 OAuth 目录不在状态目录内,单独删除
  // OAuth token 等敏感数据可能单独存放
  if (!cleanup.oauthInsideState) {
    await removePath(cleanup.oauthDir, runtime, { dryRun: opts?.dryRun });
  }
}
8.3 工作区清理
/**
 * 删除所有工作区目录
 * @param workspaceDirs - 工作区目录列表
 * @param runtime - 运行时环境
 * @param opts - 可选配置 (如 dry-run)
 */
export async function removeWorkspaceDirs(
  workspaceDirs: readonly string[],
  runtime: RuntimeEnv,
  opts?: { dryRun?: boolean },
): Promise<void> {
  // 遍历每个工作区目录并删除
  for (const workspace of workspaceDirs) {
    await removePath(workspace, runtime, { dryRun: opts?.dryRun });
  }
}
8.4 路径删除实现
/**
 * 删除指定路径
 * @param target - 要删除的路径
 * @param runtime - 运行时环境
 * @param opts - 可选配置 (dry-run 模式, 自定义标签)
 * @returns 删除结果
 */
export async function removePath(
  target: string,
  runtime: RuntimeEnv,
  opts?: { dryRun?: boolean; label?: string },
): Promise<RemovalResult> {
  // 安全检查: 验证路径是否安全
  if (isUnsafeRemovalTarget(resolved)) {
    // 拒绝删除不安全的路径
    runtime.error(`Refusing to remove unsafe path: ${displayLabel}`);
    return { ok: false };
  }
  
  // Dry-run 模式: 只打印日志,不实际删除
  if (opts?.dryRun) {
    runtime.log(`[dry-run] remove ${displayLabel}`);
    return { ok: true, skipped: true };  // 标记为跳过
  }
  
  // 执行实际删除
  try {
    // 使用 fs.rm 删除目录或文件 (recursive: 递归删除, force: 忽略错误)
    await fs.rm(resolved, { recursive: true, force: true });
    runtime.log(`Removed ${displayLabel}`);
    return { ok: true };
  } catch (err) {
    // 删除失败,记录错误
    runtime.error(`Failed to remove ${displayLabel}: ${String(err)}`);
    return { ok: false };
  }
}

完整执行流程图

┌────────────────────────────────────────────────────────────────────┐
│                         用户执行                                    │
│                  openclaw uninstall [options]                      │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  1. CLI 解析                                                      │
│     register.maintenance.ts                                        │
│     └── 解析命令行选项 (--service/--state/--workspace/--app/--all) │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  2. uninstallCommand() 入口                                        │
│     src/commands/uninstall.ts:100                                  │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  3. buildScopeSelection()                                          │
│     └── 确定卸载范围 (service/state/workspace/app)                 │
│         │                                                          │
│         ├── 有显式参数? ──是──→ 直接使用指定范围                    │
│         │                                                          │
│         └── 否 ──→ 进入交互模式                                    │
│                       │                                            │
│                       ▼                                            │
│                 multiselectStyled()                                │
│                 └── 用户选择要卸载的组件                            │
│                       │                                            │
│                       ▼                                            │
│                 confirm()                                          │
│                 └── 用户确认卸载                                    │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  4. resolveCleanupPlanFromDisk()                                   │
│     └── 获取清理路径 (stateDir, configPath, workspaceDirs)         │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  5. 分阶段执行                                                     │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ 5.1 备份建议 (如果涉及 state 或 workspace)                    │  │
│  │     └── "Recommended first: openclaw backup create"          │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ 5.2 服务卸载 (scopes.has("service"))                         │  │
│  │     │                                                        │  │
│  │     ├── isNixMode? ──是──→ 报错退出                          │  │
│  │     │                                                        │  │
│  │     ├── resolveGatewayService()                              │  │
│  │     │   └── 获取平台服务抽象 (launchd/systemd/schtasks)      │  │
│  │     │                                                        │  │
│  │     ├── service.isLoaded()                                   │  │
│  │     │   └── 检查服务是否已加载                                │  │
│  │     │                                                        │  │
│  │     ├── service.stop()                                       │  │
│  │     │   └── 停止 Gateway 服务                                │  │
│  │     │                                                        │  │
│  │     └── service.uninstall()                                  │  │
│  │         └── 卸载平台服务:                                     │  │
│  │             ├── macOS: LaunchAgent (plist 移至废纸篓)        │  │
│  │             ├── Linux:  systemd (删除 unit 文件)             │  │
│  │             └── Windows: 计划任务 + 启动项                   │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ 5.3 状态清理 (scopes.has("state"))                           │  │
│  │     │                                                        │  │
│  │     ├── removePath(stateDir)                                 │  │
│  │     │   └── 删除 ~/.openclaw 目录                            │  │
│  │     │       ├── agents/        (Agent 数据)                  │  │
│  │     │       ├── sessions/      (会话记录)                    │  │
│  │     │       ├── credentials/   (通道凭据) ← 重要!            │  │
│  │     │       └── ...                                           │  │
│  │     │                                                        │  │
│  │     ├── removePath(configPath)  [如果不在 state 内]          │  │
│  │     │   └── 删除配置文件 (openclaw.json)                     │  │
│  │     │                                                        │  │
│  │     └── removePath(oauthDir)     [如果不在 state 内]         │  │
│  │         └── 删除凭证目录 (credentials/)                      │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ 5.4 工作区清理 (scopes.has("workspace"))                     │  │
│  │     │                                                        │  │
│  │     └── removeWorkspaceDirs(workspaceDirs)                  │  │
│  │         └── 遍历删除每个工作区目录                            │  │
│  │             ├── Agent 文件 (memory, knowledge 等)            │  │
│  │             └── 用户数据                                      │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                              │                                     │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ 5.5 macOS 应用清理 (scopes.has("app"))                       │  │
│  │     │                                                        │  │
│  │     └── removePath("/Applications/OpenClaw.app")            │  │
│  │         └── 删除 macOS 应用程序                              │  │
│  └──────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  6. 完成提示                                                       │
│     "CLI still installed. Remove via npm/pnpm if desired."        │
└────────────────────────────────────────────────────────────────────┘

关键设计决策

1. 安全优先原则

  • 路径安全检查: 防止意外删除系统关键目录 (根目录、用户主目录)
  • Nix 模式保护: Nix 环境下禁用服务卸载
  • Dry-run 支持: 允许预览而不实际修改

2. 用户体验优化

  • 交互式选择: 未指定参数时提供友好的多选菜单
  • 默认选中: 智能默认选中 (service + state + workspace)
  • 备份建议: 卸载前提醒用户创建备份
  • 清晰日志: 每个操作都有日志输出

3. 平台适配策略

  • 统一抽象: GatewayService 接口屏蔽平台差异
  • 原生工具: 每个平台使用原生服务管理工具
  • macOS 特殊处理: LaunchAgent 移至废纸篓而非直接删除

相关文件索引

文件路径 职责
src/cli/program/register.maintenance.ts CLI 命令注册
src/commands/uninstall.ts 主程序卸载核心逻辑
src/commands/cleanup-utils.ts 路径清理工具函数
src/commands/cleanup-plan.ts 清理计划解析
src/daemon/service.ts 平台服务抽象
src/daemon/launchd.ts macOS LaunchAgent 实现
src/daemon/systemd.ts Linux systemd 实现
src/daemon/schtasks.ts Windows 计划任务实现
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐