项目文件夹

文件
2026-07-13 12:29:52 +08:00

432 行
19 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env python3
"""Collect project evidence and write a model-authored business context."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
from common import ensure_dir, iter_project_files, read_json, read_text, rel, write_json
DOC_EXTS = {".md", ".txt", ".rst", ".adoc"}
MAX_DOC_CHARS = 80_000
MAX_DOCS = 40
def normalize_space(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def strip_md(text: str) -> str:
text = re.sub(r"`{3}.*?`{3}", " ", text, flags=re.S)
text = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", text)
text = re.sub(r"[>#*_`|]", " ", text)
return normalize_space(text)
def skip_doc(path: Path, project: Path) -> bool:
r = rel(path, project).lower()
skip_parts = (
"node_modules",
".git/",
"dist/",
"build/",
".next/",
"coverage/",
"软件著作权申请资料",
)
return any(part in r for part in skip_parts)
def extract_headings(text: str, limit: int = 24) -> list[str]:
headings: list[str] = []
for line in text.splitlines():
clean = line.strip()
if clean.startswith("#"):
title = clean.lstrip("#").strip()
if title and title not in headings:
headings.append(title[:120])
if len(headings) >= limit:
break
return headings
def extract_opening(text: str, limit: int = 900) -> str:
clean = strip_md(text)
return clean[:limit].strip()
def collect_documents(project: Path) -> list[dict[str, Any]]:
docs: list[dict[str, Any]] = []
for path in iter_project_files(project, DOC_EXTS):
if skip_doc(path, project):
continue
try:
text = read_text(path, limit=MAX_DOC_CHARS)
except Exception:
continue
if not text.strip():
continue
docs.append(
{
"path": rel(path, project),
"size": path.stat().st_size,
"headings": extract_headings(text),
"opening": extract_opening(text),
}
)
docs.sort(key=lambda item: (item["path"].count("/"), item["path"]))
return docs[:MAX_DOCS]
def collect_code_evidence(analysis: dict[str, Any]) -> dict[str, Any]:
source = analysis.get("source") or {}
categorized = source.get("categorized_files") or {}
return {
"project_name": analysis.get("project_name"),
"software_name_candidate": analysis.get("software_name_candidate"),
"frameworks": analysis.get("frameworks") or [],
"language": analysis.get("language"),
"routes": analysis.get("routes") or [],
"feature_name_candidates": analysis.get("feature_candidates") or [],
"entry_files": categorized.get("entry") or [],
"page_files": categorized.get("page") or [],
"component_files": categorized.get("component") or [],
"api_files": categorized.get("api") or [],
"run_command_candidates": analysis.get("run_command_candidates") or [],
"package": analysis.get("package") or {},
}
def build_evidence(project: Path, analysis: dict[str, Any], software_name: str, web_notes: str) -> dict[str, Any]:
return {
"software_name": software_name,
"project_root": str(project.resolve()),
"instruction": (
"本文件只收集证据,不决定行业、功能或手册结构。"
"请由模型阅读这些证据以及必要的项目源码后,另行编写业务理解模型稿。"
),
"documents": collect_documents(project),
"code_evidence": collect_code_evidence(analysis),
"external_research_notes": web_notes,
}
def write_evidence_md(path: Path, evidence: dict[str, Any]) -> None:
lines = [
"# 业务理解证据",
"",
f"- 软件名称:{evidence['software_name']}",
f"- 项目目录:`{evidence['project_root']}`",
"",
"本文件只列出可供模型研判的项目证据,不代表最终申报口径。",
"模型需要自行判断应阅读哪些文档、抽取哪些功能、采用什么操作手册结构。",
"",
"## 代码与页面证据",
"",
]
code = evidence["code_evidence"]
for key in ("frameworks", "language", "routes", "feature_name_candidates", "entry_files", "page_files", "component_files", "api_files"):
value = code.get(key)
if value:
lines.append(f"- {key}{value}")
lines.extend(["", "## 文档证据", ""])
for doc in evidence["documents"]:
lines.extend(
[
f"### {doc['path']}",
"",
f"- 大小:{doc['size']} bytes",
f"- 标题线索:{';'.join(doc['headings']) if doc['headings'] else '无'}",
"",
doc["opening"],
"",
]
)
if evidence.get("external_research_notes"):
lines.extend(["## 外部调研摘要", "", evidence["external_research_notes"], ""])
path.write_text("\n".join(lines), encoding="utf-8")
def write_model_template(path: Path, evidence: dict[str, Any]) -> None:
template = {
"software_name": evidence["software_name"],
"product_positioning": "",
"industry": "",
"target_users": [],
"core_value": "",
"business_features": [],
"business_feature_details": {},
"operation_flow": [],
"application_purpose": "",
"main_functions": "",
"technical_characteristics": "",
"software_technical_option": "应用软件",
"software_category": "应用软件",
"manual_sections": [
{
"title": "模型自行命名章节",
"intent": "说明该章节为什么适合当前项目。",
"paragraphs": [],
"include_feature_overview": False,
"include_operation_modules": False,
"include_operation_flow": False,
}
],
"manual_modules": [
{
"title": "真实页面或核心流程名称",
"evidence": ["页面、路由、组件、README 或需求文档路径"],
"purpose": "该页面或流程在当前软件中的用途。",
"usage": "用户在什么业务场景下会使用该页面,正在处理什么具体事务。",
"entry": "用户从哪里进入该页面或流程。",
"visible_elements": ["用户实际能看到的输入框、按钮、列表、状态或结果区域"],
"operation_steps": ["按真实页面顺序描述用户动作,不写代码实现。"],
"validation_rules": ["输入限制、必填项、额度、权限、异常提示等规则;没有则留空数组。"],
"feedback": ["操作完成后用户能看到的结果、提示或状态变化。"],
"screenshot": "截图预留说明",
}
],
"system_requirements": [
{"item": "操作系统", "minimum": "按项目实际填写", "recommended": "按项目实际填写"},
{"item": "浏览器或客户端", "minimum": "按项目实际填写", "recommended": "按项目实际填写"},
],
"faq": [
{"question": "按当前软件真实使用场景填写常见问题", "answer": "给出面向普通用户的处理方法。"}
],
"glossary": [
{"term": "当前软件中的业务术语", "definition": "用普通中文解释含义。"}
],
"model_review_notes": [
"操作手册应采用软著审核友好的通用骨架:相关文档、说明、功能特点、系统要求、按真实页面/流程逐章操作、常见问题、术语表。",
"manual_modules 要按当前项目真实页面、导航入口、按钮、输入限制、系统反馈和截图位置编写,不能只写抽象功能名。",
"不要照抄范本文案;范本只说明手册需要具体、可操作、能给审核员看懂。",
"不要用关键词表决定行业和功能;必须能从项目证据或用户补充中解释来源。",
],
}
write_json(path, template)
def load_model_context(path: Path) -> dict[str, Any]:
data = read_json(path)
if not isinstance(data, dict):
raise SystemExit(f"Invalid model context JSON: {path}")
return data
def required_list(value: Any, field: str) -> list[str]:
if not isinstance(value, list):
raise SystemExit(f"Model context field must be a list: {field}")
items = [str(item).strip() for item in value if str(item).strip()]
if not items:
raise SystemExit(f"Model context field cannot be empty: {field}")
return items
def required_text(data: dict[str, Any], field: str) -> str:
value = str(data.get(field) or "").strip()
if not value:
raise SystemExit(f"Model context field cannot be empty: {field}")
return value
def normalize_model_context(model: dict[str, Any], evidence: dict[str, Any], web_notes: str) -> dict[str, Any]:
features = required_list(model.get("business_features"), "business_features")
details = model.get("business_feature_details") or {}
if not isinstance(details, dict):
raise SystemExit("Model context field must be an object: business_feature_details")
missing = [feature for feature in features if not str(details.get(feature) or "").strip()]
if missing:
raise SystemExit("Model context missing feature details: " + "、".join(missing[:12]))
sections = model.get("manual_sections") or []
if sections and not isinstance(sections, list):
raise SystemExit("Model context field must be a list: manual_sections")
manual_modules = model.get("manual_modules") or []
if manual_modules and not isinstance(manual_modules, list):
raise SystemExit("Model context field must be a list: manual_modules")
if not manual_modules:
raise SystemExit("Model context field cannot be empty: manual_modules")
for index, module in enumerate(manual_modules, start=1):
if not isinstance(module, dict):
raise SystemExit(f"manual_modules item {index} must be an object")
title = str(module.get("title") or module.get("feature") or "").strip()
for field in ("purpose", "usage", "entry", "operation_steps", "feedback"):
value = module.get(field)
if field == "usage" and not str(value or "").strip():
value = module.get("usage_scenario")
if isinstance(value, list):
missing_value = not any(str(item).strip() for item in value)
else:
missing_value = not str(value or "").strip()
if missing_value:
raise SystemExit(f"manual_modules item {index} ({title or 'untitled'}) missing field: {field}")
system_requirements = model.get("system_requirements") or []
if system_requirements and not isinstance(system_requirements, list):
raise SystemExit("Model context field must be a list: system_requirements")
if not system_requirements:
raise SystemExit("Model context field cannot be empty: system_requirements")
faq = model.get("faq") or []
if faq and not isinstance(faq, list):
raise SystemExit("Model context field must be a list: faq")
if not faq:
raise SystemExit("Model context field cannot be empty: faq")
glossary = model.get("glossary") or []
if glossary and not isinstance(glossary, list):
raise SystemExit("Model context field must be a list: glossary")
if not glossary:
raise SystemExit("Model context field cannot be empty: glossary")
context = {
"software_name": evidence["software_name"],
"business_understanding_required": True,
"source_documents": [{"path": doc["path"], "size": doc["size"]} for doc in evidence["documents"]],
"project_evidence_file": "业务理解证据.md",
"product_positioning": required_text(model, "product_positioning"),
"industry": required_text(model, "industry"),
"target_users": required_list(model.get("target_users"), "target_users"),
"core_value": required_text(model, "core_value"),
"business_features": features,
"business_feature_details": {feature: str(details.get(feature)).strip() for feature in features},
"operation_flow": required_list(model.get("operation_flow"), "operation_flow"),
"application_purpose": required_text(model, "application_purpose"),
"main_functions": required_text(model, "main_functions"),
"technical_characteristics": required_text(model, "technical_characteristics"),
"software_technical_option": str(model.get("software_technical_option") or "应用软件"),
"software_category": str(model.get("software_category") or "应用软件"),
"manual_sections": sections,
"manual_modules": manual_modules,
"system_requirements": system_requirements,
"faq": faq,
"glossary": glossary,
"model_authored": True,
"external_research_notes": web_notes,
"confirmation_required": True,
"user_confirmed": False,
"confirmation_stage": "business",
"next_action": "请确认 草稿/业务理解.md 中的软件用途、行业、目标用户、核心功能、手册结构和申请口径;确认后运行 confirm_stage.py --stage business。",
"review_notes": [
"请确认模型判断的行业领域、目标用户和主要功能是否符合实际申报口径。",
"请确认操作手册结构是否按真实页面和流程展开,而不是套用抽象功能列表。",
],
}
return context
def write_context_md(path: Path, context: dict[str, Any]) -> None:
lines = [
"# 业务理解",
"",
f"- 软件名称:{context['software_name']}",
f"- 产品定位:{context['product_positioning']}",
f"- 面向领域 / 行业:{context['industry']}",
f"- 核心价值:{context['core_value']}",
f"- 证据文件:`{context['project_evidence_file']}`",
"",
"## 目标用户",
"",
]
lines.extend(f"- {item}" for item in context["target_users"])
lines.extend(["", "## 主要业务功能", ""])
lines.extend(f"- {item}" for item in context["business_features"])
lines.extend(["", "## 功能说明", ""])
for item in context["business_features"]:
lines.append(f"- {item}{context['business_feature_details'].get(item, '')}")
lines.extend(["", "## 典型操作流程", ""])
lines.extend(f"{i}. {item}" for i, item in enumerate(context["operation_flow"], start=1))
if context.get("manual_sections"):
lines.extend(["", "## 操作手册结构建议", ""])
for i, section in enumerate(context["manual_sections"], start=1):
if isinstance(section, dict):
title = section.get("title") or f"章节 {i}"
intent = section.get("intent") or ""
else:
title = str(section)
intent = ""
lines.append(f"{i}. {title}" + (f"{intent}" if intent else ""))
if context.get("manual_modules"):
lines.extend(["", "## 操作手册页面/流程模块", ""])
for i, module in enumerate(context["manual_modules"], start=1):
if not isinstance(module, dict):
lines.append(f"{i}. {module}")
continue
title = module.get("title") or module.get("feature") or f"模块 {i}"
usage = module.get("usage") or module.get("usage_scenario") or ""
entry = module.get("entry") or ""
steps = module.get("operation_steps") or module.get("steps") or []
lines.append(f"{i}. {title}" + (f"{entry}" if entry else ""))
if usage:
lines.append(f" - 使用场景:{usage}")
if steps:
lines.append(f" - 操作要点:{';'.join(str(item) for item in steps[:4])}")
lines.extend(
[
"",
"## 申请表建议口径",
"",
f"- 开发目的:{context['application_purpose']}",
f"- 软件的主要功能:{context['main_functions']}",
f"- 技术特点:{context['technical_characteristics']}",
f"- 软件的技术特点选项:{context['software_technical_option']}",
f"- 软件分类:{context['software_category']}",
"",
"## 证据来源",
"",
]
)
lines.extend(f"- `{item['path']}`" for item in context["source_documents"])
lines.extend(["", "## 待确认", ""])
lines.extend(f"- {item}" for item in context["review_notes"])
lines.extend(
[
"",
"```text",
"STOP_FOR_USER",
f"NEXT_ACTION: {context['next_action']}",
"```",
]
)
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--project", required=True)
parser.add_argument("--analysis", required=True)
parser.add_argument("--software-name", required=True)
parser.add_argument("--out-dir", default="软件著作权申请资料/草稿")
parser.add_argument("--web-notes", help="Optional plain-text notes from external/competitor research")
parser.add_argument("--model-context", help="Model-authored business context JSON")
args = parser.parse_args()
project = Path(args.project)
analysis = read_json(Path(args.analysis))
web_notes = read_text(Path(args.web_notes)) if args.web_notes else ""
out_dir = ensure_dir(Path(args.out_dir))
evidence = build_evidence(project, analysis, args.software_name, web_notes)
write_json(out_dir / "业务理解证据.json", evidence)
write_evidence_md(out_dir / "业务理解证据.md", evidence)
if not args.model_context:
write_model_template(out_dir / "业务理解模型稿模板.json", evidence)
print(f"OK business evidence: {out_dir / '业务理解证据.md'}")
print(f"OK model template: {out_dir / '业务理解模型稿模板.json'}")
print("NEXT_ACTION: 模型需要阅读业务理解证据和项目源码,自行编写业务理解模型稿 JSON,然后用 --model-context 生成业务理解.md/json。")
return
model = load_model_context(Path(args.model_context))
context = normalize_model_context(model, evidence, web_notes)
write_json(out_dir / "业务理解.json", context)
write_context_md(out_dir / "业务理解.md", context)
print(f"OK business context: {out_dir / '业务理解.md'}")
print(f"Features: {len(context['business_features'])}")
print("STOP_FOR_USER")
print(f"NEXT_ACTION: {context['next_action']}")
if __name__ == "__main__":
main()