Publish agent-radar via gitea-publish skill
This commit is contained in:
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 schema 生成一个项目的空白档案骨架,并自动预填可机器获取的字段。
|
||||
|
||||
用法
|
||||
----
|
||||
python3 scripts/new_project.py --repo Devin-AXIS/iPolloWork
|
||||
python3 scripts/new_project.py --repo owner/name --id custom-id --status watch
|
||||
|
||||
生成后需要人工补完的内容(脚本不猜这些):
|
||||
identity / business / scenario / tech / risk / verdict
|
||||
其中 **risk.claim_vs_reality 是必填**,它承载"宣称 vs 实测"的落差分析,
|
||||
是整套档案里最不能空着的字段。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from _common import ( # noqa: E402
|
||||
DATA,
|
||||
PROJECTS,
|
||||
SCHEMA,
|
||||
classify_license,
|
||||
gh_get,
|
||||
load_yaml,
|
||||
today,
|
||||
)
|
||||
|
||||
LEAF_KEYS = {"type", "required", "desc", "enum", "auto", "note"}
|
||||
|
||||
|
||||
def render(node, indent=0, out=None, prefix="", idx=None):
|
||||
"""把 schema 的 fields 树渲染成带注释的 YAML 文本。
|
||||
|
||||
同时返回 {字段路径: 行号} 索引。回填值必须走这个索引按**路径**定位,
|
||||
不能靠硬编码缩进 —— schema 一旦加减层级,缩进常量就会悄悄错位,
|
||||
而且像 `name` 这种同名不同层的字段(项目名 vs 许可证名)会互相覆盖。
|
||||
"""
|
||||
out = out if out is not None else []
|
||||
idx = idx if idx is not None else {}
|
||||
pad = " " * indent
|
||||
for key, spec in node.items():
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
path = f"{prefix}{key}"
|
||||
if "type" in spec: # 叶子字段
|
||||
default = {
|
||||
"str": '""', "int": "0", "bool": "false",
|
||||
"list": "[]", "dict": "{}",
|
||||
}.get(spec["type"], '""')
|
||||
comment = ""
|
||||
if spec.get("enum"):
|
||||
# fields 里的 enum 是 enums 段的键名引用(字符串),不是字面量数组;
|
||||
# 直接 join 会把 "vendor_type" 拆成 v|e|n|d|o|r|_|t|y|p|e
|
||||
e = spec["enum"]
|
||||
comment = f" # {'|'.join(e) if isinstance(e, list) else e}"
|
||||
elif spec.get("desc"):
|
||||
d = re.sub(r"\s+", " ", spec["desc"].replace("\n", " "))
|
||||
comment = f" # {d[:60]}"
|
||||
idx[path] = len(out)
|
||||
out.append(f"{pad}{key}: {default}{comment}")
|
||||
elif spec and not (set(spec) & LEAF_KEYS): # 嵌套 section
|
||||
out.append(f"{pad}{key}:")
|
||||
render(spec, indent + 2, out, path + ".", idx)
|
||||
return out, idx
|
||||
|
||||
|
||||
def slug(repo: str) -> str:
|
||||
name = repo.rstrip("/").split("/")[-1]
|
||||
name = re.sub(r"\.git$", "", name)
|
||||
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--repo", required=True, help="GitHub 仓库 owner/name")
|
||||
ap.add_argument("--id", help="档案 id,默认由仓库名生成")
|
||||
ap.add_argument("--status", default="active", choices=["active", "watch", "archived"])
|
||||
args = ap.parse_args()
|
||||
|
||||
pid = args.id or slug(args.repo)
|
||||
ypath = PROJECTS / f"{pid}.yaml"
|
||||
if ypath.exists():
|
||||
print(f"档案已存在:{ypath}")
|
||||
return 1
|
||||
|
||||
print(f"==> 生成档案 {pid} <- {args.repo}")
|
||||
|
||||
# ---- 抓可自动获取的部分 ----
|
||||
info = gh_get(f"repos/{args.repo}") or {}
|
||||
lic = info.get("license") or {}
|
||||
spdx = lic.get("spdx_id") or ""
|
||||
if spdx == "NOASSERTION":
|
||||
spdx = "NOASSERTION"
|
||||
|
||||
cls = classify_license(spdx, lic.get("name", ""))
|
||||
print(f" 许可证 spdx={spdx or '(无)'} → {cls['level']} / 可见性 {cls['visibility']}")
|
||||
|
||||
schema = load_yaml(SCHEMA / "project.schema.yaml")
|
||||
lines, idx = render(schema.get("fields", {}))
|
||||
|
||||
def setval(path: str, val) -> None:
|
||||
"""按字段路径回填值,保留该行原有的缩进与键名。"""
|
||||
i = idx.get(path)
|
||||
if i is None:
|
||||
print(f" [warn] schema 中无字段 {path},跳过回填")
|
||||
return
|
||||
line = lines[i]
|
||||
indent = " " * (len(line) - len(line.lstrip()))
|
||||
key = path.split(".")[-1]
|
||||
lines[i] = f"{indent}{key}: {val}"
|
||||
|
||||
setval("id", pid)
|
||||
setval("name", f'"{info.get("name", "")}"')
|
||||
setval("repo", f'"{args.repo}"')
|
||||
setval("homepage", f'"{info.get("homepage") or ""}"')
|
||||
setval("added_at", f'"{today()}"')
|
||||
setval("status", args.status)
|
||||
|
||||
if info:
|
||||
setval("identity.tagline", f'"{info.get("description") or ""}"')
|
||||
setval("identity.first_seen", f'"{(info.get("created_at") or "")[:10]}"')
|
||||
|
||||
setval("business.license.spdx", f'"{spdx or "NOASSERTION"}"')
|
||||
setval("business.license.name", f'"{lic.get("name") or "自定义许可"}"')
|
||||
setval("business.license.openness", cls["level"])
|
||||
setval("business.license.redistributable", "true" if cls["redistributable"] else "false")
|
||||
|
||||
setval("mirror.gitea_repo", f'"gitinbox/{info.get("name") or pid}"')
|
||||
setval("mirror.visibility", cls["visibility"])
|
||||
setval("mirror.upstream", f'"https://github.com/{args.repo}"')
|
||||
|
||||
text = "\n".join(lines)
|
||||
header = (
|
||||
f"# {info.get('name') or pid} · 项目档案\n"
|
||||
f"# 生成于 {today()} · 由 scripts/new_project.py 从 schema 生成\n"
|
||||
"# 人工补完重点:business / scenario / tech / risk.claim_vs_reality / verdict\n"
|
||||
"# 字段规范见 schema/project.schema.yaml\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
PROJECTS.mkdir(parents=True, exist_ok=True)
|
||||
ypath.write_text(header + text + "\n", encoding="utf-8")
|
||||
print(f" 已写入 {ypath.relative_to(PROJECTS.parent.parent)}")
|
||||
|
||||
# ---- 登记到 registry ----
|
||||
reg_path = DATA / "registry.yaml"
|
||||
reg_text = reg_path.read_text(encoding="utf-8")
|
||||
if f"id: {pid}" not in reg_text:
|
||||
entry = (
|
||||
f" - id: {pid}\n"
|
||||
f" repo: {args.repo}\n"
|
||||
f" status: {args.status}\n"
|
||||
f" added_at: {today()}\n"
|
||||
f" license_level: {cls['level']}"
|
||||
+ (" # source-available,镜像私有\n" if cls["level"] == "L3" else "\n")
|
||||
+ f" mirror_visibility: {cls['visibility']}\n"
|
||||
)
|
||||
reg_text = reg_text.rstrip("\n") + "\n" + entry
|
||||
reg_path.write_text(reg_text, encoding="utf-8")
|
||||
print(f" 已登记到 data/registry.yaml")
|
||||
|
||||
print(f"\n下一步:\n 1. 补完档案(重点 risk.claim_vs_reality)\n"
|
||||
f" 2. python3 scripts/fetch_stats.py --id {pid}\n"
|
||||
f" 3. bash scripts/mirror_sync.sh --id {pid}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user