Files
openclaw/src/auto-reply/templating.ts
Bob 6a705a37f2 ACP: add persistent Discord channel and Telegram topic bindings (#34873)
* docs: add ACP persistent binding experiment plan

* docs: align ACP persistent binding spec to channel-local config

* docs: scope Telegram ACP bindings to forum topics only

* docs: lock bound /new and /reset behavior to in-place ACP reset

* ACP: add persistent discord/telegram conversation bindings

* ACP: fix persistent binding reuse and discord thread parent context

* docs: document channel-specific persistent ACP bindings

* ACP: split persistent bindings and share conversation id helpers

* ACP: defer configured binding init until preflight passes

* ACP: fix discord thread parent fallback and explicit disable inheritance

* ACP: keep bound /new and /reset in-place

* ACP: honor configured bindings in native command flows

* ACP: avoid configured fallback after runtime bind failure

* docs: refine ACP bindings experiment config examples

* acp: cut over to typed top-level persistent bindings

* ACP bindings: harden reset recovery and native command auth

* Docs: add ACP bound command auth proposal

* Tests: normalize i18n registry zh-CN assertion encoding

* ACP bindings: address review findings for reset and fallback routing

* ACP reset: gate hooks on success and preserve /new arguments

* ACP bindings: fix auth and binding-priority review findings

* Telegram ACP: gate ensure on auth and accepted messages

* ACP bindings: fix session-key precedence and unavailable handling

* ACP reset/native commands: honor fallback targets and abort on bootstrap failure

* Config schema: validate ACP binding channel and Telegram topic IDs

* Discord ACP: apply configured DM bindings to native commands

* ACP reset tails: dispatch through ACP after command handling

* ACP tails/native reset auth: fix target dispatch and restore full auth

* ACP reset detection: fallback to active ACP keys for DM contexts

* Tests: type runTurn mock input in ACP dispatch test

* ACP: dedup binding route bootstrap and reset target resolution

* reply: align ACP reset hooks with bound session key

* docs: replace personal discord ids with placeholders

* fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob)

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00

231 lines
7.5 KiB
TypeScript

import type { ChannelId } from "../channels/plugins/types.js";
import type {
MediaUnderstandingDecision,
MediaUnderstandingOutput,
} from "../media-understanding/types.js";
import type { StickerMetadata } from "../telegram/bot/types.js";
import type { InternalMessageChannel } from "../utils/message-channel.js";
import type { CommandArgs } from "./commands-registry.types.js";
/** Valid message channels for routing. */
export type OriginatingChannelType = ChannelId | InternalMessageChannel;
export type MsgContext = {
Body?: string;
/**
* Agent prompt body (may include envelope/history/context). Prefer this for prompt shaping.
* Should use real newlines (`\n`), not escaped `\\n`.
*/
BodyForAgent?: string;
/**
* Recent chat history for context (untrusted user content). Prefer passing this
* as structured context blocks in the user prompt rather than rendering plaintext envelopes.
*/
InboundHistory?: Array<{
sender: string;
body: string;
timestamp?: number;
}>;
/**
* Raw message body without structural context (history, sender labels).
* Legacy alias for CommandBody. Falls back to Body if not set.
*/
RawBody?: string;
/**
* Prefer for command detection; RawBody is treated as legacy alias.
*/
CommandBody?: string;
/**
* Command parsing body. Prefer this over CommandBody/RawBody when set.
* Should be the "clean" text (no history/sender context).
*/
BodyForCommands?: string;
CommandArgs?: CommandArgs;
From?: string;
To?: string;
SessionKey?: string;
/** Provider account id (multi-account). */
AccountId?: string;
ParentSessionKey?: string;
MessageSid?: string;
/** Provider-specific full message id when MessageSid is a shortened alias. */
MessageSidFull?: string;
MessageSids?: string[];
MessageSidFirst?: string;
MessageSidLast?: string;
ReplyToId?: string;
/**
* Root message id for thread reconstruction (used by Feishu for root_id).
* When a message is part of a thread, this is the id of the first message.
*/
RootMessageId?: string;
/** Provider-specific full reply-to id when ReplyToId is a shortened alias. */
ReplyToIdFull?: string;
ReplyToBody?: string;
ReplyToSender?: string;
ReplyToIsQuote?: boolean;
/** Forward origin from the reply target (when reply_to_message is a forwarded message). */
ReplyToForwardedFrom?: string;
ReplyToForwardedFromType?: string;
ReplyToForwardedFromId?: string;
ReplyToForwardedFromUsername?: string;
ReplyToForwardedFromTitle?: string;
ReplyToForwardedDate?: number;
ForwardedFrom?: string;
ForwardedFromType?: string;
ForwardedFromId?: string;
ForwardedFromUsername?: string;
ForwardedFromTitle?: string;
ForwardedFromSignature?: string;
ForwardedFromChatType?: string;
ForwardedFromMessageId?: number;
ForwardedDate?: number;
ThreadStarterBody?: string;
/** Full thread history when starting a new thread session. */
ThreadHistoryBody?: string;
IsFirstThreadTurn?: boolean;
ThreadLabel?: string;
MediaPath?: string;
MediaUrl?: string;
MediaType?: string;
MediaDir?: string;
MediaPaths?: string[];
MediaUrls?: string[];
MediaTypes?: string[];
/** Telegram sticker metadata (emoji, set name, file IDs, cached description). */
Sticker?: StickerMetadata;
/** True when current-turn sticker media is present in MediaPaths (false for cached-description path). */
StickerMediaIncluded?: boolean;
OutputDir?: string;
OutputBase?: string;
/** Remote host for SCP when media lives on a different machine (e.g., openclaw@192.168.64.3). */
MediaRemoteHost?: string;
Transcript?: string;
MediaUnderstanding?: MediaUnderstandingOutput[];
MediaUnderstandingDecisions?: MediaUnderstandingDecision[];
LinkUnderstanding?: string[];
Prompt?: string;
MaxChars?: number;
ChatType?: string;
/** Human label for envelope headers (conversation label, not sender). */
ConversationLabel?: string;
GroupSubject?: string;
/** Human label for channel-like group conversations (e.g. #general, #support). */
GroupChannel?: string;
GroupSpace?: string;
GroupMembers?: string;
GroupSystemPrompt?: string;
/** Untrusted metadata that must not be treated as system instructions. */
UntrustedContext?: string[];
/** Explicit owner allowlist overrides (trusted, configuration-derived). */
OwnerAllowFrom?: Array<string | number>;
SenderName?: string;
SenderId?: string;
SenderUsername?: string;
SenderTag?: string;
SenderE164?: string;
Timestamp?: number;
/** Provider label (e.g. whatsapp, telegram). */
Provider?: string;
/** Provider surface label (e.g. discord, slack). Prefer this over `Provider` when available. */
Surface?: string;
WasMentioned?: boolean;
CommandAuthorized?: boolean;
CommandSource?: "text" | "native";
CommandTargetSessionKey?: string;
/**
* Internal flag: command handling prepared trailing prompt text for ACP dispatch.
* Used for `/new <prompt>` and `/reset <prompt>` on ACP-bound sessions.
*/
AcpDispatchTailAfterReset?: boolean;
/** Gateway client scopes when the message originates from the gateway. */
GatewayClientScopes?: string[];
/** Thread identifier (Telegram topic id or Matrix thread event id). */
MessageThreadId?: string | number;
/** Telegram forum supergroup marker. */
IsForum?: boolean;
/** Warning: DM has topics enabled but this message is not in a topic. */
TopicRequiredButMissing?: boolean;
/**
* Originating channel for reply routing.
* When set, replies should be routed back to this provider
* instead of using lastChannel from the session.
*/
OriginatingChannel?: OriginatingChannelType;
/**
* Originating destination for reply routing.
* The chat/channel/user ID where the reply should be sent.
*/
OriginatingTo?: string;
/**
* Provider-specific parent conversation id for threaded contexts.
* For Discord threads, this is the parent channel id.
*/
ThreadParentId?: string;
/**
* Messages from hooks to be included in the response.
* Used for hook confirmation messages like "Session context saved to memory".
*/
HookMessages?: string[];
};
export type FinalizedMsgContext = Omit<MsgContext, "CommandAuthorized"> & {
/**
* Always set by finalizeInboundContext().
* Default-deny: missing/undefined becomes false.
*/
CommandAuthorized: boolean;
};
export type TemplateContext = MsgContext & {
BodyStripped?: string;
SessionId?: string;
IsNewSession?: string;
};
function formatTemplateValue(value: unknown): string {
if (value == null) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return String(value);
}
if (typeof value === "symbol" || typeof value === "function") {
return value.toString();
}
if (Array.isArray(value)) {
return value
.flatMap((entry) => {
if (entry == null) {
return [];
}
if (typeof entry === "string") {
return [entry];
}
if (typeof entry === "number" || typeof entry === "boolean" || typeof entry === "bigint") {
return [String(entry)];
}
return [];
})
.join(",");
}
if (typeof value === "object") {
return "";
}
return "";
}
// Simple {{Placeholder}} interpolation using inbound message context.
export function applyTemplate(str: string | undefined, ctx: TemplateContext) {
if (!str) {
return "";
}
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
const value = ctx[key as keyof TemplateContext];
return formatTemplateValue(value);
});
}