【OpenClaw 源码分析】OpenClaw卸载Uninstall技术分析
·
OpenClaw Uninstall 技术分析
概述
OpenClaw 的卸载功能分为两个独立的子系统:
- 主程序卸载 (
openclaw uninstall) - 卸载 Gateway 服务、本地状态、工作区等 - 插件卸载 (
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.plistLinux: ~/.config/systemd/user/openclaw-gateway.serviceWindows: 计划任务 “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: 参数解析与范围确定
/**
* 根据用户选项确定要卸载的范围
* @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: 交互式确认
当用户 未指定显式 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: 清理计划解析
// 获取 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: 分阶段执行卸载
按照以下顺序执行卸载:
// 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);
}
执行顺序设计理由:
- 先建议备份: 提醒用户数据可恢复
- 先服务后数据: 确保服务已停止后再清理数据
- 服务优先: 如果服务卸载失败,可以及时终止后续操作
阶段 6: 服务卸载详细流程
/**
* 停止并卸载 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: 平台特定服务卸载
根据当前平台解析对应的服务实现:
/**
* 获取当前平台对应的 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)
/**
* 卸载 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`);
}
}
执行命令:
launchctl bootout gui/UID/ai.openclaw.gateway- 停止并移除服务launchctl unload /Users/USER/Library/LaunchAgents/ai.openclaw.gateway.plist- 卸载配置- 将 plist 移动到
~/.Trash/- 移至废纸篓 (可恢复)
7.2 Linux (systemd)
/**
* 卸载 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`);
}
}
执行命令:
systemctl --user disable --now openclaw-gateway.service- 禁用并停止服务- 删除
~/.config/systemd/user/openclaw-gateway.service- 删除 unit 文件
7.3 Windows (schtasks)
/**
* 卸载 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 {
// 忽略文件不存在的错误
}
}
执行操作:
schtasks /Delete /F /TN "OpenClaw Gateway"- 删除计划任务- 删除启动项文件 - 删除 Windows 登录启动项
- 删除任务脚本 - 清理相关脚本文件
阶段 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 计划任务实现 |
更多推荐



所有评论(0)