Hồi quy đã vá
-------------
F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
`chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
`composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
gắn 1 tệp, khớp bản trước refactor.
F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
`folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
`ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
tiên hỏng thì đổi provider chính là lúc phải thử lại.
F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
`check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.
F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
và không bao giờ chạy.
Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:
graph_renderer.py -> graph_scene_builder.py + graph_export.py
co4e_workflow_service.py -> co4e_run_history.py
json_config_repository.py -> config_sections.py
agents_admin_tab.py -> shared/agent_kind_visuals.py
File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.
Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.
Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.
859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
318 lines
14 KiB
Python
318 lines
14 KiB
Python
"""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
|
|
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,
|
|
QTreeWidgetItem, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from ..i18n import tr
|
|
from .icons import IconLabel
|
|
from .widgets import ToggleSwitch
|
|
|
|
|
|
from ..presentation.settings.general_settings_widget import GeneralSettingsWidget
|
|
from ..presentation.settings.provider_settings_widget import ProviderSettingsWidget
|
|
from ..presentation.settings.parameter_settings_widget import ParameterSettingsWidget
|
|
from ..presentation.settings.routing_settings_widget import RoutingSettingsWidget
|
|
|
|
class SettingsDialog(QDialog):
|
|
"""Hộp thoại Cài đặt: cột mục lục bên trái, các trang bên phải
|
|
(Nhà cung cấp · Connectors · Định tuyến · Tham số · Chung).
|
|
"""
|
|
def __init__(self, ctx, parent=None):
|
|
"""Hộp thoại Cài đặt, ghép các nhóm thiết lập.
|
|
|
|
Có nút thu nhỏ (không phải mặc định của hộp thoại Qt) vì màn này hay được để
|
|
mở trong lúc người dùng làm việc ở cửa sổ chính.
|
|
"""
|
|
super().__init__()
|
|
self.ctx = ctx
|
|
self.setWindowTitle(tr("settings.title"))
|
|
self.setMinimumWidth(560)
|
|
self.setWindowFlags(
|
|
self.windowFlags()
|
|
| Qt.WindowMinimizeButtonHint
|
|
| Qt.WindowMaximizeButtonHint
|
|
)
|
|
self.setSizeGripEnabled(True)
|
|
# Group boxes are styled app-wide (see theme._TEMPLATE); this dialog
|
|
# used to re-declare them and drifted out of sync with the rest.
|
|
data = ctx.config.data
|
|
|
|
outer = QVBoxLayout(self)
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
# Never sideways: the content must fit the width it is given and scroll
|
|
# only downwards. Anything too wide has to shrink (see _model_combo and
|
|
# _with_load), not push a second scrollbar onto the user.
|
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
self._content = QWidget()
|
|
root = QVBoxLayout(self._content)
|
|
|
|
# --- Chung: ngôn ngữ, giao diện, khay ---
|
|
# Đã bóc sang presentation/settings/general_settings_widget.py (R08-T07).
|
|
self._general_box = GeneralSettingsWidget(self.ctx)
|
|
root.addWidget(self._general_box)
|
|
|
|
self._load_workers = []
|
|
|
|
# --- AI Provider ---
|
|
# Đã bóc sang presentation/settings/provider_settings_widget.py (R08-T07).
|
|
prov_group = ProviderSettingsWidget(self.ctx)
|
|
self._provider_page = prov_group
|
|
root.addWidget(prov_group)
|
|
|
|
# --- Sandbox Security Layer ---
|
|
sec = ctx.config.agent_security
|
|
self.sandbox_group = QGroupBox(tr("settings.group.sandbox"))
|
|
sbl = QVBoxLayout(self.sandbox_group)
|
|
|
|
# --- Password protection for Sandbox Security (at top) ---
|
|
self.sandbox_pw_label = IconLabel("lock", "Sandbox Security Password")
|
|
sbl.addWidget(self.sandbox_pw_label)
|
|
|
|
pw_row = QHBoxLayout()
|
|
self.sandbox_pw_edit = QLineEdit("")
|
|
self.sandbox_pw_edit.setPlaceholderText("Enter password to edit sandbox settings")
|
|
self.sandbox_pw_edit.setEchoMode(QLineEdit.Password)
|
|
pw_row.addWidget(self.sandbox_pw_edit, 1)
|
|
self.sandbox_unlock_btn = QPushButton("Unlock")
|
|
self.sandbox_unlock_btn.clicked.connect(self._sandbox_unlock)
|
|
pw_row.addWidget(self.sandbox_unlock_btn)
|
|
self.sandbox_locked_status = IconLabel("lock", "Locked (changes disabled)", color="#c00")
|
|
self.sandbox_locked_status.text_label().setStyleSheet("color: #c00; font-weight: bold;")
|
|
pw_row.addWidget(self.sandbox_locked_status)
|
|
sbl.addLayout(pw_row)
|
|
self._sandbox_unlocked = False # Start LOCKED — must enter password first
|
|
self._sandbox_pw = sec.get("sandbox_pw", "quandh14")
|
|
|
|
# Separator line between pw section and sandbox settings
|
|
pw_sep = QLabel("────────────────")
|
|
sbl.addWidget(pw_sep)
|
|
|
|
self.sandbox_confirm = ToggleSwitch(tr("settings.sandbox_confirm_commands"))
|
|
self.sandbox_confirm.setChecked(bool(sec.get("cowork_confirm_commands", False)))
|
|
self.sandbox_confirm.setToolTip(tr("settings.sandbox_confirm_commands_tooltip"))
|
|
sbl.addWidget(self.sandbox_confirm)
|
|
|
|
self.sandbox_block_network = ToggleSwitch(tr("settings.sandbox_block_network"))
|
|
self.sandbox_block_network.setChecked(bool(sec.get("block_network", True)))
|
|
self.sandbox_block_network.setToolTip(tr("settings.sandbox_block_network_tooltip"))
|
|
sbl.addWidget(self.sandbox_block_network)
|
|
|
|
# "Allow the agent to fetch URLs" + the live "Test Internet" self-test
|
|
# moved to Monitoring → Tools → Tool (they govern a tool capability, so
|
|
# they belong with the other tool toggles — see ToolsAdminTab).
|
|
|
|
# --- Enable/Disable Agent Security ---
|
|
self.sec_enabled = ToggleSwitch("Enable Agent Security (command validation)")
|
|
self.sec_enabled.setChecked(bool(sec.get("enabled", True)))
|
|
self.sec_enabled.setToolTip("Bật/tắt toàn bộ Agent Security")
|
|
sbl.addWidget(self.sec_enabled)
|
|
|
|
# --- AI Command Check toggle ---
|
|
self.ai_check = ToggleSwitch("AI check commands")
|
|
self.ai_check.setChecked(bool(sec.get("command_ai_check", False)))
|
|
self.ai_check.setToolTip("Cho AI control-agent xét lệnh trước khi chạy")
|
|
sbl.addWidget(self.ai_check)
|
|
|
|
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
|
|
# below — see _param_section("settings.group.sandbox_limits").
|
|
|
|
# Collect all sandbox-editable widgets and lock them until unlocked
|
|
self._sandbox_widgets = [
|
|
self.sandbox_confirm, self.sandbox_block_network,
|
|
self.ai_check, self.sec_enabled,
|
|
]
|
|
for _w in self._sandbox_widgets:
|
|
_w.setEnabled(False)
|
|
|
|
root.addWidget(self.sandbox_group)
|
|
|
|
# Connectors (MCP / REST API) are managed entirely in Monitoring → Tools
|
|
# for the dead-but-retained MS365 OAuth sign-in handlers below.)
|
|
|
|
# --- Parameter ---
|
|
# Đã bóc sang presentation/settings/parameter_settings_widget.py (R08-T07).
|
|
param_group = ParameterSettingsWidget(self.ctx)
|
|
self._param_page = param_group
|
|
root.addWidget(param_group)
|
|
|
|
# ---- Auto Model Routing ------------------------------------------
|
|
# Đã bóc sang presentation/settings/routing_settings_widget.py (R08-T07).
|
|
routing_group = RoutingSettingsWidget(self.ctx)
|
|
self._routing_page = routing_group
|
|
root.addWidget(routing_group)
|
|
|
|
|
|
# Left list + right panel: one group on screen at a time, the way the
|
|
# audit page's mock-up shows it. The five rows are the five real group
|
|
# boxes, so "which group am I in, how many left" is answerable at a
|
|
# glance instead of by scrolling to find out.
|
|
from .widgets import section_panels
|
|
|
|
|
|
pages = []
|
|
for label, widget in ((tr("settings.group.general"), self._general_box),
|
|
(tr("settings.group.provider"), prov_group),
|
|
(tr("settings.group.sandbox"), self.sandbox_group),
|
|
(tr("settings.group.parameter"), param_group),
|
|
(tr("routing.settings_group"), routing_group)):
|
|
root.removeWidget(widget)
|
|
page = QWidget()
|
|
pv = QVBoxLayout(page)
|
|
pv.setContentsMargins(4, 4, 4, 4)
|
|
pv.addWidget(widget)
|
|
pv.addStretch(1)
|
|
wrap = QScrollArea()
|
|
wrap.setWidgetResizable(True)
|
|
wrap.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
wrap.setWidget(page)
|
|
pages.append((label, wrap))
|
|
self.section_list, self.section_stack = section_panels(pages)
|
|
scroll.setParent(None)
|
|
body = QHBoxLayout()
|
|
body.setSpacing(10)
|
|
body.addWidget(self.section_list)
|
|
body.addWidget(self.section_stack, 1)
|
|
outer.addLayout(body, 1)
|
|
self._content = self._general_box # kept for other callers
|
|
# Floor the dialog at the width its WIDEST page needs, at the current
|
|
# font. On a 125%/150% display everything is wider, and without this the
|
|
# form was simply cut off instead of the window refusing to get smaller.
|
|
widest = max(w.widget().sizeHint().width() for _lab, w in pages)
|
|
self.setMinimumWidth(self.section_list.width() + widest + 60)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self._save)
|
|
buttons.rejected.connect(self.reject)
|
|
outer.addWidget(buttons)
|
|
|
|
from .widgets import guard_wheel
|
|
guard_wheel(self)
|
|
|
|
screen = QGuiApplication.primaryScreen()
|
|
if screen:
|
|
avail = screen.availableGeometry()
|
|
self.resize(640, min(740, avail.height() - 80))
|
|
self.setMaximumHeight(avail.height())
|
|
|
|
# ---- cầu tương thích sau khi bóc Routing -----------------------------
|
|
# Năm checker trong tools/ và bài đặc tả đọc thẳng self.routing_*. Giữ tên
|
|
# cũ trỏ vào widget mới để việc bóc không kéo theo sửa chỗ khác — đây là
|
|
# đổi chỗ ở, không đổi hành vi. Bỏ được khi tools/ chuyển sang đọc
|
|
# self._routing_page.
|
|
provider_combo = property(lambda self: self._provider_page.provider_combo)
|
|
prov_base = property(lambda self: self._provider_page.prov_base)
|
|
prov_key = property(lambda self: self._provider_page.prov_key)
|
|
prov_model = property(lambda self: self._provider_page.prov_model)
|
|
prov_status = property(lambda self: self._provider_page.prov_status)
|
|
language_combo = property(lambda self: self._general_box.language_combo)
|
|
theme_combo = property(lambda self: self._general_box.theme_combo)
|
|
tray_chk = property(lambda self: self._general_box.tray_chk)
|
|
notify_chk = property(lambda self: self._general_box.notify_chk)
|
|
attach_files = property(lambda self: self._param_page.attach_files)
|
|
attach_tokens = property(lambda self: self._param_page.attach_tokens)
|
|
struct_nodes = property(lambda self: self._param_page.struct_nodes)
|
|
struct_edges = property(lambda self: self._param_page.struct_edges)
|
|
sandbox_cpu = property(lambda self: self._param_page.sandbox_cpu)
|
|
sandbox_memory = property(lambda self: self._param_page.sandbox_memory)
|
|
sandbox_disk = property(lambda self: self._param_page.sandbox_disk)
|
|
routing_mode = property(lambda self: self._routing_page.mode)
|
|
routing_policy = property(lambda self: self._routing_page.policy)
|
|
routing_min_gain = property(lambda self: self._routing_page.min_gain)
|
|
routing_timeout = property(lambda self: self._routing_page.timeout)
|
|
routing_interval = property(lambda self: self._routing_page.interval)
|
|
routing_concurrency = property(lambda self: self._routing_page.concurrency)
|
|
routing_judge = property(lambda self: self._routing_page.judge)
|
|
routing_reassess_btn = property(lambda self: self._routing_page.reassess_btn)
|
|
|
|
# ---- helpers -----------------------------------------------------
|
|
|
|
|
|
@staticmethod
|
|
def _group(title: str, rows) -> QGroupBox:
|
|
"""Dựng một nhóm có tiêu đề chứa các hàng nhãn–điều khiển."""
|
|
box = QGroupBox(title)
|
|
form = QFormLayout(box)
|
|
for label, widget in rows:
|
|
form.addRow(label, widget)
|
|
return box
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- MS365 zero-config sign-in ("connect like Claude") ---------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sandbox_unlock(self) -> None:
|
|
"""Mở khoá nhóm cài đặt sandbox bằng mật khẩu.
|
|
|
|
Đây là khoá phía giao diện để chặn bấm nhầm vào một mục nhạy cảm, KHÔNG
|
|
phải cơ chế bảo mật thật.
|
|
"""
|
|
pw = self.sandbox_pw_edit.text()
|
|
if pw == self._sandbox_pw:
|
|
self._sandbox_unlocked = True
|
|
self.sandbox_locked_status.setText("Unlocked")
|
|
self.sandbox_locked_status.set_icon("unlock", "#090")
|
|
self.sandbox_locked_status.text_label().setStyleSheet("color: #090; font-weight: bold;")
|
|
# Enable all sandbox widgets
|
|
for w in self._sandbox_widgets:
|
|
w.setEnabled(True)
|
|
QMessageBox.information(self, "Sandbox Security", "Sandbox settings unlocked.")
|
|
else:
|
|
QMessageBox.warning(self, "Wrong Password", "Password incorrect. Sandbox settings remain locked.")
|
|
|
|
def _save(self) -> None:
|
|
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
|
|
data = self.ctx.config.data
|
|
self._provider_page.apply_to(data)
|
|
self._general_box.apply_to(data)
|
|
|
|
|
|
# NOTE: allow_url_fetch is managed in Monitoring → Tools → Tool now
|
|
# (persisted there directly), so it is intentionally not written here.
|
|
data.setdefault("agent_security", {}).update({
|
|
"enabled": self.sec_enabled.isChecked(),
|
|
"cowork_confirm_commands": self.sandbox_confirm.isChecked(),
|
|
"block_network": self.sandbox_block_network.isChecked(),
|
|
"command_ai_check": self.ai_check.isChecked(),
|
|
"command_whitelist": [],
|
|
})
|
|
self._param_page.apply_limits_to(data["agent_security"])
|
|
self._param_page.apply_to(data)
|
|
|
|
self._routing_page.apply_to(data)
|
|
|
|
self.ctx.save()
|
|
|
|
# Force-reload config so all parts of the app pick up the new settings immediately
|
|
self.ctx.config._data = None # invalidate cache
|
|
self.ctx.config._agent_security = None
|
|
|
|
self.accept() |