From 4c3b0979771cf99ef4ce70838fc0496132972215 Mon Sep 17 00:00:00 2001 From: Nam Pham Dinh Thanh Date: Tue, 25 Aug 2026 21:15:15 +0900 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20xo=C3=A1=20108=20d=C3=B2ng=20MS?= =?UTF-8?q?365=20ch=E1=BA=BFt=20trong=20settings=5Fdialog.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sót lại từ lần dời UI Connector sang Monitoring → Tools → Connector. Năm hàm: _refresh_ms365_status 13 _show_ms365_device_code 52 _ms365_sign_in 34 _ms365_sign_out 4 _close_ms365_code_dialog 5 Chứng minh chết trước khi xoá, không xoá theo cảm tính: * Dựng đồ thị lời gọi bằng ast: **mọi** lời gọi tới năm hàm này đều xuất phát từ bên trong chính năm hàm đó. Không một đường vào nào từ ngoài cụm — cả trong file lẫn toàn repo. * Ba thuộc tính chúng đọc — ms365_status, ms365_signin_btn, ms365_signout_btn — **chưa từng được gán ở đâu**. Gọi vào là AttributeError, không phải chạy sai mà là sập. * _ms365_workers chỉ được append bên trong _ms365_sign_in, nên chết theo. Dọn kèm 6 import chỉ còn dòng import: AgentWorker, icon, EXT_CATEGORIES, ExtConnectorEditDialog, AppContext, QTreeWidget. Viết lại docstring đầu file — bản cũ vẫn mô tả file này chứa nhóm Connector (CAD/CAE/MS365/Other), thứ đã không còn ở đây từ lâu. settings_dialog.py: 407 -> 303 dòng. Cộng cả R08-T07 thì từ 727 xuống 303. 632 test xanh. check_dialogs, check_no_hscroll, check_design_parity, check_orphans, check_probes_bite đều qua. Ghi lại một phát hiện phụ, CHƯA xử lý: i18n.py có 28 khoá settings.ms365_* mồ côi — 20 khoá đã không ai dùng từ trước lần dời connector, 8 khoá vừa mồ côi theo commit này. Chỉ 2 khoá còn sống (ms365_local_connected, ms365_local_none, dùng ở ui/connectors_panel.py). Xoá khoá dịch là đụng vào dữ liệu ba ngôn ngữ ở file khác nên để anh Nam quyết riêng. Co-Authored-By: Claude Opus 5 --- ui/settings_dialog.py | 134 +++++------------------------------------- 1 file changed, 15 insertions(+), 119 deletions(-) diff --git a/ui/settings_dialog.py b/ui/settings_dialog.py index ea759f6..338b926 100644 --- a/ui/settings_dialog.py +++ b/ui/settings_dialog.py @@ -1,6 +1,16 @@ -"""Settings dialog: AI provider, Sandbox, the unified Connectors (MCP) group -(CAD / CAE / MS365 / Other — MCP servers + REST connectors in one place), -and the merged "Parameter" group (Cowork / Attachments / GraphRAG caps).""" +"""Hộp thoại Cài đặt — khung lắp ráp. + +Năm mục, mỗi mục một trang: Chung, AI Provider, Bảo mật sandbox, Tham số, +Auto Model Routing. Bốn mục đầu... đúng hơn: bốn trong năm mục đã bóc sang +``presentation/settings/`` (R08-T07); file này còn giữ mục Bảo mật sandbox, +phần lắp ráp danh sách mục bên trái, và ``_save`` gọi ``apply_to`` của từng +widget con. + +Không còn phần Connector nào ở đây: nó đã dời sang Monitoring → Tools → +Connector từ trước. Ngày 25/08 dọn nốt 108 dòng MS365 chết còn sót lại của +lần dời đó — năm hàm gọi lẫn nhau, không đường vào, và đọc ba thuộc tính +chưa từng được gán nên gọi vào là AttributeError. +""" from __future__ import annotations from PySide6.QtCore import Qt @@ -8,17 +18,13 @@ from PySide6.QtGui import QGuiApplication from PySide6.QtWidgets import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, - QMessageBox, QPushButton, QScrollArea, QSpinBox, QTreeWidget, + QMessageBox, QPushButton, QScrollArea, QSpinBox, QTreeWidgetItem, QVBoxLayout, QWidget, ) -from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES -from ..core.worker import AgentWorker from ..i18n import tr -from ..state import AppContext -from .icons import icon, IconLabel +from .icons import IconLabel from .widgets import ToggleSwitch -from .ext_connector_dialog import ExtConnectorEditDialog from ..presentation.settings.general_settings_widget import GeneralSettingsWidget @@ -133,9 +139,7 @@ class SettingsDialog(QDialog): root.addWidget(self.sandbox_group) # Connectors (MCP / REST API) are managed entirely in Monitoring → Tools - # → Connector now — no connector UI in Settings. (_ms365_workers is kept # for the dead-but-retained MS365 OAuth sign-in handlers below.) - self._ms365_workers = [] # --- Parameter --- # Đã bóc sang presentation/settings/parameter_settings_widget.py (R08-T07). @@ -249,118 +253,10 @@ class SettingsDialog(QDialog): # ---- MS365 zero-config sign-in ("connect like Claude") --------------- - def _refresh_ms365_status(self) -> None: - from ..core.ms365_auth import current_identity - who = current_identity(self.ctx.config) - if who: - self.ms365_status.setText(tr("settings.ms365_signed_in", who=who)) - self.ms365_signin_btn.setEnabled(False) - self.ms365_signout_btn.setEnabled(True) - else: - self.ms365_status.setText(tr("settings.ms365_signed_out")) - self.ms365_signin_btn.setEnabled(True) - self.ms365_signout_btn.setEnabled(False) - self.ms365_signin_btn.setText(tr("settings.ms365_signin_btn")) - self.ms365_signout_btn.setText(tr("settings.ms365_signout_btn")) - def _ms365_sign_in(self) -> None: - from ..core.ms365_auth import current_identity, sign_in - self.ms365_signin_btn.setEnabled(False) - self.ms365_status.setText(tr("settings.ms365_signing_in")) - cfg = self.ctx.config - def job(worker): - # on_code fires (worker thread) with the MSAL device-flow dict — - # marshal it to the UI thread via the worker's event signal. - return sign_in(lambda flow: worker.event.emit({"device_flow": flow}), cfg) - def on_event(ev: dict) -> None: - if "device_flow" in ev: - self._show_ms365_device_code(ev["device_flow"]) - def done(_result) -> None: - self._close_ms365_code_dialog() - self.ctx.save() - self._refresh_ms365_status() - QMessageBox.information( - self, tr("settings.ms365_signin_btn"), - tr("settings.ms365_signed_in", who=current_identity(cfg))) - - def failed(err: str) -> None: - self._close_ms365_code_dialog() - self._refresh_ms365_status() - QMessageBox.warning(self, tr("settings.ms365_signin_btn"), err) - - w = AgentWorker(job) - w.event.connect(on_event) - w.finished_ok.connect(done) - w.failed.connect(failed) - self._ms365_workers.append(w) - w.start() - - def _close_ms365_code_dialog(self) -> None: - dlg = getattr(self, "_ms365_code_dialog", None) - if dlg is not None: - dlg.close() - self._ms365_code_dialog = None - - def _show_ms365_device_code(self, flow: dict) -> None: - """Auto-open the sign-in page + show the one-time code in a COPYABLE, - non-modal dialog (so the worker keeps polling and can auto-close it on - success). The code is also copied to the clipboard immediately.""" - import webbrowser - - code = flow.get("user_code", "") - url = flow.get("verification_uri", "https://microsoft.com/devicelogin") - # Auto-copy the code so the user can just paste it. - QGuiApplication.clipboard().setText(code) - # Auto-open the browser to the (code-prefilled, if available) sign-in page. - try: - webbrowser.open(flow.get("verification_uri_complete") or url) - except Exception: # noqa: BLE001 — a headless box just shows the link to click - pass - - self._close_ms365_code_dialog() - dlg = QDialog(self) - dlg.setWindowTitle(tr("settings.ms365_signin_btn")) - dlg.setMinimumWidth(420) - lay = QVBoxLayout(dlg) - info = QLabel(tr("settings.ms365_code_hint", url=url)) - info.setWordWrap(True) - info.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextBrowserInteraction) - info.setOpenExternalLinks(True) - lay.addWidget(info) - - code_row = QHBoxLayout() - code_edit = QLineEdit(code) - code_edit.setReadOnly(True) - f = code_edit.font() - f.setPointSize(f.pointSize() + 4) - f.setBold(True) - code_edit.setFont(f) - code_edit.setCursorPosition(0) - copy_btn = QPushButton(tr("settings.ms365_copy_code")) - copy_btn.setIcon(icon("document")) - copy_btn.clicked.connect(lambda: QGuiApplication.clipboard().setText(code)) - open_btn = QPushButton(tr("settings.ms365_open_link")) - open_btn.setIcon(icon("link")) - open_btn.clicked.connect(lambda: webbrowser.open(flow.get("verification_uri_complete") or url)) - code_row.addWidget(code_edit, 1) - code_row.addWidget(copy_btn) - code_row.addWidget(open_btn) - lay.addLayout(code_row) - - buttons = QDialogButtonBox(QDialogButtonBox.Close) - buttons.rejected.connect(dlg.reject) - lay.addWidget(buttons) - - self._ms365_code_dialog = dlg - dlg.show() # non-modal — sign-in polling continues; done() closes it - - def _ms365_sign_out(self) -> None: - from ..core.ms365_auth import sign_out_default - sign_out_default(self.ctx.config) - self._refresh_ms365_status()