mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
refactor(plugin-sdk): publish route helpers
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
bd1db6be3ae54ce8ba12599d5a0e57117670ecbb9ba2697d0c2fc79bfa83e3d1 plugin-sdk-api-baseline.json
|
||||
41a47969072f07398326c26fca7d419e67a2a97fb495eae15769ded574c843c8 plugin-sdk-api-baseline.jsonl
|
||||
92af5bb106da8278417701c301bc0dcc346cb21886956ab44b2b857e37b581be plugin-sdk-api-baseline.json
|
||||
9139536904eea7239a0d0060562270b06eb43ab755c9e012a5c6687447bbcb48 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -132,7 +132,6 @@ function createDiscordOriginTargetResolver(configOverride?: DiscordExecApprovalC
|
||||
}
|
||||
: null;
|
||||
},
|
||||
targetsMatch: (a, b) => a.to === b.to && a.threadId === b.threadId,
|
||||
resolveFallbackTarget: (request) => {
|
||||
const sessionConversation = resolveApprovalRequestSessionConversation({
|
||||
request,
|
||||
|
||||
@@ -85,11 +85,11 @@ function resolveSessionMatrixOriginTarget(sessionTarget: {
|
||||
};
|
||||
}
|
||||
|
||||
function matrixTargetsMatch(a: MatrixOriginTarget, b: MatrixOriginTarget): boolean {
|
||||
return (
|
||||
normalizeComparableTarget(a.to) === normalizeComparableTarget(b.to) &&
|
||||
(a.threadId ?? "") === (b.threadId ?? "")
|
||||
);
|
||||
function normalizeMatrixOriginTarget(target: MatrixOriginTarget): MatrixOriginTarget {
|
||||
return {
|
||||
...target,
|
||||
to: normalizeComparableTarget(target.to),
|
||||
};
|
||||
}
|
||||
|
||||
function hasMatrixPluginApprovers(params: { cfg: CoreConfig; accountId?: string | null }): boolean {
|
||||
@@ -159,7 +159,7 @@ const resolveMatrixOriginTarget = createChannelNativeOriginTargetResolver({
|
||||
}),
|
||||
resolveTurnSourceTarget: resolveTurnSourceMatrixOriginTarget,
|
||||
resolveSessionTarget: resolveSessionMatrixOriginTarget,
|
||||
targetsMatch: matrixTargetsMatch,
|
||||
normalizeTargetForMatch: normalizeMatrixOriginTarget,
|
||||
resolveFallbackTarget: (request) => {
|
||||
const sessionConversation = resolveApprovalRequestSessionConversation({
|
||||
request,
|
||||
|
||||
@@ -14,6 +14,10 @@ import type {
|
||||
PluginApprovalRequest,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract";
|
||||
import {
|
||||
channelRouteTargetsMatchExact,
|
||||
stringifyRouteThreadId,
|
||||
} from "openclaw/plugin-sdk/channel-route";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -69,12 +73,7 @@ function resolveTurnSourceSlackOriginTarget(request: ApprovalRequest): SlackOrig
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const threadId =
|
||||
typeof request.request.turnSourceThreadId === "string"
|
||||
? normalizeOptionalString(request.request.turnSourceThreadId)
|
||||
: typeof request.request.turnSourceThreadId === "number"
|
||||
? String(request.request.turnSourceThreadId)
|
||||
: undefined;
|
||||
const threadId = stringifyRouteThreadId(request.request.turnSourceThreadId);
|
||||
return {
|
||||
to: `${parsed.kind}:${parsed.id}`,
|
||||
threadId,
|
||||
@@ -87,12 +86,7 @@ function resolveSessionSlackOriginTarget(sessionTarget: {
|
||||
}): SlackOriginTarget {
|
||||
return {
|
||||
to: sessionTarget.to,
|
||||
threadId:
|
||||
typeof sessionTarget.threadId === "string"
|
||||
? normalizeOptionalString(sessionTarget.threadId)
|
||||
: typeof sessionTarget.threadId === "number"
|
||||
? String(sessionTarget.threadId)
|
||||
: undefined,
|
||||
threadId: stringifyRouteThreadId(sessionTarget.threadId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,10 +111,25 @@ function resolveSlackFallbackOriginTarget(request: ApprovalRequest): SlackOrigin
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSlackOriginTarget(target: SlackOriginTarget): SlackOriginTarget {
|
||||
return {
|
||||
...target,
|
||||
to: normalizeComparableTarget(target.to),
|
||||
};
|
||||
}
|
||||
|
||||
function slackTargetsMatch(a: SlackOriginTarget, b: SlackOriginTarget): boolean {
|
||||
return (
|
||||
normalizeComparableTarget(a.to) === normalizeComparableTarget(b.to) &&
|
||||
normalizeSlackThreadMatchKey(a.threadId) === normalizeSlackThreadMatchKey(b.threadId)
|
||||
channelRouteTargetsMatchExact({
|
||||
left: {
|
||||
channel: "slack",
|
||||
to: a.to,
|
||||
},
|
||||
right: {
|
||||
channel: "slack",
|
||||
to: b.to,
|
||||
},
|
||||
}) && normalizeSlackThreadMatchKey(a.threadId) === normalizeSlackThreadMatchKey(b.threadId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,6 +143,7 @@ const resolveSlackOriginTarget = createChannelNativeOriginTargetResolver({
|
||||
}),
|
||||
resolveTurnSourceTarget: resolveTurnSourceSlackOriginTarget,
|
||||
resolveSessionTarget: resolveSessionSlackOriginTarget,
|
||||
normalizeTargetForMatch: normalizeSlackOriginTarget,
|
||||
targetsMatch: slackTargetsMatch,
|
||||
resolveFallbackTarget: resolveSlackFallbackOriginTarget,
|
||||
});
|
||||
|
||||
@@ -61,12 +61,6 @@ function resolveSessionTelegramOriginTarget(sessionTarget: {
|
||||
};
|
||||
}
|
||||
|
||||
function telegramTargetsMatch(a: TelegramOriginTarget, b: TelegramOriginTarget): boolean {
|
||||
const normalizedA = normalizeTelegramChatId(a.to) ?? a.to;
|
||||
const normalizedB = normalizeTelegramChatId(b.to) ?? b.to;
|
||||
return normalizedA === normalizedB && a.threadId === b.threadId;
|
||||
}
|
||||
|
||||
const resolveTelegramOriginTarget = createChannelNativeOriginTargetResolver({
|
||||
channel: "telegram",
|
||||
shouldHandleRequest: ({ cfg, accountId, request }) =>
|
||||
@@ -77,7 +71,6 @@ const resolveTelegramOriginTarget = createChannelNativeOriginTargetResolver({
|
||||
}),
|
||||
resolveTurnSourceTarget: resolveTurnSourceTelegramOriginTarget,
|
||||
resolveSessionTarget: resolveSessionTelegramOriginTarget,
|
||||
targetsMatch: telegramTargetsMatch,
|
||||
});
|
||||
|
||||
const resolveTelegramApproverDmTargets = createChannelApproverDmTargetResolver({
|
||||
|
||||
@@ -806,6 +806,10 @@
|
||||
"types": "./dist/plugin-sdk/channel-send-result.d.ts",
|
||||
"default": "./dist/plugin-sdk/channel-send-result.js"
|
||||
},
|
||||
"./plugin-sdk/channel-route": {
|
||||
"types": "./dist/plugin-sdk/channel-route.d.ts",
|
||||
"default": "./dist/plugin-sdk/channel-route.js"
|
||||
},
|
||||
"./plugin-sdk/channel-targets": {
|
||||
"types": "./dist/plugin-sdk/channel-targets.d.ts",
|
||||
"default": "./dist/plugin-sdk/channel-targets.js"
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
"channel-pairing-paths",
|
||||
"channel-policy",
|
||||
"channel-send-result",
|
||||
"channel-route",
|
||||
"channel-targets",
|
||||
"context-visibility-runtime",
|
||||
"feishu",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeAccountId } from "../../utils/account-id.js";
|
||||
import { resolveMessageChannel } from "../../utils/message-channel.js";
|
||||
import type { AgentCommandOpts, AgentRunContext } from "./types.js";
|
||||
@@ -39,7 +40,10 @@ export function resolveAgentRunContext(opts: AgentCommandOpts): AgentRunContext
|
||||
opts.threadId !== "" &&
|
||||
opts.threadId !== null
|
||||
) {
|
||||
merged.currentThreadTs = String(opts.threadId);
|
||||
const threadId = stringifyRouteThreadId(opts.threadId);
|
||||
if (threadId) {
|
||||
merged.currentThreadTs = threadId;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate currentChannelId from the outbound target so channel threading
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ConversationRef } from "../infra/outbound/session-binding-service.js";
|
||||
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { isCronSessionKey } from "../sessions/session-key-utils.js";
|
||||
@@ -115,7 +116,7 @@ function resolveBoundConversationOrigin(params: {
|
||||
? conversationId
|
||||
: undefined) ??
|
||||
(params.requesterOrigin?.threadId != null && params.requesterOrigin.threadId !== ""
|
||||
? String(params.requesterOrigin.threadId)
|
||||
? stringifyRouteThreadId(params.requesterOrigin.threadId)
|
||||
: undefined);
|
||||
if (
|
||||
requesterTo &&
|
||||
@@ -295,7 +296,7 @@ export async function resolveSubagentCompletionOrigin(params: {
|
||||
const accountId = normalizeAccountId(requesterOrigin?.accountId);
|
||||
const threadId =
|
||||
requesterOrigin?.threadId != null && requesterOrigin.threadId !== ""
|
||||
? String(requesterOrigin.threadId).trim()
|
||||
? stringifyRouteThreadId(requesterOrigin.threadId)
|
||||
: undefined;
|
||||
const conversationId =
|
||||
threadId ||
|
||||
@@ -380,7 +381,9 @@ async function sendAnnounce(item: AnnounceQueueItem) {
|
||||
const requesterIsSubagent = isInternalAnnounceRequesterSession(item.sessionKey);
|
||||
const origin = item.origin;
|
||||
const threadId =
|
||||
origin?.threadId != null && origin.threadId !== "" ? String(origin.threadId) : undefined;
|
||||
origin?.threadId != null && origin.threadId !== ""
|
||||
? stringifyRouteThreadId(origin.threadId)
|
||||
: undefined;
|
||||
const deliveryTarget = !requesterIsSubagent
|
||||
? resolveExternalBestEffortDeliveryTarget({
|
||||
channel: origin?.channel,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveComparableTargetForLoadedChannel } from "../channels/plugins/target-parsing-loaded.js";
|
||||
import { resolveRouteTargetForLoadedChannel } from "../channels/plugins/target-parsing-loaded.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
deliveryContextFromSession,
|
||||
@@ -23,7 +23,7 @@ function normalizeAnnounceRouteTarget(context?: DeliveryContext): string | undef
|
||||
}
|
||||
const channel = normalizeOptionalString(context?.channel);
|
||||
const parsed = channel
|
||||
? resolveComparableTargetForLoadedChannel({
|
||||
? resolveRouteTargetForLoadedChannel({
|
||||
channel,
|
||||
rawTarget: rawTo,
|
||||
fallbackThreadId: context?.threadId,
|
||||
|
||||
@@ -468,7 +468,7 @@ describe("subagent announce seam flow", () => {
|
||||
expect.objectContaining({
|
||||
deliver: true,
|
||||
channel: "telegram",
|
||||
accountId: "bot:123",
|
||||
accountId: "bot-123",
|
||||
to: "-1001234567890",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { SubagentSpawnPreparation } from "../context-engine/types.js";
|
||||
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
|
||||
import { listRegisteredPluginAgentPromptGuidance } from "../plugins/command-registry-state.js";
|
||||
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
|
||||
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
@@ -1059,7 +1060,9 @@ export async function spawnSubagentDirect(
|
||||
to: childSessionOrigin?.to ?? undefined,
|
||||
accountId: childSessionOrigin?.accountId ?? undefined,
|
||||
threadId:
|
||||
childSessionOrigin?.threadId != null ? String(childSessionOrigin.threadId) : undefined,
|
||||
childSessionOrigin?.threadId != null
|
||||
? stringifyRouteThreadId(childSessionOrigin.threadId)
|
||||
: undefined,
|
||||
idempotencyKey: childIdem,
|
||||
deliver: deliverInitialChildRunDirectly,
|
||||
lane: AGENT_LANE_SUBAGENT,
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { GroupKeyResolution } from "../config/sessions.js";
|
||||
import { channelRouteDedupeKey } from "../plugin-sdk/channel-route.js";
|
||||
import { resetPluginRuntimeStateForTest } from "../plugins/runtime.js";
|
||||
import { createInboundDebouncer } from "./inbound-debounce.js";
|
||||
import { installGroupRequireMentionTestPlugins } from "./inbound.group-require-mention-test-plugins.js";
|
||||
@@ -209,7 +210,16 @@ describe("inbound dedupe", () => {
|
||||
OriginatingTo: "telegram:123",
|
||||
MessageSid: "42",
|
||||
};
|
||||
expect(buildInboundDedupeKey(ctx)).toBe("telegram|telegram:123|42");
|
||||
expect(buildInboundDedupeKey(ctx)).toBe(
|
||||
JSON.stringify([
|
||||
"",
|
||||
channelRouteDedupeKey({
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
}),
|
||||
"42",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips duplicates with the same key", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { channelRouteIdentityKey } from "../../channels/route/ref.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { resolveGlobalDedupeCache, type DedupeCache } from "../../infra/dedupe.js";
|
||||
import { channelRouteDedupeKey } from "../../plugin-sdk/channel-route.js";
|
||||
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
|
||||
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
|
||||
import {
|
||||
@@ -69,7 +69,7 @@ export function buildInboundDedupeKey(ctx: MsgContext): string | null {
|
||||
}
|
||||
const sessionScope = resolveInboundDedupeSessionScope(ctx);
|
||||
const accountId = normalizeOptionalString(ctx.AccountId) ?? "";
|
||||
const routeKey = channelRouteIdentityKey({
|
||||
const routeKey = channelRouteDedupeKey({
|
||||
channel: provider,
|
||||
to: peerId,
|
||||
accountId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { channelRouteKey, normalizeChannelRouteRef } from "../../../channels/route/ref.js";
|
||||
import { channelRouteCompactKey } from "../../../plugin-sdk/channel-route.js";
|
||||
import { defaultRuntime } from "../../../runtime.js";
|
||||
import { resolveGlobalMap } from "../../../shared/global-singleton.js";
|
||||
import {
|
||||
@@ -135,7 +135,7 @@ function resolveCrossChannelKey(item: FollowupRun): { cross?: true; key?: string
|
||||
if (!isRoutableChannel(channel) || !to) {
|
||||
return { cross: true };
|
||||
}
|
||||
const key = channelRouteKey(normalizeChannelRouteRef({ channel, to, accountId, threadId }));
|
||||
const key = channelRouteCompactKey({ channel, to, accountId, threadId });
|
||||
return key ? { key } : { cross: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { channelRouteIdentityKey } from "../../../channels/route/ref.js";
|
||||
import { resolveGlobalDedupeCache } from "../../../infra/dedupe.js";
|
||||
import { channelRouteDedupeKey } from "../../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../../shared/string-coerce.js";
|
||||
import { applyQueueDropPolicy, shouldSkipQueueItem } from "../../../utils/queue-helpers.js";
|
||||
import { kickFollowupDrainIfIdle, rememberFollowupDrainCallback } from "./drain.js";
|
||||
@@ -18,7 +18,7 @@ const RECENT_QUEUE_MESSAGE_IDS = resolveGlobalDedupeCache(RECENT_QUEUE_MESSAGE_I
|
||||
});
|
||||
|
||||
function followupRouteIdentityKey(run: FollowupRun): string {
|
||||
return channelRouteIdentityKey({
|
||||
return channelRouteDedupeKey({
|
||||
channel: run.originatingChannel,
|
||||
to: run.originatingTo,
|
||||
accountId: run.originatingAccountId,
|
||||
|
||||
@@ -2,8 +2,12 @@ import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
|
||||
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { normalizeAnyChannelId } from "../../channels/registry.js";
|
||||
import { stringifyRouteThreadId } from "../../channels/route/ref.js";
|
||||
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
|
||||
import {
|
||||
channelRouteTargetsMatchExact,
|
||||
stringifyRouteThreadId,
|
||||
type ChannelRouteTargetInput,
|
||||
} from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -102,6 +106,29 @@ function resolveTargetProviderForComparison(params: {
|
||||
return targetProvider;
|
||||
}
|
||||
|
||||
type SuppressionRouteTarget = ChannelRouteTargetInput & {
|
||||
channel: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
function normalizeRouteTargetForSuppression(params: {
|
||||
provider: string;
|
||||
rawTarget?: string;
|
||||
accountId?: string;
|
||||
threadId?: string;
|
||||
}): SuppressionRouteTarget | null {
|
||||
const to = normalizeTargetForProvider(params.provider, params.rawTarget);
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
channel: params.provider,
|
||||
to,
|
||||
...(params.accountId ? { accountId: params.accountId } : {}),
|
||||
...(params.threadId != null ? { threadId: params.threadId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function targetsMatchForSuppression(params: {
|
||||
provider: string;
|
||||
originTarget: string;
|
||||
@@ -148,21 +175,31 @@ export function shouldSuppressMessagingToolReplies(params: {
|
||||
return false;
|
||||
}
|
||||
const targetRaw = normalizeOptionalString(target.to);
|
||||
if (originRawTarget && targetRaw === originRawTarget && !target.threadId) {
|
||||
const routeAccount = originAccount ?? targetAccount;
|
||||
const originRoute = normalizeRouteTargetForSuppression({
|
||||
provider,
|
||||
rawTarget: originRawTarget,
|
||||
accountId: routeAccount,
|
||||
});
|
||||
if (!originRoute) {
|
||||
return false;
|
||||
}
|
||||
const targetRoute = normalizeRouteTargetForSuppression({
|
||||
provider: targetProvider,
|
||||
rawTarget: targetRaw,
|
||||
accountId: routeAccount,
|
||||
threadId: target.threadId,
|
||||
});
|
||||
if (!targetRoute) {
|
||||
return false;
|
||||
}
|
||||
if (channelRouteTargetsMatchExact({ left: originRoute, right: targetRoute })) {
|
||||
return true;
|
||||
}
|
||||
const originTarget = normalizeTargetForProvider(provider, originRawTarget);
|
||||
if (!originTarget) {
|
||||
return false;
|
||||
}
|
||||
const targetKey = normalizeTargetForProvider(targetProvider, targetRaw);
|
||||
if (!targetKey) {
|
||||
return false;
|
||||
}
|
||||
return targetsMatchForSuppression({
|
||||
provider,
|
||||
originTarget,
|
||||
targetKey,
|
||||
originTarget: originRoute.to,
|
||||
targetKey: targetRoute.to,
|
||||
targetThreadId: target.threadId,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,6 +159,30 @@ describe("shouldSuppressMessagingToolReplies", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("suppresses when only one side carries the account id", () => {
|
||||
expect(
|
||||
shouldSuppressMessagingToolReplies({
|
||||
messageProvider: "telegram",
|
||||
originatingTo: "123",
|
||||
accountId: "work",
|
||||
messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not suppress when route accounts differ", () => {
|
||||
expect(
|
||||
shouldSuppressMessagingToolReplies({
|
||||
messageProvider: "telegram",
|
||||
originatingTo: "123",
|
||||
accountId: "work",
|
||||
messagingToolSentTargets: [
|
||||
{ tool: "message", provider: "telegram", to: "123", accountId: "personal" },
|
||||
],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("suppresses telegram topic-origin replies when explicit threadId matches", () => {
|
||||
installTelegramSuppressionRegistry();
|
||||
expect(
|
||||
|
||||
@@ -162,6 +162,31 @@ describe("conversation resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes numeric command thread ids through the shared route contract", () => {
|
||||
registerChannelPlugin({
|
||||
...createChannelTestPluginBase({ id: "test-chat", label: "Test chat" }),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveCommandConversationResolution({
|
||||
cfg: testConfig,
|
||||
channel: "test-chat",
|
||||
accountId: "default",
|
||||
originatingTo: "test-chat:channel:parent-room",
|
||||
threadId: 42.9,
|
||||
}),
|
||||
).toEqual({
|
||||
canonical: {
|
||||
channel: "test-chat",
|
||||
accountId: "default",
|
||||
conversationId: "42",
|
||||
parentConversationId: "parent-room",
|
||||
},
|
||||
threadId: "42",
|
||||
source: "command-fallback",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the runtime inbound resolver and preserves provider canonical ids", () => {
|
||||
registerChannelPlugin({
|
||||
...createChannelTestPluginBase({ id: "discord", label: "Discord" }),
|
||||
@@ -270,6 +295,31 @@ describe("conversation resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes numeric inbound thread ids through the shared route contract", () => {
|
||||
registerChannelPlugin({
|
||||
...createChannelTestPluginBase({ id: "test-chat", label: "Test chat" }),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveInboundConversationResolution({
|
||||
cfg: testConfig,
|
||||
channel: "test-chat",
|
||||
accountId: "default",
|
||||
to: "test-chat:channel:parent-room",
|
||||
threadId: 42.9,
|
||||
}),
|
||||
).toEqual({
|
||||
canonical: {
|
||||
channel: "test-chat",
|
||||
accountId: "default",
|
||||
conversationId: "42",
|
||||
parentConversationId: "parent-room",
|
||||
},
|
||||
threadId: "42",
|
||||
source: "inbound-fallback",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves placement from runtime plugin metadata", () => {
|
||||
registerChannelPlugin({
|
||||
...createChannelTestPluginBase({ id: "telegram", label: "Telegram" }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js";
|
||||
import { normalizeConversationTargetRef } from "../infra/outbound/session-binding-normalization.js";
|
||||
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
|
||||
import { getActivePluginChannelRegistry } from "../plugins/runtime.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -249,9 +250,7 @@ export function resolveCommandConversationResolution(
|
||||
plugin,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
const threadId = normalizeOptionalString(
|
||||
params.threadId != null ? String(params.threadId) : undefined,
|
||||
);
|
||||
const threadId = stringifyRouteThreadId(params.threadId);
|
||||
|
||||
const commandParams: ChannelCommandConversationContext = {
|
||||
accountId,
|
||||
@@ -358,9 +357,7 @@ export function resolveInboundConversationResolution(
|
||||
plugin,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
const threadId = normalizeOptionalString(
|
||||
params.threadId != null ? String(params.threadId) : undefined,
|
||||
);
|
||||
const threadId = stringifyRouteThreadId(params.threadId);
|
||||
const resolverParams = {
|
||||
from: normalizeOptionalString(params.from),
|
||||
to: normalizeOptionalString(params.to),
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalThreadValue,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import type { ChatType } from "../chat-type.js";
|
||||
import {
|
||||
channelRoutesMatchExact,
|
||||
channelRoutesShareConversation,
|
||||
normalizeChannelRouteRef,
|
||||
} from "../route/ref.js";
|
||||
channelRouteTargetsMatchExact,
|
||||
channelRouteTargetsShareConversation,
|
||||
resolveChannelRouteTargetWithParser,
|
||||
type ChannelRouteExplicitTarget,
|
||||
type ChannelRouteParsedTarget,
|
||||
} from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { getLoadedChannelPluginForRead } from "./registry-loaded-read.js";
|
||||
|
||||
export type ParsedChannelExplicitTarget = {
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChatType;
|
||||
};
|
||||
export type { ChannelRouteParsedTarget } from "../../plugin-sdk/channel-route.js";
|
||||
|
||||
export type ComparableChannelTarget = {
|
||||
rawTo: string;
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChatType;
|
||||
};
|
||||
export type ParsedChannelExplicitTarget = ChannelRouteExplicitTarget;
|
||||
|
||||
/** @deprecated Use `ChannelRouteParsedTarget`. */
|
||||
export type ComparableChannelTarget = ChannelRouteParsedTarget;
|
||||
|
||||
export function parseExplicitTargetForLoadedChannel(
|
||||
channel: string,
|
||||
@@ -38,52 +30,38 @@ export function parseExplicitTargetForLoadedChannel(
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRouteTargetForLoadedChannel(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
}): ChannelRouteParsedTarget | null {
|
||||
return resolveChannelRouteTargetWithParser({
|
||||
...params,
|
||||
parseExplicitTarget: parseExplicitTargetForLoadedChannel,
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated Use `resolveRouteTargetForLoadedChannel`. */
|
||||
export function resolveComparableTargetForLoadedChannel(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
}): ComparableChannelTarget | null {
|
||||
const rawTo = normalizeOptionalString(params.rawTarget);
|
||||
if (!rawTo) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseExplicitTargetForLoadedChannel(params.channel, rawTo);
|
||||
const fallbackThreadId = normalizeOptionalThreadValue(params.fallbackThreadId);
|
||||
return {
|
||||
rawTo,
|
||||
to: parsed?.to ?? rawTo,
|
||||
threadId: normalizeOptionalThreadValue(parsed?.threadId ?? fallbackThreadId),
|
||||
chatType: parsed?.chatType,
|
||||
};
|
||||
}): ChannelRouteParsedTarget | null {
|
||||
return resolveRouteTargetForLoadedChannel(params);
|
||||
}
|
||||
|
||||
/** @deprecated Use `channelRouteTargetsMatchExact` from `openclaw/plugin-sdk/channel-route`. */
|
||||
export function comparableChannelTargetsMatch(params: {
|
||||
left?: ComparableChannelTarget | null;
|
||||
right?: ComparableChannelTarget | null;
|
||||
left?: ChannelRouteParsedTarget | null;
|
||||
right?: ChannelRouteParsedTarget | null;
|
||||
}): boolean {
|
||||
return channelRoutesMatchExact({
|
||||
left: targetToRoute(params.left),
|
||||
right: targetToRoute(params.right),
|
||||
});
|
||||
return channelRouteTargetsMatchExact(params);
|
||||
}
|
||||
|
||||
/** @deprecated Use `channelRouteTargetsShareConversation` from `openclaw/plugin-sdk/channel-route`. */
|
||||
export function comparableChannelTargetsShareRoute(params: {
|
||||
left?: ComparableChannelTarget | null;
|
||||
right?: ComparableChannelTarget | null;
|
||||
left?: ChannelRouteParsedTarget | null;
|
||||
right?: ChannelRouteParsedTarget | null;
|
||||
}): boolean {
|
||||
return channelRoutesShareConversation({
|
||||
left: targetToRoute(params.left),
|
||||
right: targetToRoute(params.right),
|
||||
});
|
||||
}
|
||||
|
||||
function targetToRoute(target?: ComparableChannelTarget | null) {
|
||||
return target
|
||||
? normalizeChannelRouteRef({
|
||||
to: target.to,
|
||||
rawTo: target.rawTo,
|
||||
threadId: target.threadId,
|
||||
chatType: target.chatType,
|
||||
})
|
||||
: undefined;
|
||||
return channelRouteTargetsShareConversation(params);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
channelRouteTargetsMatchExact,
|
||||
channelRouteTargetsShareConversation,
|
||||
} from "../../plugin-sdk/channel-route.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
comparableChannelTargetsMatch,
|
||||
comparableChannelTargetsShareRoute,
|
||||
parseExplicitTargetForChannel,
|
||||
parseExplicitTargetForLoadedChannel,
|
||||
resolveComparableTargetForChannel,
|
||||
resolveComparableTargetForLoadedChannel,
|
||||
resolveRouteTargetForChannel,
|
||||
resolveRouteTargetForLoadedChannel,
|
||||
} from "./target-parsing.js";
|
||||
|
||||
function parseThreadedTargetForTest(raw: string): {
|
||||
@@ -125,24 +129,26 @@ describe("parseExplicitTargetForChannel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("builds comparable targets from plugin-owned grammar", () => {
|
||||
it("builds route targets from plugin-owned grammar", () => {
|
||||
expect(
|
||||
resolveComparableTargetForChannel({
|
||||
resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "threaded:group:room-a:topic:77",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "mock-threaded",
|
||||
rawTo: "threaded:group:room-a:topic:77",
|
||||
to: "room-a",
|
||||
threadId: 77,
|
||||
chatType: "group",
|
||||
});
|
||||
expect(
|
||||
resolveComparableTargetForLoadedChannel({
|
||||
resolveRouteTargetForLoadedChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "threaded:group:room-a:topic:77",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "mock-threaded",
|
||||
rawTo: "threaded:group:room-a:topic:77",
|
||||
to: "room-a",
|
||||
threadId: 77,
|
||||
@@ -150,24 +156,24 @@ describe("parseExplicitTargetForChannel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("matches comparable targets when only the plugin grammar differs", () => {
|
||||
const topicTarget = resolveComparableTargetForChannel({
|
||||
it("matches route targets when only the plugin grammar differs", () => {
|
||||
const topicTarget = resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "threaded:room-a:topic:77",
|
||||
});
|
||||
const bareTarget = resolveComparableTargetForChannel({
|
||||
const bareTarget = resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "room-a",
|
||||
});
|
||||
|
||||
expect(
|
||||
comparableChannelTargetsMatch({
|
||||
channelRouteTargetsMatchExact({
|
||||
left: topicTarget,
|
||||
right: bareTarget,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
comparableChannelTargetsShareRoute({
|
||||
channelRouteTargetsShareConversation({
|
||||
left: topicTarget,
|
||||
right: bareTarget,
|
||||
}),
|
||||
@@ -175,11 +181,30 @@ describe("parseExplicitTargetForChannel", () => {
|
||||
});
|
||||
|
||||
it("compares numeric and string thread ids through the shared route contract", () => {
|
||||
const numericThread = resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "threaded:room-a:topic:77",
|
||||
});
|
||||
const stringThread = resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "room-a",
|
||||
fallbackThreadId: "77",
|
||||
});
|
||||
|
||||
expect(
|
||||
channelRouteTargetsMatchExact({
|
||||
left: numericThread,
|
||||
right: stringThread,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps deprecated comparable target helpers as route wrappers", () => {
|
||||
const numericThread = resolveComparableTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "threaded:room-a:topic:77",
|
||||
});
|
||||
const stringThread = resolveComparableTargetForChannel({
|
||||
const stringThread = resolveRouteTargetForChannel({
|
||||
channel: "mock-threaded",
|
||||
rawTarget: "room-a",
|
||||
fallbackThreadId: "77",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalThreadValue,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { resolveChannelRouteTargetWithParser } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeChatChannelId } from "../registry.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "./index.js";
|
||||
import type {
|
||||
ComparableChannelTarget,
|
||||
ChannelRouteParsedTarget,
|
||||
ParsedChannelExplicitTarget,
|
||||
} from "./target-parsing-loaded.js";
|
||||
export {
|
||||
@@ -13,9 +10,11 @@ export {
|
||||
comparableChannelTargetsShareRoute,
|
||||
parseExplicitTargetForLoadedChannel,
|
||||
resolveComparableTargetForLoadedChannel,
|
||||
resolveRouteTargetForLoadedChannel,
|
||||
} from "./target-parsing-loaded.js";
|
||||
export type {
|
||||
ComparableChannelTarget,
|
||||
ChannelRouteParsedTarget,
|
||||
ParsedChannelExplicitTarget,
|
||||
} from "./target-parsing-loaded.js";
|
||||
|
||||
@@ -38,21 +37,22 @@ export function parseExplicitTargetForChannel(
|
||||
return parseWithPlugin(getChannelPlugin, channel, rawTarget);
|
||||
}
|
||||
|
||||
export function resolveRouteTargetForChannel(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
}): ChannelRouteParsedTarget | null {
|
||||
return resolveChannelRouteTargetWithParser({
|
||||
...params,
|
||||
parseExplicitTarget: parseExplicitTargetForChannel,
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated Use `resolveRouteTargetForChannel`. */
|
||||
export function resolveComparableTargetForChannel(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
}): ComparableChannelTarget | null {
|
||||
const rawTo = normalizeOptionalString(params.rawTarget);
|
||||
if (!rawTo) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseExplicitTargetForChannel(params.channel, rawTo);
|
||||
const fallbackThreadId = normalizeOptionalThreadValue(params.fallbackThreadId);
|
||||
return {
|
||||
rawTo,
|
||||
to: parsed?.to ?? rawTo,
|
||||
threadId: normalizeOptionalThreadValue(parsed?.threadId ?? fallbackThreadId),
|
||||
chatType: parsed?.chatType,
|
||||
};
|
||||
}): ChannelRouteParsedTarget | null {
|
||||
return resolveRouteTargetForChannel(params);
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
channelRouteIdentityKey,
|
||||
channelRouteKey,
|
||||
channelRoutesMatchExact,
|
||||
channelRoutesShareConversation,
|
||||
normalizeChannelRouteRef,
|
||||
stringifyRouteThreadId,
|
||||
} from "./ref.js";
|
||||
|
||||
describe("channel route refs", () => {
|
||||
it("normalizes target, account, and thread fields", () => {
|
||||
expect(
|
||||
normalizeChannelRouteRef({
|
||||
channel: " Slack ",
|
||||
accountId: " Work ",
|
||||
rawTo: " channel:C1 ",
|
||||
to: " C1 ",
|
||||
threadId: " 171234.567 ",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "slack",
|
||||
accountId: "work",
|
||||
target: {
|
||||
rawTo: "channel:C1",
|
||||
to: "C1",
|
||||
},
|
||||
thread: {
|
||||
id: "171234.567",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes numeric thread ids for route keys", () => {
|
||||
const route = normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42.9,
|
||||
});
|
||||
|
||||
expect(stringifyRouteThreadId(route?.thread?.id)).toBe("42");
|
||||
expect(channelRouteKey(route)).toBe("telegram|-100123||42");
|
||||
});
|
||||
|
||||
it("builds a stable identity key from route-like input", () => {
|
||||
expect(
|
||||
channelRouteIdentityKey({
|
||||
channel: " Telegram ",
|
||||
to: " -100123 ",
|
||||
accountId: " Work ",
|
||||
threadId: 42.9,
|
||||
}),
|
||||
).toBe(
|
||||
channelRouteIdentityKey({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
accountId: "work",
|
||||
threadId: "42",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("matches exact routes when numeric and string thread ids are equivalent", () => {
|
||||
expect(
|
||||
channelRoutesMatchExact({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42,
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: "42",
|
||||
}),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("shares conversation when one side is the parent route", () => {
|
||||
expect(
|
||||
channelRoutesShareConversation({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
threadId: "171234.567",
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
}),
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not share different child threads", () => {
|
||||
expect(
|
||||
channelRoutesShareConversation({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "matrix",
|
||||
to: "room:!abc:example.org",
|
||||
threadId: "$root-1",
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "matrix",
|
||||
to: "room:!abc:example.org",
|
||||
threadId: "$root-2",
|
||||
}),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import type { OutboundDeliveryResult } from "../../infra/outbound/deliver.js";
|
||||
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -311,7 +312,7 @@ function buildDirectCronDeliveryIdempotencyKey(params: {
|
||||
const threadId =
|
||||
params.delivery.threadId == null || params.delivery.threadId === ""
|
||||
? ""
|
||||
: String(params.delivery.threadId);
|
||||
: (stringifyRouteThreadId(params.delivery.threadId) ?? "");
|
||||
const accountId = params.delivery.accountId?.trim() ?? "";
|
||||
const normalizedTo = normalizeDeliveryTarget(params.delivery.channel, params.delivery.to);
|
||||
return `cron-direct-delivery:v1:${executionId}:${params.delivery.channel}:${accountId}:${normalizedTo}:${threadId}`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { CliDeps } from "../../cli/outbound-send-deps.js";
|
||||
import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { resolveCronDeliveryPlan, type CronDeliveryPlan } from "../delivery-plan.js";
|
||||
import type {
|
||||
@@ -278,6 +279,7 @@ function resolveMessagingToolSentTargets(params: {
|
||||
if (!params.resolvedDelivery.ok) {
|
||||
return [];
|
||||
}
|
||||
const threadId = stringifyRouteThreadId(params.resolvedDelivery.threadId);
|
||||
return [
|
||||
{
|
||||
tool: "message",
|
||||
@@ -286,9 +288,7 @@ function resolveMessagingToolSentTargets(params: {
|
||||
? { accountId: params.resolvedDelivery.accountId }
|
||||
: {}),
|
||||
...(params.resolvedDelivery.to ? { to: params.resolvedDelivery.to } : {}),
|
||||
...(params.resolvedDelivery.threadId
|
||||
? { threadId: String(params.resolvedDelivery.threadId) }
|
||||
: {}),
|
||||
...(threadId ? { threadId } : {}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
|
||||
type RestartDeliveryContext = {
|
||||
@@ -29,10 +30,7 @@ function parseRestartDeliveryContext(params: unknown): {
|
||||
deliveryContext.channel || deliveryContext.to || deliveryContext.accountId
|
||||
? deliveryContext
|
||||
: undefined;
|
||||
const threadId =
|
||||
typeof context.threadId === "number" && Number.isFinite(context.threadId)
|
||||
? String(Math.trunc(context.threadId))
|
||||
: normalizeOptionalString(context.threadId);
|
||||
const threadId = stringifyRouteThreadId(context.threadId);
|
||||
return { deliveryContext: normalizedContext, threadId };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { EffectiveToolInventoryResult } from "../../agents/tools-effective-inventory.types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { logDebug, logWarn } from "../../logger.js";
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { ADMIN_SCOPE } from "../method-scopes.js";
|
||||
import {
|
||||
@@ -248,11 +249,11 @@ function resolveTrustedToolsEffectiveContext(params: {
|
||||
currentChannelId: delivery?.to,
|
||||
currentThreadTs:
|
||||
delivery?.threadId != null
|
||||
? String(delivery.threadId)
|
||||
? stringifyRouteThreadId(delivery.threadId)
|
||||
: loaded.entry.lastThreadId != null
|
||||
? String(loaded.entry.lastThreadId)
|
||||
? stringifyRouteThreadId(loaded.entry.lastThreadId)
|
||||
: loaded.entry.origin?.threadId != null
|
||||
? String(loaded.entry.origin.threadId)
|
||||
? stringifyRouteThreadId(loaded.entry.origin.threadId)
|
||||
: undefined,
|
||||
groupId: loaded.entry.groupId,
|
||||
groupChannel: loaded.entry.groupChannel,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "../infra/session-delivery-queue.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
|
||||
import { recordInboundSessionAndDispatchReply } from "../plugin-sdk/inbound-reply-dispatch.js";
|
||||
import type { OutboundReplyPayload } from "../plugin-sdk/reply-payload.js";
|
||||
import {
|
||||
@@ -462,7 +463,7 @@ async function loadRestartSentinelStartupTask(params: {
|
||||
const threadId =
|
||||
payload.threadId ??
|
||||
sessionThreadId ??
|
||||
(origin?.threadId != null ? String(origin.threadId) : undefined);
|
||||
(origin?.threadId != null ? stringifyRouteThreadId(origin.threadId) : undefined);
|
||||
let resolvedTo: string | undefined;
|
||||
let replyToId: string | undefined;
|
||||
let resolvedThreadId = threadId;
|
||||
@@ -488,7 +489,7 @@ async function loadRestartSentinelStartupTask(params: {
|
||||
resolvedThreadId =
|
||||
replyTransport && Object.hasOwn(replyTransport, "threadId")
|
||||
? replyTransport.threadId != null
|
||||
? String(replyTransport.threadId)
|
||||
? stringifyRouteThreadId(replyTransport.threadId)
|
||||
: undefined
|
||||
: threadId;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChannelApprovalNativeTarget } from "../channels/plugins/approval-native.types.js";
|
||||
import { channelRouteIdentityKey } from "../channels/route/ref.js";
|
||||
import { channelRouteDedupeKey } from "../plugin-sdk/channel-route.js";
|
||||
|
||||
export function buildChannelApprovalNativeTargetKey(target: ChannelApprovalNativeTarget): string {
|
||||
return channelRouteIdentityKey({
|
||||
return channelRouteDedupeKey({
|
||||
to: target.to,
|
||||
threadId: target.threadId,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getLoadedChannelPlugin,
|
||||
resolveChannelApprovalAdapter,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { channelRouteIdentityKey } from "../channels/route/ref.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import type {
|
||||
ExecApprovalForwardingConfig,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
buildPluginApprovalPendingReplyPayload,
|
||||
buildPluginApprovalResolvedReplyPayload,
|
||||
} from "../plugin-sdk/approval-renderers.js";
|
||||
import { channelRouteDedupeKey } from "../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
isDeliverableMessageChannel,
|
||||
@@ -170,7 +170,7 @@ function shouldForwardRoute(params: {
|
||||
|
||||
function buildTargetKey(target: ExecApprovalForwardTarget): string {
|
||||
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
|
||||
return channelRouteIdentityKey({
|
||||
return channelRouteDedupeKey({
|
||||
channel,
|
||||
to: target.to,
|
||||
accountId: target.accountId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
@@ -33,7 +34,7 @@ export function resolveExternalBestEffortDeliveryTarget(params: {
|
||||
accountId: deliver ? normalizeOptionalString(params.accountId) : undefined,
|
||||
threadId:
|
||||
deliver && params.threadId != null && params.threadId !== ""
|
||||
? String(params.threadId)
|
||||
? stringifyRouteThreadId(params.threadId)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringifyRouteThreadId } from "../../channels/route/ref.js";
|
||||
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
comparableChannelTargetsShareRoute,
|
||||
parseExplicitTargetForLoadedChannel,
|
||||
resolveComparableTargetForLoadedChannel,
|
||||
resolveRouteTargetForLoadedChannel,
|
||||
} from "../../channels/plugins/target-parsing-loaded.js";
|
||||
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.public.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { channelRouteTargetsShareConversation } from "../../plugin-sdk/channel-route.js";
|
||||
import { deliveryContextFromSession } from "../../utils/delivery-context.shared.js";
|
||||
import {
|
||||
isDeliverableMessageChannel,
|
||||
@@ -68,7 +68,7 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
const sessionLastChannel =
|
||||
context?.channel && isDeliverableMessageChannel(context.channel) ? context.channel : undefined;
|
||||
const parsedSessionTarget = sessionLastChannel
|
||||
? resolveComparableTargetForLoadedChannel({
|
||||
? resolveRouteTargetForLoadedChannel({
|
||||
channel: sessionLastChannel,
|
||||
rawTarget: context?.to,
|
||||
fallbackThreadId: context?.threadId,
|
||||
@@ -78,7 +78,7 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
const hasTurnSourceChannel = params.turnSourceChannel != null;
|
||||
const parsedTurnSourceTarget =
|
||||
hasTurnSourceChannel && params.turnSourceChannel
|
||||
? resolveComparableTargetForLoadedChannel({
|
||||
? resolveRouteTargetForLoadedChannel({
|
||||
channel: params.turnSourceChannel,
|
||||
rawTarget: params.turnSourceTo,
|
||||
fallbackThreadId: params.turnSourceThreadId,
|
||||
@@ -92,7 +92,7 @@ export function resolveSessionDeliveryTarget(params: {
|
||||
!params.turnSourceTo ||
|
||||
!context?.to ||
|
||||
(params.turnSourceChannel === sessionLastChannel &&
|
||||
comparableChannelTargetsShareRoute({
|
||||
channelRouteTargetsShareConversation({
|
||||
left: parsedTurnSourceTarget,
|
||||
right: parsedSessionTarget,
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// prefixed to the next prompt. We intentionally avoid persistence to keep
|
||||
// events ephemeral. Events are session-scoped and require an explicit key.
|
||||
|
||||
import { channelRouteIdentityKey } from "../channels/route/ref.js";
|
||||
import { channelRouteDedupeKey } from "../plugin-sdk/channel-route.js";
|
||||
import { resolveGlobalMap } from "../shared/global-singleton.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -136,7 +136,7 @@ function areDeliveryContextsEqual(left?: DeliveryContext, right?: DeliveryContex
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
return channelRouteIdentityKey(left) === channelRouteIdentityKey(right);
|
||||
return channelRouteDedupeKey(left) === channelRouteDedupeKey(right);
|
||||
}
|
||||
|
||||
function areSystemEventsEqual(left: SystemEvent, right: SystemEvent): boolean {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createChannelApproverDmTargetResolver,
|
||||
createChannelNativeOriginTargetResolver,
|
||||
type NativeApprovalTarget,
|
||||
nativeApprovalTargetsMatch,
|
||||
} from "./approval-native-helpers.js";
|
||||
import type { OpenClawConfig } from "./config-runtime.js";
|
||||
|
||||
describe("createChannelNativeOriginTargetResolver", () => {
|
||||
it("reuses shared turn-source routing and respects shouldHandle gating", () => {
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver({
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<NativeApprovalTarget>({
|
||||
channel: "matrix",
|
||||
shouldHandleRequest: ({ accountId }) => accountId === "ops",
|
||||
resolveTurnSourceTarget: (request) => ({
|
||||
@@ -18,7 +20,6 @@ describe("createChannelNativeOriginTargetResolver", () => {
|
||||
to: sessionTarget.to,
|
||||
threadId: sessionTarget.threadId,
|
||||
}),
|
||||
targetsMatch: (a, b) => a.to === b.to && a.threadId === b.threadId,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -64,6 +65,162 @@ describe("createChannelNativeOriginTargetResolver", () => {
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("uses shared route semantics for the default target matcher", () => {
|
||||
expect(
|
||||
nativeApprovalTargetsMatch({
|
||||
channel: "telegram",
|
||||
left: { to: "-100123", threadId: 42.9 },
|
||||
right: { to: "-100123", threadId: "42" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
nativeApprovalTargetsMatch({
|
||||
channel: "telegram",
|
||||
left: { to: "-100123", accountId: "work" },
|
||||
right: { to: "-100123" },
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<NativeApprovalTarget>({
|
||||
channel: "telegram",
|
||||
resolveTurnSourceTarget: () => ({ to: "-100123", threadId: 42.9 }),
|
||||
resolveSessionTarget: () => ({ to: "-100123", threadId: "42" }),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveOriginTarget({
|
||||
cfg: {} as OpenClawConfig,
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
turnSourceChannel: "telegram",
|
||||
turnSourceTo: "-100123",
|
||||
turnSourceThreadId: 42.9,
|
||||
turnSourceAccountId: "default",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1000,
|
||||
},
|
||||
}),
|
||||
).toEqual({ to: "-100123", threadId: 42.9 });
|
||||
});
|
||||
|
||||
it("normalizes resolved targets before matching origin candidates", () => {
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<NativeApprovalTarget>({
|
||||
channel: "slack",
|
||||
resolveTurnSourceTarget: () => ({ to: "CHANNEL:C1", threadId: "171234.567890" }),
|
||||
resolveSessionTarget: () => ({ to: "channel:c1", threadId: "171234.567890" }),
|
||||
normalizeTarget: (target) => ({
|
||||
...target,
|
||||
to: target.to.toLowerCase(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveOriginTarget({
|
||||
cfg: {} as OpenClawConfig,
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
turnSourceChannel: "slack",
|
||||
turnSourceTo: "CHANNEL:C1",
|
||||
turnSourceThreadId: "171234.567890",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1000,
|
||||
},
|
||||
}),
|
||||
).toEqual({ to: "channel:c1", threadId: "171234.567890" });
|
||||
});
|
||||
|
||||
it("normalizes custom target shapes before invoking a custom matcher", () => {
|
||||
type ProviderTarget = { id: string; shard?: string };
|
||||
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<ProviderTarget>({
|
||||
channel: "custom",
|
||||
resolveTurnSourceTarget: () => ({ id: "ROOM-1", shard: "a" }),
|
||||
resolveSessionTarget: () => ({ id: "room-1", shard: "b" }),
|
||||
normalizeTarget: (target) => ({ ...target, id: target.id.toLowerCase() }),
|
||||
targetsMatch: (left, right) => left.id === right.id,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveOriginTarget({
|
||||
cfg: {} as OpenClawConfig,
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
sessionKey: "agent:main:custom:room-1",
|
||||
turnSourceChannel: "custom",
|
||||
turnSourceTo: "ROOM-1",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1000,
|
||||
},
|
||||
}),
|
||||
).toEqual({ id: "room-1", shard: "a" });
|
||||
});
|
||||
|
||||
it("normalizes only match inputs when delivery targets must stay provider-native", () => {
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<NativeApprovalTarget>({
|
||||
channel: "slack",
|
||||
resolveTurnSourceTarget: () => ({ to: "channel:C1", threadId: "171234.567890" }),
|
||||
resolveSessionTarget: () => ({ to: "channel:c1", threadId: "171234.567890" }),
|
||||
normalizeTargetForMatch: (target) => ({
|
||||
...target,
|
||||
to: target.to.toLowerCase(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveOriginTarget({
|
||||
cfg: {} as OpenClawConfig,
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
turnSourceChannel: "slack",
|
||||
turnSourceTo: "channel:C1",
|
||||
turnSourceThreadId: "171234.567890",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1000,
|
||||
},
|
||||
}),
|
||||
).toEqual({ to: "channel:C1", threadId: "171234.567890" });
|
||||
});
|
||||
|
||||
it("keeps custom target matchers generic", () => {
|
||||
type ProviderTarget = { id: string; shard?: string };
|
||||
|
||||
const resolveOriginTarget = createChannelNativeOriginTargetResolver<ProviderTarget>({
|
||||
channel: "custom",
|
||||
resolveTurnSourceTarget: () => ({ id: "room-1", shard: "a" }),
|
||||
resolveSessionTarget: () => ({ id: "room-1", shard: "b" }),
|
||||
targetsMatch: (left, right) => left.id === right.id,
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveOriginTarget({
|
||||
cfg: {} as OpenClawConfig,
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
sessionKey: "agent:main:custom:room-1",
|
||||
turnSourceChannel: "custom",
|
||||
turnSourceTo: "room-1",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1000,
|
||||
},
|
||||
}),
|
||||
).toEqual({ id: "room-1", shard: "a" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChannelApproverDmTargetResolver", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ExecApprovalSessionTarget } from "../infra/exec-approval-session-t
|
||||
import { resolveApprovalRequestOriginTarget } from "../infra/exec-approval-session-target.js";
|
||||
import type { ExecApprovalRequest } from "../infra/exec-approvals.js";
|
||||
import type { PluginApprovalRequest } from "../infra/plugin-approvals.js";
|
||||
import { channelRouteTargetsMatchExact } from "./channel-route.js";
|
||||
import type { OpenClawConfig } from "./config-runtime.js";
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
@@ -14,12 +15,12 @@ type ApprovalResolverParams = {
|
||||
request: ApprovalRequest;
|
||||
};
|
||||
|
||||
type NativeApprovalTarget = {
|
||||
to: string;
|
||||
threadId?: string | number | null;
|
||||
};
|
||||
type NativeApprovalTargetNormalizer<TTarget> = (
|
||||
target: TTarget,
|
||||
request: ApprovalRequest,
|
||||
) => TTarget | null | undefined;
|
||||
|
||||
export function createChannelNativeOriginTargetResolver<TTarget>(params: {
|
||||
type NativeOriginResolverParams<TTarget extends NativeApprovalTarget> = {
|
||||
channel: string;
|
||||
shouldHandleRequest?: (params: ApprovalResolverParams) => boolean;
|
||||
resolveTurnSourceTarget: (request: ApprovalRequest) => TTarget | null;
|
||||
@@ -27,27 +28,130 @@ export function createChannelNativeOriginTargetResolver<TTarget>(params: {
|
||||
sessionTarget: ExecApprovalSessionTarget,
|
||||
request: ApprovalRequest,
|
||||
) => TTarget | null;
|
||||
normalizeTarget?: NativeApprovalTargetNormalizer<TTarget>;
|
||||
normalizeTargetForMatch?: NativeApprovalTargetNormalizer<TTarget>;
|
||||
targetsMatch?: (a: TTarget, b: TTarget) => boolean;
|
||||
resolveFallbackTarget?: (request: ApprovalRequest) => TTarget | null;
|
||||
};
|
||||
|
||||
type CustomOriginResolverParams<TTarget> = {
|
||||
channel: string;
|
||||
shouldHandleRequest?: (params: ApprovalResolverParams) => boolean;
|
||||
resolveTurnSourceTarget: (request: ApprovalRequest) => TTarget | null;
|
||||
resolveSessionTarget: (
|
||||
sessionTarget: ExecApprovalSessionTarget,
|
||||
request: ApprovalRequest,
|
||||
) => TTarget | null;
|
||||
normalizeTarget?: NativeApprovalTargetNormalizer<TTarget>;
|
||||
normalizeTargetForMatch?: NativeApprovalTargetNormalizer<TTarget>;
|
||||
targetsMatch: (a: TTarget, b: TTarget) => boolean;
|
||||
resolveFallbackTarget?: (request: ApprovalRequest) => TTarget | null;
|
||||
}) {
|
||||
};
|
||||
|
||||
export type NativeApprovalTarget = {
|
||||
to: string;
|
||||
accountId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
};
|
||||
|
||||
export function nativeApprovalTargetsMatch(params: {
|
||||
channel?: string | null;
|
||||
left: NativeApprovalTarget;
|
||||
right: NativeApprovalTarget;
|
||||
}): boolean {
|
||||
return channelRouteTargetsMatchExact({
|
||||
left: {
|
||||
channel: params.channel,
|
||||
to: params.left.to,
|
||||
accountId: params.left.accountId,
|
||||
threadId: params.left.threadId,
|
||||
},
|
||||
right: {
|
||||
channel: params.channel,
|
||||
to: params.right.to,
|
||||
accountId: params.right.accountId,
|
||||
threadId: params.right.threadId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isNativeApprovalTarget(value: unknown): value is NativeApprovalTarget {
|
||||
return Boolean(
|
||||
value && typeof value === "object" && typeof (value as { to?: unknown }).to === "string",
|
||||
);
|
||||
}
|
||||
|
||||
function nativeApprovalTargetMatcher(channel: string): (left: unknown, right: unknown) => boolean {
|
||||
return (left, right) =>
|
||||
isNativeApprovalTarget(left) &&
|
||||
isNativeApprovalTarget(right) &&
|
||||
nativeApprovalTargetsMatch({ channel, left, right });
|
||||
}
|
||||
|
||||
function createOriginTargetResolver<TTarget>(
|
||||
params: CustomOriginResolverParams<TTarget>,
|
||||
): (input: ApprovalResolverParams) => TTarget | null {
|
||||
return (input: ApprovalResolverParams): TTarget | null => {
|
||||
if (params.shouldHandleRequest && !params.shouldHandleRequest(input)) {
|
||||
return null;
|
||||
}
|
||||
const normalizeTarget = (target: TTarget | null): TTarget | null => {
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
return params.normalizeTarget
|
||||
? (params.normalizeTarget(target, input.request) ?? null)
|
||||
: target;
|
||||
};
|
||||
const normalizeTargetForMatch = (target: TTarget): TTarget | null =>
|
||||
params.normalizeTargetForMatch?.(target, input.request) ?? target;
|
||||
return resolveApprovalRequestOriginTarget({
|
||||
cfg: input.cfg,
|
||||
request: input.request,
|
||||
channel: params.channel,
|
||||
accountId: input.accountId,
|
||||
resolveTurnSourceTarget: params.resolveTurnSourceTarget,
|
||||
resolveTurnSourceTarget: (request) =>
|
||||
normalizeTarget(params.resolveTurnSourceTarget(request)),
|
||||
resolveSessionTarget: (sessionTarget) =>
|
||||
params.resolveSessionTarget(sessionTarget, input.request),
|
||||
targetsMatch: params.targetsMatch,
|
||||
resolveFallbackTarget: params.resolveFallbackTarget,
|
||||
normalizeTarget(params.resolveSessionTarget(sessionTarget, input.request)),
|
||||
targetsMatch: (left, right) => {
|
||||
const normalizedLeft = normalizeTargetForMatch(left);
|
||||
const normalizedRight = normalizeTargetForMatch(right);
|
||||
return Boolean(
|
||||
normalizedLeft && normalizedRight && params.targetsMatch(normalizedLeft, normalizedRight),
|
||||
);
|
||||
},
|
||||
resolveFallbackTarget: params.resolveFallbackTarget
|
||||
? (request) => normalizeTarget(params.resolveFallbackTarget?.(request) ?? null)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function hasCustomTargetsMatch<TTarget>(
|
||||
params: NativeOriginResolverParams<NativeApprovalTarget> | CustomOriginResolverParams<TTarget>,
|
||||
): params is CustomOriginResolverParams<TTarget> {
|
||||
return typeof params.targetsMatch === "function";
|
||||
}
|
||||
|
||||
export function createChannelNativeOriginTargetResolver<TTarget extends NativeApprovalTarget>(
|
||||
params: NativeOriginResolverParams<TTarget>,
|
||||
): (input: ApprovalResolverParams) => TTarget | null;
|
||||
export function createChannelNativeOriginTargetResolver<TTarget>(
|
||||
params: CustomOriginResolverParams<TTarget>,
|
||||
): (input: ApprovalResolverParams) => TTarget | null;
|
||||
export function createChannelNativeOriginTargetResolver<TTarget>(
|
||||
params: NativeOriginResolverParams<NativeApprovalTarget> | CustomOriginResolverParams<TTarget>,
|
||||
): (input: ApprovalResolverParams) => NativeApprovalTarget | TTarget | null {
|
||||
if (hasCustomTargetsMatch(params)) {
|
||||
return createOriginTargetResolver(params);
|
||||
}
|
||||
return createOriginTargetResolver({
|
||||
...params,
|
||||
targetsMatch: nativeApprovalTargetMatcher(params.channel),
|
||||
});
|
||||
}
|
||||
|
||||
export function createChannelApproverDmTargetResolver<
|
||||
TApprover,
|
||||
TTarget extends NativeApprovalTarget = NativeApprovalTarget,
|
||||
|
||||
204
src/plugin-sdk/channel-route.test.ts
Normal file
204
src/plugin-sdk/channel-route.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
channelRouteCompactKey,
|
||||
channelRouteDedupeKey,
|
||||
channelRouteIdentityKey,
|
||||
channelRouteKey,
|
||||
channelRouteTargetsMatchExact,
|
||||
channelRouteTargetsShareConversation,
|
||||
channelRoutesMatchExact,
|
||||
channelRoutesShareConversation,
|
||||
normalizeChannelRouteRef,
|
||||
resolveChannelRouteTargetWithParser,
|
||||
stringifyRouteThreadId,
|
||||
} from "./channel-route.js";
|
||||
|
||||
describe("plugin-sdk channel-route", () => {
|
||||
it("normalizes target, account, and thread fields", () => {
|
||||
expect(
|
||||
normalizeChannelRouteRef({
|
||||
channel: " Slack ",
|
||||
accountId: " Work ",
|
||||
rawTo: " channel:C1 ",
|
||||
to: " C1 ",
|
||||
threadId: " 171234.567 ",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "slack",
|
||||
accountId: "work",
|
||||
target: {
|
||||
rawTo: "channel:C1",
|
||||
to: "C1",
|
||||
},
|
||||
thread: {
|
||||
id: "171234.567",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes numeric thread ids for route keys", () => {
|
||||
const route = normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42.9,
|
||||
});
|
||||
|
||||
expect(stringifyRouteThreadId(route?.thread?.id)).toBe("42");
|
||||
expect(channelRouteCompactKey(route)).toBe("telegram|-100123||42");
|
||||
expect(channelRouteKey(route)).toBe(channelRouteCompactKey(route));
|
||||
});
|
||||
|
||||
it("builds compact route keys from raw route-like input", () => {
|
||||
expect(
|
||||
channelRouteCompactKey({
|
||||
channel: " Slack ",
|
||||
to: " C1 ",
|
||||
accountId: " Work ",
|
||||
threadId: " 171234.567 ",
|
||||
}),
|
||||
).toBe("slack|C1|work|171234.567");
|
||||
});
|
||||
|
||||
it("builds a stable dedupe key from route-like input", () => {
|
||||
expect(
|
||||
channelRouteDedupeKey({
|
||||
channel: " Telegram ",
|
||||
to: " -100123 ",
|
||||
accountId: " Work ",
|
||||
threadId: 42.9,
|
||||
}),
|
||||
).toBe(
|
||||
channelRouteDedupeKey({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
accountId: "work",
|
||||
threadId: "42",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps deprecated identity key alias wired to the dedupe key", () => {
|
||||
const input = {
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
accountId: "work",
|
||||
threadId: "42",
|
||||
};
|
||||
expect(channelRouteIdentityKey(input)).toBe(channelRouteDedupeKey(input));
|
||||
});
|
||||
|
||||
it("matches exact routes when numeric and string thread ids are equivalent", () => {
|
||||
expect(
|
||||
channelRoutesMatchExact({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42,
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: "42",
|
||||
}),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
channelRouteTargetsMatchExact({
|
||||
left: {
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: 42,
|
||||
},
|
||||
right: {
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
threadId: "42",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires account equality for exact route matches", () => {
|
||||
expect(
|
||||
channelRouteTargetsMatchExact({
|
||||
left: {
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
accountId: "work",
|
||||
},
|
||||
right: {
|
||||
channel: "telegram",
|
||||
to: "-100123",
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("shares conversation when one side is the parent route", () => {
|
||||
expect(
|
||||
channelRoutesShareConversation({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
threadId: "171234.567",
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
}),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
channelRouteTargetsShareConversation({
|
||||
left: {
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
threadId: "171234.567",
|
||||
},
|
||||
right: {
|
||||
channel: "slack",
|
||||
to: "channel:C1",
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not share different child threads", () => {
|
||||
expect(
|
||||
channelRoutesShareConversation({
|
||||
left: normalizeChannelRouteRef({
|
||||
channel: "matrix",
|
||||
to: "room:!abc:example.org",
|
||||
threadId: "$root-1",
|
||||
}),
|
||||
right: normalizeChannelRouteRef({
|
||||
channel: "matrix",
|
||||
to: "room:!abc:example.org",
|
||||
threadId: "$root-2",
|
||||
}),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves parsed route targets through an injected channel grammar", () => {
|
||||
expect(
|
||||
resolveChannelRouteTargetWithParser({
|
||||
channel: "Mock",
|
||||
rawTarget: " room-a:topic:77 ",
|
||||
fallbackThreadId: 11,
|
||||
parseExplicitTarget: (_channel, rawTarget) => {
|
||||
const match = /^(.*):topic:(\d+)$/u.exec(rawTarget);
|
||||
return match
|
||||
? { to: match[1] ?? rawTarget, threadId: Number.parseInt(match[2] ?? "", 10) }
|
||||
: null;
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "mock",
|
||||
rawTo: "room-a:topic:77",
|
||||
to: "room-a",
|
||||
threadId: 77,
|
||||
chatType: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
import { normalizeOptionalAccountId } from "../../routing/account-id.js";
|
||||
import { normalizeOptionalAccountId } from "../routing/account-id.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
normalizeOptionalThreadValue,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import type { ChatType } from "../chat-type.js";
|
||||
} from "../shared/string-coerce.js";
|
||||
|
||||
export type ChannelRouteChatType = "direct" | "group" | "channel";
|
||||
|
||||
export type ChannelRouteThreadKind = "topic" | "thread" | "reply";
|
||||
|
||||
@@ -16,7 +17,7 @@ export type ChannelRouteRef = {
|
||||
target?: {
|
||||
to: string;
|
||||
rawTo?: string;
|
||||
chatType?: ChatType;
|
||||
chatType?: ChannelRouteChatType;
|
||||
};
|
||||
thread?: {
|
||||
id: string | number;
|
||||
@@ -30,7 +31,7 @@ export type ChannelRouteRefInput = {
|
||||
accountId?: unknown;
|
||||
to?: unknown;
|
||||
rawTo?: unknown;
|
||||
chatType?: ChatType;
|
||||
chatType?: ChannelRouteChatType;
|
||||
threadId?: unknown;
|
||||
threadKind?: ChannelRouteThreadKind;
|
||||
threadSource?: ChannelRouteThreadSource;
|
||||
@@ -41,6 +42,19 @@ export type ChannelRouteTargetInput = Pick<
|
||||
"channel" | "accountId" | "to" | "rawTo" | "chatType" | "threadId"
|
||||
>;
|
||||
|
||||
export type ChannelRouteKeyInput = ChannelRouteRef | ChannelRouteTargetInput;
|
||||
|
||||
export type ChannelRouteExplicitTarget = {
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChannelRouteChatType;
|
||||
};
|
||||
|
||||
export type ChannelRouteExplicitTargetParser = (
|
||||
channel: string,
|
||||
rawTarget: string,
|
||||
) => ChannelRouteExplicitTarget | null;
|
||||
|
||||
export function normalizeRouteThreadId(value: unknown): string | number | undefined {
|
||||
return normalizeOptionalThreadValue(value);
|
||||
}
|
||||
@@ -103,7 +117,37 @@ export function normalizeChannelRouteTarget(
|
||||
return input ? normalizeChannelRouteRef(input) : undefined;
|
||||
}
|
||||
|
||||
export function channelRouteIdentityKey(input?: ChannelRouteTargetInput | null): string {
|
||||
export type ChannelRouteParsedTarget = ChannelRouteTargetInput & {
|
||||
channel: string;
|
||||
rawTo: string;
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChannelRouteChatType;
|
||||
};
|
||||
|
||||
export function resolveChannelRouteTargetWithParser(params: {
|
||||
channel: string;
|
||||
rawTarget?: string | null;
|
||||
fallbackThreadId?: string | number | null;
|
||||
parseExplicitTarget: ChannelRouteExplicitTargetParser;
|
||||
}): ChannelRouteParsedTarget | null {
|
||||
const channel = normalizeLowercaseStringOrEmpty(params.channel);
|
||||
const rawTo = normalizeOptionalString(params.rawTarget);
|
||||
if (!channel || !rawTo) {
|
||||
return null;
|
||||
}
|
||||
const parsed = params.parseExplicitTarget(channel, rawTo);
|
||||
const fallbackThreadId = normalizeOptionalThreadValue(params.fallbackThreadId);
|
||||
return {
|
||||
channel,
|
||||
rawTo,
|
||||
to: parsed?.to ?? rawTo,
|
||||
threadId: normalizeOptionalThreadValue(parsed?.threadId ?? fallbackThreadId),
|
||||
chatType: parsed?.chatType,
|
||||
};
|
||||
}
|
||||
|
||||
export function channelRouteDedupeKey(input?: ChannelRouteTargetInput | null): string {
|
||||
const route = normalizeChannelRouteTarget(input);
|
||||
return JSON.stringify([
|
||||
route?.channel ?? "",
|
||||
@@ -113,6 +157,11 @@ export function channelRouteIdentityKey(input?: ChannelRouteTargetInput | null):
|
||||
]);
|
||||
}
|
||||
|
||||
/** @deprecated Use `channelRouteDedupeKey`. */
|
||||
export function channelRouteIdentityKey(input?: ChannelRouteTargetInput | null): string {
|
||||
return channelRouteDedupeKey(input);
|
||||
}
|
||||
|
||||
function threadIdsEqual(left?: string | number, right?: string | number): boolean {
|
||||
const normalizedLeft = stringifyRouteThreadId(left);
|
||||
const normalizedRight = stringifyRouteThreadId(right);
|
||||
@@ -123,6 +172,10 @@ function accountsCompatible(left?: string, right?: string): boolean {
|
||||
return !left || !right || left === right;
|
||||
}
|
||||
|
||||
function accountsEqual(left?: string, right?: string): boolean {
|
||||
return (left ?? "") === (right ?? "");
|
||||
}
|
||||
|
||||
export function channelRoutesMatchExact(params: {
|
||||
left?: ChannelRouteRef | null;
|
||||
right?: ChannelRouteRef | null;
|
||||
@@ -133,9 +186,9 @@ export function channelRoutesMatchExact(params: {
|
||||
}
|
||||
return (
|
||||
left.channel === right.channel &&
|
||||
left.accountId === right.accountId &&
|
||||
channelRouteTarget(left) === channelRouteTarget(right) &&
|
||||
threadIdsEqual(channelRouteThreadId(left), channelRouteThreadId(right))
|
||||
left.target?.to === right.target?.to &&
|
||||
accountsEqual(left.accountId, right.accountId) &&
|
||||
threadIdsEqual(left.thread?.id, right.thread?.id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,30 +200,61 @@ export function channelRoutesShareConversation(params: {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
if (left.channel && right.channel && left.channel !== right.channel) {
|
||||
if (
|
||||
left.channel !== right.channel ||
|
||||
left.target?.to !== right.target?.to ||
|
||||
!accountsCompatible(left.accountId, right.accountId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!accountsCompatible(left.accountId, right.accountId)) {
|
||||
return false;
|
||||
}
|
||||
if (channelRouteTarget(left) !== channelRouteTarget(right)) {
|
||||
return false;
|
||||
}
|
||||
const leftThreadId = channelRouteThreadId(left);
|
||||
const rightThreadId = channelRouteThreadId(right);
|
||||
if (leftThreadId == null || rightThreadId == null) {
|
||||
if (left.thread?.id == null || right.thread?.id == null) {
|
||||
return true;
|
||||
}
|
||||
return threadIdsEqual(leftThreadId, rightThreadId);
|
||||
return threadIdsEqual(left.thread.id, right.thread.id);
|
||||
}
|
||||
|
||||
export function channelRouteKey(route?: ChannelRouteRef): string | undefined {
|
||||
const normalized = normalizeChannelRouteRef({
|
||||
channel: route?.channel,
|
||||
accountId: route?.accountId,
|
||||
to: route?.target?.to,
|
||||
threadId: route?.thread?.id,
|
||||
export function channelRouteTargetsMatchExact(params: {
|
||||
left?: ChannelRouteTargetInput | null;
|
||||
right?: ChannelRouteTargetInput | null;
|
||||
}): boolean {
|
||||
return channelRoutesMatchExact({
|
||||
left: normalizeChannelRouteTarget(params.left),
|
||||
right: normalizeChannelRouteTarget(params.right),
|
||||
});
|
||||
}
|
||||
|
||||
export function channelRouteTargetsShareConversation(params: {
|
||||
left?: ChannelRouteTargetInput | null;
|
||||
right?: ChannelRouteTargetInput | null;
|
||||
}): boolean {
|
||||
return channelRoutesShareConversation({
|
||||
left: normalizeChannelRouteTarget(params.left),
|
||||
right: normalizeChannelRouteTarget(params.right),
|
||||
});
|
||||
}
|
||||
|
||||
function isChannelRouteRef(route: ChannelRouteKeyInput): route is ChannelRouteRef {
|
||||
return "target" in route || "thread" in route;
|
||||
}
|
||||
|
||||
function normalizeChannelRouteKeyInput(
|
||||
route?: ChannelRouteKeyInput | null,
|
||||
): ChannelRouteRef | undefined {
|
||||
if (!route) {
|
||||
return undefined;
|
||||
}
|
||||
return isChannelRouteRef(route)
|
||||
? normalizeChannelRouteRef({
|
||||
channel: route.channel,
|
||||
to: route.target?.to,
|
||||
accountId: route.accountId,
|
||||
threadId: route.thread?.id,
|
||||
})
|
||||
: normalizeChannelRouteTarget(route);
|
||||
}
|
||||
|
||||
export function channelRouteCompactKey(route?: ChannelRouteKeyInput | null): string | undefined {
|
||||
const normalized = normalizeChannelRouteKeyInput(route);
|
||||
if (!normalized?.channel || !normalized.target?.to) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -181,3 +265,8 @@ export function channelRouteKey(route?: ChannelRouteRef): string | undefined {
|
||||
stringifyRouteThreadId(normalized.thread?.id) ?? "",
|
||||
].join("|");
|
||||
}
|
||||
|
||||
/** @deprecated Use `channelRouteCompactKey`. */
|
||||
export function channelRouteKey(route?: ChannelRouteRef): string | undefined {
|
||||
return channelRouteCompactKey(route);
|
||||
}
|
||||
@@ -120,6 +120,16 @@ const knownDeprecatedSurfaceMarkers = [
|
||||
file: "src/commands/doctor/shared/legacy-x-search-migrate.ts",
|
||||
marker: "tools.web.x_search",
|
||||
},
|
||||
{
|
||||
code: "channel-route-key-aliases",
|
||||
file: "src/plugin-sdk/channel-route.ts",
|
||||
marker: "channelRouteIdentityKey",
|
||||
},
|
||||
{
|
||||
code: "channel-target-comparable-aliases",
|
||||
file: "src/channels/plugins/target-parsing-loaded.ts",
|
||||
marker: "ComparableChannelTarget",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function parseDate(date: string): Date {
|
||||
|
||||
@@ -51,6 +51,49 @@ export const PLUGIN_COMPAT_RECORDS = [
|
||||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "channel-route-key-aliases",
|
||||
status: "deprecated",
|
||||
owner: "sdk",
|
||||
introduced: "2026-04-28",
|
||||
deprecated: "2026-04-28",
|
||||
warningStarts: "2026-04-28",
|
||||
removeAfter: "2026-07-28",
|
||||
replacement: "`channelRouteDedupeKey` and `channelRouteCompactKey`",
|
||||
docsPath: "/plugins/sdk-migration",
|
||||
surfaces: [
|
||||
"openclaw/plugin-sdk/channel-route channelRouteIdentityKey",
|
||||
"openclaw/plugin-sdk/channel-route channelRouteKey",
|
||||
],
|
||||
diagnostics: ["plugin SDK compatibility warning"],
|
||||
tests: [
|
||||
"src/plugin-sdk/channel-route.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "channel-target-comparable-aliases",
|
||||
status: "deprecated",
|
||||
owner: "sdk",
|
||||
introduced: "2026-04-28",
|
||||
deprecated: "2026-04-28",
|
||||
warningStarts: "2026-04-28",
|
||||
removeAfter: "2026-07-28",
|
||||
replacement:
|
||||
"`resolveRouteTargetForChannel`, `ChannelRouteParsedTarget`, `channelRouteTargetsMatchExact`, and `channelRouteTargetsShareConversation`",
|
||||
docsPath: "/plugins/sdk-migration",
|
||||
surfaces: [
|
||||
"src/channels/plugins/target-parsing ComparableChannelTarget",
|
||||
"src/channels/plugins/target-parsing resolveComparableTargetForChannel",
|
||||
"src/channels/plugins/target-parsing comparableChannelTargetsMatch",
|
||||
"src/channels/plugins/target-parsing comparableChannelTargetsShareRoute",
|
||||
],
|
||||
diagnostics: ["plugin SDK compatibility warning"],
|
||||
tests: [
|
||||
"src/channels/plugins/target-parsing.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "bundled-plugin-allowlist",
|
||||
status: "active",
|
||||
|
||||
@@ -668,6 +668,46 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps deprecated comparable channel target helpers behind compatibility shims", () => {
|
||||
const matches = findRepoFilesContaining({
|
||||
roots: [
|
||||
resolve(REPO_ROOT, "src"),
|
||||
resolve(REPO_ROOT, "extensions"),
|
||||
resolve(REPO_ROOT, "test"),
|
||||
],
|
||||
pattern:
|
||||
/\b(?:ComparableChannelTarget|resolveComparableTargetFor(?:Channel|LoadedChannel)|comparableChannelTargets(?:Match|ShareRoute))\b/u,
|
||||
exclude: [
|
||||
"src/channels/plugins/target-parsing.ts",
|
||||
"src/channels/plugins/target-parsing-loaded.ts",
|
||||
"src/channels/plugins/target-parsing.test.ts",
|
||||
"src/plugins/compat/registry.ts",
|
||||
"src/plugins/compat/registry.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||||
],
|
||||
});
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps deprecated channel route key aliases behind compatibility shims", () => {
|
||||
const matches = findRepoFilesContaining({
|
||||
roots: [
|
||||
resolve(REPO_ROOT, "src"),
|
||||
resolve(REPO_ROOT, "extensions"),
|
||||
resolve(REPO_ROOT, "test"),
|
||||
],
|
||||
pattern: /\b(?:channelRouteIdentityKey|channelRouteKey)\b/u,
|
||||
exclude: [
|
||||
"src/plugin-sdk/channel-route.ts",
|
||||
"src/plugin-sdk/channel-route.test.ts",
|
||||
"src/plugins/compat/registry.ts",
|
||||
"src/plugins/compat/registry.test.ts",
|
||||
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
|
||||
],
|
||||
});
|
||||
expect(matches).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps removed channel-named runtime boundaries out of core imports", () => {
|
||||
const matches = findRepoFilesContaining({
|
||||
roots: [resolve(REPO_ROOT, "src")],
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { OutputRuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import type { OutputRuntimeEnv, RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import { vi } from "vitest";
|
||||
|
||||
type RuntimeEnvOptions = {
|
||||
throwOnExit?: boolean;
|
||||
};
|
||||
|
||||
type TypedRuntimeEnvOptions<TRuntime> = RuntimeEnvOptions & {
|
||||
readonly __runtimeShape?: (runtime: TRuntime) => void;
|
||||
};
|
||||
|
||||
export function createRuntimeEnv(options?: RuntimeEnvOptions): OutputRuntimeEnv {
|
||||
const throwOnExit = options?.throwOnExit ?? true;
|
||||
return {
|
||||
@@ -24,8 +20,9 @@ export function createRuntimeEnv(options?: RuntimeEnvOptions): OutputRuntimeEnv
|
||||
};
|
||||
}
|
||||
|
||||
export function createTypedRuntimeEnv<TRuntime>(
|
||||
options?: TypedRuntimeEnvOptions<TRuntime>,
|
||||
export function createTypedRuntimeEnv<TRuntime extends RuntimeEnv = OutputRuntimeEnv>(
|
||||
options?: RuntimeEnvOptions,
|
||||
_runtimeShape?: (runtime: TRuntime) => void,
|
||||
): TRuntime {
|
||||
return createRuntimeEnv(options) as unknown as TRuntime;
|
||||
}
|
||||
@@ -34,8 +31,8 @@ export function createNonExitingRuntimeEnv(): OutputRuntimeEnv {
|
||||
return createRuntimeEnv({ throwOnExit: false });
|
||||
}
|
||||
|
||||
export function createNonExitingTypedRuntimeEnv<TRuntime>(
|
||||
export function createNonExitingTypedRuntimeEnv<TRuntime extends RuntimeEnv = OutputRuntimeEnv>(
|
||||
runtimeShape?: (runtime: TRuntime) => void,
|
||||
): TRuntime {
|
||||
return createTypedRuntimeEnv<TRuntime>({ throwOnExit: false, __runtimeShape: runtimeShape });
|
||||
return createTypedRuntimeEnv<TRuntime>({ throwOnExit: false }, runtimeShape);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
channelRouteKey,
|
||||
channelRouteCompactKey,
|
||||
channelRouteThreadId,
|
||||
channelRouteTarget,
|
||||
normalizeChannelRouteRef,
|
||||
} from "../channels/route/ref.js";
|
||||
normalizeChannelRouteTarget,
|
||||
} from "../plugin-sdk/channel-route.js";
|
||||
import { normalizeAccountId } from "./account-id.js";
|
||||
import type { DeliveryContext, DeliveryContextSessionSource } from "./delivery-context.types.js";
|
||||
import { normalizeMessageChannel } from "./message-channel-core.js";
|
||||
@@ -13,7 +13,7 @@ export function normalizeDeliveryContext(context?: DeliveryContext): DeliveryCon
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
const route = normalizeChannelRouteRef({
|
||||
const route = normalizeChannelRouteTarget({
|
||||
channel:
|
||||
typeof context.channel === "string"
|
||||
? (normalizeMessageChannel(context.channel) ?? context.channel.trim())
|
||||
@@ -131,13 +131,5 @@ export function mergeDeliveryContext(
|
||||
}
|
||||
|
||||
export function deliveryContextKey(context?: DeliveryContext): string | undefined {
|
||||
const normalized = normalizeDeliveryContext(context);
|
||||
return channelRouteKey(
|
||||
normalizeChannelRouteRef({
|
||||
channel: normalized?.channel,
|
||||
to: normalized?.to,
|
||||
accountId: normalized?.accountId,
|
||||
threadId: normalized?.threadId,
|
||||
}),
|
||||
);
|
||||
return channelRouteCompactKey(normalizeDeliveryContext(context));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type DeliveryContext = {
|
||||
import type { ChannelRouteTargetInput } from "../plugin-sdk/channel-route.js";
|
||||
|
||||
export type DeliveryContext = Pick<
|
||||
ChannelRouteTargetInput,
|
||||
"accountId" | "channel" | "threadId" | "to"
|
||||
> & {
|
||||
channel?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
|
||||
Reference in New Issue
Block a user