Add discovered group chat picker for group monitor setup

This commit is contained in:
InfernoXuaI
2026-05-22 15:51:07 +08:00
parent 9095229218
commit 735d36c7d8
3 changed files with 113 additions and 3 deletions
+2
View File
@@ -32,6 +32,7 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器
### 2026-05-22 更新
- TG 群监听功能增强:支持可视化配置监听规则、AI 总结参数与防刷屏策略。
- TG 群监听新增“已发现群聊”:自动显示 Bot 收到过消息的群聊 `chat_id`,可一键创建监听。
- 新增 `/update` 安全更新流程:显示本地/远端 commit、ahead/behind、工作区状态;仅允许 `ff-only` 更新。
- 更新前若检测到本地未提交改动,会拒绝更新;避免覆盖本地代码。
- 新增“回滚上次更新”按钮:更新前自动记录回滚点,可一键回滚并重启。
@@ -346,6 +347,7 @@ group_monitors:
```
- 命中 `keywords` 且未命中 `exclude_keywords` 时,会给管理员发送摘要。
- TG 群监听页面会展示“已发现群聊”(Bot 收到过消息的群),可直接点“用此群创建监听”自动填入 `chat_id`
- `summary_mode` 支持:
- `template`:固定模板摘要(默认)
- `ai`:调用 AI 生成摘要(在 TG 群监听页面可视化配置)
+89 -3
View File
@@ -25,7 +25,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urljoin
from urllib.parse import quote_plus, urljoin
import feedparser
import httpx
@@ -212,6 +212,13 @@ def init_db() -> None:
meta_value TEXT,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS discovered_group_chats (
chat_id INTEGER PRIMARY KEY,
title TEXT,
username TEXT,
last_seen_at TEXT NOT NULL,
active INTEGER DEFAULT 1
);
"""
)
for sql in [
@@ -840,6 +847,46 @@ def get_monitor_status_badge(status: dict[str, Any] | None) -> str:
return "正常"
def record_discovered_group_chat(message: Message) -> None:
if message.chat.type not in {"group", "supergroup"}:
return
chat_id = int(message.chat.id)
title = str(getattr(message.chat, "title", "") or str(chat_id))
username = str(getattr(message.chat, "username", "") or "")
with closing(db()) as conn:
conn.execute(
"""
INSERT INTO discovered_group_chats(chat_id, title, username, last_seen_at, active)
VALUES(?,?,?,?,1)
ON CONFLICT(chat_id) DO UPDATE SET
title=excluded.title,
username=excluded.username,
last_seen_at=excluded.last_seen_at,
active=1
""",
(chat_id, title, username, now_iso()),
)
conn.commit()
def list_discovered_group_chats(limit: int = 200) -> list[dict[str, Any]]:
with closing(db()) as conn:
rows = conn.execute(
"SELECT chat_id, title, username, last_seen_at, active FROM discovered_group_chats ORDER BY last_seen_at DESC LIMIT ?",
(max(1, int(limit)),),
).fetchall()
return [
{
"chat_id": int(row["chat_id"]),
"title": str(row["title"] or row["chat_id"]),
"username": str(row["username"] or ""),
"last_seen_at": str(row["last_seen_at"] or ""),
"active": bool(row["active"]),
}
for row in rows
]
def lookup_reply_target(admin_chat: int, admin_message_id: int) -> int | None:
with closing(db()) as conn:
row = conn.execute(
@@ -1189,6 +1236,7 @@ async def user_message(message: Message) -> None:
if message.chat.type != "private":
if message.chat.type in {"group", "supergroup"}:
try:
record_discovered_group_chat(message)
await handle_group_keyword_message(message)
except Exception:
logger.exception("group keyword handling failed chat_id=%s message_id=%s", message.chat.id, message.message_id)
@@ -2175,6 +2223,7 @@ def create_panel_app() -> FastAPI:
async def group_monitors_page(_: str = Depends(panel_auth)) -> str:
cfg = cfg_load_fresh()
rows = cfg.get("group_monitors") or []
discovered = list_discovered_group_chats()
trs = []
for i, gm in enumerate(rows):
if not isinstance(gm, dict):
@@ -2186,6 +2235,15 @@ def create_panel_app() -> FastAPI:
trs.append(
f"""<tr><td>{i+1}</td><td><b>{html_escape(gm.get('name') or gm.get('chat_id') or '-')}</b><br><small>{html_escape(gm.get('chat_id') or '-')}</small></td><td>{enabled}<br><small>{notify}</small></td><td>{html_escape(kws)}</td><td>{html_escape(exs)}</td><td><a class=btn href='/group-monitors/{i}/edit'>编辑</a> <a class='btn danger' href='/group-monitors/{i}/delete' onclick='return confirm("确定删除?")'>删除</a></td></tr>"""
)
discovered_rows = []
for row in discovered:
title = row["title"]
chat_id = row["chat_id"]
username = f"@{row['username']}" if row["username"] else "-"
create_link = f"/group-monitors/new?chat_id={chat_id}&name={quote_plus(title)}"
discovered_rows.append(
f"""<tr><td><b>{html_escape(title)}</b><br><small>{html_escape(username)}</small></td><td><code>{chat_id}</code></td><td>{html_escape(row['last_seen_at'])}</td><td><a class='btn ok' href='{create_link}'>用此群创建监听</a></td></tr>"""
)
body = (
"<div class=card><div class=toolbar><div><h2 style='margin:0 0 6px'>TG 群关键词监听</h2>"
"<p class=muted style='margin:0'>监听选定群并在命中关键词时给管理员发送摘要。"
@@ -2193,12 +2251,40 @@ def create_panel_app() -> FastAPI:
"<div class=actions><a class='btn primary' href='/group-monitors/new'>新增监听</a></div></div>"
"<table style='margin-top:16px'><tr><th>#</th><th>监听</th><th>状态</th><th>关键词</th><th>排除词</th><th>操作</th></tr>"
+ "".join(trs) + "</table></div>"
+ "<div class=card><h2>已发现群聊</h2><p class=muted>Bot 在群里收到消息后会自动记录群信息。可直接选择群聊创建监听。</p>"
+ "<table><tr><th>群聊</th><th>chat_id</th><th>最近活跃</th><th>操作</th></tr>"
+ ("".join(discovered_rows) if discovered_rows else "<tr><td colspan='4'>暂无已发现群聊。先把 Bot 拉进群并发送一条消息。</td></tr>")
+ "</table></div>"
)
return layout("TG 群监听", body)
@app.get("/group-monitors/new", response_class=HTMLResponse)
async def group_monitor_new(_: str = Depends(panel_auth)) -> str:
return layout("新增 TG 群监听", group_monitor_form_html())
async def group_monitor_new(
_: str = Depends(panel_auth),
chat_id: str = "",
name: str = "",
) -> str:
preset = None
if chat_id.strip() or name.strip():
preset = {
"enabled": True,
"chat_id": chat_id.strip(),
"name": name.strip(),
"keywords": [],
"exclude_keywords": [],
"notify_telegram": True,
"summary_mode": "template",
"ai_base_url": "",
"ai_api_key": "",
"ai_model": "gpt-4o-mini",
"ai_interface": "responses",
"ai_temperature": 0.2,
"ai_timeout_seconds": 30,
"ai_prompt": "",
"ai_min_interval_seconds": DEFAULT_GROUP_AI_MIN_INTERVAL_SECONDS,
"ai_dedupe_window_seconds": DEFAULT_GROUP_AI_DEDUPE_WINDOW_SECONDS,
}
return layout("新增 TG 群监听", group_monitor_form_html(preset))
@app.get("/group-monitors/{idx}/edit", response_class=HTMLResponse)
async def group_monitor_edit(idx: int, _: str = Depends(panel_auth)) -> str:
+22
View File
@@ -651,6 +651,28 @@ class MonitorRuntimeAndUpdateTest(unittest.TestCase):
app.admin_chat_ids = old_admin_chat_ids
app.summarize_group_message_ai = old_ai
def test_record_and_list_discovered_group_chats(self) -> None:
msg = SimpleNamespace(
chat=SimpleNamespace(id=-100123, type="supergroup", title="测试群A", username="group_a"),
text="hello",
caption=None,
reply_to_message=None,
message_id=1,
from_user=SimpleNamespace(id=11, first_name="u", last_name="", username="u1"),
content_type="text",
)
app.record_discovered_group_chat(msg)
rows = app.list_discovered_group_chats()
self.assertTrue(rows)
self.assertEqual(-100123, rows[0]["chat_id"])
self.assertEqual("测试群A", rows[0]["title"])
def test_group_monitors_page_keeps_discovered_chat_actions_markup(self) -> None:
source = Path("app.py").read_text(encoding="utf-8")
self.assertIn("已发现群聊", source)
self.assertIn("用此群创建监听", source)
self.assertIn("/group-monitors/new?chat_id=", source)
if __name__ == "__main__":
unittest.main()