AIPodCli 0.1.0__py3-none-any.whl
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.
- ai_pod_cli/__init__.py +7 -0
- ai_pod_cli/__main__.py +5 -0
- ai_pod_cli/cli.py +65 -0
- ai_pod_cli/client.py +124 -0
- ai_pod_cli/commands/__init__.py +1 -0
- ai_pod_cli/commands/add.py +28 -0
- ai_pod_cli/commands/build.py +226 -0
- ai_pod_cli/commands/create.py +246 -0
- ai_pod_cli/commands/init.py +233 -0
- ai_pod_cli/config.py +172 -0
- ai_pod_cli/config_store.py +83 -0
- ai_pod_cli/container.py +110 -0
- ai_pod_cli/context.py +45 -0
- ai_pod_cli/entities.py +16 -0
- ai_pod_cli/runner.py +86 -0
- ai_pod_cli/security.py +77 -0
- aipodcli-0.1.0.dist-info/METADATA +364 -0
- aipodcli-0.1.0.dist-info/RECORD +21 -0
- aipodcli-0.1.0.dist-info/WHEEL +5 -0
- aipodcli-0.1.0.dist-info/entry_points.txt +2 -0
- aipodcli-0.1.0.dist-info/top_level.txt +1 -0
ai_pod_cli/__init__.py
ADDED
ai_pod_cli/__main__.py
ADDED
ai_pod_cli/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CLI entry point — argparse setup and command dispatch."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ai_pod_cli.config import init_config_if_not_exists
|
|
7
|
+
from ai_pod_cli.commands.add import handle_add
|
|
8
|
+
from ai_pod_cli.commands.build import handle_build
|
|
9
|
+
from ai_pod_cli.commands.create import handle_create
|
|
10
|
+
from ai_pod_cli.commands.init import handle_init
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Main CLI entry point for the AI Pod engine."""
|
|
15
|
+
# 从 .env 文件加载环境变量(优先于系统环境变量)
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
load_dotenv()
|
|
18
|
+
|
|
19
|
+
# Windows 终端 GBK 兼容:强制 UTF-8 输出
|
|
20
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
22
|
+
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
|
|
23
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
24
|
+
|
|
25
|
+
# init 命令自行管理初始化,其余命令走自动初始化
|
|
26
|
+
if len(sys.argv) > 1 and sys.argv[1] != "init":
|
|
27
|
+
init_config_if_not_exists()
|
|
28
|
+
|
|
29
|
+
parser = argparse.ArgumentParser(description="AI 原生 IoC 容器低代码引擎 CLI")
|
|
30
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
31
|
+
|
|
32
|
+
# 1. init 命令定义
|
|
33
|
+
init_parser = subparsers.add_parser("init", help="初始化项目:创建目录、配置,可选 AI 生成入口文件")
|
|
34
|
+
init_parser.add_argument("desc", nargs="?", default="", help="项目描述(AI 据此判断技术栈并生成入口文件)")
|
|
35
|
+
init_parser.add_argument("--install-deps", action="store_true", help="自动安装 Python 依赖")
|
|
36
|
+
|
|
37
|
+
# 2. create 命令定义
|
|
38
|
+
create_parser = subparsers.add_parser("create", help="让 AI 创造一个组件并记录配置")
|
|
39
|
+
create_parser.add_argument("--category", choices=["entry", "entity"], required=True, help="组件分类")
|
|
40
|
+
create_parser.add_argument("--name", required=True, help="组件名称")
|
|
41
|
+
create_parser.add_argument("--desc", required=True, help="给AI传入的需求和描述")
|
|
42
|
+
|
|
43
|
+
# 3. add 命令定义
|
|
44
|
+
add_parser = subparsers.add_parser("add", help="注册一个人工编写的实体组件到配置中心")
|
|
45
|
+
add_parser.add_argument("--category", choices=["entry", "entity"], default="entity", help="组件分类(默认 entity)")
|
|
46
|
+
add_parser.add_argument("--name", required=True, help="组件名称(必须与类名一致)")
|
|
47
|
+
add_parser.add_argument("--class-path", required=True, help="完整的类路径,如 mypackage.mymodule.MyClass")
|
|
48
|
+
add_parser.add_argument("--desc", required=True, help="组件功能描述")
|
|
49
|
+
|
|
50
|
+
# 4. build 命令定义
|
|
51
|
+
build_parser = subparsers.add_parser("build", help="AI 生成 pipeline 文件并注册到 routes.toml(只生成不执行)")
|
|
52
|
+
build_parser.add_argument("cmd", nargs="?", default="", help="自然语言业务指令(AI 据此规划执行链)")
|
|
53
|
+
build_parser.add_argument("--name", default="", help="保存 pipeline 的文件名(默认从指令自动生成)")
|
|
54
|
+
build_parser.add_argument("--list", action="store_true", help="列出所有已保存的 pipeline")
|
|
55
|
+
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
|
|
58
|
+
if args.command == "init":
|
|
59
|
+
handle_init(args)
|
|
60
|
+
elif args.command == "create":
|
|
61
|
+
handle_create(args)
|
|
62
|
+
elif args.command == "add":
|
|
63
|
+
handle_add(args)
|
|
64
|
+
elif args.command == "build":
|
|
65
|
+
handle_build(args)
|
ai_pod_cli/client.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""OpenAI client initialization with lazy loading and retry logic."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_client: OpenAI | None = None
|
|
11
|
+
_model: str | None = None
|
|
12
|
+
|
|
13
|
+
# 默认重试配置
|
|
14
|
+
DEFAULT_MAX_RETRIES = 3
|
|
15
|
+
DEFAULT_RETRY_DELAY = 2 # 秒
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_client() -> OpenAI:
|
|
19
|
+
"""Get or create the OpenAI client instance (lazy singleton).
|
|
20
|
+
|
|
21
|
+
Reads configuration from environment variables:
|
|
22
|
+
- OPENAI_API_KEY: API key (required)
|
|
23
|
+
- OPENAI_BASE_URL: API base URL (defaults to https://api.openai.com/v1)
|
|
24
|
+
"""
|
|
25
|
+
global _client
|
|
26
|
+
if _client is None:
|
|
27
|
+
_client = OpenAI(
|
|
28
|
+
api_key=os.environ.get("OPENAI_API_KEY"),
|
|
29
|
+
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
|
|
30
|
+
)
|
|
31
|
+
return _client
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_model() -> str:
|
|
35
|
+
"""Get the model name from environment (defaults to deepseek-chat)."""
|
|
36
|
+
global _model
|
|
37
|
+
if _model is None:
|
|
38
|
+
_model = os.environ.get("OPENAI_MODEL", "deepseek-chat")
|
|
39
|
+
return _model
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def call_llm(
|
|
43
|
+
system_prompt: str,
|
|
44
|
+
user_content: str,
|
|
45
|
+
*,
|
|
46
|
+
json_mode: bool = False,
|
|
47
|
+
temperature: float = 0.1,
|
|
48
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
49
|
+
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
50
|
+
) -> dict | str:
|
|
51
|
+
"""Call the LLM with retry on network errors or invalid JSON.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
system_prompt: System prompt for the model.
|
|
55
|
+
user_content: User message content.
|
|
56
|
+
json_mode: If True, forces JSON output and parses the result into a dict.
|
|
57
|
+
temperature: Sampling temperature.
|
|
58
|
+
max_retries: Maximum number of retry attempts (default 3).
|
|
59
|
+
retry_delay: Base delay in seconds between retries (doubles each attempt).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Parsed dict if json_mode=True, otherwise raw string content.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
RuntimeError: If all retries are exhausted.
|
|
66
|
+
"""
|
|
67
|
+
client = get_client()
|
|
68
|
+
model = get_model()
|
|
69
|
+
|
|
70
|
+
kwargs = {
|
|
71
|
+
"model": model,
|
|
72
|
+
"messages": [
|
|
73
|
+
{"role": "system", "content": system_prompt},
|
|
74
|
+
{"role": "user", "content": user_content},
|
|
75
|
+
],
|
|
76
|
+
"temperature": temperature,
|
|
77
|
+
}
|
|
78
|
+
if json_mode:
|
|
79
|
+
kwargs["response_format"] = {"type": "json_object"}
|
|
80
|
+
|
|
81
|
+
last_error: Exception | None = None
|
|
82
|
+
delay = retry_delay
|
|
83
|
+
|
|
84
|
+
for attempt in range(1, max_retries + 1):
|
|
85
|
+
try:
|
|
86
|
+
if attempt > 1:
|
|
87
|
+
print(f" [retry] 第 {attempt}/{max_retries} 次重试...")
|
|
88
|
+
|
|
89
|
+
response = client.chat.completions.create(**kwargs)
|
|
90
|
+
raw_content = response.choices[0].message.content
|
|
91
|
+
|
|
92
|
+
if json_mode:
|
|
93
|
+
# 尝试解析 JSON
|
|
94
|
+
try:
|
|
95
|
+
result = json.loads(raw_content)
|
|
96
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
97
|
+
last_error = ValueError(f"AI 返回的内容不是合法 JSON: {e}\n原始内容: {raw_content[:300]}")
|
|
98
|
+
print(f" [retry] 第 {attempt} 次: JSON 解析失败")
|
|
99
|
+
if attempt < max_retries:
|
|
100
|
+
time.sleep(delay)
|
|
101
|
+
delay *= 2
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# 检查 code 字段是否为空(仅 create/start 场景)
|
|
105
|
+
if "code" in result and not result.get("code", "").strip():
|
|
106
|
+
last_error = ValueError("AI 返回的 code 字段为空")
|
|
107
|
+
print(f" [retry] 第 {attempt} 次: code 字段为空")
|
|
108
|
+
if attempt < max_retries:
|
|
109
|
+
time.sleep(delay)
|
|
110
|
+
delay *= 2
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
return raw_content
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
last_error = e
|
|
119
|
+
print(f" [retry] 第 {attempt} 次: API 调用失败 ({type(e).__name__})")
|
|
120
|
+
if attempt < max_retries:
|
|
121
|
+
time.sleep(delay)
|
|
122
|
+
delay *= 2
|
|
123
|
+
|
|
124
|
+
raise RuntimeError(f"LLM 调用在 {max_retries} 次重试后仍然失败: {last_error}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""`add` command — register a human-written entity into the bean config."""
|
|
2
|
+
|
|
3
|
+
from ai_pod_cli.config import load_config, save_config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_add(args):
|
|
7
|
+
"""【add 命令】将人工编写的实体组件注册到配置中心"""
|
|
8
|
+
print(f"📦 [CLI] 正在注册人工实体: '{args.name}'...")
|
|
9
|
+
|
|
10
|
+
config = load_config()
|
|
11
|
+
|
|
12
|
+
# 检查是否已存在同名组件,存在则覆盖
|
|
13
|
+
config["beans"] = [b for b in config["beans"] if b["id"] != args.name]
|
|
14
|
+
|
|
15
|
+
new_bean = {
|
|
16
|
+
"id": args.name,
|
|
17
|
+
"category": args.category,
|
|
18
|
+
"type": "human_added",
|
|
19
|
+
"class_path": args.class_path,
|
|
20
|
+
"description": args.desc,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
config["beans"].append(new_bean)
|
|
24
|
+
save_config(config)
|
|
25
|
+
|
|
26
|
+
print(f"💾 [注册成功] 实体 '{args.name}' 已写入配置中心 (class_path: {args.class_path})")
|
|
27
|
+
print(f" 分类: {args.category}")
|
|
28
|
+
print(f" 描述: {args.desc}")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""`build` command — AI generates pipeline file and registers it in routes.toml."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ai_pod_cli.client import call_llm
|
|
9
|
+
from ai_pod_cli.config import (
|
|
10
|
+
load_config, load_config_toml_keys, PIPELINES_DIR, register_route,
|
|
11
|
+
)
|
|
12
|
+
from ai_pod_cli.security import validate_code
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _slugify(text: str) -> str:
|
|
16
|
+
"""将中文或任意文本转为安全的文件名。"""
|
|
17
|
+
safe = re.sub(r'[^\w一-鿿]+', '_', text.strip())
|
|
18
|
+
return safe[:60] or "pipeline"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _save_pipeline(code: str, name: str, instruction: str) -> str:
|
|
22
|
+
"""将 AI 生成的 pipeline Python 代码保存到 pipelines/ 目录。"""
|
|
23
|
+
os.makedirs(PIPELINES_DIR, exist_ok=True)
|
|
24
|
+
filename = f"{name}.py"
|
|
25
|
+
filepath = os.path.join(PIPELINES_DIR, filename)
|
|
26
|
+
|
|
27
|
+
header = (
|
|
28
|
+
f'"""Pipeline: {instruction}\n'
|
|
29
|
+
f'Generated: {datetime.now().isoformat()}\n'
|
|
30
|
+
f'"""'
|
|
31
|
+
)
|
|
32
|
+
full_code = header + "\n\n" + code
|
|
33
|
+
|
|
34
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
35
|
+
f.write(full_code)
|
|
36
|
+
|
|
37
|
+
return filepath
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _list_pipelines() -> list[dict]:
|
|
41
|
+
"""列出所有已保存的 pipeline (.py 文件)。"""
|
|
42
|
+
if not os.path.exists(PIPELINES_DIR):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
pipelines = []
|
|
46
|
+
for f in sorted(os.listdir(PIPELINES_DIR)):
|
|
47
|
+
if f == "__init__.py":
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
filepath = os.path.join(PIPELINES_DIR, f)
|
|
51
|
+
|
|
52
|
+
if f.endswith(".py"):
|
|
53
|
+
instruction = ""
|
|
54
|
+
try:
|
|
55
|
+
with open(filepath, "r", encoding="utf-8") as fh:
|
|
56
|
+
first_lines = "".join(fh.readline() for _ in range(3))
|
|
57
|
+
match = re.search(r'Pipeline:\s*(.+)', first_lines)
|
|
58
|
+
if match:
|
|
59
|
+
instruction = match.group(1).strip()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
pipelines.append({
|
|
64
|
+
"file": f,
|
|
65
|
+
"name": f.replace(".py", ""),
|
|
66
|
+
"instruction": instruction,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return pipelines
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def handle_build(args):
|
|
73
|
+
"""【build 命令】AI 编排器:生成 pipeline 文件 → 注册到 routes.toml"""
|
|
74
|
+
|
|
75
|
+
# --- --list: 列出所有已保存的 pipeline ---
|
|
76
|
+
if args.list:
|
|
77
|
+
pipelines = _list_pipelines()
|
|
78
|
+
if not pipelines:
|
|
79
|
+
print(f"📂 {PIPELINES_DIR}/ 目录为空,还没有保存任何 pipeline。")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
print(f"📂 已保存的 Pipeline ({len(pipelines)} 条):\n")
|
|
83
|
+
for p in pipelines:
|
|
84
|
+
print(f" 🐍 {p['file']}")
|
|
85
|
+
print(f" 指令: {p['instruction']}")
|
|
86
|
+
print()
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# --- 默认: AI 生成新的 Python pipeline ---
|
|
90
|
+
if not args.cmd:
|
|
91
|
+
print("❌ 请提供指令描述。")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
print(f"🎬 [build] 人类宏观指令: '{args.cmd}'")
|
|
95
|
+
|
|
96
|
+
config = load_config()
|
|
97
|
+
existing_beans_context = json.dumps(config["beans"], indent=2, ensure_ascii=False)
|
|
98
|
+
toml_keys = load_config_toml_keys()
|
|
99
|
+
|
|
100
|
+
# 收集所有组件的 class_path 用于生成 import
|
|
101
|
+
component_imports = {}
|
|
102
|
+
for bean in config["beans"]:
|
|
103
|
+
cid = bean["id"]
|
|
104
|
+
cpath = bean["class_path"]
|
|
105
|
+
module_path, class_name = cpath.rsplit(".", 1)
|
|
106
|
+
component_imports[cid] = {"module": module_path, "class": class_name}
|
|
107
|
+
|
|
108
|
+
imports_hint = "\n".join(
|
|
109
|
+
f" - {cid}: from {info['module']} import {info['class']}"
|
|
110
|
+
for cid, info in component_imports.items()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
system_prompt = f"""
|
|
114
|
+
你是一个智能编排引擎。当前系统中注册了以下组件账本:
|
|
115
|
+
{existing_beans_context}
|
|
116
|
+
|
|
117
|
+
各组件的 import 路径:
|
|
118
|
+
{imports_hint}
|
|
119
|
+
|
|
120
|
+
当前 config.toml 中可用的配置段和键名:
|
|
121
|
+
{toml_keys}
|
|
122
|
+
组件通过注入 ConfigStore(from ai_pod_cli.config_store import ConfigStore)并用 config_store.get("section.key", default) 读取配置。
|
|
123
|
+
|
|
124
|
+
你的任务是:根据人类的自然语言指令,生成一个完整的 Python pipeline 脚本。
|
|
125
|
+
|
|
126
|
+
【生成的代码规范】:
|
|
127
|
+
1. 必须定义一个 `run(ctx)` 函数作为入口,ctx 是 PipelineContext 类型。
|
|
128
|
+
2. 在 run 函数内部:
|
|
129
|
+
- 从 ai_pod_cli.config import load_config
|
|
130
|
+
- 从 ai_pod_cli.container import build_container, Pod
|
|
131
|
+
- 用 build_container(config) 构建 DI 容器
|
|
132
|
+
- 用 S = Pod(container) 创建管道包装器
|
|
133
|
+
- 用 S(ComponentClass) 获取可管道串联的组件引用
|
|
134
|
+
3. 使用管道符 | 串联组件:
|
|
135
|
+
(S(组件A) | S(组件B)).execute_all(ctx)
|
|
136
|
+
这会自动依次执行各组件并记录轨迹。
|
|
137
|
+
4. 从 ctx.params 读取入口参数,通过 ctx.set() 传递数据。
|
|
138
|
+
5. 需要条件分支时,用 if/else 分别串联不同的管道。
|
|
139
|
+
6. 最后 return ctx.summary()。
|
|
140
|
+
7. 加上清晰的中文注释。
|
|
141
|
+
|
|
142
|
+
【PipelineContext 的 API】:
|
|
143
|
+
- ctx.params: dict — 入口参数
|
|
144
|
+
- ctx.set(key, value): 写入数据池
|
|
145
|
+
- ctx.get(key, default=None): 读取数据池
|
|
146
|
+
- ctx.record_step(component_id, result): 记录执行步骤
|
|
147
|
+
- ctx.summary(): 返回执行摘要 dict
|
|
148
|
+
|
|
149
|
+
【代码模板示例】:
|
|
150
|
+
```python
|
|
151
|
+
from ai_pod_cli.context import PipelineContext
|
|
152
|
+
from ai_pod_cli.config import load_config
|
|
153
|
+
from ai_pod_cli.container import build_container, Pod
|
|
154
|
+
from modules.stockchecker import StockChecker
|
|
155
|
+
from modules.stocknotifier import StockNotifier
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run(ctx: PipelineContext):
|
|
159
|
+
config = load_config()
|
|
160
|
+
container = build_container(config)
|
|
161
|
+
S = Pod(container)
|
|
162
|
+
|
|
163
|
+
# 步骤 1: 检查库存
|
|
164
|
+
(S(StockChecker)).execute_all(ctx)
|
|
165
|
+
|
|
166
|
+
# 条件分支:库存不足时通知管理员
|
|
167
|
+
if ctx.get("stock", 0) <= 0:
|
|
168
|
+
(S(StockNotifier)).execute_all(ctx)
|
|
169
|
+
|
|
170
|
+
return ctx.summary()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
【多组件串联示例】:
|
|
174
|
+
```python
|
|
175
|
+
# 依次执行 A → B → C(自动记录每步轨迹)
|
|
176
|
+
(S(ComponentA) | S(ComponentB) | S(ComponentC)).execute_all(ctx)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
请严格以标准 JSON 格式返回(不要包含 Markdown 块标记):
|
|
180
|
+
{{
|
|
181
|
+
"pipeline_ids": ["组件ID_1", "组件ID_2"],
|
|
182
|
+
"code": "完整的 Python pipeline 脚本代码(只包含代码,不含 ```python 标记)"
|
|
183
|
+
}}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
result = call_llm(system_prompt, f"指令: {args.cmd}", json_mode=True, temperature=0.1)
|
|
188
|
+
|
|
189
|
+
pipeline_ids = result.get("pipeline_ids", [])
|
|
190
|
+
generated_code = result.get("code", "")
|
|
191
|
+
|
|
192
|
+
print(f"🔗 [AI 编排] 执行链: {' → '.join(pipeline_ids) if pipeline_ids else '(空)'}")
|
|
193
|
+
|
|
194
|
+
if not generated_code:
|
|
195
|
+
print("❌ AI 未返回有效代码。")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"❌ AI 编排失败: {e}")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# 安全检查
|
|
203
|
+
violations = validate_code(generated_code, for_pipeline=True)
|
|
204
|
+
if violations:
|
|
205
|
+
print(f"🛡️ [安全检查失败] Pipeline 代码包含 {len(violations)} 处违规:")
|
|
206
|
+
for v in violations:
|
|
207
|
+
print(f" ❌ {v}")
|
|
208
|
+
print(f"\n Pipeline 未保存。请调整描述重试。")
|
|
209
|
+
return
|
|
210
|
+
print(f"🛡️ [安全检查通过]")
|
|
211
|
+
|
|
212
|
+
# 保存 pipeline 文件
|
|
213
|
+
name = args.name or _slugify(args.cmd)
|
|
214
|
+
filepath = _save_pipeline(generated_code, name, args.cmd)
|
|
215
|
+
print(f"💾 [Pipeline 已保存] {filepath}")
|
|
216
|
+
|
|
217
|
+
# 注册到 routes.toml
|
|
218
|
+
register_route(
|
|
219
|
+
name=name,
|
|
220
|
+
pipeline_path=filepath,
|
|
221
|
+
description=args.cmd,
|
|
222
|
+
)
|
|
223
|
+
print(f"📋 [路由已注册] {name} → {filepath}")
|
|
224
|
+
|
|
225
|
+
print(f"\n🎉 [build 完成] Pipeline 已生成并注册。")
|
|
226
|
+
print(f" 运行方式: 通过你的入口文件调用 PipelineRunner().run(\"{name}\", params)")
|