# 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": "", "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": "", "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