- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
298 lines
11 KiB
Python
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()
|