|
@@ -37,7 +37,25 @@ import {
|
|
|
CHANNEL_ID,
|
|
CHANNEL_ID,
|
|
|
getChannelCfg,
|
|
getChannelCfg,
|
|
|
normalizeAllowFrom,
|
|
normalizeAllowFrom,
|
|
|
|
|
+ resolveAllowFrom,
|
|
|
resolveApiBase,
|
|
resolveApiBase,
|
|
|
|
|
+ resolveBatchWindowMs,
|
|
|
|
|
+ resolveDmPolicy,
|
|
|
|
|
+ resolveFaq,
|
|
|
|
|
+ resolveGroupEnabled,
|
|
|
|
|
+ resolveGroupKeywords,
|
|
|
|
|
+ resolveGroupMentionTokens,
|
|
|
|
|
+ resolveGroupReplyAtSender,
|
|
|
|
|
+ resolveIgnoreMessageTypes,
|
|
|
|
|
+ resolveIgnoreWxidExact,
|
|
|
|
|
+ resolveIgnoreWxidPrefixes,
|
|
|
|
|
+ resolveHighThroughputOutboundAttemptsPer5Min,
|
|
|
|
|
+ resolveMetricsLogIntervalSec,
|
|
|
|
|
+ resolveOutboundQueueMax,
|
|
|
|
|
+ resolveOutboundRatePerSec,
|
|
|
|
|
+ resolvePollIntervalMs,
|
|
|
|
|
+ resolveReplyCooldownSecPerWxid,
|
|
|
|
|
+ resolveSelfWxid,
|
|
|
} from "./config.js";
|
|
} from "./config.js";
|
|
|
import type { Logger, RawWechatMessage, WechatAgentConfig } from "./types.js";
|
|
import type { Logger, RawWechatMessage, WechatAgentConfig } from "./types.js";
|
|
|
import {
|
|
import {
|
|
@@ -51,6 +69,15 @@ import {
|
|
|
} from "./cursor-store.js";
|
|
} from "./cursor-store.js";
|
|
|
import { join } from "node:path";
|
|
import { join } from "node:path";
|
|
|
import { withRetry } from "./retry.js";
|
|
import { withRetry } from "./retry.js";
|
|
|
|
|
+import { configureOutboundQueue, enqueueOutboundSend } from "./outbound-queue.js";
|
|
|
|
|
+import {
|
|
|
|
|
+ getOutboundMetricsSnapshot,
|
|
|
|
|
+ recordOutboundAttempt,
|
|
|
|
|
+ recordOutboundFailure,
|
|
|
|
|
+ recordOutboundSuccess,
|
|
|
|
|
+ shouldEmitOutboundFailureAlert,
|
|
|
|
|
+ shouldSkipDuplicateSend,
|
|
|
|
|
+} from "./outbound-observability.js";
|
|
|
|
|
|
|
|
export type MonitorOpts = {
|
|
export type MonitorOpts = {
|
|
|
cfg: OpenClawConfig;
|
|
cfg: OpenClawConfig;
|
|
@@ -73,6 +100,10 @@ const SEEN_MAX = 2000;
|
|
|
const SEEN_TRIM_TO = 1000;
|
|
const SEEN_TRIM_TO = 1000;
|
|
|
const DEFAULT_GROUP_KEYWORDS = ["帮我", "请问"];
|
|
const DEFAULT_GROUP_KEYWORDS = ["帮我", "请问"];
|
|
|
const DEFAULT_GROUP_MENTION_TOKENS = ["@bot", "@助手"];
|
|
const DEFAULT_GROUP_MENTION_TOKENS = ["@bot", "@助手"];
|
|
|
|
|
+const POLL_ALERT_MIN_TICKS = 10;
|
|
|
|
|
+const POLL_ALERT_FAIL_RATE = 0.5;
|
|
|
|
|
+const HIGH_THROUGHPUT_LOG_INTERVAL_MS = 300 * 1000;
|
|
|
|
|
+const THROUGHPUT_WINDOW_MS = 5 * 60 * 1000;
|
|
|
|
|
|
|
|
export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
const channelCfg = getChannelCfg(opts.cfg);
|
|
const channelCfg = getChannelCfg(opts.cfg);
|
|
@@ -95,10 +126,19 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
const client = createClient(apiBase);
|
|
const client = createClient(apiBase);
|
|
|
const pollIntervalMs = Math.max(
|
|
const pollIntervalMs = Math.max(
|
|
|
POLL_MIN_INTERVAL_MS,
|
|
POLL_MIN_INTERVAL_MS,
|
|
|
- channelCfg.pollIntervalMs ?? 10000
|
|
|
|
|
|
|
+ resolvePollIntervalMs(channelCfg) ?? 10000
|
|
|
|
|
+ );
|
|
|
|
|
+ const batchWindowMs = Math.max(0, resolveBatchWindowMs(channelCfg) ?? 3000);
|
|
|
|
|
+ const replyCooldownMs = Math.max(0, (resolveReplyCooldownSecPerWxid(channelCfg) ?? 5) * 1000);
|
|
|
|
|
+ configureOutboundQueue(
|
|
|
|
|
+ {
|
|
|
|
|
+ ratePerSec: resolveOutboundRatePerSec(channelCfg),
|
|
|
|
|
+ maxQueue: resolveOutboundQueueMax(channelCfg),
|
|
|
|
|
+ },
|
|
|
|
|
+ log
|
|
|
);
|
|
);
|
|
|
- const batchWindowMs = Math.max(0, channelCfg.batchWindowMs ?? 3000);
|
|
|
|
|
- const replyCooldownMs = Math.max(0, (channelCfg.replyCooldownSecPerWxid ?? 5) * 1000);
|
|
|
|
|
|
|
+ const baseMetricsLogIntervalMs = Math.max(30 * 1000, resolveMetricsLogIntervalSec(channelCfg) * 1000);
|
|
|
|
|
+ const highThroughputAttemptsThreshold = resolveHighThroughputOutboundAttemptsPer5Min(channelCfg);
|
|
|
|
|
|
|
|
// Persistent inbound cursor: best-effort resume after restarts.
|
|
// Persistent inbound cursor: best-effort resume after restarts.
|
|
|
// We store it alongside OpenClaw's session store so admins can back it up
|
|
// We store it alongside OpenClaw's session store so admins can back it up
|
|
@@ -114,7 +154,7 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
let cursor: InboundCursor | null = await loadCursor(cursorPath);
|
|
let cursor: InboundCursor | null = await loadCursor(cursorPath);
|
|
|
|
|
|
|
|
// Resolve bot's own wxid so we can filter self-echoes.
|
|
// Resolve bot's own wxid so we can filter self-echoes.
|
|
|
- let selfWxid: string | null = channelCfg.selfWxid?.trim() || null;
|
|
|
|
|
|
|
+ let selfWxid: string | null = resolveSelfWxid(channelCfg)?.trim() || null;
|
|
|
if (!selfWxid) {
|
|
if (!selfWxid) {
|
|
|
try {
|
|
try {
|
|
|
selfWxid = await client.getSelfWxid();
|
|
selfWxid = await client.getSelfWxid();
|
|
@@ -143,6 +183,10 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
outboundCount: 0,
|
|
outboundCount: 0,
|
|
|
faqHits: 0,
|
|
faqHits: 0,
|
|
|
};
|
|
};
|
|
|
|
|
+ let metricsLastLogAt = Date.now();
|
|
|
|
|
+ let throughputWindowStartAt = Date.now();
|
|
|
|
|
+ let throughputWindowLastAttempts = 0;
|
|
|
|
|
+ let adaptiveMetricsLogIntervalMs = baseMetricsLogIntervalMs;
|
|
|
|
|
|
|
|
// Message dedupe window
|
|
// Message dedupe window
|
|
|
const seenMsgIds = new Set<string>();
|
|
const seenMsgIds = new Set<string>();
|
|
@@ -203,8 +247,11 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
direction: "received",
|
|
direction: "received",
|
|
|
limit: 50,
|
|
limit: 50,
|
|
|
signal: opts.abortSignal,
|
|
signal: opts.abortSignal,
|
|
|
|
|
+ sinceTimestampMs: cursor?.lastTimestampMs,
|
|
|
|
|
+ sinceMsgId: cursor?.lastMsgId,
|
|
|
});
|
|
});
|
|
|
counters.pollsOk++;
|
|
counters.pollsOk++;
|
|
|
|
|
+ maybeEmitHealthLogs();
|
|
|
|
|
|
|
|
// Always process in stable order so agent context is consistent.
|
|
// Always process in stable order so agent context is consistent.
|
|
|
messages.sort((a, b) => {
|
|
messages.sort((a, b) => {
|
|
@@ -235,7 +282,7 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
opts.patchStatus({ lastInboundAt: Date.now() });
|
|
opts.patchStatus({ lastInboundAt: Date.now() });
|
|
|
|
|
|
|
|
// FAQ fast-path
|
|
// FAQ fast-path
|
|
|
- const match = matchFaq(m.content, channelCfg.faq);
|
|
|
|
|
|
|
+ const match = matchFaq(m.content, resolveFaq(channelCfg));
|
|
|
if (match) {
|
|
if (match) {
|
|
|
counters.faqHits++;
|
|
counters.faqHits++;
|
|
|
log.info(
|
|
log.info(
|
|
@@ -271,11 +318,52 @@ export async function startMonitor(opts: MonitorOpts): Promise<MonitorResult> {
|
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
|
opts.patchStatus({ lastError: msg });
|
|
opts.patchStatus({ lastError: msg });
|
|
|
log.warn(`poll error: ${msg}`);
|
|
log.warn(`poll error: ${msg}`);
|
|
|
|
|
+ maybeEmitHealthLogs();
|
|
|
} finally {
|
|
} finally {
|
|
|
pollInFlight = false;
|
|
pollInFlight = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const maybeEmitHealthLogs = () => {
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ const ticks = counters.pollsOk + counters.pollsFail;
|
|
|
|
|
+ const outboundNow = getOutboundMetricsSnapshot();
|
|
|
|
|
+ if (now - throughputWindowStartAt >= THROUGHPUT_WINDOW_MS) {
|
|
|
|
|
+ const attemptsInWindow = outboundNow.attempts - throughputWindowLastAttempts;
|
|
|
|
|
+ throughputWindowStartAt = now;
|
|
|
|
|
+ throughputWindowLastAttempts = outboundNow.attempts;
|
|
|
|
|
+ const nextInterval =
|
|
|
|
|
+ attemptsInWindow >= highThroughputAttemptsThreshold
|
|
|
|
|
+ ? Math.max(baseMetricsLogIntervalMs, HIGH_THROUGHPUT_LOG_INTERVAL_MS)
|
|
|
|
|
+ : baseMetricsLogIntervalMs;
|
|
|
|
|
+ if (nextInterval !== adaptiveMetricsLogIntervalMs) {
|
|
|
|
|
+ adaptiveMetricsLogIntervalMs = nextInterval;
|
|
|
|
|
+ log.info(
|
|
|
|
|
+ `metrics interval adjusted to ${Math.round(adaptiveMetricsLogIntervalMs / 1000)}s (outboundAttempts5m=${attemptsInWindow}, threshold=${highThroughputAttemptsThreshold})`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (now - metricsLastLogAt >= adaptiveMetricsLogIntervalMs) {
|
|
|
|
|
+ metricsLastLogAt = now;
|
|
|
|
|
+ log.info(
|
|
|
|
|
+ `metrics pollsOk=${counters.pollsOk} pollsFail=${counters.pollsFail} inbound=${counters.inboundCount} outbound=${counters.outboundCount} faqHits=${counters.faqHits} outAttempts=${outboundNow.attempts} outSuccess=${outboundNow.success} outFailed=${outboundNow.failed} outDeduped=${outboundNow.deduped} outAvgLatencyMs=${outboundNow.avgLatencyMs} metricsIntervalSec=${Math.round(adaptiveMetricsLogIntervalMs / 1000)}`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ticks >= POLL_ALERT_MIN_TICKS) {
|
|
|
|
|
+ const failRate = counters.pollsFail / ticks;
|
|
|
|
|
+ if (failRate >= POLL_ALERT_FAIL_RATE) {
|
|
|
|
|
+ log.warn(
|
|
|
|
|
+ `[ALERT] high poll failure rate fail=${counters.pollsFail} total=${ticks} ratio=${failRate.toFixed(2)}`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (shouldEmitOutboundFailureAlert()) {
|
|
|
|
|
+ log.warn(
|
|
|
|
|
+ `[ALERT] outbound failure rate high attempts=${outboundNow.attempts} failed=${outboundNow.failed} ratio=${outboundNow.failRate.toFixed(2)}`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const timer = setInterval(() => {
|
|
const timer = setInterval(() => {
|
|
|
tick().catch((err) => log.error(`tick crashed: ${String(err)}`));
|
|
tick().catch((err) => log.error(`tick crashed: ${String(err)}`));
|
|
|
}, pollIntervalMs);
|
|
}, pollIntervalMs);
|
|
@@ -460,32 +548,46 @@ async function dispatchBatchToAgent(params: DispatchParams): Promise<void> {
|
|
|
|
|
|
|
|
for (const chunk of parts) {
|
|
for (const chunk of parts) {
|
|
|
if (!chunk) continue;
|
|
if (!chunk) continue;
|
|
|
- const result = await withRetry(
|
|
|
|
|
- () =>
|
|
|
|
|
- client.sendText({
|
|
|
|
|
- toWxid: conversationId,
|
|
|
|
|
- content: chunk,
|
|
|
|
|
- ats:
|
|
|
|
|
- isGroup && channelCfg.groupReplyAtSender && senderWxid ? senderWxid : "",
|
|
|
|
|
- }),
|
|
|
|
|
- {
|
|
|
|
|
- tries: 4,
|
|
|
|
|
- baseDelayMs: 500,
|
|
|
|
|
- maxDelayMs: 4000,
|
|
|
|
|
- isRetryable: (err) => {
|
|
|
|
|
- const msg = err instanceof Error ? err.message : String(err);
|
|
|
|
|
- if (/timeout|aborted|ECONN|ENOTFOUND|EAI_AGAIN/i.test(msg)) return true;
|
|
|
|
|
- if (/HTTP 5\d\d/i.test(msg)) return true;
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ if (shouldSkipDuplicateSend("agent-reply", conversationId, chunk)) {
|
|
|
|
|
+ log.warn(`skip duplicate agent reply conversation=${conversationId}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const startedAt = Date.now();
|
|
|
|
|
+ recordOutboundAttempt("agent-reply");
|
|
|
|
|
+ try {
|
|
|
|
|
+ await enqueueOutboundSend(`agent-reply:${conversationId}`, () =>
|
|
|
|
|
+ withRetry(
|
|
|
|
|
+ async () => {
|
|
|
|
|
+ const res = await client.sendText({
|
|
|
|
|
+ toWxid: conversationId,
|
|
|
|
|
+ content: chunk,
|
|
|
|
|
+ ats:
|
|
|
|
|
+ isGroup && resolveGroupReplyAtSender(channelCfg) && senderWxid
|
|
|
|
|
+ ? senderWxid
|
|
|
|
|
+ : "",
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ throw new Error(res.msg ?? `ret=${String(res.ret)}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return res;
|
|
|
},
|
|
},
|
|
|
- }
|
|
|
|
|
- );
|
|
|
|
|
- if (!result.ok) {
|
|
|
|
|
- log.error(`send-text failed ret=${String(result.ret)} msg=${result.msg ?? ""}`);
|
|
|
|
|
- throw new Error(
|
|
|
|
|
- `send-text failed: ${result.msg ?? `ret=${String(result.ret)}`}`
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ {
|
|
|
|
|
+ tries: 4,
|
|
|
|
|
+ baseDelayMs: 500,
|
|
|
|
|
+ maxDelayMs: 4000,
|
|
|
|
|
+ isRetryable: (err) => {
|
|
|
|
|
+ const msg = err instanceof Error ? err.message : String(err);
|
|
|
|
|
+ if (/timeout|aborted|ECONN|ENOTFOUND|EAI_AGAIN/i.test(msg)) return true;
|
|
|
|
|
+ if (/HTTP 5\d\d/i.test(msg)) return true;
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ ));
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ recordOutboundFailure("agent-reply");
|
|
|
|
|
+ throw err;
|
|
|
}
|
|
}
|
|
|
|
|
+ recordOutboundSuccess("agent-reply", conversationId, chunk, Date.now() - startedAt);
|
|
|
}
|
|
}
|
|
|
log.info(`delivered reply to ${conversationId} (${text.length} chars)`);
|
|
log.info(`delivered reply to ${conversationId} (${text.length} chars)`);
|
|
|
onOutboundSent();
|
|
onOutboundSent();
|
|
@@ -521,14 +623,25 @@ async function sendFaqReply(
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
const conversationId = resolveConversationId(m);
|
|
const conversationId = resolveConversationId(m);
|
|
|
const senderWxid = resolveSenderWxid(m);
|
|
const senderWxid = resolveSenderWxid(m);
|
|
|
- const result = await client.sendText({
|
|
|
|
|
- toWxid: conversationId,
|
|
|
|
|
- content: reply,
|
|
|
|
|
- ats: m.isGroup && cfg.groupReplyAtSender && senderWxid ? senderWxid : "",
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (shouldSkipDuplicateSend("faq", conversationId, reply)) {
|
|
|
|
|
+ log.warn(`skip duplicate faq reply to ${conversationId}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ recordOutboundAttempt("faq");
|
|
|
|
|
+ const startedAt = Date.now();
|
|
|
|
|
+ const result = await enqueueOutboundSend(`faq:${conversationId}`, () =>
|
|
|
|
|
+ client.sendText({
|
|
|
|
|
+ toWxid: conversationId,
|
|
|
|
|
+ content: reply,
|
|
|
|
|
+ ats:
|
|
|
|
|
+ m.isGroup && resolveGroupReplyAtSender(cfg) && senderWxid ? senderWxid : "",
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
if (result.ok) {
|
|
if (result.ok) {
|
|
|
|
|
+ recordOutboundSuccess("faq", conversationId, reply, Date.now() - startedAt);
|
|
|
log.info(`faq reply sent to ${conversationId}`);
|
|
log.info(`faq reply sent to ${conversationId}`);
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ recordOutboundFailure("faq");
|
|
|
log.warn(
|
|
log.warn(
|
|
|
`faq reply failed to ${conversationId}: ret=${String(result.ret)} msg=${result.msg ?? ""}`
|
|
`faq reply failed to ${conversationId}: ret=${String(result.ret)} msg=${result.msg ?? ""}`
|
|
|
);
|
|
);
|
|
@@ -547,23 +660,23 @@ function shouldIgnoreMessage(
|
|
|
if (selfWxid && m.chatroomMemberWxid === selfWxid) return "self-group-echo";
|
|
if (selfWxid && m.chatroomMemberWxid === selfWxid) return "self-group-echo";
|
|
|
|
|
|
|
|
// Group switch
|
|
// Group switch
|
|
|
- if (m.isGroup && cfg.groupEnabled === false) return "group-disabled";
|
|
|
|
|
|
|
+ if (m.isGroup && resolveGroupEnabled(cfg) === false) return "group-disabled";
|
|
|
|
|
|
|
|
// Type filter
|
|
// Type filter
|
|
|
- const ignoreTypes = cfg.ignoreMessageTypes ?? [];
|
|
|
|
|
|
|
+ const ignoreTypes = resolveIgnoreMessageTypes(cfg) ?? [];
|
|
|
if (ignoreTypes.includes(m.type)) return `ignored-type:${m.type}`;
|
|
if (ignoreTypes.includes(m.type)) return `ignored-type:${m.type}`;
|
|
|
|
|
|
|
|
// v0.2: inbound still text-only
|
|
// v0.2: inbound still text-only
|
|
|
if (m.type !== "text") return "non-text";
|
|
if (m.type !== "text") return "non-text";
|
|
|
|
|
|
|
|
// Wxid prefix filter (official accounts etc.)
|
|
// Wxid prefix filter (official accounts etc.)
|
|
|
- const prefixes = cfg.ignoreWxidPrefixes ?? [];
|
|
|
|
|
|
|
+ const prefixes = resolveIgnoreWxidPrefixes(cfg) ?? [];
|
|
|
for (const p of prefixes) {
|
|
for (const p of prefixes) {
|
|
|
if (p && m.fromWxid.startsWith(p)) return `ignored-prefix:${p}`;
|
|
if (p && m.fromWxid.startsWith(p)) return `ignored-prefix:${p}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Exact wxid filter (system contacts)
|
|
// Exact wxid filter (system contacts)
|
|
|
- const exacts = cfg.ignoreWxidExact ?? [];
|
|
|
|
|
|
|
+ const exacts = resolveIgnoreWxidExact(cfg) ?? [];
|
|
|
if (exacts.includes(m.fromWxid)) return `ignored-wxid:${m.fromWxid}`;
|
|
if (exacts.includes(m.fromWxid)) return `ignored-wxid:${m.fromWxid}`;
|
|
|
|
|
|
|
|
// Empty content
|
|
// Empty content
|
|
@@ -573,10 +686,10 @@ function shouldIgnoreMessage(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function passesDmPolicy(m: RawWechatMessage, cfg: WechatAgentConfig): boolean {
|
|
function passesDmPolicy(m: RawWechatMessage, cfg: WechatAgentConfig): boolean {
|
|
|
- const dmPolicy = cfg.dmPolicy ?? "open";
|
|
|
|
|
|
|
+ const dmPolicy = resolveDmPolicy(cfg) ?? "open";
|
|
|
if (dmPolicy === "open") return true;
|
|
if (dmPolicy === "open") return true;
|
|
|
|
|
|
|
|
- const allowFrom = normalizeAllowFrom(cfg.allowFrom);
|
|
|
|
|
|
|
+ const allowFrom = normalizeAllowFrom(resolveAllowFrom(cfg));
|
|
|
if (allowFrom.includes(m.fromWxid.toLowerCase())) return true;
|
|
if (allowFrom.includes(m.fromWxid.toLowerCase())) return true;
|
|
|
|
|
|
|
|
// pairing is handled upstream in core; here we treat it like allowlist.
|
|
// pairing is handled upstream in core; here we treat it like allowlist.
|
|
@@ -603,13 +716,15 @@ function isGroupTriggered(
|
|
|
): boolean {
|
|
): boolean {
|
|
|
if (!m.isGroup) return true;
|
|
if (!m.isGroup) return true;
|
|
|
const content = m.content.toLowerCase();
|
|
const content = m.content.toLowerCase();
|
|
|
- const keywords = (cfg.groupKeywords?.length ? cfg.groupKeywords : DEFAULT_GROUP_KEYWORDS)
|
|
|
|
|
|
|
+ const groupKeywords = resolveGroupKeywords(cfg);
|
|
|
|
|
+ const keywords = (groupKeywords?.length ? groupKeywords : DEFAULT_GROUP_KEYWORDS)
|
|
|
.map((s) => s.trim().toLowerCase())
|
|
.map((s) => s.trim().toLowerCase())
|
|
|
.filter(Boolean);
|
|
.filter(Boolean);
|
|
|
if (keywords.some((kw) => content.includes(kw))) return true;
|
|
if (keywords.some((kw) => content.includes(kw))) return true;
|
|
|
|
|
|
|
|
|
|
+ const groupMentionTokens = resolveGroupMentionTokens(cfg);
|
|
|
const mentionTokens = (
|
|
const mentionTokens = (
|
|
|
- cfg.groupMentionTokens?.length ? cfg.groupMentionTokens : DEFAULT_GROUP_MENTION_TOKENS
|
|
|
|
|
|
|
+ groupMentionTokens?.length ? groupMentionTokens : DEFAULT_GROUP_MENTION_TOKENS
|
|
|
)
|
|
)
|
|
|
.map((s) => s.trim().toLowerCase())
|
|
.map((s) => s.trim().toLowerCase())
|
|
|
.filter(Boolean);
|
|
.filter(Boolean);
|