初始提交:识流 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/application/__init__.py
Normal file
0
app/application/__init__.py
Normal file
134
app/application/bot_controller.py
Normal file
134
app/application/bot_controller.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
from app.infrastructure.wechat_multi_chat_bot import WechatMultiChatBot
|
||||
|
||||
|
||||
class BotController:
|
||||
def __init__(self):
|
||||
self._lock = threading.RLock()
|
||||
self._thread = None
|
||||
self._bot = None
|
||||
self._status = "stopped"
|
||||
self._last_error = ""
|
||||
self._started_at = ""
|
||||
self._stopped_at = ""
|
||||
self._listeners = {}
|
||||
self._listener_seq = 0
|
||||
|
||||
def start(self, backend_url):
|
||||
with self._lock:
|
||||
if self._thread and self._thread.is_alive():
|
||||
return self._status_payload_locked()
|
||||
self._status = "starting"
|
||||
self._last_error = ""
|
||||
self._stopped_at = ""
|
||||
os.environ["BACKEND_URL"] = backend_url
|
||||
self._thread = threading.Thread(target=self._run, daemon=True, name="WechatBotThread")
|
||||
self._thread.start()
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
return status
|
||||
|
||||
def stop(self):
|
||||
with self._lock:
|
||||
if not self._thread or not self._thread.is_alive():
|
||||
self._status = "stopped"
|
||||
self._bot = None
|
||||
self._stopped_at = self._now()
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
return status
|
||||
self._status = "stopping"
|
||||
bot = self._bot
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
if bot:
|
||||
bot.stop()
|
||||
return status
|
||||
|
||||
def status(self):
|
||||
listeners = None
|
||||
with self._lock:
|
||||
if self._status in {"running", "starting", "stopping"} and self._thread and not self._thread.is_alive():
|
||||
if self._status != "error":
|
||||
self._status = "stopped"
|
||||
self._bot = None
|
||||
self._stopped_at = self._stopped_at or self._now()
|
||||
listeners = list(self._listeners.values())
|
||||
status = self._status_payload_locked()
|
||||
if listeners is not None:
|
||||
self._notify_status_listeners(listeners, status)
|
||||
return status
|
||||
|
||||
def add_status_listener(self, callback, emit_initial=False):
|
||||
with self._lock:
|
||||
self._listener_seq += 1
|
||||
listener_id = self._listener_seq
|
||||
self._listeners[listener_id] = callback
|
||||
status = self._status_payload_locked()
|
||||
if emit_initial:
|
||||
try:
|
||||
callback(status)
|
||||
except Exception:
|
||||
pass
|
||||
return listener_id
|
||||
|
||||
def remove_status_listener(self, listener_id):
|
||||
with self._lock:
|
||||
self._listeners.pop(listener_id, None)
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
bot = WechatMultiChatBot()
|
||||
with self._lock:
|
||||
self._bot = bot
|
||||
self._status = "running"
|
||||
self._started_at = self._now()
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
bot.run_forever()
|
||||
with self._lock:
|
||||
self._status = "stopped"
|
||||
self._bot = None
|
||||
self._stopped_at = self._now()
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._status = "error"
|
||||
self._bot = None
|
||||
self._last_error = traceback.format_exc()
|
||||
self._stopped_at = self._now()
|
||||
status = self._status_payload_locked()
|
||||
listeners = list(self._listeners.values())
|
||||
self._notify_status_listeners(listeners, status)
|
||||
|
||||
def _status_payload_locked(self):
|
||||
return {
|
||||
"status": self._status,
|
||||
"running": self._status == "running",
|
||||
"last_error": self._last_error,
|
||||
"started_at": self._started_at,
|
||||
"stopped_at": self._stopped_at,
|
||||
}
|
||||
|
||||
def _notify_status_listeners(self, listeners, status):
|
||||
for callback in listeners:
|
||||
try:
|
||||
callback(status)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _now(self):
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
bot_controller = BotController()
|
||||
157
app/application/services.py
Normal file
157
app/application/services.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class AppConfig:
|
||||
def __init__(self):
|
||||
self.project_root = Path(__file__).resolve().parents[2]
|
||||
self.runtime_mode = self._normalize_runtime_mode(os.getenv("OPENCLAW_RUNTIME_MODE") or os.getenv("OPENCLAW_BACKEND_MODE"))
|
||||
self.venv_root = self.project_root / ".venv"
|
||||
self.venv_python = self.venv_root / "Scripts" / "python.exe"
|
||||
self.venv_pythonw = self.venv_root / "Scripts" / "pythonw.exe"
|
||||
self.host = os.getenv("APP_HOST", "127.0.0.1")
|
||||
self.port = int(os.getenv("APP_PORT", "5000"))
|
||||
|
||||
def _normalize_runtime_mode(self, value):
|
||||
mode = (value or "").strip().lower()
|
||||
if mode in {"release", "prod", "production", "bundle", "packaged"}:
|
||||
return "release"
|
||||
return "dev"
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return f"http://{self.host}:{self.port}"
|
||||
|
||||
@property
|
||||
def python_executable(self):
|
||||
return Path(sys.executable).resolve()
|
||||
|
||||
@property
|
||||
def python_prefix(self):
|
||||
return Path(sys.prefix).resolve()
|
||||
|
||||
@property
|
||||
def is_release_mode(self):
|
||||
return self.runtime_mode == "release"
|
||||
|
||||
@property
|
||||
def backend_entry(self):
|
||||
return self.project_root / "backend_main.py"
|
||||
|
||||
@property
|
||||
def is_running_in_project_venv(self):
|
||||
if self.is_release_mode:
|
||||
return False
|
||||
try:
|
||||
if self.python_prefix == self.venv_root.resolve():
|
||||
return True
|
||||
allowed_targets = {
|
||||
self.venv_python.resolve(),
|
||||
self.venv_pythonw.resolve(),
|
||||
}
|
||||
return self.python_executable in allowed_targets
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class BackendRuntime:
|
||||
def __init__(self, config=None):
|
||||
self.config = config or AppConfig()
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.config.host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self.config.port
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return self.config.base_url
|
||||
|
||||
@property
|
||||
def backend_entry(self):
|
||||
return self.config.backend_entry
|
||||
|
||||
def is_backend_alive(self, host=None, port=None, timeout=0.4):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
return sock.connect_ex((host or self.host, port or self.port)) == 0
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def launch_backend_process(self):
|
||||
if self.config.is_release_mode:
|
||||
raise RuntimeError("当前运行模式为发布态,不能从 Python 壳层再启动 backend_main.py")
|
||||
backend_python = self.config.venv_python
|
||||
if not backend_python.exists():
|
||||
raise RuntimeError(f"未找到后端解释器: {backend_python}")
|
||||
backend_entry = self.backend_entry
|
||||
if not backend_entry.exists():
|
||||
raise RuntimeError(f"未找到后端入口: {backend_entry}")
|
||||
env = os.environ.copy()
|
||||
env["OPENCLAW_PROJECT_VENV_REEXEC"] = "1"
|
||||
env.setdefault("OPENCLAW_BACKEND_MODE", "dev")
|
||||
env.setdefault("OPENCLAW_RUNTIME_MODE", "dev")
|
||||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
return subprocess.Popen(
|
||||
[str(backend_python), str(backend_entry)],
|
||||
cwd=str(self.config.project_root),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
|
||||
def terminate_backend_process(self, process, timeout=3):
|
||||
if process is None or process.poll() is not None:
|
||||
return
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=timeout)
|
||||
except Exception:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
|
||||
|
||||
class FrontendRuntime:
|
||||
def __init__(self, config=None, backend_runtime=None):
|
||||
self.config = config or AppConfig()
|
||||
self.backend_runtime = backend_runtime or BackendRuntime(self.config)
|
||||
self.frontend_dev_url = os.getenv("FRONTEND_DEV_URL", "http://127.0.0.1:5173").strip().rstrip("/")
|
||||
self.frontend_dist_index = self.config.project_root / "frontend" / "dist" / "index.html"
|
||||
self.backend_wait_timeout_ms = 15000
|
||||
self.backend_wait_interval_ms = 200
|
||||
|
||||
def resolve_frontend_entry_url(self, backend_url):
|
||||
if self.frontend_dev_url:
|
||||
try:
|
||||
requests.get(self.frontend_dev_url, timeout=1.2)
|
||||
return self.frontend_dev_url
|
||||
except Exception:
|
||||
pass
|
||||
if self.frontend_dist_index.exists():
|
||||
return backend_url.rstrip("/") + "/index.html"
|
||||
return backend_url.rstrip("/") + "/admin.html"
|
||||
|
||||
def wait_for_backend(self, process=None, timeout_ms=None, interval_ms=None):
|
||||
timeout_ms = timeout_ms or self.backend_wait_timeout_ms
|
||||
interval_ms = interval_ms or self.backend_wait_interval_ms
|
||||
started_at = time.time() * 1000
|
||||
while True:
|
||||
if self.backend_runtime.is_backend_alive():
|
||||
return True, ""
|
||||
if process is not None and process.poll() is not None:
|
||||
return False, f"独立后台进程已退出,退出码: {process.returncode}"
|
||||
elapsed = time.time() * 1000 - started_at
|
||||
if elapsed >= timeout_ms:
|
||||
return False, f"后台启动超时,{int(elapsed)}ms 内未监听 {self.backend_runtime.host}:{self.backend_runtime.port}"
|
||||
time.sleep(interval_ms / 1000.0)
|
||||
Reference in New Issue
Block a user