refactor: centralize target normalization
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
- **BREAKING:** `clawdbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
- **BREAKING:** `clawdbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||||
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
- **BREAKING:** remove legacy provider-specific target resolution fallbacks; target resolution is centralized with plugin hints + directory lookups.
|
||||||
|
- **BREAKING:** drop legacy target normalization helpers; use outbound target normalization and resolver flows.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||||
|
|||||||
@@ -31,12 +31,3 @@ export function isMessagingToolSendAction(
|
|||||||
if (!plugin?.actions?.extractToolSend) return false;
|
if (!plugin?.actions?.extractToolSend) return false;
|
||||||
return Boolean(plugin.actions.extractToolSend({ args })?.to);
|
return Boolean(plugin.actions.extractToolSend({ args })?.to);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeTargetForProvider(provider: string, raw?: string): string | undefined {
|
|
||||||
if (!raw) return undefined;
|
|
||||||
const providerId = normalizeChannelId(provider);
|
|
||||||
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
|
|
||||||
const normalized =
|
|
||||||
plugin?.messaging?.normalizeTarget?.(raw) ?? (raw.trim().toLowerCase() || undefined);
|
|
||||||
return normalized || undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import { truncateUtf16Safe } from "../utils.js";
|
import { truncateUtf16Safe } from "../utils.js";
|
||||||
import { type MessagingToolSend, normalizeTargetForProvider } from "./pi-embedded-messaging.js";
|
import { type MessagingToolSend } from "./pi-embedded-messaging.js";
|
||||||
|
import { normalizeTargetForProvider } from "../infra/outbound/target-normalization.js";
|
||||||
|
|
||||||
const TOOL_RESULT_MAX_CHARS = 8000;
|
const TOOL_RESULT_MAX_CHARS = 8000;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
|
import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js";
|
||||||
import { normalizeTargetForProvider } from "../../agents/pi-embedded-messaging.js";
|
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
|
||||||
import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
|
import type { MessagingToolSend } from "../../agents/pi-embedded-runner.js";
|
||||||
import type { ReplyToMode } from "../../config/types.js";
|
import type { ReplyToMode } from "../../config/types.js";
|
||||||
import type { OriginatingChannelType } from "../templating.js";
|
import type { OriginatingChannelType } from "../templating.js";
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ export const CHANNEL_TARGET_DESCRIPTION =
|
|||||||
export const CHANNEL_TARGETS_DESCRIPTION =
|
export const CHANNEL_TARGETS_DESCRIPTION =
|
||||||
"Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.";
|
"Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.";
|
||||||
|
|
||||||
export function normalizeChannelTargetInput(raw: string): string {
|
|
||||||
return raw.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyTargetToParams(params: {
|
export function applyTargetToParams(params: {
|
||||||
action: string;
|
action: string;
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ export type DirectoryCacheKey = {
|
|||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
kind: ChannelDirectoryEntryKind;
|
kind: ChannelDirectoryEntryKind;
|
||||||
source: "cache" | "live";
|
source: "cache" | "live";
|
||||||
|
signature?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
|
export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
|
||||||
return `${key.channel}:${key.accountId ?? "default"}:${key.kind}:${key.source}`;
|
const signature = key.signature ?? "default";
|
||||||
|
return `${key.channel}:${key.accountId ?? "default"}:${key.kind}:${key.source}:${signature}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DirectoryCache<T> {
|
export class DirectoryCache<T> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeTargetForProvider } from "../../agents/pi-embedded-messaging.js";
|
import { normalizeTargetForProvider } from "./target-normalization.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelId,
|
ChannelId,
|
||||||
ChannelMessageActionName,
|
ChannelMessageActionName,
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
export function missingTargetMessage(provider: string, hint?: string): string {
|
export function missingTargetMessage(provider: string, hint?: string): string {
|
||||||
const suffix = hint ? ` ${hint}` : "";
|
return `Delivering to ${provider} requires target${formatHint(hint)}`;
|
||||||
return `Delivering to ${provider} requires target${suffix}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function missingTargetError(provider: string, hint?: string): Error {
|
export function missingTargetError(provider: string, hint?: string): Error {
|
||||||
return new Error(missingTargetMessage(provider, hint));
|
return new Error(missingTargetMessage(provider, hint));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ambiguousTargetMessage(provider: string, raw: string, hint?: string): string {
|
||||||
|
return `Ambiguous target "${raw}" for ${provider}. Provide a unique name or an explicit id.${formatHint(hint, true)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ambiguousTargetError(provider: string, raw: string, hint?: string): Error {
|
||||||
|
return new Error(ambiguousTargetMessage(provider, raw, hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unknownTargetMessage(provider: string, raw: string, hint?: string): string {
|
||||||
|
return `Unknown target "${raw}" for ${provider}.${formatHint(hint, true)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unknownTargetError(provider: string, raw: string, hint?: string): Error {
|
||||||
|
return new Error(unknownTargetMessage(provider, raw, hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHint(hint?: string, withLabel = false): string {
|
||||||
|
if (!hint) return "";
|
||||||
|
return withLabel ? ` Hint: ${hint}` : ` ${hint}`;
|
||||||
|
}
|
||||||
|
|||||||
31
src/infra/outbound/target-normalization.ts
Normal file
31
src/infra/outbound/target-normalization.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
|
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||||
|
|
||||||
|
export function normalizeChannelTargetInput(raw: string): string {
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTargetForProvider(provider: string, raw?: string): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const providerId = normalizeChannelId(provider);
|
||||||
|
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
|
||||||
|
const normalized =
|
||||||
|
plugin?.messaging?.normalizeTarget?.(raw) ?? (raw.trim().toLowerCase() || undefined);
|
||||||
|
return normalized || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTargetResolverSignature(channel: ChannelId): string {
|
||||||
|
const plugin = getChannelPlugin(channel);
|
||||||
|
const hint = plugin?.messaging?.targetHint ?? "";
|
||||||
|
const looksLike = plugin?.messaging?.looksLikeTargetId;
|
||||||
|
const source = looksLike ? looksLike.toString() : "";
|
||||||
|
return hashSignature(`${hint}|${source}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashSignature(value: string): string {
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < value.length; i += 1) {
|
||||||
|
hash = ((hash << 5) + hash) ^ value.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return (hash >>> 0).toString(36);
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { normalizeTargetForProvider } from "../../agents/pi-embedded-messaging.js";
|
|
||||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelDirectoryEntry,
|
ChannelDirectoryEntry,
|
||||||
@@ -7,8 +6,13 @@ import type {
|
|||||||
} from "../../channels/plugins/types.js";
|
} from "../../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
import { normalizeChannelTargetInput } from "./channel-target.js";
|
|
||||||
import { buildDirectoryCacheKey, DirectoryCache } from "./directory-cache.js";
|
import { buildDirectoryCacheKey, DirectoryCache } from "./directory-cache.js";
|
||||||
|
import {
|
||||||
|
buildTargetResolverSignature,
|
||||||
|
normalizeChannelTargetInput,
|
||||||
|
normalizeTargetForProvider,
|
||||||
|
} from "./target-normalization.js";
|
||||||
|
import { ambiguousTargetError, unknownTargetError } from "./target-errors.js";
|
||||||
|
|
||||||
export type TargetResolveKind = ChannelDirectoryEntryKind | "channel";
|
export type TargetResolveKind = ChannelDirectoryEntryKind | "channel";
|
||||||
|
|
||||||
@@ -223,11 +227,13 @@ async function getDirectoryEntries(params: {
|
|||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
preferLiveOnMiss?: boolean;
|
preferLiveOnMiss?: boolean;
|
||||||
}): Promise<ChannelDirectoryEntry[]> {
|
}): Promise<ChannelDirectoryEntry[]> {
|
||||||
|
const signature = buildTargetResolverSignature(params.channel);
|
||||||
const cacheKey = buildDirectoryCacheKey({
|
const cacheKey = buildDirectoryCacheKey({
|
||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
kind: params.kind,
|
kind: params.kind,
|
||||||
source: "cache",
|
source: "cache",
|
||||||
|
signature,
|
||||||
});
|
});
|
||||||
const cached = directoryCache.get(cacheKey, params.cfg);
|
const cached = directoryCache.get(cacheKey, params.cfg);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
@@ -249,6 +255,7 @@ async function getDirectoryEntries(params: {
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
kind: params.kind,
|
kind: params.kind,
|
||||||
source: "live",
|
source: "live",
|
||||||
|
signature,
|
||||||
});
|
});
|
||||||
const liveEntries = await listDirectoryEntries({
|
const liveEntries = await listDirectoryEntries({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
@@ -276,6 +283,9 @@ export async function resolveMessagingTarget(params: {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return { ok: false, error: new Error("Target is required") };
|
return { ok: false, error: new Error("Target is required") };
|
||||||
}
|
}
|
||||||
|
const plugin = getChannelPlugin(params.channel);
|
||||||
|
const providerLabel = plugin?.meta?.label ?? params.channel;
|
||||||
|
const hint = plugin?.messaging?.targetHint;
|
||||||
const kind = detectTargetKind(raw, params.preferredKind);
|
const kind = detectTargetKind(raw, params.preferredKind);
|
||||||
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
||||||
if (looksLikeTargetId({ channel: params.channel, raw, normalized })) {
|
if (looksLikeTargetId({ channel: params.channel, raw, normalized })) {
|
||||||
@@ -316,13 +326,13 @@ export async function resolveMessagingTarget(params: {
|
|||||||
if (match.kind === "ambiguous") {
|
if (match.kind === "ambiguous") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: new Error(`Ambiguous target "${raw}". Provide a unique name or an explicit id.`),
|
error: ambiguousTargetError(providerLabel, raw, hint),
|
||||||
candidates: match.entries,
|
candidates: match.entries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: new Error(`Unknown target "${raw}" for ${params.channel}.`),
|
error: unknownTargetError(providerLabel, raw, hint),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user