Skip to content

Commit 6441ef9

Browse files
committed
[fit] 完善逻辑、增加类型校验
1 parent b84473e commit 6441ef9

File tree

5 files changed

+155
-46
lines changed

5 files changed

+155
-46
lines changed
Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
import json
22
import shutil
3+
import uuid
4+
import tarfile
5+
from datetime import datetime
36
from pathlib import Path
4-
from fit_cli.utils.build import calculate_checksum, parse_python_file
7+
from fit_cli.utils.build import calculate_checksum, parse_python_file, type_errors
58

69
def generate_tools_json(base_dir: Path, plugin_name: str):
710
"""生成 tools.json"""
8-
src_dir = base_dir / plugin_name / "src"
11+
global type_errors
12+
type_errors.clear()
13+
14+
src_dir = base_dir / "src"
915
if not src_dir.exists():
1016
print(f"❌ 未找到插件目录 {src_dir}")
1117
return None
18+
19+
build_dir = base_dir / "build"
20+
if not build_dir.exists():
21+
build_dir.mkdir(exist_ok=True)
1222

1323
tools_json = {
1424
"version": "1.0.0",
@@ -27,42 +37,67 @@ def generate_tools_json(base_dir: Path, plugin_name: str):
2737
if len(tool_groups) > 0:
2838
tools_json["toolGroups"].extend(tool_groups)
2939

30-
path = base_dir / "tools.json"
40+
if type_errors:
41+
print("❌ tools.json 类型校验失败:")
42+
for err in set(type_errors):
43+
print(f" - {err}")
44+
print("请修改为支持的类型:int, float, str, bool, dict, list, tuple, set, bytes")
45+
return None # 终止构建
46+
47+
path = build_dir / "tools.json"
3148
path.write_text(json.dumps(tools_json, indent=2, ensure_ascii=False), encoding="utf-8")
3249
print(f"✅ 已生成 {path}")
3350
return tools_json
3451

3552

3653
def generate_plugin_json(base_dir: Path, plugin_name: str):
3754
"""生成 plugin.json"""
38-
tar_path = base_dir / f"{plugin_name}.tar"
55+
build_dir = base_dir / "build"
56+
tar_path = build_dir / f"{plugin_name}.tar"
3957
if not tar_path.exists():
4058
print(f"❌ TAR 文件 {tar_path} 不存在,请先打包源代码")
4159
return None
42-
# 计算 TAR 文件的 SHA256
60+
4361
checksum = calculate_checksum(tar_path)
62+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
63+
short_uuid = str(uuid.uuid4())[:8]
64+
unique_name = f"{plugin_name}-{timestamp}-{short_uuid}"
65+
4466
plugin_json = {
4567
"checksum": checksum,
4668
"name": plugin_name,
4769
"description": f"{plugin_name} 插件",
4870
"type": "python",
4971
"uniqueness": {
50-
"name": plugin_name
72+
"name": unique_name
5173
}
5274
}
53-
path = base_dir / "plugin.json"
75+
path = build_dir / "plugin.json"
5476
path.write_text(json.dumps(plugin_json, indent=2, ensure_ascii=False), encoding="utf-8")
5577
print(f"✅ 已生成 {path}")
5678
return plugin_json
5779

5880

5981
def make_plugin_tar(base_dir: Path, plugin_name: str):
6082
"""打包源代码为 tar 格式"""
61-
tar_path = base_dir / f"{plugin_name}.tar"
62-
plugin_dir = base_dir / plugin_name
83+
build_dir = base_dir / "build"
84+
if not build_dir.exists():
85+
build_dir.mkdir(exist_ok=True)
86+
87+
tar_path = build_dir / f"{plugin_name}.tar"
88+
plugin_dir = base_dir
6389

64-
shutil.make_archive(str(tar_path.with_suffix("")), "tar", plugin_dir)
65-
print(f"✅ 已生成打包文件 {tar_path}")
90+
with tarfile.open(tar_path, "w") as tar:
91+
# 遍历插件目录下的所有文件
92+
for item in plugin_dir.rglob("*"):
93+
# 排除build目录及其内容
94+
if "build" in item.parts:
95+
continue
96+
97+
if item.is_file():
98+
arcname = Path(plugin_name) / item.relative_to(plugin_dir)
99+
tar.add(item, arcname=arcname)
100+
print(f"✅ 已打包源代码 {tar_path}")
66101

67102

68103
def run(args):
@@ -73,10 +108,8 @@ def run(args):
73108
if not base_dir.exists():
74109
print(f"❌ 插件目录 {base_dir} 不存在,请先运行 fit_cli init {args.name}")
75110
return
76-
77-
# 打包源代码
78-
make_plugin_tar(base_dir, plugin_name)
79-
80-
# 生成 JSON
81-
generate_tools_json(base_dir, plugin_name)
82-
generate_plugin_json(base_dir, plugin_name)
111+
# 生成 tools.json
112+
tools_json = generate_tools_json(base_dir, plugin_name)
113+
if tools_json is not None:
114+
make_plugin_tar(base_dir, plugin_name) # 打包源代码
115+
generate_plugin_json(base_dir, plugin_name) # 生成 plugin.json

framework/fit/python/fit_cli/commands/init_cmd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def hello(name: str) -> str: # 定义可供调用的函数,特别注意需要
1313
1414
修改函数名和参数
1515
- 函数名(hello)应根据功能调整,例如 concat, multiply
16-
- 参数(name: str)可以增加多个,类型也可以是 int, float 等
16+
- 参数(name: str)可以增加多个,类型支持 int, float, str, bool, dict, list, tuple, set, bytes, Union
1717
"""
1818
1919
return f"Hello, {name}!" # 提供函数实现逻辑
@@ -39,7 +39,7 @@ def create_file(path: Path, content: str = "", overwrite: bool = False):
3939
def generate_plugin_structure(plugin_name: str):
4040
"""生成插件目录和文件结构"""
4141
base_dir = Path("plugin") / plugin_name
42-
src_dir = base_dir / plugin_name / "src"
42+
src_dir = base_dir / "src"
4343

4444
# 创建目录
4545
create_directory(base_dir)

framework/fit/python/fit_cli/commands/package_cmd.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
def package_to_zip(plugin_name: str):
55
"""将 build 生成的文件打包为 zip"""
66
base_dir = Path("plugin") / plugin_name
7+
build_dir = base_dir / "build"
78

89
# 待打包的文件列表
910
files_to_zip = [
10-
base_dir / f"{plugin_name}.tar",
11-
base_dir / "tools.json",
12-
base_dir / "plugin.json"
11+
build_dir / f"{plugin_name}.tar",
12+
build_dir / "tools.json",
13+
build_dir / "plugin.json"
1314
]
1415

1516
# 检查文件是否存在
@@ -19,7 +20,7 @@ def package_to_zip(plugin_name: str):
1920
return None
2021

2122
# 打包文件
22-
zip_path = base_dir.parent / f"{plugin_name}.zip"
23+
zip_path = build_dir / f"{plugin_name}_package.zip"
2324
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
2425
for file in files_to_zip:
2526
zipf.write(file, arcname=file.name)
@@ -32,9 +33,13 @@ def run(args):
3233
"""package 命令入口"""
3334
plugin_name = args.name
3435
base_dir = Path("plugin") / plugin_name
36+
build_dir = base_dir / "build"
3537

3638
if not base_dir.exists():
3739
print(f"❌ 插件目录 {base_dir} 不存在,请先运行 init 和 build 命令")
3840
return
41+
if not build_dir.exists():
42+
print(f"❌ 构建目录 {build_dir} 不存在,请先运行 build 命令")
43+
return
3944

4045
package_to_zip(plugin_name)

framework/fit/python/fit_cli/readme.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,47 @@ FIT CLI 支持 3 个核心子命令:init(初始化)、build(构建)、
1515
```bash
1616
python -m fit_cli init %{your_plugin_name}
1717
```
18-
· 参数:%{your_plugin_name} - 自定义插件名称
18+
· 参数:``%{your_plugin_name}``: 自定义插件名称
1919

20-
会在 plugin 目录中创建 %{your_plugin_name} 目录,包含源代码目录、示例插件函数等。
20+
会在 ``plugin`` 目录中创建如下结构:
21+
22+
└── plugin/
23+
└──%{your_plugin_name}/
24+
└── src/
25+
├── __init__.py
26+
└── plugin.py # 插件源码模板
2127

2228
### build
2329

2430
在完成插件的开发后,执行
2531
```bash
2632
python -m fit_cli build %{your_plugin_name}
2733
```
28-
· 参数:%{your_plugin_name} - 自定义插件名称
34+
· 参数:``%{your_plugin_name}``: 插件目录名称
35+
36+
``%{your_plugin_name}`` 目录生成:
2937

30-
解析插件源代码,在 plugin 目录中生成 %{your_plugin_name}.tar 文件,包含插件的所有源代码,并生成 tools.json 和 plugin.json 文件。
38+
└──%{your_plugin_name}/
39+
└── build/
40+
├── %{your_plugin_name}.tar # 插件源码打包文件(工具包)。
41+
├── tools.json # 工具的元数据。
42+
└── plugin.json # 插件的完整性校验与唯一性校验以及插件的基本信息。
3143

32-
开发者可根据自己的需要,修改完善tools.json 和 plugin.json 文件。
44+
开发者可根据自己的需要,修改完善``tools.json````plugin.json`` 文件,比如修改 ``description````uniqueness``等条目
3345

3446
### package
3547

3648
在完成插件的构建后,执行
3749
```bash
3850
python -m fit_cli package %{your_plugin_name}
3951
```
40-
· 参数:%{your_plugin_name} - 自定义插件名称
52+
· 参数:``%{your_plugin_name}``: 插件目录名称
4153

42-
%{your_plugin_name}.tar 文件、tools.json 和 plugin.json 文件打包为 zip 文件。
54+
``plugin/%{your_plugin_name}/build/`` 目录生成最终打包文件: ``%{your_plugin_name}_package.zip``
4355

4456
---
4557

4658
## 注意事项
4759

48-
1. 在运行 init, build 或 package 子命令前,请先切换至 framework/fit/python 项目根目录下
60+
1. 运行命令前,请切换至 framework/fit/python 项目根目录
4961
2. 更多详细信息和使用说明,可参考 https://github.com/ModelEngine-Group/fit-framework 官方仓库。

framework/fit/python/fit_cli/utils/build.py

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,56 @@
33
from pathlib import Path
44

55
TYPE_MAP = {
6+
# 基础类型
67
"int": "integer",
78
"float": "number",
89
"str": "string",
910
"bool": "boolean",
11+
"bytes": "string",
12+
# 容器类型
1013
"dict": "object",
14+
"Dict": "object",
15+
"Union": "object",
1116
"list": "array",
17+
"List": "array",
1218
"tuple": "array",
19+
"Tuple": "array",
1320
"set": "array",
21+
"Set": "array",
22+
# 特殊类型
23+
"None": "null",
1424
}
1525

26+
type_errors = []
27+
1628
def parse_type(annotation):
1729
"""解析参数类型"""
18-
if isinstance(annotation, ast.Name):
19-
return TYPE_MAP.get(annotation.id, "string"), None, True # True=必填
30+
global type_errors
31+
32+
if annotation is None:
33+
type_errors.append("缺少类型注解(必须显式指定参数类型)")
34+
return "invalid", None, True
35+
36+
elif isinstance(annotation, ast.Name):
37+
if annotation.id in TYPE_MAP:
38+
return TYPE_MAP[annotation.id], None, True
39+
else:
40+
type_errors.append(f"不支持的类型: {annotation.id}")
41+
return "invalid", None, True
42+
43+
elif isinstance(annotation, ast.Constant) and annotation.value is None:
44+
return "null", None, False
2045

2146
elif isinstance(annotation, ast.Subscript):
2247
if isinstance(annotation.value, ast.Name):
2348
container = annotation.value.id
2449

25-
# List[int] / list[str]
50+
# List[int]
2651
if container in ("list", "List"):
2752
item_type, _, _ = parse_type(annotation.slice)
53+
if item_type == "invalid":
54+
type_errors.append(f"不支持的列表元素类型: {annotation.slice}")
55+
return "invalid", None, True
2856
return "array", {"type": item_type}, True
2957

3058
# Dict[str, int] → object
@@ -34,13 +62,48 @@ def parse_type(annotation):
3462
# Optional[int]
3563
elif container == "Optional":
3664
inner_type, inner_items, _ = parse_type(annotation.slice)
65+
if inner_type == "invalid":
66+
type_errors.append(f"不支持的Optional类型: {annotation.slice}")
67+
return "invalid", None, False
3768
return inner_type, inner_items, False
38-
39-
# Union[str, int] → 简化为 string
69+
70+
# Union[str, int]
4071
elif container == "Union":
41-
return "string", None, True
42-
43-
return "string", None, True
72+
return "object", None, True
73+
74+
# Tuple[str]
75+
elif container in ("tuple", "Tuple"):
76+
items = []
77+
if isinstance(annotation.slice, ast.Tuple):
78+
for elt in annotation.slice.elts:
79+
item_type, _, _ = parse_type(elt)
80+
if item_type == "invalid":
81+
type_errors.append(f"不支持的元组元素类型: {ast.dump(elt)}")
82+
return "invalid", None, True
83+
items.append({"type":item_type})
84+
return "array", f"{items}", True
85+
else:
86+
item_type, _, _ = parse_type(annotation.slice)
87+
if item_type == "invalid":
88+
type_errors.append(f"不支持的元组元素类型: {ast.dump(annotation.slice)}")
89+
return "invalid", None, True
90+
return "array", {"type":item_type}, True
91+
92+
# Set[int]
93+
elif container in ("set", "Set"):
94+
item_type, _, _ = parse_type(annotation.slice)
95+
if item_type == "invalid":
96+
type_errors.append(f"不支持的集合元素类型: {annotation.slice}")
97+
return "invalid", None, True
98+
return "array", {"type": item_type}, True
99+
100+
101+
else:
102+
type_errors.append(f"不支持的容器类型: {container}")
103+
return "invalid", None, True
104+
105+
type_errors.append(f"无法识别的类型: {ast.dump(annotation)}")
106+
return "invalid", None, True
44107

45108

46109
def parse_parameters(args):
@@ -52,11 +115,7 @@ def parse_parameters(args):
52115
for arg in args.args:
53116
arg_name = arg.arg
54117
order.append(arg_name)
55-
arg_type = "string"
56-
items = None
57-
is_required = True
58-
if arg.annotation:
59-
arg_type, items, is_required = parse_type(arg.annotation)
118+
arg_type, items, is_required = parse_type(arg.annotation)
60119
# 定义参数
61120
prop_def = {
62121
"defaultValue": "",
@@ -160,7 +219,7 @@ def parse_python_file(file_path: Path):
160219
"order": order,
161220
"return": {
162221
"name": "",
163-
"description": description or f"{func_name} 的返回值",
222+
"description": f"{func_name} 函数的返回值",
164223
"type": return_schema["type"],
165224
**({"items": return_schema["items"]} if "items" in return_schema else {}),
166225
"convertor": "",

0 commit comments

Comments
 (0)