front/xhs-agent-desktop/app/main_window.py
zzh 5a26ae0048 feat: 小红书模块迁移、登录重构与 xhs-agent-desktop 桌面 Agent
- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手)

- 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度

- 登录页重构,整合微信登录与 OMS JWT 认证

- 订单、渠道、用户中心等页面与 API 增强

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:13:13 +08:00

298 lines
11 KiB
Python

"""OMS 小红书 Agent 桌面主窗口。"""
from __future__ import annotations
import logging
import sys
from datetime import datetime
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QCloseEvent, QFont
from PyQt6.QtWidgets import (
QApplication,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
QPlainTextEdit,
QPushButton,
QSpinBox,
QVBoxLayout,
QWidget,
)
from app.agent_worker import AgentWorker
from app.config_store import AgentSettings
from app.engine_path import ensure_runtime_importable, extension_dir
logger = logging.getLogger(__name__)
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("OMS 小红书 Agent")
self.setMinimumSize(640, 520)
self.resize(720, 580)
self._settings = AgentSettings.load()
self._worker: AgentWorker | None = None
self._bridge = None
root = QWidget()
self.setCentralWidget(root)
layout = QVBoxLayout(root)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(12)
layout.addWidget(self._build_intro())
layout.addWidget(self._build_connection_group())
layout.addWidget(self._build_local_group())
layout.addWidget(self._build_status_group())
layout.addWidget(self._build_log_group(), 1)
self._apply_settings_to_form()
self._try_init_bridge()
def _build_intro(self) -> QLabel:
lbl = QLabel(
"独立桌面端:内置 Chrome 扩展 + Bridge 引擎,连接 OMS 后由网页远程调度。"
"在网页「小红书 → 启动服务 → Agent 连接指南」复制 API 地址与 Token。"
)
lbl.setWordWrap(True)
lbl.setObjectName("intro")
lbl.setStyleSheet("color:#64748b;font-size:12px;")
return lbl
def _build_connection_group(self) -> QGroupBox:
box = QGroupBox("OMS 连接")
form = QFormLayout(box)
form.setSpacing(8)
self.input_oms_url = QLineEdit()
self.input_oms_url.setPlaceholderText("http://localhost:5281/admin")
form.addRow("API 地址", self.input_oms_url)
self.input_token = QLineEdit()
self.input_token.setEchoMode(QLineEdit.EchoMode.Password)
self.input_token.setPlaceholderText("从网页「Agent 连接指南」复制 JWT")
form.addRow("JWT Token", self.input_token)
self.input_device_name = QLineEdit()
self.input_device_name.setPlaceholderText("如:办公室电脑")
form.addRow("设备名称", self.input_device_name)
self.input_bridge_port = QSpinBox()
self.input_bridge_port.setRange(1024, 65535)
self.input_bridge_port.setValue(9333)
form.addRow("Bridge 端口", self.input_bridge_port)
self.lbl_extension = QLabel(extension_dir().as_posix())
self.lbl_extension.setWordWrap(True)
self.lbl_extension.setStyleSheet("color:#475569;font-size:11px;")
form.addRow("Chrome 扩展", self.lbl_extension)
row = QHBoxLayout()
self.btn_save = QPushButton("保存配置")
self.btn_save.clicked.connect(self._save_settings)
self.btn_connect = QPushButton("连接 OMS")
self.btn_connect.setStyleSheet(
"background:#4F46E5;color:#fff;padding:6px 16px;border-radius:4px;"
)
self.btn_connect.clicked.connect(self._toggle_agent)
row.addWidget(self.btn_save)
row.addStretch(1)
row.addWidget(self.btn_connect)
form.addRow("", row)
return box
def _build_local_group(self) -> QGroupBox:
box = QGroupBox("本机引擎(也可在网页远程控制)")
row = QHBoxLayout(box)
self.btn_engine_start = QPushButton("启动引擎")
self.btn_engine_stop = QPushButton("停止引擎")
self.btn_login = QPushButton("浏览器登录")
self.btn_check = QPushButton("刷新状态")
for btn in (self.btn_engine_start, self.btn_engine_stop, self.btn_login, self.btn_check):
row.addWidget(btn)
self.btn_engine_start.clicked.connect(self._local_start_engine)
self.btn_engine_stop.clicked.connect(self._local_stop_engine)
self.btn_login.clicked.connect(self._local_browser_login)
self.btn_check.clicked.connect(self._refresh_local_status)
return box
def _build_status_group(self) -> QGroupBox:
box = QGroupBox("状态")
grid = QFormLayout(box)
mono = QFont("Consolas", 10)
self.lbl_device_id = QLabel("-")
self.lbl_device_id.setFont(mono)
self.lbl_agent_state = QLabel("未连接")
self.lbl_engine = QLabel("-")
self.lbl_extension = QLabel("-")
self.lbl_login = QLabel("-")
grid.addRow("设备 ID", self.lbl_device_id)
grid.addRow("Agent", self.lbl_agent_state)
grid.addRow("引擎", self.lbl_engine)
grid.addRow("扩展", self.lbl_extension)
grid.addRow("小红书", self.lbl_login)
return box
def _build_log_group(self) -> QGroupBox:
box = QGroupBox("运行日志")
lay = QVBoxLayout(box)
self.log_view = QPlainTextEdit()
self.log_view.setReadOnly(True)
self.log_view.setMaximumBlockCount(500)
lay.addWidget(self.log_view)
return box
def _apply_settings_to_form(self) -> None:
self.input_oms_url.setText(self._settings.oms_url)
self.input_token.setText(self._settings.token)
self.input_device_name.setText(self._settings.device_name)
self.input_bridge_port.setValue(self._settings.bridge_port)
def _collect_settings(self) -> AgentSettings:
return AgentSettings(
oms_url=self.input_oms_url.text().strip() or AgentSettings().oms_url,
token=self.input_token.text().strip(),
device_name=self.input_device_name.text().strip(),
bridge_port=self.input_bridge_port.value(),
)
def _save_settings(self) -> None:
self._settings = self._collect_settings()
self._settings.save()
self._append_log("配置已保存")
def _try_init_bridge(self) -> None:
try:
ensure_runtime_importable()
from xhs_bridge.service import XhsBridgeService
self._bridge = XhsBridgeService()
self.lbl_device_id.setText(self._bridge.license.device_id)
self._refresh_local_status()
self._append_log("内置运行时已就绪")
except Exception as exc:
self._append_log(f"初始化失败: {exc}")
QMessageBox.warning(self, "运行时错误", str(exc))
def _toggle_agent(self) -> None:
if self._worker and self._worker.isRunning():
self._append_log("正在断开…")
self._worker.request_stop()
self.btn_connect.setEnabled(False)
return
self._settings = self._collect_settings()
if not self._settings.token:
QMessageBox.warning(self, "提示", "请填写 JWT Token")
return
self._settings.save()
self._worker = AgentWorker(self._settings)
self._worker.log_line.connect(self._append_log)
self._worker.status_changed.connect(self._on_agent_status)
self._worker.started_ok.connect(self._on_agent_started)
self._worker.failed.connect(self._on_agent_failed)
self._worker.stopped.connect(self._on_agent_stopped)
self._worker.start()
self.btn_connect.setText("断开连接")
self.lbl_agent_state.setText("连接中…")
def _on_agent_started(self, info: dict) -> None:
self.lbl_device_id.setText(str(info.get("device_id", "-")))
self.lbl_agent_state.setText("已连接 · 等待网页下发任务")
self._append_log("Agent 主循环已启动,请在网页「启动服务」查看在线状态")
def _on_agent_status(self, st: dict) -> None:
self.lbl_engine.setText("运行中" if st.get("engine_running") else "未启动")
self.lbl_extension.setText("已连接" if st.get("extension_connected") else "未连接")
self.lbl_login.setText("已登录" if st.get("xhs_logged_in") else "未登录")
def _on_agent_failed(self, msg: str) -> None:
self._append_log(f"错误: {msg}")
QMessageBox.critical(self, "Agent 异常", msg)
self._on_agent_stopped()
def _on_agent_stopped(self) -> None:
self.btn_connect.setEnabled(True)
self.btn_connect.setText("连接 OMS")
self.lbl_agent_state.setText("未连接")
self._worker = None
def _local_start_engine(self) -> None:
if not self._bridge:
return
try:
pid = self._bridge.start_engine()
self._append_log(f"引擎已启动 pid={pid}")
self._refresh_local_status()
except Exception as exc:
QMessageBox.warning(self, "启动失败", str(exc))
def _local_stop_engine(self) -> None:
if not self._bridge:
return
try:
self._bridge.stop_engine()
self._append_log("引擎已停止")
self._refresh_local_status()
except Exception as exc:
QMessageBox.warning(self, "停止失败", str(exc))
def _local_browser_login(self) -> None:
if not self._bridge:
return
try:
self._bridge.ensure_engine_running()
result = self._bridge.browser_login()
self._append_log(str(result.get("message") or result))
self._refresh_local_status()
except Exception as exc:
QMessageBox.warning(self, "登录", str(exc))
def _refresh_local_status(self) -> None:
if not self._bridge:
return
try:
st = self._bridge.status()
self.lbl_engine.setText("运行中" if st.get("bridge_running") else "未启动")
self.lbl_extension.setText("已连接" if st.get("extension_connected") else "未连接")
self.lbl_login.setText("已登录" if st.get("login_ok") else "未登录")
except Exception as exc:
self._append_log(f"状态刷新失败: {exc}")
def _append_log(self, text: str) -> None:
ts = datetime.now().strftime("%H:%M:%S")
self.log_view.appendPlainText(f"[{ts}] {text}")
def closeEvent(self, event: QCloseEvent) -> None:
if self._worker and self._worker.isRunning():
box = QMessageBox.question(
self,
"退出",
"Agent 正在运行,确定退出?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if box != QMessageBox.StandardButton.Yes:
event.ignore()
return
self._worker.request_stop()
self._worker.wait(3000)
event.accept()
def run_app() -> int:
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
app = QApplication(sys.argv)
app.setApplicationName("OMS XHS Agent")
win = MainWindow()
win.show()
return app.exec()