Electron桌面端:应用内更新时自动修改用户配置/记忆文件(可落地方案 + 实现代码)

场景:Electron 桌面端支持“应用内自动更新”,但更新过程只能替换应用安装目录下的新版本资源,无法直接修改用户目录(如 ~/.openclaw)内的配置/记忆文件。
目标:用户安装新版本后首次启动,由新版本自动检测“本次更新需要修改哪些用户文件”,并按规则完成补丁更新或覆盖更新,同时可追踪已执行状态,避免重复执行。


1. 方案思路

核心思想是把“更新后需要做的事情”做成一个启动期 Post-Update 任务系统

  • 执行时机:新版本启动后、启动 gateway 之前执行一次(主进程最早期可控位置)。
  • 任务清单:打包前硬编码在项目代码中(每次发版只需要追加一个 task)。
  • 幂等执行:用 taskId 记录“是否执行过”,同一个 task 在同一台机器只执行一次。
  • 可观测:执行/跳过/失败都写入 ~/.openclaw/app.log

本项目的落盘路径约定:

  • 用户配置:~/.openclaw/openclaw.json
  • 用户记忆(workspace):~/.openclaw/workspace/
  • AnClaw 自有状态:~/.openclaw/anclaw.config.json
  • 应用日志:~/.openclaw/app.log

2. 本次更新需要改哪些文件

下面用一个“示例任务”来说明机制可以做什么:

  1. ~/.openclaw/openclaw.json:补丁写入若干示例字段(如开关、默认参数等)
  2. ~/.openclaw/workspace/SAMPLE.md:从 anclaw_sources/SAMPLE.md 覆盖到用户 workspace(演示“覆盖更新”)
  3. ~/.openclaw/workspace/TIPS.md:从 anclaw_sources/TIPS.md 覆盖到用户 workspace(演示“覆盖更新”)

你实际发版时,只需要把示例任务替换为你自己的真实任务即可。


3. 实现步骤(工程落地)

3.1 打包阶段:把 anclaw_sources 带进安装包(resources)

打包脚本会把项目根目录下的 anclaw_sources/ 复制到资源目录 resources/targets/<platform-arch>/anclaw-sources/,供运行时读取:

关键代码位置:scripts/package-resources.js 的 Step 2.7(函数 bundleAnclawSources)。

function bundleAnclawSources(targetBase) {
  const sourceDir = path.join(ROOT, "anclaw_sources");
  const destDir = path.join(targetBase, "anclaw-sources");
  ensureDir(destDir);
  // 复制整个 anclaw_sources 到 resources 目录
}

这样在打包态应用可以从 resources/anclaw-sources 读取更新材料;在开发态则从 anclaw_sources 读取。

3.2 运行时:新增 post-update 任务系统(task list + 状态落盘)

新增模块:src/post-update.ts

  • 定义 POST_UPDATE_TASKS 任务数组(硬编码)
  • 执行时读取/写入 ~/.openclaw/anclaw.config.json 里的 postUpdate.appliedTasks
  • 对每个 task:
    • 已执行过 → 打印 skip 日志
    • 未执行过 → 执行 task,成功后记录 appliedAt/appVersion

状态文件结构在 src/anclaw-config.ts 中扩展:

postUpdate?: {
  appliedTasks?: Record<string, { appliedAt: string; appVersion: string }>;
  files?: Record<string, { appliedAt: string; appVersion: string; sha256: string }>;
};

3.3 接入启动链路:启动 gateway 前先跑 post-update

在主进程启动流程 src/main.ts 的归属判定分支里,在启动 gateway 前调用:

await runPostUpdateTasks();
await startGatewayAndShowMain("app:startup");

目前已覆盖:

  • anclaw
  • legacy-anclaw
  • external-openclaw

3.4 日志:所有执行/跳过写入 app.log

本项目 logger 固定写入 ~/.openclaw/app.log(并镜像到 stdout/stderr):

// src/logger.ts
const LOG_PATH = path.join(resolveUserStateDir(), "app.log");
function write(level: string, msg: string): void {
  const line = `[${new Date().toISOString()}] [${level}] ${msg}\n`;
  getLogStream().write(line);
}

因此在 post-update 里用 log.info/log.error 就会写入 app.log


4. 核心代码实现(可直接复用)

4.1 任务框架:幂等执行 + 记录 appliedAt + 写日志

代码位置:src/post-update.ts(函数 runPostUpdateTasks)。

const POST_UPDATE_TASKS: PostUpdateTask[] = [
  { id: "2026.04.xx-demo-config-patch", run: taskPatchUserConfigExample },
  { id: "2026.04.xx-demo-workspace-overwrite", run: taskOverwriteWorkspaceDocsExample },
];

export async function runPostUpdateTasks(): Promise<void> {
  const ctx: PostUpdateContext = { appVersion: app.getVersion() };
  const { state, persist } = getOrCreatePostUpdateState();
  state.appliedTasks ??= {};

  log.info(`[post-update] start (appVersion=${ctx.appVersion}, totalTasks=${POST_UPDATE_TASKS.length}, appliedTasks=${Object.keys(state.appliedTasks).length})`);

  for (const task of POST_UPDATE_TASKS) {
    if (state.appliedTasks[task.id]) {
      log.info(`[post-update] skip task (${task.id}) reason=already_applied`);
      continue;
    }

    try {
      log.info(`[post-update] run task (${task.id})`);
      await task.run(ctx);
      state.appliedTasks[task.id] = { appliedAt: formatLocalTimestamp(), appVersion: ctx.appVersion };
      persist();
      log.info(`[post-update] done task (${task.id})`);
    } catch (err: any) {
      log.error(`[post-update] task failed (${task.id}): ${err?.message ?? err}`);
      break;
    }
  }

  log.info("[post-update] end");
}

说明:

  • taskId 一旦发布不要修改(改了就会重新执行一次)
  • appliedAt 使用本地时间(带 +08:00 这种偏移),更符合排查习惯
  • 日志写入 ~/.openclaw/app.log(logger 时间仍是 UTC ISO,属于预期)

4.2 示例任务 1:补丁更新 openclaw.json

仅对已有 openclaw.json 生效,不存在则不创建(避免 fresh install 提前生成配置)。

示例代码(字段为演示用,可按你的业务需求替换):

async function taskPatchUserConfigExample(ctx: PostUpdateContext): Promise<void> {
  const configPath = resolveUserConfigPath();
  if (!fs.existsSync(configPath)) return;

  const config = readUserConfig();

  setDeep(config, ["features", "demoEnabled"], true);
  setDeep(config, ["ui", "demoBannerDismissed"], false);
  setDeep(config, ["runtime", "demoParam"], { retries: 3, timeoutMs: 5000 });

  writeUserConfig(config);
  log.info(`[post-update] updated openclaw.json (${ctx.appVersion})`);
}

补充:setDeep() 用于安全写入深层路径,自动创建中间节点,避免 Cannot read property ... of undefined

4.3 示例任务 2:覆盖 workspace 下的若干 markdown(演示“覆盖更新”)

本次需求是全覆盖,直接覆盖用户原有文件。

示例代码(文件名为演示用,可按你的业务需求替换):

async function taskOverwriteWorkspaceDocsExample(ctx: PostUpdateContext): Promise<void> {
  const sourceDir = resolveAnclawSourcesDir();
  const workspaceDir = path.join(app.getPath("home"), ".openclaw", "workspace");
  fs.mkdirSync(workspaceDir, { recursive: true });

  const { state, persist } = getOrCreatePostUpdateState();
  state.files ??= {};

  for (const fileName of ["SAMPLE.md", "TIPS.md"]) {
    const srcPath = path.join(sourceDir, fileName);
    if (!fs.existsSync(srcPath)) continue;

    const content = fs.readFileSync(srcPath, "utf-8");
    fs.writeFileSync(path.join(workspaceDir, fileName), content, "utf-8");

    state.files[`workspace/${fileName}`] = {
      appliedAt: formatLocalTimestamp(),
      appVersion: ctx.appVersion,
      sha256: sha256Text(content),
    };
  }

  persist();
  log.info(`[post-update] overwrite workspace/SAMPLE.md & workspace/TIPS.md (${ctx.appVersion})`);
}

这会在 anclaw.config.jsonpostUpdate.files 里记录每个文件的摘要,方便后续排查“到底写入了什么版本的内容”。


5. 如何测试是否生效

5.1 验证 post-update 是否执行

  1. 启动 AnClaw
  2. 打开 ~/.openclaw/app.log,搜索关键字:
    • [post-update] start
    • [post-update] run task
    • [post-update] done task
    • [post-update] skip task

5.2 验证 openclaw.json 是否被补丁更新

  1. 确认 ~/.openclaw/openclaw.json 存在
  2. 启动 AnClaw 后检查 openclaw.json 中是否出现/更新了字段:
    • features.demoEnabled
    • ui.demoBannerDismissed
    • runtime.demoParam

5.3 验证 workspace/SAMPLE.md、workspace/TIPS.md 是否被覆盖

  1. 在启动前手工修改 ~/.openclaw/workspace/SAMPLE.md 写入一行明显的内容(例如 USER_CUSTOM_LINE
  2. 启动 AnClaw
  3. 再打开 ~/.openclaw/workspace/SAMPLE.md
    • USER_CUSTOM_LINE 应该消失(因为是全覆盖)
    • 文件内容应与 anclaw_sources/SAMPLE.md 完全一致
  4. 同理验证 ~/.openclaw/workspace/TIPS.md

5.4 验证“只执行一次”

  1. 连续启动两次应用
  2. 第二次启动应出现 skip task (...) reason=already_applied

如需强制让任务重新执行:

  • 编辑 ~/.openclaw/anclaw.config.json,删除 postUpdate.appliedTasks(或删除整个 postUpdate 字段)
  • 再启动应用

6. 常见坑与建议

6.1 openclaw.json schema 不兼容会导致 gateway 起不来

openclaw 会对 openclaw.json 做 schema 校验;写入未知字段会出现类似错误:

  • Config invalid
  • Unrecognized key: "..."

建议:

  • Post-Update 对 openclaw.json 的写入只写“确认可用的字段”
  • 新增字段前先用目标 openclaw 版本做一次启动验证
  • 日志定位优先看:
    • ~/.openclaw/app.log(应用侧)
    • %TEMP%\\openclaw\\openclaw-YYYY-MM-DD.log(gateway 侧)

6.2 appliedAt 用本地时间,但 app.log 的时间是 UTC

  • appliedAt(anclaw.config.json)用本地时间(带 +08:00
  • app.log 每行时间是 new Date().toISOString()(UTC,带 Z

二者都是正确的,只是表现形式不同。


7. 小结

这套方案的关键价值:

  • 把“更新要做的变更”从安装过程转移到新版本首次启动
  • taskId 做幂等,天然支持跨版本升级与多次升级
  • 对用户文件的修改可观测、可回放、可定位
  • 业务侧扩展成本低:每次发版只需要追加一个 task
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐