CI / test (pull_request) Canceled after 0s
Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:
* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của Qt; ứng dụng không cài
``QTranslator`` nào và bản PySide6 đang dùng cũng không đóng gói file
``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.
``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.
Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:
* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").
Kèm theo, phần giao diện người dùng yêu cầu:
* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.
Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.
Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
196 lines
9.4 KiB
Python
196 lines
9.4 KiB
Python
"""Tạo, sửa, đổi tên, xoá, nhân bản workflow — R08-T09.
|
|
|
|
Chỉ thao tác trên danh sách. Phần chạy một workflow nằm ở ``co4e_runs.py``,
|
|
phần vẽ node nằm ở canvas.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import List, Optional
|
|
from PySide6.QtCore import QSize, Qt
|
|
from PySide6.QtWidgets import QMenu
|
|
from ...core import co4e
|
|
from ...core.co4e import STEP_DONE, STEP_RUNNING
|
|
from ...i18n import tr
|
|
from ...ui.dialog_buttons import ask_text
|
|
from ...ui.icons import icon
|
|
from ...presentation.co4e.co4e_chat_view import _skill_names
|
|
|
|
_LOCKED_NODE_STATUSES = (STEP_RUNNING, STEP_DONE)
|
|
|
|
|
|
class Co4EWorkflowCrudMixin:
|
|
"""Phần tạo/mở/lưu/xoá luồng của Co4E Studio.
|
|
|
|
Tách khỏi ``Co4ETab`` vì đây là nhóm việc trên *danh sách luồng đã lưu*,
|
|
không phải trên khung vẽ đang mở — dù hai bên nói chuyện với nhau liên tục.
|
|
"""
|
|
def _apply_workflow(self, wf: co4e.Workflow) -> None:
|
|
"""Đưa một luồng lên khung vẽ và chuyển hội thoại sang nhật ký của chính nó.
|
|
|
|
KHÔNG xoá ``_flow_outputs``: mỗi luồng giữ ngữ cảnh tích luỹ riêng, đổi
|
|
tab mà xoá là mất kết quả của luồng khác. Luồng cũ xếp dọc thì tự bẻ lại
|
|
thành trái→phải cho khớp bố cục hiện tại.
|
|
"""
|
|
self._wf = wf
|
|
# Per-flow outputs are kept in self._flow_outputs[wf.id] — do NOT clear
|
|
# here (switching tabs must not wipe another flow's accumulated context).
|
|
# Switch the visible conversation to THIS flow's own log.
|
|
self.chat_stack.setCurrentWidget(self._ensure_flow_log(wf.id))
|
|
self.name_edit.setText(wf.name)
|
|
self.canvas.load(wf.nodes, wf.edges)
|
|
self.config.clear_step()
|
|
if wf.nodes:
|
|
self.canvas.relayout_if_vertical() # convert old top-down flows to left→right
|
|
self.canvas.fit_view()
|
|
self._update_run_btn() # reflect THIS flow's run state
|
|
self._refresh_usage_total() # show THIS flow's token/cost total
|
|
def _new_workflow(self) -> None:
|
|
"""Tạo luồng trống mới, đưa con trỏ vào ô tên và báo trạng thái.
|
|
|
|
Trước đây bấm nút này khi khung vẽ đã có sẵn một luồng trống chưa đặt tên
|
|
sẽ tạo ra một luồng trống y hệt — đúng logic nhưng nhìn không thấy gì đổi,
|
|
nên nút bị hiểu là hỏng. Giờ nói rõ vừa xảy ra chuyện gì và đặt con trỏ
|
|
vào đúng việc tiếp theo: đặt tên.
|
|
"""
|
|
self._open_flow(co4e.new_workflow(tr("co4e.untitled")))
|
|
# Pressing this while the canvas already holds an empty untitled flow
|
|
# produced an identical empty untitled flow — correct, and completely
|
|
# invisible, so the button read as broken. Say what happened and put the
|
|
# cursor where the next thing to do is: naming it.
|
|
self.name_edit.setFocus()
|
|
self.name_edit.selectAll()
|
|
self.status_message.emit(tr("co4e.new_flow_ready"))
|
|
def _selected_wf(self) -> Optional[co4e.Workflow]:
|
|
"""Materialise the selected saved-flow row into a Workflow."""
|
|
item = self.wf_list.currentItem()
|
|
if item is None:
|
|
return None
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
return co4e.get_workflow(ident)
|
|
def _load_selected_workflow(self, *_a) -> None:
|
|
"""Mở luồng đang chọn trong danh sách (hoặc chuyển sang tab của nó nếu đã mở)."""
|
|
wf = self._selected_wf()
|
|
if wf is not None:
|
|
self._open_flow(wf) # open (or focus) its browser-style tab
|
|
def _edit_selected_workflow(self) -> None:
|
|
"""Mở luồng đang chọn để sửa; chưa chọn gì thì nhắc người dùng chọn."""
|
|
wf = self._selected_wf()
|
|
if wf is None:
|
|
self.status_message.emit(tr("co4e.select_flow"))
|
|
return
|
|
self._open_flow(wf)
|
|
def _duplicate_selected_workflow(self) -> None:
|
|
"""Nhân bản luồng đang chọn thành một luồng mới rồi nạp lại danh sách."""
|
|
wf = self._selected_wf()
|
|
if wf is None:
|
|
self.status_message.emit(tr("co4e.select_flow"))
|
|
return
|
|
dup = co4e.duplicate_workflow(wf)
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.duplicated_msg", name=dup.name))
|
|
def _wf_context_menu(self, pos) -> None:
|
|
"""Menu chuột phải trên danh sách luồng: sửa, đổi tên, nhân bản, chạy nền, xoá."""
|
|
lw = self.wf_list
|
|
item = lw.itemAt(pos)
|
|
if item is None:
|
|
return
|
|
lw.setCurrentItem(item)
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
menu = QMenu(lw)
|
|
act_edit = menu.addAction(icon("edit"), tr("co4e.edit"))
|
|
act_rename = menu.addAction(icon("edit"), tr("co4e.rename"))
|
|
act_dup = menu.addAction(icon("branch"), tr("co4e.duplicate"))
|
|
act_run = menu.addAction(icon("play"), tr("co4e.run_bg"))
|
|
act_del = menu.addAction(icon("trash"), tr("co4e.delete"))
|
|
chosen = menu.exec(lw.viewport().mapToGlobal(pos))
|
|
if chosen is act_edit:
|
|
self._edit_selected_workflow()
|
|
elif chosen is act_rename:
|
|
self._rename_workflow(ident)
|
|
elif chosen is act_dup:
|
|
self._duplicate_selected_workflow()
|
|
elif chosen is act_run:
|
|
self._run_selected_in_background()
|
|
elif chosen is act_del:
|
|
self._delete_selected_workflow()
|
|
def _rename_workflow(self, ident: str) -> None:
|
|
"""Rename a saved flow in place (e.g. to match its function/task)."""
|
|
wf = co4e.get_workflow(ident)
|
|
if wf is None:
|
|
return
|
|
name, ok = ask_text(self, tr("co4e.rename"), tr("co4e.rename_prompt"),
|
|
text=wf.name)
|
|
name = (name or "").strip()
|
|
if not ok or not name:
|
|
return
|
|
wf.name = name
|
|
co4e.save_workflow(wf)
|
|
if self._wf.id == ident:
|
|
self.name_edit.setText(name)
|
|
self._wf.name = name
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.renamed_msg", name=name))
|
|
def _delete_selected_workflow(self) -> None:
|
|
"""Xoá luồng đang chọn khỏi đĩa rồi nạp lại danh sách."""
|
|
item = self.wf_list.currentItem()
|
|
if item is None:
|
|
return
|
|
_kind, ident = item.data(Qt.UserRole)
|
|
co4e.delete_workflow(ident)
|
|
self._reload_sidebar()
|
|
def _sync_wf_from_canvas(self) -> None:
|
|
"""Chép node, cạnh và tên từ khung vẽ về đối tượng luồng trước khi lưu."""
|
|
self._wf.nodes = self.canvas.nodes()
|
|
self._wf.edges = self.canvas.edges()
|
|
self._wf.name = self.name_edit.text().strip() or tr("co4e.untitled")
|
|
def _save(self, as_template: bool) -> None:
|
|
"""Lưu luồng đang mở; ``as_template`` đánh dấu nó là mẫu dùng lại được."""
|
|
self._sync_wf_from_canvas()
|
|
self._wf.is_template = as_template
|
|
co4e.save_workflow(self._wf)
|
|
self._reload_sidebar()
|
|
self.status_message.emit(tr("co4e.saved_msg", name=self._wf.name))
|
|
def _autosave(self) -> None:
|
|
"""Tự lưu sau mỗi thay đổi trên khung vẽ — nhưng chỉ với luồng ĐÃ tồn tại
|
|
trên đĩa, để luồng nháp chưa đặt tên không tự nhảy vào danh sách.
|
|
"""
|
|
if co4e.get_workflow(self._wf.id) is not None:
|
|
self._sync_wf_from_canvas()
|
|
co4e.save_workflow(self._wf)
|
|
def _on_name_changed(self, text: str) -> None:
|
|
"""Gõ tên mới thì cập nhật luôn nhãn tab của luồng đang mở."""
|
|
self._wf.name = text.strip() or tr("co4e.untitled")
|
|
self._sync_active_flow_tab_text()
|
|
def _add_blank_step(self) -> None:
|
|
"""Thêm một bước trống vào giữa khung nhìn hiện tại."""
|
|
self.canvas.add_palette_step(co4e.Step(label="New Step"),
|
|
self.canvas.mapToScene(self.canvas.rect().center()))
|
|
def _on_node_selected(self, node_id: str) -> None:
|
|
"""Chọn một node thì nạp bước đó vào bảng thuộc tính, tự mở bảng nếu đang gập.
|
|
|
|
Bước đang chạy hoặc đã chạy xong thì khoá ô nhập liệu ngay khi nạp —
|
|
tránh sửa nhầm cấu hình của lần chạy đang xem kết quả.
|
|
"""
|
|
for n in self.canvas.nodes():
|
|
if n.id == node_id:
|
|
self.config.load_step(node_id, n.data, _skill_names())
|
|
self.config.set_locked(self.canvas.node_status(node_id) in _LOCKED_NODE_STATUSES)
|
|
if self._config_collapsed:
|
|
self._toggle_config()
|
|
return
|
|
def _on_config_changed(self) -> None:
|
|
"""Sửa thuộc tính bước thì vẽ lại mọi node (nhãn/model có thể đổi) rồi tự lưu."""
|
|
for n in self.canvas.nodes():
|
|
self.canvas.refresh_node(n.id)
|
|
self._autosave()
|
|
def _wf_by_id(self, wf_id: str) -> Optional[co4e.Workflow]:
|
|
"""Resolve a flow id to a Workflow — saved, or the open canvas."""
|
|
wf = co4e.get_workflow(wf_id)
|
|
if wf is not None:
|
|
return wf
|
|
if self._wf.id == wf_id:
|
|
self._sync_wf_from_canvas()
|
|
return self._wf
|
|
return None
|