diff --git a/web/src/app/home/bots/components/bot-form/PluginProcessorBindings.tsx b/web/src/app/home/bots/components/bot-form/PluginProcessorBindings.tsx
index 23de2ab18..71a89c36b 100644
--- a/web/src/app/home/bots/components/bot-form/PluginProcessorBindings.tsx
+++ b/web/src/app/home/bots/components/bot-form/PluginProcessorBindings.tsx
@@ -19,8 +19,8 @@ import type {
import { httpClient } from '@/app/infra/http';
import { extractI18nObject } from '@/i18n/I18nProvider';
import {
- eventPatternCovers,
eventPatternLabel,
+ processorEventCompatibility,
} from '@/app/home/components/event-patterns/event-pattern-groups';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { AuthenticatedPluginIcon } from '@/components/AuthenticatedPluginIcon';
@@ -45,7 +45,7 @@ import {
} from '@/components/ui/dialog';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
-function EventCompatibilityWarning({
+function ProcessorEvents({
patterns = [],
supportedEvents,
}: {
@@ -53,28 +53,45 @@ function EventCompatibilityWarning({
supportedEvents: string[];
}) {
const { t } = useTranslation();
- // Legacy adapters only emit message.received when no events are declared.
- const supported = supportedEvents.length
- ? supportedEvents
- : ['message.received'];
- const unsupported = [...new Set(patterns)].filter(
- (pattern) => !supported.some((event) => eventPatternCovers(event, pattern)),
+ const { entries, matchingEvents } = processorEventCompatibility(
+ patterns,
+ supportedEvents,
);
- if (!unsupported.length) return null;
+ const incomplete = entries.some((entry) => !entry.supported);
return (
-
-
-
- {t('bots.pluginSubscriptions.incompleteEvents', {
- events: unsupported
- .map((pattern) => eventPatternLabel(pattern, t))
- .join(' · '),
- })}
+
+
+ {entries.map(({ pattern, supported }) => (
+
+ {!supported && (
+
+ )}
+ {eventPatternLabel(pattern, t)}
+
+ ))}
+ {incomplete && (
+
+ {t('bots.pluginSubscriptions.incompleteEvents', {
+ events:
+ matchingEvents
+ .map((pattern) => eventPatternLabel(pattern, t))
+ .join(' · ') || t('common.none'),
+ })}
+
+ )}
);
}
@@ -225,17 +242,7 @@ export default function PluginProcessorBindings({
>
{agent?.component_ref?.replace('plugin:', '')}
- eventPatternLabel(pattern, t))
- .join(' · ')}
- >
- {(agent?.supported_event_patterns ?? [])
- .map((pattern) => eventPatternLabel(pattern, t))
- .join(' · ') || t('agents.eventProcessor.unavailable')}
-
-
@@ -374,12 +381,7 @@ export default function PluginProcessorBindings({
{agent.component_ref?.replace('plugin:', '')}
-
- {(agent.supported_event_patterns ?? [])
- .map((pattern) => eventPatternLabel(pattern, t))
- .join(' · ')}
-
-
@@ -441,12 +443,7 @@ export default function PluginProcessorBindings({
{descriptor.plugin_author}/{descriptor.plugin_name}
-
- {descriptor.supported_event_patterns
- .map((pattern) => eventPatternLabel(pattern, t))
- .join(' · ')}
-
-
diff --git a/web/src/app/home/components/event-patterns/event-pattern-groups.ts b/web/src/app/home/components/event-patterns/event-pattern-groups.ts
index 1780d1ef4..4a7bfcb61 100644
--- a/web/src/app/home/components/event-patterns/event-pattern-groups.ts
+++ b/web/src/app/home/components/event-patterns/event-pattern-groups.ts
@@ -12,6 +12,34 @@ export function eventPatternCovers(supported: string, required: string) {
);
}
+export function processorEventCompatibility(
+ patterns: string[],
+ supportedEvents: string[],
+) {
+ // Legacy adapters only emit message.received when no events are declared.
+ const supported = supportedEvents.length
+ ? supportedEvents
+ : ['message.received'];
+ const entries = [...new Set(patterns)]
+ .map((pattern) => ({
+ pattern,
+ supported: supported.some((event) => eventPatternCovers(event, pattern)),
+ }))
+ .sort((left, right) => Number(right.supported) - Number(left.supported));
+ const matchingEvents = [
+ ...new Set(
+ patterns.flatMap((pattern) =>
+ supported.flatMap((event) => {
+ if (eventPatternCovers(pattern, event)) return [event];
+ if (eventPatternCovers(event, pattern)) return [pattern];
+ return [];
+ }),
+ ),
+ ),
+ ];
+ return { entries, matchingEvents };
+}
+
function eventPatternNamespace(pattern: string) {
if (pattern === '*') return '*';
return pattern.split('.')[0] || pattern;
diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts
index 02b3601cd..bf43a47ab 100644
--- a/web/src/i18n/locales/en-US.ts
+++ b/web/src/i18n/locales/en-US.ts
@@ -357,7 +357,7 @@ const enUS = {
bots: {
pluginSubscriptions: {
incompleteEvents:
- 'This bot cannot fully cover these subscribed events: {{events}}. The processor may work only partially; you can still select and bind it.',
+ 'This bot only supports some of the subscribed events ({{events}}). The processor may not behave as intended in all cases.',
description:
'Automatically receive events declared by the plugin, independently of the routes above.',
empty: 'No plugin processors are bound.',
diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts
index d36774399..9e70cb6e3 100644
--- a/web/src/i18n/locales/es-ES.ts
+++ b/web/src/i18n/locales/es-ES.ts
@@ -363,7 +363,28 @@ const esES = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
+ eventWildcard: 'Todos los eventos',
+ eventNamespaceWildcard: '{{namespace}}.*',
+ eventNames: {
+ message_received: 'Mensaje recibido',
+ message_edited: 'Mensaje editado',
+ message_deleted: 'Mensaje eliminado',
+ message_reaction: 'Reacción a un mensaje',
+ feedback_received: 'Comentarios recibidos',
+ friend_request_received: 'Solicitud de amistad recibida',
+ friend_added: 'Amigo añadido',
+ group_member_joined: 'Miembro unido al grupo',
+ group_member_left: 'Miembro salió del grupo',
+ group_member_banned: 'Miembro bloqueado',
+ bot_invited_to_group: 'Bot invitado al grupo',
+ bot_removed_from_group: 'Bot eliminado del grupo',
+ bot_muted: 'Bot silenciado',
+ bot_unmuted: 'Bot ya no silenciado',
+ platform_specific: 'Evento específico de la plataforma',
+ },
pluginSubscriptions: {
+ incompleteEvents:
+ 'Este bot solo admite algunos de los eventos suscritos ({{events}}). El procesador podría no comportarse como se espera en todos los casos.',
description:
'Recibe automáticamente los eventos declarados por el plugin, de forma independiente de las rutas anteriores.',
empty: 'No hay procesadores vinculados.',
diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts
index 6d511d6ff..2db1fdacf 100644
--- a/web/src/i18n/locales/ja-JP.ts
+++ b/web/src/i18n/locales/ja-JP.ts
@@ -363,7 +363,7 @@ const jaJP = {
bots: {
pluginSubscriptions: {
incompleteEvents:
- 'このボットは次の監視イベントに完全には対応していません:{{events}}。一部の機能が動作しない可能性がありますが、選択して登録できます。',
+ 'このボットは監視対象の一部のイベント({{events}})にのみ対応しています。プロセッサーが完全には動作しない可能性があります。',
description:
'プラグインが宣言したイベントを自動で受信し、上のルートとは独立して実行します。',
empty: 'プラグインプロセッサーは未登録です。',
diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts
index 233ef98c2..69eb0ac43 100644
--- a/web/src/i18n/locales/ru-RU.ts
+++ b/web/src/i18n/locales/ru-RU.ts
@@ -360,7 +360,28 @@ const ruRU = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
+ eventWildcard: 'Все события',
+ eventNamespaceWildcard: '{{namespace}}.*',
+ eventNames: {
+ message_received: 'Получено сообщение',
+ message_edited: 'Сообщение изменено',
+ message_deleted: 'Сообщение удалено',
+ message_reaction: 'Реакция на сообщение',
+ feedback_received: 'Получен отзыв',
+ friend_request_received: 'Получен запрос в друзья',
+ friend_added: 'Друг добавлен',
+ group_member_joined: 'Участник вступил в группу',
+ group_member_left: 'Участник покинул группу',
+ group_member_banned: 'Участник заблокирован',
+ bot_invited_to_group: 'Бот приглашён в группу',
+ bot_removed_from_group: 'Бот удалён из группы',
+ bot_muted: 'Боту запрещено отправлять сообщения',
+ bot_unmuted: 'Боту разрешено отправлять сообщения',
+ platform_specific: 'Событие конкретной платформы',
+ },
pluginSubscriptions: {
+ incompleteEvents:
+ 'Этот бот поддерживает только часть отслеживаемых событий ({{events}}). Обработчик может работать не во всех случаях так, как ожидается.',
description:
'Автоматически получает события, объявленные плагином, независимо от маршрутов выше.',
empty: 'Обработчики плагинов не привязаны.',
diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts
index da7009b62..0ea1bc6f6 100644
--- a/web/src/i18n/locales/th-TH.ts
+++ b/web/src/i18n/locales/th-TH.ts
@@ -347,7 +347,28 @@ const thTH = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
+ eventWildcard: 'ทุกเหตุการณ์',
+ eventNamespaceWildcard: '{{namespace}}.*',
+ eventNames: {
+ message_received: 'ได้รับข้อความ',
+ message_edited: 'แก้ไขข้อความ',
+ message_deleted: 'ลบข้อความ',
+ message_reaction: 'การแสดงความรู้สึกต่อข้อความ',
+ feedback_received: 'ได้รับข้อเสนอแนะ',
+ friend_request_received: 'ได้รับคำขอเป็นเพื่อน',
+ friend_added: 'เพิ่มเพื่อน',
+ group_member_joined: 'สมาชิกเข้าร่วมกลุ่ม',
+ group_member_left: 'สมาชิกออกจากกลุ่ม',
+ group_member_banned: 'สมาชิกถูกแบน',
+ bot_invited_to_group: 'บอตได้รับเชิญเข้ากลุ่ม',
+ bot_removed_from_group: 'บอตถูกนำออกจากกลุ่ม',
+ bot_muted: 'บอตถูกปิดสิทธิ์ส่งข้อความ',
+ bot_unmuted: 'บอตได้รับสิทธิ์ส่งข้อความคืน',
+ platform_specific: 'เหตุการณ์เฉพาะแพลตฟอร์ม',
+ },
pluginSubscriptions: {
+ incompleteEvents:
+ 'บอตนี้รองรับเพียงบางเหตุการณ์ที่ตัวประมวลผลติดตาม ({{events}}) ตัวประมวลผลอาจทำงานได้ไม่ครบถ้วน',
description:
'รับเหตุการณ์ที่ปลั๊กอินประกาศไว้โดยอัตโนมัติ และทำงานแยกจากเส้นทางด้านบน',
empty: 'ยังไม่ได้เชื่อมโยงตัวประมวลผลปลั๊กอิน',
diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts
index 783a35bd4..41739f1ca 100644
--- a/web/src/i18n/locales/vi-VN.ts
+++ b/web/src/i18n/locales/vi-VN.ts
@@ -356,7 +356,28 @@ const viVN = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
+ eventWildcard: 'Tất cả sự kiện',
+ eventNamespaceWildcard: '{{namespace}}.*',
+ eventNames: {
+ message_received: 'Nhận tin nhắn',
+ message_edited: 'Tin nhắn được chỉnh sửa',
+ message_deleted: 'Tin nhắn bị xóa',
+ message_reaction: 'Cảm xúc cho tin nhắn',
+ feedback_received: 'Nhận phản hồi',
+ friend_request_received: 'Nhận lời mời kết bạn',
+ friend_added: 'Đã thêm bạn',
+ group_member_joined: 'Thành viên tham gia nhóm',
+ group_member_left: 'Thành viên rời nhóm',
+ group_member_banned: 'Thành viên bị cấm tham gia nhóm',
+ bot_invited_to_group: 'Bot được mời vào nhóm',
+ bot_removed_from_group: 'Bot bị xóa khỏi nhóm',
+ bot_muted: 'Bot bị cấm gửi tin nhắn',
+ bot_unmuted: 'Bot được phép gửi tin nhắn trở lại',
+ platform_specific: 'Sự kiện riêng của nền tảng',
+ },
pluginSubscriptions: {
+ incompleteEvents:
+ 'Bot hiện chỉ hỗ trợ một số sự kiện mà bộ xử lý theo dõi ({{events}}). Bộ xử lý có thể không hoạt động đầy đủ trong mọi trường hợp.',
description:
'Tự động nhận sự kiện do plugin khai báo, hoạt động độc lập với các tuyến ở trên.',
empty: 'Chưa liên kết bộ xử lý plugin.',
diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts
index 48c255590..1e9dff0bc 100644
--- a/web/src/i18n/locales/zh-Hans.ts
+++ b/web/src/i18n/locales/zh-Hans.ts
@@ -342,7 +342,7 @@ const zhHans = {
bots: {
pluginSubscriptions: {
incompleteEvents:
- '当前机器人无法完整支持以下监听事件:{{events}}。处理器的表现可能不完整。',
+ '当前机器人仅支持部分事件({{events}}),处理器表现可能不完整。',
description: '自动接收插件声明的事件,与上方事件路由独立执行。',
empty: '尚未绑定插件处理器。',
add: '添加插件处理器',
diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts
index b9641cff3..482441363 100644
--- a/web/src/i18n/locales/zh-Hant.ts
+++ b/web/src/i18n/locales/zh-Hant.ts
@@ -336,7 +336,28 @@ const zhHant = {
"Uses the Workspace owner's LangBot Account billing and credits.",
},
bots: {
+ eventWildcard: '全部事件',
+ eventNamespaceWildcard: '{{namespace}}.*',
+ eventNames: {
+ message_received: '收到訊息',
+ message_edited: '訊息已編輯',
+ message_deleted: '訊息已刪除',
+ message_reaction: '訊息表情回應',
+ feedback_received: '收到回饋',
+ friend_request_received: '收到好友請求',
+ friend_added: '新增好友',
+ group_member_joined: '成員加入群組',
+ group_member_left: '成員離開群組',
+ group_member_banned: '成員被封禁',
+ bot_invited_to_group: '機器人被邀請加入群組',
+ bot_removed_from_group: '機器人被移出群組',
+ bot_muted: '機器人被禁言',
+ bot_unmuted: '機器人被解除禁言',
+ platform_specific: '平台特定事件',
+ },
pluginSubscriptions: {
+ incompleteEvents:
+ '目前機器人僅支援部分事件({{events}}),處理器表現可能不完整。',
description: '自動接收外掛宣告的事件,與上方事件路由獨立執行。',
empty: '尚未綁定外掛處理器。',
add: '新增外掛處理器',
diff --git a/web/tests/e2e/bot-processor-compatibility.spec.ts b/web/tests/e2e/bot-processor-compatibility.spec.ts
index 0cb983540..5bae6e43f 100644
--- a/web/tests/e2e/bot-processor-compatibility.spec.ts
+++ b/web/tests/e2e/bot-processor-compatibility.spec.ts
@@ -1,14 +1,14 @@
import { expect, test, type Page } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
-const warning = /This bot cannot fully cover these subscribed events:/;
+const warning = /This bot only supports some of the subscribed events/;
const definitions = [
{ name: 'Messages only', patterns: ['message.received'] },
{
name: 'Community helper',
patterns: [
- 'message.received',
'group.member_joined',
+ 'message.received',
'friend.request_received',
],
},
@@ -102,11 +102,28 @@ test('partial support warns in existing choices and saved bindings, and changes
const community = dialog
.locator('label')
.filter({ hasText: 'Community helper' });
- await expect(community.getByRole('status')).toContainText('Member joined');
- await expect(community.getByRole('status')).toContainText('Friend request');
- await expect(community.getByRole('status')).not.toContainText(
- 'Message received',
+ await expect(community.getByRole('status')).toContainText(
+ '(Message received)',
);
+ await expect(community.getByRole('status')).not.toContainText(
+ 'Member joined',
+ );
+ await expect(community.getByRole('listitem')).toHaveText([
+ 'Message received',
+ 'Member joined group',
+ 'Friend request received',
+ ]);
+ for (const pattern of ['group.member_joined', 'friend.request_received']) {
+ const event = community.getByTitle(pattern, { exact: true });
+ await expect(event).toHaveClass(/text-amber-/);
+ await expect(event.locator('svg')).toHaveCount(1);
+ }
+ await expect(
+ community.getByTitle('message.received', { exact: true }),
+ ).not.toHaveClass(/text-amber-/);
+ await expect(
+ community.getByTitle('message.received', { exact: true }).locator('svg'),
+ ).toHaveCount(0);
await expect(
dialog
.locator('label')
@@ -122,6 +139,15 @@ test('partial support warns in existing choices and saved bindings, and changes
).toHaveCount(1);
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.reload();
+ await expect(
+ page
+ .getByRole('region', { name: 'Plugin processor', exact: true })
+ .getByRole('listitem'),
+ ).toHaveText([
+ 'Message received',
+ 'Member joined group',
+ 'Friend request received',
+ ]);
await expect(
page.getByRole('status').filter({ hasText: warning }),
).toHaveCount(1);
@@ -159,6 +185,21 @@ test('new component choices warn for missing and wildcard events but allow creat
.getByRole('status'),
).toContainText(warning);
}
+ await expect(
+ dialog.getByRole('button', { name: /Group observer/ }).getByRole('status'),
+ ).toContainText('(None)');
+ await expect(
+ dialog.getByRole('button', { name: /All observer/ }).getByRole('status'),
+ ).toContainText('(Message received)');
+ const community = dialog.getByRole('button', { name: /Community helper/ });
+ await expect(community.getByRole('listitem')).toHaveText([
+ 'Message received',
+ 'Member joined group',
+ 'Friend request received',
+ ]);
+ await expect(
+ community.getByTitle('group.member_joined', { exact: true }).locator('svg'),
+ ).toHaveCount(1);
await expect(
dialog.getByRole('button', { name: /Messages only/ }).getByRole('status'),
).toHaveCount(0);
diff --git a/web/tests/unit/event-pattern-coverage.test.mjs b/web/tests/unit/event-pattern-coverage.test.mjs
index b6b0a9dbb..f2e179e35 100644
--- a/web/tests/unit/event-pattern-coverage.test.mjs
+++ b/web/tests/unit/event-pattern-coverage.test.mjs
@@ -14,7 +14,10 @@ const exports = {};
new Function(
'exports',
ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.CommonJS },
+ compilerOptions: {
+ module: ts.ModuleKind.CommonJS,
+ target: ts.ScriptTarget.ES2022,
+ },
}).outputText,
)(exports);
@@ -34,3 +37,56 @@ for (const [supported, required, covered] of [
assert.equal(exports.eventPatternCovers(supported, required), covered);
});
}
+
+test('processor events put covered declarations first and report only the intersection', () => {
+ const result = exports.processorEventCompatibility(
+ [
+ 'group.member_joined',
+ 'message.received',
+ 'friend.request_received',
+ 'message.received',
+ ],
+ ['message.received', 'message.edited'],
+ );
+ assert.deepEqual(result.entries, [
+ { pattern: 'message.received', supported: true },
+ { pattern: 'group.member_joined', supported: false },
+ { pattern: 'friend.request_received', supported: false },
+ ]);
+ assert.deepEqual(result.matchingEvents, ['message.received']);
+});
+
+test('wildcard subscriptions show matching concrete adapter events without claiming full coverage', () => {
+ const result = exports.processorEventCompatibility(
+ ['group.*'],
+ ['group.member_joined', 'message.received'],
+ );
+ assert.deepEqual(result.entries, [{ pattern: 'group.*', supported: false }]);
+ assert.deepEqual(result.matchingEvents, ['group.member_joined']);
+ assert.deepEqual(
+ exports.processorEventCompatibility(['*'], ['message.received'])
+ .matchingEvents,
+ ['message.received'],
+ );
+});
+
+test('adapter wildcards cover concrete subscriptions without expanding the intersection', () => {
+ const result = exports.processorEventCompatibility(
+ ['group.member_joined'],
+ ['group.*'],
+ );
+ assert.equal(result.entries[0].supported, true);
+ assert.deepEqual(result.matchingEvents, ['group.member_joined']);
+});
+
+test('legacy adapters default to messages and unmatched subscriptions have an empty intersection', () => {
+ assert.deepEqual(
+ exports.processorEventCompatibility(['*'], []).matchingEvents,
+ ['message.received'],
+ );
+ assert.deepEqual(
+ exports.processorEventCompatibility(['group.*'], []).matchingEvents,
+ [],
+ );
+ assert.deepEqual(exports.processorEventCompatibility([], []).entries, []);
+});