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, '\\$&');
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TRIGGER_PATTERN = new RegExp(
|
export function buildTriggerPattern(trigger: string): RegExp {
|
||||||
`^@${escapeRegex(ASSISTANT_NAME)}\\b`,
|
return new RegExp(`^${escapeRegex(trigger.trim())}\\b`, 'i');
|
||||||
'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.
|
// Timezone for scheduled tasks, message formatting, etc.
|
||||||
// Validates each candidate is a real IANA identifier before accepting.
|
// Validates each candidate is a real IANA identifier before accepting.
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import { ASSISTANT_NAME, TRIGGER_PATTERN } from './config.js';
|
import {
|
||||||
|
ASSISTANT_NAME,
|
||||||
|
getTriggerPattern,
|
||||||
|
TRIGGER_PATTERN,
|
||||||
|
} from './config.js';
|
||||||
import {
|
import {
|
||||||
escapeXml,
|
escapeXml,
|
||||||
formatMessages,
|
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) ---
|
// --- Outbound formatting (internal tag stripping + prefix) ---
|
||||||
|
|
||||||
describe('stripInternalTags', () => {
|
describe('stripInternalTags', () => {
|
||||||
@@ -207,7 +233,7 @@ describe('formatOutbound', () => {
|
|||||||
|
|
||||||
describe('trigger gating (requiresTrigger interaction)', () => {
|
describe('trigger gating (requiresTrigger interaction)', () => {
|
||||||
// Replicates the exact logic from processGroupMessages and startMessageLoop:
|
// 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(
|
function shouldRequireTrigger(
|
||||||
isMainGroup: boolean,
|
isMainGroup: boolean,
|
||||||
requiresTrigger: boolean | undefined,
|
requiresTrigger: boolean | undefined,
|
||||||
@@ -218,39 +244,51 @@ describe('trigger gating (requiresTrigger interaction)', () => {
|
|||||||
function shouldProcess(
|
function shouldProcess(
|
||||||
isMainGroup: boolean,
|
isMainGroup: boolean,
|
||||||
requiresTrigger: boolean | undefined,
|
requiresTrigger: boolean | undefined,
|
||||||
|
trigger: string | undefined,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!shouldRequireTrigger(isMainGroup, requiresTrigger)) return true;
|
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)', () => {
|
it('main group always processes (no trigger needed)', () => {
|
||||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
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', () => {
|
it('main group processes even with requiresTrigger=true', () => {
|
||||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
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)', () => {
|
it('non-main group with requiresTrigger=undefined requires trigger (defaults to true)', () => {
|
||||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
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', () => {
|
it('non-main group with requiresTrigger=true requires trigger', () => {
|
||||||
const msgs = [makeMsg({ content: 'hello no 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', () => {
|
it('non-main group with requiresTrigger=true processes when trigger present', () => {
|
||||||
const msgs = [makeMsg({ content: `@${ASSISTANT_NAME} do something` })];
|
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)', () => {
|
it('non-main group with requiresTrigger=false always processes (no trigger needed)', () => {
|
||||||
const msgs = [makeMsg({ content: 'hello no trigger' })];
|
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 {
|
import {
|
||||||
ASSISTANT_NAME,
|
ASSISTANT_NAME,
|
||||||
|
DEFAULT_TRIGGER,
|
||||||
|
getTriggerPattern,
|
||||||
GROUPS_DIR,
|
GROUPS_DIR,
|
||||||
IDLE_TIMEOUT,
|
IDLE_TIMEOUT,
|
||||||
ONECLI_URL,
|
ONECLI_URL,
|
||||||
POLL_INTERVAL,
|
POLL_INTERVAL,
|
||||||
TIMEZONE,
|
TIMEZONE,
|
||||||
TRIGGER_PATTERN,
|
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import './channels/index.js';
|
import './channels/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -215,10 +216,11 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
|
|||||||
|
|
||||||
// For non-main groups, check if trigger is required and present
|
// For non-main groups, check if trigger is required and present
|
||||||
if (!isMainGroup && group.requiresTrigger !== false) {
|
if (!isMainGroup && group.requiresTrigger !== false) {
|
||||||
|
const triggerPattern = getTriggerPattern(group.trigger);
|
||||||
const allowlistCfg = loadSenderAllowlist();
|
const allowlistCfg = loadSenderAllowlist();
|
||||||
const hasTrigger = missedMessages.some(
|
const hasTrigger = missedMessages.some(
|
||||||
(m) =>
|
(m) =>
|
||||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
triggerPattern.test(m.content.trim()) &&
|
||||||
(m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
(m.is_from_me || isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||||
);
|
);
|
||||||
if (!hasTrigger) return true;
|
if (!hasTrigger) return true;
|
||||||
@@ -397,7 +399,7 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
}
|
}
|
||||||
messageLoopRunning = true;
|
messageLoopRunning = true;
|
||||||
|
|
||||||
logger.info(`NanoClaw running (trigger: @${ASSISTANT_NAME})`);
|
logger.info(`NanoClaw running (default trigger: ${DEFAULT_TRIGGER})`);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@@ -443,10 +445,11 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
// Non-trigger messages accumulate in DB and get pulled as
|
// Non-trigger messages accumulate in DB and get pulled as
|
||||||
// context when a trigger eventually arrives.
|
// context when a trigger eventually arrives.
|
||||||
if (needsTrigger) {
|
if (needsTrigger) {
|
||||||
|
const triggerPattern = getTriggerPattern(group.trigger);
|
||||||
const allowlistCfg = loadSenderAllowlist();
|
const allowlistCfg = loadSenderAllowlist();
|
||||||
const hasTrigger = groupMessages.some(
|
const hasTrigger = groupMessages.some(
|
||||||
(m) =>
|
(m) =>
|
||||||
TRIGGER_PATTERN.test(m.content.trim()) &&
|
triggerPattern.test(m.content.trim()) &&
|
||||||
(m.is_from_me ||
|
(m.is_from_me ||
|
||||||
isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
isTriggerAllowed(chatJid, m.sender, allowlistCfg)),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user