mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:40:22 +00:00
Config: require Discord ID strings (#18220)
This commit is contained in:
@@ -120,4 +120,113 @@ describe("doctor config flow", () => {
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("converts numeric discord ids to strings on repair", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "openclaw.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
allowFrom: [123],
|
||||
dm: { allowFrom: [456], groupChannels: [789] },
|
||||
execApprovals: { approvers: [321] },
|
||||
guilds: {
|
||||
"100": {
|
||||
users: [111],
|
||||
roles: [222],
|
||||
channels: {
|
||||
general: { users: [333], roles: [444] },
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
work: {
|
||||
allowFrom: [555],
|
||||
dm: { allowFrom: [666], groupChannels: [777] },
|
||||
execApprovals: { approvers: [888] },
|
||||
guilds: {
|
||||
"200": {
|
||||
users: [999],
|
||||
roles: [1010],
|
||||
channels: {
|
||||
help: { users: [1111], roles: [1212] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await loadAndMaybeMigrateDoctorConfig({
|
||||
options: { nonInteractive: true, repair: true },
|
||||
confirm: async () => false,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
discord: {
|
||||
allowFrom: string[];
|
||||
dm: { allowFrom: string[]; groupChannels: string[] };
|
||||
execApprovals: { approvers: string[] };
|
||||
guilds: Record<
|
||||
string,
|
||||
{
|
||||
users: string[];
|
||||
roles: string[];
|
||||
channels: Record<string, { users: string[]; roles: string[] }>;
|
||||
}
|
||||
>;
|
||||
accounts: Record<
|
||||
string,
|
||||
{
|
||||
allowFrom: string[];
|
||||
dm: { allowFrom: string[]; groupChannels: string[] };
|
||||
execApprovals: { approvers: string[] };
|
||||
guilds: Record<
|
||||
string,
|
||||
{
|
||||
users: string[];
|
||||
roles: string[];
|
||||
channels: Record<string, { users: string[]; roles: string[] }>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(cfg.channels.discord.allowFrom).toEqual(["123"]);
|
||||
expect(cfg.channels.discord.dm.allowFrom).toEqual(["456"]);
|
||||
expect(cfg.channels.discord.dm.groupChannels).toEqual(["789"]);
|
||||
expect(cfg.channels.discord.execApprovals.approvers).toEqual(["321"]);
|
||||
expect(cfg.channels.discord.guilds["100"].users).toEqual(["111"]);
|
||||
expect(cfg.channels.discord.guilds["100"].roles).toEqual(["222"]);
|
||||
expect(cfg.channels.discord.guilds["100"].channels.general.users).toEqual(["333"]);
|
||||
expect(cfg.channels.discord.guilds["100"].channels.general.roles).toEqual(["444"]);
|
||||
expect(cfg.channels.discord.accounts.work.allowFrom).toEqual(["555"]);
|
||||
expect(cfg.channels.discord.accounts.work.dm.allowFrom).toEqual(["666"]);
|
||||
expect(cfg.channels.discord.accounts.work.dm.groupChannels).toEqual(["777"]);
|
||||
expect(cfg.channels.discord.accounts.work.execApprovals.approvers).toEqual(["888"]);
|
||||
expect(cfg.channels.discord.accounts.work.guilds["200"].users).toEqual(["999"]);
|
||||
expect(cfg.channels.discord.accounts.work.guilds["200"].roles).toEqual(["1010"]);
|
||||
expect(cfg.channels.discord.accounts.work.guilds["200"].channels.help.users).toEqual([
|
||||
"1111",
|
||||
]);
|
||||
expect(cfg.channels.discord.accounts.work.guilds["200"].channels.help.roles).toEqual([
|
||||
"1212",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -395,6 +395,164 @@ async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig): Promi
|
||||
return { config: next, changes };
|
||||
}
|
||||
|
||||
type DiscordNumericIdHit = { path: string; entry: number };
|
||||
|
||||
type DiscordIdListRef = {
|
||||
pathLabel: string;
|
||||
holder: Record<string, unknown>;
|
||||
key: string;
|
||||
};
|
||||
|
||||
function collectDiscordAccountScopes(
|
||||
cfg: OpenClawConfig,
|
||||
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
||||
const scopes: Array<{ prefix: string; account: Record<string, unknown> }> = [];
|
||||
const discord = asObjectRecord(cfg.channels?.discord);
|
||||
if (!discord) {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
scopes.push({ prefix: "channels.discord", account: discord });
|
||||
const accounts = asObjectRecord(discord.accounts);
|
||||
if (!accounts) {
|
||||
return scopes;
|
||||
}
|
||||
for (const key of Object.keys(accounts)) {
|
||||
const account = asObjectRecord(accounts[key]);
|
||||
if (!account) {
|
||||
continue;
|
||||
}
|
||||
scopes.push({ prefix: `channels.discord.accounts.${key}`, account });
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
function collectDiscordIdLists(
|
||||
prefix: string,
|
||||
account: Record<string, unknown>,
|
||||
): DiscordIdListRef[] {
|
||||
const refs: DiscordIdListRef[] = [
|
||||
{ pathLabel: `${prefix}.allowFrom`, holder: account, key: "allowFrom" },
|
||||
];
|
||||
const dm = asObjectRecord(account.dm);
|
||||
if (dm) {
|
||||
refs.push({ pathLabel: `${prefix}.dm.allowFrom`, holder: dm, key: "allowFrom" });
|
||||
refs.push({ pathLabel: `${prefix}.dm.groupChannels`, holder: dm, key: "groupChannels" });
|
||||
}
|
||||
const execApprovals = asObjectRecord(account.execApprovals);
|
||||
if (execApprovals) {
|
||||
refs.push({
|
||||
pathLabel: `${prefix}.execApprovals.approvers`,
|
||||
holder: execApprovals,
|
||||
key: "approvers",
|
||||
});
|
||||
}
|
||||
const guilds = asObjectRecord(account.guilds);
|
||||
if (!guilds) {
|
||||
return refs;
|
||||
}
|
||||
|
||||
for (const guildId of Object.keys(guilds)) {
|
||||
const guild = asObjectRecord(guilds[guildId]);
|
||||
if (!guild) {
|
||||
continue;
|
||||
}
|
||||
refs.push({ pathLabel: `${prefix}.guilds.${guildId}.users`, holder: guild, key: "users" });
|
||||
refs.push({ pathLabel: `${prefix}.guilds.${guildId}.roles`, holder: guild, key: "roles" });
|
||||
const channels = asObjectRecord(guild.channels);
|
||||
if (!channels) {
|
||||
continue;
|
||||
}
|
||||
for (const channelId of Object.keys(channels)) {
|
||||
const channel = asObjectRecord(channels[channelId]);
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
refs.push({
|
||||
pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.users`,
|
||||
holder: channel,
|
||||
key: "users",
|
||||
});
|
||||
refs.push({
|
||||
pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.roles`,
|
||||
holder: channel,
|
||||
key: "roles",
|
||||
});
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function scanDiscordNumericIdEntries(cfg: OpenClawConfig): DiscordNumericIdHit[] {
|
||||
const hits: DiscordNumericIdHit[] = [];
|
||||
const scanList = (pathLabel: string, list: unknown) => {
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
for (const [index, entry] of list.entries()) {
|
||||
if (typeof entry !== "number") {
|
||||
continue;
|
||||
}
|
||||
hits.push({ path: `${pathLabel}[${index}]`, entry });
|
||||
}
|
||||
};
|
||||
|
||||
for (const scope of collectDiscordAccountScopes(cfg)) {
|
||||
for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) {
|
||||
scanList(ref.pathLabel, ref.holder[ref.key]);
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
function maybeRepairDiscordNumericIds(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
const hits = scanDiscordNumericIdEntries(cfg);
|
||||
if (hits.length === 0) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const next = structuredClone(cfg);
|
||||
const changes: string[] = [];
|
||||
|
||||
const repairList = (pathLabel: string, holder: Record<string, unknown>, key: string) => {
|
||||
const raw = holder[key];
|
||||
if (!Array.isArray(raw)) {
|
||||
return;
|
||||
}
|
||||
let converted = 0;
|
||||
const updated = raw.map((entry) => {
|
||||
if (typeof entry === "number") {
|
||||
converted += 1;
|
||||
return String(entry);
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
if (converted === 0) {
|
||||
return;
|
||||
}
|
||||
holder[key] = updated;
|
||||
changes.push(
|
||||
`- ${pathLabel}: converted ${converted} numeric ${converted === 1 ? "entry" : "entries"} to strings`,
|
||||
);
|
||||
};
|
||||
|
||||
for (const scope of collectDiscordAccountScopes(next)) {
|
||||
for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) {
|
||||
repairList(ref.pathLabel, ref.holder, ref.key);
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
return { config: next, changes };
|
||||
}
|
||||
|
||||
async function maybeMigrateLegacyConfig(): Promise<string[]> {
|
||||
const changes: string[] = [];
|
||||
const home = resolveHomeDir();
|
||||
@@ -533,6 +691,14 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
pendingChanges = true;
|
||||
cfg = repair.config;
|
||||
}
|
||||
|
||||
const discordRepair = maybeRepairDiscordNumericIds(candidate);
|
||||
if (discordRepair.changes.length > 0) {
|
||||
note(discordRepair.changes.join("\n"), "Doctor changes");
|
||||
candidate = discordRepair.config;
|
||||
pendingChanges = true;
|
||||
cfg = discordRepair.config;
|
||||
}
|
||||
} else {
|
||||
const hits = scanTelegramAllowFromUsernameEntries(candidate);
|
||||
if (hits.length > 0) {
|
||||
@@ -544,6 +710,17 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
|
||||
const discordHits = scanDiscordNumericIdEntries(candidate);
|
||||
if (discordHits.length > 0) {
|
||||
note(
|
||||
[
|
||||
`- Discord allowlists contain ${discordHits.length} numeric entries (e.g. ${discordHits[0]?.path}=${discordHits[0]?.entry}).`,
|
||||
`- Discord IDs must be strings; run "${formatCliCommand("openclaw doctor --fix")}" to convert numeric IDs to quoted strings.`,
|
||||
].join("\n"),
|
||||
"Doctor warnings",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const unknown = stripUnknownConfigKeys(candidate);
|
||||
|
||||
Reference in New Issue
Block a user