Lane N3 (Co4E Studio) — dùng bộ workflow refactor-god-file, mỗi bước có characterization test trước khi tách, hậu kiểm ranh giới tầng sau mỗi bước: - skills_list_panel.py / agent_list_panel.py — 2 khu vực sidebar - co4e_canvas_widget.py + canvas_items.py + canvas_interaction_mixin.py — Co4ECanvas tách 3 file (vượt 400 dòng nếu đứng một mình) - node_property_panel.py + node_property_actions_mixin.py + step_config_section.py — StepConfigPanel, cùng lý do - co4e_run_control_widget.py — RunsPagePanel (trang Flow Status) - co4e_chat_view.py — ChatPanel + _ChatInput + helper autocomplete - palette_list.py — _PaletteList dời khỏi ui/co4e_tab.py, hết import ngược presentation -> ui (agent/skills panel giờ import top-level) ui/co4e_tab.py giảm 2089 -> 1878 dòng, chỉ còn phần wiring + business logic (Co4ERunManager/AgentWorker chưa đổi — nằm ngoài phạm vi này, xem docstring presentation/co4e/co4e_tab.py). ui/co4e_canvas.py và ui/co4e_config_panel.py còn lại là compat shim re-export, không đổi API cho bên gọi. Thêm tests/test_co4e_integration.py — dựng thật Co4ETab qua build_co4e_tab(), lái luồng qua nhiều panel trong cùng instance (thêm node, mở/gập chat, chuyển trang Flow Status rồi quay lại không mất state canvas) — bắt lỗi wiring xuyên-panel mà characterization test từng panel riêng không thấy được. Đã xác minh: pytest 348 passed/1 skipped, tools/check_co4e.py sạch, không file nào >400 dòng, domain/application không import PySide6, và so pixel before/after (git worktree tại HEAD cũ) ra 0/1.125.000 pixel khác biệt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
6.2 KiB
Python
115 lines
6.2 KiB
Python
"""Bản ghi "một lần chạy flow" — DTO thuần Python cho tầng domain.
|
|
|
|
Bối cảnh: ``Co4ERunManager``/``RunHandle`` cũ (``core/co4e_run_manager.py``)
|
|
trộn ba việc vào một ``QObject``: (1) dữ liệu một run cần nhớ để hiện Flow
|
|
Status, (2) logic chạy job trên ``AgentWorker``/``QThread``, và (3) logic
|
|
đọc/ghi lịch sử ra đĩa. Tách phần (1) ra thành ``RunRecord`` ở đây giúp nó độc
|
|
lập với Qt và với việc đọc/ghi đĩa — đúng quy ước ``domain/__init__.py``: domain
|
|
không được biết PySide6 tồn tại và không được chạm đĩa/mạng. Phần (2) và (3)
|
|
chuyển sang ``application/workflows/co4e_workflow_service.py``
|
|
(``Co4EWorkflowService``), nơi được phép import ``core/`` và làm việc với đĩa.
|
|
|
|
Vì sao trường ``wf`` là dict thô chứ không phải đối tượng ``Workflow``: lớp
|
|
``Workflow`` sống ở ``core/co4e.py``, và việc dựng nó từ/thành dict
|
|
(``workflow_to_dict``/``workflow_from_dict``) nằm trong module đó. Domain
|
|
không được import ``cowork_local.core.*``, nên ``RunRecord`` giữ nguyên đúng
|
|
hình dạng dữ liệu mà bản ghi lịch sử đã có sẵn trên đĩa hôm nay: một dict thô
|
|
(kết quả ``workflow_to_dict``) hoặc ``None``. Việc quy đổi dict <-> đối tượng
|
|
``Workflow`` là việc của tầng application, nơi được phép import ``core``.
|
|
|
|
Quirk giữ nguyên có chủ ý — đã bị "đóng đinh" bởi
|
|
``tests/characterization/test_co4e_run_manager_behavior.py`` (quirk #1 và #7
|
|
trong docstring đầu file đó, xem thêm ``RunHandle.to_record``/``from_record``
|
|
gốc) — ĐỪNG "dọn" các chỗ này khi đọc code dưới đây, chúng trông như bug nhưng
|
|
là hành vi đã được test khẳng định:
|
|
* ``total`` âm bị ``max(0, total)`` kẹp về 0 ngay lúc khởi tạo, không giữ
|
|
nguyên giá trị âm.
|
|
* ``from_dict()`` đổi ``status == "running"`` đọc từ đĩa thành ``"stopped"``
|
|
(lý do: app tắt giữa lúc một run đang "running" thì worker của nó đã mất
|
|
theo, nên đọc lại không còn coi là đang chạy) — nhưng ``to_dict()`` vẫn ghi
|
|
đúng ``"running"`` xuống đĩa tại thời điểm lưu. Đây là một round-trip
|
|
*không đối xứng* có chủ ý.
|
|
* ``from_dict({})``/``from_dict(None)`` mặc định ``status`` là ``"done"``
|
|
(không phải ``"running"``) — nên KHÔNG bị nhánh phía trên đổi thành
|
|
"stopped".
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, Optional
|
|
|
|
|
|
class RunRecord:
|
|
"""DTO domain: trạng thái sống của một lần chạy flow, thuần dữ liệu.
|
|
|
|
Vai trò: đây là "danh từ" mà ``Co4EWorkflowService`` (application/) đọc/ghi
|
|
và mà UI Flow Status hiển thị — không có hành vi chạy worker, không đọc/ghi
|
|
đĩa. Nó ở tầng domain vì đây là quy tắc nghiệp vụ ổn định (hình dạng một
|
|
lần chạy flow cần nhớ những gì) độc lập với Qt lẫn với cơ chế lưu trữ.
|
|
"""
|
|
|
|
def __init__(self, run_id: str, wf_id: str, name: str, total: int,
|
|
plan_mode: bool, manual: bool, created_by: str = "", created_at: str = "",
|
|
project_id: str = ""):
|
|
self.id = run_id
|
|
self.wf_id = wf_id
|
|
self.name = name
|
|
self.project_id = project_id # workspace run này thuộc về (Flow Status lọc theo project)
|
|
self.total = max(0, total) # quirk cố ý: total âm bị kẹp về 0, xem docstring đầu file
|
|
self.done = 0
|
|
self.status = "running" # running | done | error | stopped
|
|
self.plan_mode = plan_mode
|
|
self.manual = manual
|
|
self.created_by = created_by
|
|
self.created_at = created_at
|
|
self.error = ""
|
|
self.node_status: Dict[str, str] = {}
|
|
self.wf: Optional[dict] = None # snapshot workflow dạng dict thô (xem docstring đầu file)
|
|
self.out_dir = "" # thư mục workspace mà run này ghi file vào
|
|
|
|
@property
|
|
def running(self) -> bool:
|
|
return self.status == "running"
|
|
|
|
def progress_text(self) -> str:
|
|
return f"{self.done}/{self.total}" if self.total else self.status
|
|
|
|
# ---- (de)serialization --------------------------------------------
|
|
def to_dict(self) -> dict:
|
|
"""Hình dạng bản ghi lịch sử trên đĩa.
|
|
|
|
PHẢI khớp đúng bộ khoá mà ``RunHandle.to_record()`` gốc
|
|
(``core/co4e_run_manager.py``) đang ghi hôm nay — file JSON lịch sử cũ
|
|
và mới dùng chung một định dạng trong lúc cả hai lớp còn chạy song
|
|
song (bản cũ chưa bị xoá).
|
|
"""
|
|
return {
|
|
"id": self.id, "wf_id": self.wf_id, "name": self.name,
|
|
"total": self.total, "done": self.done, "status": self.status,
|
|
"plan_mode": self.plan_mode, "manual": self.manual,
|
|
"created_by": self.created_by, "created_at": self.created_at,
|
|
"error": self.error, "node_status": dict(self.node_status),
|
|
"wf": self.wf, "out_dir": self.out_dir, "project_id": self.project_id,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, rec: dict) -> "RunRecord":
|
|
rec = dict(rec or {})
|
|
r = cls(str(rec.get("id", "")), str(rec.get("wf_id", "")),
|
|
rec.get("name", ""), int(rec.get("total", 0) or 0),
|
|
bool(rec.get("plan_mode")), bool(rec.get("manual")),
|
|
created_by=rec.get("created_by", ""), created_at=rec.get("created_at", ""))
|
|
r.done = int(rec.get("done", 0) or 0)
|
|
r.status = rec.get("status", "done")
|
|
# quirk cố ý (xem docstring đầu file): round-trip không đối xứng —
|
|
# "running" đọc lại từ đĩa luôn bị chốt thành "stopped".
|
|
if r.status == "running":
|
|
r.status = "stopped"
|
|
r.error = rec.get("error", "")
|
|
r.node_status = dict(rec.get("node_status") or {})
|
|
r.out_dir = rec.get("out_dir", "")
|
|
r.project_id = rec.get("project_id", "")
|
|
# Giữ nguyên dict thô -- KHONG parse thanh doi tuong Workflow o day (do
|
|
# la viec cua tang application, xem docstring dau file).
|
|
r.wf = rec.get("wf")
|
|
return r
|