Publish agent-radar via gitea-publish skill

This commit is contained in:
gitea-publish
2026-09-01 15:14:54 +08:00
commit 78b87e8b2f
17 changed files with 2279 additions and 0 deletions
+174
View File
@@ -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())