Feature/delta team/epic r04 #7

Merged
gitea-admin merged 67 commits from feature/delta-team/epic-R04 into main 2026-08-31 05:15:15 +00:00
3 changed files with 205 additions and 1 deletions
Showing only changes of commit 2246d55286 - Show all commits
+5 -1
View File
@@ -19,6 +19,7 @@ from PySide6.QtWidgets import (
from . import APP_NAME, DISPLAY_NAME, __version__
from .config import PROVIDER_LABELS, AppConfig
from .i18n import LANGUAGE_SHORT, LANGUAGES, get_language, on_language_changed, set_language, tr
from .presentation.shell.bootstrap import build_context
from .state import AppContext
from .ui.widgets import tidy_popup
from .theme import current_palette, set_active_theme, stylesheet
@@ -1293,7 +1294,10 @@ def run(argv: List[str] | None = None) -> int:
app = QApplication.instance() or QApplication(argv)
app.setApplicationName(APP_NAME)
app.setWindowIcon(app_icon())
ctx = AppContext(AppConfig.load())
# Composition Root: presentation/shell/bootstrap.py quyết định app chạy
# bằng mảnh nào. Từ R02, đó là JsonConfigRepository + kho bí mật của hệ
# điều hành, không còn config.py::AppConfig.
ctx = build_context()
set_language(ctx.config.language)
# Built-in default skills (if any are bundled) are always-on and loaded
# straight from the package; tidy away any copy seeded by older versions so they
@@ -170,6 +170,164 @@ class JsonConfigRepository:
disabled.append(name)
self.data["tools_disabled"] = disabled
# ---- phần bù để thay được AppConfig ----------------------------------
# 21 thành viên dưới đây chép nguyên ngữ nghĩa từ ``config.py::AppConfig``.
# Không phải thiết kế mới: chừng nào 29 file còn gọi qua ``ctx.config`` thì
# repository phải trả lời được đúng những câu hỏi cũ, nếu không thì không
# tráo được. Dọn lại là việc của các R sau, không phải của R02.
#: Các chế độ định tuyến. Delta thêm "fallback" ở R03-T03. Định nghĩa ở đây
#: là bản chính; ``tests/test_config_repository.py`` có bài đối chiếu với
#: ``config.py`` để hai bên lệch nhau là đỏ ngay.
ROUTING_MODES = ("off", "auto", "manual", "fallback")
@classmethod
def load(cls, path: Path | None = None, *, secrets: SecretStore | None = None):
"""Dựng repository từ đường dẫn mặc định — thay ``AppConfig.load()``."""
if path is None:
from ... import config as legacy
path = legacy.CONFIG_PATH
return cls(Path(path), secrets=secrets)
@property
def path(self) -> Path:
return self._file.path
# ---- TLS -------------------------------------------------------------
@property
def ca_bundle(self) -> str:
"""Đường dẫn file PEM riêng, hoặc '' để kiểm chứng chỉ như bình thường.
Dùng làm tham số ``verify=`` của ``requests`` cho mọi lượt gọi HTTPS."""
return (self.data.get("tls_ca_bundle") or "").strip()
@ca_bundle.setter
def ca_bundle(self, value: str) -> None:
self.data["tls_ca_bundle"] = (value or "").strip()
# ---- MS365 -----------------------------------------------------------
@property
def ms365(self) -> Dict[str, Any]:
return self.data.setdefault("ms365", copy.deepcopy(self._defaults["ms365"]))
def ms365_try_unlock(self, code: str) -> bool:
"""Mở khoá nhóm MS365 trong Cài đặt cho phiên này.
Đâ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 xác thực Microsoft. Không bao giờ được lưu ở trạng thái đã mở."""
if (code or "") and code == self.ms365.get("unlock_code", ""):
self.data["ms365"]["unlocked"] = True
return True
return False
def ms365_lock(self) -> None:
self.data.setdefault("ms365", {})["unlocked"] = False
# ---- nhóm cấu hình đọc thẳng ------------------------------------------
@property
def code(self) -> Dict[str, Any]:
return self.data["code"]
@property
def teams(self) -> Dict[str, Any]:
return self.data["teams"]
@property
def history(self) -> Dict[str, Any]:
return self.data["history"]
@property
def codebase_memory(self) -> Dict[str, Any]:
return self.data["codebase_memory"]
@property
def cowork(self) -> Dict[str, Any]:
return self.data["cowork"]
@property
def mcp_servers(self) -> list:
return self.data.setdefault("mcp_servers", [])
@property
def structure(self) -> Dict[str, Any]:
return self.data.setdefault("structure", {"max_nodes": 400, "max_edges": 400})
@property
def monitoring_visibility(self) -> Dict[str, bool]:
return self.data.setdefault(
"monitoring_visibility",
copy.deepcopy(self._defaults["monitoring_visibility"]))
@property
def ext_connectors(self) -> Dict[str, list]:
"""Connector (MCP) gom theo nhóm CAD/CAE/MS365/Other."""
d = self.data.setdefault(
"ext_connectors", {"cad": [], "cae": [], "ms365": [], "other": []})
for cat in ("cad", "cae", "ms365", "other"):
d.setdefault(cat, [])
return d
# ---- công tắc tổng cho connector -------------------------------------
@property
def connect_external(self) -> bool:
"""Tắt cái này là agent không nối tới connector ngoài nào cả. Mặc định
BẬT để cấu hình đang chạy không đổi hành vi."""
return bool(self.data.setdefault("tools", {}).get("connect_external", True))
def set_connect_external(self, enabled: bool) -> None:
self.data.setdefault("tools", {})["connect_external"] = bool(enabled)
self.save()
# ---- những thứ đã gieo sẵn -------------------------------------------
@property
def seeded_library_skills(self) -> list:
"""Slug của skill thư viện đã gieo — để cái người dùng xoá đi không bị
lặng lẽ gieo lại."""
return list(self.data.setdefault("seeded_library_skills", []))
@seeded_library_skills.setter
def seeded_library_skills(self, slugs) -> None:
self.data["seeded_library_skills"] = list(dict.fromkeys(slugs or []))
@property
def seeded_builtin_flows(self) -> list:
"""Id của flow Co4E dựng sẵn đã gieo (cùng quy tắc tôn trọng việc người
dùng đã xoá như seeded_library_skills)."""
return list(self.data.setdefault("seeded_builtin_flows", []))
@seeded_builtin_flows.setter
def seeded_builtin_flows(self, ids) -> None:
self.data["seeded_builtin_flows"] = list(dict.fromkeys(ids or []))
# ---- định tuyến theo từng bề mặt chat --------------------------------
def routing_mode_for(self, surface: str) -> str:
"""Chế độ có hiệu lực cho một bề mặt chat.
Đặt riêng cho bề mặt thì thắng; để trống thì lấy ``switch_mode`` chung.
Giá trị lạ rơi về "off" — định tuyến luôn là thứ phải bật, kể cả khi
có người sửa tay file cấu hình."""
routing = self.routing
override = (routing.get("surface_modes", {}) or {}).get(surface, "")
mode = override or routing.get("switch_mode", "off")
return mode if mode in self.ROUTING_MODES else "off"
def set_routing_mode_for(self, surface: str, mode: str) -> None:
mode = mode if mode in self.ROUTING_MODES else "off"
self.routing.setdefault("surface_modes", {})[surface] = mode
self.save()
# ---- tiện ích --------------------------------------------------------
def model_label(self) -> str:
return str(self.provider_conf().get("model", "?"))
# ---- ghi -------------------------------------------------------------
def save(self) -> None:
"""Ghi nguyên tử. Không bao giờ để lộ trạng thái mở khoá ms365."""
+42
View File
@@ -0,0 +1,42 @@
"""Composition Root — R08-T10.
Một chỗ duy nhất quyết định app chạy bằng những mảnh nào. Trước đây quyết định
đó nằm rải trong ``app.py::run``, lẫn với việc dựng cửa sổ; tách ra để đổi một
mảnh (ví dụ thay kho bí mật) không phải đụng vào mã giao diện.
Đây cũng là chỗ hoàn tất R02: từ đây app chạy bằng :class:`JsonConfigRepository`
chứ không còn ``config.py::AppConfig``. Hai thứ đổi thật sự:
* ghi cấu hình qua ``AtomicJsonFile`` — mất điện giữa lúc lưu không làm hỏng file
* API key nằm trong kho bí mật của hệ điều hành, không nằm trong ``config.json``
Máy không có kho bí mật (Linux headless, CI, hoặc keyring hỏng) vẫn chạy bình
thường: repository nhận ``secrets=None`` và đọc khoá thẳng từ file như cũ. Thà
để khoá trong file còn hơn xoá đi rồi người dùng mất khoá mà không hiểu vì sao.
"""
from __future__ import annotations
from pathlib import Path
from ...infrastructure.config.json_config_repository import JsonConfigRepository
from ...infrastructure.secrets.keyring_adapter import KeyringAdapter
from ...state import AppContext
def build_secret_store():
"""Kho bí mật của hệ điều hành, hoặc None nếu máy này không có.
``KeyringAdapter`` không bao giờ ném lỗi — nó tự báo ``available``. Trả về
None thay vì một adapter chết để chỗ gọi khỏi phải đoán.
"""
store = KeyringAdapter()
return store if store.available else None
def build_config(path: Path | None = None) -> JsonConfigRepository:
return JsonConfigRepository.load(path, secrets=build_secret_store())
def build_context(path: Path | None = None) -> AppContext:
"""Dựng AppContext hoàn chỉnh — điểm vào cho ``app.run()`` và cho checker."""
return AppContext(build_config(path))