【开发小技巧】Electron应用内更新用户目录下的文件
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. 本次更新需要改哪些文件
下面用一个“示例任务”来说明机制可以做什么:
~/.openclaw/openclaw.json:补丁写入若干示例字段(如开关、默认参数等)~/.openclaw/workspace/SAMPLE.md:从anclaw_sources/SAMPLE.md覆盖到用户 workspace(演示“覆盖更新”)~/.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");
目前已覆盖:
anclawlegacy-anclawexternal-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.json 的 postUpdate.files 里记录每个文件的摘要,方便后续排查“到底写入了什么版本的内容”。
5. 如何测试是否生效
5.1 验证 post-update 是否执行
- 启动 AnClaw
- 打开
~/.openclaw/app.log,搜索关键字:[post-update] start[post-update] run task[post-update] done task[post-update] skip task
5.2 验证 openclaw.json 是否被补丁更新
- 确认
~/.openclaw/openclaw.json存在 - 启动 AnClaw 后检查
openclaw.json中是否出现/更新了字段:features.demoEnabledui.demoBannerDismissedruntime.demoParam
5.3 验证 workspace/SAMPLE.md、workspace/TIPS.md 是否被覆盖
- 在启动前手工修改
~/.openclaw/workspace/SAMPLE.md写入一行明显的内容(例如USER_CUSTOM_LINE) - 启动 AnClaw
- 再打开
~/.openclaw/workspace/SAMPLE.md:USER_CUSTOM_LINE应该消失(因为是全覆盖)- 文件内容应与
anclaw_sources/SAMPLE.md完全一致
- 同理验证
~/.openclaw/workspace/TIPS.md
5.4 验证“只执行一次”
- 连续启动两次应用
- 第二次启动应出现
skip task (...) reason=already_applied
如需强制让任务重新执行:
- 编辑
~/.openclaw/anclaw.config.json,删除postUpdate.appliedTasks(或删除整个postUpdate字段) - 再启动应用
6. 常见坑与建议
6.1 openclaw.json schema 不兼容会导致 gateway 起不来
openclaw 会对 openclaw.json 做 schema 校验;写入未知字段会出现类似错误:
Config invalidUnrecognized 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
更多推荐
所有评论(0)