初始提交:识流 AI 助手项目
微信自动回复机器人,基于截图+OCR识别消息,支持关键词规则和 AI(OpenAI/DeepSeek/Dify)自动回复。 技术栈:PySide6 + Flask + Vue3 + RapidOCR + SQLite 注:OCR大模型文件(.onnx / .pdiparams)不纳入版本控制,需单独下载。 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
0
app/infrastructure/router/__init__.py
Normal file
0
app/infrastructure/router/__init__.py
Normal file
0
app/infrastructure/router/backend/__init__.py
Normal file
0
app/infrastructure/router/backend/__init__.py
Normal file
137
app/infrastructure/router/backend/bot.py
Normal file
137
app/infrastructure/router/backend/bot.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import json
|
||||
import queue
|
||||
|
||||
from flask import Response, jsonify, request, stream_with_context
|
||||
|
||||
from app.application.bot_controller import bot_controller
|
||||
from app.infrastructure.service.backend.db import set_setting
|
||||
from app.infrastructure.service.logging.log_query_service import clear_logs, query_event_json, query_events, query_summary, query_trace
|
||||
from app.infrastructure.service.logging.log_service import log_event, new_trace_id
|
||||
|
||||
|
||||
def _sync_runtime_settings(status):
|
||||
current = (status.get("status") or "stopped").strip().lower()
|
||||
running = current == "running"
|
||||
transitional = current in {"starting", "stopping"}
|
||||
errored = current == "error"
|
||||
if running:
|
||||
set_setting("listener_enabled", "1")
|
||||
set_setting("listener_runtime_status", "running")
|
||||
elif transitional:
|
||||
set_setting("listener_runtime_status", current)
|
||||
elif errored:
|
||||
set_setting("listener_enabled", "0")
|
||||
set_setting("listener_runtime_status", "error")
|
||||
else:
|
||||
set_setting("listener_enabled", "0")
|
||||
set_setting("listener_runtime_status", "stopped")
|
||||
|
||||
|
||||
def register_bot_routes(app):
|
||||
def _stream_payload(status):
|
||||
return f"event: status\ndata: {json.dumps({'success': True, **status}, ensure_ascii=False)}\n\n"
|
||||
|
||||
@app.route("/api/bot/status", methods=["GET"])
|
||||
def api_bot_status():
|
||||
trace_id = new_trace_id("api")
|
||||
status = bot_controller.status()
|
||||
_sync_runtime_settings(status)
|
||||
log_event("INFO", "api", "api.bot.status", trace_id, "status", "ok", "查询监听状态成功", extra={"status": status.get("status")})
|
||||
return jsonify({"success": True, **status})
|
||||
|
||||
@app.route("/api/bot/status/stream", methods=["GET"])
|
||||
def api_bot_status_stream():
|
||||
status_queue = queue.Queue(maxsize=8)
|
||||
|
||||
def on_status(status):
|
||||
_sync_runtime_settings(status)
|
||||
try:
|
||||
status_queue.put_nowait(status)
|
||||
except queue.Full:
|
||||
try:
|
||||
status_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
try:
|
||||
status_queue.put_nowait(status)
|
||||
except queue.Full:
|
||||
pass
|
||||
|
||||
listener_id = bot_controller.add_status_listener(on_status, emit_initial=True)
|
||||
|
||||
@stream_with_context
|
||||
def generate():
|
||||
try:
|
||||
yield "retry: 2000\n\n"
|
||||
while True:
|
||||
try:
|
||||
status = status_queue.get(timeout=15)
|
||||
yield _stream_payload(status)
|
||||
except queue.Empty:
|
||||
yield "event: ping\ndata: {}\n\n"
|
||||
finally:
|
||||
bot_controller.remove_status_listener(listener_id)
|
||||
|
||||
return Response(generate(), mimetype="text/event-stream", headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
})
|
||||
|
||||
@app.route("/api/logs/v2/events", methods=["GET"])
|
||||
def api_logs_v2_events():
|
||||
trace_id = request.args.get("trace_id") or ""
|
||||
module = request.args.get("module") or ""
|
||||
level = request.args.get("level") or ""
|
||||
event = request.args.get("event") or ""
|
||||
start_ts = request.args.get("start_ts") or ""
|
||||
end_ts = request.args.get("end_ts") or ""
|
||||
keyword = request.args.get("keyword") or ""
|
||||
page = request.args.get("page", 1)
|
||||
size = request.args.get("size", 50)
|
||||
payload = query_events(module=module or None, level=level or None, event=event or None, trace_id=trace_id or None, start_ts=start_ts or None, end_ts=end_ts or None, keyword=keyword or None, page=page, size=size)
|
||||
return jsonify({"success": True, **payload})
|
||||
|
||||
@app.route("/api/logs/v2/trace/<trace_id>", methods=["GET"])
|
||||
def api_logs_v2_trace(trace_id):
|
||||
items = query_trace(trace_id)
|
||||
return jsonify({"success": True, "trace_id": trace_id, "items": items})
|
||||
|
||||
@app.route("/api/logs/v2/summary", methods=["GET"])
|
||||
def api_logs_v2_summary():
|
||||
limit = request.args.get("limit", 300)
|
||||
payload = query_summary(limit=limit)
|
||||
return jsonify({"success": True, **payload})
|
||||
|
||||
@app.route("/api/logs/v2/event/<event_id>", methods=["GET"])
|
||||
def api_logs_v2_event(event_id):
|
||||
item = query_event_json(event_id)
|
||||
if not item:
|
||||
return jsonify({"success": False, "error": "event_not_found"}), 404
|
||||
return jsonify({"success": True, "item": item})
|
||||
|
||||
@app.route("/api/logs/v2/clear", methods=["POST"])
|
||||
def api_logs_v2_clear():
|
||||
body = request.get_json(silent=True) or {}
|
||||
module = (body.get("module") or request.values.get("module") or request.args.get("module") or "").strip()
|
||||
payload = clear_logs(module=module or None)
|
||||
return jsonify({"success": True, **payload})
|
||||
|
||||
@app.route("/api/bot/start", methods=["POST"])
|
||||
def api_bot_start():
|
||||
trace_id = new_trace_id("api")
|
||||
backend_url = (request.values.get("backend_url") or "").strip()
|
||||
if not backend_url:
|
||||
backend_url = request.host_url.rstrip("/") + "/api/messages/receive"
|
||||
status = bot_controller.start(backend_url)
|
||||
_sync_runtime_settings(status)
|
||||
log_event("INFO", "api", "api.bot.start", trace_id, "start", "ok", "启动监听请求已处理", extra={"status": status.get("status"), "backend_url": backend_url})
|
||||
return jsonify({"success": True, **status})
|
||||
|
||||
@app.route("/api/bot/stop", methods=["POST"])
|
||||
def api_bot_stop():
|
||||
trace_id = new_trace_id("api")
|
||||
status = bot_controller.stop()
|
||||
_sync_runtime_settings(status)
|
||||
log_event("INFO", "api", "api.bot.stop", trace_id, "stop", "ok", "停止监听请求已处理", extra={"status": status.get("status")})
|
||||
return jsonify({"success": True, **status})
|
||||
72
app/infrastructure/router/backend/messages.py
Normal file
72
app/infrastructure/router/backend/messages.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request
|
||||
|
||||
from app.infrastructure.service.backend.ai import call_ai
|
||||
from app.infrastructure.service.backend.db import find_rule_reply, get_conn, get_setting
|
||||
from app.infrastructure.service.logging.log_service import log_event, new_trace_id
|
||||
|
||||
|
||||
def register_message_routes(app):
|
||||
@app.route("/api/messages/receive", methods=["POST"])
|
||||
def api_receive_message():
|
||||
trace_id = new_trace_id("api")
|
||||
data = request.get_json(silent=True) or request.form.to_dict() or {}
|
||||
content = (data.get("content") or "").strip()
|
||||
wx_user_id = (data.get("wx_user_id") or "").strip()
|
||||
wx_nickname = (data.get("wx_nickname") or "").strip()
|
||||
is_friend_request = int(data.get("is_friend_request") or 0)
|
||||
ocr_confidence = str(data.get("ocr_confidence") or "").strip()
|
||||
ocr_bubble_side = str(data.get("ocr_bubble_side") or "").strip()
|
||||
if not content and not is_friend_request:
|
||||
log_event("WARNING", "api", "api.messages.receive", trace_id, "validate", "failed", "消息内容为空且非好友请求", reason="content_empty")
|
||||
return jsonify({"error": "content is empty"}), 400
|
||||
|
||||
conn = get_conn()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("INSERT INTO messages (wx_user_id, wx_nickname, direction, content, is_friend_request, reply_strategy, reply_reason, ocr_confidence, ocr_bubble_side, created_at) VALUES (%s, %s, 'in', %s, %s, %s, %s, %s, %s, %s)", (wx_user_id, wx_nickname, content, is_friend_request, "none", "received", ocr_confidence, ocr_bubble_side, now))
|
||||
in_msg_id = cur.lastrowid
|
||||
|
||||
auto_on = str(get_setting("auto_reply_enabled", "1") or "1").strip().lower() in ["1", "true", "yes", "on"]
|
||||
full_auto_on = str(get_setting("full_auto_reply_enabled", "0") or "0").strip().lower() in ["1", "true", "yes", "on"]
|
||||
reply_text = ""
|
||||
used_rule_id = None
|
||||
reply_strategy = "none"
|
||||
reply_reason = ""
|
||||
|
||||
if not auto_on:
|
||||
reply_reason = "auto_reply_disabled"
|
||||
else:
|
||||
rule = find_rule_reply(content)
|
||||
if rule:
|
||||
reply_text = (rule.get("reply_text") or "").strip()
|
||||
used_rule_id = rule.get("id")
|
||||
reply_strategy = "rule"
|
||||
reply_reason = f"rule_hit:{used_rule_id}"
|
||||
elif full_auto_on:
|
||||
reply_strategy = "full_auto"
|
||||
reply_reason = "full_auto_enabled"
|
||||
reply_text = call_ai(content, wx_user_id)
|
||||
else:
|
||||
reply_reason = "rule_miss"
|
||||
|
||||
should_reply = auto_on and bool(reply_text)
|
||||
|
||||
reply_msg_id = None
|
||||
if should_reply:
|
||||
is_ai_reply = 0 if used_rule_id else 1
|
||||
cur.execute("INSERT INTO messages (wx_user_id, wx_nickname, direction, content, is_ai_reply, rule_id, reply_strategy, reply_reason, created_at) VALUES (%s, %s, 'out', %s, %s, %s, %s, %s, %s)", (wx_user_id, wx_nickname, reply_text, is_ai_reply, used_rule_id, reply_strategy, reply_reason, now))
|
||||
reply_msg_id = cur.lastrowid
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
conn.rollback()
|
||||
log_event("ERROR", "api", "api.messages.receive", trace_id, "persist", "failed", "消息入库或回复处理失败", reason="db_error", extra={"error": str(exc)})
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
log_event("INFO", "audit", "audit.decision", trace_id, "decision", "ok", "回复决策完成", reason=reply_reason or "none", extra={"wx_user_id": wx_user_id, "strategy": reply_strategy, "should_reply": should_reply, "rule_id": used_rule_id or ""})
|
||||
log_event("INFO", "api", "api.messages.receive", trace_id, "done", "ok", "消息处理完成", extra={"in_message_id": in_msg_id, "reply_message_id": reply_msg_id or "", "should_reply": should_reply})
|
||||
return jsonify({"success": True, "should_reply": should_reply, "reply_text": reply_text, "in_message_id": in_msg_id, "reply_message_id": reply_msg_id})
|
||||
149
app/infrastructure/router/backend/rules.py
Normal file
149
app/infrastructure/router/backend/rules.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import jsonify, request, send_from_directory
|
||||
|
||||
from app.application.bot_controller import bot_controller
|
||||
from app.infrastructure.service.backend.config import ASSETS_DIR, STATIC_DIR
|
||||
from app.infrastructure.service.backend.db import SQLITE_DB_PATH, get_conn, get_setting, set_setting
|
||||
from app.infrastructure.service.logging.log_service import log_event, new_trace_id
|
||||
|
||||
LOCAL_SETTING_DEFAULTS = {
|
||||
"auto_reply_enabled": "1",
|
||||
"listener_enabled": "0",
|
||||
"listener_runtime_status": "stopped",
|
||||
"full_auto_reply_enabled": "0",
|
||||
"reply_fallback_mode": "ai",
|
||||
}
|
||||
LOCAL_SETTING_CACHE = {}
|
||||
|
||||
|
||||
def _load_local_settings(force=False):
|
||||
if LOCAL_SETTING_CACHE and not force:
|
||||
return
|
||||
for key, default in LOCAL_SETTING_DEFAULTS.items():
|
||||
LOCAL_SETTING_CACHE[key] = str(get_setting(key, default))
|
||||
|
||||
|
||||
|
||||
def _get_local_setting(key, default=None):
|
||||
_load_local_settings(force=True)
|
||||
return LOCAL_SETTING_CACHE.get(key, default)
|
||||
|
||||
|
||||
|
||||
def _set_local_setting(key, value):
|
||||
v = str(value)
|
||||
LOCAL_SETTING_CACHE[key] = v
|
||||
set_setting(key, v)
|
||||
|
||||
|
||||
|
||||
def _frontend_index_available():
|
||||
return STATIC_DIR != ASSETS_DIR and (STATIC_DIR / "index.html").exists()
|
||||
|
||||
|
||||
|
||||
def register_rule_routes(app):
|
||||
@app.route("/")
|
||||
@app.route("/index.html")
|
||||
def frontend_page():
|
||||
if _frontend_index_available():
|
||||
return send_from_directory(STATIC_DIR, "index.html")
|
||||
return send_from_directory(ASSETS_DIR, "admin.html")
|
||||
|
||||
@app.route("/admin.html")
|
||||
def admin_page():
|
||||
return send_from_directory(ASSETS_DIR, "admin.html")
|
||||
|
||||
@app.route("/api/rules", methods=["GET", "POST"])
|
||||
def api_rules():
|
||||
action = request.values.get("action", "list")
|
||||
conn = get_conn()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if action == "list":
|
||||
cur.execute("SELECT * FROM auto_reply_rules ORDER BY id DESC")
|
||||
rows = cur.fetchall()
|
||||
log_event("INFO", "api", "api.rules.list", new_trace_id("rules"), "query", "ok", "查询回复匹配配置", extra={"db_path": str(SQLITE_DB_PATH), "count": len(rows)})
|
||||
return jsonify({"success": True, "data": rows, "db_path": str(SQLITE_DB_PATH)})
|
||||
|
||||
if action == "create":
|
||||
keyword = (request.values.get("keyword") or "").strip()
|
||||
match_type = request.values.get("match_type", "contain")
|
||||
reply_text = (request.values.get("reply_text") or "").strip()
|
||||
is_active = int(request.values.get("is_active", 1))
|
||||
if not keyword or not reply_text:
|
||||
return jsonify({"success": False, "message": "关键词和回复内容不能为空"})
|
||||
if match_type not in ["contain", "equal"]:
|
||||
match_type = "contain"
|
||||
cur.execute("INSERT INTO auto_reply_rules(keyword, match_type, reply_text, is_active, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s)", (keyword, match_type, reply_text, is_active, now, now))
|
||||
conn.commit()
|
||||
return jsonify({"success": True})
|
||||
|
||||
if action == "toggle":
|
||||
rule_id = int(request.values.get("id", 0))
|
||||
is_active = int(request.values.get("is_active", 0))
|
||||
if rule_id <= 0:
|
||||
return jsonify({"success": False, "message": "参数错误"})
|
||||
cur.execute("UPDATE auto_reply_rules SET is_active = %s, updated_at = %s WHERE id = %s", (is_active, now, rule_id))
|
||||
conn.commit()
|
||||
return jsonify({"success": True})
|
||||
|
||||
if action == "delete":
|
||||
rule_id = int(request.values.get("id", 0))
|
||||
if rule_id <= 0:
|
||||
return jsonify({"success": False, "message": "参数错误"})
|
||||
cur.execute("DELETE FROM auto_reply_rules WHERE id = %s", (rule_id,))
|
||||
conn.commit()
|
||||
return jsonify({"success": True})
|
||||
|
||||
if action == "settings_get":
|
||||
auto_on = _get_local_setting("auto_reply_enabled", "1") == "1"
|
||||
full_auto_on = _get_local_setting("full_auto_reply_enabled", "0") == "1"
|
||||
reply_fallback_mode = (_get_local_setting("reply_fallback_mode", "ai") or "ai").strip() or "ai"
|
||||
bot_status = bot_controller.status()
|
||||
runtime_status = (bot_status.get("status") or "stopped").strip().lower()
|
||||
if runtime_status not in ["running", "starting", "stopping", "stopped", "error"]:
|
||||
runtime_status = "stopped"
|
||||
listener_on = runtime_status in ["running", "starting"]
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"auto_reply_enabled": auto_on,
|
||||
"listener_enabled": listener_on,
|
||||
"listener_runtime_status": runtime_status,
|
||||
"listener_intent_enabled": _get_local_setting("listener_enabled", "0") == "1",
|
||||
"full_auto_reply_enabled": full_auto_on,
|
||||
"reply_fallback_mode": reply_fallback_mode,
|
||||
})
|
||||
|
||||
if action == "settings_set":
|
||||
if "auto_reply_enabled" in request.values:
|
||||
auto_on = "1" if request.values.get("auto_reply_enabled", "1") == "1" else "0"
|
||||
_set_local_setting("auto_reply_enabled", auto_on)
|
||||
if "listener_enabled" in request.values:
|
||||
listener_on = "1" if request.values.get("listener_enabled", "0") == "1" else "0"
|
||||
_set_local_setting("listener_enabled", listener_on)
|
||||
if "listener_runtime_status" in request.values:
|
||||
runtime_status = (request.values.get("listener_runtime_status") or "stopped").strip().lower()
|
||||
if runtime_status not in ["running", "starting", "stopping", "stopped", "error"]:
|
||||
runtime_status = "stopped"
|
||||
_set_local_setting("listener_runtime_status", runtime_status)
|
||||
if "full_auto_reply_enabled" in request.values:
|
||||
full_auto_on = "1" if request.values.get("full_auto_reply_enabled", "0") == "1" else "0"
|
||||
_set_local_setting("full_auto_reply_enabled", full_auto_on)
|
||||
if "reply_fallback_mode" in request.values:
|
||||
reply_fallback_mode = (request.values.get("reply_fallback_mode") or "ai").strip() or "ai"
|
||||
_set_local_setting("reply_fallback_mode", reply_fallback_mode)
|
||||
return jsonify({"success": True})
|
||||
|
||||
if action == "messages_recent":
|
||||
limit = int(request.values.get("limit", 50))
|
||||
limit = max(1, min(100, limit))
|
||||
cur.execute("SELECT * FROM messages ORDER BY id DESC LIMIT %s", (limit,))
|
||||
rows = cur.fetchall()
|
||||
return jsonify({"success": True, "data": rows})
|
||||
|
||||
return jsonify({"success": False, "message": "未知操作"})
|
||||
finally:
|
||||
conn.close()
|
||||
9
app/infrastructure/router/backend_routes.py
Normal file
9
app/infrastructure/router/backend_routes.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from app.infrastructure.router.backend.rules import register_rule_routes
|
||||
from app.infrastructure.router.backend.messages import register_message_routes
|
||||
from app.infrastructure.router.backend.bot import register_bot_routes
|
||||
|
||||
|
||||
def register_routes(app):
|
||||
register_rule_routes(app)
|
||||
register_message_routes(app)
|
||||
register_bot_routes(app)
|
||||
Reference in New Issue
Block a user