720 lines
32 KiB
Python
720 lines
32 KiB
Python
#!/usr/bin/env python3
|
||
"""Generate the Markdown draft for application form information."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import platform
|
||
import re
|
||
import shutil
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from common import ensure_dir, read_json, read_text
|
||
|
||
|
||
MIN_MAIN_FUNCTION_CHARS = 500
|
||
MAX_MAIN_FUNCTION_CHARS = 1300
|
||
|
||
|
||
FIELD_ORDER = [
|
||
"软件全称",
|
||
"软件简称",
|
||
"版本号",
|
||
"软件分类",
|
||
"开发完成日期",
|
||
"开发方式",
|
||
"软件说明",
|
||
"发表状态",
|
||
"首次发表日期",
|
||
"著作权人",
|
||
"权利范围",
|
||
"权利取得方式",
|
||
"开发的硬件环境",
|
||
"运行的硬件环境",
|
||
"开发该软件的操作系统",
|
||
"软件开发环境 / 开发工具",
|
||
"该软件的运行平台 / 操作系统",
|
||
"软件运行支撑环境 / 支持软件",
|
||
"编程语言",
|
||
"源程序量",
|
||
"开发目的",
|
||
"面向领域 / 行业",
|
||
"软件的主要功能",
|
||
"软件的技术特点",
|
||
"页数",
|
||
]
|
||
|
||
|
||
def effective_len(value: str) -> int:
|
||
return len(str(value or "").replace(" ", "").replace("\n", ""))
|
||
|
||
|
||
def clean_text(value: Any) -> str:
|
||
text = str(value or "").strip()
|
||
text = re.sub(r"\s+", " ", text)
|
||
text = text.replace("`", "").replace("#", "").strip()
|
||
return text
|
||
|
||
|
||
def trim_effective(value: str, max_chars: int = MAX_MAIN_FUNCTION_CHARS) -> str:
|
||
if effective_len(value) <= max_chars:
|
||
return value
|
||
result: list[str] = []
|
||
count = 0
|
||
for char in value:
|
||
if char not in (" ", "\n"):
|
||
count += 1
|
||
if count > max_chars:
|
||
break
|
||
result.append(char)
|
||
return "".join(result).rstrip(",。;、 ") + "。"
|
||
|
||
|
||
def business_feature_pairs(business: dict[str, Any] | None) -> list[tuple[str, str]]:
|
||
if not business:
|
||
return []
|
||
features = business.get("business_features") or []
|
||
details = business.get("business_feature_details") or {}
|
||
if not isinstance(features, list):
|
||
return []
|
||
if not isinstance(details, dict):
|
||
details = {}
|
||
pairs: list[tuple[str, str]] = []
|
||
for feature in features:
|
||
name = clean_text(feature)
|
||
if not name:
|
||
continue
|
||
detail = clean_text(details.get(name))
|
||
pairs.append((name, detail))
|
||
return pairs
|
||
|
||
|
||
def summarize_business_features(software_name: str, business: dict[str, Any] | None) -> str:
|
||
pairs = business_feature_pairs(business)
|
||
if not business or not pairs:
|
||
return ""
|
||
|
||
industry = clean_text(business.get("industry"))
|
||
target_users = business.get("target_users") or []
|
||
if isinstance(target_users, list):
|
||
target_text = "、".join(clean_text(item) for item in target_users if clean_text(item))
|
||
else:
|
||
target_text = clean_text(target_users)
|
||
core_value = clean_text(business.get("core_value"))
|
||
product_positioning = clean_text(business.get("product_positioning"))
|
||
operation_flow = business.get("operation_flow") or []
|
||
if isinstance(operation_flow, list):
|
||
flow_steps = [clean_text(item).rstrip("。") for item in operation_flow if clean_text(item)]
|
||
flow_text = ",再".join(flow_steps)
|
||
else:
|
||
flow_text = clean_text(operation_flow)
|
||
|
||
feature_names = "、".join(name for name, _ in pairs[:8])
|
||
parts: list[str] = []
|
||
if product_positioning:
|
||
parts.append(product_positioning.rstrip("。") + "。")
|
||
else:
|
||
scope = f"面向{industry}" if industry else "面向实际业务场景"
|
||
users = f",服务于{target_text}" if target_text else ""
|
||
parts.append(f"{software_name}是一套{scope}{users}的应用软件。")
|
||
parts.append(f"软件主要提供{feature_names}等功能。")
|
||
if core_value:
|
||
parts.append(core_value.rstrip("。") + "。")
|
||
|
||
for name, detail in pairs[:8]:
|
||
if detail:
|
||
detail = detail.rstrip("。")
|
||
if detail.startswith(("用户", "系统")):
|
||
parts.append(f"在{name}功能中,{detail}。")
|
||
else:
|
||
parts.append(f"{name}功能主要{detail}。")
|
||
else:
|
||
parts.append(f"{name}功能支持用户完成相关业务操作,并在处理完成后返回对应结果。")
|
||
|
||
if flow_text:
|
||
parts.append(f"用户通常先{flow_text},系统在关键步骤提供状态反馈和结果展示。")
|
||
|
||
result = "".join(parts)
|
||
while effective_len(result) < MIN_MAIN_FUNCTION_CHARS:
|
||
result += (
|
||
f"围绕{feature_names}等核心功能,软件将用户输入、过程处理、结果查看和资料管理组织在连续的操作流程中,"
|
||
"用户可以根据页面提示逐步完成业务处理,系统保存必要的数据记录并提供清晰的反馈信息,"
|
||
"便于用户后续继续查看、复核和调整相关内容。"
|
||
)
|
||
return trim_effective(result)
|
||
|
||
|
||
def summarize_features(analysis: dict[str, Any], software_name: str, business: dict[str, Any] | None = None) -> str:
|
||
"""Generate a substantive main-function description for the application form.
|
||
|
||
When business context JSON provides main_functions it will be used upstream; this
|
||
function is a fallback that assembles the best available evidence into a multi-
|
||
paragraph description targeting the 500-1300 Chinese-character window required by the
|
||
Chinese copyright office.
|
||
"""
|
||
business_summary = summarize_business_features(software_name, business)
|
||
if business_summary:
|
||
return business_summary
|
||
|
||
features = analysis.get("feature_candidates") or []
|
||
readme = (analysis.get("readme_excerpt") or "").strip()
|
||
routes = analysis.get("routes") or []
|
||
|
||
readable_features = []
|
||
for feature in features:
|
||
name = humanize_feature(str(feature))
|
||
if re.search(r"[A-Za-z]", name):
|
||
continue
|
||
if name and name not in readable_features:
|
||
readable_features.append(name)
|
||
|
||
parts: list[str] = []
|
||
|
||
# Opening overview paragraph
|
||
feature_list = "、".join(readable_features[:12]) if readable_features else "信息展示、业务处理、数据管理和系统交互"
|
||
parts.append(
|
||
f"{software_name}是一套面向用户业务场景的综合软件系统,"
|
||
f"主要提供{feature_list}等核心功能模块。"
|
||
f"系统通过清晰的操作界面和合理的业务流程设计,帮助用户高效完成日常工作和业务协作。"
|
||
)
|
||
|
||
# Module-by-module breakdown
|
||
detail_parts: list[str] = []
|
||
for name in readable_features[:8]:
|
||
skip = {"软件登录", "用户注册", "用户认证", "首页", "数据看板", "系统设置"}
|
||
if name in skip:
|
||
continue
|
||
detail_parts.append(f"{name}模块支持用户进行相关数据的查看、录入和管理操作,提供完整的业务处理能力和结果反馈。")
|
||
|
||
if not detail_parts:
|
||
route_display = [r.strip("/") for r in routes[:6] if r != "/" and not r.startswith("/:")]
|
||
if route_display:
|
||
for route_name in route_display[:6]:
|
||
label = route_name.replace("-", " ").replace("_", " ").title()
|
||
detail_parts.append(f"{label}模块支持用户进行相关数据的查看、录入和管理操作,提供完整的业务处理能力和结果反馈。")
|
||
else:
|
||
detail_parts.append("用户可通过系统界面完成数据查询、信息录入、业务处理和结果导出等操作。")
|
||
detail_parts.append("系统支持多角色用户的协同工作,不同权限用户可访问相应的功能模块。")
|
||
detail_parts.append("系统提供数据持久化存储和历史记录追溯能力,保障业务数据的完整性和可审计性。")
|
||
|
||
# Limit detail parts to avoid exceeding max length
|
||
combined = "".join(detail_parts)
|
||
if len(combined) + len("".join(parts)) > MAX_MAIN_FUNCTION_CHARS:
|
||
while detail_parts and len("".join(detail_parts)) + len("".join(parts)) > MAX_MAIN_FUNCTION_CHARS:
|
||
detail_parts.pop()
|
||
|
||
parts.extend(detail_parts)
|
||
|
||
# Closing paragraph
|
||
if readme:
|
||
first_line = readme.splitlines()[0][:80]
|
||
parts.append(f"系统核心业务围绕{first_line}展开,覆盖从信息采集到结果呈现的完整操作链路。")
|
||
|
||
result = "".join(parts)
|
||
|
||
while effective_len(result) < MIN_MAIN_FUNCTION_CHARS:
|
||
padding = (
|
||
"此外,系统还提供了配套的数据管理、用户操作记录、状态跟踪和系统配置等辅助功能模块,"
|
||
"各个功能模块之间通过统一的界面布局和操作规范协同运行,用户可以在不同模块间灵活切换和处理跨模块的业务流程。"
|
||
"系统整体设计注重业务完整性和操作连续性,能够满足用户日常工作中对信息处理和业务管理的核心需求。"
|
||
"系统界面设计遵循清晰直观的原则,主要操作入口集中展示,用户无需复杂培训即可上手使用。"
|
||
"系统的数据管理能力包括数据的录入、存储、查询、修改和删除等基本操作,同时支持数据批量处理和导入导出功能。"
|
||
"在业务处理方面,系统支持多步骤业务流程的串联执行,各环节之间数据自动流转,减少用户重复录入。"
|
||
"系统还提供了灵活的配置选项,管理员可以根据实际业务需求调整系统参数和功能开关。"
|
||
)
|
||
result += padding
|
||
|
||
return trim_effective(result)
|
||
|
||
|
||
def humanize_feature(name: str) -> str:
|
||
value = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
|
||
value = value.replace("-", " ").replace("_", " ").strip()
|
||
key = value.lower().replace(" ", "")
|
||
mapping = {
|
||
"login": "软件登录",
|
||
"register": "用户注册",
|
||
"auth": "用户认证",
|
||
"home": "首页",
|
||
"dashboard": "数据看板",
|
||
"project": "项目管理",
|
||
"projects": "项目管理",
|
||
"projectsettings": "项目设置",
|
||
"projectssettings": "项目设置",
|
||
"settings": "系统设置",
|
||
"asset": "资源管理",
|
||
"assets": "资源管理",
|
||
"assethub": "资源中心",
|
||
"billing": "费用管理",
|
||
"agentstatusbar": "智能体状态展示",
|
||
"messagebubble": "消息展示",
|
||
"chatpanel": "对话面板",
|
||
"chatinput": "对话输入",
|
||
"assetpanel": "资源面板",
|
||
}
|
||
return mapping.get(key, value.title() if re.search(r"[A-Za-z]", value) else value)
|
||
|
||
|
||
def build_fields(
|
||
analysis: dict[str, Any],
|
||
manifest: dict[str, Any],
|
||
software_name: str,
|
||
version: str,
|
||
answers: dict[str, str],
|
||
business: dict[str, Any] | None = None,
|
||
) -> dict[str, str]:
|
||
frameworks = analysis.get("frameworks") or []
|
||
framework_text = "、".join(frameworks) if frameworks else "前端工程化框架"
|
||
language = analysis.get("language") or "待用户确认"
|
||
project = Path(analysis.get("project_root") or ".")
|
||
hardware_hint = current_hardware_environment()
|
||
dev_os_hint = current_operating_system()
|
||
version_hint = version_confirmation_hint(analysis, version)
|
||
software_name_hint = f"待用户确认(建议:{software_name};请确认最终软件全称)"
|
||
|
||
defaults = {
|
||
"软件全称": software_name_hint,
|
||
"软件简称": "",
|
||
"版本号": version_hint,
|
||
"软件分类": (business.get("software_category") or "应用软件") if business else "应用软件",
|
||
"开发完成日期": "待用户确认(YYYY-MM-DD)",
|
||
"开发方式": (business.get("development_situation") or "单独开发") if business else "单独开发",
|
||
"软件说明": "原创",
|
||
"发表状态": "待用户确认(已发表/未发表)",
|
||
"首次发表日期": "待用户确认(YYYY-MM-DD,未发表则留空)",
|
||
"著作权人": "待用户确认",
|
||
"权利范围": (business.get("rights_scope") or "全部权利") if business else "全部权利",
|
||
"权利取得方式": (business.get("rights_acquisition") or "原始取得") if business else "原始取得",
|
||
"开发的硬件环境": hardware_hint,
|
||
"运行的硬件环境": hardware_hint,
|
||
"开发该软件的操作系统": dev_os_hint,
|
||
"软件开发环境 / 开发工具": f"开发环境: {dev_os_hint}/开发工具: {infer_ide_name(project)}",
|
||
"该软件的运行平台 / 操作系统": infer_runtime_os(analysis),
|
||
"软件运行支撑环境 / 支持软件": infer_runtime_support(analysis, project),
|
||
"编程语言": language,
|
||
"源程序量": str(manifest.get("source_line_count") or manifest.get("selected_source_line_count") or "待用户确认"),
|
||
"开发目的": (business.get("application_purpose") or f"待用户确认(≤50字符,需说明开发目的,不能只写软件名称)") if business else "待用户确认(≤50字符,需说明开发目的,不能只写软件名称)",
|
||
"面向领域 / 行业": (business.get("industry") or "待用户确认") if business else "待用户确认",
|
||
"软件的主要功能": (business.get("main_functions") or summarize_features(analysis, software_name, business)) if business else summarize_features(analysis, software_name, business),
|
||
"软件的技术特点": (business.get("technical_characteristics") or f"系统采用{framework_text}构建前端界面,结合模块化组件、路由组织、接口封装和状态管理实现业务功能") if business else f"系统采用{framework_text}构建前端界面,结合模块化组件、路由组织、接口封装和状态管理实现业务功能",
|
||
"页数": str(manifest.get("total_pages") or "待用户确认"),
|
||
}
|
||
defaults.update({k: v for k, v in answers.items() if v})
|
||
|
||
# 未发表时首次发表日期应为空,不触发待确认门禁
|
||
publish_status = defaults.get("发表状态", "")
|
||
if "未发表" in publish_status and "已发表" not in publish_status:
|
||
if "待用户确认" in defaults.get("首次发表日期", "") or not defaults.get("首次发表日期", "").strip():
|
||
defaults["首次发表日期"] = ""
|
||
|
||
# 软件的主要功能:确保业务理解提供的内容也满足最低字数
|
||
main_func = defaults.get("软件的主要功能", "")
|
||
if main_func and "待用户确认" not in main_func and effective_len(main_func) < MIN_MAIN_FUNCTION_CHARS:
|
||
defaults["软件的主要功能"] = summarize_features(analysis, software_name, business)
|
||
|
||
return defaults
|
||
|
||
|
||
def version_numbers(value: str) -> tuple[int, ...]:
|
||
raw = str(value or "").strip()
|
||
raw = raw.lstrip("vV")
|
||
parts = re.findall(r"\d+", raw)
|
||
return tuple(int(part) for part in parts[:3])
|
||
|
||
|
||
def version_less_than_1(value: str) -> bool:
|
||
numbers = version_numbers(value)
|
||
return bool(numbers) and numbers[0] < 1
|
||
|
||
|
||
def normalize_version_label(value: str) -> str:
|
||
raw = str(value or "").strip()
|
||
if not raw:
|
||
return ""
|
||
return raw if raw.upper().startswith("V") else f"V{raw}"
|
||
|
||
|
||
def project_version_candidate(analysis: dict[str, Any]) -> str:
|
||
value = str((analysis.get("package") or {}).get("version") or "").strip()
|
||
if value and value.upper() != "V1.0":
|
||
return normalize_version_label(value)
|
||
return ""
|
||
|
||
|
||
def version_confirmation_hint(analysis: dict[str, Any], requested_version: str) -> str:
|
||
project_version = project_version_candidate(analysis)
|
||
requested = normalize_version_label(requested_version or "V1.0")
|
||
if project_version and version_less_than_1(project_version):
|
||
return (
|
||
f"待用户确认(项目版本号为 {project_version},软著首次提交通常建议从 V1.0 开始;"
|
||
f"请确认填写 V1.0 还是 {project_version})"
|
||
)
|
||
if not project_version and version_less_than_1(requested):
|
||
return (
|
||
f"待用户确认(当前建议版本号为 {requested},软著首次提交通常建议从 V1.0 开始;"
|
||
f"请确认填写 V1.0 还是 {requested})"
|
||
)
|
||
if project_version and project_version != requested:
|
||
return f"待用户确认(项目版本号为 {project_version},当前建议为 {requested};请确认最终申报版本号)"
|
||
return f"待用户确认(建议:{requested};请确认最终版本号)"
|
||
|
||
|
||
def format_gb(size: int | None) -> str:
|
||
if not size:
|
||
return ""
|
||
return f"{size / (1024 ** 3):.0f}GB"
|
||
|
||
|
||
def total_memory_bytes() -> int | None:
|
||
try:
|
||
return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
|
||
except (AttributeError, OSError, ValueError):
|
||
return None
|
||
|
||
|
||
def current_hardware_environment() -> str:
|
||
parts: list[str] = []
|
||
cpu_count = os.cpu_count()
|
||
machine = platform.machine()
|
||
processor = platform.processor()
|
||
if processor and processor != machine and processor.lower() != "arm":
|
||
parts.append(f"CPU {processor}")
|
||
if cpu_count:
|
||
parts.append(f"CPU {cpu_count}核")
|
||
if machine:
|
||
parts.append(f"架构 {machine}")
|
||
memory = format_gb(total_memory_bytes())
|
||
if memory:
|
||
parts.append(f"内存 {memory}")
|
||
try:
|
||
disk = shutil.disk_usage(Path.home())
|
||
disk_total = format_gb(disk.total)
|
||
if disk_total:
|
||
parts.append(f"硬盘 {disk_total}")
|
||
except OSError:
|
||
pass
|
||
if parts:
|
||
return "、".join(parts)
|
||
return "待用户确认"
|
||
|
||
|
||
def current_operating_system() -> str:
|
||
system = platform.system()
|
||
if system == "Darwin":
|
||
version = platform.mac_ver()[0]
|
||
label = f"macOS {version}" if version else f"macOS(Darwin {platform.release()})"
|
||
elif system == "Windows":
|
||
label = f"Windows {platform.release()}"
|
||
elif system == "Linux":
|
||
label = f"Linux {platform.release()}"
|
||
else:
|
||
label = f"{system} {platform.release()}".strip() or "待用户确认"
|
||
return label
|
||
|
||
|
||
def infer_ide_name(project: Path) -> str:
|
||
if (project / ".idea").exists():
|
||
return "WebStorm 或 IntelliJ IDEA"
|
||
if (project / ".vscode").exists():
|
||
return "Visual Studio Code"
|
||
if list(project.glob("*.code-workspace")):
|
||
return "Visual Studio Code"
|
||
return "Visual Studio Code"
|
||
|
||
|
||
def infer_runtime_os(analysis: dict[str, Any]) -> str:
|
||
frameworks = set(analysis.get("frameworks") or [])
|
||
deps = set((analysis.get("package") or {}).get("dependency_names") or [])
|
||
if "Electron" in frameworks or "electron" in deps or "Tauri" in frameworks or "@tauri-apps/api" in deps:
|
||
return "Windows 10/11 或 macOS 13及以上版本"
|
||
if frameworks & {"Vue", "React", "Vite", "Next.js", "Nuxt", "Svelte", "Astro", "Angular"}:
|
||
return "Windows 10/11 或 macOS 13及以上版本"
|
||
return "Windows 10/11 或 macOS 13及以上版本"
|
||
|
||
|
||
def project_file(project: Path, relative: str) -> Path | None:
|
||
if not relative:
|
||
return None
|
||
path = project / relative
|
||
return path if path.exists() else None
|
||
|
||
|
||
def load_project_package(project: Path, analysis: dict[str, Any]) -> dict[str, Any]:
|
||
package_path = project_file(project, (analysis.get("package") or {}).get("path") or "")
|
||
if package_path:
|
||
try:
|
||
return read_json(package_path)
|
||
except Exception:
|
||
return {}
|
||
return {}
|
||
|
||
|
||
def read_readme(project: Path) -> str:
|
||
for name in ("README.md", "README.zh.md", "readme.md", "Readme.md"):
|
||
path = project / name
|
||
if path.exists():
|
||
try:
|
||
return read_text(path, limit=12000)
|
||
except Exception:
|
||
return ""
|
||
return ""
|
||
|
||
|
||
def extract_requirement_bullets(text: str) -> list[str]:
|
||
wanted = ("python", "node", "docker", "compose", "postgres", "redis", "chrome", "edge", "safari")
|
||
# Patterns that indicate a feature description rather than a runtime requirement.
|
||
_feature_start = re.compile(r"^(?:[一-鿿]|L\d|P\d|[A-Z]\d\s)")
|
||
bullets: list[str] = []
|
||
for line in text.splitlines():
|
||
match = re.match(r"\s*[-*]\s+(.+)", line)
|
||
if not match:
|
||
continue
|
||
item = match.group(1).strip()
|
||
if any(key in item.lower() for key in wanted) and item not in bullets:
|
||
if len(item) > 80:
|
||
continue
|
||
if _feature_start.match(item) and "(" in item:
|
||
continue
|
||
bullets.append(item)
|
||
return bullets[:8]
|
||
|
||
|
||
def detect_package_manager(project: Path, package_path: str) -> str:
|
||
base = (project / package_path).parent if package_path else project
|
||
checks = [
|
||
("pnpm-lock.yaml", "pnpm"),
|
||
("yarn.lock", "Yarn"),
|
||
("bun.lock", "Bun"),
|
||
("bun.lockb", "Bun"),
|
||
("package-lock.json", "npm"),
|
||
]
|
||
for filename, manager in checks:
|
||
if (base / filename).exists() or (project / filename).exists():
|
||
return manager
|
||
return "npm"
|
||
|
||
|
||
def has_support_term(items: list[str], term: str) -> bool:
|
||
return any(term.lower() in item.lower() for item in items)
|
||
|
||
|
||
def infer_runtime_support(analysis: dict[str, Any], project: Path) -> str:
|
||
"""Infer runtime support environment (≤50 chars plain text)."""
|
||
package_info = load_project_package(project, analysis)
|
||
package_path = (analysis.get("package") or {}).get("path") or ""
|
||
deps = set((analysis.get("package") or {}).get("dependency_names") or [])
|
||
frameworks = set(analysis.get("frameworks") or [])
|
||
|
||
support: list[str] = []
|
||
readme_requirements = extract_requirement_bullets(read_readme(project))
|
||
|
||
if package_info or deps or frameworks & {"Vue", "React", "Vite", "Next.js", "Nuxt", "Svelte", "Astro", "Angular"}:
|
||
if not has_support_term(support, "node"):
|
||
node_engine = str((package_info.get("engines") or {}).get("node") or "").strip()
|
||
support.append(f"Node.js {node_engine}".strip() if node_engine else "Node.js")
|
||
support.append(detect_package_manager(project, package_path))
|
||
support.append("现代浏览器")
|
||
|
||
if ((project / "pyproject.toml").exists() or any(project.glob("*/pyproject.toml"))) and not has_support_term(support, "python"):
|
||
support.append("Python")
|
||
if ((project / "requirements.txt").exists() or list(project.glob("*/requirements*.txt"))) and not has_support_term(support, "python"):
|
||
support.append("Python")
|
||
|
||
if ((project / "docker-compose.yml").exists() or (project / "docker-compose.yaml").exists() or list(project.glob("docker-compose*.yml"))) and not has_support_term(support, "docker"):
|
||
support.append("Docker")
|
||
|
||
compose_text = ""
|
||
for compose in list(project.glob("docker-compose*.yml")) + list(project.glob("docker-compose*.yaml")):
|
||
try:
|
||
compose_text += "\n" + read_text(compose, limit=20000).lower()
|
||
except Exception:
|
||
continue
|
||
if "postgres" in compose_text:
|
||
support.append("PostgreSQL")
|
||
if "redis" in compose_text:
|
||
support.append("Redis")
|
||
|
||
# Also incorporate readme requirements into support software
|
||
for req in readme_requirements:
|
||
if not has_support_term(support, req.split()[0].lower()):
|
||
support.append(req)
|
||
|
||
# Deduplicate
|
||
unique: list[str] = []
|
||
for item in support:
|
||
clean = str(item).strip().rstrip(";;")
|
||
if clean and clean not in unique:
|
||
unique.append(clean)
|
||
|
||
if not unique:
|
||
return "待用户确认"
|
||
|
||
# Enforce ≤50 char limit: trim items if needed
|
||
result = "、".join(unique)
|
||
if len(result) > 50:
|
||
trimmed: list[str] = []
|
||
for item in unique:
|
||
candidate = "、".join(trimmed + [item])
|
||
if len(candidate) <= 50:
|
||
trimmed.append(item)
|
||
else:
|
||
break
|
||
result = "、".join(trimmed) if trimmed else unique[0][:50]
|
||
return result
|
||
|
||
|
||
def write_application_md(path: Path, fields: dict[str, str], analysis: dict[str, Any], manifest: dict[str, Any], business: dict[str, Any] | None = None) -> None:
|
||
# 兜底:未发表时首次发表日期不应输出"待用户确认",直接留空
|
||
publish_status = fields.get("发表状态", "")
|
||
if "未发表" in publish_status and "已发表" not in publish_status:
|
||
if "待用户确认" in (fields.get("首次发表日期") or "") or not (fields.get("首次发表日期") or "").strip():
|
||
fields["首次发表日期"] = ""
|
||
|
||
lines = ["# 申请表信息", ""]
|
||
for field in FIELD_ORDER:
|
||
lines.append(f"➤{field}:{fields.get(field, '待用户确认')}")
|
||
pending = [field for field in FIELD_ORDER if "待用户确认" in (fields.get(field) or "")]
|
||
|
||
# Build warnings for common issues
|
||
warnings: list[str] = []
|
||
soft_name = fields.get("软件全称", "")
|
||
clean_name = str(soft_name).strip()
|
||
raw_name = clean_name
|
||
if "待用户确认" in clean_name and "建议:" in clean_name:
|
||
try:
|
||
raw_name = clean_name.split("建议:")[1].rstrip(")").split(";")[0].strip()
|
||
except (IndexError, ValueError):
|
||
raw_name = clean_name
|
||
for suffix in ["软件", "平台"]:
|
||
if raw_name.endswith(suffix):
|
||
warnings.append(f"软件全称以「{suffix}」结尾,存在被驳回风险。建议考虑去掉「{suffix}」后缀或改用其他命名方式。")
|
||
|
||
main_func = fields.get("软件的主要功能", "")
|
||
if main_func and "待用户确认" not in main_func:
|
||
func_len = len(str(main_func).replace(" ", "").replace("\n", ""))
|
||
if func_len < MIN_MAIN_FUNCTION_CHARS:
|
||
warnings.append(f"软件的主要功能仅有 {func_len} 字,应不少于 {MIN_MAIN_FUNCTION_CHARS} 字。请扩写功能说明。")
|
||
elif func_len > MAX_MAIN_FUNCTION_CHARS:
|
||
warnings.append(f"软件的主要功能共 {func_len} 字,超过建议上限 {MAX_MAIN_FUNCTION_CHARS} 字。请精简。")
|
||
|
||
# Check character limits for fields with ≤50 or ≤100 constraints
|
||
char_limit_fields = {
|
||
"开发的硬件环境": 50,
|
||
"运行的硬件环境": 50,
|
||
"开发该软件的操作系统": 50,
|
||
"软件开发环境 / 开发工具": 50,
|
||
"该软件的运行平台 / 操作系统": 50,
|
||
"软件运行支撑环境 / 支持软件": 50,
|
||
"开发目的": 50,
|
||
"面向领域 / 行业": 50,
|
||
"软件的技术特点": 100,
|
||
}
|
||
for field_name, limit in char_limit_fields.items():
|
||
value = fields.get(field_name, "")
|
||
if value and "待用户确认" not in value and len(value) > limit:
|
||
warnings.append(f"{field_name}共 {len(value)} 字符,超过限制 {limit} 字符。请精简。")
|
||
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## 字段填写口径",
|
||
"",
|
||
"- 软件全称:必须由用户确认;最终正式资料文件名、代码页眉和操作手册中的软件名称均以本字段为准。",
|
||
"- 软件简称:可选;如有常用简称则填写。",
|
||
"- 版本号:必须由用户确认;如果项目版本小于 V1.0,软著首次提交通常建议使用 V1.0,也可按实际项目版本填写,最终以本字段为准。",
|
||
"- 软件分类:应用软件/嵌入式软件/中间件/系统软件/其他。",
|
||
"- 开发完成日期、首次发表日期:必须使用 YYYY-MM-DD 格式。",
|
||
"- 开发方式:单独开发/合作开发/委托开发/下达任务开发。",
|
||
"- 软件说明:原创 / 修改(含翻译软件、合成软件)。",
|
||
"- 发表状态:已发表或未发表;已发表需附首次发表日期。",
|
||
"- 软件开发环境 / 开发工具:≤50字符,格式 `开发环境: <OS>/开发工具: <IDE名称>`。",
|
||
"- 开发该软件的操作系统:≤50字符,填写实际开发电脑的操作系统版本。",
|
||
"- 该软件的运行平台 / 操作系统:≤50字符,填写软件运行所在的操作系统或浏览器环境。",
|
||
"- 软件运行支撑环境 / 支持软件:≤50字符,直接列出运行依赖(如 Node.js、npm、浏览器),不加格式前缀。",
|
||
"- 开发的硬件环境和运行的硬件环境:≤50字符,可使用检测到的电脑配置作为建议值。",
|
||
"- 源程序量:纯数字(不含'行'字),指全部源程序的总行数。",
|
||
"- 开发目的:≤50字符,用一句话说明目的,不能只写软件名称。",
|
||
"- 面向领域 / 行业:≤50字符。",
|
||
"- 软件的主要功能:500~1300字。",
|
||
"- 软件的技术特点:多选标签(APP/游戏软件/教育软件/金融软件/医疗软件/地理信息软件/云计算软件/信息安全软件/大数据软件/人工智能软件/VR软件/5G软件/小程序/物联网软件/智慧城市软件)+ 文本描述≤100字符。",
|
||
"",
|
||
"## 项目分析摘要",
|
||
"",
|
||
f"- 项目目录:{analysis.get('project_root', '')}",
|
||
f"- 框架:{'、'.join(analysis.get('frameworks') or []) or '未识别'}",
|
||
f"- 源码文件数:{analysis.get('source', {}).get('total_file_count', analysis.get('source', {}).get('file_count', 0))}",
|
||
f"- 源程序量(含空行):{analysis.get('source', {}).get('total_line_count', analysis.get('source', {}).get('line_count', 0))}",
|
||
f"- 代码材料页数:{manifest.get('total_pages', 0)}",
|
||
f"- 代码输出模式:{manifest.get('mode', '')}",
|
||
f"- 业务理解:{'已读取 草稿/业务理解.json' if business else '未提供,使用项目分析兜底'}",
|
||
"",
|
||
"## 待确认字段",
|
||
"",
|
||
]
|
||
)
|
||
if warnings:
|
||
lines.append("## 字段提醒")
|
||
lines.append("")
|
||
lines.extend(f"- {w}" for w in warnings)
|
||
lines.append("")
|
||
if pending:
|
||
lines.extend(f"- {field}" for field in pending)
|
||
else:
|
||
lines.append("- 无")
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"```text",
|
||
"STOP_FOR_USER",
|
||
"NEXT_ACTION: 请补全并确认申请表字段;确认后运行 confirm_stage.py --stage application-fields。",
|
||
"```",
|
||
]
|
||
)
|
||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
|
||
|
||
def require_confirmed_business(business: dict[str, Any] | None) -> None:
|
||
if business is None:
|
||
raise SystemExit(
|
||
"STOP_FOR_USER\n"
|
||
"NEXT_ACTION: 申请表信息必须基于已确认的业务理解生成。请先生成并确认 草稿/业务理解.md。"
|
||
)
|
||
if business.get("confirmation_required") and not business.get("user_confirmed"):
|
||
raise SystemExit(
|
||
"STOP_FOR_USER\n"
|
||
"NEXT_ACTION: 业务理解尚未确认。请先确认 草稿/业务理解.md,"
|
||
"再运行 `python3 <SKILL_DIR>/scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage business --note \"<用户确认内容>\"`。"
|
||
)
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--analysis", required=True)
|
||
parser.add_argument("--code-manifest", required=True)
|
||
parser.add_argument("--software-name", required=True)
|
||
parser.add_argument("--version", default="V1.0")
|
||
parser.add_argument("--answers", help="Optional JSON object with confirmed field values")
|
||
parser.add_argument("--business-context", help="Business context JSON generated before material drafting")
|
||
parser.add_argument("--out-dir", default="软件著作权申请资料/草稿")
|
||
args = parser.parse_args()
|
||
|
||
analysis = read_json(Path(args.analysis))
|
||
manifest = read_json(Path(args.code_manifest))
|
||
answers = read_json(Path(args.answers)) if args.answers else {}
|
||
business = read_json(Path(args.business_context)) if args.business_context else None
|
||
require_confirmed_business(business)
|
||
out_dir = ensure_dir(Path(args.out_dir))
|
||
|
||
fields = build_fields(analysis, manifest, args.software_name, args.version, answers, business)
|
||
out_path = out_dir / "申请表信息.md"
|
||
write_application_md(out_path, fields, analysis, manifest, business)
|
||
print(f"OK application draft: {out_path}")
|
||
print("STOP_FOR_USER")
|
||
print("NEXT_ACTION: 请补全并确认申请表字段;确认后运行 confirm_stage.py --stage application-fields。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|