mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:24:31 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -52,7 +52,9 @@ export function resolveAgentDeliveryPlan(params: {
|
||||
});
|
||||
|
||||
const resolvedChannel = (() => {
|
||||
if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
||||
if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) {
|
||||
return INTERNAL_MESSAGE_CHANNEL;
|
||||
}
|
||||
if (requestedChannel === "last") {
|
||||
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||
return baseDelivery.channel;
|
||||
@@ -60,7 +62,9 @@ export function resolveAgentDeliveryPlan(params: {
|
||||
return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
||||
}
|
||||
|
||||
if (isGatewayMessageChannel(requestedChannel)) return requestedChannel;
|
||||
if (isGatewayMessageChannel(requestedChannel)) {
|
||||
return requestedChannel;
|
||||
}
|
||||
|
||||
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||
return baseDelivery.channel;
|
||||
|
||||
@@ -19,6 +19,8 @@ const DISCORD_ADAPTER: ChannelMessageAdapter = {
|
||||
};
|
||||
|
||||
export function getChannelMessageAdapter(channel: ChannelId): ChannelMessageAdapter {
|
||||
if (channel === "discord") return DISCORD_ADAPTER;
|
||||
if (channel === "discord") {
|
||||
return DISCORD_ADAPTER;
|
||||
}
|
||||
return DEFAULT_ADAPTER;
|
||||
}
|
||||
|
||||
@@ -16,24 +16,34 @@ function isKnownChannel(value: string): boolean {
|
||||
}
|
||||
|
||||
function isAccountEnabled(account: unknown): boolean {
|
||||
if (!account || typeof account !== "object") return true;
|
||||
if (!account || typeof account !== "object") {
|
||||
return true;
|
||||
}
|
||||
const enabled = (account as { enabled?: boolean }).enabled;
|
||||
return enabled !== false;
|
||||
}
|
||||
|
||||
async function isPluginConfigured(plugin: ChannelPlugin, cfg: OpenClawConfig): Promise<boolean> {
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
if (accountIds.length === 0) return false;
|
||||
if (accountIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const accountId of accountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
const enabled = plugin.config.isEnabled
|
||||
? plugin.config.isEnabled(account, cfg)
|
||||
: isAccountEnabled(account);
|
||||
if (!enabled) continue;
|
||||
if (!plugin.config.isConfigured) return true;
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
if (!plugin.config.isConfigured) {
|
||||
return true;
|
||||
}
|
||||
const configured = await plugin.config.isConfigured(account, cfg);
|
||||
if (configured) return true;
|
||||
if (configured) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -44,7 +54,9 @@ export async function listConfiguredMessageChannels(
|
||||
): Promise<MessageChannelId[]> {
|
||||
const channels: MessageChannelId[] = [];
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (!isKnownChannel(plugin.id)) continue;
|
||||
if (!isKnownChannel(plugin.id)) {
|
||||
continue;
|
||||
}
|
||||
if (await isPluginConfigured(plugin, cfg)) {
|
||||
channels.push(plugin.id);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ export function applyTargetToParams(params: {
|
||||
throw new Error("Use `target` for actions that accept a destination.");
|
||||
}
|
||||
|
||||
if (!target) return;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (mode === "channelId") {
|
||||
params.args.channelId = target;
|
||||
return;
|
||||
|
||||
@@ -124,7 +124,9 @@ function createPluginHandler(params: {
|
||||
gifPlayback?: boolean;
|
||||
}): ChannelHandler | null {
|
||||
const outbound = params.outbound;
|
||||
if (!outbound?.sendText || !outbound?.sendMedia) return null;
|
||||
if (!outbound?.sendText || !outbound?.sendMedia) {
|
||||
return null;
|
||||
}
|
||||
const sendText = outbound.sendText;
|
||||
const sendMedia = outbound.sendMedia;
|
||||
const chunker = outbound.chunker ?? null;
|
||||
@@ -244,10 +246,14 @@ export async function deliverOutboundPayloads(params: {
|
||||
? chunkMarkdownTextWithMode(text, textLimit, "newline")
|
||||
: chunkByParagraph(text, textLimit);
|
||||
|
||||
if (!blockChunks.length && text) blockChunks.push(text);
|
||||
if (!blockChunks.length && text) {
|
||||
blockChunks.push(text);
|
||||
}
|
||||
for (const blockChunk of blockChunks) {
|
||||
const chunks = handler.chunker(blockChunk, textLimit);
|
||||
if (!chunks.length && blockChunk) chunks.push(blockChunk);
|
||||
if (!chunks.length && blockChunk) {
|
||||
chunks.push(blockChunk);
|
||||
}
|
||||
for (const chunk of chunks) {
|
||||
throwIfAborted(abortSignal);
|
||||
results.push(await handler.sendText(chunk));
|
||||
@@ -346,7 +352,9 @@ export async function deliverOutboundPayloads(params: {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!params.bestEffort) throw err;
|
||||
if (!params.bestEffort) {
|
||||
throw err;
|
||||
}
|
||||
params.onError?.(err, payloadSummary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ export class DirectoryCache<T> {
|
||||
get(key: string, cfg: OpenClawConfig): T | undefined {
|
||||
this.resetIfConfigChanged(cfg);
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
if (Date.now() - entry.fetchedAt > this.ttlMs) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
@@ -43,13 +45,17 @@ export class DirectoryCache<T> {
|
||||
|
||||
clearMatching(match: (key: string) => boolean): void {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (match(key)) this.cache.delete(key);
|
||||
if (match(key)) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(cfg?: OpenClawConfig): void {
|
||||
this.cache.clear();
|
||||
if (cfg) this.lastConfigRef = cfg;
|
||||
if (cfg) {
|
||||
this.lastConfigRef = cfg;
|
||||
}
|
||||
}
|
||||
|
||||
private resetIfConfigChanged(cfg: OpenClawConfig): void {
|
||||
|
||||
@@ -31,9 +31,13 @@ type OutboundDeliveryMeta = {
|
||||
|
||||
const resolveChannelLabel = (channel: string) => {
|
||||
const pluginLabel = getChannelPlugin(channel as ChannelId)?.meta.label;
|
||||
if (pluginLabel) return pluginLabel;
|
||||
if (pluginLabel) {
|
||||
return pluginLabel;
|
||||
}
|
||||
const normalized = normalizeChatChannelId(channel);
|
||||
if (normalized) return getChatChannelMeta(normalized).label;
|
||||
if (normalized) {
|
||||
return getChatChannelMeta(normalized).label;
|
||||
}
|
||||
return channel;
|
||||
};
|
||||
|
||||
@@ -48,10 +52,18 @@ export function formatOutboundDeliverySummary(
|
||||
const label = resolveChannelLabel(result.channel);
|
||||
const base = `✅ Sent via ${label}. Message ID: ${result.messageId}`;
|
||||
|
||||
if ("chatId" in result) return `${base} (chat ${result.chatId})`;
|
||||
if ("channelId" in result) return `${base} (channel ${result.channelId})`;
|
||||
if ("roomId" in result) return `${base} (room ${result.roomId})`;
|
||||
if ("conversationId" in result) return `${base} (conversation ${result.conversationId})`;
|
||||
if ("chatId" in result) {
|
||||
return `${base} (chat ${result.chatId})`;
|
||||
}
|
||||
if ("channelId" in result) {
|
||||
return `${base} (channel ${result.channelId})`;
|
||||
}
|
||||
if ("roomId" in result) {
|
||||
return `${base} (room ${result.roomId})`;
|
||||
}
|
||||
if ("conversationId" in result) {
|
||||
return `${base} (conversation ${result.conversationId})`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,9 @@ export function getToolResult(
|
||||
}
|
||||
|
||||
function extractToolPayload(result: AgentToolResult<unknown>): unknown {
|
||||
if (result.details !== undefined) return result.details;
|
||||
if (result.details !== undefined) {
|
||||
return result.details;
|
||||
}
|
||||
const textBlock = Array.isArray(result.content)
|
||||
? result.content.find(
|
||||
(block) =>
|
||||
@@ -188,7 +190,9 @@ async function maybeApplyCrossContextMarker(params: {
|
||||
toolContext: params.toolContext,
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
if (!decoration) return params.message;
|
||||
if (!decoration) {
|
||||
return params.message;
|
||||
}
|
||||
return applyCrossContextMessageDecoration({
|
||||
params: params.args,
|
||||
message: params.message,
|
||||
@@ -199,11 +203,17 @@ async function maybeApplyCrossContextMarker(params: {
|
||||
|
||||
function readBooleanParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
||||
const raw = params[key];
|
||||
if (typeof raw === "boolean") return raw;
|
||||
if (typeof raw === "boolean") {
|
||||
return raw;
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (trimmed === "true") return true;
|
||||
if (trimmed === "false") return false;
|
||||
if (trimmed === "true") {
|
||||
return true;
|
||||
}
|
||||
if (trimmed === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -213,13 +223,23 @@ function resolveSlackAutoThreadId(params: {
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
}): string | undefined {
|
||||
const context = params.toolContext;
|
||||
if (!context?.currentThreadTs || !context.currentChannelId) return undefined;
|
||||
if (!context?.currentThreadTs || !context.currentChannelId) {
|
||||
return undefined;
|
||||
}
|
||||
// Only mirror auto-threading when Slack would reply in the active thread for this channel.
|
||||
if (context.replyToMode !== "all" && context.replyToMode !== "first") return undefined;
|
||||
if (context.replyToMode !== "all" && context.replyToMode !== "first") {
|
||||
return undefined;
|
||||
}
|
||||
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") return undefined;
|
||||
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) return undefined;
|
||||
if (context.replyToMode === "first" && context.hasRepliedRef?.value) return undefined;
|
||||
if (!parsedTarget || parsedTarget.kind !== "channel") {
|
||||
return undefined;
|
||||
}
|
||||
if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase()) {
|
||||
return undefined;
|
||||
}
|
||||
if (context.replyToMode === "first" && context.hasRepliedRef?.value) {
|
||||
return undefined;
|
||||
}
|
||||
return context.currentThreadTs;
|
||||
}
|
||||
|
||||
@@ -266,14 +286,20 @@ function inferAttachmentFilename(params: {
|
||||
if (mediaHint.startsWith("file://")) {
|
||||
const filePath = fileURLToPath(mediaHint);
|
||||
const base = path.basename(filePath);
|
||||
if (base) return base;
|
||||
if (base) {
|
||||
return base;
|
||||
}
|
||||
} else if (/^https?:\/\//i.test(mediaHint)) {
|
||||
const url = new URL(mediaHint);
|
||||
const base = path.basename(url.pathname);
|
||||
if (base) return base;
|
||||
if (base) {
|
||||
return base;
|
||||
}
|
||||
} else {
|
||||
const base = path.basename(mediaHint);
|
||||
if (base) return base;
|
||||
if (base) {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through to content-type based default
|
||||
@@ -287,9 +313,13 @@ function normalizeBase64Payload(params: { base64?: string; contentType?: string
|
||||
base64?: string;
|
||||
contentType?: string;
|
||||
} {
|
||||
if (!params.base64) return { base64: params.base64, contentType: params.contentType };
|
||||
if (!params.base64) {
|
||||
return { base64: params.base64, contentType: params.contentType };
|
||||
}
|
||||
const match = /^data:([^;]+);base64,(.*)$/i.exec(params.base64.trim());
|
||||
if (!match) return { base64: params.base64, contentType: params.contentType };
|
||||
if (!match) {
|
||||
return { base64: params.base64, contentType: params.contentType };
|
||||
}
|
||||
const [, mime, payload] = match;
|
||||
return {
|
||||
base64: payload,
|
||||
@@ -305,7 +335,9 @@ async function hydrateSetGroupIconParams(params: {
|
||||
action: ChannelMessageActionName;
|
||||
dryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
if (params.action !== "setGroupIcon") return;
|
||||
if (params.action !== "setGroupIcon") {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaHint = readStringParam(params.args, "media", { trim: false });
|
||||
const fileHint =
|
||||
@@ -362,7 +394,9 @@ async function hydrateSendAttachmentParams(params: {
|
||||
action: ChannelMessageActionName;
|
||||
dryRun?: boolean;
|
||||
}): Promise<void> {
|
||||
if (params.action !== "sendAttachment") return;
|
||||
if (params.action !== "sendAttachment") {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaHint = readStringParam(params.args, "media", { trim: false });
|
||||
const fileHint =
|
||||
@@ -372,7 +406,9 @@ async function hydrateSendAttachmentParams(params: {
|
||||
readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
|
||||
const caption = readStringParam(params.args, "caption", { allowEmpty: true })?.trim();
|
||||
const message = readStringParam(params.args, "message", { allowEmpty: true })?.trim();
|
||||
if (!caption && message) params.args.caption = message;
|
||||
if (!caption && message) {
|
||||
params.args.caption = message;
|
||||
}
|
||||
|
||||
const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
|
||||
const normalized = normalizeBase64Payload({
|
||||
@@ -416,7 +452,9 @@ async function hydrateSendAttachmentParams(params: {
|
||||
|
||||
function parseButtonsParam(params: Record<string, unknown>): void {
|
||||
const raw = params.buttons;
|
||||
if (typeof raw !== "string") return;
|
||||
if (typeof raw !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
delete params.buttons;
|
||||
@@ -431,7 +469,9 @@ function parseButtonsParam(params: Record<string, unknown>): void {
|
||||
|
||||
function parseCardParam(params: Record<string, unknown>): void {
|
||||
const raw = params.card;
|
||||
if (typeof raw !== "string") return;
|
||||
if (typeof raw !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
delete params.card;
|
||||
@@ -511,7 +551,9 @@ type ResolvedActionContext = {
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
|
||||
if (!input.gateway) return undefined;
|
||||
if (!input.gateway) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
url: input.gateway.url,
|
||||
token: input.gateway.token,
|
||||
@@ -562,7 +604,9 @@ async function handleBroadcastAction(
|
||||
channel: targetChannel,
|
||||
input: target,
|
||||
});
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
if (!resolved.ok) {
|
||||
throw resolved.error;
|
||||
}
|
||||
const sendResult = await runMessageAction({
|
||||
...input,
|
||||
action: "send",
|
||||
@@ -579,7 +623,9 @@ async function handleBroadcastAction(
|
||||
result: sendResult.kind === "send" ? sendResult.sendResult : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) throw err;
|
||||
if (isAbortError(err)) {
|
||||
throw err;
|
||||
}
|
||||
results.push({
|
||||
channel: targetChannel,
|
||||
to: target,
|
||||
@@ -640,17 +686,25 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const seenMedia = new Set<string>();
|
||||
const pushMedia = (value?: string | null) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return;
|
||||
if (seenMedia.has(trimmed)) return;
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
if (seenMedia.has(trimmed)) {
|
||||
return;
|
||||
}
|
||||
seenMedia.add(trimmed);
|
||||
mergedMediaUrls.push(trimmed);
|
||||
};
|
||||
pushMedia(mediaHint);
|
||||
for (const url of parsed.mediaUrls ?? []) pushMedia(url);
|
||||
for (const url of parsed.mediaUrls ?? []) {
|
||||
pushMedia(url);
|
||||
}
|
||||
pushMedia(parsed.mediaUrl);
|
||||
message = parsed.text;
|
||||
params.message = message;
|
||||
if (!params.replyTo && parsed.replyToId) params.replyTo = parsed.replyToId;
|
||||
if (!params.replyTo && parsed.replyToId) {
|
||||
params.replyTo = parsed.replyToId;
|
||||
}
|
||||
if (!params.media) {
|
||||
// Use path/filePath if media not set, then fall back to parsed directives
|
||||
params.media = mergedMediaUrls[0] || undefined;
|
||||
|
||||
@@ -75,15 +75,25 @@ export function actionHasTarget(
|
||||
params: Record<string, unknown>,
|
||||
): boolean {
|
||||
const to = typeof params.to === "string" ? params.to.trim() : "";
|
||||
if (to) return true;
|
||||
if (to) {
|
||||
return true;
|
||||
}
|
||||
const channelId = typeof params.channelId === "string" ? params.channelId.trim() : "";
|
||||
if (channelId) return true;
|
||||
if (channelId) {
|
||||
return true;
|
||||
}
|
||||
const aliases = ACTION_TARGET_ALIASES[action];
|
||||
if (!aliases) return false;
|
||||
if (!aliases) {
|
||||
return false;
|
||||
}
|
||||
return aliases.some((alias) => {
|
||||
const value = params[alias];
|
||||
if (typeof value === "string") return value.trim().length > 0;
|
||||
if (typeof value === "number") return Number.isFinite(value);
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +155,9 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
||||
accountId: params.accountId,
|
||||
mode: "explicit",
|
||||
});
|
||||
if (!resolvedTarget.ok) throw resolvedTarget.error;
|
||||
if (!resolvedTarget.ok) {
|
||||
throw resolvedTarget.error;
|
||||
}
|
||||
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
|
||||
@@ -39,16 +39,26 @@ function resolveContextGuardTarget(
|
||||
action: ChannelMessageActionName,
|
||||
params: Record<string, unknown>,
|
||||
): string | undefined {
|
||||
if (!CONTEXT_GUARDED_ACTIONS.has(action)) return undefined;
|
||||
|
||||
if (action === "thread-reply" || action === "thread-create") {
|
||||
if (typeof params.channelId === "string") return params.channelId;
|
||||
if (typeof params.to === "string") return params.to;
|
||||
if (!CONTEXT_GUARDED_ACTIONS.has(action)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof params.to === "string") return params.to;
|
||||
if (typeof params.channelId === "string") return params.channelId;
|
||||
if (action === "thread-reply" || action === "thread-create") {
|
||||
if (typeof params.channelId === "string") {
|
||||
return params.channelId;
|
||||
}
|
||||
if (typeof params.to === "string") {
|
||||
return params.to;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof params.to === "string") {
|
||||
return params.to;
|
||||
}
|
||||
if (typeof params.channelId === "string") {
|
||||
return params.channelId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -62,10 +72,14 @@ function isCrossContextTarget(params: {
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
}): boolean {
|
||||
const currentTarget = params.toolContext?.currentChannelId?.trim();
|
||||
if (!currentTarget) return false;
|
||||
if (!currentTarget) {
|
||||
return false;
|
||||
}
|
||||
const normalizedTarget = normalizeTarget(params.channel, params.target);
|
||||
const normalizedCurrent = normalizeTarget(params.channel, currentTarget);
|
||||
if (!normalizedTarget || !normalizedCurrent) return false;
|
||||
if (!normalizedTarget || !normalizedCurrent) {
|
||||
return false;
|
||||
}
|
||||
return normalizedTarget !== normalizedCurrent;
|
||||
}
|
||||
|
||||
@@ -77,10 +91,16 @@ export function enforceCrossContextPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): void {
|
||||
const currentTarget = params.toolContext?.currentChannelId?.trim();
|
||||
if (!currentTarget) return;
|
||||
if (!CONTEXT_GUARDED_ACTIONS.has(params.action)) return;
|
||||
if (!currentTarget) {
|
||||
return;
|
||||
}
|
||||
if (!CONTEXT_GUARDED_ACTIONS.has(params.action)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.cfg.tools?.message?.allowCrossContextSend) return;
|
||||
if (params.cfg.tools?.message?.allowCrossContextSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProvider = params.toolContext?.currentChannelProvider;
|
||||
const allowWithinProvider =
|
||||
@@ -97,10 +117,14 @@ export function enforceCrossContextPolicy(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowWithinProvider) return;
|
||||
if (allowWithinProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = resolveContextGuardTarget(params.action, params.args);
|
||||
if (!target) return;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCrossContextTarget({ channel: params.channel, target, toolContext: params.toolContext })) {
|
||||
return;
|
||||
@@ -118,13 +142,21 @@ export async function buildCrossContextDecoration(params: {
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
accountId?: string | null;
|
||||
}): Promise<CrossContextDecoration | null> {
|
||||
if (!params.toolContext?.currentChannelId) return null;
|
||||
if (!params.toolContext?.currentChannelId) {
|
||||
return null;
|
||||
}
|
||||
// Skip decoration for direct tool sends (agent composing, not forwarding)
|
||||
if (params.toolContext.skipCrossContextDecoration) return null;
|
||||
if (!isCrossContextTarget(params)) return null;
|
||||
if (params.toolContext.skipCrossContextDecoration) {
|
||||
return null;
|
||||
}
|
||||
if (!isCrossContextTarget(params)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markerConfig = params.cfg.tools?.message?.crossContext?.marker;
|
||||
if (markerConfig?.enabled === false) return null;
|
||||
if (markerConfig?.enabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentName =
|
||||
(await lookupDirectoryDisplay({
|
||||
|
||||
@@ -36,7 +36,9 @@ export type OutboundSendContext = {
|
||||
};
|
||||
|
||||
function extractToolPayload(result: AgentToolResult<unknown>): unknown {
|
||||
if (result.details !== undefined) return result.details;
|
||||
if (result.details !== undefined) {
|
||||
return result.details;
|
||||
}
|
||||
const textBlock = Array.isArray(result.content)
|
||||
? result.content.find(
|
||||
(block) =>
|
||||
|
||||
@@ -53,16 +53,24 @@ const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i;
|
||||
const SLACK_CHANNEL_TYPE_CACHE = new Map<string, "channel" | "group" | "dm" | "unknown">();
|
||||
|
||||
function looksLikeUuid(value: string): boolean {
|
||||
if (UUID_RE.test(value) || UUID_COMPACT_RE.test(value)) return true;
|
||||
if (UUID_RE.test(value) || UUID_COMPACT_RE.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const compact = value.replace(/-/g, "");
|
||||
if (!/^[0-9a-f]+$/i.test(compact)) return false;
|
||||
if (!/^[0-9a-f]+$/i.test(compact)) {
|
||||
return false;
|
||||
}
|
||||
return /[a-f]/i.test(compact);
|
||||
}
|
||||
|
||||
function normalizeThreadId(value?: string | number | null): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
if (value == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) return undefined;
|
||||
if (!Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return String(Math.trunc(value));
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
@@ -73,7 +81,9 @@ function stripProviderPrefix(raw: string, channel: string): string {
|
||||
const trimmed = raw.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const prefix = `${channel.toLowerCase()}:`;
|
||||
if (lower.startsWith(prefix)) return trimmed.slice(prefix.length).trim();
|
||||
if (lower.startsWith(prefix)) {
|
||||
return trimmed.slice(prefix.length).trim();
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -86,14 +96,20 @@ function inferPeerKind(params: {
|
||||
resolvedTarget?: ResolvedMessagingTarget;
|
||||
}): RoutePeerKind {
|
||||
const resolvedKind = params.resolvedTarget?.kind;
|
||||
if (resolvedKind === "user") return "dm";
|
||||
if (resolvedKind === "channel") return "channel";
|
||||
if (resolvedKind === "user") {
|
||||
return "dm";
|
||||
}
|
||||
if (resolvedKind === "channel") {
|
||||
return "channel";
|
||||
}
|
||||
if (resolvedKind === "group") {
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const chatTypes = plugin?.capabilities?.chatTypes ?? [];
|
||||
const supportsChannel = chatTypes.includes("channel");
|
||||
const supportsGroup = chatTypes.includes("group");
|
||||
if (supportsChannel && !supportsGroup) return "channel";
|
||||
if (supportsChannel && !supportsGroup) {
|
||||
return "channel";
|
||||
}
|
||||
return "group";
|
||||
}
|
||||
return "dm";
|
||||
@@ -123,9 +139,13 @@ async function resolveSlackChannelType(params: {
|
||||
channelId: string;
|
||||
}): Promise<"channel" | "group" | "dm" | "unknown"> {
|
||||
const channelId = params.channelId.trim();
|
||||
if (!channelId) return "unknown";
|
||||
if (!channelId) {
|
||||
return "unknown";
|
||||
}
|
||||
const cached = SLACK_CHANNEL_TYPE_CACHE.get(`${params.accountId ?? "default"}:${channelId}`);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const groupChannels = normalizeAllowListLower(account.dm?.groupChannels);
|
||||
@@ -181,7 +201,9 @@ async function resolveSlackSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): Promise<OutboundSessionRoute | null> {
|
||||
const parsed = parseSlackTarget(params.target, { defaultKind: "channel" });
|
||||
if (!parsed) return null;
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const isDm = parsed.kind === "user";
|
||||
let peerKind: RoutePeerKind = isDm ? "dm" : "channel";
|
||||
if (!isDm && /^G/i.test(parsed.id)) {
|
||||
@@ -191,8 +213,12 @@ async function resolveSlackSession(
|
||||
accountId: params.accountId,
|
||||
channelId: parsed.id,
|
||||
});
|
||||
if (channelType === "group") peerKind = "group";
|
||||
if (channelType === "dm") peerKind = "dm";
|
||||
if (channelType === "group") {
|
||||
peerKind = "group";
|
||||
}
|
||||
if (channelType === "dm") {
|
||||
peerKind = "dm";
|
||||
}
|
||||
}
|
||||
const peer: RoutePeer = {
|
||||
kind: peerKind,
|
||||
@@ -230,7 +256,9 @@ function resolveDiscordSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const parsed = parseDiscordTarget(params.target, { defaultKind: "channel" });
|
||||
if (!parsed) return null;
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const isDm = parsed.kind === "user";
|
||||
const peer: RoutePeer = {
|
||||
kind: isDm ? "dm" : "channel",
|
||||
@@ -267,7 +295,9 @@ function resolveTelegramSession(
|
||||
): OutboundSessionRoute | null {
|
||||
const parsed = parseTelegramTarget(params.target);
|
||||
const chatId = parsed.chatId.trim();
|
||||
if (!chatId) return null;
|
||||
if (!chatId) {
|
||||
return null;
|
||||
}
|
||||
const parsedThreadId = parsed.messageThreadId;
|
||||
const fallbackThreadId = normalizeThreadId(params.threadId);
|
||||
const resolvedThreadId =
|
||||
@@ -307,7 +337,9 @@ function resolveWhatsAppSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const normalized = normalizeWhatsAppTarget(params.target);
|
||||
if (!normalized) return null;
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = isWhatsAppGroupJid(normalized);
|
||||
const peer: RoutePeer = {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
@@ -337,7 +369,9 @@ function resolveSignalSession(
|
||||
const lowered = stripped.toLowerCase();
|
||||
if (lowered.startsWith("group:")) {
|
||||
const groupId = stripped.slice("group:".length).trim();
|
||||
if (!groupId) return null;
|
||||
if (!groupId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "group", id: groupId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -362,7 +396,9 @@ function resolveSignalSession(
|
||||
} else if (lowered.startsWith("u:")) {
|
||||
recipient = stripped.slice("u:".length).trim();
|
||||
}
|
||||
if (!recipient) return null;
|
||||
if (!recipient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uuidCandidate = recipient.toLowerCase().startsWith("uuid:")
|
||||
? recipient.slice("uuid:".length)
|
||||
@@ -397,7 +433,9 @@ function resolveIMessageSession(
|
||||
const parsed = parseIMessageTarget(params.target);
|
||||
if (parsed.kind === "handle") {
|
||||
const handle = normalizeIMessageHandle(parsed.to);
|
||||
if (!handle) return null;
|
||||
if (!handle) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "dm", id: handle };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -422,7 +460,9 @@ function resolveIMessageSession(
|
||||
: parsed.kind === "chat_guid"
|
||||
? parsed.chatGuid
|
||||
: parsed.chatIdentifier;
|
||||
if (!peerId) return null;
|
||||
if (!peerId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "group", id: peerId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -454,7 +494,9 @@ function resolveMatrixSession(
|
||||
const isUser =
|
||||
params.resolvedTarget?.kind === "user" || stripped.startsWith("@") || /^user:/i.test(stripped);
|
||||
const rawId = stripKindPrefix(stripped);
|
||||
if (!rawId) return null;
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: isUser ? "dm" : "channel", id: rawId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -477,13 +519,17 @@ function resolveMSTeamsSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^(msteams|teams):/i, "").trim();
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
const isUser = lower.startsWith("user:");
|
||||
const rawId = stripKindPrefix(trimmed);
|
||||
if (!rawId) return null;
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const conversationId = rawId.split(";")[0] ?? rawId;
|
||||
const isChannel = !isUser && /@thread\.tacv2/i.test(conversationId);
|
||||
const peer: RoutePeer = {
|
||||
@@ -515,7 +561,9 @@ function resolveMattermostSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^mattermost:/i, "").trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const isUser = lower.startsWith("user:") || trimmed.startsWith("@");
|
||||
@@ -523,7 +571,9 @@ function resolveMattermostSession(
|
||||
trimmed = trimmed.slice(1).trim();
|
||||
}
|
||||
const rawId = stripKindPrefix(trimmed);
|
||||
if (!rawId) return null;
|
||||
if (!rawId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: isUser ? "dm" : "channel", id: rawId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -565,7 +615,9 @@ function resolveBlueBubblesSession(
|
||||
const peerId = isGroup
|
||||
? rawPeerId.replace(/^(chat_id|chat_guid|chat_identifier):/i, "")
|
||||
: rawPeerId;
|
||||
if (!peerId) return null;
|
||||
if (!peerId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = {
|
||||
kind: isGroup ? "group" : "dm",
|
||||
id: peerId,
|
||||
@@ -591,10 +643,14 @@ function resolveNextcloudTalkSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = params.target.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
trimmed = trimmed.replace(/^(nextcloud-talk|nc-talk|nc):/i, "").trim();
|
||||
trimmed = trimmed.replace(/^room:/i, "").trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "group", id: trimmed };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -619,7 +675,9 @@ function resolveZaloSession(
|
||||
const trimmed = stripProviderPrefix(params.target, "zalo")
|
||||
.replace(/^(zl):/i, "")
|
||||
.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = trimmed.toLowerCase().startsWith("group:");
|
||||
const peerId = stripKindPrefix(trimmed);
|
||||
const peer: RoutePeer = { kind: isGroup ? "group" : "dm", id: peerId };
|
||||
@@ -646,7 +704,9 @@ function resolveZalouserSession(
|
||||
const trimmed = stripProviderPrefix(params.target, "zalouser")
|
||||
.replace(/^(zlu):/i, "")
|
||||
.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const isGroup = trimmed.toLowerCase().startsWith("group:");
|
||||
const peerId = stripKindPrefix(trimmed);
|
||||
// Keep DM vs group aligned with inbound sessions for Zalo Personal.
|
||||
@@ -672,7 +732,9 @@ function resolveNostrSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const trimmed = stripProviderPrefix(params.target, "nostr").trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: "dm", id: trimmed };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -693,7 +755,9 @@ function resolveNostrSession(
|
||||
|
||||
function normalizeTlonShip(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.startsWith("~") ? trimmed : `~${trimmed}`;
|
||||
}
|
||||
|
||||
@@ -702,7 +766,9 @@ function resolveTlonSession(
|
||||
): OutboundSessionRoute | null {
|
||||
let trimmed = stripProviderPrefix(params.target, "tlon");
|
||||
trimmed = trimmed.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
let isGroup =
|
||||
lower.startsWith("group:") || lower.startsWith("room:") || lower.startsWith("chat/");
|
||||
@@ -754,13 +820,17 @@ function resolveFallbackSession(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): OutboundSessionRoute | null {
|
||||
const trimmed = stripProviderPrefix(params.target, params.channel).trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const peerKind = inferPeerKind({
|
||||
channel: params.channel,
|
||||
resolvedTarget: params.resolvedTarget,
|
||||
});
|
||||
const peerId = stripKindPrefix(trimmed);
|
||||
if (!peerId) return null;
|
||||
if (!peerId) {
|
||||
return null;
|
||||
}
|
||||
const peer: RoutePeer = { kind: peerKind, id: peerId };
|
||||
const baseSessionKey = buildBaseSessionKey({
|
||||
cfg: params.cfg,
|
||||
@@ -786,7 +856,9 @@ export async function resolveOutboundSessionRoute(
|
||||
params: ResolveOutboundSessionRouteParams,
|
||||
): Promise<OutboundSessionRoute | null> {
|
||||
const target = params.target.trim();
|
||||
if (!target) return null;
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
switch (params.channel) {
|
||||
case "slack":
|
||||
return await resolveSlackSession({ ...params, target });
|
||||
|
||||
@@ -19,11 +19,17 @@ function mergeMediaUrls(...lists: Array<Array<string | undefined> | undefined>):
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
for (const list of lists) {
|
||||
if (!list) continue;
|
||||
if (!list) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of list) {
|
||||
const trimmed = entry?.trim();
|
||||
if (!trimmed) continue;
|
||||
if (seen.has(trimmed)) continue;
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
if (seen.has(trimmed)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
merged.push(trimmed);
|
||||
}
|
||||
@@ -52,8 +58,12 @@ export function normalizeReplyPayloadsForDelivery(payloads: ReplyPayload[]): Rep
|
||||
replyToCurrent: payload.replyToCurrent || parsed.replyToCurrent,
|
||||
audioAsVoice: Boolean(payload.audioAsVoice || parsed.audioAsVoice),
|
||||
};
|
||||
if (parsed.isSilent && mergedMedia.length === 0) return [];
|
||||
if (!isRenderablePayload(next)) return [];
|
||||
if (parsed.isSilent && mergedMedia.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (!isRenderablePayload(next)) {
|
||||
return [];
|
||||
}
|
||||
return [next];
|
||||
});
|
||||
}
|
||||
@@ -90,7 +100,11 @@ export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): Outb
|
||||
|
||||
export function formatOutboundPayloadLog(payload: NormalizedOutboundPayload): string {
|
||||
const lines: string[] = [];
|
||||
if (payload.text) lines.push(payload.text.trimEnd());
|
||||
for (const url of payload.mediaUrls) lines.push(`MEDIA:${url}`);
|
||||
if (payload.text) {
|
||||
lines.push(payload.text.trimEnd());
|
||||
}
|
||||
for (const url of payload.mediaUrls) {
|
||||
lines.push(`MEDIA:${url}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export function unknownTargetError(provider: string, raw: string, hint?: string)
|
||||
}
|
||||
|
||||
function formatTargetHint(hint?: string, withLabel = false): string {
|
||||
if (!hint) return "";
|
||||
if (!hint) {
|
||||
return "";
|
||||
}
|
||||
return withLabel ? ` Hint: ${hint}` : ` ${hint}`;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ export function normalizeChannelTargetInput(raw: string): string {
|
||||
}
|
||||
|
||||
export function normalizeTargetForProvider(provider: string, raw?: string): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const providerId = normalizeChannelId(provider);
|
||||
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
|
||||
const normalized =
|
||||
|
||||
@@ -51,8 +51,12 @@ export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?:
|
||||
const channelKey = params.channel;
|
||||
const accountKey = params.accountId ?? "default";
|
||||
directoryCache.clearMatching((key) => {
|
||||
if (!key.startsWith(`${channelKey}:`)) return false;
|
||||
if (!params.accountId) return true;
|
||||
if (!key.startsWith(`${channelKey}:`)) {
|
||||
return false;
|
||||
}
|
||||
if (!params.accountId) {
|
||||
return true;
|
||||
}
|
||||
return key.startsWith(`${channelKey}:${accountKey}:`);
|
||||
});
|
||||
}
|
||||
@@ -91,14 +95,24 @@ export function formatTargetDisplay(params: {
|
||||
(lowered.startsWith("user:") ? "user" : lowered.startsWith("channel:") ? "group" : undefined);
|
||||
|
||||
if (display) {
|
||||
if (display.startsWith("#") || display.startsWith("@")) return display;
|
||||
if (kind === "user") return `@${display}`;
|
||||
if (kind === "group" || kind === "channel") return `#${display}`;
|
||||
if (display.startsWith("#") || display.startsWith("@")) {
|
||||
return display;
|
||||
}
|
||||
if (kind === "user") {
|
||||
return `@${display}`;
|
||||
}
|
||||
if (kind === "group" || kind === "channel") {
|
||||
return `#${display}`;
|
||||
}
|
||||
return display;
|
||||
}
|
||||
|
||||
if (!trimmedTarget) return trimmedTarget;
|
||||
if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) return trimmedTarget;
|
||||
if (!trimmedTarget) {
|
||||
return trimmedTarget;
|
||||
}
|
||||
if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) {
|
||||
return trimmedTarget;
|
||||
}
|
||||
|
||||
const channelPrefix = `${params.channel}:`;
|
||||
const withoutProvider = trimmedTarget.toLowerCase().startsWith(channelPrefix)
|
||||
@@ -116,11 +130,19 @@ export function formatTargetDisplay(params: {
|
||||
}
|
||||
|
||||
function preserveTargetCase(channel: ChannelId, raw: string, normalized: string): string {
|
||||
if (channel !== "slack") return normalized;
|
||||
if (channel !== "slack") {
|
||||
return normalized;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (/^channel:/i.test(trimmed) || /^user:/i.test(trimmed)) return trimmed;
|
||||
if (trimmed.startsWith("#")) return `channel:${trimmed.slice(1).trim()}`;
|
||||
if (trimmed.startsWith("@")) return `user:${trimmed.slice(1).trim()}`;
|
||||
if (/^channel:/i.test(trimmed) || /^user:/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.startsWith("#")) {
|
||||
return `channel:${trimmed.slice(1).trim()}`;
|
||||
}
|
||||
if (trimmed.startsWith("@")) {
|
||||
return `user:${trimmed.slice(1).trim()}`;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@@ -129,12 +151,20 @@ function detectTargetKind(
|
||||
raw: string,
|
||||
preferred?: TargetResolveKind,
|
||||
): TargetResolveKind {
|
||||
if (preferred) return preferred;
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return "group";
|
||||
if (!trimmed) {
|
||||
return "group";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) return "user";
|
||||
if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) return "group";
|
||||
if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) {
|
||||
return "user";
|
||||
}
|
||||
if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) {
|
||||
return "group";
|
||||
}
|
||||
|
||||
// For some channels (e.g., BlueBubbles/iMessage), bare phone numbers are almost always DM targets.
|
||||
if ((channel === "bluebubbles" || channel === "imessage") && /^\+?\d{6,}$/.test(trimmed)) {
|
||||
@@ -155,7 +185,9 @@ function matchesDirectoryEntry(params: {
|
||||
query: string;
|
||||
}): boolean {
|
||||
const query = normalizeQuery(params.query);
|
||||
if (!query) return false;
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
const id = stripTargetPrefixes(normalizeDirectoryEntryId(params.channel, params.entry));
|
||||
const name = params.entry.name ? stripTargetPrefixes(params.entry.name) : "";
|
||||
const handle = params.entry.handle ? stripTargetPrefixes(params.entry.handle) : "";
|
||||
@@ -171,8 +203,12 @@ function resolveMatch(params: {
|
||||
const matches = params.entries.filter((entry) =>
|
||||
matchesDirectoryEntry({ channel: params.channel, entry, query: params.query }),
|
||||
);
|
||||
if (matches.length === 0) return { kind: "none" as const };
|
||||
if (matches.length === 1) return { kind: "single" as const, entry: matches[0] };
|
||||
if (matches.length === 0) {
|
||||
return { kind: "none" as const };
|
||||
}
|
||||
if (matches.length === 1) {
|
||||
return { kind: "single" as const, entry: matches[0] };
|
||||
}
|
||||
return { kind: "ambiguous" as const, entries: matches };
|
||||
}
|
||||
|
||||
@@ -187,12 +223,16 @@ async function listDirectoryEntries(params: {
|
||||
}): Promise<ChannelDirectoryEntry[]> {
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const directory = plugin?.directory;
|
||||
if (!directory) return [];
|
||||
if (!directory) {
|
||||
return [];
|
||||
}
|
||||
const runtime = params.runtime ?? defaultRuntime;
|
||||
const useLive = params.source === "live";
|
||||
if (params.kind === "user") {
|
||||
const fn = useLive ? (directory.listPeersLive ?? directory.listPeers) : directory.listPeers;
|
||||
if (!fn) return [];
|
||||
if (!fn) {
|
||||
return [];
|
||||
}
|
||||
return await fn({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId ?? undefined,
|
||||
@@ -202,7 +242,9 @@ async function listDirectoryEntries(params: {
|
||||
});
|
||||
}
|
||||
const fn = useLive ? (directory.listGroupsLive ?? directory.listGroups) : directory.listGroups;
|
||||
if (!fn) return [];
|
||||
if (!fn) {
|
||||
return [];
|
||||
}
|
||||
return await fn({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId ?? undefined,
|
||||
@@ -230,7 +272,9 @@ async function getDirectoryEntries(params: {
|
||||
signature,
|
||||
});
|
||||
const cached = directoryCache.get(cacheKey, params.cfg);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const entries = await listDirectoryEntries({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
@@ -269,8 +313,12 @@ function pickAmbiguousMatch(
|
||||
entries: ChannelDirectoryEntry[],
|
||||
mode: ResolveAmbiguousMode,
|
||||
): ChannelDirectoryEntry | null {
|
||||
if (entries.length === 0) return null;
|
||||
if (mode === "first") return entries[0] ?? null;
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (mode === "first") {
|
||||
return entries[0] ?? null;
|
||||
}
|
||||
const ranked = entries.map((entry) => ({
|
||||
entry,
|
||||
rank: typeof entry.rank === "number" ? entry.rank : 0,
|
||||
@@ -300,19 +348,33 @@ export async function resolveMessagingTarget(params: {
|
||||
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
||||
const looksLikeTargetId = (): boolean => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return false;
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
const lookup = plugin?.messaging?.targetResolver?.looksLikeId;
|
||||
if (lookup) return lookup(trimmed, normalized);
|
||||
if (/^(channel|group|user):/i.test(trimmed)) return true;
|
||||
if (/^[@#]/.test(trimmed)) return true;
|
||||
if (lookup) {
|
||||
return lookup(trimmed, normalized);
|
||||
}
|
||||
if (/^(channel|group|user):/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^[@#]/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (/^\+?\d{6,}$/.test(trimmed)) {
|
||||
// BlueBubbles/iMessage phone numbers should usually resolve via the directory to a DM chat,
|
||||
// otherwise the provider may pick an existing group containing that handle.
|
||||
if (params.channel === "bluebubbles" || params.channel === "imessage") return false;
|
||||
if (params.channel === "bluebubbles" || params.channel === "imessage") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@thread")) {
|
||||
return true;
|
||||
}
|
||||
if (/^(conversation|user):/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@thread")) return true;
|
||||
if (/^(conversation|user):/i.test(trimmed)) return true;
|
||||
return false;
|
||||
};
|
||||
if (looksLikeTargetId()) {
|
||||
|
||||
@@ -184,7 +184,9 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
target = rawTarget;
|
||||
} else if (typeof rawTarget === "string") {
|
||||
const normalized = normalizeChannelId(rawTarget);
|
||||
if (normalized) target = normalized;
|
||||
if (normalized) {
|
||||
target = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (target === "none") {
|
||||
@@ -279,12 +281,16 @@ function resolveHeartbeatSenderId(params: {
|
||||
}
|
||||
if (candidates.length > 0 && allowList.length > 0) {
|
||||
const matched = candidates.find((candidate) => allowList.includes(candidate));
|
||||
if (matched) return matched;
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
if (candidates.length > 0 && allowList.length === 0) {
|
||||
return candidates[0];
|
||||
}
|
||||
if (allowList.length > 0) return allowList[0];
|
||||
if (allowList.length > 0) {
|
||||
return allowList[0];
|
||||
}
|
||||
return candidates[0] ?? "heartbeat";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user