mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 13:25:18 +08:00
feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
d099033d79
commit
3a77306c3c
@@ -0,0 +1,41 @@
|
||||
# Keyword Alert
|
||||
|
||||
Monitor group chat messages for specific keywords and receive instant private message alerts.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔔 Real-time keyword monitoring across group chats
|
||||
- 📋 Configurable keyword lists (comma-separated)
|
||||
- 🎯 Monitor specific groups or all groups
|
||||
- 🤖 Choose which bot sends alerts
|
||||
- ⏱️ Cooldown to prevent alert spam
|
||||
- 🔤 Optional case-sensitive matching
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Configure keywords you want to monitor (e.g., `bug,urgent,help`)
|
||||
2. Set your user/session ID as the admin to receive alerts
|
||||
3. When someone sends a message containing a keyword in a monitored group, you get a private message alert with the full context
|
||||
|
||||
## Configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
|---|---|---|
|
||||
| Keywords | Comma-separated keywords to monitor | (required) |
|
||||
| Group IDs | Comma-separated group IDs (empty = all) | All groups |
|
||||
| Admin Session ID | Who receives the alerts | (required) |
|
||||
| Alert Bot | Which bot sends the alert | First available |
|
||||
| Case Sensitive | Case-sensitive matching | Off |
|
||||
| Cooldown | Seconds between same-keyword alerts per group | 60 |
|
||||
|
||||
## Alert Format
|
||||
|
||||
```
|
||||
🔔 关键词告警
|
||||
━━━━━━━━━━━━━━
|
||||
关键词: urgent
|
||||
群组: 123456789
|
||||
发送者: 987654321
|
||||
━━━━━━━━━━━━━━
|
||||
Hey, this is urgent, the server is down!
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
# Shared runtime placement (`shared-runtime-v1` + `stateless-v1`)
|
||||
|
||||
This source declares `execution.sharedRuntime: shared-runtime-v1` and
|
||||
`execution.componentModel: stateless-v1` in `manifest.yaml`. It is a source
|
||||
declaration only: it is **not certified**, has no signed archive, and has not been
|
||||
accepted against a live multi-Workspace deployment. The exact archive still needs
|
||||
independent review and acceptance runs.
|
||||
|
||||
Under shared placement one `KeywordAlert` object and one `KeywordMonitor` event
|
||||
listener serve every installation of the artifact digest. Installation attachments
|
||||
are lightweight contexts; they do not create another object graph.
|
||||
|
||||
## Per invocation
|
||||
|
||||
- `keywords`, `group_ids`, `admin_id`, `bot`, `case_sensitive` and
|
||||
`cooldown_seconds` are read through `self.get_config()` on every group-message
|
||||
invocation, from the plugin or from the listener component's
|
||||
`get_plugin_config()`. No config value is stored on the shared object.
|
||||
- `get_installation_binding()` is read per invocation and never retained past the
|
||||
call; it is read when the cooldown table for the current installation is resolved.
|
||||
- `initialize()` is process-scoped and only logs: it runs once per worker with an
|
||||
empty config, before any installation is known.
|
||||
|
||||
## Per binding, released on revocation
|
||||
|
||||
- `_cooldowns` is keyed by the installation scope
|
||||
`<instance_uuid>:<workspace_uuid>:<installation_uuid>` and then by
|
||||
`(group_id, keyword)`. `runtime_revision` is deliberately excluded from the token
|
||||
so a worker upgrade does not hand a tenant a fresh cooldown window.
|
||||
- `BasePlugin.on_installation_revoked(binding)` pops exactly that scope and touches
|
||||
process-local state only — no Host API is available during revocation. Nothing is
|
||||
persisted, so no durable row is involved.
|
||||
- Dedicated (binding-less) workers keep a single binding-less cooldown table, i.e.
|
||||
their existing behaviour.
|
||||
|
||||
## Known limits
|
||||
|
||||
- Cooldowns are in-memory and process-lifetime only (unchanged pre-existing
|
||||
behaviour): a worker restart clears them, and revocation clears a revoked
|
||||
installation's window.
|
||||
- The per-installation cooldown table grows with the number of distinct
|
||||
(group, keyword) pairs seen by that installation; it is freed by revocation. The
|
||||
runtime revokes on uninstall, disable, Workspace removal and upgrade.
|
||||
- Revocation is serialized per installation but may overlap with sibling
|
||||
installations' invocations; per-installation state is isolated, nothing is shared.
|
||||
- Alert delivery runs inside the group-message invocation and therefore uses that
|
||||
invocation's authority; the plugin creates no detached task.
|
||||
|
||||
## Tests
|
||||
|
||||
`tests/test_installation_cooldown.py` drives one plugin object plus one real
|
||||
`KeywordMonitor` component object through two distinct `InstallationBinding`s (same
|
||||
instance and Workspace, different installation uuids) with the installed SDK's
|
||||
`bind_invocation`, over a stub Host that records alert relays. It asserts that the
|
||||
same message in the same group produces one alert **per** installation (pre-fix:
|
||||
one relay total, because A's process-global `(group, keyword)` cooldown suppressed
|
||||
B), that the cooldown still suppresses a repeat inside one installation, that
|
||||
revocation releases only the revoked installation's table (so its next alert is
|
||||
allowed again while a sibling is untouched), and that the scope token is the full
|
||||
binding.
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<circle cx="50" cy="50" r="45" fill="#E74C3C" />
|
||||
<text x="50" y="58" text-anchor="middle" font-size="40" fill="white" font-family="Arial">🔔</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 249 B |
@@ -0,0 +1 @@
|
||||
# AutoTranslate event listener
|
||||
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from langbot_plugin.api.definition.components.common.event_listener import EventListener
|
||||
from langbot_plugin.api.entities import events, context
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
logger = logging.getLogger("KeywordAlert.monitor")
|
||||
|
||||
|
||||
class KeywordMonitor(EventListener):
|
||||
"""Monitors group messages for configured keywords."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@self.handler(events.GroupMessageReceived)
|
||||
async def on_group_message(event_context: context.EventContext):
|
||||
await self._handle(event_context)
|
||||
|
||||
async def _handle(self, event_context: context.EventContext):
|
||||
try:
|
||||
plugin = self.plugin
|
||||
event = event_context.event
|
||||
|
||||
group_id = str(event.launcher_id)
|
||||
sender_id = str(event.sender_id)
|
||||
|
||||
# Check if this group is monitored
|
||||
allowed_groups = plugin.parse_group_ids()
|
||||
if allowed_groups is not None and group_id not in allowed_groups:
|
||||
return
|
||||
|
||||
# Extract text
|
||||
text_parts = []
|
||||
for component in event.message_chain:
|
||||
if isinstance(component, platform_message.Plain):
|
||||
text_parts.append(component.text)
|
||||
text = "".join(text_parts).strip()
|
||||
if not text:
|
||||
return
|
||||
|
||||
# Check keywords
|
||||
keywords = plugin.parse_keywords()
|
||||
if not keywords:
|
||||
return
|
||||
|
||||
config = plugin.get_config()
|
||||
case_sensitive = config.get("case_sensitive", False)
|
||||
match_text = text if case_sensitive else text.lower()
|
||||
|
||||
for keyword in keywords:
|
||||
match_keyword = keyword if case_sensitive else keyword.lower()
|
||||
if match_keyword in match_text:
|
||||
if plugin.check_cooldown(group_id, keyword):
|
||||
await plugin.send_alert(group_id, sender_id, keyword, text)
|
||||
break # One alert per message
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in keyword monitor: {e}\n{traceback.format_exc()}")
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: EventListener
|
||||
metadata:
|
||||
name: monitor
|
||||
label:
|
||||
en_US: Keyword Monitor
|
||||
zh_Hans: 关键词监控
|
||||
spec:
|
||||
execution:
|
||||
python:
|
||||
path: monitor.py
|
||||
attr: KeywordMonitor
|
||||
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from langbot_plugin.api.definition.plugin import BasePlugin
|
||||
|
||||
logger = logging.getLogger("KeywordAlert")
|
||||
|
||||
# Scope used when the active invocation carries no installation binding.
|
||||
LEGACY_SCOPE = ""
|
||||
|
||||
|
||||
class KeywordAlert(BasePlugin):
|
||||
"""Monitor group messages for keywords and alert admins via private message."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Cooldown tracking, keyed by installation scope then
|
||||
# (group_id, keyword): one object graph serves every installation of
|
||||
# this artifact digest, so a cooldown must never suppress a sibling
|
||||
# installation's alert. Entries are released in
|
||||
# on_installation_revoked().
|
||||
self._cooldowns: dict[str, dict[tuple[str, str], float]] = {}
|
||||
|
||||
async def initialize(self):
|
||||
# Process-scoped only: no tenant config or state is read here.
|
||||
logger.info("KeywordAlert plugin initialized")
|
||||
|
||||
async def on_installation_revoked(self, binding) -> None:
|
||||
"""Release one installation's cooldown table (no Host call is made here)."""
|
||||
self._cooldowns.pop(self._scope(binding), None)
|
||||
|
||||
def _scope(self, binding=None) -> str:
|
||||
"""Return the full installation scope of this invocation.
|
||||
|
||||
The whole binding is used so two Workspaces that reuse an installation
|
||||
UUID never share a cooldown. Dedicated placement has no binding and keeps
|
||||
its process-local scope.
|
||||
"""
|
||||
if binding is None:
|
||||
binding = self.get_installation_binding()
|
||||
if binding is None:
|
||||
return LEGACY_SCOPE
|
||||
return f"{binding.instance_uuid}:{binding.workspace_uuid}:{binding.installation_uuid}"
|
||||
|
||||
def parse_keywords(self) -> list[str]:
|
||||
raw = self.get_config().get("keywords", "")
|
||||
return [k.strip() for k in raw.split(",") if k.strip()]
|
||||
|
||||
def parse_group_ids(self) -> Optional[set[str]]:
|
||||
raw = self.get_config().get("group_ids", "")
|
||||
if not raw or not raw.strip():
|
||||
return None # Monitor all groups
|
||||
return {g.strip() for g in raw.split(",") if g.strip()}
|
||||
|
||||
def check_cooldown(self, group_id: str, keyword: str) -> bool:
|
||||
"""Returns True if alert is allowed (not in cooldown)."""
|
||||
config = self.get_config()
|
||||
cooldown = int(config.get("cooldown_seconds", 60))
|
||||
key = (group_id, keyword)
|
||||
now = time.time()
|
||||
table = self._cooldowns.setdefault(self._scope(), {})
|
||||
last = table.get(key, 0)
|
||||
if now - last < cooldown:
|
||||
return False
|
||||
table[key] = now
|
||||
return True
|
||||
|
||||
async def send_alert(self, group_id: str, sender_id: str, keyword: str, text: str):
|
||||
"""Send private alert to admin."""
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
config = self.get_config()
|
||||
admin_id = config.get("admin_id", "")
|
||||
if not admin_id:
|
||||
logger.warning("No admin_id configured, skipping alert")
|
||||
return
|
||||
|
||||
bot_uuid = config.get("bot")
|
||||
if not bot_uuid:
|
||||
bots = await self.get_bots()
|
||||
if not bots:
|
||||
logger.warning("No bots available to send alert")
|
||||
return
|
||||
bot_uuid = bots[0]
|
||||
|
||||
alert_text = (
|
||||
f"🔔 关键词告警\n"
|
||||
f"━━━━━━━━━━━━━━\n"
|
||||
f"关键词: {keyword}\n"
|
||||
f"群组: {group_id}\n"
|
||||
f"发送者: {sender_id}\n"
|
||||
f"━━━━━━━━━━━━━━\n"
|
||||
f"{text[:500]}"
|
||||
)
|
||||
|
||||
chain = platform_message.MessageChain([
|
||||
platform_message.Plain(text=alert_text)
|
||||
])
|
||||
|
||||
try:
|
||||
await self.send_message(
|
||||
bot_uuid=bot_uuid,
|
||||
target_type="person",
|
||||
target_id=admin_id,
|
||||
message_chain=chain,
|
||||
)
|
||||
logger.info(f"Alert sent: keyword='{keyword}' group={group_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send alert: {e}")
|
||||
@@ -0,0 +1,101 @@
|
||||
apiVersion: v1
|
||||
kind: Plugin
|
||||
metadata:
|
||||
author: langbot-team
|
||||
name: KeywordAlert
|
||||
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/misc/KeywordAlert
|
||||
version: 0.1.3
|
||||
description:
|
||||
en_US: 'Monitor group chat messages for keywords and send private alerts to admins.
|
||||
Supports multiple groups and keyword lists.'
|
||||
zh_Hans: '监控群聊消息中的关键词,私聊通知管理员。支持配置多个群和多个关键词。'
|
||||
zh_Hant: 監控群組聊天訊息中的關鍵字並向管理員發送私人警報。支援多個群組和關鍵字清單。
|
||||
ja_JP: キーワードに関するグループ チャット メッセージを監視し、管理者にプライベート アラートを送信します。複数のグループとキーワード
|
||||
リストをサポートします。
|
||||
vi_VN: Theo dõi tin nhắn trò chuyện nhóm để tìm từ khóa và gửi thông báo
|
||||
riêng tư cho quản trị viên. Hỗ trợ nhiều nhóm và danh sách từ khóa.
|
||||
th_TH:
|
||||
ตรวจสอบข้อความแชทกลุ่มเพื่อหาคำสำคัญและส่งการแจ้งเตือนส่วนตัวไปยังผู้ดูแลระบบ
|
||||
รองรับหลายกลุ่มและรายการคำหลัก
|
||||
es_ES: Supervise los mensajes de chat grupal en busca de palabras clave y
|
||||
envíe alertas privadas a los administradores. Admite múltiples grupos y
|
||||
listas de palabras clave.
|
||||
label:
|
||||
en_US: Keyword Alert
|
||||
zh_Hans: 关键词监控
|
||||
zh_Hant: Keyword Alert
|
||||
ja_JP: Keyword Alert
|
||||
vi_VN: Keyword Alert
|
||||
th_TH: Keyword Alert
|
||||
es_ES: Keyword Alert
|
||||
icon: assets/icon.svg
|
||||
spec:
|
||||
config:
|
||||
- name: keywords
|
||||
type: string
|
||||
label:
|
||||
en_US: 'Keywords (comma-separated)'
|
||||
zh_Hans: '关键词(逗号分隔)'
|
||||
description:
|
||||
en_US: 'Comma-separated keywords to monitor, e.g.: bug,urgent,help'
|
||||
zh_Hans: '逗号分隔的关键词列表,例如:bug,紧急,求助'
|
||||
required: true
|
||||
- name: group_ids
|
||||
type: string
|
||||
label:
|
||||
en_US: 'Group IDs (comma-separated, empty = all groups)'
|
||||
zh_Hans: '群组 ID(逗号分隔,留空 = 所有群)'
|
||||
description:
|
||||
en_US: 'Only monitor these groups. Leave empty to monitor all groups.'
|
||||
zh_Hans: '仅监控这些群。留空则监控所有群。'
|
||||
required: false
|
||||
- name: admin_id
|
||||
type: string
|
||||
label:
|
||||
en_US: 'Admin Session ID'
|
||||
zh_Hans: '管理员会话 ID'
|
||||
description:
|
||||
en_US: 'The user/session ID to receive alert notifications via private message'
|
||||
zh_Hans: '接收告警私聊通知的用户/会话 ID'
|
||||
required: true
|
||||
- name: bot
|
||||
type: bot-selector
|
||||
label:
|
||||
en_US: 'Alert Bot'
|
||||
zh_Hans: '告警机器人'
|
||||
description:
|
||||
en_US: 'Select which bot sends the alert. If not set, the first available bot
|
||||
will be used.'
|
||||
zh_Hans: '选择发送告警的机器人。未设置时使用第一个可用的机器人。'
|
||||
required: false
|
||||
- name: case_sensitive
|
||||
type: bool
|
||||
label:
|
||||
en_US: 'Case Sensitive'
|
||||
zh_Hans: '区分大小写'
|
||||
description:
|
||||
en_US: 'Whether keyword matching is case sensitive (default: no)'
|
||||
zh_Hans: '关键词匹配是否区分大小写(默认否)'
|
||||
required: false
|
||||
default: false
|
||||
- name: cooldown_seconds
|
||||
type: int
|
||||
label:
|
||||
en_US: 'Cooldown (seconds)'
|
||||
zh_Hans: '冷却时间(秒)'
|
||||
description:
|
||||
en_US: 'Minimum interval between alerts for the same keyword in the same group
|
||||
(default: 60)'
|
||||
zh_Hans: '同群同关键词两次告警最短间隔秒数(默认 60)'
|
||||
required: false
|
||||
default: 60
|
||||
components:
|
||||
EventListener:
|
||||
fromDirs:
|
||||
- path: components/event_listener/
|
||||
execution:
|
||||
python:
|
||||
path: main.py
|
||||
attr: KeywordAlert
|
||||
sharedRuntime: shared-runtime-v1
|
||||
componentModel: stateless-v1
|
||||
@@ -0,0 +1,41 @@
|
||||
# Alerta de palabra clave
|
||||
|
||||
Supervise los mensajes de chat grupal en busca de palabras clave específicas y reciba alertas instantáneas de mensajes privados.
|
||||
|
||||
## Características
|
||||
|
||||
- 🔔 Monitoreo de palabras clave en tiempo real en chats grupales
|
||||
- 📋 Listas de palabras clave configurables (separadas por comas)
|
||||
- 🎯 Monitorear grupos específicos o todos los grupos
|
||||
- 🤖 Elige qué bot envía alertas
|
||||
- ⏱️ Tiempo de reutilización para evitar alertas no deseadas
|
||||
- 🔤 Coincidencia opcional que distingue entre mayúsculas y minúsculas
|
||||
|
||||
## Cómo funciona
|
||||
|
||||
1. Configure las palabras clave que desea monitorear (por ejemplo, "error, urgente, ayuda")
|
||||
2. Configure su ID de usuario/sesión como administrador para recibir alertas.
|
||||
3. Cuando alguien envía un mensaje que contiene una palabra clave en un grupo monitoreado, recibe una alerta de mensaje privado con el contexto completo.
|
||||
|
||||
## Configuración
|
||||
|
||||
| Opción | Descripción | Predeterminado |
|
||||
|---|---|---|
|
||||
| Palabras clave | Palabras clave separadas por comas para monitorear | (obligatorio) |
|
||||
| ID de grupo | ID de grupo separados por comas (vacíos = todos) | Todos los grupos |
|
||||
| ID de sesión de administrador | Quién recibe las alertas | (obligatorio) |
|
||||
| Bot de alerta | ¿Qué bot envía la alerta? Primero disponible |
|
||||
| Distingue entre mayúsculas y minúsculas | Coincidencia que distingue entre mayúsculas y minúsculas | Apagado |
|
||||
| Enfriamiento | Segundos entre alertas de la misma palabra clave por grupo | 60 |
|
||||
|
||||
## Formato de alerta
|
||||
|
||||
```
|
||||
🔔 关键词告警
|
||||
━━━━━━━━━━━━━━
|
||||
关键词: urgent
|
||||
群组: 123456789
|
||||
发送者: 987654321
|
||||
━━━━━━━━━━━━━━
|
||||
Hey, this is urgent, the server is down!
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# キーワードアラート
|
||||
|
||||
グループチャットのメッセージを監視し、キーワードが含まれる場合にプライベートメッセージで管理者に通知します。
|
||||
|
||||
## 機能
|
||||
|
||||
- 🔔 リアルタイムキーワード監視
|
||||
- 📋 複数キーワード対応(カンマ区切り)
|
||||
- 🎯 特定のグループまたは全グループを監視
|
||||
- 🤖 アラート送信ボットを選択可能
|
||||
- ⏱️ クールダウンでアラートスパムを防止
|
||||
- 🔤 大文字小文字の区別オプション
|
||||
|
||||
## 設定
|
||||
|
||||
| オプション | 説明 | デフォルト |
|
||||
|---|---|---|
|
||||
| キーワード | カンマ区切りのキーワード | (必須) |
|
||||
| グループID | カンマ区切り(空=全グループ) | 全グループ |
|
||||
| 管理者ID | アラートを受信するユーザー | (必須) |
|
||||
| ボット | アラートを送信するボット | 最初の利用可能なボット |
|
||||
| 大文字小文字区別 | 大文字小文字を区別するか | オフ |
|
||||
| クールダウン | 同じキーワードのアラート間隔(秒) | 60 |
|
||||
@@ -0,0 +1,41 @@
|
||||
# การแจ้งเตือนคำหลัก
|
||||
|
||||
ตรวจสอบข้อความแชทกลุ่มสำหรับคำสำคัญเฉพาะ และรับการแจ้งเตือนข้อความส่วนตัวทันที
|
||||
|
||||
## คุณสมบัติ
|
||||
|
||||
- 🔔 การตรวจสอบคำหลักแบบเรียลไทม์ในการแชทกลุ่ม
|
||||
- 📋 รายการคำหลักที่กำหนดค่าได้ (คั่นด้วยเครื่องหมายจุลภาค)
|
||||
- 🎯 ติดตามเฉพาะกลุ่มหรือทุกกลุ่ม
|
||||
- 🤖 เลือกบอทที่จะส่งการแจ้งเตือน
|
||||
- ⏱️ คูลดาวน์เพื่อป้องกันสแปมการแจ้งเตือน
|
||||
- 🔤 การจับคู่แบบคำนึงถึงตัวพิมพ์เล็กและใหญ่หรือไม่ก็ได้
|
||||
|
||||
## มันทำงานอย่างไร
|
||||
|
||||
1. กำหนดค่าคำหลักที่คุณต้องการตรวจสอบ (เช่น`bug,urgent,help`)
|
||||
2. ตั้งค่า ID ผู้ใช้/เซสชันของคุณเป็นผู้ดูแลระบบเพื่อรับการแจ้งเตือน
|
||||
3. เมื่อมีคนส่งข้อความที่มีคำสำคัญในกลุ่มที่ได้รับการตรวจสอบ คุณจะได้รับการแจ้งเตือนข้อความส่วนตัวพร้อมบริบททั้งหมด
|
||||
|
||||
## การกำหนดค่า
|
||||
|
||||
| ตัวเลือก | คำอธิบาย | ค่าเริ่มต้น |
|
||||
|---|---|---|
|
||||
| คำสำคัญ | คำหลักที่คั่นด้วยเครื่องหมายจุลภาคเพื่อติดตาม | (จำเป็น) |
|
||||
| รหัสกลุ่ม | ID กลุ่มที่คั่นด้วยเครื่องหมายจุลภาค (ว่าง = ทั้งหมด) | ทุกกลุ่ม |
|
||||
| รหัสเซสชันผู้ดูแลระบบ | ใครได้รับการแจ้งเตือน | (จำเป็น) |
|
||||
| บอทแจ้งเตือน | บอทตัวไหนส่งการแจ้งเตือน | วางจำหน่ายครั้งแรก |
|
||||
| คำนึงถึงขนาดตัวพิมพ์ | การจับคู่แบบคำนึงถึงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ | ปิด |
|
||||
| คูลดาวน์ | วินาทีระหว่างการแจ้งเตือนคำหลักเดียวกันต่อกลุ่ม | 60 |
|
||||
|
||||
## รูปแบบการแจ้งเตือน
|
||||
|
||||
```
|
||||
🔔 关键词告警
|
||||
━━━━━━━━━━━━━━
|
||||
关键词: urgent
|
||||
群组: 123456789
|
||||
发送者: 987654321
|
||||
━━━━━━━━━━━━━━
|
||||
Hey, this is urgent, the server is down!
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# Cảnh báo từ khóa
|
||||
|
||||
Giám sát tin nhắn trò chuyện nhóm để tìm từ khóa cụ thể và nhận thông báo tin nhắn riêng tư ngay lập tức.
|
||||
|
||||
## Đặc trưng
|
||||
|
||||
- 🔔 Giám sát từ khóa theo thời gian thực trên các cuộc trò chuyện nhóm
|
||||
- 📋 Danh sách từ khóa có thể định cấu hình (được phân tách bằng dấu phẩy)
|
||||
- 🎯 Giám sát các nhóm cụ thể hoặc tất cả các nhóm
|
||||
- 🤖 Chọn bot nào gửi thông báo
|
||||
- ⏱️ Thời gian hồi chiêu để ngăn chặn cảnh báo spam
|
||||
- 🔤 Tùy chọn kết hợp phân biệt chữ hoa chữ thường
|
||||
|
||||
## Nó hoạt động như thế nào
|
||||
|
||||
1. Định cấu hình các từ khóa bạn muốn theo dõi (ví dụ:`lỗi,khẩn cấp,trợ giúp`)
|
||||
2. Đặt ID người dùng/phiên của bạn làm quản trị viên để nhận thông báo
|
||||
3. Khi ai đó gửi tin nhắn có chứa từ khóa trong nhóm được giám sát, bạn sẽ nhận được thông báo tin nhắn riêng tư với ngữ cảnh đầy đủ
|
||||
|
||||
## Cấu hình
|
||||
|
||||
| Tùy chọn | Mô tả | Mặc định |
|
||||
|---|---|---|
|
||||
| Từ khóa | Từ khóa được phân tách bằng dấu phẩy để theo dõi | (bắt buộc) |
|
||||
| ID nhóm | ID nhóm được phân tách bằng dấu phẩy (trống = tất cả) | Tất cả các nhóm |
|
||||
| ID phiên quản trị viên | Ai nhận được thông báo | (bắt buộc) |
|
||||
| Cảnh báo Bot | Bot nào gửi cảnh báo | Có sẵn đầu tiên |
|
||||
| Phân biệt chữ hoa chữ thường | Khớp phân biệt chữ hoa chữ thường | Tắt |
|
||||
| Thời gian hồi chiêu | Số giây giữa các cảnh báo từ khóa giống nhau cho mỗi nhóm | 60 |
|
||||
|
||||
## Định dạng cảnh báo
|
||||
|
||||
```
|
||||
🔔 关键词告警
|
||||
━━━━━━━━━━━━━━
|
||||
关键词: urgent
|
||||
群组: 123456789
|
||||
发送者: 987654321
|
||||
━━━━━━━━━━━━━━
|
||||
Hey, this is urgent, the server is down!
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# 关键词监控
|
||||
|
||||
监控群聊消息中的关键词,命中时立即私聊通知管理员。
|
||||
|
||||
## 功能
|
||||
|
||||
- 🔔 实时监控群聊关键词
|
||||
- 📋 支持多关键词配置(逗号分隔)
|
||||
- 🎯 可指定监控群组或监控所有群
|
||||
- 🤖 选择发送告警的机器人
|
||||
- ⏱️ 冷却机制防止告警轰炸
|
||||
- 🔤 可选大小写敏感匹配
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 配置需要监控的关键词(如 `bug,紧急,求助`)
|
||||
2. 填写接收告警的管理员会话 ID
|
||||
3. 当群消息命中关键词时,你会收到私聊告警通知
|
||||
|
||||
## 配置
|
||||
|
||||
| 选项 | 说明 | 默认值 |
|
||||
|---|---|---|
|
||||
| 关键词 | 逗号分隔的关键词列表 | (必填) |
|
||||
| 群组 ID | 逗号分隔的群组 ID(留空 = 全部群) | 所有群 |
|
||||
| 管理员会话 ID | 接收告警的用户 ID | (必填) |
|
||||
| 告警机器人 | 发送告警的 bot | 第一个可用 |
|
||||
| 大小写敏感 | 是否区分大小写 | 否 |
|
||||
| 冷却时间 | 同群同关键词最短间隔秒数 | 60 |
|
||||
@@ -0,0 +1,41 @@
|
||||
# 關鍵字提醒
|
||||
|
||||
監控群組聊天訊息中的特定關鍵字並接收即時私人訊息警報。
|
||||
|
||||
## 特徵
|
||||
|
||||
- 🔔 跨群聊即時關鍵字監控
|
||||
- 📋可設定的關鍵字清單(以逗號分隔)
|
||||
- 🎯 監控特定組或所有組
|
||||
- 🤖 選擇哪個機器人發送警報
|
||||
- ⏱️ 冷卻以防止警報垃圾郵件
|
||||
- 🔤 可選區分大小寫的匹配
|
||||
|
||||
## 它是如何工作的
|
||||
|
||||
1. 設定要監控的關鍵字(例如`bug,urgent,help`)
|
||||
2. 將您的使用者/會話 ID 設定為管理員以接收警報
|
||||
3. 當有人在受監控的群組中傳送包含關鍵字的訊息時,您會收到包含完整上下文的私人訊息警報
|
||||
|
||||
## 配置
|
||||
|
||||
|選項 |說明 |預設 |
|
||||
|---|---|---|
|
||||
|關鍵字|要監控的以逗號分隔的關鍵字 | (必填)|
|
||||
|組 ID |以逗號分隔的組 ID(空 = 全部)|所有團體|
|
||||
|管理會話 ID |誰收到警報 | (必填)|
|
||||
|警報機器人 |哪個機器人發送警報 |首次上市 |
|
||||
|區分大小寫 |區分大小寫的匹配 |關閉 |
|
||||
|冷卻時間|每組相同關鍵字警報之間的秒數 | 60|
|
||||
|
||||
## 警報格式
|
||||
|
||||
```
|
||||
🔔 关键词告警
|
||||
━━━━━━━━━━━━━━
|
||||
关键词: urgent
|
||||
群组: 123456789
|
||||
发送者: 987654321
|
||||
━━━━━━━━━━━━━━
|
||||
Hey, this is urgent, the server is down!
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
"""Two installations, one KeywordAlert object graph: alerts never cross cooldowns.
|
||||
|
||||
Evidence model: one real plugin object plus one real ``KeywordMonitor``
|
||||
component object are driven through the real SDK
|
||||
``bind_invocation``/``InstallationBinding`` with two distinct installation
|
||||
bindings; the stub Host records every ``SEND_MESSAGE`` relay, so a suppressed
|
||||
alert is observable as a smaller relay list.
|
||||
|
||||
Pre-fix failure (asserted below): ``_cooldowns`` was one process-global dict
|
||||
keyed by ``(group_id, keyword)`` only. Installation A's first alert therefore
|
||||
suppressed installation B's alert for the same group/keyword, and
|
||||
``on_installation_revoked`` had nothing to release. The identical message below
|
||||
produced one relay instead of two.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from langbot_plugin.api.entities import events
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
from langbot_plugin.api.proxies.invocation import bind_invocation
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction as Action
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load(path: Path, name: str):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
alert_main = _load(PLUGIN_ROOT / "main.py", "keyword_alert_main")
|
||||
monitor_module = _load(PLUGIN_ROOT / "components" / "event_listener" / "monitor.py", "keyword_monitor")
|
||||
|
||||
|
||||
class FlatHost:
|
||||
"""Stub Host that records alert relays (no live platform call)."""
|
||||
|
||||
def __init__(self):
|
||||
self.sent: list[dict] = []
|
||||
|
||||
async def call_action(self, action, data, *args, **kwargs):
|
||||
if action == Action.SEND_MESSAGE:
|
||||
self.sent.append(data)
|
||||
return {"result": "ok"}
|
||||
if action == Action.GET_BOTS:
|
||||
return {"bots": ["fallback-bot"]}
|
||||
raise AssertionError(f"unexpected Host action: {action}")
|
||||
|
||||
|
||||
CONFIG = {
|
||||
"keywords": "urgent",
|
||||
"group_ids": "",
|
||||
"admin_id": "admin-1",
|
||||
"bot": "bot-1",
|
||||
"case_sensitive": False,
|
||||
"cooldown_seconds": 3600,
|
||||
}
|
||||
|
||||
|
||||
def binding(installation: str, *, workspace: str = "workspace-1"):
|
||||
return InstallationBinding(
|
||||
instance_uuid="instance-1",
|
||||
workspace_uuid=workspace,
|
||||
installation_uuid=installation,
|
||||
placement_generation=1,
|
||||
runtime_revision=1,
|
||||
artifact_digest="a" * 64,
|
||||
)
|
||||
|
||||
|
||||
def build_graph():
|
||||
plugin = alert_main.KeywordAlert()
|
||||
host = FlatHost()
|
||||
plugin.plugin_runtime_handler = host
|
||||
monitor = monitor_module.KeywordMonitor()
|
||||
monitor.plugin = plugin
|
||||
handler = monitor.registered_handlers[events.GroupMessageReceived][0]
|
||||
return plugin, host, handler
|
||||
|
||||
|
||||
def group_message(text: str, *, group_id: str = "group-1", sender_id: str = "user-1"):
|
||||
event = SimpleNamespace(
|
||||
launcher_id=group_id,
|
||||
sender_id=sender_id,
|
||||
message_chain=platform_message.MessageChain([platform_message.Plain(text=text)]),
|
||||
)
|
||||
return SimpleNamespace(event=event)
|
||||
|
||||
|
||||
def _texts(payloads: list[dict]) -> list[str]:
|
||||
return [
|
||||
"".join(
|
||||
part.get("text", "")
|
||||
for part in payload["message_chain"]
|
||||
if part.get("type") == "Plain"
|
||||
)
|
||||
for payload in payloads
|
||||
]
|
||||
|
||||
|
||||
def test_one_installation_cooldown_does_not_suppress_another():
|
||||
async def scenario():
|
||||
plugin, host, handler = build_graph()
|
||||
first, second = binding("installation-a"), binding("installation-b")
|
||||
|
||||
with bind_invocation(host, config=CONFIG, binding=first):
|
||||
await handler(group_message("this is urgent"))
|
||||
with bind_invocation(host, config=CONFIG, binding=second):
|
||||
await handler(group_message("this is urgent"))
|
||||
|
||||
# Pre-fix: only one relay, because the shared (group, keyword) key was
|
||||
# already in cooldown from installation A.
|
||||
assert len(host.sent) == 2
|
||||
assert all(payload["target_id"] == "admin-1" for payload in host.sent)
|
||||
assert _texts(host.sent)[0] == _texts(host.sent)[1]
|
||||
assert "urgent" in _texts(host.sent)[1]
|
||||
|
||||
# Each installation keeps its own cooldown table.
|
||||
assert set(plugin._cooldowns) == {plugin._scope(first), plugin._scope(second)}
|
||||
|
||||
# The cooldown still works inside one installation.
|
||||
with bind_invocation(host, config=CONFIG, binding=first):
|
||||
await handler(group_message("urgent again"))
|
||||
assert len(host.sent) == 2
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_cooldown_is_released_on_revocation():
|
||||
async def scenario():
|
||||
plugin, host, handler = build_graph()
|
||||
first = binding("installation-a")
|
||||
|
||||
with bind_invocation(host, config=CONFIG, binding=first):
|
||||
await handler(group_message("this is urgent"))
|
||||
assert len(host.sent) == 1
|
||||
|
||||
await plugin.on_installation_revoked(first)
|
||||
assert plugin._scope(first) not in plugin._cooldowns
|
||||
|
||||
with bind_invocation(host, config=CONFIG, binding=first):
|
||||
await handler(group_message("this is urgent"))
|
||||
assert len(host.sent) == 2
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_cooldown_key_includes_the_full_binding():
|
||||
plugin = alert_main.KeywordAlert()
|
||||
same_installation_other_workspace = binding("installation-a", workspace="workspace-2")
|
||||
assert plugin._scope(binding("installation-a")) != plugin._scope(same_installation_other_workspace)
|
||||
assert plugin._scope(binding("installation-a")) == "instance-1:workspace-1:installation-a"
|
||||
assert plugin._scope() == alert_main.LEGACY_SCOPE
|
||||
|
||||
|
||||
def test_revocation_of_an_unknown_binding_is_a_no_op():
|
||||
async def scenario():
|
||||
plugin, _host, _handler = build_graph()
|
||||
await plugin.on_installation_revoked(binding("installation-never-seen"))
|
||||
assert plugin._cooldowns == {}
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_dedicated_binding_less_invocation_keeps_its_cooldown():
|
||||
"""A dedicated worker's invocation carries no binding; it keeps its own scope."""
|
||||
|
||||
async def scenario():
|
||||
plugin, host, handler = build_graph()
|
||||
with bind_invocation(host, config=CONFIG): # no installation binding
|
||||
await handler(group_message("this is urgent"))
|
||||
assert len(host.sent) == 1
|
||||
assert set(plugin._cooldowns) == {alert_main.LEGACY_SCOPE}
|
||||
|
||||
with bind_invocation(host, config=CONFIG):
|
||||
await handler(group_message("urgent again"))
|
||||
assert len(host.sent) == 1 # the legacy cooldown still suppresses the repeat
|
||||
|
||||
asyncio.run(scenario())
|
||||
Reference in New Issue
Block a user