From 54fb59b8f383620d90a4829b59df27707561a2bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 05:25:35 +0000 Subject: [PATCH] feat: extend Telegram dock commands and config hashing (#929) Thanks @grp06. Co-authored-by: George Pickett --- CHANGELOG.md | 1 + docs/tools/slash-commands.md | 6 +++--- src/auto-reply/commands-registry.data.ts | 22 ++++++++++++--------- src/auto-reply/commands-registry.test.ts | 4 ++++ src/config/config.ts | 1 + src/config/io.ts | 12 +++++++++++ src/gateway/server-bridge-methods-config.ts | 6 ++++-- src/gateway/server-methods/config.ts | 6 ++++-- src/gateway/server.config-patch.test.ts | 9 +++++++-- 9 files changed, 49 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8f915b0..75f140711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Fixes - Browser: add tests for snapshot labels/efficient query params and labeled image responses. +- Telegram: register dock native commands with underscores to avoid `BOT_COMMAND_INVALID` (#929, fixes #901) — thanks @grp06. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. - Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli. - Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06. diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index c091d5abd..beeecf917 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -63,9 +63,9 @@ Text + native (when enabled): - `/cost on|off` (toggle per-response usage line) - `/stop` - `/restart` -- `/dock-telegram` (switch replies to Telegram) -- `/dock-discord` (switch replies to Discord) -- `/dock-slack` (switch replies to Slack) +- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram) +- `/dock-discord` (alias: `/dock_discord`) (switch replies to Discord) +- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack) - `/activation mention|always` (groups only) - `/send on|off|inherit` (owner-only) - `/reset` or `/new` diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 1707c79b4..817476853 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -27,6 +27,18 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti }; } +type ChannelDock = ReturnType[number]; + +function defineDockCommand(dock: ChannelDock): ChatCommandDefinition { + return defineChatCommand({ + key: `dock:${dock.id}`, + nativeName: `dock_${dock.id}`, + description: `Switch to ${dock.id} for replies.`, + textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`], + acceptsArgs: false, + }); +} + function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliases: string[]): void { const command = commands.find((entry) => entry.key === key); if (!command) { @@ -238,15 +250,7 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => { }), ...listChannelDocks() .filter((dock) => dock.capabilities.nativeCommands) - .map((dock) => - defineChatCommand({ - key: `dock:${dock.id}`, - nativeName: `dock-${dock.id}`, - description: `Switch to ${dock.id} for replies.`, - textAlias: `/dock-${dock.id}`, - acceptsArgs: false, - }), - ), + .map((dock) => defineDockCommand(dock)), ]; registerAlias(commands, "status", "/usage"); diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 8112c49f5..bbc20b784 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -113,4 +113,8 @@ describe("commands registry", () => { "/help@otherbot", ); }); + + it("normalizes dock command aliases", () => { + expect(normalizeCommandBody("/dock_telegram")).toBe("/dock-telegram"); + }); }); diff --git a/src/config/config.ts b/src/config/config.ts index 30469b400..02d924e20 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -3,6 +3,7 @@ export { loadConfig, parseConfigJson5, readConfigFileSnapshot, + resolveConfigSnapshotHash, writeConfigFile, } from "./io.js"; export { migrateLegacyConfig } from "./legacy-migrate.js"; diff --git a/src/config/io.ts b/src/config/io.ts index 089ecadc1..1926f46be 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -58,6 +58,18 @@ function hashConfigRaw(raw: string | null): string { .digest("hex"); } +export function resolveConfigSnapshotHash(snapshot: { + hash?: string; + raw?: string | null; +}): string | null { + if (typeof snapshot.hash === "string") { + const trimmed = snapshot.hash.trim(); + if (trimmed) return trimmed; + } + if (typeof snapshot.raw !== "string") return null; + return hashConfigRaw(snapshot.raw); +} + export type ConfigIoDeps = { fs?: typeof fs; json5?: typeof JSON5; diff --git a/src/gateway/server-bridge-methods-config.ts b/src/gateway/server-bridge-methods-config.ts index 65d4211b1..230574f37 100644 --- a/src/gateway/server-bridge-methods-config.ts +++ b/src/gateway/server-bridge-methods-config.ts @@ -4,6 +4,7 @@ import { loadConfig, parseConfigJson5, readConfigFileSnapshot, + resolveConfigSnapshotHash, validateConfigObject, writeConfigFile, } from "../config/config.js"; @@ -33,7 +34,8 @@ function requireConfigBaseHash( snapshot: Awaited>, ): { ok: true } | { ok: false; error: { code: string; message: string } } { if (!snapshot.exists) return { ok: true }; - if (typeof snapshot.raw !== "string" || !snapshot.hash) { + const snapshotHash = resolveConfigSnapshotHash(snapshot); + if (!snapshotHash) { return { ok: false, error: { @@ -52,7 +54,7 @@ function requireConfigBaseHash( }, }; } - if (baseHash !== snapshot.hash) { + if (baseHash !== snapshotHash) { return { ok: false, error: { diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index ea618510a..ba5f8d740 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -4,6 +4,7 @@ import { loadConfig, parseConfigJson5, readConfigFileSnapshot, + resolveConfigSnapshotHash, validateConfigObject, writeConfigFile, } from "../../config/config.js"; @@ -42,7 +43,8 @@ function requireConfigBaseHash( respond: RespondFn, ): boolean { if (!snapshot.exists) return true; - if (typeof snapshot.raw !== "string" || !snapshot.hash) { + const snapshotHash = resolveConfigSnapshotHash(snapshot); + if (!snapshotHash) { respond( false, undefined, @@ -65,7 +67,7 @@ function requireConfigBaseHash( ); return false; } - if (baseHash !== snapshot.hash) { + if (baseHash !== snapshotHash) { respond( false, undefined, diff --git a/src/gateway/server.config-patch.test.ts b/src/gateway/server.config-patch.test.ts index 20d46f143..f07c891f8 100644 --- a/src/gateway/server.config-patch.test.ts +++ b/src/gateway/server.config-patch.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; +import { resolveConfigSnapshotHash } from "../config/config.js"; + import { connectOk, installGatewayTestHooks, @@ -43,12 +45,15 @@ describe("gateway config.patch", () => { params: {}, }), ); - const getRes = await onceMessage<{ ok: boolean; payload?: { hash?: string } }>( + const getRes = await onceMessage<{ ok: boolean; payload?: { hash?: string; raw?: string } }>( ws, (o) => o.type === "res" && o.id === getId, ); expect(getRes.ok).toBe(true); - const baseHash = getRes.payload?.hash; + const baseHash = resolveConfigSnapshotHash({ + hash: getRes.payload?.hash, + raw: getRes.payload?.raw, + }); expect(typeof baseHash).toBe("string"); const patchId = "req-patch";