435 lines
16 KiB
Python
435 lines
16 KiB
Python
"""Tab 4: account management UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ..widgets import *
|
|
from ..workers import AccountLoginCheckWorker as _RealAccountLoginCheckWorker
|
|
class AccountDialog(QDialog):
|
|
"""Dialog for adding or editing one account."""
|
|
|
|
def __init__(self, parent=None, account=None, default_port=9222, config=None):
|
|
super().__init__(parent)
|
|
self._account = account
|
|
self._config = config
|
|
self.setWindowTitle("编辑账号" if account else "新增账号")
|
|
|
|
self.account_name_edit = QLineEdit()
|
|
self.alias_edit = QLineEdit()
|
|
self.region_host_edit = QLineEdit(accounts.DEFAULT_REGION_HOST)
|
|
self.debug_port_spin = QSpinBox()
|
|
self.debug_port_spin.setRange(1, 65535)
|
|
self.debug_port_spin.setValue(int(default_port))
|
|
self.password_edit = QLineEdit()
|
|
self.password_edit.setEchoMode(QLineEdit.Password)
|
|
self.note_edit = QPlainTextEdit()
|
|
self.note_edit.setMaximumHeight(76)
|
|
self.slug_edit = QLineEdit()
|
|
self.slug_edit.setReadOnly(True)
|
|
self.user_data_dir_edit = QLineEdit()
|
|
self.user_data_dir_edit.setReadOnly(True)
|
|
|
|
if account is not None:
|
|
self.account_name_edit.setText(account.account_name)
|
|
self.alias_edit.setText(account.alias)
|
|
self.region_host_edit.setText(account.region_host)
|
|
self.debug_port_spin.setValue(int(account.debug_port))
|
|
self.password_edit.setText(account.password or "")
|
|
self.note_edit.setPlainText(account.note or "")
|
|
self.slug_edit.setText(account.slug)
|
|
self.user_data_dir_edit.setText(account.user_data_dir)
|
|
|
|
form = QFormLayout()
|
|
form.addRow("账号名", self.account_name_edit)
|
|
form.addRow("别名", self.alias_edit)
|
|
form.addRow("地区", self.region_host_edit)
|
|
form.addRow("调试端口", self.debug_port_spin)
|
|
form.addRow("密码", self.password_edit)
|
|
form.addRow("备注", self.note_edit)
|
|
form.addRow("slug", self.slug_edit)
|
|
form.addRow("数据目录", self.user_data_dir_edit)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.addLayout(form)
|
|
layout.addWidget(buttons)
|
|
|
|
self.alias_edit.textChanged.connect(self._update_path_preview)
|
|
self._update_path_preview()
|
|
|
|
def _update_path_preview(self):
|
|
alias = self.alias_edit.text().strip()
|
|
if not alias:
|
|
self.slug_edit.clear()
|
|
self.user_data_dir_edit.clear()
|
|
return
|
|
try:
|
|
slug = account_config.make_slug(alias)
|
|
self.slug_edit.setText(slug)
|
|
if self._account is not None and alias == self._account.alias:
|
|
self.user_data_dir_edit.setText(self._account.user_data_dir)
|
|
else:
|
|
self.user_data_dir_edit.setText(
|
|
accounts.preview_user_data_dir(alias, config=self._config)
|
|
)
|
|
except Exception:
|
|
self.slug_edit.clear()
|
|
self.user_data_dir_edit.clear()
|
|
|
|
def values(self):
|
|
return {
|
|
"account_name": self.account_name_edit.text().strip(),
|
|
"alias": self.alias_edit.text().strip(),
|
|
"region_host": self.region_host_edit.text().strip(),
|
|
"debug_port": self.debug_port_spin.value(),
|
|
"password": self.password_edit.text(),
|
|
"note": self.note_edit.toPlainText().strip(),
|
|
}
|
|
|
|
|
|
|
|
|
|
def _new_account_dialog(*args, **kwargs):
|
|
dialog_class = _package_attr("AccountDialog", AccountDialog)
|
|
return dialog_class(*args, **kwargs)
|
|
|
|
|
|
def AccountLoginCheckWorker(*args, **kwargs):
|
|
return _call_package_attr("AccountLoginCheckWorker", _RealAccountLoginCheckWorker, *args, **kwargs)
|
|
|
|
class AccountsTab(QWidget):
|
|
COLUMNS = ["账号名", "别名", "地区", "端口", "登录状态", "备注"]
|
|
|
|
def __init__(self, parent=None, db_path=None, config=None, status_callback=None):
|
|
super().__init__(parent)
|
|
self.config = appconfig.load_config() if config is None else config
|
|
self.db_path = _database_path(db_path, self.config)
|
|
self.status_callback = status_callback
|
|
self.account_rows = []
|
|
self.login_statuses = {}
|
|
self.threads = []
|
|
|
|
self.table = QTableWidget(0, len(self.COLUMNS))
|
|
self.table.setHorizontalHeaderLabels(self.COLUMNS)
|
|
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
self.table.verticalHeader().setVisible(False)
|
|
|
|
self.add_button = QPushButton("新增")
|
|
self.edit_button = QPushButton("编辑")
|
|
self.delete_button = QPushButton("删除")
|
|
self.delete_button.setObjectName("deleteAccountButton")
|
|
self.delete_button.setStyleSheet(_danger_outline_button_style("deleteAccountButton"))
|
|
self.launch_button = QPushButton("启动登录")
|
|
self.check_button = QPushButton("检测登录")
|
|
self.shortcut_button = QPushButton("快捷方式")
|
|
|
|
toolbar = QHBoxLayout()
|
|
for button in (
|
|
self.add_button,
|
|
self.edit_button,
|
|
self.delete_button,
|
|
self.launch_button,
|
|
self.check_button,
|
|
self.shortcut_button,
|
|
):
|
|
toolbar.addWidget(button)
|
|
toolbar.addStretch(1)
|
|
|
|
self.empty_label = QLabel("")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(18, 18, 18, 18)
|
|
layout.addLayout(toolbar)
|
|
layout.addWidget(self.table, 1)
|
|
layout.addWidget(self.empty_label)
|
|
|
|
self.add_button.clicked.connect(self.add_account)
|
|
self.edit_button.clicked.connect(self.edit_account)
|
|
self.delete_button.clicked.connect(self.delete_account)
|
|
self.launch_button.clicked.connect(self.launch_login)
|
|
self.check_button.clicked.connect(self.check_login)
|
|
self.shortcut_button.clicked.connect(self.create_shortcut)
|
|
self.table.itemSelectionChanged.connect(self._update_button_state)
|
|
self.table.doubleClicked.connect(self.edit_account)
|
|
|
|
self.refresh_accounts()
|
|
|
|
def _set_status(self, message):
|
|
if self.status_callback is not None:
|
|
self.status_callback(message)
|
|
|
|
def _selected_account(self):
|
|
selected = self.table.selectionModel().selectedRows()
|
|
if not selected:
|
|
return None
|
|
row = selected[0].row()
|
|
if row < 0 or row >= len(self.account_rows):
|
|
return None
|
|
return self.account_rows[row]
|
|
|
|
def _update_button_state(self):
|
|
has_selection = self._selected_account() is not None
|
|
for button in (
|
|
self.edit_button,
|
|
self.delete_button,
|
|
self.launch_button,
|
|
self.check_button,
|
|
self.shortcut_button,
|
|
):
|
|
button.setEnabled(has_selection)
|
|
|
|
def refresh_accounts(self):
|
|
try:
|
|
self.account_rows = accounts.list_accounts(
|
|
path=self.db_path,
|
|
config=self.config,
|
|
)
|
|
except Exception as exc:
|
|
self.account_rows = []
|
|
self._set_status(f"账号读取失败:{exc}")
|
|
|
|
self.table.setRowCount(len(self.account_rows))
|
|
for row, account in enumerate(self.account_rows):
|
|
status = self.login_statuses.get(account.alias, "未知")
|
|
values = [
|
|
account.account_name,
|
|
account.alias,
|
|
account.region_host,
|
|
str(account.debug_port),
|
|
_login_status_display(status),
|
|
account.note or "",
|
|
]
|
|
for column, value in enumerate(values):
|
|
item = QTableWidgetItem(value)
|
|
if column == 4:
|
|
item.setForeground(_login_status_color(status))
|
|
self.table.setItem(row, column, item)
|
|
self.empty_label.setText("" if self.account_rows else "暂无账号")
|
|
self._update_button_state()
|
|
|
|
def _show_error(self, message):
|
|
QMessageBox.warning(self, "账号管理", str(message))
|
|
self._set_status(str(message))
|
|
|
|
def add_account(self, checked=False):
|
|
try:
|
|
default_port = accounts.next_debug_port(
|
|
path=self.db_path,
|
|
config=self.config,
|
|
)
|
|
except Exception:
|
|
default_port = appconfig.default_debug_port(self.config)
|
|
dialog = _new_account_dialog(self, default_port=default_port, config=self.config)
|
|
if dialog.exec() != QDialog.Accepted:
|
|
return
|
|
values = dialog.values()
|
|
if self._should_warn_plaintext_password(values):
|
|
self._show_plaintext_password_warning()
|
|
try:
|
|
accounts.create_account(
|
|
path=self.db_path,
|
|
config=self.config,
|
|
**values,
|
|
)
|
|
except Exception as exc:
|
|
self._show_error(exc)
|
|
return
|
|
self.refresh_accounts()
|
|
self._set_status("账号已新增")
|
|
|
|
def edit_account(self, checked=False):
|
|
account = self._selected_account()
|
|
if account is None:
|
|
return
|
|
dialog = _new_account_dialog(
|
|
self,
|
|
account=account,
|
|
default_port=account.debug_port,
|
|
config=self.config,
|
|
)
|
|
if dialog.exec() != QDialog.Accepted:
|
|
return
|
|
values = dialog.values()
|
|
if self._should_warn_plaintext_password(values, account):
|
|
self._show_plaintext_password_warning()
|
|
try:
|
|
updated = accounts.update_account(
|
|
account.alias,
|
|
path=self.db_path,
|
|
config=self.config,
|
|
**values,
|
|
)
|
|
except Exception as exc:
|
|
self._show_error(exc)
|
|
return
|
|
if updated.alias != account.alias:
|
|
self.login_statuses.pop(account.alias, None)
|
|
self.refresh_accounts()
|
|
self._set_status("账号已保存")
|
|
|
|
def delete_account(self, checked=False):
|
|
account = self._selected_account()
|
|
if account is None:
|
|
return
|
|
answer = QMessageBox.question(
|
|
self,
|
|
"删除账号",
|
|
f"确认删除账号「{account.alias}」?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No,
|
|
)
|
|
if answer != QMessageBox.Yes:
|
|
return
|
|
try:
|
|
accounts.delete_account(account.alias, path=self.db_path, config=self.config)
|
|
except Exception as exc:
|
|
self._show_error(exc)
|
|
return
|
|
self.login_statuses.pop(account.alias, None)
|
|
self.refresh_accounts()
|
|
self._set_status("账号已删除")
|
|
|
|
def launch_login(self, checked=False):
|
|
account = self._selected_account()
|
|
if account is None:
|
|
return
|
|
run_id = _safe_create_run_log(
|
|
"chrome_launch",
|
|
db_path=self.db_path,
|
|
total=1,
|
|
options={"alias": account.alias, "debug_port": account.debug_port},
|
|
)
|
|
started = time.monotonic()
|
|
_safe_add_run_log_event(
|
|
run_id,
|
|
f"step=launch_chrome result=start detail=账号 {account.alias} debug_port={account.debug_port}",
|
|
db_path=self.db_path,
|
|
account=account,
|
|
)
|
|
try:
|
|
process = accounts.launch_for_login(account, config=self.config)
|
|
except Exception as exc:
|
|
elapsed_ms = _elapsed_ms(started)
|
|
safe_error = diagnostics.redact_log_text(str(exc) or exc.__class__.__name__)
|
|
_safe_add_run_log_event(
|
|
run_id,
|
|
f"step=launch_chrome result=failed detail={safe_error} elapsed_ms={elapsed_ms}",
|
|
db_path=self.db_path,
|
|
account=account,
|
|
level="error",
|
|
)
|
|
_safe_write_diagnostic_log(
|
|
"Chrome启动失败",
|
|
level="ERROR",
|
|
step="launch_chrome",
|
|
account=account,
|
|
elapsed_ms=elapsed_ms,
|
|
payload={"alias": account.alias, "debug_port": account.debug_port, "error": safe_error},
|
|
exc=exc,
|
|
log_dir=diagnostics.DEFAULT_LOG_DIR,
|
|
)
|
|
_safe_finish_run_log(
|
|
run_id,
|
|
db_path=self.db_path,
|
|
status="failed",
|
|
done=0,
|
|
failed_count=1,
|
|
summary_json={"ok": False, "alias": account.alias, "error": safe_error},
|
|
)
|
|
self._show_error(safe_error)
|
|
return
|
|
elapsed_ms = _elapsed_ms(started)
|
|
pid = getattr(process, "pid", None)
|
|
_safe_add_run_log_event(
|
|
run_id,
|
|
f"step=launch_chrome result=success detail=账号 {account.alias} pid={pid or ''} elapsed_ms={elapsed_ms}",
|
|
db_path=self.db_path,
|
|
account=account,
|
|
)
|
|
_safe_finish_run_log(
|
|
run_id,
|
|
db_path=self.db_path,
|
|
status="done",
|
|
done=1,
|
|
success_count=1,
|
|
failed_count=0,
|
|
summary_json={"ok": True, "alias": account.alias, "debug_port": account.debug_port, "pid": pid},
|
|
)
|
|
self.login_statuses[account.alias] = "已启动"
|
|
self.refresh_accounts()
|
|
self._set_status("Chrome 已启动,请人工登录")
|
|
|
|
def check_login(self, checked=False):
|
|
account = self._selected_account()
|
|
if account is None:
|
|
return
|
|
self.login_statuses[account.alias] = "检测中"
|
|
self.refresh_accounts()
|
|
worker = AccountLoginCheckWorker(
|
|
account,
|
|
db_path=self.db_path,
|
|
config=self.config,
|
|
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
|
)
|
|
worker.finished.connect(self._on_login_check_finished)
|
|
worker.failed.connect(
|
|
lambda _task_id, error, alias=account.alias: self._on_login_check_failed(
|
|
alias,
|
|
error,
|
|
)
|
|
)
|
|
thread = run_worker(worker, start=False)
|
|
thread.finished.connect(lambda: self._forget_thread(thread))
|
|
self.threads.append(thread)
|
|
thread.start()
|
|
|
|
def _forget_thread(self, thread):
|
|
if thread in self.threads:
|
|
self.threads.remove(thread)
|
|
|
|
def _on_login_check_finished(self, payload):
|
|
if payload.get("ok") is False and not payload.get("alias"):
|
|
return
|
|
alias = payload.get("alias")
|
|
status = payload.get("status") or {}
|
|
if alias:
|
|
self.login_statuses[alias] = accounts.login_status_text(status)
|
|
self.refresh_accounts()
|
|
self._set_status("登录状态已刷新")
|
|
|
|
def _on_login_check_failed(self, alias, error):
|
|
self.login_statuses[alias] = "检测失败"
|
|
self.refresh_accounts()
|
|
self._set_status(f"登录检测失败:{error}")
|
|
|
|
def create_shortcut(self, checked=False):
|
|
account = self._selected_account()
|
|
if account is None:
|
|
return
|
|
try:
|
|
shortcut_path = accounts.create_shortcut(account, config=self.config)
|
|
except Exception as exc:
|
|
self._show_error(exc)
|
|
return
|
|
self._set_status(f"快捷方式已生成:{shortcut_path}")
|
|
QMessageBox.information(self, "账号管理", f"快捷方式已生成:\n{shortcut_path}")
|
|
|
|
def _should_warn_plaintext_password(self, values, account=None):
|
|
new_password = str((values or {}).get("password") or "")
|
|
current_password = str(getattr(account, "password", None) or "")
|
|
return bool(new_password) and new_password != current_password
|
|
|
|
def _show_plaintext_password_warning(self):
|
|
QMessageBox.warning(
|
|
self,
|
|
PLAINTEXT_SECRET_TITLE,
|
|
PLAINTEXT_PASSWORD_WARNING,
|
|
)
|
|
|
|
|