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>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""Tám tab của màn Giám sát, mỗi tab một file: Tổng quan, Sandbox, Sự kiện bảo
|
||||
mật, Lịch sử MCP, Nhật ký hành động, Trạng thái Agent, Quản trị Agent, Công cụ.
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,9 @@ from ..shared.filter_scaffold import build_filter_scaffold
|
||||
|
||||
|
||||
class ActionLogsTab(QWidget):
|
||||
"""Tab "Nhật ký hành động": bảng mọi thao tác đọc/ghi tệp và chạy lệnh của agent."""
|
||||
def __init__(self, ctx, on_refresh_all: Callable[[], None]):
|
||||
"""Tab Nhật ký hành động: bảng sự kiện kèm bộ lọc dùng chung."""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._ai_state: dict = {}
|
||||
@@ -32,9 +34,11 @@ class ActionLogsTab(QWidget):
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
self.table.set_events(events)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.table.retranslate()
|
||||
self.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder"))
|
||||
self.detail_panel.retranslate()
|
||||
@@ -42,4 +46,5 @@ class ActionLogsTab(QWidget):
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
start_ai_filter(self._ctx, search, ai_btn, self._ai_state)
|
||||
|
||||
@@ -35,6 +35,11 @@ class AgentEditDialog(QDialog):
|
||||
def __init__(self, parent=None, ctx: Optional[AppContext] = None,
|
||||
agent: Optional[admin_agents.AdminAgent] = None,
|
||||
default_model_hint: str = ""):
|
||||
"""Form thêm/sửa một agent quản trị.
|
||||
|
||||
Danh sách model của từng provider được nạp nền và nhớ lại, để đổi qua đổi
|
||||
lại giữa các provider không phải gọi mạng lần nữa.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.ctx = ctx
|
||||
self._existing = agent
|
||||
@@ -96,15 +101,18 @@ class AgentEditDialog(QDialog):
|
||||
form.addRow(buttons)
|
||||
|
||||
def _load_live_models(self) -> None:
|
||||
"""Nạp danh sách model đang sống của mọi provider, ở luồng nền."""
|
||||
if self.ctx is None:
|
||||
return
|
||||
self.load_models_btn.setEnabled(False)
|
||||
ctx = self.ctx
|
||||
|
||||
def job(_worker: AgentWorker):
|
||||
"""Chạy nền: hỏi từng provider danh sách model."""
|
||||
return preview_ai.fetch_live_models(ctx)
|
||||
|
||||
def done(result: dict) -> None:
|
||||
"""Ghi nhớ danh sách model rồi nạp vào ô chọn theo provider đang chọn."""
|
||||
self.load_models_btn.setEnabled(True)
|
||||
self._live_models = result or {}
|
||||
self._refresh_model_combo()
|
||||
@@ -113,6 +121,7 @@ class AgentEditDialog(QDialog):
|
||||
tr("agents_admin.load_models_empty"))
|
||||
|
||||
def failed(err: str) -> None:
|
||||
"""Nạp model lỗi: hiện cảnh báo và mở khoá lại nút."""
|
||||
self.load_models_btn.setEnabled(True)
|
||||
QMessageBox.warning(self, tr("agents_admin.add_title"), err)
|
||||
|
||||
@@ -123,6 +132,7 @@ class AgentEditDialog(QDialog):
|
||||
w.start()
|
||||
|
||||
def _refresh_model_combo(self) -> None:
|
||||
"""Nạp lại ô chọn model theo provider đang chọn, giữ nguyên thứ đang gõ."""
|
||||
provider_key = self.provider_combo.currentData()
|
||||
current_text = self.model_combo.currentText().strip()
|
||||
models = self._live_models.get(provider_key, []) if provider_key else []
|
||||
@@ -133,6 +143,7 @@ class AgentEditDialog(QDialog):
|
||||
self.model_combo.blockSignals(False)
|
||||
|
||||
def result_fields(self) -> Dict[str, str]:
|
||||
"""Các trường agent lấy từ form, để chỗ gọi dựng bản ghi."""
|
||||
return {
|
||||
"name": self.name_edit.text().strip(),
|
||||
"task_kind": self.kind_combo.currentData(),
|
||||
|
||||
@@ -18,8 +18,10 @@ from ..shared.formatters import agent_avatar_icon
|
||||
|
||||
|
||||
class AgentStatusTab(QWidget):
|
||||
"""Tab "Trạng thái Agent": đang có bao nhiêu lượt chat, task và luồng chạy."""
|
||||
def __init__(self, ctx, on_refresh_all: Callable[[], None],
|
||||
cowork=None, structure=None, task_scheduler=None):
|
||||
"""Tab Tình trạng agent. Các widget truyền vào có thể là None khi dựng tab lẻ."""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._cowork = cowork
|
||||
@@ -45,6 +47,7 @@ class AgentStatusTab(QWidget):
|
||||
self.title_refresh_btn = parts["title_refresh_btn"]
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.title_lbl.setText(tr(self.title_key))
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
self.table.setHorizontalHeaderLabels([
|
||||
@@ -52,6 +55,7 @@ class AgentStatusTab(QWidget):
|
||||
])
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Đếm lại số việc đang chạy ở từng phân hệ và cập nhật bảng."""
|
||||
cowork_n = len(self._cowork.active_workers()) if self._cowork is not None else 0
|
||||
task_n = self._task_scheduler.running_count() if self._task_scheduler is not None else 0
|
||||
ask_worker = getattr(self._structure, "_ask_worker", None)
|
||||
|
||||
@@ -12,88 +12,45 @@ The header's "Kiểm tra tất cả" icon probes each agent's effective provider
|
||||
(``check_agent``) and shows the result as the Trạng thái pill (OK / error /
|
||||
checking…) — separate from the per-row Kích hoạt switch, which only toggles
|
||||
the config flag.
|
||||
|
||||
Màu, huy hiệu và avatar theo loại việc nằm ở
|
||||
``../shared/agent_kind_visuals.py``. File này từng giữ bản sao riêng của ba
|
||||
hàm dùng chung — ``_initials``, ``_fmt_updated``, ``_kind_avatar_icon`` —
|
||||
giống hệt ``shared/formatters.py`` đến từng dòng; nay dùng thẳng bản chung
|
||||
để định dạng thời gian và avatar không lệch nhau giữa các bảng Giám sát.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .agent_edit_dialog import AgentEditDialog
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
|
||||
QHeaderView, QLabel, QLineEdit, QMessageBox, QPlainTextEdit, QPushButton,
|
||||
QHBoxLayout, QHeaderView, QLabel, QMessageBox, QPushButton,
|
||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ....config import PROVIDER_LABELS
|
||||
from ....core import admin_agents, preview_ai
|
||||
from ....core import admin_agents
|
||||
from ....core.worker import AgentWorker
|
||||
from ....i18n import on_language_changed, tr
|
||||
from ....state import AppContext
|
||||
from ....ui.icons import icon
|
||||
from ....ui.widgets import ToggleSwitch, badge_pill_widget
|
||||
from .agent_edit_dialog import AgentEditDialog
|
||||
from ..shared.agent_kind_visuals import KIND_BADGE, STATUS_BADGE, kind_avatar_icon
|
||||
from ..shared.formatters import fmt_event_time
|
||||
|
||||
_PROVIDER_DEFAULT = "" # "" = each machine's own active provider (unchanged default)
|
||||
|
||||
# Identity colour (avatar circle) + badge tone per task_kind — same "fixed
|
||||
# colour regardless of theme" convention as monitoring_tab.py's per-agent
|
||||
# avatars, plus a badge object name (theme.py) for the Vai trò pill. Seven
|
||||
# kinds, seven distinct tones — no two kinds share a badge colour.
|
||||
_KIND_COLOUR = {
|
||||
"search": "#8A8886", "monitor": "#FFB900", "cowork": "#0078D4",
|
||||
"graphrag": "#8764B8", "schedule": "#107C10", "security": "#D13438",
|
||||
"help": "#E3008C",
|
||||
}
|
||||
_KIND_BADGE = {
|
||||
"search": "badgeNeutral", "monitor": "badgeWarn", "cowork": "badge",
|
||||
"graphrag": "badgePurple", "schedule": "badgeSuccess", "security": "badgeDanger",
|
||||
"help": "badgePink",
|
||||
}
|
||||
_STATUS_BADGE = {
|
||||
"unchecked": "badgeNeutral", "checking": "badgeWarn",
|
||||
"ok": "badgeSuccess", "bad": "badgeDanger",
|
||||
}
|
||||
|
||||
|
||||
def _initials(name: str) -> str:
|
||||
return "".join(w[0] for w in name.split() if w)[:2].upper()
|
||||
|
||||
|
||||
def _fmt_updated(ts: str) -> str:
|
||||
""""dd/MM hh:mm" — same Cập nhật/Thời gian format as Monitoring's
|
||||
Bảo mật/MCP/Hành động tables (``_fmt_event_time`` in monitoring_tab.py)."""
|
||||
try:
|
||||
dt = datetime.fromisoformat(ts)
|
||||
except (TypeError, ValueError):
|
||||
return ts
|
||||
return dt.strftime("%d/%m %H:%M")
|
||||
|
||||
|
||||
def _kind_avatar_icon(kind: str, name: str, size: int = 20) -> QIcon:
|
||||
pm = QPixmap(size, size)
|
||||
pm.fill(Qt.transparent)
|
||||
p = QPainter(pm)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
p.setPen(Qt.NoPen)
|
||||
p.setBrush(QColor(_KIND_COLOUR.get(kind, "#0078D4")))
|
||||
p.drawEllipse(0, 0, size, size)
|
||||
font = QFont()
|
||||
font.setPixelSize(max(7, size // 2))
|
||||
font.setBold(True)
|
||||
p.setFont(font)
|
||||
p.setPen(QColor("#FFFFFF"))
|
||||
p.drawText(pm.rect(), Qt.AlignCenter, _initials(name))
|
||||
p.end()
|
||||
return QIcon(pm)
|
||||
|
||||
|
||||
|
||||
|
||||
class AgentsAdminTab(QWidget):
|
||||
"""Tab "Quản trị Agent": cấu hình prompt, provider và model cho từng agent chuyên trách, kèm nút kiểm tra tình trạng hoạt động."""
|
||||
def __init__(self, ctx: AppContext):
|
||||
"""Dựng bảng agent quản trị.
|
||||
|
||||
Kết quả kiểm tra lần gần nhất được giữ trong ``_status`` và sống qua các lần
|
||||
vẽ lại — người dùng bấm Kiểm tra một lần rồi lọc/sắp xếp vẫn thấy kết quả.
|
||||
"""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
# Last operational-health result per agent_id → (ok, message). Populated
|
||||
@@ -178,14 +135,17 @@ class AgentsAdminTab(QWidget):
|
||||
|
||||
# ---- storage ---------------------------------------------------------
|
||||
def _dir(self):
|
||||
"""Thư mục chứa agent quản trị (dùng thư mục chia sẻ nếu có cấu hình)."""
|
||||
return admin_agents.agents_admin_dir(self.ctx.config.shared_dir)
|
||||
|
||||
def _default_model_hint(self) -> str:
|
||||
"""Model mặc định gợi ý cho agent mới — lấy từ provider đang chọn."""
|
||||
conf = self.ctx.config.provider_conf(self.ctx.config.active_provider)
|
||||
return conf.get("model", "")
|
||||
|
||||
# ---- CRUD -------------------------------------------------------------
|
||||
def _add(self) -> None:
|
||||
"""Thêm một agent quản trị mới qua hộp thoại."""
|
||||
dlg = AgentEditDialog(self, ctx=self.ctx, default_model_hint=self._default_model_hint())
|
||||
if not dlg.exec():
|
||||
return
|
||||
@@ -201,6 +161,7 @@ class AgentsAdminTab(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _edit_agent(self, agent_id: str) -> None:
|
||||
"""Sửa một agent; id không còn tồn tại thì bỏ qua."""
|
||||
agent = admin_agents.load_agent(agent_id, self._dir())
|
||||
if agent is None:
|
||||
return
|
||||
@@ -224,6 +185,7 @@ class AgentsAdminTab(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _delete_agent(self, agent_id: str) -> None:
|
||||
"""Xoá một agent sau khi hỏi xác nhận."""
|
||||
agent = admin_agents.load_agent(agent_id, self._dir())
|
||||
if agent is None:
|
||||
return
|
||||
@@ -235,6 +197,7 @@ class AgentsAdminTab(QWidget):
|
||||
self.refresh()
|
||||
|
||||
def _set_enabled(self, agent_id: str, enabled: bool) -> None:
|
||||
"""Bật/tắt một agent; trạng thái không đổi thì không ghi đĩa."""
|
||||
agent = admin_agents.load_agent(agent_id, self._dir())
|
||||
if agent is None or agent.enabled == enabled:
|
||||
return
|
||||
@@ -248,7 +211,7 @@ class AgentsAdminTab(QWidget):
|
||||
# ---- view --------------------------------------------------------------
|
||||
def _status_cell(self, agent_id: str) -> tuple:
|
||||
"""(state_key, display_text, tooltip) for the Trạng thái pill —
|
||||
state_key indexes _STATUS_BADGE for the badge's colour tone."""
|
||||
state_key indexes STATUS_BADGE for the badge's colour tone."""
|
||||
res = self._status.get(agent_id)
|
||||
if res is None:
|
||||
return ("unchecked", tr("agents_admin.status_unchecked").lstrip("— ").strip(),
|
||||
@@ -259,6 +222,7 @@ class AgentsAdminTab(QWidget):
|
||||
return ("ok" if ok else "bad"), (tr("agents_admin.status_ok") if ok else tr("agents_admin.status_bad")), msg
|
||||
|
||||
def _toggle_widget(self, agent_id: str, enabled: bool) -> QWidget:
|
||||
"""Dựng ô công tắc bật/tắt cho một dòng trong bảng."""
|
||||
container = QWidget()
|
||||
container.setStyleSheet("background: transparent;")
|
||||
lay = QHBoxLayout(container)
|
||||
@@ -271,6 +235,7 @@ class AgentsAdminTab(QWidget):
|
||||
return container
|
||||
|
||||
def _row_actions_widget(self, agent_id: str) -> QWidget:
|
||||
"""Dựng cụm nút sửa/xoá cho một dòng trong bảng."""
|
||||
container = QWidget()
|
||||
container.setStyleSheet("background: transparent;")
|
||||
lay = QHBoxLayout(container)
|
||||
@@ -295,6 +260,7 @@ class AgentsAdminTab(QWidget):
|
||||
def refresh(self) -> None:
|
||||
# Make sure the built-in in-app Help assistant exists, so the Admin can
|
||||
# manage its provider/model here (the floating Help widget uses it).
|
||||
"""Vẽ lại bảng agent."""
|
||||
admin_agents.ensure_help_agent(self._dir())
|
||||
agents = admin_agents.list_agents(self._dir())
|
||||
self.table.setRowCount(len(agents))
|
||||
@@ -307,10 +273,10 @@ class AgentsAdminTab(QWidget):
|
||||
model = tr("agents_admin.default_model", model=default_model or "—")
|
||||
|
||||
name_item = QTableWidgetItem(agent.name)
|
||||
name_item.setIcon(_kind_avatar_icon(agent.task_kind, agent.name))
|
||||
name_item.setIcon(kind_avatar_icon(agent.task_kind, agent.name))
|
||||
self.table.setItem(row, 0, name_item)
|
||||
|
||||
kind_tone = _KIND_BADGE.get(agent.task_kind, "badge")
|
||||
kind_tone = KIND_BADGE.get(agent.task_kind, "badge")
|
||||
self.table.setCellWidget(
|
||||
row, 1, badge_pill_widget(tr(f"agents_admin.kind.{agent.task_kind}"), kind_tone))
|
||||
|
||||
@@ -318,12 +284,12 @@ class AgentsAdminTab(QWidget):
|
||||
self.table.setCellWidget(row, 3, self._toggle_widget(agent.agent_id, agent.enabled))
|
||||
|
||||
state_key, status_text, status_tip = self._status_cell(agent.agent_id)
|
||||
status_widget = badge_pill_widget(status_text, _STATUS_BADGE[state_key])
|
||||
status_widget = badge_pill_widget(status_text, STATUS_BADGE[state_key])
|
||||
if status_tip:
|
||||
status_widget.setToolTip(status_tip)
|
||||
self.table.setCellWidget(row, 4, status_widget)
|
||||
|
||||
self.table.setItem(row, 5, QTableWidgetItem(_fmt_updated(agent.updated)))
|
||||
self.table.setItem(row, 5, QTableWidgetItem(fmt_event_time(agent.updated)))
|
||||
self.table.setCellWidget(row, 6, self._row_actions_widget(agent.agent_id))
|
||||
|
||||
# ResizeToContents doesn't measure a cell WIDGET's real width (only
|
||||
@@ -349,14 +315,17 @@ class AgentsAdminTab(QWidget):
|
||||
ctx = self.ctx
|
||||
|
||||
def job(_worker: AgentWorker) -> dict:
|
||||
"""Chạy nền: gọi thử từng agent để kiểm tra tình trạng hoạt động."""
|
||||
return {a.agent_id: admin_agents.check_agent(ctx, a) for a in agents}
|
||||
|
||||
def done(result: dict) -> None:
|
||||
"""Ghi kết quả kiểm tra vào bảng trạng thái rồi vẽ lại."""
|
||||
self.check_btn.setEnabled(True)
|
||||
self._status.update(result or {})
|
||||
self.refresh()
|
||||
|
||||
def failed(err: str) -> None:
|
||||
"""Kiểm tra lỗi: đánh dấu mọi agent là hỏng kèm lý do (cắt ở 200 ký tự)."""
|
||||
self.check_btn.setEnabled(True)
|
||||
for a in agents:
|
||||
self._status[a.agent_id] = (False, err[:200])
|
||||
@@ -369,6 +338,7 @@ class AgentsAdminTab(QWidget):
|
||||
w.start()
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề, gợi ý và tên cột."""
|
||||
self._title_lbl.setText(tr("agents_admin.page_title"))
|
||||
self._hint.setText(tr("agents_admin.hint"))
|
||||
self.table.setHorizontalHeaderLabels([
|
||||
|
||||
@@ -15,7 +15,9 @@ from ..shared.filter_scaffold import build_filter_scaffold
|
||||
|
||||
|
||||
class McpTab(QWidget):
|
||||
"""Tab "Lịch sử MCP": bảng các lượt gọi tool MCP kèm độ trễ và kết quả."""
|
||||
def __init__(self, ctx, on_refresh_all: Callable[[], None]):
|
||||
"""Tab Lịch sử MCP: bảng sự kiện kèm bộ lọc dùng chung."""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._ai_state: dict = {}
|
||||
@@ -32,9 +34,11 @@ class McpTab(QWidget):
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
self.table.set_events(events)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.table.retranslate()
|
||||
self.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder"))
|
||||
self.detail_panel.retranslate()
|
||||
@@ -42,4 +46,5 @@ class McpTab(QWidget):
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
start_ai_filter(self._ctx, search, ai_btn, self._ai_state)
|
||||
|
||||
@@ -25,10 +25,21 @@ from .sandbox_tab import SandboxDetailsCard
|
||||
|
||||
|
||||
class OverviewTab(QWidget):
|
||||
"""Tab "Tổng quan" của màn Giám sát: tài nguyên máy, thẻ token/chi phí, ngân
|
||||
sách, trạng thái sandbox, hoạt động gần đây và trích nhật ký kiểm toán.
|
||||
|
||||
Một cột dọc duy nhất, mỗi phần là một mục có tiêu đề — bố cục này là chủ ý
|
||||
để mắt quét từ trên xuống thay vì phải nhảy giữa hai cột.
|
||||
"""
|
||||
def __init__(self, ctx, *, on_status_message: Callable[[str], None],
|
||||
on_settings_changed: Callable[[], None],
|
||||
on_view_all_action_logs: Callable[[], None],
|
||||
action_logs_tab_visible: bool):
|
||||
"""Tab Tổng quan.
|
||||
|
||||
Mọi việc ra ngoài tab (báo trạng thái, mở màn khác) đều đi qua callback
|
||||
truyền vào, nên tab này không cần biết cửa sổ chính.
|
||||
"""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self._on_status_message = on_status_message
|
||||
@@ -64,6 +75,7 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- Token Usage & Cost ------------------------------------------------
|
||||
def _build_usage_section(self, root: QVBoxLayout) -> None:
|
||||
"""Dựng mục thẻ token/chi phí kèm thẻ ngân sách."""
|
||||
self.usage_group = QGroupBox()
|
||||
self.usage_group.setObjectName("monSection")
|
||||
usage_lay = QGridLayout(self.usage_group)
|
||||
@@ -88,6 +100,7 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- Recent Activity ----------------------------------------------------
|
||||
def _build_activity_section(self) -> None:
|
||||
"""Dựng mục "Hoạt động gần đây" (thêm vào cột ở dưới, cạnh nhật ký kiểm toán)."""
|
||||
self.activity_group = QGroupBox()
|
||||
self.activity_group.setObjectName("monSection")
|
||||
act_lay = QVBoxLayout(self.activity_group)
|
||||
@@ -98,12 +111,16 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- Resource Usage -------------------------------------------------------
|
||||
def _build_resource_section(self, root: QVBoxLayout) -> None:
|
||||
"""Dựng mục tài nguyên: CPU, bộ nhớ, đĩa, mạng và trạng thái sandbox."""
|
||||
self.resource_group = QGroupBox()
|
||||
self.resource_group.setObjectName("monSection")
|
||||
res_lay = QHBoxLayout(self.resource_group)
|
||||
res_lay.setSpacing(6)
|
||||
|
||||
def _pair():
|
||||
"""Thêm một cặp nhãn–giá trị vào hàng tài nguyên, tự chèn dấu phân cách giữa
|
||||
các cặp.
|
||||
"""
|
||||
if not self._res_first:
|
||||
sep = QLabel(chr(0xB7))
|
||||
sep.setObjectName("hint")
|
||||
@@ -117,6 +134,7 @@ class OverviewTab(QWidget):
|
||||
return lbl, val
|
||||
|
||||
def _bar_row():
|
||||
"""Thêm một cặp nhãn–giá trị kèm thanh tiến độ (CPU, bộ nhớ)."""
|
||||
lbl, val = _pair()
|
||||
bar = QProgressBar()
|
||||
bar.setVisible(False)
|
||||
@@ -132,6 +150,7 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- Audit Log preview --------------------------------------------------
|
||||
def _build_audit_section(self, root: QVBoxLayout, action_logs_tab_visible: bool) -> None:
|
||||
"""Dựng mục trích nhật ký kiểm toán; ẩn hẳn khi tab Nhật ký hành động bị tắt."""
|
||||
self.audit_group = QGroupBox()
|
||||
self.audit_group.setObjectName("monSection")
|
||||
audit_lay = QVBoxLayout(self.audit_group)
|
||||
@@ -156,6 +175,10 @@ class OverviewTab(QWidget):
|
||||
self._refresh_budget()
|
||||
|
||||
def _refresh_budget(self) -> None:
|
||||
"""Cập nhật thẻ ngân sách: còn lại / tổng, phần trăm đã dùng, cảnh báo khi vượt 85%.
|
||||
|
||||
Không ghi đè ô nhập khi người dùng đang gõ trong đó.
|
||||
"""
|
||||
from ....core import model_pricing as mp
|
||||
pricing = {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})}
|
||||
status = ut.budget_status(self.ctx.config)
|
||||
@@ -175,6 +198,12 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- resource usage --------------------------------------------------------
|
||||
def _refresh_resource_usage(self) -> None:
|
||||
"""Đọc CPU/RAM/đĩa/mạng của CHÍNH tiến trình app.
|
||||
|
||||
Đĩa và mạng là tốc độ tức thời, tính bằng hiệu hai lần lấy mẫu chia cho
|
||||
khoảng thời gian giữa chúng — nên lần đo đầu tiên luôn hiện "—". Không có
|
||||
``psutil`` thì cả mục chuyển sang "—" chứ không làm vỡ màn hình.
|
||||
"""
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
@@ -233,6 +262,7 @@ class OverviewTab(QWidget):
|
||||
self.network_val.setText(na)
|
||||
|
||||
def _set_resource_na(self) -> None:
|
||||
"""Đặt mọi chỉ số tài nguyên về "—" (khi thiếu ``psutil``)."""
|
||||
na = tr("monitoring.na")
|
||||
self.cpu_bar.setValue(0)
|
||||
self.cpu_val.setText(na)
|
||||
@@ -243,6 +273,7 @@ class OverviewTab(QWidget):
|
||||
|
||||
# ---- usage cards -----------------------------------------------------------
|
||||
def _activity_line(self, event: dict) -> str:
|
||||
"""Một dòng hoạt động: dấu ✓/!/✗ theo kết quả, tên việc, và thời gian tương đối."""
|
||||
ok = event.get("ok", True)
|
||||
if ok:
|
||||
mark = f"<span style='color:{DOT_GREEN};'>✓</span>"
|
||||
@@ -257,6 +288,11 @@ class OverviewTab(QWidget):
|
||||
return f"{mark} {name}{suffix}"
|
||||
|
||||
def _refresh_usage_cards(self) -> None:
|
||||
"""Cập nhật các thẻ token và chi phí.
|
||||
|
||||
Đồng bộ bảng đơn giá trước rồi mới tính tiền, để con số chi phí luôn khớp
|
||||
với bảng giá người dùng vừa sửa.
|
||||
"""
|
||||
from ....core import model_pricing as mp
|
||||
mp.sync_to_usage(self.ctx.config) # cost total comes straight from the price table
|
||||
pricing = {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})}
|
||||
@@ -297,6 +333,7 @@ class OverviewTab(QWidget):
|
||||
self.audit_lbl.setText(tr("monitoring.overview_no_activity"))
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho mọi tiêu đề mục và nhãn."""
|
||||
self.usage_group.setTitle(tr("monitoring.overview_usage_title").upper().replace("&", "&&"))
|
||||
self.budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip"))
|
||||
self.budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip"))
|
||||
|
||||
@@ -19,7 +19,13 @@ from ....ui.icons import icon
|
||||
|
||||
|
||||
class PricingPanel(QGroupBox):
|
||||
"""Bảng đơn giá model trong màn Giám sát: nhập, xuất, tự dò và sửa tay.
|
||||
|
||||
Đơn giá ở đây là thứ Dashboard dùng để quy token ra tiền, nên sửa ở đây là
|
||||
mọi con số chi phí trong app đổi theo.
|
||||
"""
|
||||
def __init__(self, ctx, on_status_message: Callable[[str], None]):
|
||||
"""Bảng đơn giá theo model, sửa được ngay tại chỗ."""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self._on_status_message = on_status_message
|
||||
@@ -68,6 +74,7 @@ class PricingPanel(QGroupBox):
|
||||
self._reload_table()
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề, nút và tên cột."""
|
||||
self.setTitle(tr("monitoring.pricing_title").upper())
|
||||
self.ccy_lbl.setText(tr("monitoring.pricing_currency"))
|
||||
self.import_btn.setText(tr("monitoring.pricing_import"))
|
||||
@@ -81,6 +88,7 @@ class PricingPanel(QGroupBox):
|
||||
tr("monitoring.pricing_col_output")])
|
||||
|
||||
def _reload_table(self, *_a) -> None:
|
||||
"""Nạp lại toàn bộ bảng đơn giá từ cấu hình."""
|
||||
from ....core import model_pricing as mp
|
||||
to_ccy = self.ccy.currentData() or "USD"
|
||||
entries = mp.list_entries(self.ctx.config)
|
||||
@@ -95,6 +103,7 @@ class PricingPanel(QGroupBox):
|
||||
self.table.setItem(r, c, QTableWidgetItem(str(v)))
|
||||
|
||||
def _import_pricing(self) -> None:
|
||||
"""Nhập đơn giá từ file Excel/CSV, gộp vào bảng hiện có."""
|
||||
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
||||
|
||||
from ....core import model_pricing as mp
|
||||
@@ -117,6 +126,7 @@ class PricingPanel(QGroupBox):
|
||||
self._on_status_message(tr("monitoring.pricing_imported", n=len(imported)))
|
||||
|
||||
def _export_pricing(self) -> None:
|
||||
"""Xuất file mẫu để điền đơn giá rồi nhập ngược lại."""
|
||||
from PySide6.QtWidgets import QFileDialog
|
||||
|
||||
from ....core import model_pricing as mp
|
||||
@@ -128,6 +138,7 @@ class PricingPanel(QGroupBox):
|
||||
self._on_status_message(tr("monitoring.pricing_exported"))
|
||||
|
||||
def _add_pricing_row(self) -> None:
|
||||
"""Thêm một dòng đơn giá trống để người dùng điền tay."""
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
from ....core import model_pricing as mp
|
||||
@@ -143,6 +154,7 @@ class PricingPanel(QGroupBox):
|
||||
self._reload_table()
|
||||
|
||||
def _autolink_pricing(self) -> None:
|
||||
"""Nhờ AI dò đơn giá cho các model đang dùng mà chưa có giá, chạy ở luồng nền."""
|
||||
from ....core import model_pricing as mp
|
||||
from ....core.worker import AgentWorker
|
||||
if self._worker is not None:
|
||||
@@ -152,9 +164,11 @@ class PricingPanel(QGroupBox):
|
||||
ccy = self.ccy.currentData() or "USD"
|
||||
|
||||
def job(_w):
|
||||
"""Chạy nền: nhờ AI dò đơn giá cho các model đang dùng mà chưa có giá."""
|
||||
return {"entries": mp.auto_link(ctx, default_ccy=ccy)}
|
||||
|
||||
def done(r):
|
||||
"""Đổ đơn giá vừa dò được vào bảng và lưu lại."""
|
||||
self._worker = None
|
||||
self.link_btn.setEnabled(True)
|
||||
self.ctx.save()
|
||||
@@ -162,6 +176,7 @@ class PricingPanel(QGroupBox):
|
||||
self._on_status_message(tr("monitoring.pricing_linked", n=len(r.get("entries", []))))
|
||||
|
||||
def failed(_e):
|
||||
"""Dò giá lỗi: chỉ mở khoá lại nút, giữ nguyên bảng đang có."""
|
||||
self._worker = None
|
||||
self.link_btn.setEnabled(True)
|
||||
|
||||
@@ -172,6 +187,7 @@ class PricingPanel(QGroupBox):
|
||||
w.start()
|
||||
|
||||
def _delete_pricing_row(self) -> None:
|
||||
"""Xoá dòng đơn giá đang chọn."""
|
||||
from ....core import model_pricing as mp
|
||||
row = self.table.currentRow()
|
||||
entries = mp.list_entries(self.ctx.config)
|
||||
|
||||
@@ -26,7 +26,11 @@ from .security_settings_tab import PermissionsCard
|
||||
|
||||
|
||||
class SandboxDetailsCard(QGroupBox):
|
||||
"""Thẻ "Trạng thái sandbox": backend cách ly đang dùng, giới hạn tài nguyên và
|
||||
trạng thái chặn mạng.
|
||||
"""
|
||||
def __init__(self, ctx, on_settings_changed: Callable[[], None]):
|
||||
"""Thẻ thông tin sandbox: cơ chế cách ly đang dùng và các giới hạn của nó."""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._on_settings_changed = on_settings_changed
|
||||
@@ -77,6 +81,7 @@ class SandboxDetailsCard(QGroupBox):
|
||||
detail_lay.addWidget(self.permissions_card)
|
||||
|
||||
def _open_settings(self) -> None:
|
||||
"""Mở hộp thoại Cài đặt tới nhóm an toàn, đóng xong thì làm mới thẻ."""
|
||||
open_settings_and_notify(self._ctx, self, self._on_settings_changed)
|
||||
|
||||
def _sync_more_label(self, *_a) -> None:
|
||||
@@ -85,6 +90,7 @@ class SandboxDetailsCard(QGroupBox):
|
||||
self.more_btn.setText(("▾ " if open_ else "▸ ") + tr("monitoring.overview_sbx_detail"))
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.setTitle(tr("monitoring.overview_sandbox_details_title").upper().replace("&", "&&"))
|
||||
self.id_lbl.setText(tr("monitoring.overview_sandbox_id"))
|
||||
self.status_lbl.setText(tr("monitoring.overview_status"))
|
||||
@@ -96,6 +102,7 @@ class SandboxDetailsCard(QGroupBox):
|
||||
self._sync_more_label()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Đọc lại cấu hình an toàn và cập nhật các dòng trạng thái."""
|
||||
sec = self._ctx.config.agent_security
|
||||
net_blocked = bool(sec.get("block_network"))
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@ from ..shared.filter_scaffold import build_filter_scaffold
|
||||
|
||||
|
||||
class SecurityEventsTab(QWidget):
|
||||
"""Tab "Sự kiện bảo mật": các lượt bị chặn hoặc phải xin phê duyệt."""
|
||||
def __init__(self, ctx, on_refresh_all: Callable[[], None]):
|
||||
"""Tab Sự kiện bảo mật.
|
||||
|
||||
Bỏ cột Kết quả (mọi dòng đều là bị chặn) và thay bằng cột Hành động có tô
|
||||
màu theo mức nghiêm trọng.
|
||||
"""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._ai_state: dict = {}
|
||||
@@ -34,9 +40,11 @@ class SecurityEventsTab(QWidget):
|
||||
self.detail_panel = parts["detail_panel"]
|
||||
|
||||
def set_events(self, events: List[dict]) -> None:
|
||||
"""Đổ danh sách sự kiện vào bảng."""
|
||||
self.table.set_events(events)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.table.retranslate()
|
||||
self.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder"))
|
||||
self.detail_panel.retranslate()
|
||||
@@ -44,4 +52,5 @@ class SecurityEventsTab(QWidget):
|
||||
self.title_refresh_btn.setText(tr("monitoring.refresh"))
|
||||
|
||||
def _start_ai_filter(self, search: QLineEdit, ai_btn: QPushButton) -> None:
|
||||
"""Nhờ AI dịch câu tìm kiếm tự nhiên thành từ khoá lọc."""
|
||||
start_ai_filter(self._ctx, search, ai_btn, self._ai_state)
|
||||
|
||||
@@ -20,7 +20,9 @@ from ..shared.open_settings import open_settings_and_notify
|
||||
|
||||
|
||||
class PermissionsCard(QGroupBox):
|
||||
"""Thẻ "Quyền": agent đang được phép làm gì với tệp, mạng và lệnh shell."""
|
||||
def __init__(self, ctx, on_settings_changed: Callable[[], None]):
|
||||
"""Thẻ thiết lập quyền: agent được đụng tới gì mà không phải hỏi."""
|
||||
super().__init__()
|
||||
self._ctx = ctx
|
||||
self._on_settings_changed = on_settings_changed
|
||||
@@ -38,9 +40,11 @@ class PermissionsCard(QGroupBox):
|
||||
perm_lay.addWidget(self.edit_btn, 0, Qt.AlignLeft)
|
||||
|
||||
def _open_settings(self) -> None:
|
||||
"""Mở hộp thoại Cài đặt tới nhóm an toàn, đóng xong thì làm mới thẻ."""
|
||||
open_settings_and_notify(self._ctx, self, self._on_settings_changed)
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.setTitle(tr("monitoring.overview_permissions_title").upper())
|
||||
self.fs_lbl.setText(tr("monitoring.overview_perm_fs"))
|
||||
self.fs_val.setText(tr("monitoring.overview_perm_fs_value"))
|
||||
@@ -52,6 +56,7 @@ class PermissionsCard(QGroupBox):
|
||||
self.edit_btn.setText(tr("monitoring.overview_edit"))
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Đọc lại cấu hình an toàn và cập nhật các dòng quyền."""
|
||||
net_blocked = bool(self._ctx.config.agent_security.get("block_network"))
|
||||
self.network_val.setText(
|
||||
tr("monitoring.overview_perm_network_blocked") if net_blocked
|
||||
|
||||
@@ -43,6 +43,7 @@ _TOOL_ICON_NAME = {
|
||||
|
||||
|
||||
def _tool_icon_pixmap(name: str, size: int = 28) -> QPixmap:
|
||||
"""Vẽ icon tròn cho một thẻ công cụ."""
|
||||
pm = QPixmap(size, size)
|
||||
pm.fill(Qt.transparent)
|
||||
p = QPainter(pm)
|
||||
@@ -59,6 +60,7 @@ def _tool_icon_pixmap(name: str, size: int = 28) -> QPixmap:
|
||||
|
||||
|
||||
def _clear_flow(flow: FlowLayout) -> None:
|
||||
"""Xoá sạch mọi widget trong một ``FlowLayout`` trước khi dựng lại."""
|
||||
while flow.count():
|
||||
item = flow.takeAt(0)
|
||||
w = item.widget()
|
||||
@@ -67,7 +69,13 @@ def _clear_flow(flow: FlowLayout) -> None:
|
||||
|
||||
|
||||
class ToolsAdminTab(QWidget):
|
||||
"""Tab "Công cụ" trong màn Giám sát: bật/tắt từng tool dựng sẵn và tool MCP.
|
||||
|
||||
Tắt một tool ở đây là agent không còn được gọi nó nữa — danh sách tắt lưu
|
||||
trong cấu hình dưới dạng ``tools_disabled``.
|
||||
"""
|
||||
def __init__(self, ctx: AppContext):
|
||||
"""Tab quản trị tool, chia thành các tab con theo nhóm tool."""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
root = QVBoxLayout(self)
|
||||
@@ -133,12 +141,14 @@ class ToolsAdminTab(QWidget):
|
||||
|
||||
# ---- built-in tools card grid ---------------------------------------------
|
||||
def refresh(self) -> None:
|
||||
"""Vẽ lại toàn bộ lưới thẻ công cụ theo cấu hình mới nhất."""
|
||||
disabled = set(self.ctx.config.tools_disabled)
|
||||
_clear_flow(self._tool_flow)
|
||||
for spec in TOOL_SPECS:
|
||||
self._tool_flow.addWidget(self._tool_card(spec, spec.name not in disabled))
|
||||
|
||||
def _tool_card(self, spec, enabled: bool) -> QWidget:
|
||||
"""Dựng một thẻ công cụ: icon, tên, mô tả và công tắc bật/tắt."""
|
||||
card = QFrame()
|
||||
card.setFrameShape(QFrame.NoFrame)
|
||||
style_card(card)
|
||||
@@ -190,6 +200,7 @@ class ToolsAdminTab(QWidget):
|
||||
return card
|
||||
|
||||
def _toggle_builtin(self, name: str, enabled: bool) -> None:
|
||||
"""Bật/tắt một tool dựng sẵn và ghi ngay vào cấu hình."""
|
||||
self.ctx.config.set_tool_enabled(name, enabled)
|
||||
# For fetch_url, the Enabled toggle also governs the runtime web-access
|
||||
# gate (agent_security.allow_url_fetch) — one control for the capability.
|
||||
@@ -209,17 +220,20 @@ class ToolsAdminTab(QWidget):
|
||||
return
|
||||
|
||||
def job(worker):
|
||||
"""Chạy nền: chẩn đoán kết nối Internet và tình trạng chứng chỉ TLS."""
|
||||
from ....core import tls_trust
|
||||
ok, message = tls_trust.diagnose_internet()
|
||||
return {"ok": ok, "message": message}
|
||||
|
||||
def done(result):
|
||||
"""Hiện kết quả chẩn đoán, tô xanh/đỏ theo kết quả."""
|
||||
ok = result.get("ok")
|
||||
self.test_internet_status.setText(result.get("message", ""))
|
||||
self.test_internet_status.setStyleSheet("color: #090;" if ok else "color: #c00;")
|
||||
self.test_internet_btn.setEnabled(True)
|
||||
|
||||
def failed(e):
|
||||
"""Chẩn đoán ném lỗi: hiện lỗi bằng màu đỏ."""
|
||||
self.test_internet_status.setText(str(e))
|
||||
self.test_internet_status.setStyleSheet("color: #c00;")
|
||||
self.test_internet_btn.setEnabled(True)
|
||||
@@ -235,6 +249,7 @@ class ToolsAdminTab(QWidget):
|
||||
|
||||
# ---- i18n -----------------------------------------------------------------
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn rồi vẽ lại lưới thẻ."""
|
||||
self.subtabs.setTabText(0, tr("tools_admin.subtab_tool"))
|
||||
self.subtabs.setTabText(1, tr("tools_admin.subtab_connector"))
|
||||
self._hint.setText(tr("tools_admin.hint"))
|
||||
|
||||
Reference in New Issue
Block a user