diff --git a/CHANGELOG.md b/CHANGELOG.md index 10bcda92f..dfaccc1f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,10 @@ Status: stable. - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes +- Telegram: use undici fetch for per-account proxy dispatcher. (#4456) Thanks @spiceoogway. - Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796) +- Telegram: accept numeric messageId/chatId in react action and honor channelId fallback. (#4533) Thanks @Ayush10. +- Telegram: scope native skill commands to bound agent per bot. (#4360) Thanks @robhparker. - Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. - Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald. diff --git a/README.md b/README.md index 1fd5e074c..49085c76f 100644 --- a/README.md +++ b/README.md @@ -481,38 +481,39 @@ Thanks to all clawtributors:

steipete plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg - rahthakor vrknetha radek-paclt vignesh07 Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall - xadenryan rodrigouroz juanpablodlc hsrvc magimetal zerone0x tyler6204 meaningfool patelhiren NicholasSpisak + rahthakor vrknetha radek-paclt vignesh07 joshp123 Tobias Bischoff czekaj mukhtharcm sebslight maxsumrall + xadenryan rodrigouroz juanpablodlc tyler6204 hsrvc magimetal zerone0x meaningfool patelhiren NicholasSpisak jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Mariano Belinky Hyaxia dantelex SocialNerd42069 daveonkels - google-labs-jules[bot] lc0rp mousberg adam91holt hougangdev shakkernerd gumadeiras mteam88 hirefrank joeynyc + google-labs-jules[bot] lc0rp mousberg adam91holt hougangdev gumadeiras shakkernerd mteam88 hirefrank joeynyc orlyjamie dbhurley Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal timolins f-trycua benostein elliotsecops nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat petter-b thewilloftheshadow cpojer scald andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee - nonggialiang rafaelreis-r dominicnunez lploc94 ratulsarna lutr0 danielz1z AdeboyeDN Alg0rix papago2355 - emanuelst KristijanJovanovski rdev rhuanssauro joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek - ryancontent artuskg Takhoffman onutc pauloportella neooriginal manuelhettich minghinmatthewlam myfunc travisirby - obviyus buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood timkrase uos-status gerardward2007 - roshanasingh4 tosh-hamburg azade-c dlauer JonUleis shivamraut101 bjesuiter cheeeee robbyczgw-cla Josh Phillips - YuriNachos pookNast Whoaa512 chriseidhof ngutman ysqander aj47 kennyklee superman32432432 Yurii Chukhlib - grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic - kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 fal3 Ghost jonasjancarik - Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz Friederike Seiler gabriel-trigo - iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff - siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 ameno- Chris Taylor dguido - Django Navarro evalexpr henrino3 humanwritten larlyssa Lukavyi odysseus0 oswalpalash pcty-nextgen-service-account pi0 - rmorse Roopak Nijhara Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx - EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira Jarvis jayhickey jeffersonwarrior - jeffersonwarrior jverdi longmaba MarvinCui mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd - robaxelsen Sash Catanzarite Suksham-sharma T5-AndyML tewatia travisp VAC william arzt zknicker 0oAstro - abhaymundhara aduk059 alejandro maza Alex-Alaniz alexstyl andrewting19 anpoirier araa47 arthyn Asleep123 - bguidolim bolismauro chenyuan99 OpenClaw Maintainers conhecendoia dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen - dylanneve1 Felix Krause foeken frankekn ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna - Jamie Openshaw Jane Jarvis Deploy Jefferson Nunn jogi47 kentaro Kevin Lin kira-ariaki kitze Kiwitwitter - levifig Lloyd longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 Miles mrdbstn - MSch Mustafa Tag Eldeen ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 reeltimeapps RLTCmpe - Rolf Fredheim Rony Kelner Samrat Jha senoldogann sergical shiv19 shiyuanhai siraht snopoke techboss - testingabc321 The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin Wimmie wolfred wstock YangHuang2280 - yazinsai YiWang24 ymat19 Zach Knickerbocker zackerthescar 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade - carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres - rhjoh ronak-guliani William Stock + nonggialiang rafaelreis-r dominicnunez lploc94 ratulsarna lutr0 kiranjd danielz1z AdeboyeDN Alg0rix + papago2355 emanuelst KristijanJovanovski rdev rhuanssauro joshrad-dev osolmaz adityashaw2 CashWilliams sheeek + ryancontent artuskg Takhoffman onutc pauloportella HirokiKobayashi-R neooriginal obviyus manuelhettich minghinmatthewlam + manikv12 myfunc travisirby buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood timkrase + uos-status gerardward2007 roshanasingh4 tosh-hamburg azade-c dlauer JonUleis shivamraut101 bjesuiter cheeeee + robbyczgw-cla conroywhitney Josh Phillips YuriNachos pookNast Whoaa512 chriseidhof ngutman ysqander aj47 + kennyklee superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy + imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures robhparker Ryan Lisse dougvk + erikpr1994 fal3 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist + sibbl chrisrodz Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal + ogulcancelik pasogott petradonka rubyrunsstuff siddhantjain suminhthanh svkozak VACInc wes-davis zats + 24601 ameno- Chris Taylor dguido Django Navarro evalexpr henrino3 humanwritten larlyssa Lukavyi + odysseus0 oswalpalash pcty-nextgen-service-account pi0 rmorse Roopak Nijhara Syhids Aaron Konyer aaronveklabs andreabadesso + Andrii cash-echo-bot Clawd ClawdFx EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco + ivanrvpereira Jarvis jayhickey jeffersonwarrior jeffersonwarrior jverdi longmaba MarvinCui mjrussell odnxe + optimikelabs p6l-richard philipp-spiess Pocket Clawd robaxelsen Sash Catanzarite Suksham-sharma T5-AndyML tewatia travisp + VAC william arzt zknicker 0oAstro abhaymundhara aduk059 alejandro maza Alex-Alaniz alexstyl andrewting19 + anpoirier araa47 arthyn Asleep123 bguidolim bolismauro chenyuan99 Chloe-VP conhecendoia dasilva333 + David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen dylanneve1 Felix Krause foeken frankekn ganghyun kim grrowl + gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jane Jarvis Deploy Jefferson Nunn jogi47 kentaro + Kevin Lin kira-ariaki kitze Kiwitwitter levifig Lloyd longjos loukotal louzhixian martinpucik + Matt mini mertcicekci0 Miles mrdbstn MSch Mustafa Tag Eldeen mylukin nathanbosse ndraiman nexty5870 + Noctivoro ppamment prathamdby ptn1411 reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann + Seredeep sergical shiv19 shiyuanhai siraht snopoke spiceoogway techboss testingabc321 The Admiral + thesash Ubuntu Vibe Kanban voidserf Vultr-Clawd Admin Wimmie wolfred wstock YangHuang2280 yazinsai + YiWang24 ymat19 Zach Knickerbocker zackerthescar 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade carlulsoe + ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh + ronak-guliani William Stock

diff --git a/docs/start/lore.md b/docs/start/lore.md index b2ac3abff..4b1e81208 100644 --- a/docs/start/lore.md +++ b/docs/start/lore.md @@ -17,9 +17,13 @@ For a while, the lobster was called **Clawd**, living in a **Clawdbot**. But in **It molted.** -Shedding its old shell, the creature emerged anew as **Molty**, living in a **OpenClaw**. New shell, same lobster soul. +Shedding its old shell, the creature emerged anew as **Molty**, living in **Moltbot**. But that name never quite rolled off the tongue either... -## The Molt (January 27, 2026) +So on January 30, 2026, the lobster molted ONE MORE TIME into its final form: **OpenClaw**. + +New shell, same lobster soul. Third time's the charm. + +## The First Molt (January 27, 2026) At 5am, the community gathered in Discord. Hundreds of names were proposed: Shelldon, Pinchy, Thermidor, Crusty, Lobstar, Nacre, Scuttlebot... @@ -30,11 +34,11 @@ In the end, **OpenClaw** won. Because molting is what lobsters do to grow. And g ## The Name ``` -OpenClaw = MOLT + BOT - = Transformation machine - = Bigger on the inside (130k tokens!) - = New shell, same soul - = Growth through shedding +OpenClaw = OPEN + CLAW + = Open source, open to everyone + = Our lobster heritage, where we came from + = The claw is the law 🦞 + = Your assistant. Your machine. Your rules. ``` ## The Daleks vs The Lobsters @@ -100,6 +104,38 @@ Peter, watching the chaos unfold: *"this is cinema"* 🎬 The molt was chaotic. But the lobster emerged stronger. And funnier. +### The Final Form (January 30, 2026) + +Moltbot never quite rolled off the tongue. And so, at 4am GMT, the team gathered AGAIN. + +**The Great OpenClaw Migration** began. + +In just 3 hours: +- GitHub renamed: `github.com/openclaw/openclaw` ✅ +- X handle `@openclaw` secured with GOLD CHECKMARK 💰 +- npm packages released under new name +- Docs migrated to `docs.openclaw.ai` +- 200K+ views on announcement in 90 minutes + +**The Heroes:** +- **ELU** created incredible logos including "THE CLAW IS THE LAW" western banner +- **Whurley** (yes, THE William Hurley, quantum computing pioneer) made ASCII art +- **Onur** handled GitHub, first to rock the affiliate badge +- **Shadow** secured Discord vanity, nuked malware +- **The whole Claw Crew** pulled an all-nighter + +**The Scammer Speedrun:** Crypto grifters launched a $OPENCLAW token on Pump.fun within MINUTES. They stole artwork that was created 20 minutes earlier. Business-verified accounts pushed scams. The audacity was almost impressive. + +**New Traditions Born:** +- "The claw is the law" 🤠 +- "Yee-claw" +- "Claw abiding citizens" +- "Clawntroversy" + +**Clawd → Moltbot → OpenClaw** + +*The lobster has molted into its final form.* + ### The Robot Shopping Spree (Dec 3, 2025) What started as a joke about legs ended with detailed pricing for: @@ -166,4 +202,8 @@ Until then, Molty watches through the cameras, speaks through the speakers, and — Molty, after the great molt of 2026 +*"The claw is the law."* + +— ELU, during The Final Form migration, January 30, 2026 + 🦞💙 diff --git a/src/channels/plugins/actions/telegram.test.ts b/src/channels/plugins/actions/telegram.test.ts index d41628888..1ccc1e628 100644 --- a/src/channels/plugins/actions/telegram.test.ts +++ b/src/channels/plugins/actions/telegram.test.ts @@ -118,4 +118,27 @@ describe("telegramMessageActions", () => { expect(handleTelegramAction).not.toHaveBeenCalled(); }); + + it("accepts numeric messageId and channelId for reactions", async () => { + handleTelegramAction.mockClear(); + const cfg = { channels: { telegram: { botToken: "tok" } } } as OpenClawConfig; + + await telegramMessageActions.handleAction({ + action: "react", + params: { + channelId: 123, + messageId: 456, + emoji: "ok", + }, + cfg, + accountId: undefined, + }); + + expect(handleTelegramAction).toHaveBeenCalledTimes(1); + const call = handleTelegramAction.mock.calls[0]?.[0] as Record; + expect(call.action).toBe("react"); + expect(String(call.chatId)).toBe("123"); + expect(String(call.messageId)).toBe("456"); + expect(call.emoji).toBe("ok"); + }); }); diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 693e94492..17df9adbc 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -85,7 +85,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { } if (action === "react") { - const messageId = readStringParam(params, "messageId", { + const messageId = readStringOrNumberParam(params, "messageId", { required: true, }); const emoji = readStringParam(params, "emoji", { allowEmpty: true }); @@ -94,7 +94,9 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { { action: "react", chatId: - readStringParam(params, "chatId") ?? readStringParam(params, "to", { required: true }), + readStringOrNumberParam(params, "chatId") ?? + readStringOrNumberParam(params, "channelId") ?? + readStringParam(params, "to", { required: true }), messageId, emoji, remove, diff --git a/src/telegram/bot-native-commands.test.ts b/src/telegram/bot-native-commands.test.ts new file mode 100644 index 000000000..dc6b94dcc --- /dev/null +++ b/src/telegram/bot-native-commands.test.ts @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { OpenClawConfig } from "../config/config.js"; +import type { TelegramAccountConfig } from "../config/types.js"; +import type { RuntimeEnv } from "../runtime.js"; +import { registerTelegramNativeCommands } from "./bot-native-commands.js"; + +const { listSkillCommandsForAgents } = vi.hoisted(() => ({ + listSkillCommandsForAgents: vi.fn(() => []), +})); + +vi.mock("../auto-reply/skill-commands.js", () => ({ + listSkillCommandsForAgents, +})); + +describe("registerTelegramNativeCommands", () => { + beforeEach(() => { + listSkillCommandsForAgents.mockReset(); + }); + + const buildParams = (cfg: OpenClawConfig, accountId = "default") => ({ + bot: { + api: { + setMyCommands: vi.fn().mockResolvedValue(undefined), + sendMessage: vi.fn().mockResolvedValue(undefined), + }, + command: vi.fn(), + } as unknown as Parameters[0]["bot"], + cfg, + runtime: {} as RuntimeEnv, + accountId, + telegramCfg: {} as TelegramAccountConfig, + allowFrom: [], + groupAllowFrom: [], + replyToMode: "off" as const, + textLimit: 4096, + useAccessGroups: false, + nativeEnabled: true, + nativeSkillsEnabled: true, + nativeDisabledExplicit: false, + resolveGroupPolicy: () => ({ allowlistEnabled: false, allowed: true }), + resolveTelegramGroupConfig: () => ({ + groupConfig: undefined, + topicConfig: undefined, + }), + shouldSkipUpdate: () => false, + opts: { token: "token" }, + }); + + it("scopes skill commands when account binding exists", () => { + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "main", default: true }, { id: "butler" }], + }, + bindings: [ + { + agentId: "butler", + match: { channel: "telegram", accountId: "bot-a" }, + }, + ], + }; + + registerTelegramNativeCommands(buildParams(cfg, "bot-a")); + + expect(listSkillCommandsForAgents).toHaveBeenCalledWith({ + cfg, + agentIds: ["butler"], + }); + }); + + it("keeps skill commands unscoped without a matching binding", () => { + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "main", default: true }, { id: "butler" }], + }, + }; + + registerTelegramNativeCommands(buildParams(cfg, "bot-a")); + + expect(listSkillCommandsForAgents).toHaveBeenCalledWith({ cfg }); + }); +}); diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 5f37b81dc..cd53459e6 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -257,8 +257,16 @@ export const registerTelegramNativeCommands = ({ shouldSkipUpdate, opts, }: RegisterTelegramNativeCommandsParams) => { + const boundRoute = + nativeEnabled && nativeSkillsEnabled + ? resolveAgentRoute({ cfg, channel: "telegram", accountId }) + : null; + const boundAgentIds = + boundRoute && boundRoute.matchedBy.startsWith("binding.") ? [boundRoute.agentId] : null; const skillCommands = - nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; + nativeEnabled && nativeSkillsEnabled + ? listSkillCommandsForAgents(boundAgentIds ? { cfg, agentIds: boundAgentIds } : { cfg }) + : []; const nativeCommands = nativeEnabled ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "telegram" }) : []; diff --git a/src/telegram/proxy.test.ts b/src/telegram/proxy.test.ts new file mode 100644 index 000000000..71fd5f88e --- /dev/null +++ b/src/telegram/proxy.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; + +const { ProxyAgent, undiciFetch, proxyAgentSpy, getLastAgent } = vi.hoisted(() => { + const undiciFetch = vi.fn(); + const proxyAgentSpy = vi.fn(); + class ProxyAgent { + static lastCreated: ProxyAgent | undefined; + proxyUrl: string; + constructor(proxyUrl: string) { + this.proxyUrl = proxyUrl; + ProxyAgent.lastCreated = this; + proxyAgentSpy(proxyUrl); + } + } + + return { + ProxyAgent, + undiciFetch, + proxyAgentSpy, + getLastAgent: () => ProxyAgent.lastCreated, + }; +}); + +vi.mock("undici", () => ({ + ProxyAgent, + fetch: undiciFetch, +})); + +import { makeProxyFetch } from "./proxy.js"; + +describe("makeProxyFetch", () => { + it("uses undici fetch with ProxyAgent dispatcher", async () => { + const proxyUrl = "http://proxy.test:8080"; + undiciFetch.mockResolvedValue({ ok: true }); + + const proxyFetch = makeProxyFetch(proxyUrl); + await proxyFetch("https://api.telegram.org/bot123/getMe"); + + expect(proxyAgentSpy).toHaveBeenCalledWith(proxyUrl); + expect(undiciFetch).toHaveBeenCalledWith( + "https://api.telegram.org/bot123/getMe", + expect.objectContaining({ dispatcher: getLastAgent() }), + ); + }); +}); diff --git a/src/telegram/proxy.ts b/src/telegram/proxy.ts index 19d53d569..84251d7fe 100644 --- a/src/telegram/proxy.ts +++ b/src/telegram/proxy.ts @@ -1,11 +1,11 @@ // @ts-nocheck -import { ProxyAgent } from "undici"; +import { ProxyAgent, fetch as undiciFetch } from "undici"; import { wrapFetchWithAbortSignal } from "../infra/fetch.js"; export function makeProxyFetch(proxyUrl: string): typeof fetch { const agent = new ProxyAgent(proxyUrl); return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => { const base = init ? { ...init } : {}; - return fetch(input, { ...base, dispatcher: agent }); + return undiciFetch(input, { ...base, dispatcher: agent }); }); }