Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:
- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
- docx family: kept docx (official, full toolchain) + docx-cn
(GB/T 9704 Chinese official-document constants),
dropped docx_writer (no scripts, name collided with docx)
- humanizer family: kept humanizer-zh (6 zh reference docs),
dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
live credentials
Result: 25 skills, 572 files, ~7.5 MB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
||
"""原子工具:Job JSON Schema 校验
|
||
|
||
校验 Job JSON 文件的合法性,composer 模式额外校验布局名称和 source 引用。
|
||
|
||
用法:
|
||
python validate_job.py job.json
|
||
python validate_job.py job.json --dry-run # 校验 + 尝试连接数据源
|
||
|
||
输出 (stdout):
|
||
{"status": "ok", "mode": "...", "summary": {...}}
|
||
{"status": "error", "errors": [...]}
|
||
|
||
退出码: 0=通过, 1=失败
|
||
"""
|
||
|
||
import sys
|
||
import json
|
||
import argparse
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||
|
||
from ppt_station.models.job import Job
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Job JSON 校验工具")
|
||
parser.add_argument("job_json", help="Job JSON 文件路径")
|
||
parser.add_argument("--dry-run", action="store_true",
|
||
help="额外尝试连接数据源(不渲染)")
|
||
args = parser.parse_args()
|
||
|
||
job_path = Path(args.job_json)
|
||
if not job_path.exists():
|
||
print(json.dumps({"status": "error", "errors": [f"文件不存在: {job_path}"]},
|
||
ensure_ascii=False))
|
||
sys.exit(1)
|
||
|
||
# 1. 基础 Schema 校验
|
||
errors = []
|
||
try:
|
||
with open(job_path, "r", encoding="utf-8") as f:
|
||
raw = json.load(f)
|
||
job = Job(**raw)
|
||
except json.JSONDecodeError as e:
|
||
print(json.dumps({"status": "error", "errors": [f"JSON 语法错误: {e}"]},
|
||
ensure_ascii=False))
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(json.dumps({"status": "error", "errors": [f"Schema 校验失败: {e}"]},
|
||
ensure_ascii=False))
|
||
sys.exit(1)
|
||
|
||
# 2. Composer 模式额外校验
|
||
if job.mode == "composer":
|
||
from ppt_station.composer.layouts import LAYOUT_REGISTRY
|
||
|
||
all_sources = set(job.datasources.keys())
|
||
if job.transforms:
|
||
all_sources.update(job.transforms.keys())
|
||
|
||
for i, page in enumerate(job.pages):
|
||
# 校验布局名称
|
||
if page.layout not in LAYOUT_REGISTRY:
|
||
available = ", ".join(LAYOUT_REGISTRY.keys())
|
||
errors.append(f"pages[{i}]: 未知布局 '{page.layout}'。可用: {available}")
|
||
|
||
# 校验 datasource/source 引用
|
||
if "datasource" in page.data:
|
||
if page.data["datasource"] not in all_sources:
|
||
errors.append(
|
||
f"pages[{i}]: datasource '{page.data['datasource']}' "
|
||
f"不存在于 datasources/transforms 中"
|
||
)
|
||
# source 值不在已知数据源中 → 视为脚注文本,不报错
|
||
|
||
# 校验嵌套 datasource/source 引用 (two_charts)
|
||
for key in ("left", "right"):
|
||
if key in page.data and isinstance(page.data[key], dict):
|
||
sub = page.data[key]
|
||
ds_ref = sub.get("datasource") or sub.get("source")
|
||
if ds_ref and ds_ref not in all_sources:
|
||
errors.append(
|
||
f"pages[{i}].data.{key}: datasource/source '{ds_ref}' "
|
||
f"不存在于 datasources/transforms 中"
|
||
)
|
||
|
||
if errors:
|
||
print(json.dumps({"status": "error", "errors": errors}, ensure_ascii=False))
|
||
sys.exit(1)
|
||
|
||
# 3. 构造摘要
|
||
summary = {"mode": job.mode, "datasources": list(job.datasources.keys())}
|
||
if job.mode == "template":
|
||
summary["slides"] = len(job.slides)
|
||
summary["template"] = job.template.path
|
||
elif job.mode == "composer":
|
||
summary["pages"] = len(job.pages)
|
||
summary["theme"] = job.theme or "jp_finance"
|
||
summary["layouts"] = [p.layout for p in job.pages]
|
||
|
||
if job.transforms:
|
||
summary["transforms"] = list(job.transforms.keys())
|
||
|
||
# 4. 可选 dry-run:尝试连接数据源
|
||
if args.dry_run and job.datasources:
|
||
from ppt_station.connectors import ConnectorFactory
|
||
|
||
ds_status = {}
|
||
for name, ds in job.datasources.items():
|
||
try:
|
||
print(f"尝试连接: {name}", file=sys.stderr)
|
||
df = ConnectorFactory.load_data(name, ds)
|
||
ds_status[name] = {"ok": True, "rows": len(df), "columns": list(df.columns)}
|
||
except Exception as e:
|
||
ds_status[name] = {"ok": False, "error": str(e)}
|
||
summary["datasource_check"] = ds_status
|
||
|
||
print(json.dumps({"status": "ok", "summary": summary}, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|