glink-engine 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ pycache/
2
+ *.pyc
3
+ __pycache__/
4
+ .ruff_cache/
5
+ .mypy_cache/
6
+ *.bak
7
+ *.bak2
8
+ *.fresh
9
+ *.log
10
+ forge-audit-*.md
11
+ forge-arbitration.md
12
+ checkpoints/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gary Lin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: glink-engine
3
+ Version: 0.3.0
4
+ Summary: Multi-Agent Workflow Orchestration Engine. One Bus. Zero Friction.
5
+ Author: Gary Lin
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: agent,ai,automation,orchestration,pipeline,workflow
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Glink
13
+
14
+ > **Multi-Agent Workflow Orchestration. One Bus. Zero Friction.**
15
+
16
+ Glink is a lightweight orchestration engine that turns your AI agents into a **collaborative assembly line**. Define a workflow in YAML, and Glink routes each step to the right agent — passing context, handling failures, and logging every heartbeat onto a shared event bus.
17
+
18
+ No database. No message queue. No external dependencies.
19
+
20
+ ---
21
+
22
+ ## Architecture
23
+
24
+ ```
25
+ ┌──────────────────────────────────────────────────────────┐
26
+ │ Glink Daemon │
27
+ │ │
28
+ │ ┌─────────────────────────────────────────────────────┐ │
29
+ │ │ Main Bus │ │
30
+ │ │ Append-only JSONL Timeline │ │
31
+ │ └──────┬──────────┬──────────┬──────────┬─────────────┘ │
32
+ │ │ │ │ │ │
33
+ │ ┌────▼───┐ ┌───▼────┐ ┌───▼────┐ ┌──▼──────┐ │
34
+ │ │Agent-1 │ │Agent-2 │ │Agent-3 │ │... │ │
35
+ │ │:8420 │ │:8431 │ │:8432 │ │ │ │
36
+ │ └────────┘ └────────┘ └────────┘ └─────────┘ │
37
+ │ │ │ │ │ │
38
+ │ └──────────┴──────────┴──────────┘ │
39
+ │ Your AI Fleet │
40
+ └──────────────────────────────────────────────────────────┘
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ # Define your agents in main.py or daemon/core.py
49
+ # Default port mapping (customize freely):
50
+ # agent-1 :8420 (generalist)
51
+ # agent-2 :8431 (backend)
52
+ # agent-3 :8432 (frontend/UI)
53
+ # agent-4 :8434 (data)
54
+ # agent-5 :8435 (testing)
55
+
56
+ # Run a workflow (auto-resumes from last checkpoint)
57
+ python3 glink-daemon.py my-workflow
58
+
59
+ # Force restart from step 1
60
+ python3 glink-daemon.py my-workflow --force
61
+
62
+ # Jump to a specific step
63
+ python3 glink-daemon.py my-workflow --step 4
64
+
65
+ # Serve-only mode (API daemon without running workflow)
66
+ python3 glink-daemon.py --serve
67
+
68
+ # Open dashboard
69
+ open http://127.0.0.1:8426/commander.html
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Features
75
+
76
+ | Feature | Description |
77
+ |:--------|:------------|
78
+ | **YAML Workflows** | Define steps, agents, dependencies, and fallbacks in one file |
79
+ | **Main Bus** | JSONL blackboard — append-only, agent-agnostic, replayable |
80
+ | **Smart Routing** | Primary agent down? Auto-fallback to the next in line |
81
+ | **Checkpoint Resume** | Crash mid-workflow? Restart picks up where it left off |
82
+ | **Dependency Graph** | Steps can `depends_on` each other; Glink handles ordering |
83
+ | **Retry Loop** | Auto-retry failed steps (configurable, default 2×) |
84
+ | **HTTP API + SSE** | Live status, agent health, and event stream on `:8426` |
85
+ | **Self-Healing** | Daemon auto-restarts on crash, PID-based watchdog |
86
+ | **Webhook Alerts** | Push notifications to any HTTP endpoint |
87
+ | **Zero External Deps** | Pure Python 3.10+, standard library. No pip install. |
88
+
89
+ ---
90
+
91
+ ## Define a Workflow
92
+
93
+ ```yaml
94
+ name: my-pipeline
95
+ version: 1.0.0
96
+ description: "A simple 3-step demo"
97
+
98
+ global_context: |
99
+ You are part of a multi-agent orchestration pipeline.
100
+ Build upon the output of previous steps.
101
+
102
+ steps:
103
+ - id: step-1
104
+ executor: agent-1
105
+ title: "Generate content"
106
+ output_file: projects/demo/step1.txt
107
+ task: |
108
+ Create a summary of what makes a good multi-agent workflow.
109
+
110
+ - id: step-2
111
+ executor: agent-2
112
+ title: "Enhance"
113
+ input_file: projects/demo/step1.txt
114
+ output_file: projects/demo/step2.md
115
+ task: |
116
+ Read and enhance the step-1 output. Add code examples.
117
+
118
+ - id: step-3
119
+ executor: agent-3
120
+ title: "Verify"
121
+ input_file: projects/demo/step2.md
122
+ output_file: projects/demo/VERIFIED.md
123
+ task: |
124
+ Verify the enhanced document is complete.
125
+ Append a verification seal if all checks pass.
126
+ ```
127
+
128
+ ---
129
+
130
+ ## API Reference
131
+
132
+ All endpoints served on the configured port (`:8426` by default).
133
+
134
+ | Method | Endpoint | Description |
135
+ |:-------|:---------|:------------|
136
+ | `GET` | `/health` | Liveness check |
137
+ | `GET` | `/status` | Full project status + step-by-step progress |
138
+ | `GET` | `/status/agents` | Which agents are online |
139
+ | `GET` | `/status/events?n=20` | Last N bus events |
140
+ | `GET` | `/intel/step` | Detailed intelligence per step stage |
141
+ | `GET` | `/intel/agents` | Agent-specific metrics |
142
+ | `GET` | `/intel/timeline` | Step timeline visualization data |
143
+ | `GET` | `/events/stream` | SSE real-time event stream |
144
+ | `POST` | `/restart` | Resume from last checkpoint |
145
+ | `POST` | `/restart?force` | Force restart from step 1 |
146
+ | `POST` | `/restart?step=N` | Jump to specific step |
147
+
148
+ ---
149
+
150
+ ## Configuration
151
+
152
+ See `glink-config.yaml`:
153
+
154
+ ```yaml
155
+ project:
156
+ default: hello-world
157
+
158
+ scheduling:
159
+ max_retries: 2
160
+ poll_interval: 3
161
+ poll_max_wait: 180
162
+ max_concurrent_steps: 1
163
+
164
+ reporting:
165
+ channels:
166
+ - type: console
167
+ label: "Glink"
168
+ # - type: webhook
169
+ # url: "https://hooks.example.com/..."
170
+ # label: "Slack"
171
+
172
+ server:
173
+ host: "127.0.0.1"
174
+ port: 8426
175
+
176
+ security:
177
+ startup_timeout: 10
178
+ ```
179
+
180
+ Environment variables:
181
+ - `GLINK_DEFAULT_PROJECT` — override default project name
182
+ - `GLINK_PORT` — override API server port
183
+ - `GLINK_REPORTER` — set to `webhook`, `console`, or `silent`
184
+ - `GLINK_ALERT_WEBHOOK` — webhook URL for alerts
185
+
186
+ ---
187
+
188
+ ## Real-World Usage
189
+
190
+ Glink was used to orchestrate a **10-step game development pipeline** across 5 agents:
191
+
192
+ - Step 1-4: 3D scene, physics, textures, UI
193
+ - Step 5-6: Game systems (save/load, scoring)
194
+ - Step 7-8: Quality verification
195
+ - Result: Single playable HTML file, 97 KB / 2,751 lines
196
+
197
+ All built by agent collaboration — zero lines of human-written code.
198
+
199
+ ---
200
+
201
+ ## Project Structure
202
+
203
+ ```
204
+ glink/
205
+ ├── glink-daemon.py # CLI entry point
206
+ ├── glink-config.yaml # Configuration
207
+ ├── daemon/
208
+ │ ├── core.py # Workflow orchestration engine
209
+ │ ├── api.py # HTTP API server (17 endpoints)
210
+ │ ├── checks.py # PID management & auto-recovery
211
+ │ ├── config.py # Config loader
212
+ │ └── log.py # Reporter initialization
213
+ ├── bus/
214
+ │ ├── main_bus.py # JSONL event bus
215
+ │ └── agent_client.py # Agent HTTP client
216
+ ├── reporter/
217
+ │ └── reporter.py # Notification session (webhook/console)
218
+ ├── dashboard/
219
+ │ ├── commander.html # C2 dashboard (realtime)
220
+ │ └── index.html # Legacy dashboard
221
+ ├── workflows/ # Your YAML workflow definitions
222
+ └── projects/ # Step outputs by project
223
+ ```
224
+
225
+ ---
226
+
227
+ ## License
228
+
229
+ MIT — free for any use, open or commercial.
@@ -0,0 +1,218 @@
1
+ # Glink
2
+
3
+ > **Multi-Agent Workflow Orchestration. One Bus. Zero Friction.**
4
+
5
+ Glink is a lightweight orchestration engine that turns your AI agents into a **collaborative assembly line**. Define a workflow in YAML, and Glink routes each step to the right agent — passing context, handling failures, and logging every heartbeat onto a shared event bus.
6
+
7
+ No database. No message queue. No external dependencies.
8
+
9
+ ---
10
+
11
+ ## Architecture
12
+
13
+ ```
14
+ ┌──────────────────────────────────────────────────────────┐
15
+ │ Glink Daemon │
16
+ │ │
17
+ │ ┌─────────────────────────────────────────────────────┐ │
18
+ │ │ Main Bus │ │
19
+ │ │ Append-only JSONL Timeline │ │
20
+ │ └──────┬──────────┬──────────┬──────────┬─────────────┘ │
21
+ │ │ │ │ │ │
22
+ │ ┌────▼───┐ ┌───▼────┐ ┌───▼────┐ ┌──▼──────┐ │
23
+ │ │Agent-1 │ │Agent-2 │ │Agent-3 │ │... │ │
24
+ │ │:8420 │ │:8431 │ │:8432 │ │ │ │
25
+ │ └────────┘ └────────┘ └────────┘ └─────────┘ │
26
+ │ │ │ │ │ │
27
+ │ └──────────┴──────────┴──────────┘ │
28
+ │ Your AI Fleet │
29
+ └──────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Define your agents in main.py or daemon/core.py
38
+ # Default port mapping (customize freely):
39
+ # agent-1 :8420 (generalist)
40
+ # agent-2 :8431 (backend)
41
+ # agent-3 :8432 (frontend/UI)
42
+ # agent-4 :8434 (data)
43
+ # agent-5 :8435 (testing)
44
+
45
+ # Run a workflow (auto-resumes from last checkpoint)
46
+ python3 glink-daemon.py my-workflow
47
+
48
+ # Force restart from step 1
49
+ python3 glink-daemon.py my-workflow --force
50
+
51
+ # Jump to a specific step
52
+ python3 glink-daemon.py my-workflow --step 4
53
+
54
+ # Serve-only mode (API daemon without running workflow)
55
+ python3 glink-daemon.py --serve
56
+
57
+ # Open dashboard
58
+ open http://127.0.0.1:8426/commander.html
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Features
64
+
65
+ | Feature | Description |
66
+ |:--------|:------------|
67
+ | **YAML Workflows** | Define steps, agents, dependencies, and fallbacks in one file |
68
+ | **Main Bus** | JSONL blackboard — append-only, agent-agnostic, replayable |
69
+ | **Smart Routing** | Primary agent down? Auto-fallback to the next in line |
70
+ | **Checkpoint Resume** | Crash mid-workflow? Restart picks up where it left off |
71
+ | **Dependency Graph** | Steps can `depends_on` each other; Glink handles ordering |
72
+ | **Retry Loop** | Auto-retry failed steps (configurable, default 2×) |
73
+ | **HTTP API + SSE** | Live status, agent health, and event stream on `:8426` |
74
+ | **Self-Healing** | Daemon auto-restarts on crash, PID-based watchdog |
75
+ | **Webhook Alerts** | Push notifications to any HTTP endpoint |
76
+ | **Zero External Deps** | Pure Python 3.10+, standard library. No pip install. |
77
+
78
+ ---
79
+
80
+ ## Define a Workflow
81
+
82
+ ```yaml
83
+ name: my-pipeline
84
+ version: 1.0.0
85
+ description: "A simple 3-step demo"
86
+
87
+ global_context: |
88
+ You are part of a multi-agent orchestration pipeline.
89
+ Build upon the output of previous steps.
90
+
91
+ steps:
92
+ - id: step-1
93
+ executor: agent-1
94
+ title: "Generate content"
95
+ output_file: projects/demo/step1.txt
96
+ task: |
97
+ Create a summary of what makes a good multi-agent workflow.
98
+
99
+ - id: step-2
100
+ executor: agent-2
101
+ title: "Enhance"
102
+ input_file: projects/demo/step1.txt
103
+ output_file: projects/demo/step2.md
104
+ task: |
105
+ Read and enhance the step-1 output. Add code examples.
106
+
107
+ - id: step-3
108
+ executor: agent-3
109
+ title: "Verify"
110
+ input_file: projects/demo/step2.md
111
+ output_file: projects/demo/VERIFIED.md
112
+ task: |
113
+ Verify the enhanced document is complete.
114
+ Append a verification seal if all checks pass.
115
+ ```
116
+
117
+ ---
118
+
119
+ ## API Reference
120
+
121
+ All endpoints served on the configured port (`:8426` by default).
122
+
123
+ | Method | Endpoint | Description |
124
+ |:-------|:---------|:------------|
125
+ | `GET` | `/health` | Liveness check |
126
+ | `GET` | `/status` | Full project status + step-by-step progress |
127
+ | `GET` | `/status/agents` | Which agents are online |
128
+ | `GET` | `/status/events?n=20` | Last N bus events |
129
+ | `GET` | `/intel/step` | Detailed intelligence per step stage |
130
+ | `GET` | `/intel/agents` | Agent-specific metrics |
131
+ | `GET` | `/intel/timeline` | Step timeline visualization data |
132
+ | `GET` | `/events/stream` | SSE real-time event stream |
133
+ | `POST` | `/restart` | Resume from last checkpoint |
134
+ | `POST` | `/restart?force` | Force restart from step 1 |
135
+ | `POST` | `/restart?step=N` | Jump to specific step |
136
+
137
+ ---
138
+
139
+ ## Configuration
140
+
141
+ See `glink-config.yaml`:
142
+
143
+ ```yaml
144
+ project:
145
+ default: hello-world
146
+
147
+ scheduling:
148
+ max_retries: 2
149
+ poll_interval: 3
150
+ poll_max_wait: 180
151
+ max_concurrent_steps: 1
152
+
153
+ reporting:
154
+ channels:
155
+ - type: console
156
+ label: "Glink"
157
+ # - type: webhook
158
+ # url: "https://hooks.example.com/..."
159
+ # label: "Slack"
160
+
161
+ server:
162
+ host: "127.0.0.1"
163
+ port: 8426
164
+
165
+ security:
166
+ startup_timeout: 10
167
+ ```
168
+
169
+ Environment variables:
170
+ - `GLINK_DEFAULT_PROJECT` — override default project name
171
+ - `GLINK_PORT` — override API server port
172
+ - `GLINK_REPORTER` — set to `webhook`, `console`, or `silent`
173
+ - `GLINK_ALERT_WEBHOOK` — webhook URL for alerts
174
+
175
+ ---
176
+
177
+ ## Real-World Usage
178
+
179
+ Glink was used to orchestrate a **10-step game development pipeline** across 5 agents:
180
+
181
+ - Step 1-4: 3D scene, physics, textures, UI
182
+ - Step 5-6: Game systems (save/load, scoring)
183
+ - Step 7-8: Quality verification
184
+ - Result: Single playable HTML file, 97 KB / 2,751 lines
185
+
186
+ All built by agent collaboration — zero lines of human-written code.
187
+
188
+ ---
189
+
190
+ ## Project Structure
191
+
192
+ ```
193
+ glink/
194
+ ├── glink-daemon.py # CLI entry point
195
+ ├── glink-config.yaml # Configuration
196
+ ├── daemon/
197
+ │ ├── core.py # Workflow orchestration engine
198
+ │ ├── api.py # HTTP API server (17 endpoints)
199
+ │ ├── checks.py # PID management & auto-recovery
200
+ │ ├── config.py # Config loader
201
+ │ └── log.py # Reporter initialization
202
+ ├── bus/
203
+ │ ├── main_bus.py # JSONL event bus
204
+ │ └── agent_client.py # Agent HTTP client
205
+ ├── reporter/
206
+ │ └── reporter.py # Notification session (webhook/console)
207
+ ├── dashboard/
208
+ │ ├── commander.html # C2 dashboard (realtime)
209
+ │ └── index.html # Legacy dashboard
210
+ ├── workflows/ # Your YAML workflow definitions
211
+ └── projects/ # Step outputs by project
212
+ ```
213
+
214
+ ---
215
+
216
+ ## License
217
+
218
+ MIT — free for any use, open or commercial.
@@ -0,0 +1,12 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Glink Bus 包 — 共享工具与总线模块"""
3
+
4
+ import re
5
+
6
+ # ── 项目名白名单:仅允许字母、数字、下划线、连字符(防 path traversal)──
7
+ _PROJECT_NAME_RE = re.compile(r"[^\w\-]")
8
+
9
+
10
+ def sanitize_project_name(project_name: str) -> str:
11
+ """过滤项目名,防止 path traversal(仅保留 [\\w\\-])"""
12
+ return _PROJECT_NAME_RE.sub("", project_name or "")
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ agent_client — Glink 共享的 Agent 通讯与工作流加载模块
4
+
5
+ 由 glink.py(一次性调度引擎)和 glink-daemon.py(带断点续跑的守护进程)共享。
6
+
7
+ 导出:
8
+ - AGENT_PORTS: Agent 名称 → HTTP 端口的统一映射
9
+ - call_agent(): HTTP 调用 Agent 的 /ask 接口
10
+ - load_workflow(): 从 workflows/ 或 bus/projects/ 加载 yaml 工作流
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+ import urllib.error
19
+ import urllib.request
20
+ from typing import Any
21
+
22
+ import yaml
23
+
24
+ # ── Agent 端口映射(唯一真源)────────────────────────────
25
+ # 同一端口可有多个别名(如 标准版/扎古、代码臂/Forge/forge)
26
+ AGENT_PORTS: dict[str, int] = {
27
+ "标准版": 8420,
28
+ "扎古": 8420,
29
+ "重锤": 8431,
30
+ "绘墨": 8432,
31
+ "大黄蜂": 8434,
32
+ "Laser": 8435,
33
+ "代码臂": 8436,
34
+ "Forge": 8436,
35
+ "forge": 8436,
36
+ }
37
+
38
+ DEFAULT_AGENT_PORT = 8420
39
+ DEFAULT_TIMEOUT = 600
40
+
41
+ # ── 项目名白名单(防 path traversal,从 bus/__init__.py 导入)──
42
+ from . import sanitize_project_name
43
+
44
+ _sanitize_project_name = sanitize_project_name # 兼容别名
45
+
46
+
47
+ # ── HTTP 调用 Agent ─────────────────────────────────────
48
+ def call_agent(
49
+ agent: str,
50
+ task: str,
51
+ port: int | None = None,
52
+ timeout: int = DEFAULT_TIMEOUT,
53
+ parse_reply: bool = True,
54
+ ) -> dict[str, Any]:
55
+ """HTTP 调用 agent 的 /ask 接口。
56
+
57
+ Args:
58
+ agent: Agent 名称(如 "重锤"、"Forge")
59
+ task: 发送给 Agent 的任务描述
60
+ port: 显式端口;不传则查 AGENT_PORTS
61
+ timeout: 请求超时秒数
62
+ parse_reply: True=尝试解析 JSON 取 reply 字段;False=直接返回原始响应
63
+
64
+ Returns:
65
+ {"status": "ok", "output": "<reply 或原始响应前500字>"}
66
+ {"status": "failed", "error": "<错误描述>"}
67
+ """
68
+ if port is None:
69
+ port = AGENT_PORTS.get(agent, DEFAULT_AGENT_PORT)
70
+
71
+ url = f"http://127.0.0.1:{port}/ask"
72
+ payload = json.dumps({"message": task, "session": True}).encode()
73
+ req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
74
+
75
+ max_response_size = 1 * 1024 * 1024 # 1 MB
76
+ chunk_size = 64 * 1024 # 64 KB
77
+
78
+ try:
79
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
80
+ chunks = []
81
+ total = 0
82
+ while True:
83
+ chunk = resp.read(chunk_size)
84
+ if not chunk:
85
+ break
86
+ total += len(chunk)
87
+ if total > max_response_size:
88
+ max_read = max_response_size - (total - len(chunk))
89
+ if max_read > 0:
90
+ chunks.append(chunk[:max_read])
91
+ while resp.read(chunk_size):
92
+ pass
93
+ body = b"".join(chunks).decode()
94
+ body = body[:max_response_size] + "\n\n[TRUNCATED: Response exceeded 1MB limit]"
95
+ break
96
+ chunks.append(chunk)
97
+ else:
98
+ body = b"".join(chunks).decode()
99
+
100
+ if parse_reply:
101
+ try:
102
+ output = json.loads(body).get("reply", body[:500])
103
+ except json.JSONDecodeError:
104
+ output = body[:500]
105
+ else:
106
+ output = body[:500]
107
+ return {"status": "ok", "output": output}
108
+ except urllib.error.HTTPError as e:
109
+ body = e.read().decode()[:200]
110
+ return {"status": "failed", "error": f"HTTP {e.code}: {body}"}
111
+ except Exception as e:
112
+ return {"status": "failed", "error": str(e)}
113
+
114
+
115
+ # ── 工作流加载 ───────────────────────────────────────────
116
+ def load_workflow(project_name: str, base_dir: str | None = None) -> dict[str, Any]:
117
+ """加载工作流 YAML,先查 workflows/,再查 bus/projects/。
118
+
119
+ Args:
120
+ project_name: 项目名(会被白名单过滤)
121
+ base_dir: Glink 根目录;不传则用本文件所在目录的父级
122
+
123
+ Returns:
124
+ 解析后的工作流字典
125
+
126
+ Raises:
127
+ SystemExit(1): 找不到工作流文件
128
+ """
129
+ if base_dir is None:
130
+ # 本文件位于 <glink>/bus/agent_client.py,父级 = glink 根
131
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
132
+
133
+ workflows_dir = os.path.join(base_dir, "workflows")
134
+ bus_projects_dir = os.path.join(base_dir, "bus", "projects")
135
+
136
+ safe_name = _sanitize_project_name(project_name)
137
+ candidates = [
138
+ os.path.join(workflows_dir, f"{safe_name}.yaml"),
139
+ os.path.join(bus_projects_dir, f"{safe_name}.yaml"),
140
+ ]
141
+
142
+ for path in candidates:
143
+ if os.path.exists(path):
144
+ with open(path, encoding="utf-8") as f:
145
+ return yaml.safe_load(f)
146
+
147
+ print(f"❌ 找不到工作流: {safe_name}", file=sys.stderr)
148
+ sys.exit(1)