Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac66721
Add open hardware license
Maple127667 Jul 30, 2026
b692174
Clarify appearance model licensing and safety
Maple127667 Jul 30, 2026
67573ec
Merge pull request #2 from Roboparty/dev
ClaudiaGardner Aug 12, 2026
d762789
Merge pull request #1 from Roboparty/dev
ClaudiaGardner Aug 12, 2026
f06e69f
Correct camera model license status
ClaudiaGardner Aug 12, 2026
e3d1f0b
Clarify GPLv3 and upstream Apache-2.0 licensing
ClaudiaGardner Aug 12, 2026
a7dd94f
Document CERN-OHL-W-2.0 license
ClaudiaGardner Aug 12, 2026
c09ba3a
Document CERN-OHL-W-2.0 license
ClaudiaGardner Aug 12, 2026
4bf6143
Merge pull request #2 from Roboparty/codex/license-audit-20260812
ClaudiaGardner Aug 12, 2026
37aac9c
Merge pull request #3 from Roboparty/codex/license-audit-20260812
ClaudiaGardner Aug 12, 2026
471acb2
Merge pull request #8 from Roboparty/codex/license-audit-20260812
ClaudiaGardner Aug 12, 2026
6a42270
Merge pull request #1 from Roboparty/codex/license-audit-20260812
ClaudiaGardner Aug 12, 2026
22e42f3
add automated subtree snapshot workflow
Maple127667 Aug 13, 2026
738a049
Merge commit '471acb24eab7051ab4357e6c95309c94e66345e3' into dev
Maple127667 Aug 13, 2026
bde7d98
Merge commit '6a42270997f9ff8af10fad791d7390e8a5c0755a' into dev
Maple127667 Aug 13, 2026
cab883f
Merge commit '4bf61434f4925fbd9076a82d3639b0d6e7133f60' into dev
Maple127667 Aug 13, 2026
3f3e105
Merge commit '37aac9ca665e92731444a1618320078e7ba21569' into dev
Maple127667 Aug 13, 2026
38ebb75
Squashed 'modules/roboparty_train/robolab/data/robots/roboparty/rpo/'…
Maple127667 Aug 13, 2026
b0a1a2f
Merge commit '38ebb75498d5b865adc0a87a291c3a5875a0331d' into dev
Maple127667 Aug 13, 2026
64d9090
harden snapshot reporting
Maple127667 Aug 13, 2026
79738a5
make snapshot reports repository-specific
Maple127667 Aug 13, 2026
a83c50a
use third-level headings for changed repositories
Maple127667 Aug 13, 2026
db689fb
add signed Feishu group change notifications
Maple127667 Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions .github/scripts/report_feishu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Append one deterministic snapshot result to a Feishu docx document."""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo


FEISHU_API = "https://open.feishu.cn/open-apis"


def request_json(url: str, method: str = "GET", payload: object | None = None, token: str | None = None) -> dict:
headers = {"Content-Type": "application/json; charset=utf-8"}
if token:
headers["Authorization"] = f"Bearer {token}"
body = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
request = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")[:2000]
raise RuntimeError(f"Feishu HTTP {exc.code}: {detail}") from exc
if result.get("code", 0) != 0:
raise RuntimeError(f"Feishu API error {result.get('code')}: {result.get('msg')}")
return result


def rich_text(content: str) -> dict:
return {
"elements": [
{
"text_run": {
"content": content[:5000],
"text_element_style": {},
}
}
],
"style": {},
}


def text_block(content: str) -> dict:
return {"block_type": 2, "text": rich_text(content)}


def heading_block(content: str) -> dict:
return {"block_type": 4, "heading2": rich_text(content)}


def heading3_block(content: str) -> dict:
return {"block_type": 5, "heading3": rich_text(content)}


def load_report(path: Path) -> dict:
if path.exists():
return json.loads(path.read_text(encoding="utf-8"))
return {
"status": "failed",
"changed": False,
"updated": [],
"changed_repositories": [],
"changed_files": 0,
"deleted_files": 0,
"additions": 0,
"deletions": 0,
"changed_lines": 0,
"base_sha": "unknown",
"final_sha": "unknown",
"error": "GitHub Actions 未生成同步报告。",
}


def build_blocks(report: dict) -> list[dict]:
now = datetime.now(ZoneInfo("Asia/Shanghai"))
workflow_result = os.environ.get("WORKFLOW_RESULT", "unknown")
if workflow_result != "success":
report["status"] = "failed"
if not report.get("error"):
report["error"] = f"GitHub 同步任务结果:{workflow_result}"

status = report.get("status", "failed")
changed = bool(report.get("changed"))
changed_repositories = list(report.get("changed_repositories") or [])
status_text = "✅ 同步成功" if status == "success" else "❌ 同步失败"
if status == "success" and not changed:
status_text = "ℹ️ 今日无改动"

run_id = os.environ.get("GITHUB_RUN_ID", "").strip()
run_url = (
f"{os.environ.get('GITHUB_SERVER_URL', 'https://github.com')}/"
f"{os.environ.get('GITHUB_REPOSITORY', 'Roboparty/roboto_origin')}/actions/runs/{run_id}"
if run_id
else ""
)
pr_url = os.environ.get("PR_URL", "").strip()
base_sha = str(report.get("base_sha", "unknown"))[:12]
final_sha = str(report.get("final_sha", "unknown"))[:12]

blocks = [
heading_block(f"{now:%Y-%m-%d} 自动快照报告"),
text_block(f"状态:{status_text}"),
text_block(f"时间:{now:%Y-%m-%d %H:%M:%S}(Asia/Shanghai)"),
text_block(
"统计:"
f"{report.get('changed_files', 0)} 个文件,"
f"{report.get('deleted_files', 0)} 个删除,"
f"+{report.get('additions', 0)} / -{report.get('deletions', 0)}"
),
text_block(f"快照提交:{base_sha} → {final_sha}"),
text_block(
f"实际改动仓库:{len(changed_repositories)} 个(未变化仓库已省略)"
if status == "success"
else "实际改动仓库:同步失败,未完成统计"
),
]
for repository in changed_repositories:
name = str(repository.get("name", "unknown"))
before = str(repository.get("before", "unknown"))[:12]
upstream = str(repository.get("upstream", "unknown"))[:12]
blocks.append(heading3_block(f"🔔 发生改动:{name}"))
details = [
f"仓库:{repository.get('repository', '')}",
f"快照路径:{repository.get('path', '')}",
f"上游提交:{before} → {upstream}",
(
"统计:"
f"{repository.get('changed_files', 0)} 个文件,"
f"{repository.get('deleted_files', 0)} 个删除,"
f"+{repository.get('additions', 0)} / -{repository.get('deletions', 0)}"
),
]
compare_url = str(repository.get("compare_url") or "")
if compare_url:
details.append(f"上游对比:{compare_url}")
details.append("文件:")
for file_change in repository.get("files") or []:
path = str(file_change.get("path", ""))
prefix = str(repository.get("path", "")).rstrip("/") + "/"
if path.startswith(prefix):
path = path[len(prefix) :]
added = file_change.get("additions")
deleted = file_change.get("deletions")
line_stat = "二进制" if added is None or deleted is None else f"+{added} / -{deleted}"
details.append(f"- [{file_change.get('status', '?')}] {path}({line_stat})")
blocks.extend(text_blocks(details))
if run_url:
blocks.append(text_block(f"GitHub Actions:{run_url}"))
else:
blocks.append(text_block("执行方式:合并前本地手动快照验证"))
if pr_url:
blocks.append(text_block(f"dev → main PR:{pr_url}"))
if report.get("error"):
blocks.append(text_block(f"错误:{str(report['error'])[:1500]}"))
return blocks


def text_blocks(lines: list[str], max_chars: int = 4500) -> list[dict]:
chunks: list[str] = []
current = ""
for line in lines:
candidate = line if not current else f"{current}\n{line}"
if len(candidate) <= max_chars:
current = candidate
continue
if current:
chunks.append(current)
current = line[:max_chars]
if current:
chunks.append(current)
return [text_block(chunk) for chunk in chunks]


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--report", required=True, type=Path)
args = parser.parse_args()

app_id = os.environ.get("FEISHU_APP_ID", "")
app_secret = os.environ.get("FEISHU_APP_SECRET", "")
document_id = os.environ.get("FEISHU_DOCUMENT_ID", "")
if not all((app_id, app_secret, document_id)):
print("Feishu document secrets are incomplete", file=sys.stderr)
return 1

auth = request_json(
f"{FEISHU_API}/auth/v3/tenant_access_token/internal",
method="POST",
payload={"app_id": app_id, "app_secret": app_secret},
)
token = auth.get("tenant_access_token")
if not token:
raise RuntimeError("Feishu authentication returned no tenant_access_token")

report = load_report(args.report)
blocks = build_blocks(report)
created = 0
for index in range(0, len(blocks), 50):
result = request_json(
f"{FEISHU_API}/docx/v1/documents/{document_id}/blocks/{document_id}/children",
method="POST",
payload={"children": blocks[index : index + 50], "index": -1},
token=token,
)
created += len(result.get("data", {}).get("children", []))
print(f"Appended {created} report blocks to the Feishu document")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading