@clawling/clawchat-plugin-openclaw 2026.5.13-dev.2 → 2026.5.13-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -0
- package/dist/setup-entry.js +31 -1
- package/dist/src/config-compat.js +120 -0
- package/index.ts +7 -0
- package/openclaw.plugin.json +7 -0
- package/package.json +1 -1
- package/setup-entry.ts +51 -1
- package/src/config-compat.ts +154 -0
package/dist/index.js
CHANGED
|
@@ -16,5 +16,12 @@ export default defineChannelPluginEntry({
|
|
|
16
16
|
registerOpenclawClawlingCommands(api);
|
|
17
17
|
registerClawChatPromptInjection(api);
|
|
18
18
|
registerOpenclawClawlingTools(api);
|
|
19
|
+
// NOTE: the legacy-rename config migration is intentionally NOT registered
|
|
20
|
+
// here. The host's setup-migration runner only loads a plugin's SETUP source
|
|
21
|
+
// (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
|
|
22
|
+
// `register(api)` in "setup-only" mode; registrations made in `registerFull`
|
|
23
|
+
// (full/tool-discovery modes) are never collected for migrations, and this
|
|
24
|
+
// gated full-load path doesn't run for the rename scenario anyway. The
|
|
25
|
+
// migration is wired into setup-entry.ts instead.
|
|
19
26
|
},
|
|
20
27
|
});
|
package/dist/setup-entry.js
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
1
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { CHANNEL_ID } from "./src/config.js";
|
|
2
3
|
import { openclawClawlingSetupPlugin } from "./src/channel.setup.js";
|
|
3
|
-
|
|
4
|
+
import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.js";
|
|
5
|
+
/**
|
|
6
|
+
* Host setup-source `register` hook.
|
|
7
|
+
*
|
|
8
|
+
* The OpenClaw setup-migration runner (`setup-registry`) loads this module as
|
|
9
|
+
* the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
|
|
10
|
+
* / `runtimeSetupEntry`), unwraps the default export, and — when that export is
|
|
11
|
+
* an object exposing a `register` function whose `id` matches the plugin id —
|
|
12
|
+
* calls `register(api)` in setup-only mode. This is the ONLY ungated path that
|
|
13
|
+
* collects config migrations for the plugin-rename scenario.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
|
|
16
|
+
* `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
|
|
17
|
+
*/
|
|
18
|
+
export function register(api) {
|
|
19
|
+
api.registerConfigMigration?.((config) => migrateLegacyClawChatChannelConfig(config));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Default export consumed by BOTH host loaders of this setup source:
|
|
23
|
+
* - the channel-setup loader unwraps `default` and reads `.plugin`
|
|
24
|
+
* (`defineSetupPluginEntry` yields `{ plugin }`);
|
|
25
|
+
* - the setup-migration runner unwraps `default` and reads `.register`,
|
|
26
|
+
* rejecting it unless `.id` matches the plugin id.
|
|
27
|
+
* So the default export must carry `plugin`, `id`, and `register` together.
|
|
28
|
+
*/
|
|
29
|
+
export default {
|
|
30
|
+
...defineSetupPluginEntry(openclawClawlingSetupPlugin),
|
|
31
|
+
id: CHANNEL_ID,
|
|
32
|
+
register,
|
|
33
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CHANNEL_ID } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compatibility migration for the plugin rename (commit 260044f): the plugin
|
|
4
|
+
* `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
|
|
5
|
+
* `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
|
|
6
|
+
*
|
|
7
|
+
* Users upgrading from the old version have all their state — channel block
|
|
8
|
+
* (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
|
|
9
|
+
* `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
|
|
10
|
+
* id. The new code only reads the new id, so the channel silently fails to
|
|
11
|
+
* load. This migration moves the old-keyed state onto the new id.
|
|
12
|
+
*
|
|
13
|
+
* The function is PURE: it clones the input via `structuredClone` and never
|
|
14
|
+
* mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
|
|
15
|
+
* returns an equivalent config with `changes: []`.
|
|
16
|
+
*/
|
|
17
|
+
export const LEGACY_CHANNEL_ID = "openclaw-clawchat";
|
|
18
|
+
export const TARGET_CHANNEL_ID = CHANNEL_ID;
|
|
19
|
+
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
20
|
+
function isBlockedObjectKey(key) {
|
|
21
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function isNonEmptyRecord(value) {
|
|
27
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Copy keys from `source` into `target` only when they are absent/undefined in
|
|
31
|
+
* `target` (target is more current — never overwrite an existing value).
|
|
32
|
+
* Recurses into nested records. Skips prototype-pollution keys.
|
|
33
|
+
*/
|
|
34
|
+
function mergeMissing(target, source) {
|
|
35
|
+
for (const [key, value] of Object.entries(source)) {
|
|
36
|
+
if (value === undefined || isBlockedObjectKey(key))
|
|
37
|
+
continue;
|
|
38
|
+
const existing = target[key];
|
|
39
|
+
if (existing === undefined) {
|
|
40
|
+
target[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (isRecord(existing) && isRecord(value))
|
|
44
|
+
mergeMissing(existing, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Replace `from` → `to` in a string array, preserving order and deduping. */
|
|
48
|
+
function replaceAndDedup(list, from, to) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const raw of list) {
|
|
51
|
+
const value = raw === from ? to : raw;
|
|
52
|
+
if (!out.includes(value))
|
|
53
|
+
out.push(value);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export function migrateLegacyClawChatChannelConfig(config) {
|
|
58
|
+
const changes = [];
|
|
59
|
+
if (!isRecord(config))
|
|
60
|
+
return { config, changes };
|
|
61
|
+
const next = structuredClone(config);
|
|
62
|
+
// 1. channels — move/merge the old channel block onto the new id.
|
|
63
|
+
const channels = next.channels;
|
|
64
|
+
if (isRecord(channels)) {
|
|
65
|
+
const oldChannel = channels[LEGACY_CHANNEL_ID];
|
|
66
|
+
if (isNonEmptyRecord(oldChannel)) {
|
|
67
|
+
const newChannel = channels[TARGET_CHANNEL_ID];
|
|
68
|
+
if (!isNonEmptyRecord(newChannel)) {
|
|
69
|
+
channels[TARGET_CHANNEL_ID] = oldChannel;
|
|
70
|
+
changes.push(`Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
mergeMissing(newChannel, oldChannel);
|
|
74
|
+
changes.push(`Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`);
|
|
75
|
+
}
|
|
76
|
+
delete channels[LEGACY_CHANNEL_ID];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// plugins.* live under a single `plugins` record.
|
|
80
|
+
const plugins = next.plugins;
|
|
81
|
+
if (isRecord(plugins)) {
|
|
82
|
+
// 2. plugins.allow — replace old id with new id (append if missing), dedup.
|
|
83
|
+
const allow = plugins.allow;
|
|
84
|
+
if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
|
|
85
|
+
const replaced = replaceAndDedup(allow.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
|
|
86
|
+
plugins.allow = replaced;
|
|
87
|
+
changes.push(`Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
88
|
+
}
|
|
89
|
+
// 3. plugins.entries — merge-missing the old entry into the new one.
|
|
90
|
+
const entries = plugins.entries;
|
|
91
|
+
if (isRecord(entries)) {
|
|
92
|
+
const oldEntry = entries[LEGACY_CHANNEL_ID];
|
|
93
|
+
if (oldEntry !== undefined) {
|
|
94
|
+
if (isRecord(oldEntry)) {
|
|
95
|
+
const newEntry = entries[TARGET_CHANNEL_ID];
|
|
96
|
+
if (!isRecord(newEntry)) {
|
|
97
|
+
entries[TARGET_CHANNEL_ID] = oldEntry;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
mergeMissing(newEntry, oldEntry);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
delete entries[LEGACY_CHANNEL_ID];
|
|
104
|
+
changes.push(`Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
|
|
109
|
+
const tools = next.tools;
|
|
110
|
+
if (isRecord(tools)) {
|
|
111
|
+
for (const key of ["allow", "alsoAllow"]) {
|
|
112
|
+
const list = tools[key];
|
|
113
|
+
if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
|
|
114
|
+
tools[key] = replaceAndDedup(list.filter((v) => typeof v === "string"), LEGACY_CHANNEL_ID, TARGET_CHANNEL_ID);
|
|
115
|
+
changes.push(`Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { config: next, changes };
|
|
120
|
+
}
|
package/index.ts
CHANGED
|
@@ -20,5 +20,12 @@ export default defineChannelPluginEntry({
|
|
|
20
20
|
registerOpenclawClawlingCommands(api);
|
|
21
21
|
registerClawChatPromptInjection(api as unknown as ClawChatPromptInjectionApi);
|
|
22
22
|
registerOpenclawClawlingTools(api);
|
|
23
|
+
// NOTE: the legacy-rename config migration is intentionally NOT registered
|
|
24
|
+
// here. The host's setup-migration runner only loads a plugin's SETUP source
|
|
25
|
+
// (`openclaw.setupEntry`/`runtimeSetupEntry` → setup-entry.ts) and calls its
|
|
26
|
+
// `register(api)` in "setup-only" mode; registrations made in `registerFull`
|
|
27
|
+
// (full/tool-discovery modes) are never collected for migrations, and this
|
|
28
|
+
// gated full-load path doesn't run for the rename scenario anyway. The
|
|
29
|
+
// migration is wired into setup-entry.ts instead.
|
|
23
30
|
},
|
|
24
31
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawchat-plugin-openclaw",
|
|
3
3
|
"kind": "channel",
|
|
4
|
+
"legacyPluginIds": ["openclaw-clawchat"],
|
|
5
|
+
"configContracts": {
|
|
6
|
+
"compatibilityMigrationPaths": [
|
|
7
|
+
"channels.openclaw-clawchat",
|
|
8
|
+
"plugins.entries.openclaw-clawchat"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
"channels": ["clawchat-plugin-openclaw"],
|
|
5
12
|
"skills": ["./skills"],
|
|
6
13
|
"activation": {
|
package/package.json
CHANGED
package/setup-entry.ts
CHANGED
|
@@ -1,4 +1,54 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
1
2
|
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
3
|
+
import { CHANNEL_ID } from "./src/config.ts";
|
|
2
4
|
import { openclawClawlingSetupPlugin } from "./src/channel.setup.ts";
|
|
5
|
+
import { migrateLegacyClawChatChannelConfig } from "./src/config-compat.ts";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Minimal shape of the host setup-registration api we touch. The host calls
|
|
9
|
+
* `register(api)` on the SETUP source (this module) in `registrationMode:
|
|
10
|
+
* "setup-only"`; `registerConfigMigration` is the hook that collects
|
|
11
|
+
* compatibility migrations (run by `openclaw doctor` / setup). It is optional
|
|
12
|
+
* here so a host that doesn't expose it can't throw.
|
|
13
|
+
*/
|
|
14
|
+
interface SetupRegisterApi {
|
|
15
|
+
registerConfigMigration?: (
|
|
16
|
+
migration: (config: OpenClawConfig) => {
|
|
17
|
+
config: OpenClawConfig;
|
|
18
|
+
changes: string[];
|
|
19
|
+
},
|
|
20
|
+
) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Host setup-source `register` hook.
|
|
25
|
+
*
|
|
26
|
+
* The OpenClaw setup-migration runner (`setup-registry`) loads this module as
|
|
27
|
+
* the plugin's setup source (resolved from `package.json` `openclaw.setupEntry`
|
|
28
|
+
* / `runtimeSetupEntry`), unwraps the default export, and — when that export is
|
|
29
|
+
* an object exposing a `register` function whose `id` matches the plugin id —
|
|
30
|
+
* calls `register(api)` in setup-only mode. This is the ONLY ungated path that
|
|
31
|
+
* collects config migrations for the plugin-rename scenario.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors the host's own built-in pattern (e.g. amazon-bedrock setup-api.js:
|
|
34
|
+
* `api.registerConfigMigration((config) => migrateAmazonBedrockLegacyConfig(config))`).
|
|
35
|
+
*/
|
|
36
|
+
export function register(api: SetupRegisterApi): void {
|
|
37
|
+
api.registerConfigMigration?.((config) =>
|
|
38
|
+
migrateLegacyClawChatChannelConfig(config),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default export consumed by BOTH host loaders of this setup source:
|
|
44
|
+
* - the channel-setup loader unwraps `default` and reads `.plugin`
|
|
45
|
+
* (`defineSetupPluginEntry` yields `{ plugin }`);
|
|
46
|
+
* - the setup-migration runner unwraps `default` and reads `.register`,
|
|
47
|
+
* rejecting it unless `.id` matches the plugin id.
|
|
48
|
+
* So the default export must carry `plugin`, `id`, and `register` together.
|
|
49
|
+
*/
|
|
50
|
+
export default {
|
|
51
|
+
...defineSetupPluginEntry(openclawClawlingSetupPlugin),
|
|
52
|
+
id: CHANNEL_ID,
|
|
53
|
+
register,
|
|
54
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { CHANNEL_ID } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compatibility migration for the plugin rename (commit 260044f): the plugin
|
|
6
|
+
* `id` and channel key changed from the OLD `openclaw-clawchat` to the NEW
|
|
7
|
+
* `clawchat-plugin-openclaw` (= {@link CHANNEL_ID}).
|
|
8
|
+
*
|
|
9
|
+
* Users upgrading from the old version have all their state — channel block
|
|
10
|
+
* (token/userId/ownerUserId/refreshToken/…), `plugins.allow`,
|
|
11
|
+
* `plugins.entries`, and `tools.allow`/`tools.alsoAllow` — keyed under the old
|
|
12
|
+
* id. The new code only reads the new id, so the channel silently fails to
|
|
13
|
+
* load. This migration moves the old-keyed state onto the new id.
|
|
14
|
+
*
|
|
15
|
+
* The function is PURE: it clones the input via `structuredClone` and never
|
|
16
|
+
* mutates its argument. It is IDEMPOTENT: a config with no old id anywhere
|
|
17
|
+
* returns an equivalent config with `changes: []`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const LEGACY_CHANNEL_ID = "openclaw-clawchat" as const;
|
|
21
|
+
export const TARGET_CHANNEL_ID = CHANNEL_ID;
|
|
22
|
+
|
|
23
|
+
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
24
|
+
|
|
25
|
+
function isBlockedObjectKey(key: string): boolean {
|
|
26
|
+
return BLOCKED_OBJECT_KEYS.has(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isNonEmptyRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Copy keys from `source` into `target` only when they are absent/undefined in
|
|
39
|
+
* `target` (target is more current — never overwrite an existing value).
|
|
40
|
+
* Recurses into nested records. Skips prototype-pollution keys.
|
|
41
|
+
*/
|
|
42
|
+
function mergeMissing(
|
|
43
|
+
target: Record<string, unknown>,
|
|
44
|
+
source: Record<string, unknown>,
|
|
45
|
+
): void {
|
|
46
|
+
for (const [key, value] of Object.entries(source)) {
|
|
47
|
+
if (value === undefined || isBlockedObjectKey(key)) continue;
|
|
48
|
+
const existing = target[key];
|
|
49
|
+
if (existing === undefined) {
|
|
50
|
+
target[key] = value;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isRecord(existing) && isRecord(value)) mergeMissing(existing, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Replace `from` → `to` in a string array, preserving order and deduping. */
|
|
58
|
+
function replaceAndDedup(list: string[], from: string, to: string): string[] {
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
for (const raw of list) {
|
|
61
|
+
const value = raw === from ? to : raw;
|
|
62
|
+
if (!out.includes(value)) out.push(value);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function migrateLegacyClawChatChannelConfig(config: OpenClawConfig): {
|
|
68
|
+
config: OpenClawConfig;
|
|
69
|
+
changes: string[];
|
|
70
|
+
} {
|
|
71
|
+
const changes: string[] = [];
|
|
72
|
+
if (!isRecord(config)) return { config, changes };
|
|
73
|
+
|
|
74
|
+
const next = structuredClone(config) as Record<string, unknown>;
|
|
75
|
+
|
|
76
|
+
// 1. channels — move/merge the old channel block onto the new id.
|
|
77
|
+
const channels = next.channels;
|
|
78
|
+
if (isRecord(channels)) {
|
|
79
|
+
const oldChannel = channels[LEGACY_CHANNEL_ID];
|
|
80
|
+
if (isNonEmptyRecord(oldChannel)) {
|
|
81
|
+
const newChannel = channels[TARGET_CHANNEL_ID];
|
|
82
|
+
if (!isNonEmptyRecord(newChannel)) {
|
|
83
|
+
channels[TARGET_CHANNEL_ID] = oldChannel;
|
|
84
|
+
changes.push(
|
|
85
|
+
`Moved channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
mergeMissing(newChannel, oldChannel);
|
|
89
|
+
changes.push(
|
|
90
|
+
`Merged channel "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}" (filled missing fields; kept existing values).`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
delete channels[LEGACY_CHANNEL_ID];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// plugins.* live under a single `plugins` record.
|
|
98
|
+
const plugins = next.plugins;
|
|
99
|
+
if (isRecord(plugins)) {
|
|
100
|
+
// 2. plugins.allow — replace old id with new id (append if missing), dedup.
|
|
101
|
+
const allow = plugins.allow;
|
|
102
|
+
if (Array.isArray(allow) && allow.includes(LEGACY_CHANNEL_ID)) {
|
|
103
|
+
const replaced = replaceAndDedup(
|
|
104
|
+
allow.filter((v): v is string => typeof v === "string"),
|
|
105
|
+
LEGACY_CHANNEL_ID,
|
|
106
|
+
TARGET_CHANNEL_ID,
|
|
107
|
+
);
|
|
108
|
+
plugins.allow = replaced;
|
|
109
|
+
changes.push(
|
|
110
|
+
`Updated plugins.allow: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. plugins.entries — merge-missing the old entry into the new one.
|
|
115
|
+
const entries = plugins.entries;
|
|
116
|
+
if (isRecord(entries)) {
|
|
117
|
+
const oldEntry = entries[LEGACY_CHANNEL_ID];
|
|
118
|
+
if (oldEntry !== undefined) {
|
|
119
|
+
if (isRecord(oldEntry)) {
|
|
120
|
+
const newEntry = entries[TARGET_CHANNEL_ID];
|
|
121
|
+
if (!isRecord(newEntry)) {
|
|
122
|
+
entries[TARGET_CHANNEL_ID] = oldEntry;
|
|
123
|
+
} else {
|
|
124
|
+
mergeMissing(newEntry, oldEntry);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
delete entries[LEGACY_CHANNEL_ID];
|
|
128
|
+
changes.push(
|
|
129
|
+
`Merged plugins.entries["${LEGACY_CHANNEL_ID}"] → plugins.entries["${TARGET_CHANNEL_ID}"].`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 4. tools.allow + tools.alsoAllow — replace old id with new id, dedup.
|
|
136
|
+
const tools = next.tools;
|
|
137
|
+
if (isRecord(tools)) {
|
|
138
|
+
for (const key of ["allow", "alsoAllow"] as const) {
|
|
139
|
+
const list = tools[key];
|
|
140
|
+
if (Array.isArray(list) && list.includes(LEGACY_CHANNEL_ID)) {
|
|
141
|
+
tools[key] = replaceAndDedup(
|
|
142
|
+
list.filter((v): v is string => typeof v === "string"),
|
|
143
|
+
LEGACY_CHANNEL_ID,
|
|
144
|
+
TARGET_CHANNEL_ID,
|
|
145
|
+
);
|
|
146
|
+
changes.push(
|
|
147
|
+
`Updated tools.${key}: "${LEGACY_CHANNEL_ID}" → "${TARGET_CHANNEL_ID}".`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { config: next as OpenClawConfig, changes };
|
|
154
|
+
}
|