Skip to content

Commit 19065e4

Browse files
authored
Improve Telegram groups config shape diagnostics (#83260)
* Improve Telegram groups config diagnostics Add targeted guidance when channels.telegram.groups uses a non-object shape so startup/config validation and doctor explain the required group-id object map and topic nesting. * fix(config): keep channel validation hints generic
1 parent 88585da commit 19065e4

4 files changed

Lines changed: 123 additions & 6 deletions

File tree

extensions/telegram/src/doctor.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {
55
collectTelegramApiRootWarnings,
66
collectTelegramEmptyAllowlistExtraWarnings,
77
collectTelegramGroupPolicyWarnings,
8+
collectTelegramMalformedGroupsWarnings,
89
collectTelegramMissingEnvTokenWarnings,
910
collectTelegramSelectedQuoteToolProgressWarnings,
1011
maybeRepairTelegramApiRoots,
1112
maybeRepairTelegramAllowFromUsernames,
1213
scanTelegramBotEndpointApiRoots,
1314
scanTelegramInvalidAllowFromEntries,
15+
scanTelegramMalformedGroupsConfig,
1416
scanTelegramSelectedQuoteToolProgressWarnings,
1517
telegramDoctor,
1618
} from "./doctor.js";
@@ -206,6 +208,42 @@ describe("telegram doctor", () => {
206208
).toHaveLength(1);
207209
});
208210

211+
it("warns when Telegram groups use a non-object shape", async () => {
212+
const cfg = {
213+
channels: {
214+
telegram: {
215+
groups: ["-1001234567890"],
216+
accounts: {
217+
work: {
218+
groups: null,
219+
},
220+
},
221+
},
222+
},
223+
} as unknown as OpenClawConfig;
224+
225+
const hits = scanTelegramMalformedGroupsConfig(cfg);
226+
expect(hits).toEqual([
227+
{ path: "channels.telegram.groups", actualType: "array" },
228+
{ path: "channels.telegram.accounts.work.groups", actualType: "null" },
229+
]);
230+
231+
const warnings = collectTelegramMalformedGroupsWarnings({
232+
hits,
233+
doctorFixCommand: "openclaw doctor --fix",
234+
});
235+
expect(warnings[0]).toContain("object map keyed by Telegram group/chat id");
236+
expect(warnings[1]).toContain('channels.telegram.groups."-1001234567890".topics."99"');
237+
expect(warnings[1]).toContain("openclaw doctor --fix");
238+
239+
expect(
240+
await telegramDoctor.collectPreviewWarnings?.({
241+
cfg,
242+
doctorFixCommand: "openclaw doctor --fix",
243+
}),
244+
).toEqual(expect.arrayContaining(warnings));
245+
});
246+
209247
it("repairs @username entries to numeric ids", async () => {
210248
lookupTelegramChatIdMock.mockResolvedValue("111");
211249

extensions/telegram/src/doctor.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
2727

2828
type TelegramAllowFromInvalidHit = { path: string; entry: string };
29+
type TelegramMalformedGroupsHit = { path: string; actualType: string };
2930
type TelegramSelectedQuoteToolProgressHit = { path: string; replyToMode: string };
3031
type TelegramApiRootBotEndpointHit = {
3132
path: string;
@@ -131,6 +132,53 @@ function collectTelegramAllowFromLists(
131132
return refs;
132133
}
133134

135+
function describeConfigValueType(value: unknown): string {
136+
if (Array.isArray(value)) {
137+
return "array";
138+
}
139+
if (value === null) {
140+
return "null";
141+
}
142+
return typeof value;
143+
}
144+
145+
export function scanTelegramMalformedGroupsConfig(
146+
cfg: OpenClawConfig,
147+
): TelegramMalformedGroupsHit[] {
148+
const hits: TelegramMalformedGroupsHit[] = [];
149+
for (const scope of collectTelegramAccountScopes(cfg)) {
150+
if (!Object.prototype.hasOwnProperty.call(scope.account, "groups")) {
151+
continue;
152+
}
153+
const groups = scope.account.groups;
154+
if (asObjectRecord(groups)) {
155+
continue;
156+
}
157+
hits.push({
158+
path: `${scope.prefix}.groups`,
159+
actualType: describeConfigValueType(groups),
160+
});
161+
}
162+
return hits;
163+
}
164+
165+
export function collectTelegramMalformedGroupsWarnings(params: {
166+
hits: TelegramMalformedGroupsHit[];
167+
doctorFixCommand: string;
168+
}): string[] {
169+
if (params.hits.length === 0) {
170+
return [];
171+
}
172+
const sample = params.hits[0] ?? {
173+
path: "channels.telegram.groups",
174+
actualType: "unknown",
175+
};
176+
return [
177+
`- ${sanitizeForLog(sample.path)} has invalid Telegram groups shape (${sanitizeForLog(sample.actualType)}); expected an object map keyed by Telegram group/chat id, not an array, string, or null.`,
178+
`- Example shape: channels.telegram.groups."-1001234567890".topics."99" = { agentId: "support" }. Use topics for forum-topic routing, then rerun ${params.doctorFixCommand} for any remaining Telegram config cleanup.`,
179+
];
180+
}
181+
134182
export function scanTelegramInvalidAllowFromEntries(
135183
cfg: OpenClawConfig,
136184
): TelegramAllowFromInvalidHit[] {
@@ -557,6 +605,10 @@ export const telegramDoctor: ChannelDoctorAdapter = {
557605
normalizeCompatibilityConfig: normalizeTelegramCompatibilityConfig,
558606
collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [
559607
...collectTelegramMissingEnvTokenWarnings({ cfg, env }),
608+
...collectTelegramMalformedGroupsWarnings({
609+
hits: scanTelegramMalformedGroupsConfig(cfg),
610+
doctorFixCommand,
611+
}),
560612
...collectTelegramInvalidAllowFromWarnings({
561613
hits: scanTelegramInvalidAllowFromEntries(cfg),
562614
doctorFixCommand,

src/config/validation.channel-metadata.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,28 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => {
246246

247247
expect(result.ok).toBe(true);
248248
});
249+
250+
it("keeps raw channel validation diagnostics plugin-agnostic", () => {
251+
const result = validateConfigObjectRawWithPlugins({
252+
channels: {
253+
telegram: {
254+
groups: ["-1001234567890"],
255+
},
256+
},
257+
});
258+
259+
expect(result.ok).toBe(false);
260+
if (!result.ok) {
261+
expect(result.issues).toContainEqual(
262+
expect.objectContaining({
263+
path: "channels.telegram.groups",
264+
message: expect.stringContaining("invalid config:"),
265+
}),
266+
);
267+
expect(result.issues[0]?.message).not.toContain("Telegram groups");
268+
expect(result.issues[0]?.message).not.toContain("openclaw doctor --fix");
269+
}
270+
});
249271
});
250272

251273
describe("validateConfigObjectRawWithPlugins plugin config defaults", () => {

src/config/validation.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ function collectAllowedValuesFromBundledChannelSchemaPath(
231231
return collectAllowedValuesFromJsonSchemaNode(targetNode);
232232
}
233233

234+
function formatRawChannelConfigIssueMessage(message: string): string {
235+
return `invalid config: ${message}`;
236+
}
234237
function collectRawBundledChannelConfigIssues(config: OpenClawConfig): ConfigValidationIssue[] {
235238
if (!config.channels || !isRecord(config.channels)) {
236239
return [];
@@ -253,10 +256,11 @@ function collectRawBundledChannelConfigIssues(config: OpenClawConfig): ConfigVal
253256
const message = error.additionalProperty
254257
? `${error.message}: "${error.additionalProperty}"`
255258
: error.message;
259+
const path =
260+
error.path === "<root>" ? `channels.${channelId}` : `channels.${channelId}.${error.path}`;
256261
issues.push({
257-
path:
258-
error.path === "<root>" ? `channels.${channelId}` : `channels.${channelId}.${error.path}`,
259-
message: `invalid config: ${message}`,
262+
path,
263+
message: formatRawChannelConfigIssueMessage(message),
260264
allowedValues: error.allowedValues,
261265
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
262266
});
@@ -1401,10 +1405,11 @@ function validateConfigObjectWithPluginsBase(
14011405
});
14021406
if (!result.ok) {
14031407
for (const error of result.errors) {
1408+
const path =
1409+
error.path === "<root>" ? `channels.${trimmed}` : `channels.${trimmed}.${error.path}`;
14041410
issues.push({
1405-
path:
1406-
error.path === "<root>" ? `channels.${trimmed}` : `channels.${trimmed}.${error.path}`,
1407-
message: `invalid config: ${error.message}`,
1411+
path,
1412+
message: formatRawChannelConfigIssueMessage(error.message),
14081413
allowedValues: error.allowedValues,
14091414
allowedValuesHiddenCount: error.allowedValuesHiddenCount,
14101415
});

0 commit comments

Comments
 (0)