從這篇開始,會真正打開 WinFlow 2.0 的 GUI。本系列不打算把每個函式都講一遍,目標比較務實:讓你能依照自己公司的口令,把 Flow 組出來。架構細節、開發筆記會另外放在 Repo 的 doc/,需要時再翻。
Repo:WinFlow 2.0
若 Linux 環境沒有 Tkinter,先補裝(常見於 WSL):
sudo apt update
sudo apt install python3-tk
LSF 也請先確認能動(見 Day 3)。完成後在 repo 根目錄執行:
python winflow_gui.py
視窗開起來後,先切到 Generator
Template 清單會列出 blank,以及所有成功註冊進系統的 Flow。內建至少會看到 example。我們先選 example,再按 Load。
若按的是 Open,則是去開一份已經符合格式的
flow.json,不是載範本。

若按照前面幾天的設定,Queue 名稱多半是 normal。不確定就再跑一次 bqueues。下面的 Reference 今天先略過,直接按 Load Template。

一條完整的 APR + QC Flow 會出現在畫布上。這就是昨天那份 JSON 的視覺版:flow 往右走,QC 往旁邊長。
接下來做一件範本裡沒有提供的事:新增一個 QC Summary。它接在 Route 後面,而且必須等所有 QC 都完成才能跑
點 Add Job,會看到目前已註冊的 Job 節點。把必要欄位填好,按住 Left Ctrl 把所有要當 Parent 的節點選起來,最後按 Save。
GUI 做的事情,其實就是幫你寫昨天那些 parents / children。你選的是節點,系統寫的是 stage/task/job key。
會用之後,來看它為什麼能「加一個資料夾就多一種範本」。目錄大致如下
winflow/generator/
├── __init__.py
├── __main__.py
├── cli.py # 命令列入口與流水線編排
├── core/
│ ├── builder.py # FlowBuilder 抽象類
│ ├── registry.py # @register / get_builder / list_flows
│ ├── context.py # BuildContext
│ ├── models.py # Flow/Stage/Task/Job + make_* 工廠
│ └── io.py # 寫出 flow.json
├── parsers/ # 讀設定檔,後面帶入 builder 會用到
│ ├── setting_sh.py # 解析 csh set 語句
│ └── block_stream.py # 解析 block 列表(example 未強制使用)
├── flows/
│ ├── __init__.py
│ └── example/ # **我們現在畫面上看到的**
│ ├── __init__.py
│ └── builder.py # ExampleFlowBuilder
├── editor/ # 畫布用的文件/依賴/佈局(不是 Tk 本體)
│ ├── document.py # FlowDocument、模板載入、匯出排序
│ ├── deps.py # link/unlink parents/children
│ ├── graph.py # 畫布 JobGraph / layout
│ └── nodes.py # node/*.json 模板庫讀寫
└── node/ # Add Job 預置節點,JSON 放這裡
├── blank_job.json
├── FLOOR_PLAN.json
├── Q_FLOOR_PLAN.json
└── ...
這裡有兩條完全不同的「註冊」:
| 你想加什麼 | 放哪裡 | GUI 哪裡出現 |
|---|---|---|
| 一種完整 Flow 範本(一載入就是一張圖) | flows/<name>/ + @register |
Template 下拉選單 |
| 一顆可重用的 Job 積木 | generator/node/*.json |
Add Job 清單 |
不要把兩件事混在同一個資料夾。範本是「一整場蘿蔔蹲的預設口令」;積木是「臨時加進來的一個人」。
假設我們要新增一種 Flow,名稱叫 ithome。
├── flows/
│ ├── __init__.py # 加上 from winflow.generator.flows import ithome
│ ├── example/
│ │ ├── __init__.py
│ │ └── builder.py
│ └── ithome/ # 新建這個資料夾
│ ├── __init__.py # 把 ithomeFlowBuilder 匯出
│ └── builder.py
關鍵有三步,少一步 GUI 就看不見:
builder.py 裡用 @register("ithome") 掛上 FlowBuilder 子類ithome/__init__.py 把 class 匯出flows/__init__.py import 這個 package,讓 decorator 在程式啟動時真的跑到Template 下拉選單不是寫死 blank / example。它會去問 list_flows():誰成功註冊,誰就出現。所以 ithome 一掛上,重開 GUI 就會在清單裡。
先寫一個空的 builder.py:
from __future__ import annotations
from dataclasses import dataclass
from typing import List
from winflow.config import get_config, get_section
from winflow.generator.core.builder import FlowBuilder
from winflow.generator.core.context import BuildContext
from winflow.generator.core.models import Flow, make_flow
from winflow.generator.core.registry import register
@dataclass(frozen=True)
class IthomeFlowConfig:
"""Defaults for the ithome flow; override via config.json without models.py."""
flow_name: str = "ithome"
@register("ithome")
class ithomeFlowBuilder(FlowBuilder):
"""Build the bundled demo place-and-route style example flow."""
@classmethod
def validate_context(cls, context: BuildContext) -> List[str]:
return []
@classmethod
def build(cls, context: BuildContext) -> Flow:
cfg = get_section("ithome", IthomeFlowConfig())
gen_cfg = get_config().generator
stages = []
return make_flow(cfg.flow_name, stages, poll_interval=gen_cfg.poll_interval)
get_section("ithome", IthomeFlowConfig()) 值得多看一眼:預設值活在這個 flow 自己的 dataclass 裡,不必去改中央的 models.py。config.json 若有 "ithome" 區塊會覆寫;沒有就用這裡的預設。新 flow 不該每次都去碰核心設定檔。
重開 GUI 後,Template 清單會出現 ithome。現在它還是空殼,畫布上沒有任何 Stage。接下來我們把"人"放進場。
預期結果:
IT_2 ______________
|
v
IT_1 —> IT_3 —> IT4 -> IT_5
| ^
|________________|
口令翻譯成依賴:
IT_1 與 IT_2 都是 Root,可以一起跑IT_3 等 IT_1
IT4 等 IT_3 與 IT_2
IT_5 等 IT_3 與 IT4(因此也間接等齊 IT_2)先補常用函式與預設設定:
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from winflow.config import get_config, get_section
from winflow.generator.core.builder import FlowBuilder
from winflow.generator.core.context import BuildContext
from winflow.generator.core.models import Flow, Job, Stage, make_flow, make_job
from winflow.generator.core.registry import register
JOB_NAMES = ("IT_1", "IT_2", "IT_3", "IT4", "IT_5")
@dataclass(frozen=True)
class IthomeFlowConfig:
"""Defaults for the ithome flow; override via config.json without models.py."""
flow_name: str = "ithome"
script_dir: str = "ithome_flow"
out_dir: str = "ithome_flow/out"
command_template: str = "./{script_dir}/{job_name}.csh"
output_template: str = "{out_dir}/{job_name}.done"
default_queue: str = "normal"
default_cpu: str = "1"
# 幫忙打包一些設定值用的
def _cfg() -> IthomeFlowConfig:
return get_section("ithome", IthomeFlowConfig())
def job_output_path(job_name: str) -> str:
cfg = _cfg()
return cfg.output_template.format(job_name=job_name, out_dir=cfg.out_dir)
def job_command(job_name: str) -> str:
cfg = _cfg()
return cfg.command_template.format(job_name=job_name, script_dir=cfg.script_dir)
# 這邊是因為設計架構上我讓每個 job 都有額外的 stage 跟 Task 的屬性
# 主要是希望建 flow 的人可以把階層區分好,也許有利以後的操作,但實際 Flow 在跑只會依賴 parent 跟 child 的關係
# 我們這邊就先偷懶讓 stage 跟 Task 都跟 job 同名
def _wrap_jobs_as_stages(jobs: List[Job]) -> List[Stage]:
"""Flow schema requires stage/task nesting; derive both from each job name."""
stages: List[Stage] = []
for job in jobs:
name = job["name"]
stages.append({"name": name, "tasks": [{"name": name, "jobs": [job]}]})
return stages
重點在 build_ithome_stages。注意我們沒有手寫 parents / children,只宣告每個 job 的 input / output 檔。WinFlow 會在欄位還缺的時候,用檔案對應把依賴給補出來:
def build_ithome_stages(
queue: Optional[str] = None,
cpu: Optional[str] = None,
machine: str = "",
) -> List[Stage]:
cfg = _cfg()
queue = queue if queue is not None else cfg.default_queue
cpu = cpu if cpu is not None else cfg.default_cpu
out = {name: job_output_path(name) for name in JOB_NAMES}
# WinFlow2.0 會透過 in/output , 自動去生成 parent 跟 child 的關係
# 使用者只需要哪些 job 結束後換誰跑就好了
jobs = [
make_job("IT_1", job_command("IT_1"), [], [out["IT_1"]], queue, cpu, machine),
make_job("IT_2", job_command("IT_2"), [], [out["IT_2"]], queue, cpu, machine),
make_job("IT_3", job_command("IT_3"), [out["IT_1"]], [out["IT_3"]], queue, cpu, machine),
make_job("IT4", job_command("IT4"), [out["IT_3"], out["IT_2"]], [out["IT4"]], queue, cpu, machine),
make_job("IT_5",job_command("IT_5"),[out["IT_3"], out["IT4"]],[out["IT_5"]],queue,cpu, machine,),
]
return _wrap_jobs_as_stages(jobs)
讀這段時可以對著圖核對:
IT_1、IT_2 的 inputs 是空的 → 兩個 RootIT_3 吃 IT_1.done
IT4 同時吃 IT_3.done 與 IT_2.done → 匯流IT_5 吃 IT_3.done 與 IT4.done
使用者在 builder 裡只要想「誰做完輪到誰」;檔案路徑就是那句口令的實體化。這樣就完成一種可載入的範本了。
積木比範本更單純。準備好「單一 Job」的 JSON,放到 generator/node/ 即可。
我們直接用剛剛的 ithome。先按 Export flow.json。

打開檔案,把其中一個 Job 區塊複製出來,並拿掉 parents 與 children。積木不該帶上一次 DAG 的記憶,否則 Add Job 時會把舊邊一起貼進來。
以 IT_1 為例:把 blank_job.json 複製成 IT_1.json,改 flow_name、stage / task / job 名稱,再貼上這份設定:
{
"flow_name": "ithome",
"poll_interval": 20,
"stages": [
{
"name": "IT_1",
"tasks": [
{
"name": "IT_1",
"jobs": [
{
"name": "IT_1",
"command": "./ithome_flow/IT_1.csh",
"queue": "normal",
"cpu": 1,
"inputs": [],
"outputs": [
"ithome_flow/out/IT_1.done"
]
}
]
}
]
}
]
}
重開 GUI,再按一次 Add Job,清單裡就會出現這顆節點。

![]()
example,再 Accel 加一顆 QC Summary@register + import,Template 清單會自己出現新 flowmodels.py
parents / children
generator/node/