diff --git a/src/infra/exec-approval-forwarder.test.ts b/src/infra/exec-approval-forwarder.test.ts index ca4d81e012e..d1d72aecd24 100644 --- a/src/infra/exec-approval-forwarder.test.ts +++ b/src/infra/exec-approval-forwarder.test.ts @@ -424,7 +424,7 @@ describe("exec approval forwarder", () => { channel: "whatsapp", to: "+15555550123", accountId: "work", - threadId: "1739201675.123", + threadId: 1739201675, }), ); } finally { diff --git a/src/infra/exec-approval-forwarder.ts b/src/infra/exec-approval-forwarder.ts index ca9abbc80b5..1008531d2f1 100644 --- a/src/infra/exec-approval-forwarder.ts +++ b/src/infra/exec-approval-forwarder.ts @@ -1,7 +1,6 @@ import type { ReplyPayload } from "../auto-reply/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; -import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import type { ExecApprovalForwardingConfig, ExecApprovalForwardTarget, @@ -18,13 +17,13 @@ import { } from "../utils/message-channel.js"; import { resolveExecApprovalCommandDisplay } from "./exec-approval-command-display.js"; import { buildExecApprovalPendingReplyPayload } from "./exec-approval-reply.js"; +import { resolveExecApprovalSessionTarget } from "./exec-approval-session-target.js"; import type { ExecApprovalDecision, ExecApprovalRequest, ExecApprovalResolved, } from "./exec-approvals.js"; import { deliverOutboundPayloads } from "./outbound/deliver.js"; -import { resolveSessionDeliveryTarget } from "./outbound/targets.js"; const log = createSubsystemLogger("gateway/exec-approvals"); export type { ExecApprovalRequest, ExecApprovalResolved }; @@ -281,21 +280,9 @@ function defaultResolveSessionTarget(params: { cfg: OpenClawConfig; request: ExecApprovalRequest; }): ExecApprovalForwardTarget | null { - const sessionKey = params.request.request.sessionKey?.trim(); - if (!sessionKey) { - return null; - } - const parsed = parseAgentSessionKey(sessionKey); - const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; - const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); - const store = loadSessionStore(storePath); - const entry = store[sessionKey]; - if (!entry) { - return null; - } - const target = resolveSessionDeliveryTarget({ - entry, - requestedChannel: "last", + const target = resolveExecApprovalSessionTarget({ + cfg: params.cfg, + request: params.request, turnSourceChannel: normalizeTurnSourceChannel(params.request.request.turnSourceChannel), turnSourceTo: params.request.request.turnSourceTo?.trim() || undefined, turnSourceAccountId: params.request.request.turnSourceAccountId?.trim() || undefined, diff --git a/src/infra/exec-approval-session-target.ts b/src/infra/exec-approval-session-target.ts new file mode 100644 index 00000000000..71535914c38 --- /dev/null +++ b/src/infra/exec-approval-session-target.ts @@ -0,0 +1,69 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; +import { parseAgentSessionKey } from "../routing/session-key.js"; +import type { ExecApprovalRequest } from "./exec-approvals.js"; +import { resolveSessionDeliveryTarget } from "./outbound/targets.js"; + +export type ExecApprovalSessionTarget = { + channel?: string; + to: string; + accountId?: string; + threadId?: number; +}; + +function normalizeOptionalString(value?: string | null): string | undefined { + const normalized = value?.trim(); + return normalized ? normalized : undefined; +} + +function normalizeOptionalThreadId(value?: string | number | null): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (typeof value !== "string") { + return undefined; + } + const normalized = Number.parseInt(value, 10); + return Number.isFinite(normalized) ? normalized : undefined; +} + +export function resolveExecApprovalSessionTarget(params: { + cfg: OpenClawConfig; + request: ExecApprovalRequest; + turnSourceChannel?: string | null; + turnSourceTo?: string | null; + turnSourceAccountId?: string | null; + turnSourceThreadId?: string | number | null; +}): ExecApprovalSessionTarget | null { + const sessionKey = normalizeOptionalString(params.request.request.sessionKey); + if (!sessionKey) { + return null; + } + const parsed = parseAgentSessionKey(sessionKey); + const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; + const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); + const store = loadSessionStore(storePath); + const entry = store[sessionKey]; + if (!entry) { + return null; + } + + const target = resolveSessionDeliveryTarget({ + entry, + requestedChannel: "last", + turnSourceChannel: normalizeOptionalString(params.turnSourceChannel), + turnSourceTo: normalizeOptionalString(params.turnSourceTo), + turnSourceAccountId: normalizeOptionalString(params.turnSourceAccountId), + turnSourceThreadId: normalizeOptionalThreadId(params.turnSourceThreadId), + }); + if (!target.to) { + return null; + } + + return { + channel: normalizeOptionalString(target.channel), + to: target.to, + accountId: normalizeOptionalString(target.accountId), + threadId: normalizeOptionalThreadId(target.threadId), + }; +} diff --git a/src/media/fetch.telegram-network.test.ts b/src/media/fetch.telegram-network.test.ts index c9989867f0b..cb4cb1ab5b1 100644 --- a/src/media/fetch.telegram-network.test.ts +++ b/src/media/fetch.telegram-network.test.ts @@ -2,47 +2,35 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveTelegramTransport } from "../telegram/fetch.js"; import { fetchRemoteMedia } from "./fetch.js"; -const undiciFetch = vi.hoisted(() => vi.fn()); -const AgentCtor = vi.hoisted(() => - vi.fn(function MockAgent( - this: { options?: Record }, - options?: Record, - ) { - this.options = options; - }), -); -const EnvHttpProxyAgentCtor = vi.hoisted(() => - vi.fn(function MockEnvHttpProxyAgent( - this: { options?: Record }, - options?: Record, - ) { - this.options = options; - }), -); -const ProxyAgentCtor = vi.hoisted(() => - vi.fn(function MockProxyAgent( - this: { options?: Record | string }, - options?: Record | string, - ) { - this.options = options; - }), -); +const undiciMocks = vi.hoisted(() => { + const createDispatcherCtor = | string>() => + vi.fn(function MockDispatcher(this: { options?: T }, options?: T) { + this.options = options; + }); + + return { + fetch: vi.fn(), + agentCtor: createDispatcherCtor>(), + envHttpProxyAgentCtor: createDispatcherCtor>(), + proxyAgentCtor: createDispatcherCtor | string>(), + }; +}); vi.mock("undici", () => ({ - Agent: AgentCtor, - EnvHttpProxyAgent: EnvHttpProxyAgentCtor, - ProxyAgent: ProxyAgentCtor, - fetch: undiciFetch, + Agent: undiciMocks.agentCtor, + EnvHttpProxyAgent: undiciMocks.envHttpProxyAgentCtor, + ProxyAgent: undiciMocks.proxyAgentCtor, + fetch: undiciMocks.fetch, })); describe("fetchRemoteMedia telegram network policy", () => { type LookupFn = NonNullable[0]["lookupFn"]>; afterEach(() => { - undiciFetch.mockReset(); - AgentCtor.mockClear(); - EnvHttpProxyAgentCtor.mockClear(); - ProxyAgentCtor.mockClear(); + undiciMocks.fetch.mockReset(); + undiciMocks.agentCtor.mockClear(); + undiciMocks.envHttpProxyAgentCtor.mockClear(); + undiciMocks.proxyAgentCtor.mockClear(); vi.unstubAllEnvs(); }); @@ -50,7 +38,7 @@ describe("fetchRemoteMedia telegram network policy", () => { const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, ]) as unknown as LookupFn; - undiciFetch.mockResolvedValueOnce( + undiciMocks.fetch.mockResolvedValueOnce( new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), { status: 200, headers: { "content-type": "image/jpeg" }, @@ -76,7 +64,7 @@ describe("fetchRemoteMedia telegram network policy", () => { }, }); - const init = undiciFetch.mock.calls[0]?.[1] as + const init = undiciMocks.fetch.mock.calls[0]?.[1] as | (RequestInit & { dispatcher?: { options?: { @@ -100,7 +88,7 @@ describe("fetchRemoteMedia telegram network policy", () => { const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, ]) as unknown as LookupFn; - undiciFetch.mockResolvedValueOnce( + undiciMocks.fetch.mockResolvedValueOnce( new Response(new Uint8Array([0x25, 0x50, 0x44, 0x46]), { status: 200, headers: { "content-type": "application/pdf" }, @@ -126,7 +114,7 @@ describe("fetchRemoteMedia telegram network policy", () => { }, }); - const init = undiciFetch.mock.calls[0]?.[1] as + const init = undiciMocks.fetch.mock.calls[0]?.[1] as | (RequestInit & { dispatcher?: { options?: { @@ -137,6 +125,6 @@ describe("fetchRemoteMedia telegram network policy", () => { | undefined; expect(init?.dispatcher?.options?.uri).toBe("http://127.0.0.1:7890"); - expect(ProxyAgentCtor).toHaveBeenCalled(); + expect(undiciMocks.proxyAgentCtor).toHaveBeenCalled(); }); }); diff --git a/src/telegram/exec-approvals-handler.ts b/src/telegram/exec-approvals-handler.ts index 65488928469..01e3b51bedd 100644 --- a/src/telegram/exec-approvals-handler.ts +++ b/src/telegram/exec-approvals-handler.ts @@ -1,5 +1,4 @@ import type { OpenClawConfig } from "../config/config.js"; -import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { GatewayClient } from "../gateway/client.js"; import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js"; import type { EventFrame } from "../gateway/protocol/index.js"; @@ -8,8 +7,8 @@ import { buildExecApprovalPendingReplyPayload, type ExecApprovalPendingReplyParams, } from "../infra/exec-approval-reply.js"; +import { resolveExecApprovalSessionTarget } from "../infra/exec-approval-session-target.js"; import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js"; -import { resolveSessionDeliveryTarget } from "../infra/outbound/targets.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { normalizeAccountId, parseAgentSessionKey } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -120,40 +119,14 @@ function resolveRequestSessionTarget(params: { cfg: OpenClawConfig; request: ExecApprovalRequest; }): { to: string; accountId?: string; threadId?: number; channel?: string } | null { - const sessionKey = params.request.request.sessionKey?.trim(); - if (!sessionKey) { - return null; - } - const parsed = parseAgentSessionKey(sessionKey); - const agentId = parsed?.agentId ?? params.request.request.agentId ?? "main"; - const storePath = resolveStorePath(params.cfg.session?.store, { agentId }); - const store = loadSessionStore(storePath); - const entry = store[sessionKey]; - if (!entry) { - return null; - } - const target = resolveSessionDeliveryTarget({ - entry, - requestedChannel: "last", + return resolveExecApprovalSessionTarget({ + cfg: params.cfg, + request: params.request, turnSourceChannel: params.request.request.turnSourceChannel ?? undefined, turnSourceTo: params.request.request.turnSourceTo ?? undefined, turnSourceAccountId: params.request.request.turnSourceAccountId ?? undefined, turnSourceThreadId: params.request.request.turnSourceThreadId ?? undefined, }); - if (!target.to) { - return null; - } - return { - channel: target.channel ?? undefined, - to: target.to, - accountId: target.accountId ?? undefined, - threadId: - typeof target.threadId === "number" - ? target.threadId - : typeof target.threadId === "string" - ? Number.parseInt(target.threadId, 10) - : undefined, - }; } function resolveTelegramSourceTarget(params: {