mirror of https://github.com/openclaw/openclaw.git
refactor(channels): route core through registered plugin capabilities
This commit is contained in:
parent
471e059b69
commit
63cbc097b5
|
|
@ -102,6 +102,11 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
|||
},
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
createManager: ({ cfg, accountId }) =>
|
||||
createBlueBubblesConversationBindingManager({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
}),
|
||||
},
|
||||
actions: bluebubblesMessageActions,
|
||||
bindings: {
|
||||
|
|
|
|||
|
|
@ -112,8 +112,16 @@ function createChannelRuntime(
|
|||
): OpenClawPluginApi["runtime"] {
|
||||
return {
|
||||
channel: {
|
||||
[runtimeKey]: {
|
||||
[sendKey]: sendMessage,
|
||||
outbound: {
|
||||
loadAdapter: async (channelId: string) =>
|
||||
channelId === runtimeKey
|
||||
? ({
|
||||
sendText: async ({ to, text, ...opts }: Record<string, unknown>) =>
|
||||
await sendMessage(to, text, opts),
|
||||
sendMedia: async ({ to, text, ...opts }: Record<string, unknown>) =>
|
||||
await sendMessage(to, text, opts),
|
||||
} as const)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawPluginApi["runtime"];
|
||||
|
|
@ -210,7 +218,7 @@ describe("device-pair /pair qr", () => {
|
|||
expectedTarget: "123",
|
||||
expectedOpts: {
|
||||
accountId: "default",
|
||||
messageThreadId: 271,
|
||||
threadId: 271,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -240,7 +248,7 @@ describe("device-pair /pair qr", () => {
|
|||
expectedTarget: "user:U123",
|
||||
expectedOpts: {
|
||||
accountId: "default",
|
||||
threadTs: "1234567890.000001",
|
||||
threadId: "1234567890.000001",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ type QrCommandContext = {
|
|||
};
|
||||
|
||||
type QrChannelSender = {
|
||||
resolveSend: (api: OpenClawPluginApi) => QrSendFn | undefined;
|
||||
createOpts: (params: {
|
||||
ctx: QrCommandContext;
|
||||
qrFilePath: string;
|
||||
|
|
@ -84,24 +83,16 @@ type QrChannelSender = {
|
|||
}) => Record<string, unknown>;
|
||||
};
|
||||
|
||||
type QrSendFn = (to: string, text: string, opts: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
function coerceQrSend(send: unknown): QrSendFn | undefined {
|
||||
return typeof send === "function" ? (send as QrSendFn) : undefined;
|
||||
}
|
||||
|
||||
const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
||||
telegram: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.telegram?.sendMessageTelegram),
|
||||
createOpts: ({ ctx, qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
mediaUrl: qrFilePath,
|
||||
mediaLocalRoots,
|
||||
...(typeof ctx.messageThreadId === "number" ? { messageThreadId: ctx.messageThreadId } : {}),
|
||||
...(ctx.messageThreadId != null ? { threadId: ctx.messageThreadId } : {}),
|
||||
...(accountId ? { accountId } : {}),
|
||||
}),
|
||||
},
|
||||
discord: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.discord?.sendMessageDiscord),
|
||||
createOpts: ({ qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
mediaUrl: qrFilePath,
|
||||
mediaLocalRoots,
|
||||
|
|
@ -109,16 +100,14 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
|||
}),
|
||||
},
|
||||
slack: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.slack?.sendMessageSlack),
|
||||
createOpts: ({ ctx, qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
mediaUrl: qrFilePath,
|
||||
mediaLocalRoots,
|
||||
...(ctx.messageThreadId != null ? { threadTs: String(ctx.messageThreadId) } : {}),
|
||||
...(ctx.messageThreadId != null ? { threadId: String(ctx.messageThreadId) } : {}),
|
||||
...(accountId ? { accountId } : {}),
|
||||
}),
|
||||
},
|
||||
signal: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.signal?.sendMessageSignal),
|
||||
createOpts: ({ qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
mediaUrl: qrFilePath,
|
||||
mediaLocalRoots,
|
||||
|
|
@ -126,7 +115,6 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
|||
}),
|
||||
},
|
||||
imessage: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.imessage?.sendMessageIMessage),
|
||||
createOpts: ({ qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
mediaUrl: qrFilePath,
|
||||
mediaLocalRoots,
|
||||
|
|
@ -134,7 +122,6 @@ const QR_CHANNEL_SENDERS: Record<string, QrChannelSender> = {
|
|||
}),
|
||||
},
|
||||
whatsapp: {
|
||||
resolveSend: (api) => coerceQrSend(api.runtime?.channel?.whatsapp?.sendMessageWhatsApp),
|
||||
createOpts: ({ qrFilePath, mediaLocalRoots, accountId }) => ({
|
||||
verbose: false,
|
||||
mediaUrl: qrFilePath,
|
||||
|
|
@ -518,20 +505,22 @@ async function sendQrPngToSupportedChannel(params: {
|
|||
if (!sender) {
|
||||
return false;
|
||||
}
|
||||
const send = sender.resolveSend(params.api);
|
||||
const adapter = await params.api.runtime.channel.outbound.loadAdapter(params.ctx.channel);
|
||||
const send = adapter?.sendMedia;
|
||||
if (!send) {
|
||||
return false;
|
||||
}
|
||||
await send(
|
||||
params.target,
|
||||
params.caption,
|
||||
sender.createOpts({
|
||||
await send({
|
||||
cfg: params.api.config,
|
||||
to: params.target,
|
||||
text: params.caption,
|
||||
...sender.createOpts({
|
||||
ctx: params.ctx,
|
||||
qrFilePath: params.qrFilePath,
|
||||
mediaLocalRoots,
|
||||
accountId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -769,7 +758,8 @@ export default definePluginEntry({
|
|||
channelKeys.join(",") || "none"
|
||||
}`,
|
||||
);
|
||||
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
|
||||
const adapter = await api.runtime.channel.outbound.loadAdapter("telegram");
|
||||
const send = adapter?.sendText;
|
||||
if (!send) {
|
||||
throw new Error(
|
||||
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
|
||||
|
|
@ -777,10 +767,11 @@ export default definePluginEntry({
|
|||
)})`,
|
||||
);
|
||||
}
|
||||
await send(target, formatSetupInstructions(payload.expiresAtMs), {
|
||||
...(typeof ctx.messageThreadId === "number"
|
||||
? { messageThreadId: ctx.messageThreadId }
|
||||
: {}),
|
||||
await send({
|
||||
cfg: api.config,
|
||||
to: target,
|
||||
text: formatSetupInstructions(payload.expiresAtMs),
|
||||
...(ctx.messageThreadId != null ? { threadId: ctx.messageThreadId } : {}),
|
||||
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
|
||||
});
|
||||
api.logger.info?.(
|
||||
|
|
|
|||
|
|
@ -254,17 +254,23 @@ async function notifySubscriber(params: {
|
|||
subscriber: NotifySubscription;
|
||||
text: string;
|
||||
}): Promise<boolean> {
|
||||
const send = params.api.runtime?.channel?.telegram?.sendMessageTelegram;
|
||||
const adapter = await params.api.runtime.channel.outbound.loadAdapter("telegram");
|
||||
const send = adapter?.sendText;
|
||||
if (!send) {
|
||||
params.api.logger.warn("device-pair: telegram runtime unavailable for pairing notifications");
|
||||
params.api.logger.warn(
|
||||
"device-pair: telegram outbound adapter unavailable for pairing notifications",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await send(params.subscriber.to, params.text, {
|
||||
await send({
|
||||
cfg: params.api.config,
|
||||
to: params.subscriber.to,
|
||||
text: params.text,
|
||||
...(params.subscriber.accountId ? { accountId: params.subscriber.accountId } : {}),
|
||||
...(typeof params.subscriber.messageThreadId === "number"
|
||||
? { messageThreadId: params.subscriber.messageThreadId }
|
||||
...(params.subscriber.messageThreadId != null
|
||||
? { threadId: params.subscriber.messageThreadId }
|
||||
: {}),
|
||||
});
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ import { PAIRING_APPROVED_MESSAGE, resolveChannelMediaMaxBytes } from "../runtim
|
|||
import type { ResolvedIMessageAccount } from "./accounts.js";
|
||||
import { monitorIMessageProvider } from "./monitor.js";
|
||||
import { probeIMessage } from "./probe.js";
|
||||
import { getIMessageRuntime } from "./runtime.js";
|
||||
import { sendMessageIMessage } from "./send.js";
|
||||
import { imessageSetupWizard } from "./setup-surface.js";
|
||||
|
||||
type IMessageSendFn = ReturnType<
|
||||
typeof getIMessageRuntime
|
||||
>["channel"]["imessage"]["sendMessageIMessage"];
|
||||
type IMessageSendFn = typeof sendMessageIMessage;
|
||||
|
||||
export async function sendIMessageOutbound(params: {
|
||||
cfg: Parameters<typeof import("./accounts.js").resolveIMessageAccount>[0]["cfg"];
|
||||
|
|
@ -21,8 +19,7 @@ export async function sendIMessageOutbound(params: {
|
|||
replyToId?: string;
|
||||
}) {
|
||||
const send =
|
||||
resolveOutboundSendDep<IMessageSendFn>(params.deps, "imessage") ??
|
||||
getIMessageRuntime().channel.imessage.sendMessageIMessage;
|
||||
resolveOutboundSendDep<IMessageSendFn>(params.deps, "imessage") ?? sendMessageIMessage;
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
|
|
@ -41,7 +38,7 @@ export async function sendIMessageOutbound(params: {
|
|||
}
|
||||
|
||||
export async function notifyIMessageApproval(id: string): Promise<void> {
|
||||
await getIMessageRuntime().channel.imessage.sendMessageIMessage(id, PAIRING_APPROVED_MESSAGE);
|
||||
await sendMessageIMessage(id, PAIRING_APPROVED_MESSAGE);
|
||||
}
|
||||
|
||||
export async function probeIMessageAccount(timeoutMs?: number) {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,11 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount, IMessageProb
|
|||
},
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
createManager: ({ cfg, accountId }) =>
|
||||
createIMessageConversationBindingManager({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
}),
|
||||
},
|
||||
bindings: {
|
||||
compileConfiguredBinding: ({ conversationId }) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
|
||||
const SERVICE_PREFIXES = ["imessage:", "sms:", "auto:"] as const;
|
||||
const CHAT_TARGET_PREFIX_RE =
|
||||
/^(chat_id:|chatid:|chat:|chat_guid:|chatguid:|guid:|chat_identifier:|chatidentifier:|chatident:)/i;
|
||||
|
||||
function trimMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = raw.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function looksLikeHandleOrPhoneTarget(params: {
|
||||
raw: string;
|
||||
prefixPattern: RegExp;
|
||||
phonePattern?: RegExp;
|
||||
}): boolean {
|
||||
const trimmed = params.raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (params.prefixPattern.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
return (params.phonePattern ?? /^\+?\d{3,}$/).test(trimmed);
|
||||
}
|
||||
|
||||
export function normalizeIMessageHandle(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
if (lowered.startsWith("imessage:")) {
|
||||
return normalizeIMessageHandle(trimmed.slice("imessage:".length));
|
||||
}
|
||||
if (lowered.startsWith("sms:")) {
|
||||
return normalizeIMessageHandle(trimmed.slice("sms:".length));
|
||||
}
|
||||
if (lowered.startsWith("auto:")) {
|
||||
return normalizeIMessageHandle(trimmed.slice("auto:".length));
|
||||
}
|
||||
if (CHAT_TARGET_PREFIX_RE.test(trimmed)) {
|
||||
const prefix = trimmed.match(CHAT_TARGET_PREFIX_RE)?.[0];
|
||||
if (!prefix) {
|
||||
return "";
|
||||
}
|
||||
const value = trimmed.slice(prefix.length).trim();
|
||||
return `${prefix.toLowerCase()}${value}`;
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
const normalized = normalizeE164(trimmed);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
return trimmed.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
export function normalizeIMessageMessagingTarget(raw: string): string | undefined {
|
||||
const trimmed = trimMessagingTarget(raw);
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
for (const prefix of SERVICE_PREFIXES) {
|
||||
if (lower.startsWith(prefix)) {
|
||||
const remainder = trimmed.slice(prefix.length).trim();
|
||||
const normalizedHandle = normalizeIMessageHandle(remainder);
|
||||
if (!normalizedHandle) {
|
||||
return undefined;
|
||||
}
|
||||
if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) {
|
||||
return normalizedHandle;
|
||||
}
|
||||
return `${prefix}${normalizedHandle}`;
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizeIMessageHandle(trimmed);
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
export function looksLikeIMessageTargetId(raw: string): boolean {
|
||||
const trimmed = trimMessagingTarget(raw);
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (CHAT_TARGET_PREFIX_RE.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
return looksLikeHandleOrPhoneTarget({
|
||||
raw: trimmed,
|
||||
prefixPattern: /^(imessage:|sms:|auto:)/i,
|
||||
});
|
||||
}
|
||||
|
|
@ -54,6 +54,10 @@ import {
|
|||
resolveMatrixDirectUserId,
|
||||
resolveMatrixTargetIdentity,
|
||||
} from "./matrix/target-ids.js";
|
||||
import {
|
||||
setMatrixThreadBindingIdleTimeoutBySessionKey,
|
||||
setMatrixThreadBindingMaxAgeBySessionKey,
|
||||
} from "./matrix/thread-bindings-shared.js";
|
||||
import { getMatrixRuntime } from "./runtime.js";
|
||||
import { resolveMatrixOutboundSessionRoute } from "./session-route.js";
|
||||
import { matrixSetupAdapter } from "./setup-core.js";
|
||||
|
|
@ -310,6 +314,46 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
|||
},
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
setIdleTimeoutBySessionKey: ({ targetSessionKey, accountId, idleTimeoutMs }) =>
|
||||
setMatrixThreadBindingIdleTimeoutBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? "",
|
||||
idleTimeoutMs,
|
||||
}).map((binding) => ({
|
||||
boundAt: binding.boundAt,
|
||||
lastActivityAt:
|
||||
typeof binding.metadata?.lastActivityAt === "number"
|
||||
? binding.metadata.lastActivityAt
|
||||
: binding.boundAt,
|
||||
idleTimeoutMs:
|
||||
typeof binding.metadata?.idleTimeoutMs === "number"
|
||||
? binding.metadata.idleTimeoutMs
|
||||
: undefined,
|
||||
maxAgeMs:
|
||||
typeof binding.metadata?.maxAgeMs === "number"
|
||||
? binding.metadata.maxAgeMs
|
||||
: undefined,
|
||||
})),
|
||||
setMaxAgeBySessionKey: ({ targetSessionKey, accountId, maxAgeMs }) =>
|
||||
setMatrixThreadBindingMaxAgeBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? "",
|
||||
maxAgeMs,
|
||||
}).map((binding) => ({
|
||||
boundAt: binding.boundAt,
|
||||
lastActivityAt:
|
||||
typeof binding.metadata?.lastActivityAt === "number"
|
||||
? binding.metadata.lastActivityAt
|
||||
: binding.boundAt,
|
||||
idleTimeoutMs:
|
||||
typeof binding.metadata?.idleTimeoutMs === "number"
|
||||
? binding.metadata.idleTimeoutMs
|
||||
: undefined,
|
||||
maxAgeMs:
|
||||
typeof binding.metadata?.maxAgeMs === "number"
|
||||
? binding.metadata.maxAgeMs
|
||||
: undefined,
|
||||
})),
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeMatrixMessagingTarget,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { signalMessageActions } from "./message-actions.js";
|
|||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize.js";
|
||||
import { resolveSignalOutboundTarget } from "./outbound-session.js";
|
||||
import { probeSignal, type SignalProbe } from "./probe.js";
|
||||
import { resolveSignalReactionLevel } from "./reaction-level.js";
|
||||
import {
|
||||
buildBaseChannelStatusSummary,
|
||||
chunkText,
|
||||
|
|
@ -237,6 +238,15 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount, SignalProbe> =
|
|||
resolveDmPolicy: (account) => account.config.dmPolicy,
|
||||
resolveGroupPolicy: (account) => account.config.groupPolicy,
|
||||
}),
|
||||
agentPrompt: {
|
||||
reactionGuidance: ({ cfg, accountId }) => {
|
||||
const level = resolveSignalReactionLevel({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
}).agentReactionGuidance;
|
||||
return level ? { level, channelLabel: "Signal" } : undefined;
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSignalMessagingTarget,
|
||||
parseExplicitTarget: ({ raw }) => parseSignalExplicitTarget(raw),
|
||||
|
|
|
|||
|
|
@ -40,8 +40,11 @@ import {
|
|||
type ResolvedTelegramAccount,
|
||||
} from "./accounts.js";
|
||||
import { resolveTelegramAutoThreadId } from "./action-threading.js";
|
||||
import { lookupTelegramChatId } from "./api-fetch.js";
|
||||
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
|
||||
import * as auditModule from "./audit.js";
|
||||
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
|
||||
import { telegramMessageActions as telegramMessageActionsImpl } from "./channel-actions.js";
|
||||
import {
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
|
|
@ -58,14 +61,16 @@ import {
|
|||
resolveTelegramGroupRequireMention,
|
||||
resolveTelegramGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
||||
import * as monitorModule from "./monitor.js";
|
||||
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
|
||||
import { sendTelegramPayloadMessages } from "./outbound-adapter.js";
|
||||
import { parseTelegramReplyToMessageId, parseTelegramThreadId } from "./outbound-params.js";
|
||||
import * as probeModule from "./probe.js";
|
||||
import type { TelegramProbe } from "./probe.js";
|
||||
import { resolveTelegramReactionLevel } from "./reaction-level.js";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
import { sendTypingTelegram } from "./send.js";
|
||||
import { sendMessageTelegram, sendPollTelegram, sendTypingTelegram } from "./send.js";
|
||||
import { telegramSetupAdapter } from "./setup-core.js";
|
||||
import { telegramSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
|
|
@ -76,54 +81,31 @@ import {
|
|||
} from "./shared.js";
|
||||
import { collectTelegramStatusIssues } from "./status-issues.js";
|
||||
import { parseTelegramTarget } from "./targets.js";
|
||||
import {
|
||||
createTelegramThreadBindingManager,
|
||||
setTelegramThreadBindingIdleTimeoutBySessionKey,
|
||||
setTelegramThreadBindingMaxAgeBySessionKey,
|
||||
} from "./thread-bindings.js";
|
||||
import { resolveTelegramToken } from "./token.js";
|
||||
|
||||
type TelegramSendFn = ReturnType<
|
||||
typeof getTelegramRuntime
|
||||
>["channel"]["telegram"]["sendMessageTelegram"];
|
||||
type TelegramSendFn = typeof sendMessageTelegram;
|
||||
|
||||
type TelegramSendOptions = NonNullable<Parameters<TelegramSendFn>[2]>;
|
||||
|
||||
type TelegramStatusRuntimeHelpers = {
|
||||
probeTelegram?: typeof probeModule.probeTelegram;
|
||||
collectTelegramUnmentionedGroupIds?: typeof auditModule.collectTelegramUnmentionedGroupIds;
|
||||
auditTelegramGroupMembership?: typeof auditModule.auditTelegramGroupMembership;
|
||||
monitorTelegramProvider?: typeof monitorModule.monitorTelegramProvider;
|
||||
};
|
||||
|
||||
function getTelegramStatusRuntimeHelpers(): TelegramStatusRuntimeHelpers {
|
||||
try {
|
||||
return (getTelegramRuntime().channel?.telegram ?? {}) as TelegramStatusRuntimeHelpers;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Telegram runtime not initialized") {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTelegramProbe() {
|
||||
return getTelegramStatusRuntimeHelpers().probeTelegram ?? probeModule.probeTelegram;
|
||||
return probeModule.probeTelegram;
|
||||
}
|
||||
|
||||
function resolveTelegramAuditCollector() {
|
||||
return (
|
||||
getTelegramStatusRuntimeHelpers().collectTelegramUnmentionedGroupIds ??
|
||||
auditModule.collectTelegramUnmentionedGroupIds
|
||||
);
|
||||
return auditModule.collectTelegramUnmentionedGroupIds;
|
||||
}
|
||||
|
||||
function resolveTelegramAuditMembership() {
|
||||
return (
|
||||
getTelegramStatusRuntimeHelpers().auditTelegramGroupMembership ??
|
||||
auditModule.auditTelegramGroupMembership
|
||||
);
|
||||
return auditModule.auditTelegramGroupMembership;
|
||||
}
|
||||
|
||||
function resolveTelegramMonitor() {
|
||||
return (
|
||||
getTelegramStatusRuntimeHelpers().monitorTelegramProvider ??
|
||||
monitorModule.monitorTelegramProvider
|
||||
);
|
||||
return monitorModule.monitorTelegramProvider;
|
||||
}
|
||||
|
||||
function buildTelegramSendOptions(params: {
|
||||
|
|
@ -167,8 +149,7 @@ async function sendTelegramOutbound(params: {
|
|||
gatewayClientScopes?: readonly string[] | null;
|
||||
}) {
|
||||
const send =
|
||||
resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ??
|
||||
getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
resolveOutboundSendDep<TelegramSendFn>(params.deps, "telegram") ?? sendMessageTelegram;
|
||||
return await send(
|
||||
params.to,
|
||||
params.text,
|
||||
|
|
@ -321,6 +302,72 @@ function resolveTelegramOutboundSessionRoute(params: {
|
|||
};
|
||||
}
|
||||
|
||||
async function resolveTelegramTargets(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
inputs: string[];
|
||||
kind: "user" | "group";
|
||||
}) {
|
||||
if (params.kind !== "user") {
|
||||
return params.inputs.map((input) => ({
|
||||
input,
|
||||
resolved: false as const,
|
||||
note: "Telegram runtime target resolution only supports usernames for direct-message lookups.",
|
||||
}));
|
||||
}
|
||||
const account = resolveTelegramAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const token = account.token.trim();
|
||||
if (!token) {
|
||||
return params.inputs.map((input) => ({
|
||||
input,
|
||||
resolved: false as const,
|
||||
note: "Telegram bot token is required to resolve @username targets.",
|
||||
}));
|
||||
}
|
||||
return await Promise.all(
|
||||
params.inputs.map(async (input) => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
input,
|
||||
resolved: false as const,
|
||||
note: "Telegram target is required.",
|
||||
};
|
||||
}
|
||||
const normalized = trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
|
||||
try {
|
||||
const id = await lookupTelegramChatId({
|
||||
token,
|
||||
chatId: normalized,
|
||||
network: account.config.network,
|
||||
});
|
||||
if (!id) {
|
||||
return {
|
||||
input,
|
||||
resolved: false as const,
|
||||
note: "Telegram username could not be resolved by the configured bot.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
input,
|
||||
resolved: true as const,
|
||||
id,
|
||||
name: normalized,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
input,
|
||||
resolved: false as const,
|
||||
note: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const telegramNativeApprovalAdapter = createApproverRestrictedNativeApprovalAdapter({
|
||||
channel: "telegram",
|
||||
channelLabel: "Telegram",
|
||||
|
|
@ -341,16 +388,13 @@ const telegramNativeApprovalAdapter = createApproverRestrictedNativeApprovalAdap
|
|||
});
|
||||
|
||||
const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.describeMessageTool?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) =>
|
||||
getTelegramRuntime().channel.telegram.messageActions?.extractToolSend?.(ctx) ?? null,
|
||||
describeMessageTool: (ctx) => telegramMessageActionsImpl.describeMessageTool?.(ctx) ?? null,
|
||||
extractToolSend: (ctx) => telegramMessageActionsImpl.extractToolSend?.(ctx) ?? null,
|
||||
handleAction: async (ctx) => {
|
||||
const ma = getTelegramRuntime().channel.telegram.messageActions;
|
||||
if (!ma?.handleAction) {
|
||||
if (!telegramMessageActionsImpl.handleAction) {
|
||||
throw new Error("Telegram message actions not available");
|
||||
}
|
||||
return ma.handleAction(ctx);
|
||||
return await telegramMessageActionsImpl.handleAction(ctx);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -418,10 +462,47 @@ export const telegramPlugin = createChatChannelPlugin({
|
|||
fallbackTo,
|
||||
}),
|
||||
},
|
||||
conversationBindings: {
|
||||
supportsCurrentConversationBinding: true,
|
||||
createManager: ({ accountId }) =>
|
||||
createTelegramThreadBindingManager({
|
||||
accountId: accountId ?? undefined,
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
}),
|
||||
setIdleTimeoutBySessionKey: ({ targetSessionKey, accountId, idleTimeoutMs }) =>
|
||||
setTelegramThreadBindingIdleTimeoutBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? undefined,
|
||||
idleTimeoutMs,
|
||||
}),
|
||||
setMaxAgeBySessionKey: ({ targetSessionKey, accountId, maxAgeMs }) =>
|
||||
setTelegramThreadBindingMaxAgeBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? undefined,
|
||||
maxAgeMs,
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
||||
},
|
||||
agentPrompt: {
|
||||
messageToolCapabilities: ({ cfg, accountId }) => {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return inlineButtonsScope === "off" ? [] : ["inlineButtons"];
|
||||
},
|
||||
reactionGuidance: ({ cfg, accountId }) => {
|
||||
const level = resolveTelegramReactionLevel({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
}).agentReactionGuidance;
|
||||
return level ? { level, channelLabel: "Telegram" } : undefined;
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeTelegramMessagingTarget,
|
||||
parseExplicitTarget: ({ raw }) => parseTelegramExplicitTarget(raw),
|
||||
|
|
@ -450,6 +531,10 @@ export const telegramPlugin = createChatChannelPlugin({
|
|||
hint: "<chatId>",
|
||||
},
|
||||
},
|
||||
resolver: {
|
||||
resolveTargets: async ({ cfg, accountId, inputs, kind }) =>
|
||||
await resolveTelegramTargets({ cfg, accountId, inputs, kind }),
|
||||
},
|
||||
lifecycle: {
|
||||
onAccountConfigChanged: async ({ prevCfg, nextCfg, accountId }) => {
|
||||
const previousToken = resolveTelegramAccount({ cfg: prevCfg, accountId }).token.trim();
|
||||
|
|
@ -685,16 +770,11 @@ export const telegramPlugin = createChatChannelPlugin({
|
|||
message: PAIRING_APPROVED_MESSAGE,
|
||||
normalizeAllowEntry: createPairingPrefixStripper(/^(telegram|tg):/i),
|
||||
notify: async ({ cfg, id, message, accountId }) => {
|
||||
const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg, {
|
||||
accountId,
|
||||
});
|
||||
const { token } = resolveTelegramToken(cfg, { accountId });
|
||||
if (!token) {
|
||||
throw new Error("telegram token not configured");
|
||||
}
|
||||
await getTelegramRuntime().channel.telegram.sendMessageTelegram(id, message, {
|
||||
token,
|
||||
accountId,
|
||||
});
|
||||
await sendMessageTelegram(id, message, { token, accountId });
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -759,8 +839,7 @@ export const telegramPlugin = createChatChannelPlugin({
|
|||
gatewayClientScopes,
|
||||
}) => {
|
||||
const send =
|
||||
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ??
|
||||
getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
resolveOutboundSendDep<TelegramSendFn>(deps, "telegram") ?? sendMessageTelegram;
|
||||
const result = await sendTelegramPayloadMessages({
|
||||
send,
|
||||
to,
|
||||
|
|
@ -839,7 +918,7 @@ export const telegramPlugin = createChatChannelPlugin({
|
|||
isAnonymous,
|
||||
gatewayClientScopes,
|
||||
}) =>
|
||||
await getTelegramRuntime().channel.telegram.sendPollTelegram(to, poll, {
|
||||
await sendPollTelegram(to, poll, {
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
messageThreadId: parseTelegramThreadId(threadId),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { sendMessageTelegram } from "../../extensions/telegram/runtime-api.js";
|
||||
import { loadConfig } from "../../src/config/config.js";
|
||||
import { sendMessageTelegram } from "../../src/plugin-sdk/telegram-runtime.js";
|
||||
import { matchPluginCommand, executePluginCommand } from "../../src/plugins/commands.js";
|
||||
import { loadOpenClawPlugins } from "../../src/plugins/loader.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,49 @@ export function resolveChannelMessageToolHints(params: {
|
|||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function resolveChannelMessageToolCapabilities(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
channel?: string | null;
|
||||
accountId?: string | null;
|
||||
}): string[] {
|
||||
const channelId = normalizeAnyChannelId(params.channel);
|
||||
if (!channelId) {
|
||||
return [];
|
||||
}
|
||||
const resolve = getChannelPlugin(channelId)?.agentPrompt?.messageToolCapabilities;
|
||||
if (!resolve) {
|
||||
return [];
|
||||
}
|
||||
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
||||
return (resolve({ cfg, accountId: params.accountId }) ?? [])
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function resolveChannelReactionGuidance(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
channel?: string | null;
|
||||
accountId?: string | null;
|
||||
}): { level: "minimal" | "extensive"; channel: string } | undefined {
|
||||
const channelId = normalizeAnyChannelId(params.channel);
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
const resolve = getChannelPlugin(channelId)?.agentPrompt?.reactionGuidance;
|
||||
if (!resolve) {
|
||||
return undefined;
|
||||
}
|
||||
const cfg = params.cfg ?? ({} as OpenClawConfig);
|
||||
const resolved = resolve({ cfg, accountId: params.accountId });
|
||||
if (!resolved?.level) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
level: resolved.level,
|
||||
channel: resolved.channelLabel?.trim() || channelId,
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetLoggedListActionErrors() {
|
||||
messageActionTesting.resetLoggedMessageActionErrors();
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ import {
|
|||
} from "../../context-engine/index.js";
|
||||
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
||||
import { generateSecureToken } from "../../infra/secure-random.js";
|
||||
import { resolveSignalReactionLevel } from "../../plugin-sdk/signal.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramReactionLevel,
|
||||
} from "../../plugin-sdk/telegram.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.js";
|
||||
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
|
|
@ -34,7 +29,12 @@ import { resolveOpenClawAgentDir } from "../agent-paths.js";
|
|||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||
import type { ExecElevatedDefaults } from "../bash-tools.js";
|
||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
|
||||
import { listChannelSupportedActions, resolveChannelMessageToolHints } from "../channel-tools.js";
|
||||
import {
|
||||
listChannelSupportedActions,
|
||||
resolveChannelMessageToolCapabilities,
|
||||
resolveChannelMessageToolHints,
|
||||
resolveChannelReactionGuidance,
|
||||
} from "../channel-tools.js";
|
||||
import {
|
||||
hasMeaningfulConversationContent,
|
||||
isRealConversationMessage,
|
||||
|
|
@ -521,43 +521,35 @@ export async function compactEmbeddedPiSessionDirect(
|
|||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
if (runtimeChannel === "telegram" && params.config) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
if (inlineButtonsScope !== "off") {
|
||||
if (!runtimeCapabilities) {
|
||||
runtimeCapabilities = [];
|
||||
}
|
||||
if (
|
||||
!runtimeCapabilities.some((cap) => String(cap).trim().toLowerCase() === "inlinebuttons")
|
||||
) {
|
||||
runtimeCapabilities.push("inlineButtons");
|
||||
const promptCapabilities =
|
||||
runtimeChannel && params.config
|
||||
? resolveChannelMessageToolCapabilities({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
})
|
||||
: [];
|
||||
if (promptCapabilities.length > 0) {
|
||||
runtimeCapabilities ??= [];
|
||||
const seenCapabilities = new Set(
|
||||
runtimeCapabilities.map((cap) => String(cap).trim().toLowerCase()),
|
||||
);
|
||||
for (const capability of promptCapabilities) {
|
||||
const normalizedCapability = capability.trim().toLowerCase();
|
||||
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
|
||||
continue;
|
||||
}
|
||||
seenCapabilities.add(normalizedCapability);
|
||||
runtimeCapabilities.push(capability);
|
||||
}
|
||||
}
|
||||
const reactionGuidance =
|
||||
runtimeChannel && params.config
|
||||
? (() => {
|
||||
if (runtimeChannel === "telegram") {
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
}
|
||||
if (runtimeChannel === "signal") {
|
||||
const resolved = resolveSignalReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Signal" } : undefined;
|
||||
}
|
||||
return undefined;
|
||||
})()
|
||||
? resolveChannelReactionGuidance({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
})
|
||||
: undefined;
|
||||
const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ import {
|
|||
shouldInjectOllamaCompatNumCtx,
|
||||
wrapOllamaCompatNumCtx,
|
||||
} from "../../../plugin-sdk/ollama.js";
|
||||
import { resolveSignalReactionLevel } from "../../../plugin-sdk/signal.js";
|
||||
import {
|
||||
resolveTelegramInlineButtonsScope,
|
||||
resolveTelegramReactionLevel,
|
||||
} from "../../../plugin-sdk/telegram-runtime.js";
|
||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
||||
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
|
||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||
|
|
@ -48,7 +43,9 @@ import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstra
|
|||
import { createCacheTrace } from "../../cache-trace.js";
|
||||
import {
|
||||
listChannelSupportedActions,
|
||||
resolveChannelMessageToolCapabilities,
|
||||
resolveChannelMessageToolHints,
|
||||
resolveChannelReactionGuidance,
|
||||
} from "../../channel-tools.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js";
|
||||
import { resolveOpenClawDocsPath } from "../../docs-path.js";
|
||||
|
|
@ -532,43 +529,35 @@ export async function runEmbeddedAttempt(
|
|||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
if (runtimeChannel === "telegram" && params.config) {
|
||||
const inlineButtonsScope = resolveTelegramInlineButtonsScope({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
if (inlineButtonsScope !== "off") {
|
||||
if (!runtimeCapabilities) {
|
||||
runtimeCapabilities = [];
|
||||
}
|
||||
if (
|
||||
!runtimeCapabilities.some((cap) => String(cap).trim().toLowerCase() === "inlinebuttons")
|
||||
) {
|
||||
runtimeCapabilities.push("inlineButtons");
|
||||
const promptCapabilities =
|
||||
runtimeChannel && params.config
|
||||
? resolveChannelMessageToolCapabilities({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
})
|
||||
: [];
|
||||
if (promptCapabilities.length > 0) {
|
||||
runtimeCapabilities ??= [];
|
||||
const seenCapabilities = new Set(
|
||||
runtimeCapabilities.map((cap) => String(cap).trim().toLowerCase()),
|
||||
);
|
||||
for (const capability of promptCapabilities) {
|
||||
const normalizedCapability = capability.trim().toLowerCase();
|
||||
if (!normalizedCapability || seenCapabilities.has(normalizedCapability)) {
|
||||
continue;
|
||||
}
|
||||
seenCapabilities.add(normalizedCapability);
|
||||
runtimeCapabilities.push(capability);
|
||||
}
|
||||
}
|
||||
const reactionGuidance =
|
||||
runtimeChannel && params.config
|
||||
? (() => {
|
||||
if (runtimeChannel === "telegram") {
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
}
|
||||
if (runtimeChannel === "signal") {
|
||||
const resolved = resolveSignalReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Signal" } : undefined;
|
||||
}
|
||||
return undefined;
|
||||
})()
|
||||
? resolveChannelReactionGuidance({
|
||||
cfg: params.config,
|
||||
channel: runtimeChannel,
|
||||
accountId: params.agentAccountId,
|
||||
})
|
||||
: undefined;
|
||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
|
||||
const reasoningTagHint = isReasoningTagProvider(params.provider);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
type ButtonRow = Array<{ text: string; callback_data: string }>;
|
||||
|
||||
export type ProviderInfo = {
|
||||
id: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type ModelsKeyboardParams = {
|
||||
provider: string;
|
||||
models: readonly string[];
|
||||
currentModel?: string;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize?: number;
|
||||
modelNames?: ReadonlyMap<string, string>;
|
||||
};
|
||||
|
||||
const MODELS_PAGE_SIZE = 8;
|
||||
const MAX_CALLBACK_DATA_BYTES = 64;
|
||||
const CALLBACK_PREFIX = {
|
||||
providers: "mdl_prov",
|
||||
back: "mdl_back",
|
||||
list: "mdl_list_",
|
||||
selectStandard: "mdl_sel_",
|
||||
selectCompact: "mdl_sel/",
|
||||
} as const;
|
||||
|
||||
function truncateModelId(value: string, maxChars: number): string {
|
||||
if (value.length <= maxChars) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
|
||||
}
|
||||
|
||||
function buildModelSelectionCallbackData(params: {
|
||||
provider: string;
|
||||
model: string;
|
||||
}): string | null {
|
||||
const fullCallbackData = `${CALLBACK_PREFIX.selectStandard}${params.provider}/${params.model}`;
|
||||
if (Buffer.byteLength(fullCallbackData, "utf8") <= MAX_CALLBACK_DATA_BYTES) {
|
||||
return fullCallbackData;
|
||||
}
|
||||
const compactCallbackData = `${CALLBACK_PREFIX.selectCompact}${params.model}`;
|
||||
return Buffer.byteLength(compactCallbackData, "utf8") <= MAX_CALLBACK_DATA_BYTES
|
||||
? compactCallbackData
|
||||
: null;
|
||||
}
|
||||
|
||||
export function buildProviderKeyboard(providers: ProviderInfo[]): ButtonRow[] {
|
||||
if (providers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: ButtonRow[] = [];
|
||||
let currentRow: ButtonRow = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
currentRow.push({
|
||||
text: `${provider.id} (${provider.count})`,
|
||||
callback_data: `mdl_list_${provider.id}_1`,
|
||||
});
|
||||
if (currentRow.length === 2) {
|
||||
rows.push(currentRow);
|
||||
currentRow = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] {
|
||||
const { provider, models, currentModel, currentPage, totalPages, modelNames } = params;
|
||||
const pageSize = params.pageSize ?? MODELS_PAGE_SIZE;
|
||||
|
||||
if (models.length === 0) {
|
||||
return [[{ text: "<< Back", callback_data: CALLBACK_PREFIX.back }]];
|
||||
}
|
||||
|
||||
const rows: ButtonRow[] = [];
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, models.length);
|
||||
const pageModels = models.slice(startIndex, endIndex);
|
||||
const currentModelId = currentModel?.includes("/")
|
||||
? currentModel.split("/").slice(1).join("/")
|
||||
: currentModel;
|
||||
|
||||
for (const model of pageModels) {
|
||||
const callbackData = buildModelSelectionCallbackData({ provider, model });
|
||||
if (!callbackData) {
|
||||
continue;
|
||||
}
|
||||
const isCurrentModel = model === currentModelId;
|
||||
const displayLabel = modelNames?.get(`${provider}/${model}`) ?? model;
|
||||
const displayText = truncateModelId(displayLabel, 38);
|
||||
rows.push([
|
||||
{
|
||||
text: isCurrentModel ? `${displayText} ✓` : displayText,
|
||||
callback_data: callbackData,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const navRow: ButtonRow = [];
|
||||
if (currentPage > 1) {
|
||||
navRow.push({
|
||||
text: "Previous",
|
||||
callback_data: `${CALLBACK_PREFIX.list}${provider}_${currentPage - 1}`,
|
||||
});
|
||||
}
|
||||
if (currentPage < totalPages) {
|
||||
navRow.push({
|
||||
text: "Next",
|
||||
callback_data: `${CALLBACK_PREFIX.list}${provider}_${currentPage + 1}`,
|
||||
});
|
||||
}
|
||||
if (navRow.length > 0) {
|
||||
rows.push(navRow);
|
||||
}
|
||||
|
||||
rows.push([{ text: "<< Back", callback_data: CALLBACK_PREFIX.providers }]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function buildBrowseProvidersButton(): ButtonRow[] {
|
||||
return [[{ text: "Browse providers", callback_data: CALLBACK_PREFIX.providers }]];
|
||||
}
|
||||
|
||||
export function getModelsPageSize(): number {
|
||||
return MODELS_PAGE_SIZE;
|
||||
}
|
||||
|
||||
export function calculateTotalPages(totalModels: number, pageSize = MODELS_PAGE_SIZE): number {
|
||||
if (totalModels <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.ceil(totalModels / pageSize);
|
||||
}
|
||||
|
|
@ -10,15 +10,15 @@ import {
|
|||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { rejectUnauthorizedCommand } from "./command-gates.js";
|
||||
import {
|
||||
buildModelsKeyboard,
|
||||
buildProviderKeyboard,
|
||||
calculateTotalPages,
|
||||
getModelsPageSize,
|
||||
type ProviderInfo,
|
||||
} from "../../plugin-sdk/telegram-runtime.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { rejectUnauthorizedCommand } from "./command-gates.js";
|
||||
} from "./commands-models.telegram.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
const PAGE_SIZE_DEFAULT = 20;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,42 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
|
||||
import {
|
||||
resolveThreadBindingIdleTimeoutMs,
|
||||
resolveThreadBindingInactivityExpiresAt,
|
||||
resolveThreadBindingMaxAgeExpiresAt,
|
||||
resolveThreadBindingMaxAgeMs,
|
||||
} from "../../plugin-sdk/discord.js";
|
||||
|
||||
function resolveThreadBindingIdleTimeoutMs(params: {
|
||||
record: { idleTimeoutMs?: number };
|
||||
defaultIdleTimeoutMs: number;
|
||||
}): number {
|
||||
return typeof params.record.idleTimeoutMs === "number"
|
||||
? Math.max(0, Math.floor(params.record.idleTimeoutMs))
|
||||
: params.defaultIdleTimeoutMs;
|
||||
}
|
||||
|
||||
function resolveThreadBindingInactivityExpiresAt(params: {
|
||||
record: { boundAt: number; lastActivityAt: number; idleTimeoutMs?: number };
|
||||
defaultIdleTimeoutMs: number;
|
||||
}): number | undefined {
|
||||
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMs(params);
|
||||
return idleTimeoutMs > 0
|
||||
? Math.max(params.record.lastActivityAt, params.record.boundAt) + idleTimeoutMs
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveThreadBindingMaxAgeMs(params: {
|
||||
record: { maxAgeMs?: number };
|
||||
defaultMaxAgeMs: number;
|
||||
}): number {
|
||||
return typeof params.record.maxAgeMs === "number"
|
||||
? Math.max(0, Math.floor(params.record.maxAgeMs))
|
||||
: params.defaultMaxAgeMs;
|
||||
}
|
||||
|
||||
function resolveThreadBindingMaxAgeExpiresAt(params: {
|
||||
record: { boundAt: number; maxAgeMs?: number };
|
||||
defaultMaxAgeMs: number;
|
||||
}): number | undefined {
|
||||
const maxAgeMs = resolveThreadBindingMaxAgeMs(params);
|
||||
return maxAgeMs > 0 ? params.record.boundAt + maxAgeMs : undefined;
|
||||
}
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const getThreadBindingManagerMock = vi.fn();
|
||||
|
|
@ -29,10 +59,30 @@ const hoisted = vi.hoisted(() => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock("../../plugins/runtime/index.js", async () => {
|
||||
vi.mock("../../plugins/runtime/index.js", () => {
|
||||
return {
|
||||
createPluginRuntime: () => ({
|
||||
channel: {
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey: ({ channelId, ...params }: Record<string, unknown>) => {
|
||||
if (channelId === "telegram") {
|
||||
return hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock(params);
|
||||
}
|
||||
if (channelId === "matrix") {
|
||||
return hoisted.setMatrixThreadBindingIdleTimeoutBySessionKeyMock(params);
|
||||
}
|
||||
return hoisted.setThreadBindingIdleTimeoutBySessionKeyMock(params);
|
||||
},
|
||||
setMaxAgeBySessionKey: ({ channelId, ...params }: Record<string, unknown>) => {
|
||||
if (channelId === "telegram") {
|
||||
return hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock(params);
|
||||
}
|
||||
if (channelId === "matrix") {
|
||||
return hoisted.setMatrixThreadBindingMaxAgeBySessionKeyMock(params);
|
||||
}
|
||||
return hoisted.setThreadBindingMaxAgeBySessionKeyMock(params);
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
threadBindings: {
|
||||
getManager: hoisted.getThreadBindingManagerMock,
|
||||
|
|
@ -45,12 +95,6 @@ vi.mock("../../plugins/runtime/index.js", async () => {
|
|||
unbindBySessionKey: vi.fn(),
|
||||
},
|
||||
},
|
||||
telegram: {
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey: hoisted.setTelegramThreadBindingIdleTimeoutBySessionKeyMock,
|
||||
setMaxAgeBySessionKey: hoisted.setTelegramThreadBindingMaxAgeBySessionKeyMock,
|
||||
},
|
||||
},
|
||||
matrix: {
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey: hoisted.setMatrixThreadBindingIdleTimeoutBySessionKeyMock,
|
||||
|
|
@ -62,11 +106,8 @@ vi.mock("../../plugins/runtime/index.js", async () => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock("../../infra/outbound/session-binding-service.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../../infra/outbound/session-binding-service.js")>();
|
||||
vi.mock("../../infra/outbound/session-binding-service.js", () => {
|
||||
return {
|
||||
...actual,
|
||||
getSessionBindingService: () => ({
|
||||
bind: vi.fn(),
|
||||
getCapabilities: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -666,12 +666,14 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
|
|||
});
|
||||
}
|
||||
return action === SESSION_ACTION_IDLE
|
||||
? channelRuntime.telegram.threadBindings.setIdleTimeoutBySessionKey({
|
||||
? channelRuntime.threadBindings.setIdleTimeoutBySessionKey({
|
||||
channelId: "telegram",
|
||||
targetSessionKey: telegramBinding!.targetSessionKey,
|
||||
accountId,
|
||||
idleTimeoutMs: durationMs,
|
||||
})
|
||||
: channelRuntime.telegram.threadBindings.setMaxAgeBySessionKey({
|
||||
: channelRuntime.threadBindings.setMaxAgeBySessionKey({
|
||||
channelId: "telegram",
|
||||
targetSessionKey: telegramBinding!.targetSessionKey,
|
||||
accountId,
|
||||
maxAgeMs: durationMs,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import {
|
|||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { buildBrowseProvidersButton } from "../../plugin-sdk/telegram-runtime.js";
|
||||
import { shortenHomePath } from "../../utils.js";
|
||||
import { resolveSelectedAndActiveModel } from "../model-runtime.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { resolveModelsCommandReply } from "./commands-models.js";
|
||||
import { buildBrowseProvidersButton } from "./commands-models.telegram.js";
|
||||
import {
|
||||
formatAuthLabel,
|
||||
type ModelAuthDetailMode,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseTelegramTarget } from "../../plugin-sdk/telegram-runtime.js";
|
||||
import { parseExplicitTargetForChannel } from "../../channels/plugins/target-parsing.js";
|
||||
|
||||
type TelegramConversationParams = {
|
||||
ctx: {
|
||||
|
|
@ -25,7 +25,7 @@ export function resolveTelegramConversationId(
|
|||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
const chatId = toCandidates
|
||||
.map((candidate) => parseTelegramTarget(candidate).chatId.trim())
|
||||
.map((candidate) => parseExplicitTargetForChannel("telegram", candidate)?.to.trim() ?? "")
|
||||
.find((candidate) => candidate.length > 0);
|
||||
if (!chatId) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type {
|
|||
MediaUnderstandingDecision,
|
||||
MediaUnderstandingOutput,
|
||||
} from "../media-understanding/types.js";
|
||||
import type { StickerMetadata } from "../plugin-sdk/telegram-runtime.js";
|
||||
import type { InputProvenance } from "../sessions/input-provenance.js";
|
||||
import type { InternalMessageChannel } from "../utils/message-channel.js";
|
||||
import type { CommandArgs } from "./commands-registry.types.js";
|
||||
|
|
@ -11,6 +10,18 @@ import type { CommandArgs } from "./commands-registry.types.js";
|
|||
/** Valid message channels for routing. */
|
||||
export type OriginatingChannelType = ChannelId | InternalMessageChannel;
|
||||
|
||||
export type StickerContextMetadata = {
|
||||
cachedDescription?: string;
|
||||
emoji?: string;
|
||||
setName?: string;
|
||||
description?: string;
|
||||
fileId?: string;
|
||||
fileUniqueId?: string;
|
||||
uniqueFileId?: string;
|
||||
isAnimated?: boolean;
|
||||
isVideo?: boolean;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export type MsgContext = {
|
||||
Body?: string;
|
||||
/**
|
||||
|
|
@ -94,7 +105,7 @@ export type MsgContext = {
|
|||
MediaUrls?: string[];
|
||||
MediaTypes?: string[];
|
||||
/** Telegram sticker metadata (emoji, set name, file IDs, cached description). */
|
||||
Sticker?: StickerMetadata;
|
||||
Sticker?: StickerContextMetadata;
|
||||
/** True when current-turn sticker media is present in MediaPaths (false for cached-description path). */
|
||||
StickerMediaIncluded?: boolean;
|
||||
OutputDir?: string;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDiscordRuntimeGroupPolicy } from "../../../plugin-sdk/discord-surface.js";
|
||||
import { resolveIMessageRuntimeGroupPolicy } from "../../../plugin-sdk/imessage-policy.js";
|
||||
import { resolveSlackRuntimeGroupPolicy } from "../../../plugin-sdk/slack-surface.js";
|
||||
import { resolveTelegramRuntimeGroupPolicy } from "../../../plugin-sdk/telegram-runtime-surface.js";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "../../../config/runtime-group-policy.js";
|
||||
import { whatsappAccessControlTesting } from "../../../plugin-sdk/whatsapp-surface.js";
|
||||
import {
|
||||
evaluateZaloGroupAccess,
|
||||
|
|
@ -11,7 +8,7 @@ import {
|
|||
import { installChannelRuntimeGroupPolicyFallbackSuite } from "./suites.js";
|
||||
|
||||
describe("channel runtime group policy contract", () => {
|
||||
type ResolvedGroupPolicy = ReturnType<typeof resolveDiscordRuntimeGroupPolicy>;
|
||||
type ResolvedGroupPolicy = ReturnType<typeof resolveOpenProviderRuntimeGroupPolicy>;
|
||||
|
||||
function expectResolvedGroupPolicyCase(
|
||||
resolved: Pick<ResolvedGroupPolicy, "groupPolicy" | "providerMissingFallbackApplied">,
|
||||
|
|
@ -31,12 +28,12 @@ describe("channel runtime group policy contract", () => {
|
|||
|
||||
function expectResolvedDiscordGroupPolicyCase(params: {
|
||||
providerConfigPresent: Parameters<
|
||||
typeof resolveDiscordRuntimeGroupPolicy
|
||||
typeof resolveOpenProviderRuntimeGroupPolicy
|
||||
>[0]["providerConfigPresent"];
|
||||
groupPolicy: Parameters<typeof resolveDiscordRuntimeGroupPolicy>[0]["groupPolicy"];
|
||||
groupPolicy: Parameters<typeof resolveOpenProviderRuntimeGroupPolicy>[0]["groupPolicy"];
|
||||
expected: Pick<ResolvedGroupPolicy, "groupPolicy" | "providerMissingFallbackApplied">;
|
||||
}) {
|
||||
expectResolvedGroupPolicyCase(resolveDiscordRuntimeGroupPolicy(params), params.expected);
|
||||
expectResolvedGroupPolicyCase(resolveOpenProviderRuntimeGroupPolicy(params), params.expected);
|
||||
}
|
||||
|
||||
function expectAllowedZaloGroupAccessCase(
|
||||
|
|
@ -52,7 +49,7 @@ describe("channel runtime group policy contract", () => {
|
|||
|
||||
describe("slack", () => {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveSlackRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open default when channels.slack is configured",
|
||||
defaultGroupPolicyUnderTest: "open",
|
||||
missingConfigLabel: "fails closed when channels.slack is missing and no defaults are set",
|
||||
|
|
@ -62,7 +59,7 @@ describe("channel runtime group policy contract", () => {
|
|||
|
||||
describe("telegram", () => {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveTelegramRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open fallback when channels.telegram is configured",
|
||||
defaultGroupPolicyUnderTest: "disabled",
|
||||
missingConfigLabel: "fails closed when channels.telegram is missing and no defaults are set",
|
||||
|
|
@ -82,7 +79,7 @@ describe("channel runtime group policy contract", () => {
|
|||
|
||||
describe("imessage", () => {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveIMessageRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open fallback when channels.imessage is configured",
|
||||
defaultGroupPolicyUnderTest: "disabled",
|
||||
missingConfigLabel: "fails closed when channels.imessage is missing and no defaults are set",
|
||||
|
|
@ -92,7 +89,7 @@ describe("channel runtime group policy contract", () => {
|
|||
|
||||
describe("discord", () => {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveDiscordRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open default when channels.discord is configured",
|
||||
defaultGroupPolicyUnderTest: "open",
|
||||
missingConfigLabel: "fails closed when channels.discord is missing and no defaults are set",
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@ import {
|
|||
type SessionBindingCapabilities,
|
||||
type SessionBindingRecord,
|
||||
} from "../../../infra/outbound/session-binding-service.js";
|
||||
import { createBlueBubblesConversationBindingManager } from "../../../plugin-sdk/bluebubbles.js";
|
||||
import { createDiscordThreadBindingManager } from "../../../plugin-sdk/discord.js";
|
||||
import { createFeishuThreadBindingManager } from "../../../plugin-sdk/feishu.js";
|
||||
import { createIMessageConversationBindingManager } from "../../../plugin-sdk/imessage.js";
|
||||
import {
|
||||
listLineAccountIds,
|
||||
resolveDefaultLineAccountId,
|
||||
|
|
@ -22,13 +20,13 @@ import {
|
|||
resetMatrixThreadBindingsForTests,
|
||||
setMatrixRuntime,
|
||||
} from "../../../plugin-sdk/matrix.js";
|
||||
import { createTelegramThreadBindingManager } from "../../../plugin-sdk/telegram-runtime.js";
|
||||
import { loadBundledPluginTestApiSync } from "../../../test-utils/bundled-plugin-public-surface.js";
|
||||
import {
|
||||
listBundledChannelPlugins,
|
||||
requireBundledChannelPlugin,
|
||||
setBundledChannelRuntime,
|
||||
} from "../bundled.js";
|
||||
import { createChannelConversationBindingManager } from "../conversation-bindings.js";
|
||||
import type { ChannelPlugin } from "../types.js";
|
||||
import {
|
||||
channelPluginSurfaceKeys,
|
||||
|
|
@ -655,7 +653,8 @@ const sessionBindingContractEntries: Record<
|
|||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
createBlueBubblesConversationBindingManager({
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
|
|
@ -665,7 +664,8 @@ const sessionBindingContractEntries: Record<
|
|||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
createBlueBubblesConversationBindingManager({
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
|
|
@ -694,10 +694,12 @@ const sessionBindingContractEntries: Record<
|
|||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
createBlueBubblesConversationBindingManager({
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "bluebubbles",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
}).stop();
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
|
|
@ -829,7 +831,8 @@ const sessionBindingContractEntries: Record<
|
|||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
createIMessageConversationBindingManager({
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
|
|
@ -839,7 +842,8 @@ const sessionBindingContractEntries: Record<
|
|||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
createIMessageConversationBindingManager({
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
});
|
||||
|
|
@ -868,10 +872,12 @@ const sessionBindingContractEntries: Record<
|
|||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
createIMessageConversationBindingManager({
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "imessage",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
}).stop();
|
||||
});
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "imessage",
|
||||
accountId: "default",
|
||||
|
|
@ -937,10 +943,10 @@ const sessionBindingContractEntries: Record<
|
|||
placements: ["current"],
|
||||
},
|
||||
getCapabilities: () => {
|
||||
createTelegramThreadBindingManager({
|
||||
void createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
return getSessionBindingService().getCapabilities({
|
||||
channel: "telegram",
|
||||
|
|
@ -948,10 +954,10 @@ const sessionBindingContractEntries: Record<
|
|||
});
|
||||
},
|
||||
bindAndResolve: async () => {
|
||||
createTelegramThreadBindingManager({
|
||||
await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
const service = getSessionBindingService();
|
||||
const binding = await service.bind({
|
||||
|
|
@ -977,12 +983,12 @@ const sessionBindingContractEntries: Record<
|
|||
},
|
||||
unbindAndVerify: unbindAndExpectClearedSessionBinding,
|
||||
cleanup: async () => {
|
||||
const manager = createTelegramThreadBindingManager({
|
||||
const manager = await createChannelConversationBindingManager({
|
||||
channelId: "telegram",
|
||||
cfg: baseSessionBindingCfg,
|
||||
accountId: "default",
|
||||
persist: false,
|
||||
enableSweeper: false,
|
||||
});
|
||||
manager.stop();
|
||||
await manager?.stop();
|
||||
expectClearedSessionBinding({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { getChannelPlugin } from "./registry.js";
|
||||
import type { ChannelId } from "./types.js";
|
||||
|
||||
export async function createChannelConversationBindingManager(params: {
|
||||
channelId: ChannelId;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): Promise<{ stop: () => void | Promise<void> } | null> {
|
||||
const createManager = getChannelPlugin(params.channelId)?.conversationBindings?.createManager;
|
||||
if (!createManager) {
|
||||
return null;
|
||||
}
|
||||
return await createManager({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
export function setChannelConversationBindingIdleTimeoutBySessionKey(params: {
|
||||
channelId: ChannelId;
|
||||
targetSessionKey: string;
|
||||
accountId?: string | null;
|
||||
idleTimeoutMs: number;
|
||||
}): Array<{
|
||||
boundAt: number;
|
||||
lastActivityAt: number;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
}> {
|
||||
const setIdleTimeoutBySessionKey = getChannelPlugin(params.channelId)?.conversationBindings
|
||||
?.setIdleTimeoutBySessionKey;
|
||||
if (!setIdleTimeoutBySessionKey) {
|
||||
return [];
|
||||
}
|
||||
return setIdleTimeoutBySessionKey({
|
||||
targetSessionKey: params.targetSessionKey,
|
||||
accountId: params.accountId,
|
||||
idleTimeoutMs: params.idleTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function setChannelConversationBindingMaxAgeBySessionKey(params: {
|
||||
channelId: ChannelId;
|
||||
targetSessionKey: string;
|
||||
accountId?: string | null;
|
||||
maxAgeMs: number;
|
||||
}): Array<{
|
||||
boundAt: number;
|
||||
lastActivityAt: number;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
}> {
|
||||
const setMaxAgeBySessionKey = getChannelPlugin(params.channelId)?.conversationBindings
|
||||
?.setMaxAgeBySessionKey;
|
||||
if (!setMaxAgeBySessionKey) {
|
||||
return [];
|
||||
}
|
||||
return setMaxAgeBySessionKey({
|
||||
targetSessionKey: params.targetSessionKey,
|
||||
accountId: params.accountId,
|
||||
maxAgeMs: params.maxAgeMs,
|
||||
});
|
||||
}
|
||||
|
|
@ -643,6 +643,33 @@ export type ChannelConfiguredBindingProvider = {
|
|||
|
||||
export type ChannelConversationBindingSupport = {
|
||||
supportsCurrentConversationBinding?: boolean;
|
||||
setIdleTimeoutBySessionKey?: (params: {
|
||||
targetSessionKey: string;
|
||||
accountId?: string | null;
|
||||
idleTimeoutMs: number;
|
||||
}) => Array<{
|
||||
boundAt: number;
|
||||
lastActivityAt: number;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
}>;
|
||||
setMaxAgeBySessionKey?: (params: {
|
||||
targetSessionKey: string;
|
||||
accountId?: string | null;
|
||||
maxAgeMs: number;
|
||||
}) => Array<{
|
||||
boundAt: number;
|
||||
lastActivityAt: number;
|
||||
idleTimeoutMs?: number;
|
||||
maxAgeMs?: number;
|
||||
}>;
|
||||
createManager?: (params: { cfg: OpenClawConfig; accountId?: string | null }) =>
|
||||
| {
|
||||
stop: () => void | Promise<void>;
|
||||
}
|
||||
| Promise<{
|
||||
stop: () => void | Promise<void>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
|
||||
|
|
|
|||
|
|
@ -463,6 +463,14 @@ export type ChannelMessagingAdapter = {
|
|||
|
||||
export type ChannelAgentPromptAdapter = {
|
||||
messageToolHints?: (params: { cfg: OpenClawConfig; accountId?: string | null }) => string[];
|
||||
messageToolCapabilities?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => string[] | undefined;
|
||||
reactionGuidance?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}) => { level: "minimal" | "extensive"; channelLabel?: string } | undefined;
|
||||
};
|
||||
|
||||
export type ChannelDirectoryEntryKind = "user" | "group" | "channel";
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type {
|
|||
ChannelConfiguredBindingConversationRef,
|
||||
ChannelConfiguredBindingMatch,
|
||||
ChannelConfiguredBindingProvider,
|
||||
ChannelConversationBindingSupport,
|
||||
ChannelPairingAdapter,
|
||||
ChannelSecurityAdapter,
|
||||
ChannelSetupAdapter,
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import { inspectTelegramAccount as inspectTelegramAccountImpl } from "../plugin-sdk/telegram-runtime.js";
|
||||
|
||||
export type { InspectedTelegramAccount } from "../plugin-sdk/telegram-runtime.js";
|
||||
|
||||
type InspectTelegramAccount =
|
||||
typeof import("../plugin-sdk/telegram-runtime.js").inspectTelegramAccount;
|
||||
|
||||
export function inspectTelegramAccount(
|
||||
...args: Parameters<InspectTelegramAccount>
|
||||
): ReturnType<InspectTelegramAccount> {
|
||||
return inspectTelegramAccountImpl(...args);
|
||||
}
|
||||
|
|
@ -1,59 +1,19 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { getChannelPlugin } from "./plugins/registry.js";
|
||||
import type { ChannelId } from "./plugins/types.js";
|
||||
|
||||
type DiscordInspectModule = typeof import("./read-only-account-inspect.discord.runtime.js");
|
||||
type SlackInspectModule = typeof import("./read-only-account-inspect.slack.runtime.js");
|
||||
type TelegramInspectModule = typeof import("./read-only-account-inspect.telegram.js");
|
||||
|
||||
let discordInspectModulePromise: Promise<DiscordInspectModule> | undefined;
|
||||
let slackInspectModulePromise: Promise<SlackInspectModule> | undefined;
|
||||
let telegramInspectModulePromise: Promise<TelegramInspectModule> | undefined;
|
||||
|
||||
function loadDiscordInspectModule() {
|
||||
discordInspectModulePromise ??= import("./read-only-account-inspect.discord.runtime.js");
|
||||
return discordInspectModulePromise;
|
||||
}
|
||||
|
||||
function loadSlackInspectModule() {
|
||||
slackInspectModulePromise ??= import("./read-only-account-inspect.slack.runtime.js");
|
||||
return slackInspectModulePromise;
|
||||
}
|
||||
|
||||
function loadTelegramInspectModule() {
|
||||
telegramInspectModulePromise ??= import("./read-only-account-inspect.telegram.js");
|
||||
return telegramInspectModulePromise;
|
||||
}
|
||||
|
||||
export type ReadOnlyInspectedAccount =
|
||||
| Awaited<ReturnType<DiscordInspectModule["inspectDiscordAccount"]>>
|
||||
| Awaited<ReturnType<SlackInspectModule["inspectSlackAccount"]>>
|
||||
| Awaited<ReturnType<TelegramInspectModule["inspectTelegramAccount"]>>;
|
||||
export type ReadOnlyInspectedAccount = Record<string, unknown>;
|
||||
|
||||
export async function inspectReadOnlyChannelAccount(params: {
|
||||
channelId: ChannelId;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): Promise<ReadOnlyInspectedAccount | null> {
|
||||
if (params.channelId === "discord") {
|
||||
const { inspectDiscordAccount } = await loadDiscordInspectModule();
|
||||
return inspectDiscordAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const inspectAccount = getChannelPlugin(params.channelId)?.config.inspectAccount;
|
||||
if (!inspectAccount) {
|
||||
return null;
|
||||
}
|
||||
if (params.channelId === "slack") {
|
||||
const { inspectSlackAccount } = await loadSlackInspectModule();
|
||||
return inspectSlackAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
if (params.channelId === "telegram") {
|
||||
const { inspectTelegramAccount } = await loadTelegramInspectModule();
|
||||
return inspectTelegramAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
return (await Promise.resolve(
|
||||
inspectAccount(params.cfg, params.accountId),
|
||||
)) as ReadOnlyInspectedAccount | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,28 @@
|
|||
import { sendMessageIMessage as sendMessageIMessageImpl } from "../../plugin-sdk/imessage.js";
|
||||
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
|
||||
type RuntimeSend = {
|
||||
sendMessage: typeof import("../../plugin-sdk/imessage.js").sendMessageIMessage;
|
||||
type IMessageRuntimeSendOpts = {
|
||||
config?: ReturnType<typeof loadConfig>;
|
||||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
accountId?: string;
|
||||
replyToId?: string;
|
||||
};
|
||||
|
||||
export const runtimeSend = {
|
||||
sendMessage: sendMessageIMessageImpl,
|
||||
} satisfies RuntimeSend;
|
||||
sendMessage: async (to: string, text: string, opts: IMessageRuntimeSendOpts = {}) => {
|
||||
const outbound = await loadChannelOutboundAdapter("imessage");
|
||||
if (!outbound?.sendText) {
|
||||
throw new Error("iMessage outbound adapter is unavailable.");
|
||||
}
|
||||
return await outbound.sendText({
|
||||
cfg: opts.config ?? loadConfig(),
|
||||
to,
|
||||
text,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
mediaLocalRoots: opts.mediaLocalRoots,
|
||||
accountId: opts.accountId,
|
||||
replyToId: opts.replyToId,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,39 @@
|
|||
import { sendMessageTelegram as sendMessageTelegramImpl } from "../../plugin-sdk/telegram-runtime.js";
|
||||
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
|
||||
type RuntimeSend = {
|
||||
sendMessage: typeof import("../../plugin-sdk/telegram-runtime.js").sendMessageTelegram;
|
||||
type TelegramRuntimeSendOpts = {
|
||||
cfg?: ReturnType<typeof loadConfig>;
|
||||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
accountId?: string;
|
||||
messageThreadId?: string | number;
|
||||
replyToMessageId?: string | number;
|
||||
silent?: boolean;
|
||||
forceDocument?: boolean;
|
||||
gatewayClientScopes?: readonly string[];
|
||||
};
|
||||
|
||||
export const runtimeSend = {
|
||||
sendMessage: sendMessageTelegramImpl,
|
||||
} satisfies RuntimeSend;
|
||||
sendMessage: async (to: string, text: string, opts: TelegramRuntimeSendOpts = {}) => {
|
||||
const outbound = await loadChannelOutboundAdapter("telegram");
|
||||
if (!outbound?.sendText) {
|
||||
throw new Error("Telegram outbound adapter is unavailable.");
|
||||
}
|
||||
return await outbound.sendText({
|
||||
cfg: opts.cfg ?? loadConfig(),
|
||||
to,
|
||||
text,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
mediaLocalRoots: opts.mediaLocalRoots,
|
||||
accountId: opts.accountId,
|
||||
threadId: opts.messageThreadId,
|
||||
replyToId:
|
||||
opts.replyToMessageId == null
|
||||
? undefined
|
||||
: String(opts.replyToMessageId).trim() || undefined,
|
||||
silent: opts.silent,
|
||||
forceDocument: opts.forceDocument,
|
||||
gatewayClientScopes: opts.gatewayClientScopes,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,34 +1,31 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { TelegramNetworkConfig } from "../../../config/types.telegram.js";
|
||||
|
||||
const resolveCommandSecretRefsViaGatewayMock = vi.hoisted(() => vi.fn());
|
||||
const listTelegramAccountIdsMock = vi.hoisted(() => vi.fn());
|
||||
const inspectTelegramAccountMock = vi.hoisted(() => vi.fn());
|
||||
const lookupTelegramChatIdMock = vi.hoisted(() => vi.fn());
|
||||
const resolveTelegramAccountMock = vi.hoisted(() => vi.fn());
|
||||
const telegramResolverMock = vi.hoisted(() => vi.fn());
|
||||
const getChannelPluginMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../../cli/command-secret-gateway.js", () => ({
|
||||
resolveCommandSecretRefsViaGateway: resolveCommandSecretRefsViaGatewayMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../../plugin-sdk/telegram.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../plugin-sdk/telegram.js")>();
|
||||
vi.mock("../../../channels/read-only-account-inspect.telegram.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("../../../channels/read-only-account-inspect.telegram.js")
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
listTelegramAccountIds: listTelegramAccountIdsMock,
|
||||
inspectTelegramAccount: inspectTelegramAccountMock,
|
||||
lookupTelegramChatId: lookupTelegramChatIdMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../plugin-sdk/account-resolution.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../plugin-sdk/account-resolution.js")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveTelegramAccount: resolveTelegramAccountMock,
|
||||
};
|
||||
});
|
||||
vi.mock("../../../channels/plugins/registry.js", () => ({
|
||||
getChannelPlugin: getChannelPluginMock,
|
||||
}));
|
||||
|
||||
import {
|
||||
collectTelegramAllowFromUsernameWarnings,
|
||||
|
|
@ -55,34 +52,16 @@ describe("doctor telegram provider warnings", () => {
|
|||
.mockReset()
|
||||
.mockImplementation((_params: { cfg: OpenClawConfig; accountId: string }) => ({
|
||||
enabled: true,
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
tokenStatus: "configured",
|
||||
}));
|
||||
resolveTelegramAccountMock
|
||||
.mockReset()
|
||||
.mockImplementation((params: { cfg: OpenClawConfig; accountId?: string | null }) => {
|
||||
const accountId = params.accountId?.trim() || "default";
|
||||
const telegram = params.cfg.channels?.telegram ?? {};
|
||||
const account =
|
||||
accountId === "default"
|
||||
? telegram
|
||||
: ((telegram.accounts?.[accountId] as Record<string, unknown> | undefined) ?? {});
|
||||
const token =
|
||||
typeof account.botToken === "string"
|
||||
? account.botToken
|
||||
: typeof telegram.botToken === "string"
|
||||
? telegram.botToken
|
||||
: "";
|
||||
return {
|
||||
accountId,
|
||||
token,
|
||||
tokenSource: token ? "config" : "none",
|
||||
config:
|
||||
account && typeof account === "object" && "network" in account
|
||||
? { network: account.network as TelegramNetworkConfig | undefined }
|
||||
: {},
|
||||
};
|
||||
});
|
||||
lookupTelegramChatIdMock.mockReset();
|
||||
telegramResolverMock.mockReset();
|
||||
getChannelPluginMock.mockReset().mockReturnValue({
|
||||
resolver: {
|
||||
resolveTargets: telegramResolverMock,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("shows first-run guidance when groups are not configured yet", () => {
|
||||
|
|
@ -213,18 +192,18 @@ describe("doctor telegram provider warnings", () => {
|
|||
});
|
||||
|
||||
it("repairs Telegram @username allowFrom entries to numeric ids", async () => {
|
||||
lookupTelegramChatIdMock.mockImplementation(async ({ chatId }: { chatId: string }) => {
|
||||
switch (chatId.toLowerCase()) {
|
||||
telegramResolverMock.mockImplementation(async ({ inputs }: { inputs: string[] }) => {
|
||||
switch (inputs[0]?.toLowerCase()) {
|
||||
case "@testuser":
|
||||
return "111";
|
||||
return [{ input: inputs[0], resolved: true, id: "111" }];
|
||||
case "@groupuser":
|
||||
return "222";
|
||||
return [{ input: inputs[0], resolved: true, id: "222" }];
|
||||
case "@topicuser":
|
||||
return "333";
|
||||
return [{ input: inputs[0], resolved: true, id: "333" }];
|
||||
case "@accountuser":
|
||||
return "444";
|
||||
return [{ input: inputs[0], resolved: true, id: "444" }];
|
||||
default:
|
||||
return null;
|
||||
return [{ input: inputs[0], resolved: false }];
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -268,11 +247,11 @@ describe("doctor telegram provider warnings", () => {
|
|||
});
|
||||
|
||||
it("sanitizes Telegram allowFrom repair change lines before logging", async () => {
|
||||
lookupTelegramChatIdMock.mockImplementation(async ({ chatId }: { chatId: string }) => {
|
||||
if (chatId === "@\u001b[31mtestuser") {
|
||||
return "12345";
|
||||
telegramResolverMock.mockImplementation(async ({ inputs }: { inputs: string[] }) => {
|
||||
if (inputs[0] === "@\u001b[31mtestuser") {
|
||||
return [{ input: inputs[0], resolved: true, id: "12345" }];
|
||||
}
|
||||
return null;
|
||||
return [{ input: inputs[0], resolved: false }];
|
||||
});
|
||||
|
||||
const result = await maybeRepairTelegramAllowFromUsernames({
|
||||
|
|
@ -296,13 +275,9 @@ describe("doctor telegram provider warnings", () => {
|
|||
it("keeps Telegram allowFrom entries unchanged when configured credentials are unavailable", async () => {
|
||||
inspectTelegramAccountMock.mockImplementation(() => ({
|
||||
enabled: true,
|
||||
tokenStatus: "configured_unavailable",
|
||||
}));
|
||||
resolveTelegramAccountMock.mockImplementation(() => ({
|
||||
accountId: "default",
|
||||
token: "",
|
||||
tokenSource: "none",
|
||||
config: {},
|
||||
tokenSource: "env",
|
||||
tokenStatus: "configured_unavailable",
|
||||
}));
|
||||
|
||||
const result = await maybeRepairTelegramAllowFromUsernames({
|
||||
|
|
@ -332,7 +307,7 @@ describe("doctor telegram provider warnings", () => {
|
|||
line.includes("configured Telegram bot credentials are unavailable"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(lookupTelegramChatIdMock).not.toHaveBeenCalled();
|
||||
expect(telegramResolverMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses network settings for Telegram allowFrom repair but ignores apiRoot and proxy", async () => {
|
||||
|
|
@ -357,15 +332,7 @@ describe("doctor telegram provider warnings", () => {
|
|||
hadUnresolvedTargets: false,
|
||||
});
|
||||
listTelegramAccountIdsMock.mockImplementation(() => ["work"]);
|
||||
resolveTelegramAccountMock.mockImplementation(() => ({
|
||||
accountId: "work",
|
||||
token: "tok",
|
||||
tokenSource: "config",
|
||||
config: {
|
||||
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
|
||||
},
|
||||
}));
|
||||
lookupTelegramChatIdMock.mockResolvedValue("12345");
|
||||
telegramResolverMock.mockResolvedValue([{ input: "@testuser", resolved: true, id: "12345" }]);
|
||||
|
||||
const result = await maybeRepairTelegramAllowFromUsernames({
|
||||
channels: {
|
||||
|
|
@ -388,11 +355,12 @@ describe("doctor telegram provider warnings", () => {
|
|||
};
|
||||
};
|
||||
expect(cfg.channels?.telegram?.accounts?.work?.allowFrom).toEqual(["12345"]);
|
||||
expect(lookupTelegramChatIdMock).toHaveBeenCalledWith({
|
||||
token: "tok",
|
||||
chatId: "@testuser",
|
||||
signal: expect.any(AbortSignal),
|
||||
network: { autoSelectFamily: false, dnsResultOrder: "ipv4first" },
|
||||
expect(telegramResolverMock).toHaveBeenCalledWith({
|
||||
cfg: expect.any(Object),
|
||||
accountId: "work",
|
||||
inputs: ["@testuser"],
|
||||
kind: "user",
|
||||
runtime: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getChannelPlugin } from "../../../channels/plugins/registry.js";
|
||||
import {
|
||||
inspectTelegramAccount,
|
||||
isNumericTelegramUserId,
|
||||
|
|
@ -7,8 +8,7 @@ import {
|
|||
import { resolveCommandSecretRefsViaGateway } from "../../../cli/command-secret-gateway.js";
|
||||
import { getChannelsCommandSecretTargetIds } from "../../../cli/command-secret-targets.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { TelegramNetworkConfig } from "../../../config/types.telegram.js";
|
||||
import { lookupTelegramChatId } from "../../../plugin-sdk/telegram.js";
|
||||
import { createNonExitingRuntime } from "../../../runtime.js";
|
||||
import { describeUnknownError } from "../../../secrets/shared.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
import { hasAllowFromEntries } from "../shared/allowlist.js";
|
||||
|
|
@ -24,11 +24,6 @@ type TelegramAllowFromListRef = {
|
|||
key: "allowFrom" | "groupAllowFrom";
|
||||
};
|
||||
|
||||
type ResolvedTelegramLookupAccount = {
|
||||
token: string;
|
||||
network?: TelegramNetworkConfig;
|
||||
};
|
||||
|
||||
export function collectTelegramAccountScopes(
|
||||
cfg: OpenClawConfig,
|
||||
): Array<{ prefix: string; account: Record<string, unknown> }> {
|
||||
|
|
@ -160,8 +155,7 @@ export async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig)
|
|||
return inspected.enabled && inspected.tokenStatus === "configured_unavailable";
|
||||
});
|
||||
const tokenResolutionWarnings: string[] = [];
|
||||
const lookupAccounts: ResolvedTelegramLookupAccount[] = [];
|
||||
const seenLookupAccounts = new Set<string>();
|
||||
const resolverAccountIds: string[] = [];
|
||||
for (const accountId of listTelegramAccountIds(resolvedConfig)) {
|
||||
let inspected: ReturnType<typeof inspectTelegramAccount>;
|
||||
try {
|
||||
|
|
@ -181,27 +175,25 @@ export async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig)
|
|||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const network = inspected.config.network;
|
||||
const cacheKey = `${token}::${JSON.stringify(network ?? {})}`;
|
||||
if (seenLookupAccounts.has(cacheKey)) {
|
||||
continue;
|
||||
}
|
||||
seenLookupAccounts.add(cacheKey);
|
||||
lookupAccounts.push({ token, network });
|
||||
resolverAccountIds.push(accountId);
|
||||
}
|
||||
|
||||
if (lookupAccounts.length === 0) {
|
||||
const telegramResolver = getChannelPlugin("telegram")?.resolver?.resolveTargets;
|
||||
if (resolverAccountIds.length === 0 || !telegramResolver) {
|
||||
return {
|
||||
config: cfg,
|
||||
changes: [
|
||||
...tokenResolutionWarnings,
|
||||
hasConfiguredUnavailableToken
|
||||
? `- Telegram allowFrom contains @username entries, but configured Telegram bot credentials are unavailable in this command path; cannot auto-resolve (start the gateway or make the secret source available, then rerun doctor --fix).`
|
||||
: `- Telegram allowFrom contains @username entries, but no Telegram bot token is configured; cannot auto-resolve (run setup or replace with numeric sender IDs).`,
|
||||
: !telegramResolver
|
||||
? `- Telegram allowFrom contains @username entries, but the Telegram channel resolver is unavailable; cannot auto-resolve in this command path.`
|
||||
: `- Telegram allowFrom contains @username entries, but no Telegram bot token is configured; cannot auto-resolve (run setup or replace with numeric sender IDs).`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const resolverRuntime = createNonExitingRuntime();
|
||||
const resolveUserId = async (raw: string): Promise<string | null> => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
|
|
@ -218,23 +210,20 @@ export async function maybeRepairTelegramAllowFromUsernames(cfg: OpenClawConfig)
|
|||
return null;
|
||||
}
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
for (const account of lookupAccounts) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 4000);
|
||||
for (const accountId of resolverAccountIds) {
|
||||
try {
|
||||
const id = await lookupTelegramChatId({
|
||||
token: account.token,
|
||||
chatId: username,
|
||||
signal: controller.signal,
|
||||
network: account.network,
|
||||
const [resolved] = await telegramResolver({
|
||||
cfg: resolvedConfig,
|
||||
accountId,
|
||||
inputs: [username],
|
||||
kind: "user",
|
||||
runtime: resolverRuntime,
|
||||
});
|
||||
if (id) {
|
||||
return id;
|
||||
if (resolved?.resolved && resolved.id) {
|
||||
return resolved.id;
|
||||
}
|
||||
} catch {
|
||||
// ignore and try next token
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
// ignore and try next configured account
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply
|
|||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import {
|
||||
setChannelConversationBindingIdleTimeoutBySessionKey,
|
||||
setChannelConversationBindingMaxAgeBySessionKey,
|
||||
} from "../../channels/plugins/conversation-bindings.js";
|
||||
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
||||
import { recordInboundSession } from "../../channels/session.js";
|
||||
import {
|
||||
resolveChannelGroupPolicy,
|
||||
|
|
@ -56,15 +61,17 @@ import {
|
|||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import {
|
||||
setThreadBindingIdleTimeoutBySessionKey,
|
||||
setThreadBindingMaxAgeBySessionKey,
|
||||
} from "../../plugin-sdk/discord.js";
|
||||
import { buildAgentSessionKey, resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { defineCachedValue } from "./runtime-cache.js";
|
||||
import { createRuntimeDiscord } from "./runtime-discord.js";
|
||||
import { createRuntimeIMessage } from "./runtime-imessage.js";
|
||||
import { createRuntimeLine } from "./runtime-line.js";
|
||||
import { createRuntimeMatrix } from "./runtime-matrix.js";
|
||||
import { createRuntimeSignal } from "./runtime-signal.js";
|
||||
import { createRuntimeSlack } from "./runtime-slack.js";
|
||||
import { createRuntimeTelegram } from "./runtime-telegram.js";
|
||||
import { createRuntimeWhatsApp } from "./runtime-whatsapp.js";
|
||||
import type { PluginRuntime } from "./types.js";
|
||||
|
||||
|
|
@ -151,23 +158,74 @@ export function createRuntimeChannel(): PluginRuntime["channel"] {
|
|||
shouldComputeCommandAuthorized,
|
||||
shouldHandleTextCommands,
|
||||
},
|
||||
outbound: {
|
||||
loadAdapter: loadChannelOutboundAdapter,
|
||||
},
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey: ({ channelId, targetSessionKey, accountId, idleTimeoutMs }) => {
|
||||
switch (channelId) {
|
||||
case "discord":
|
||||
return setThreadBindingIdleTimeoutBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId,
|
||||
idleTimeoutMs,
|
||||
});
|
||||
case "matrix":
|
||||
return setChannelConversationBindingIdleTimeoutBySessionKey({
|
||||
channelId,
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? "",
|
||||
idleTimeoutMs,
|
||||
});
|
||||
case "telegram":
|
||||
return setChannelConversationBindingIdleTimeoutBySessionKey({
|
||||
channelId,
|
||||
targetSessionKey,
|
||||
accountId,
|
||||
idleTimeoutMs,
|
||||
});
|
||||
}
|
||||
},
|
||||
setMaxAgeBySessionKey: ({ channelId, targetSessionKey, accountId, maxAgeMs }) => {
|
||||
switch (channelId) {
|
||||
case "discord":
|
||||
return setThreadBindingMaxAgeBySessionKey({
|
||||
targetSessionKey,
|
||||
accountId,
|
||||
maxAgeMs,
|
||||
});
|
||||
case "matrix":
|
||||
return setChannelConversationBindingMaxAgeBySessionKey({
|
||||
channelId,
|
||||
targetSessionKey,
|
||||
accountId: accountId ?? "",
|
||||
maxAgeMs,
|
||||
});
|
||||
case "telegram":
|
||||
return setChannelConversationBindingMaxAgeBySessionKey({
|
||||
channelId,
|
||||
targetSessionKey,
|
||||
accountId,
|
||||
maxAgeMs,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
} satisfies Omit<
|
||||
PluginRuntime["channel"],
|
||||
"discord" | "slack" | "telegram" | "matrix" | "signal" | "imessage" | "whatsapp" | "line"
|
||||
"discord" | "slack" | "matrix" | "signal" | "whatsapp" | "line"
|
||||
> &
|
||||
Partial<
|
||||
Pick<
|
||||
PluginRuntime["channel"],
|
||||
"discord" | "slack" | "telegram" | "matrix" | "signal" | "imessage" | "whatsapp" | "line"
|
||||
"discord" | "slack" | "matrix" | "signal" | "whatsapp" | "line"
|
||||
>
|
||||
>;
|
||||
|
||||
defineCachedValue(channelRuntime, "discord", createRuntimeDiscord);
|
||||
defineCachedValue(channelRuntime, "slack", createRuntimeSlack);
|
||||
defineCachedValue(channelRuntime, "telegram", createRuntimeTelegram);
|
||||
defineCachedValue(channelRuntime, "matrix", createRuntimeMatrix);
|
||||
defineCachedValue(channelRuntime, "signal", createRuntimeSignal);
|
||||
defineCachedValue(channelRuntime, "imessage", createRuntimeIMessage);
|
||||
defineCachedValue(channelRuntime, "whatsapp", createRuntimeWhatsApp);
|
||||
defineCachedValue(channelRuntime, "line", createRuntimeLine);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { installChannelRuntimeGroupPolicyFallbackSuite } from "../../../src/channels/plugins/contracts/suites.js";
|
||||
import { resolveDiscordRuntimeGroupPolicy } from "../../../src/plugin-sdk/discord-surface.js";
|
||||
import { resolveIMessageRuntimeGroupPolicy } from "../../../src/plugin-sdk/imessage-policy.js";
|
||||
import { resolveSlackRuntimeGroupPolicy } from "../../../src/plugin-sdk/slack-surface.js";
|
||||
import { resolveTelegramRuntimeGroupPolicy } from "../../../src/plugin-sdk/telegram-runtime-surface.js";
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "../../../src/config/runtime-group-policy.js";
|
||||
import { whatsappAccessControlTesting } from "../../../src/plugin-sdk/whatsapp-surface.js";
|
||||
import {
|
||||
evaluateZaloGroupAccess,
|
||||
|
|
@ -12,7 +9,7 @@ import {
|
|||
|
||||
export function installSlackGroupPolicyContractSuite() {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveSlackRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open default when channels.slack is configured",
|
||||
defaultGroupPolicyUnderTest: "open",
|
||||
missingConfigLabel: "fails closed when channels.slack is missing and no defaults are set",
|
||||
|
|
@ -22,7 +19,7 @@ export function installSlackGroupPolicyContractSuite() {
|
|||
|
||||
export function installTelegramGroupPolicyContractSuite() {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveTelegramRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open fallback when channels.telegram is configured",
|
||||
defaultGroupPolicyUnderTest: "disabled",
|
||||
missingConfigLabel: "fails closed when channels.telegram is missing and no defaults are set",
|
||||
|
|
@ -42,7 +39,7 @@ export function installWhatsAppGroupPolicyContractSuite() {
|
|||
|
||||
export function installIMessageGroupPolicyContractSuite() {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveIMessageRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open fallback when channels.imessage is configured",
|
||||
defaultGroupPolicyUnderTest: "disabled",
|
||||
missingConfigLabel: "fails closed when channels.imessage is missing and no defaults are set",
|
||||
|
|
@ -52,7 +49,7 @@ export function installIMessageGroupPolicyContractSuite() {
|
|||
|
||||
export function installDiscordGroupPolicyContractSuite() {
|
||||
installChannelRuntimeGroupPolicyFallbackSuite({
|
||||
resolve: resolveDiscordRuntimeGroupPolicy,
|
||||
resolve: resolveOpenProviderRuntimeGroupPolicy,
|
||||
configuredLabel: "keeps open default when channels.discord is configured",
|
||||
defaultGroupPolicyUnderTest: "open",
|
||||
missingConfigLabel: "fails closed when channels.discord is missing and no defaults are set",
|
||||
|
|
@ -60,7 +57,7 @@ export function installDiscordGroupPolicyContractSuite() {
|
|||
});
|
||||
|
||||
it("respects explicit provider policy", () => {
|
||||
const resolved = resolveDiscordRuntimeGroupPolicy({
|
||||
const resolved = resolveOpenProviderRuntimeGroupPolicy({
|
||||
providerConfigPresent: false,
|
||||
groupPolicy: "disabled",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import type { IMessageProbe } from "../../../extensions/imessage/runtime-api.js";
|
||||
import {
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
type TelegramProbe,
|
||||
type TelegramTokenResolution,
|
||||
} from "../../../extensions/telegram/api.js";
|
||||
import type {
|
||||
BaseProbeResult,
|
||||
BaseTokenResolution,
|
||||
|
|
@ -11,7 +18,6 @@ import {
|
|||
type DiscordProbe,
|
||||
type DiscordTokenResolution,
|
||||
} from "../../../src/plugin-sdk/discord-surface.js";
|
||||
import type { IMessageProbe } from "../../../src/plugin-sdk/imessage.js";
|
||||
import type { LineProbeResult } from "../../../src/plugin-sdk/line.js";
|
||||
import type { SignalProbe } from "../../../src/plugin-sdk/signal-surface.js";
|
||||
import {
|
||||
|
|
@ -19,12 +25,6 @@ import {
|
|||
listSlackDirectoryPeersFromConfig,
|
||||
type SlackProbe,
|
||||
} from "../../../src/plugin-sdk/slack-surface.js";
|
||||
import {
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
type TelegramProbe,
|
||||
type TelegramTokenResolution,
|
||||
} from "../../../src/plugin-sdk/telegram-surface.js";
|
||||
import {
|
||||
listWhatsAppDirectoryGroupsFromConfig,
|
||||
listWhatsAppDirectoryPeersFromConfig,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {
|
|||
import { __testing as sessionBindingTesting } from "../../../src/infra/outbound/session-binding-service.js";
|
||||
import { feishuThreadBindingTesting } from "../../../src/plugin-sdk/feishu-conversation.js";
|
||||
import { resetMatrixThreadBindingsForTests } from "../../../src/plugin-sdk/matrix.js";
|
||||
import { resetTelegramThreadBindingsForTests } from "../../../src/plugin-sdk/telegram-runtime-surface.js";
|
||||
import { loadBundledPluginTestApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js";
|
||||
|
||||
const { discordThreadBindingTesting } = loadBundledPluginTestApiSync<{
|
||||
|
|
@ -30,6 +29,9 @@ const { discordThreadBindingTesting } = loadBundledPluginTestApiSync<{
|
|||
resetThreadBindingsForTests: () => void;
|
||||
};
|
||||
}>("discord");
|
||||
const { resetTelegramThreadBindingsForTests } = loadBundledPluginTestApiSync<{
|
||||
resetTelegramThreadBindingsForTests: () => Promise<void>;
|
||||
}>("telegram");
|
||||
|
||||
function hasEntries<T extends { id: string }>(
|
||||
entries: readonly T[],
|
||||
|
|
|
|||
|
|
@ -290,14 +290,21 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
|||
shouldHandleTextCommands:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
},
|
||||
outbound: {
|
||||
loadAdapter: vi.fn() as unknown as PluginRuntime["channel"]["outbound"]["loadAdapter"],
|
||||
},
|
||||
threadBindings: {
|
||||
setIdleTimeoutBySessionKey:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setIdleTimeoutBySessionKey"],
|
||||
setMaxAgeBySessionKey:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setMaxAgeBySessionKey"],
|
||||
},
|
||||
discord: {} as PluginRuntime["channel"]["discord"],
|
||||
activity: {} as PluginRuntime["channel"]["activity"],
|
||||
line: {} as PluginRuntime["channel"]["line"],
|
||||
slack: {} as PluginRuntime["channel"]["slack"],
|
||||
telegram: {} as PluginRuntime["channel"]["telegram"],
|
||||
matrix: {} as PluginRuntime["channel"]["matrix"],
|
||||
signal: {} as PluginRuntime["channel"]["signal"],
|
||||
imessage: {} as PluginRuntime["channel"]["imessage"],
|
||||
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
||||
},
|
||||
events: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue