Files
cowork-local/infrastructure/config/settings_facade.py
T
anhtnm1andClaude Opus 5 e29a0ccdbd refactor: vá 4 hồi quy, tách 4 file chạm trần LOC, docstring lên 100%
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>
2026-08-30 10:41:45 +09:00

217 lines
8.5 KiB
Python

"""Khung nhìn có kiểu cho từng nhóm cấu hình — R02-T03.
Vấn đề đang có: khắp nơi viết ``ctx.config.routing.get("switch_mode", "off")``.
Gõ sai một chữ thì lặng lẽ nhận giá trị mặc định, không ai biết cho tới khi
tính năng "không hiểu sao không chạy". Đếm được **156 lời gọi ``ctx.config.*``
trong 29 file** kiểu đó.
Ở đây mỗi nhóm cấu hình có một lớp: gõ sai tên thuộc tính là lỗi ngay, và kiểu
dữ liệu ghi rõ ràng nên đọc code là biết ``confirm_timeout_sec`` là số giây
chứ không phải mili giây.
Cố ý KHÔNG dùng dataclass đông cứng: đây là *khung nhìn* lên dict cấu hình
sống, sửa qua đây là sửa vào dict rồi ``save()`` là xuống đĩa. Sao chép thành
dataclass thì lại sinh chuyện đồng bộ hai chiều.
SEAM · dựng 2026-08-22 · chưa nối dây (F-05)
------------------------------------------------------------
Được nối khi: ít nhất một trong 156 lời gọi ``ctx.config.*`` chuyển sang đọc qua ``Settings``.
Để dormant thì sao: Mục đích của nó là chặn lỗi gõ sai tên khoá. Không ai
dùng thì không chặn được gì cả.
Cổng ``scripts/check_orphan_modules.py`` đếm tuổi seam từ ngày trên
và nhắc khi quá ``SEAM_MAX_AGE_DAYS``. Đổi nội dung dòng đó thì cổng
đọc theo — đừng sửa ngày để làm im lời nhắc.
"""
from __future__ import annotations
from typing import Any, Dict
class _View:
"""Khung nhìn lên một nhánh của dict cấu hình."""
def __init__(self, data: Dict[str, Any]):
"""Bọc một nhánh dict cấu hình để đọc bằng thuộc tính thay vì tra khoá."""
self._d = data
def _get(self, key: str, default: Any) -> Any:
"""Đọc một khoá, coi ``None`` như thiếu.
Cấu hình cũ có chỗ ghi ``null``; nếu trả thẳng ``None`` ra ngoài thì
``str(None)`` thành chuỗi "None" và lỗi hiện ra ở tận nơi dùng.
"""
value = self._d.get(key, default)
return default if value is None else value
def raw(self) -> Dict[str, Any]:
"""Dict gốc — dùng khi cần đọc khoá chưa được đưa vào khung nhìn.
Có mặt để không ai bị kẹt: thiếu thuộc tính thì dùng tạm ``raw()`` rồi
mở issue bổ sung, chứ đừng vòng lại ``ctx.config.data``.
"""
return self._d
class ProviderSettings(_View):
"""Một provider: đi đâu, model nào, khoá nào.
``api_key`` ở đây là thứ ``JsonConfigRepository.provider_conf()`` đã ghép
sẵn từ kho bí mật — xem đường A trong ``GammaTeam_decisions.md``.
"""
@property
def base_url(self) -> str:
"""Endpoint của provider. '' nghĩa là chưa cấu hình."""
return str(self._get("base_url", ""))
@property
def model(self) -> str:
"""Model mặc định của provider này. '' nghĩa là chưa chọn."""
return str(self._get("model", ""))
@property
def api_key(self) -> str:
"""Khoá API — đã được ``provider_conf()`` ghép từ kho bí mật của hệ điều hành."""
return str(self._get("api_key", ""))
@property
def configured(self) -> bool:
"""Đủ thông tin để gọi được chưa.
Ollama chạy cục bộ nên không cần khoá — đó là lý do điều kiện là
"có base_url và model", không phải "có api_key".
"""
return bool(self.base_url and self.model)
class RoutingSettings(_View):
"""Định tuyến model tự động (``core/routing/``)."""
@property
def switch_mode(self) -> str:
"""``"off"`` | ``"auto"`` | ``"manual"``."""
return str(self._get("switch_mode", "off"))
@switch_mode.setter
def switch_mode(self, value: str) -> None:
"""Đặt chế độ định tuyến chung. Sửa thẳng vào dict cấu hình sống."""
self._d["switch_mode"] = value
@property
def enabled(self) -> bool:
"""Định tuyến tự động có đang bật không (tức ``switch_mode`` khác "off")."""
return self.switch_mode != "off"
@property
def policy(self) -> str:
"""``"balanced"`` | ``"cheap"`` | ``"quality"``…"""
return str(self._get("policy", "balanced"))
@property
def min_score_gain(self) -> float:
"""Phải hơn model hiện tại bao nhiêu điểm mới đáng đổi."""
return float(self._get("min_score_gain", 0.05))
@property
def confirm_timeout_sec(self) -> int:
"""GIÂY, không phải mili giây — đọc tên là biết, khỏi phải mò."""
return int(self._get("confirm_timeout_sec", 60))
@property
def reassess_interval_hours(self) -> int:
"""GIỜ giữa hai lần chấm điểm lại danh mục model."""
return int(self._get("reassess_interval_hours", 24))
@property
def per_provider_concurrency(self) -> int:
"""Số lượt gọi chạy song song tối đa cho mỗi provider khi dò/chấm điểm."""
return int(self._get("per_provider_concurrency", 2))
@property
def judge_provider(self) -> str:
"""Provider dùng làm trọng tài chấm điểm model. '' nghĩa là dùng provider đang chọn."""
return str(self._get("judge_provider", ""))
@property
def judge_model(self) -> str:
"""Model dùng làm trọng tài chấm điểm. '' nghĩa là dùng model mặc định của trọng tài."""
return str(self._get("judge_model", ""))
class SecuritySettings(_View):
"""Chính sách an toàn cho agent (``core/agent_security.py``)."""
@property
def enabled(self) -> bool:
"""Công tắc tổng của lớp an toàn. Tắt là bỏ qua mọi bước kiểm dưới đây."""
return bool(self._get("enabled", True))
@property
def validate_prompt(self) -> bool:
"""Có quét prompt người dùng tìm dấu hiệu tấn công tiêm lệnh không."""
return bool(self._get("validate_prompt", True))
@property
def validate_attachments(self) -> bool:
"""Có kiểm tệp đính kèm (đuôi/kiểu MIME nguy hiểm) trước khi đưa vào lượt chat không."""
return bool(self._get("validate_attachments", True))
@property
def validate_commands(self) -> bool:
"""Có phân loại rủi ro lệnh shell trước khi chạy không."""
return bool(self._get("validate_commands", True))
@property
def command_ai_check(self) -> bool:
"""Có nhờ thêm AI xét lệnh khi bộ luật tĩnh chưa chắc chắn không. Mặc định tắt vì tốn một lượt gọi."""
return bool(self._get("command_ai_check", False))
@property
def cowork_confirm_commands(self) -> bool:
"""Có hỏi trước khi chạy lệnh không.
Ứng với ``PolicyOutcome.ASK`` trong
``domain/security/tool_policy.py``.
"""
return bool(self._get("cowork_confirm_commands", True))
@property
def rules_onedrive_url(self) -> str:
"""Link OneDrive tới bộ luật an toàn dùng chung. '' nghĩa là dùng bản đóng gói sẵn trong app."""
return str(self._get("rules_onedrive_url", ""))
@property
def admin_email(self) -> str:
"""Email quản trị nhận cảnh báo vi phạm. '' nghĩa là không gửi."""
return str(self._get("admin_email", ""))
class Settings:
"""Cửa vào duy nhất cho các nhóm cấu hình có kiểu.
>>> s = Settings(repo)
>>> if s.routing.enabled and s.provider().configured:
... ...
"""
def __init__(self, repo):
"""Bọc một ``ConfigRepository`` — mọi lượt đọc/ghi đều đi xuống nó, lớp này chỉ
đổi cách gọi cho dễ đọc.
"""
self._repo = repo
def provider(self, name: str | None = None) -> ProviderSettings:
"""Khung nhìn cấu hình của một provider; bỏ trống thì lấy provider đang chọn."""
return ProviderSettings(self._repo.provider_conf(name))
@property
def routing(self) -> RoutingSettings:
"""Khung nhìn nhóm cấu hình định tuyến model."""
return RoutingSettings(self._repo.routing)
@property
def security(self) -> SecuritySettings:
"""Khung nhìn nhóm cấu hình an toàn cho agent."""
return SecuritySettings(self._repo.agent_security)