diff --git a/Dockerfile b/Dockerfile index d09ef0a9ce1..cba29fb48d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -406,6 +406,8 @@ ENV NPM_CONFIG_OFFLINE=true \ # This must fail the image build if registration fails; otherwise the sandbox # can boot with a discoverable plugin manifest but without the /nemoclaw runtime # command registered in the active Gateway. +# Re-apply WeChat account seeding after OpenClaw doctor/plugin-install touches +# openclaw.json; the seed script no-ops unless WeChat is actively configured. # Prune non-runtime metadata from staged bundled plugin dependencies before # this layer is committed; deleting it in a later layer would not reduce the # OCI image imported by k3s. @@ -413,6 +415,7 @@ ENV NPM_CONFIG_OFFLINE=true \ RUN openclaw plugins install /opt/nemoclaw \ && openclaw plugins enable nemoclaw \ && openclaw plugins inspect nemoclaw --json > /dev/null \ + && python3 /usr/local/lib/nemoclaw/seed-wechat-accounts.py \ && if [ -d /sandbox/.openclaw/plugin-runtime-deps ]; then \ find /sandbox/.openclaw/plugin-runtime-deps -type f \( \ -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o \ diff --git a/scripts/seed-wechat-accounts.py b/scripts/seed-wechat-accounts.py index eff6c49742c..5c4935c2e4f 100755 --- a/scripts/seed-wechat-accounts.py +++ b/scripts/seed-wechat-accounts.py @@ -19,8 +19,9 @@ # is registered. Without channels.openclaw-weixin.accounts..enabled=true # in openclaw.json, the plugin's auth/accounts.ts considers the account # disabled and the bridge won't start, even if the per-account state files -# above exist. generate-openclaw-config.py invokes this only after -# plugins.installs.openclaw-weixin has been preserved from the base image. +# above exist. The patch also restores the openclaw-weixin plugin registry +# entry because later OpenClaw config rewrites can drop it while leaving the +# pre-installed extension files in place. # # State dir resolution mirrors the upstream's resolveStateDir(): # $OPENCLAW_STATE_DIR || $CLAWDBOT_STATE_DIR || ~/.openclaw @@ -53,6 +54,12 @@ import sys +WECHAT_PLUGIN_ID = "openclaw-weixin" +WECHAT_PLUGIN_SPEC = "@tencent-weixin/openclaw-weixin@2.4.2" +WECHAT_PLUGIN_INSTALL = { + "source": "npm", + "spec": WECHAT_PLUGIN_SPEC, +} WECHAT_TOKEN_PLACEHOLDER = "openshell:resolve:env:WECHAT_BOT_TOKEN" @@ -141,6 +148,31 @@ def _patch_openclaw_config(account_id: str) -> None: ) return + plugins = cfg.setdefault("plugins", {}) + if not isinstance(plugins, dict): + plugins = {} + cfg["plugins"] = plugins + installs = plugins.setdefault("installs", {}) + if not isinstance(installs, dict): + installs = {} + plugins["installs"] = installs + wechat_install = installs.get(WECHAT_PLUGIN_ID) + if ( + not isinstance(wechat_install, dict) + or wechat_install.get("source") != WECHAT_PLUGIN_INSTALL["source"] + or not wechat_install.get("spec") + ): + installs[WECHAT_PLUGIN_ID] = dict(WECHAT_PLUGIN_INSTALL) + entries = plugins.setdefault("entries", {}) + if not isinstance(entries, dict): + entries = {} + plugins["entries"] = entries + wechat_entry = entries.setdefault(WECHAT_PLUGIN_ID, {}) + if not isinstance(wechat_entry, dict): + wechat_entry = {} + entries[WECHAT_PLUGIN_ID] = wechat_entry + wechat_entry["enabled"] = True + channels = cfg.setdefault("channels", {}) weixin = channels.setdefault("openclaw-weixin", {}) weixin["channelConfigUpdatedAt"] = _js_iso_utc() diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index 27633c4d55b..d14c0b2b589 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -266,9 +266,8 @@ describe("generate-openclaw-config.py: config generation", () => { it("seeds channels.openclaw-weixin when the base plugin install registry exists", () => { const configPath = path.join(tmpDir, ".openclaw", "openclaw.json"); const installEntry = { - type: "npm", + source: "npm", spec: "@tencent-weixin/openclaw-weixin@2.4.2", - resolved: "@tencent-weixin/openclaw-weixin@2.4.2", }; fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync( @@ -327,9 +326,8 @@ describe("generate-openclaw-config.py: config generation", () => { const configPath = path.join(tmpDir, ".openclaw", "openclaw.json"); fs.mkdirSync(path.dirname(configPath), { recursive: true }); const installEntry = { - type: "npm", + source: "npm", spec: "@tencent-weixin/openclaw-weixin@2.4.2", - resolved: "@tencent-weixin/openclaw-weixin@2.4.2", }; fs.writeFileSync( configPath, diff --git a/test/seed-wechat-accounts.test.ts b/test/seed-wechat-accounts.test.ts index 7c48a6849f1..e0937851441 100644 --- a/test/seed-wechat-accounts.test.ts +++ b/test/seed-wechat-accounts.test.ts @@ -229,6 +229,40 @@ describe("seed-wechat-accounts.py: openclaw.json patching (channels.openclaw-wei expect(cfg.channels["openclaw-weixin"].accounts.primary.enabled).toBe(true); }); + it("restores plugin registration and channel block after a later OpenClaw config rewrite drops them", () => { + // The Dockerfile invokes this seed script again after OpenClaw doctor and + // plugin installation because those commands can rewrite openclaw.json + // after generate-openclaw-config.py first runs. Re-running the seed must + // be enough to put the upstream WeChat plugin and channel registration + // back; otherwise the gateway rejects channels.openclaw-weixin as an + // unknown channel id at startup. + writeOpenclawConfig({ + channels: { + telegram: { accounts: { default: { enabled: true } } }, + slack: { accounts: { default: { enabled: true } } }, + }, + plugins: {}, + }); + + const result = runSeed({ + NEMOCLAW_WECHAT_CONFIG_B64: configB64({ + accountId: "primary", + baseUrl: "https://ilinkai.wechat.com", + userId: "wxid-42", + }), + }); + expect(result.status).toBe(0); + + const cfg = readJson(path.join(tmpDir, ".openclaw", "openclaw.json")); + expect(cfg.plugins.installs["openclaw-weixin"]).toEqual({ + source: "npm", + spec: "@tencent-weixin/openclaw-weixin@2.4.2", + }); + expect(cfg.plugins.entries["openclaw-weixin"].enabled).toBe(true); + expect(Object.keys(cfg.channels)).toEqual(["telegram", "slack", "openclaw-weixin"]); + expect(cfg.channels["openclaw-weixin"].accounts.primary.enabled).toBe(true); + }); + it("bails (and warns) when openclaw.json is missing — does not invent a config", () => { // generate-openclaw-config.py runs first and is responsible for producing // openclaw.json. If it failed silently, we'd rather print a warning than