diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a16b88f52..1970b546e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai - Cron/Run log: clean up settled per-path run-log write queue entries so long-running cron uptime does not retain stale promise bookkeeping in memory. - Cron/Run log: harden `cron.runs` run-log path resolution by rejecting path-separator `id`/`jobId` inputs and enforcing reads within the per-cron `runs/` directory. - Cron/Announce: when announce delivery target resolution fails (for example multiple configured channels with no explicit target), skip injecting fallback `Cron (error): ...` into the main session so runs fail cleanly without accidental last-route sends. (#24074) +- Cron/Telegram: validate cron `delivery.to` with shared Telegram target parsing and resolve legacy `@username`/`t.me` targets to numeric IDs at send-time for deterministic delivery target writeback. (#21930) Thanks @kesor. - Cron/Isolation: force fresh session IDs for isolated cron runs so `sessionTarget="isolated"` executions never reuse prior run context. (#23470) Thanks @echoVic. - Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams). - Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) diff --git a/src/cron/service.jobs.test.ts b/src/cron/service.jobs.test.ts index e80e957d62e..c5c0632475b 100644 --- a/src/cron/service.jobs.test.ts +++ b/src/cron/service.jobs.test.ts @@ -152,6 +152,87 @@ describe("applyJobPatch", () => { ).not.toThrow(); expect(job.delivery).toEqual({ mode: "webhook", to: "https://example.invalid/trim" }); }); + + it("rejects Telegram delivery with invalid target (chatId/topicId format)", () => { + const job = createIsolatedAgentTurnJob("job-telegram-invalid", { + mode: "announce", + channel: "telegram", + to: "-10012345/6789", + }); + + expect(() => applyJobPatch(job, { enabled: true })).toThrow( + 'Invalid Telegram delivery target "-10012345/6789". Use colon (:) as delimiter for topics, not slash. Valid formats: -1001234567890, -1001234567890:123, -1001234567890:topic:123, @username, https://t.me/username', + ); + }); + + it("accepts Telegram delivery with t.me URL", () => { + const job = createIsolatedAgentTurnJob("job-telegram-tme", { + mode: "announce", + channel: "telegram", + to: "https://t.me/mychannel", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery with t.me URL (no https)", () => { + const job = createIsolatedAgentTurnJob("job-telegram-tme-no-https", { + mode: "announce", + channel: "telegram", + to: "t.me/mychannel", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery with valid target (plain chat id)", () => { + const job = createIsolatedAgentTurnJob("job-telegram-valid", { + mode: "announce", + channel: "telegram", + to: "-1001234567890", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery with valid target (colon delimiter)", () => { + const job = createIsolatedAgentTurnJob("job-telegram-valid-colon", { + mode: "announce", + channel: "telegram", + to: "-1001234567890:123", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery with valid target (topic marker)", () => { + const job = createIsolatedAgentTurnJob("job-telegram-valid-topic", { + mode: "announce", + channel: "telegram", + to: "-1001234567890:topic:456", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery without target", () => { + const job = createIsolatedAgentTurnJob("job-telegram-no-target", { + mode: "announce", + channel: "telegram", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); + + it("accepts Telegram delivery with @username", () => { + const job = createIsolatedAgentTurnJob("job-telegram-username", { + mode: "announce", + channel: "telegram", + to: "@mybot", + }); + + expect(() => applyJobPatch(job, { enabled: true })).not.toThrow(); + }); }); function createMockState(now: number): CronServiceState { diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 19b8d26e91b..db42b80ba54 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -83,6 +83,23 @@ export function assertSupportedJobSpec(job: Pick) { if (!job.delivery) { return; @@ -98,6 +115,12 @@ function assertDeliverySupport(job: Pick) if (job.sessionTarget !== "isolated") { throw new Error('cron channel delivery config is only supported for sessionTarget="isolated"'); } + if (job.delivery.channel === "telegram") { + const telegramError = validateTelegramDeliveryTarget(job.delivery.to); + if (telegramError) { + throw new Error(telegramError); + } + } } export function findJobOrThrow(state: CronServiceState, id: string) {