



























@@ -1,16 +1,21 @@
1-import type { WebClient as SlackWebClient } from "@slack/web-api";
2-import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
3-import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
41import { normalizeHostname } from "openclaw/plugin-sdk/host-runtime";
52import { resolveRequestUrl } from "openclaw/plugin-sdk/request-url";
63import type { SlackAttachment, SlackFile } from "../types.js";
4+export { MAX_SLACK_MEDIA_FILES, type SlackMediaResult } from "./media-types.js";
5+import { MAX_SLACK_MEDIA_FILES, type SlackMediaResult } from "./media-types.js";
76import {
87type FetchLike,
98fetchRemoteMedia,
109fetchWithRuntimeDispatcher,
11-logVerbose,
1210saveMediaBuffer,
1311} from "./media.runtime.js";
12+export {
13+resetSlackThreadStarterCacheForTest,
14+resolveSlackThreadHistory,
15+resolveSlackThreadStarter,
16+type SlackThreadMessage,
17+type SlackThreadStarter,
18+} from "./thread.js";
14191520function normalizeLowercaseStringOrEmpty(value: unknown): string {
1621return typeof value === "string" ? value.trim().toLowerCase() : "";
@@ -165,13 +170,6 @@ function looksLikeHtmlBuffer(buffer: Buffer): boolean {
165170return head.startsWith("<!doctype html") || head.startsWith("<html");
166171}
167172168-export type SlackMediaResult = {
169-path: string;
170-contentType?: string;
171-placeholder: string;
172-};
173-174-export const MAX_SLACK_MEDIA_FILES = 8;
175173const MAX_SLACK_MEDIA_CONCURRENCY = 3;
176174const MAX_SLACK_FORWARDED_ATTACHMENTS = 8;
177175@@ -373,186 +371,3 @@ export async function resolveSlackAttachmentContent(params: {
373371}
374372return { text: combinedText, media: allMedia };
375373}
376-377-export type SlackThreadStarter = {
378-text: string;
379-userId?: string;
380-botId?: string;
381-ts?: string;
382-files?: SlackFile[];
383-};
384-385-type SlackThreadStarterCacheEntry = {
386-value: SlackThreadStarter;
387-cachedAt: number;
388-};
389-390-const THREAD_STARTER_CACHE = new Map<string, SlackThreadStarterCacheEntry>();
391-const THREAD_STARTER_CACHE_TTL_MS = 6 * 60 * 60_000;
392-const THREAD_STARTER_CACHE_MAX = 2000;
393-394-function evictThreadStarterCache(): void {
395-const now = Date.now();
396-for (const [cacheKey, entry] of THREAD_STARTER_CACHE.entries()) {
397-if (now - entry.cachedAt > THREAD_STARTER_CACHE_TTL_MS) {
398-THREAD_STARTER_CACHE.delete(cacheKey);
399-}
400-}
401-pruneMapToMaxSize(THREAD_STARTER_CACHE, THREAD_STARTER_CACHE_MAX);
402-}
403-404-function formatSlackFilePlaceholder(files: SlackFile[] | undefined): string {
405-return `[attached: ${files?.map((file) => file.name ?? "file").join(", ") ?? "file"}]`;
406-}
407-408-export async function resolveSlackThreadStarter(params: {
409-channelId: string;
410-threadTs: string;
411-client: SlackWebClient;
412-}): Promise<SlackThreadStarter | null> {
413-evictThreadStarterCache();
414-const cacheKey = `${params.channelId}:${params.threadTs}`;
415-const cached = THREAD_STARTER_CACHE.get(cacheKey);
416-if (cached && Date.now() - cached.cachedAt <= THREAD_STARTER_CACHE_TTL_MS) {
417-return cached.value;
418-}
419-if (cached) {
420-THREAD_STARTER_CACHE.delete(cacheKey);
421-}
422-try {
423-const response = (await params.client.conversations.replies({
424-channel: params.channelId,
425-ts: params.threadTs,
426-limit: 1,
427-inclusive: true,
428-})) as {
429-messages?: Array<{
430-text?: string;
431-user?: string;
432-bot_id?: string;
433-ts?: string;
434-files?: SlackFile[];
435-}>;
436-};
437-const message = response?.messages?.[0];
438-const text = (message?.text ?? "").trim();
439-const files = message?.files?.length ? message.files : undefined;
440-if (!message || (!text && !files)) {
441-return null;
442-}
443-const starter: SlackThreadStarter = {
444-text: text || formatSlackFilePlaceholder(files),
445-userId: message.user,
446-botId: message.bot_id,
447-ts: message.ts,
448- files,
449-};
450-if (THREAD_STARTER_CACHE.has(cacheKey)) {
451-THREAD_STARTER_CACHE.delete(cacheKey);
452-}
453-THREAD_STARTER_CACHE.set(cacheKey, {
454-value: starter,
455-cachedAt: Date.now(),
456-});
457-evictThreadStarterCache();
458-return starter;
459-} catch (err) {
460-logVerbose(
461-`slack thread starter fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`,
462-);
463-return null;
464-}
465-}
466-467-export function resetSlackThreadStarterCacheForTest(): void {
468-THREAD_STARTER_CACHE.clear();
469-}
470-471-export type SlackThreadMessage = {
472-text: string;
473-userId?: string;
474-ts?: string;
475-botId?: string;
476-files?: SlackFile[];
477-};
478-479-type SlackRepliesPageMessage = {
480-text?: string;
481-user?: string;
482-bot_id?: string;
483-ts?: string;
484-files?: SlackFile[];
485-};
486-487-type SlackRepliesPage = {
488-messages?: SlackRepliesPageMessage[];
489-response_metadata?: { next_cursor?: string };
490-};
491-492-/**
493- * Fetches the most recent messages in a Slack thread (excluding the current message).
494- * Used to populate thread context when a new thread session starts.
495- *
496- * Uses cursor pagination and keeps only the latest N retained messages so long threads
497- * still produce up-to-date context without unbounded memory growth.
498- */
499-export async function resolveSlackThreadHistory(params: {
500-channelId: string;
501-threadTs: string;
502-client: SlackWebClient;
503-currentMessageTs?: string;
504-limit?: number;
505-}): Promise<SlackThreadMessage[]> {
506-const maxMessages = params.limit ?? 20;
507-if (!Number.isFinite(maxMessages) || maxMessages <= 0) {
508-return [];
509-}
510-511-// Slack recommends no more than 200 per page.
512-const fetchLimit = 200;
513-const retained: SlackRepliesPageMessage[] = [];
514-let cursor: string | undefined;
515-516-try {
517-do {
518-const response = (await params.client.conversations.replies({
519-channel: params.channelId,
520-ts: params.threadTs,
521-limit: fetchLimit,
522-inclusive: true,
523- ...(cursor ? { cursor } : {}),
524-})) as SlackRepliesPage;
525-526-for (const msg of response.messages ?? []) {
527-// Keep messages with text OR file attachments
528-if (!msg.text?.trim() && !msg.files?.length) {
529-continue;
530-}
531-if (params.currentMessageTs && msg.ts === params.currentMessageTs) {
532-continue;
533-}
534-retained.push(msg);
535-if (retained.length > maxMessages) {
536-retained.shift();
537-}
538-}
539-540-const next = response.response_metadata?.next_cursor;
541-cursor = typeof next === "string" && next.trim().length > 0 ? next.trim() : undefined;
542-} while (cursor);
543-544-return retained.map((msg) => ({
545-// For file-only messages, create a placeholder showing attached filenames
546-text: msg.text?.trim() ? msg.text : formatSlackFilePlaceholder(msg.files),
547-userId: msg.user,
548-botId: msg.bot_id,
549-ts: msg.ts,
550-files: msg.files,
551-}));
552-} catch (err) {
553-logVerbose(
554-`slack thread history fetch failed channel=${params.channelId} ts=${params.threadTs}: ${formatErrorMessage(err)}`,
555-);
556-return [];
557-}
558-}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。