mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 07:41:08 +00:00
218 lines
7.4 KiB
TypeScript
218 lines
7.4 KiB
TypeScript
import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound";
|
|
import type { ContextVisibilityMode } from "openclaw/plugin-sdk/config-runtime";
|
|
import { readSessionUpdatedAt } from "openclaw/plugin-sdk/config-runtime";
|
|
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
import {
|
|
filterSupplementalContextItems,
|
|
shouldIncludeSupplementalContext,
|
|
} from "openclaw/plugin-sdk/security-runtime";
|
|
import type { ResolvedSlackAccount } from "../../accounts.js";
|
|
import type { SlackMessageEvent } from "../../types.js";
|
|
import { resolveSlackAllowListMatch } from "../allow-list.js";
|
|
import type { SlackMonitorContext } from "../context.js";
|
|
import {
|
|
resolveSlackMedia,
|
|
resolveSlackThreadHistory,
|
|
type SlackMediaResult,
|
|
type SlackThreadStarter,
|
|
} from "../media.js";
|
|
|
|
export type SlackThreadContextData = {
|
|
threadStarterBody: string | undefined;
|
|
threadHistoryBody: string | undefined;
|
|
threadSessionPreviousTimestamp: number | undefined;
|
|
threadLabel: string | undefined;
|
|
threadStarterMedia: SlackMediaResult[] | null;
|
|
};
|
|
|
|
function isSlackThreadContextSenderAllowed(params: {
|
|
allowFromLower: string[];
|
|
allowNameMatching: boolean;
|
|
userId?: string;
|
|
userName?: string;
|
|
botId?: string;
|
|
}): boolean {
|
|
if (params.allowFromLower.length === 0 || params.botId) {
|
|
return true;
|
|
}
|
|
if (!params.userId) {
|
|
return false;
|
|
}
|
|
return resolveSlackAllowListMatch({
|
|
allowList: params.allowFromLower,
|
|
id: params.userId,
|
|
name: params.userName,
|
|
allowNameMatching: params.allowNameMatching,
|
|
}).allowed;
|
|
}
|
|
|
|
export async function resolveSlackThreadContextData(params: {
|
|
ctx: SlackMonitorContext;
|
|
account: ResolvedSlackAccount;
|
|
message: SlackMessageEvent;
|
|
isThreadReply: boolean;
|
|
threadTs: string | undefined;
|
|
threadStarter: SlackThreadStarter | null;
|
|
roomLabel: string;
|
|
storePath: string;
|
|
sessionKey: string;
|
|
allowFromLower: string[];
|
|
allowNameMatching: boolean;
|
|
contextVisibilityMode: ContextVisibilityMode;
|
|
envelopeOptions: ReturnType<
|
|
typeof import("openclaw/plugin-sdk/channel-inbound").resolveEnvelopeFormatOptions
|
|
>;
|
|
effectiveDirectMedia: SlackMediaResult[] | null;
|
|
}): Promise<SlackThreadContextData> {
|
|
let threadStarterBody: string | undefined;
|
|
let threadHistoryBody: string | undefined;
|
|
let threadSessionPreviousTimestamp: number | undefined;
|
|
let threadLabel: string | undefined;
|
|
let threadStarterMedia: SlackMediaResult[] | null = null;
|
|
|
|
if (!params.isThreadReply || !params.threadTs) {
|
|
return {
|
|
threadStarterBody,
|
|
threadHistoryBody,
|
|
threadSessionPreviousTimestamp,
|
|
threadLabel,
|
|
threadStarterMedia,
|
|
};
|
|
}
|
|
|
|
const starter = params.threadStarter;
|
|
const starterSenderName =
|
|
params.allowNameMatching && starter?.userId
|
|
? (await params.ctx.resolveUserName(starter.userId))?.name
|
|
: undefined;
|
|
const starterAllowed =
|
|
!starter ||
|
|
isSlackThreadContextSenderAllowed({
|
|
allowFromLower: params.allowFromLower,
|
|
allowNameMatching: params.allowNameMatching,
|
|
userId: starter.userId,
|
|
userName: starterSenderName,
|
|
botId: starter.botId,
|
|
});
|
|
const includeStarterContext =
|
|
!starter ||
|
|
shouldIncludeSupplementalContext({
|
|
mode: params.contextVisibilityMode,
|
|
kind: "thread",
|
|
senderAllowed: starterAllowed,
|
|
});
|
|
|
|
if (starter?.text && includeStarterContext) {
|
|
threadStarterBody = starter.text;
|
|
const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
|
|
threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`;
|
|
if (!params.effectiveDirectMedia && starter.files && starter.files.length > 0) {
|
|
threadStarterMedia = await resolveSlackMedia({
|
|
files: starter.files,
|
|
token: params.ctx.botToken,
|
|
maxBytes: params.ctx.mediaMaxBytes,
|
|
});
|
|
if (threadStarterMedia) {
|
|
const starterPlaceholders = threadStarterMedia.map((item) => item.placeholder).join(", ");
|
|
logVerbose(`slack: hydrated thread starter file ${starterPlaceholders} from root message`);
|
|
}
|
|
}
|
|
} else {
|
|
threadLabel = `Slack thread ${params.roomLabel}`;
|
|
}
|
|
if (starter?.text && !includeStarterContext) {
|
|
logVerbose(
|
|
`slack: omitted thread starter from context (mode=${params.contextVisibilityMode}, sender_allowed=${starterAllowed ? "yes" : "no"})`,
|
|
);
|
|
}
|
|
|
|
const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20;
|
|
threadSessionPreviousTimestamp = readSessionUpdatedAt({
|
|
storePath: params.storePath,
|
|
sessionKey: params.sessionKey,
|
|
});
|
|
|
|
if (threadInitialHistoryLimit > 0 && !threadSessionPreviousTimestamp) {
|
|
const threadHistory = await resolveSlackThreadHistory({
|
|
channelId: params.message.channel,
|
|
threadTs: params.threadTs,
|
|
client: params.ctx.app.client,
|
|
currentMessageTs: params.message.ts,
|
|
limit: threadInitialHistoryLimit,
|
|
});
|
|
|
|
if (threadHistory.length > 0) {
|
|
const uniqueUserIds = [
|
|
...new Set(
|
|
threadHistory.map((item) => item.userId).filter((id): id is string => Boolean(id)),
|
|
),
|
|
];
|
|
const userMap = new Map<string, { name?: string }>();
|
|
await Promise.all(
|
|
uniqueUserIds.map(async (id) => {
|
|
const user = await params.ctx.resolveUserName(id);
|
|
if (user) {
|
|
userMap.set(id, user);
|
|
}
|
|
}),
|
|
);
|
|
|
|
const { items: filteredThreadHistory, omitted: omittedHistoryCount } =
|
|
filterSupplementalContextItems({
|
|
items: threadHistory,
|
|
mode: params.contextVisibilityMode,
|
|
kind: "thread",
|
|
isSenderAllowed: (historyMsg) => {
|
|
const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
|
|
return isSlackThreadContextSenderAllowed({
|
|
allowFromLower: params.allowFromLower,
|
|
allowNameMatching: params.allowNameMatching,
|
|
userId: historyMsg.userId,
|
|
userName: msgUser?.name,
|
|
botId: historyMsg.botId,
|
|
});
|
|
},
|
|
});
|
|
if (omittedHistoryCount > 0) {
|
|
logVerbose(
|
|
`slack: omitted ${omittedHistoryCount} thread message(s) from context (mode=${params.contextVisibilityMode})`,
|
|
);
|
|
}
|
|
|
|
const historyParts: string[] = [];
|
|
for (const historyMsg of filteredThreadHistory) {
|
|
const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
|
|
const msgSenderName =
|
|
msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown");
|
|
const isBot = Boolean(historyMsg.botId);
|
|
const role = isBot ? "assistant" : "user";
|
|
const msgWithId = `${historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${params.message.channel}]`;
|
|
historyParts.push(
|
|
formatInboundEnvelope({
|
|
channel: "Slack",
|
|
from: `${msgSenderName} (${role})`,
|
|
timestamp: historyMsg.ts ? Math.round(Number(historyMsg.ts) * 1000) : undefined,
|
|
body: msgWithId,
|
|
chatType: "channel",
|
|
envelope: params.envelopeOptions,
|
|
}),
|
|
);
|
|
}
|
|
if (historyParts.length > 0) {
|
|
threadHistoryBody = historyParts.join("\n\n");
|
|
logVerbose(
|
|
`slack: populated thread history with ${filteredThreadHistory.length} messages for new session`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
threadStarterBody,
|
|
threadHistoryBody,
|
|
threadSessionPreviousTimestamp,
|
|
threadLabel,
|
|
threadStarterMedia,
|
|
};
|
|
}
|