430 lines
14 KiB
Python
430 lines
14 KiB
Python
# utils/output_writer.py - 代码文件 & JSON 输出工具
|
||
import os
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Dict, List
|
||
|
||
import config
|
||
|
||
|
||
# 各语言文件扩展名映射
|
||
LANGUAGE_EXT_MAP: Dict[str, str] = {
|
||
"python": ".py",
|
||
"javascript": ".js",
|
||
"typescript": ".ts",
|
||
"java": ".java",
|
||
"go": ".go",
|
||
"rust": ".rs",
|
||
"cpp": ".cpp",
|
||
"c": ".c",
|
||
"csharp": ".cs",
|
||
"ruby": ".rb",
|
||
"php": ".php",
|
||
"swift": ".swift",
|
||
"kotlin": ".kt",
|
||
}
|
||
|
||
# 合法的通用类型集合
|
||
VALID_TYPES = {
|
||
"integer", "string", "boolean", "float",
|
||
"list", "dict", "object", "void", "any",
|
||
}
|
||
|
||
# 合法的 inout 值
|
||
VALID_INOUT = {"in", "out", "inout"}
|
||
|
||
|
||
def get_file_extension(language: str) -> str:
|
||
"""
|
||
获取指定语言的文件扩展名
|
||
|
||
Args:
|
||
language: 编程语言名称(小写)
|
||
|
||
Returns:
|
||
文件扩展名(含点号,如 '.py')
|
||
"""
|
||
return LANGUAGE_EXT_MAP.get(language.lower(), ".txt")
|
||
|
||
|
||
def build_project_output_dir(project_name: str) -> str:
|
||
"""
|
||
构建项目输出目录路径
|
||
|
||
Args:
|
||
project_name: 项目名称
|
||
|
||
Returns:
|
||
输出目录路径
|
||
"""
|
||
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in project_name)
|
||
return os.path.join(config.OUTPUT_BASE_DIR, safe_name)
|
||
|
||
|
||
def ensure_project_dir(project_name: str) -> str:
|
||
"""
|
||
确保项目输出目录存在,不存在则创建
|
||
|
||
Args:
|
||
project_name: 项目名称
|
||
|
||
Returns:
|
||
创建好的目录路径
|
||
"""
|
||
output_dir = build_project_output_dir(project_name)
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
init_file = os.path.join(output_dir, "__init__.py")
|
||
if not os.path.exists(init_file):
|
||
Path(init_file).write_text(
|
||
"# Auto-generated project package\n", encoding="utf-8"
|
||
)
|
||
return output_dir
|
||
|
||
|
||
def write_code_file(
|
||
output_dir: str,
|
||
function_name: str,
|
||
language: str,
|
||
content: str,
|
||
) -> str:
|
||
"""
|
||
将代码内容写入指定目录的文件
|
||
|
||
Args:
|
||
output_dir: 输出目录路径
|
||
function_name: 函数名(用于生成文件名)
|
||
language: 编程语言
|
||
content: 代码内容
|
||
|
||
Returns:
|
||
写入的文件完整路径
|
||
"""
|
||
ext = get_file_extension(language)
|
||
file_name = f"{function_name}{ext}"
|
||
file_path = os.path.join(output_dir, file_name)
|
||
Path(file_path).write_text(content, encoding="utf-8")
|
||
return file_path
|
||
|
||
|
||
def write_project_readme(
|
||
output_dir: str,
|
||
project_name: str,
|
||
requirements_summary: str,
|
||
) -> str:
|
||
"""
|
||
在项目目录生成 README.md 文件
|
||
|
||
Args:
|
||
output_dir: 项目输出目录
|
||
project_name: 项目名称
|
||
requirements_summary: 功能需求摘要文本
|
||
|
||
Returns:
|
||
README.md 文件路径
|
||
"""
|
||
readme_content = f"""# {project_name}
|
||
|
||
> Auto-generated by Requirement Analyzer
|
||
|
||
## 功能需求列表
|
||
|
||
{requirements_summary}
|
||
"""
|
||
readme_path = os.path.join(output_dir, "README.md")
|
||
Path(readme_path).write_text(readme_content, encoding="utf-8")
|
||
return readme_path
|
||
|
||
|
||
# ══════════════════════════════════════════════════════
|
||
# 函数签名 JSON 导出
|
||
# ══════════════════════════════════════════════════════
|
||
|
||
def build_signatures_document(
|
||
project_name: str,
|
||
project_description: str,
|
||
signatures: List[dict],
|
||
) -> dict:
|
||
"""
|
||
将函数签名列表包装为带项目信息的顶层文档结构。
|
||
|
||
Args:
|
||
project_name: 项目名称,写入 "project" 字段
|
||
project_description: 项目描述,写入 "description" 字段
|
||
signatures: 函数签名 dict 列表,写入 "functions" 字段
|
||
|
||
Returns:
|
||
顶层文档 dict,结构为::
|
||
|
||
{
|
||
"project": "<project_name>",
|
||
"description": "<project_description>",
|
||
"functions": [ ... ]
|
||
}
|
||
"""
|
||
return {
|
||
"project": project_name,
|
||
"description": project_description or "",
|
||
"functions": signatures,
|
||
}
|
||
|
||
|
||
def patch_signatures_with_url(
|
||
signatures: List[dict],
|
||
func_name_to_url: Dict[str, str],
|
||
) -> List[dict]:
|
||
"""
|
||
将代码文件的路径(URL)回写到对应函数签名的 "url" 字段。
|
||
|
||
遍历签名列表,根据 signature["name"] 在 func_name_to_url 中查找
|
||
对应路径,找到则写入 "url" 字段;未找到则写入空字符串,不抛出异常。
|
||
|
||
"url" 字段插入位置紧跟在 "type" 字段之后,以保持字段顺序的可读性::
|
||
|
||
{
|
||
"name": "create_user",
|
||
"requirement_id": "REQ.01",
|
||
"description": "...",
|
||
"type": "function",
|
||
"url": "/abs/path/to/create_user.py", ← 新增
|
||
"parameters": { ... },
|
||
"return": { ... }
|
||
}
|
||
|
||
Args:
|
||
signatures: 原始签名列表(in-place 修改)
|
||
func_name_to_url: {函数名: 代码文件绝对路径} 映射表,
|
||
由 CodeGenerator.generate_batch() 的进度回调收集
|
||
|
||
Returns:
|
||
修改后的签名列表(与传入的同一对象,方便链式调用)
|
||
"""
|
||
for sig in signatures:
|
||
func_name = sig.get("name", "")
|
||
url = func_name_to_url.get(func_name, "")
|
||
_insert_field_after(sig, after_key="type", new_key="url", new_value=url)
|
||
return signatures
|
||
|
||
|
||
def _insert_field_after(
|
||
d: dict,
|
||
after_key: str,
|
||
new_key: str,
|
||
new_value,
|
||
) -> None:
|
||
"""
|
||
在有序 dict 中将 new_key 插入到 after_key 之后。
|
||
若 after_key 不存在,则追加到末尾。
|
||
若 new_key 已存在,则直接更新其值(不改变位置)。
|
||
|
||
Args:
|
||
d: 目标 dict(Python 3.7+ 保证插入顺序)
|
||
after_key: 参考键名
|
||
new_key: 要插入的键名
|
||
new_value: 要插入的值
|
||
"""
|
||
if new_key in d:
|
||
d[new_key] = new_value
|
||
return
|
||
|
||
items = list(d.items())
|
||
insert_pos = len(items)
|
||
for i, (k, _) in enumerate(items):
|
||
if k == after_key:
|
||
insert_pos = i + 1
|
||
break
|
||
|
||
items.insert(insert_pos, (new_key, new_value))
|
||
d.clear()
|
||
d.update(items)
|
||
|
||
|
||
def write_function_signatures_json(
|
||
output_dir: str,
|
||
signatures: List[dict],
|
||
project_name: str,
|
||
project_description: str,
|
||
file_name: str = "function_signatures.json",
|
||
) -> str:
|
||
"""
|
||
将函数签名列表连同项目信息一起导出为 JSON 文件。
|
||
|
||
输出的 JSON 顶层结构为::
|
||
|
||
{
|
||
"project": "<project_name>",
|
||
"description": "<project_description>",
|
||
"functions": [
|
||
{
|
||
"name": "...",
|
||
"requirement_id": "...",
|
||
"description": "...",
|
||
"type": "function",
|
||
"url": "/abs/path/to/xxx.py",
|
||
"parameters": { ... },
|
||
"return": { ... }
|
||
},
|
||
...
|
||
]
|
||
}
|
||
|
||
Args:
|
||
output_dir: JSON 文件写入目录
|
||
signatures: 函数签名 dict 列表(应已通过
|
||
patch_signatures_with_url() 写入 "url" 字段)
|
||
project_name: 项目名称
|
||
project_description: 项目描述
|
||
file_name: 输出文件名,默认 function_signatures.json
|
||
|
||
Returns:
|
||
写入的 JSON 文件完整路径
|
||
|
||
Raises:
|
||
OSError: 目录不可写
|
||
"""
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
document = build_signatures_document(project_name, project_description, signatures)
|
||
file_path = os.path.join(output_dir, file_name)
|
||
with open(file_path, "w", encoding="utf-8") as f:
|
||
json.dump(document, f, ensure_ascii=False, indent=2)
|
||
return file_path
|
||
|
||
|
||
# ══════════════════════════════════════════════════════
|
||
# 签名结构校验
|
||
# ══════════════════════════════════════════════════════
|
||
|
||
def validate_signature_schema(signature: dict) -> List[str]:
|
||
"""
|
||
校验单个函数签名 dict 是否符合规范。
|
||
|
||
校验范围:
|
||
- 顶层必填字段:name / requirement_id / description / type / parameters
|
||
- 可选字段 "url":若存在则必须为非空字符串
|
||
- parameters:每个参数的 type / inout / required 字段
|
||
- return:type 字段 + on_success / on_failure 子结构
|
||
- void 函数:on_success / on_failure 应为 null
|
||
- 非 void 函数:on_success / on_failure 必须存在,
|
||
且 value(非空)与 description(非空)均需填写
|
||
|
||
Args:
|
||
signature: 单个函数签名 dict
|
||
|
||
Returns:
|
||
错误信息字符串列表,列表为空表示校验通过
|
||
"""
|
||
errors: List[str] = []
|
||
|
||
# ── 顶层必填字段 ──────────────────────────────────
|
||
for key in ("name", "requirement_id", "description", "type", "parameters"):
|
||
if key not in signature:
|
||
errors.append(f"缺少顶层字段: '{key}'")
|
||
|
||
# ── url 字段(可选,存在时校验非空)─────────────────
|
||
if "url" in signature:
|
||
if not isinstance(signature["url"], str):
|
||
errors.append("'url' 字段必须是字符串类型")
|
||
elif signature["url"] == "":
|
||
errors.append("'url' 字段不能为空字符串(代码文件路径未成功回写)")
|
||
|
||
# ── parameters ────────────────────────────────────
|
||
params = signature.get("parameters", {})
|
||
if not isinstance(params, dict):
|
||
errors.append("'parameters' 必须是 dict 类型")
|
||
else:
|
||
for pname, pdef in params.items():
|
||
if not isinstance(pdef, dict):
|
||
errors.append(f"参数 '{pname}' 定义必须是 dict")
|
||
continue
|
||
# type(支持联合类型,如 "string|integer")
|
||
if "type" not in pdef:
|
||
errors.append(f"参数 '{pname}' 缺少 'type' 字段")
|
||
else:
|
||
parts = [p.strip() for p in pdef["type"].split("|")]
|
||
if not all(p in VALID_TYPES for p in parts):
|
||
errors.append(
|
||
f"参数 '{pname}' 的 type='{pdef['type']}' 含有不合法的类型"
|
||
)
|
||
# inout
|
||
if "inout" not in pdef:
|
||
errors.append(f"参数 '{pname}' 缺少 'inout' 字段")
|
||
elif pdef["inout"] not in VALID_INOUT:
|
||
errors.append(
|
||
f"参数 '{pname}' 的 inout='{pdef['inout']}' 应为 in/out/inout"
|
||
)
|
||
# required
|
||
if "required" not in pdef:
|
||
errors.append(f"参数 '{pname}' 缺少 'required' 字段")
|
||
elif not isinstance(pdef["required"], bool):
|
||
errors.append(
|
||
f"参数 '{pname}' 的 'required' 应为布尔值 true/false,"
|
||
f"当前为: {pdef['required']!r}"
|
||
)
|
||
|
||
# ── return ────────────────────────────────────────
|
||
ret = signature.get("return")
|
||
if ret is None:
|
||
errors.append(
|
||
"缺少 'return' 字段(void 函数请填 "
|
||
"{\"type\": \"void\", \"on_success\": null, \"on_failure\": null})"
|
||
)
|
||
elif not isinstance(ret, dict):
|
||
errors.append("'return' 必须是 dict 类型")
|
||
else:
|
||
ret_type = ret.get("type")
|
||
if not ret_type:
|
||
errors.append("'return' 缺少 'type' 字段")
|
||
elif ret_type not in VALID_TYPES:
|
||
errors.append(f"'return.type'='{ret_type}' 不在合法类型列表中")
|
||
|
||
is_void = (ret_type == "void")
|
||
|
||
for sub_key in ("on_success", "on_failure"):
|
||
sub = ret.get(sub_key)
|
||
if is_void:
|
||
if sub is not None:
|
||
errors.append(
|
||
f"void 函数的 'return.{sub_key}' 应为 null,"
|
||
f"当前为: {sub!r}"
|
||
)
|
||
else:
|
||
if sub is None:
|
||
errors.append(
|
||
f"非 void 函数缺少 'return.{sub_key}',"
|
||
f"请描述{'成功' if sub_key == 'on_success' else '失败'}时的返回值"
|
||
)
|
||
elif not isinstance(sub, dict):
|
||
errors.append(f"'return.{sub_key}' 必须是 dict 类型")
|
||
else:
|
||
if "value" not in sub:
|
||
errors.append(f"'return.{sub_key}' 缺少 'value' 字段")
|
||
elif sub["value"] == "":
|
||
errors.append(
|
||
f"'return.{sub_key}.value' 不能为空字符串,"
|
||
f"请填写具体返回值、值域描述或结构示例"
|
||
)
|
||
if "description" not in sub or sub.get("description") in (None, ""):
|
||
errors.append(f"'return.{sub_key}.description' 不能为空")
|
||
|
||
return errors
|
||
|
||
|
||
def validate_all_signatures(signatures: List[dict]) -> Dict[str, List[str]]:
|
||
"""
|
||
批量校验函数签名列表。
|
||
|
||
注意:此函数接受的是纯签名列表(即顶层文档的 "functions" 字段),
|
||
而非包含 project/description 的顶层文档。
|
||
|
||
Args:
|
||
signatures: 函数签名 dict 列表
|
||
|
||
Returns:
|
||
{函数名: [错误信息, ...]} 字典,仅包含有错误的条目
|
||
"""
|
||
report: Dict[str, List[str]] = {}
|
||
for sig in signatures:
|
||
name = sig.get("name", f"unknown_{id(sig)}")
|
||
errs = validate_signature_schema(sig)
|
||
if errs:
|
||
report[name] = errs
|
||
return report |