Merge pull request #1402 from mrbob-git/codex/fix-issue-1141-per-group-trigger
Fix per-group trigger_pattern matching
This commit is contained in:
@@ -64,10 +64,18 @@ function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export const TRIGGER_PATTERN = new RegExp(
|
||||
`^@${escapeRegex(ASSISTANT_NAME)}\\b`,
|
||||
'i',
|
||||
);
|
||||
export function buildTriggerPattern(trigger: string): RegExp {
|
||||
return new RegExp(`^${escapeRegex(trigger.trim())}\\b`, 'i');
|
||||
}
|
||||
|
||||
export const DEFAULT_TRIGGER = `@${ASSISTANT_NAME}`;
|
||||
|
||||
export function getTriggerPattern(trigger?: string): RegExp {
|
||||
const normalizedTrigger = trigger?.trim();
|
||||
return buildTriggerPattern(normalizedTrigger || DEFAULT_TRIGGER);
|
||||
}
|
||||
|
||||
export const TRIGGER_PATTERN = buildTriggerPattern(DEFAULT_TRIGGER);
|
||||
|
||||
// Timezone for scheduled tasks, message formatting, etc.
|
||||
// Validates each candidate is a real IANA identifier before accepting.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { ASSISTANT_NAME, TRIGGER_PATTERN } from './config.js';
|
||||
import {
|
||||
ASSISTANT_NAME,
|
||||
getTriggerPattern,
|
||||
TRIGGER_PATTERN,
|
||||
} from './config.js';
|
||||
import {
|
||||
escapeXml,
|
||||
formatMessages,
|
||||
@@ -161,6 +165,28 @@ describe('TRIGGER_PATTERN', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTriggerPattern', () => {
|
||||
it('uses the configured per-group trigger when provided', () => {
|
||||
const pattern = getTriggerPattern('@Claw');
|
||||
|
||||
expect(pattern.test('@Claw hello')).toBe(true);
|
||||
expect(pattern.test(`@${ASSISTANT_NAME} hello`)).toBe(false);
|
||||
});
|
||||
|
||||
it('falls back to the default trigger when group trigger is missing', () => {
|
||||
const pattern = getTriggerPattern(undefined);
|
||||
|
||||
expect(pattern.test(`@${ASSISTANT_NAME} hello`)).toBe(true);
|
||||
});
|
||||
|
||||
it('treats regex characters in custom triggers literally', () => {
|
||||
const pattern = getTriggerPattern('@C.L.A.U.D.E');
|
||||
|
||||
expect(pattern.test('@C.L.A.U.D.E hello')).toBe(true);
|
||||
expect(pattern.test('@CXLXAUXDXE hello')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Outbound formatting (internal tag stripping + prefix) ---
|
||||
|
||||
describe('stripInternalTags', () => {
|
||||
@@ -207,7 +233,7 @@ describe('formatOutbound', () => {
|
||||
|
||||
describe('trigger gating (requiresTrigger interaction)', () => {
|
||||
// Replicates the exact logic from processGroupMessages and startMessageLoop:
|
||||
// if (!isMainGroup && group.requiresTrigger !== false) { check trigger }
|
||||
// if (!isMainGroup && group.requiresTrigger !== false) { check group.trigger }
|
||||
function shouldRequireTrigger(
|
||||
isMainGroup: boolean,
|
||||
requiresTrigger: boolean | undefined,
|
||||
@@ -218,39 +244,51 @@ describe('trigger gating (requiresTrigger interaction)', () => {
|
||||
function shouldProcess(
|
||||
isMainGroup: boolean,
|
||||
requiresTrigger: boolean | undefined,
|
||||
trigger: string | undefined,
|
||||
messages: NewMessage[],
|
||||
): boolean {
|
||||
if (!shouldRequireTrigger(isMainGroup, requiresTrigger)) return true;
|
||||
return messages.some((m) => TRIGGER_PATTERN.test(m.content.trim()));
|
||||
const triggerPattern = getTriggerPattern(trigger);
|
||||
return messages.some((m) => triggerPattern.test(m.content.trim()));
|
||||
}
|
||||
|
||||
it('main group always processes (no trigger needed)', () => {
|
||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
||||
expect(shouldProcess(true, undefined, msgs)).toBe(true);
|
||||
expect(shouldProcess(true, undefined, undefined, msgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('main group processes even with requiresTrigger=true', () => {
|
||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
||||
expect(shouldProcess(true, true, msgs)).toBe(true);
|
||||
expect(shouldProcess(true, true, undefined, msgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('non-main group with requiresTrigger=undefined requires trigger (defaults to true)', () => {
|
||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
||||
expect(shouldProcess(false, undefined, msgs)).toBe(false);
|
||||
expect(shouldProcess(false, undefined, undefined, msgs)).toBe(false);
|
||||
});
|
||||
|
||||
it('non-main group with requiresTrigger=true requires trigger', () => {
|
||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
||||
expect(shouldProcess(false, true, msgs)).toBe(false);
|
||||
expect(shouldProcess(false, true, undefined, msgs)).toBe(false);
|
||||
});
|
||||
|
||||
it('non-main group with requiresTrigger=true processes when trigger present', () => {
|
||||
const msgs = [makeMsg({ content: `@${ASSISTANT_NAME} do something` })];
|
||||
expect(shouldProcess(false, true, msgs)).toBe(true);
|
||||
expect(shouldProcess(false, true, undefined, msgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('non-main group uses its per-group trigger instead of the default trigger', () => {
|
||||
const msgs = [makeMsg({ content: '@Claw do something' })];
|
||||
expect(shouldProcess(false, true, '@Claw', msgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('non-main group does not process when only the default trigger is present for a custom-trigger group', () => {
|
||||
const msgs = [makeMsg({ content: `@${ASSISTANT_NAME} do something` })];
|
||||
expect(shouldProcess(false, true, '@Claw', msgs)).toBe(false);
|
||||
});
|
||||
|
||||
it('non-main group with requiresTrigger=false always processes (no trigger needed)', () => {
|
||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
||||
expect(shouldProcess(false, false, msgs)).toBe(true);
|
||||
expect(shouldProcess(false, false, undefined, msgs)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
11
src/index.ts
11
src/index.ts
@@ -5,12 +5,13 @@ import { OneCLI } from '@onecli-sh/sdk';
|
||||
|
||||
import {
|
||||
ASSISTANT_NAME,
|
||||
DEFAULT_TRIGGER,
|
||||
getTriggerPattern,
|
||||
GROUPS_DIR,
|
||||
IDLE_TIMEOUT,
|
||||
ONECLI_URL,
|
||||
POLL_INTERVAL,
|
||||
TIMEZONE,
|
||||
TRIGGER_PATTERN,
|
||||
} from './config.js';
|
||||
import './channels/index.js';
|
||||
import {
|
||||
@@ -215,10 +216,11 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
||||
|
||||
// For non-main groups, check if trigger is required and present
|
||||
if (!isMainGroup && group.requiresTrigger !== false) {
|
||||
const triggerPattern = getTriggerPattern(group.trigger);
|
||||
const allowlistCfg = loadSenderAllowlist();
|
||||
const hasTrigger = missedMessages.some(
|
||||
(m) =>
|
||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||
triggerPattern.test(m.content.trim()) &&
|
||||
(m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||
);
|
||||
if (!hasTrigger) return true;
|
||||
@@ -397,7 +399,7 @@ async function startMessageLoop(): Promise<void> {
|
||||
}
|
||||
messageLoopRunning = true;
|
||||
|
||||
logger.info(`NanoClaw running (trigger: @${ASSISTANT_NAME})`);
|
||||
logger.info(`NanoClaw running (default trigger: ${DEFAULT_TRIGGER})`);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -443,10 +445,11 @@ async function startMessageLoop(): Promise<void> {
|
||||
// Non-trigger messages accumulate in DB and get pulled as
|
||||
// context when a trigger eventually arrives.
|
||||
if (needsTrigger) {
|
||||
const triggerPattern = getTriggerPattern(group.trigger);
|
||||
const allowlistCfg = loadSenderAllowlist();
|
||||
const hasTrigger = groupMessages.some(
|
||||
(m) =>
|
||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
||||
triggerPattern.test(m.content.trim()) &&
|
||||
(m.is_from_me ||
|
||||
isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user