mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 08:32:00 +00:00
refactor(extensions): extract feishu dedup and mattermost onchar helpers
This commit is contained in:
@@ -10,6 +10,7 @@ import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } fro
|
||||
import type { DynamicAgentCreationConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { tryRecordMessage } from "./dedup.js";
|
||||
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
||||
import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
|
||||
import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js";
|
||||
@@ -23,37 +24,6 @@ import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import { getMessageFeishu, sendMessageFeishu } from "./send.js";
|
||||
|
||||
// --- Message deduplication ---
|
||||
// Prevent duplicate processing when WebSocket reconnects or Feishu redelivers messages.
|
||||
const DEDUP_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
const DEDUP_MAX_SIZE = 1_000;
|
||||
const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // cleanup every 5 minutes
|
||||
const processedMessageIds = new Map<string, number>(); // messageId -> timestamp
|
||||
let lastCleanupTime = Date.now();
|
||||
|
||||
function tryRecordMessage(messageId: string): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
// Throttled cleanup: evict expired entries at most once per interval
|
||||
if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
|
||||
for (const [id, ts] of processedMessageIds) {
|
||||
if (now - ts > DEDUP_TTL_MS) processedMessageIds.delete(id);
|
||||
}
|
||||
lastCleanupTime = now;
|
||||
}
|
||||
|
||||
if (processedMessageIds.has(messageId)) return false;
|
||||
|
||||
// Evict oldest entries if cache is full
|
||||
if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
|
||||
const first = processedMessageIds.keys().next().value!;
|
||||
processedMessageIds.delete(first);
|
||||
}
|
||||
|
||||
processedMessageIds.set(messageId, now);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Permission error extraction ---
|
||||
// Extract permission grant URL from Feishu API error response.
|
||||
type PermissionError = {
|
||||
|
||||
33
extensions/feishu/src/dedup.ts
Normal file
33
extensions/feishu/src/dedup.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Prevent duplicate processing when WebSocket reconnects or Feishu redelivers messages.
|
||||
const DEDUP_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
const DEDUP_MAX_SIZE = 1_000;
|
||||
const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // cleanup every 5 minutes
|
||||
const processedMessageIds = new Map<string, number>(); // messageId -> timestamp
|
||||
let lastCleanupTime = Date.now();
|
||||
|
||||
export function tryRecordMessage(messageId: string): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
// Throttled cleanup: evict expired entries at most once per interval.
|
||||
if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
|
||||
for (const [id, ts] of processedMessageIds) {
|
||||
if (now - ts > DEDUP_TTL_MS) {
|
||||
processedMessageIds.delete(id);
|
||||
}
|
||||
}
|
||||
lastCleanupTime = now;
|
||||
}
|
||||
|
||||
if (processedMessageIds.has(messageId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evict oldest entries if cache is full.
|
||||
if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
|
||||
const first = processedMessageIds.keys().next().value!;
|
||||
processedMessageIds.delete(first);
|
||||
}
|
||||
|
||||
processedMessageIds.set(messageId, now);
|
||||
return true;
|
||||
}
|
||||
25
extensions/mattermost/src/mattermost/monitor-onchar.ts
Normal file
25
extensions/mattermost/src/mattermost/monitor-onchar.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
|
||||
|
||||
export function resolveOncharPrefixes(prefixes: string[] | undefined): string[] {
|
||||
const cleaned = prefixes?.map((entry) => entry.trim()).filter(Boolean) ?? DEFAULT_ONCHAR_PREFIXES;
|
||||
return cleaned.length > 0 ? cleaned : DEFAULT_ONCHAR_PREFIXES;
|
||||
}
|
||||
|
||||
export function stripOncharPrefix(
|
||||
text: string,
|
||||
prefixes: string[],
|
||||
): { triggered: boolean; stripped: string } {
|
||||
const trimmed = text.trimStart();
|
||||
for (const prefix of prefixes) {
|
||||
if (!prefix) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return {
|
||||
triggered: true,
|
||||
stripped: trimmed.slice(prefix.length).trimStart(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return { triggered: false, stripped: text };
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
rawDataToString,
|
||||
resolveThreadSessionKeys,
|
||||
} from "./monitor-helpers.js";
|
||||
import { resolveOncharPrefixes, stripOncharPrefix } from "./monitor-onchar.js";
|
||||
import { sendMessageMattermost } from "./send.js";
|
||||
|
||||
export type MonitorMattermostOpts = {
|
||||
@@ -75,7 +76,6 @@ const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 60_000;
|
||||
const RECENT_MATTERMOST_MESSAGE_MAX = 2000;
|
||||
const CHANNEL_CACHE_TTL_MS = 5 * 60_000;
|
||||
const USER_CACHE_TTL_MS = 10 * 60_000;
|
||||
const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
|
||||
|
||||
const recentInboundMessages = createDedupeCache({
|
||||
ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
|
||||
@@ -103,30 +103,6 @@ function normalizeMention(text: string, mention: string | undefined): string {
|
||||
return text.replace(re, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function resolveOncharPrefixes(prefixes: string[] | undefined): string[] {
|
||||
const cleaned = prefixes?.map((entry) => entry.trim()).filter(Boolean) ?? DEFAULT_ONCHAR_PREFIXES;
|
||||
return cleaned.length > 0 ? cleaned : DEFAULT_ONCHAR_PREFIXES;
|
||||
}
|
||||
|
||||
function stripOncharPrefix(
|
||||
text: string,
|
||||
prefixes: string[],
|
||||
): { triggered: boolean; stripped: string } {
|
||||
const trimmed = text.trimStart();
|
||||
for (const prefix of prefixes) {
|
||||
if (!prefix) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return {
|
||||
triggered: true,
|
||||
stripped: trimmed.slice(prefix.length).trimStart(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return { triggered: false, stripped: text };
|
||||
}
|
||||
|
||||
function isSystemPost(post: MattermostPost): boolean {
|
||||
const type = post.type?.trim();
|
||||
return Boolean(type);
|
||||
|
||||
Reference in New Issue
Block a user