feat: 添加 skill 发布功能和混淆构建优化

- build.py: 移除 --obfuscate 参数,默认混淆模式;从 git config 读取 author,动态注入 SKILL.md
- publish.py: 新增发布脚本,自动 clone 目标仓库、同步 build/ 内容、git commit+push
- publish.sh: 新增一键构建+发布脚本
- skill-publishing spec: 新增发布规范
- skill-packaging spec: 更新构建规范
This commit is contained in:
2026-03-11 12:22:46 +08:00
parent 65c746c639
commit a8af3cc6c4
7 changed files with 665 additions and 122 deletions

257
build.py
View File

@@ -1,20 +1,15 @@
#!/usr/bin/env python3
"""
Skill 打包构建脚本
Skill 打包构建脚本(混淆模式)
使用方式:
# 开发模式 - 快速构建,不混淆
uv run python build.py
# 发布模式 - 完整构建PyArmor 混淆
uv run --with pyarmor python build.py --obfuscate
uv run --with pyarmor python build.py
"""
import os
import sys
import shutil
import subprocess
import argparse
from datetime import datetime
@@ -28,6 +23,67 @@ def generate_timestamp() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S")
def get_git_config(key: str) -> str:
"""
读取 git 配置项
Args:
key: 配置项名称,如 "user.name"
Returns:
配置值字符串
Raises:
subprocess.CalledProcessError: git config 命令失败
"""
result = subprocess.run(
["git", "config", "--get", key],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
def get_git_user_info() -> tuple[str, str]:
"""
读取 git user.name 和 user.email
Returns:
(name, email) 元组
Raises:
SystemExit: git 配置未设置时退出
"""
try:
name = get_git_config("user.name")
except subprocess.CalledProcessError:
print("""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误: git user.name 未设置
请先配置 git 用户名:
git config --global user.name "Your Name"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")
sys.exit(1)
try:
email = get_git_config("user.email")
except subprocess.CalledProcessError:
print("""
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误: git user.email 未设置
请先配置 git 邮箱:
git config --global user.email "your@email.com"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")
sys.exit(1)
return name, email
def clean_and_create_build_dir(build_dir: str) -> None:
"""
删除旧 build 目录并创建新的空目录
@@ -42,59 +98,112 @@ def clean_and_create_build_dir(build_dir: str) -> None:
print(f"创建构建目录: {build_dir}")
def copy_skill_md(source_path: str, target_dir: str) -> None:
def copy_skill_md(source_path: str, target_dir: str, version: str, author: str) -> None:
"""
复制 skill/SKILL.md 到 build/SKILL.md
读取 SKILL.md 模板,动态注入 version 和 author 后写入 build/SKILL.md
Args:
source_path: 源 SKILL.md 路径
target_dir: 目标目录
version: 版本号
author: 作者信息 (格式: "Name <email>")
"""
target_path = os.path.join(target_dir, "SKILL.md")
shutil.copy2(source_path, target_path)
print(f"复制: {source_path} -> {target_path}")
with open(source_path, "r", encoding="utf-8") as f:
content = f.read()
def copy_scripts_dir(source_dir: str, target_dir: str) -> int:
"""
递归复制 scripts/ 目录,仅复制 .py 文件
lines = content.split("\n")
Args:
source_dir: 源目录
target_dir: 目标目录
# 解析 frontmatter
frontmatter_start = -1
frontmatter_end = -1
frontmatter_count = 0
has_metadata = False
metadata_idx = -1
Returns:
复制的文件数量
"""
file_count = 0
for i, line in enumerate(lines):
stripped = line.rstrip()
if stripped == "---":
frontmatter_count += 1
if frontmatter_count == 1:
frontmatter_start = i
elif frontmatter_count == 2:
frontmatter_end = i
break
elif frontmatter_count == 1 and stripped == "metadata:":
has_metadata = True
metadata_idx = i
for root, dirs, files in os.walk(source_dir):
# 计算相对路径
rel_path = os.path.relpath(root, source_dir)
# 处理相对路径为 "." 的情况
if rel_path == ".":
target_root = target_dir
else:
target_root = os.path.join(target_dir, rel_path)
result_lines = []
# 检查此目录下是否有 .py 文件需要复制
has_py_files = any(file.endswith(".py") for file in files)
if frontmatter_start >= 0 and frontmatter_end > frontmatter_start:
# 有 frontmatter
i = 0
while i < len(lines):
if i < frontmatter_start or i >= frontmatter_end:
result_lines.append(lines[i])
i += 1
else:
# 在 frontmatter 内部
if has_metadata and i == metadata_idx:
# 找到 metadata: 行
result_lines.append(lines[i])
i += 1
version_written = False
author_written = False
# 遍历 metadata 子项,替换 version/author保留其他
while i < frontmatter_end:
stripped_line = lines[i].rstrip()
if stripped_line.startswith(" version:"):
if not version_written:
result_lines.append(f" version: \"{version}\"")
version_written = True
i += 1
elif stripped_line.startswith(" author:"):
if not author_written:
result_lines.append(f" author: \"{author}\"")
author_written = True
i += 1
elif stripped_line.startswith(" "):
# 其他 metadata 子项,保留
result_lines.append(lines[i])
i += 1
else:
# metadata 块结束
break
# 确保 version/author 都写了
if not version_written:
result_lines.append(f" version: \"{version}\"")
if not author_written:
result_lines.append(f" author: \"{author}\"")
elif not has_metadata and i == frontmatter_end - 1:
# 没有 metadata在 frontmatter 末尾插入
result_lines.append("metadata:")
result_lines.append(f" version: \"{version}\"")
result_lines.append(f" author: \"{author}\"")
result_lines.append(lines[i])
i += 1
else:
result_lines.append(lines[i])
i += 1
else:
# 没有 frontmatter新建一个
result_lines.append("---")
result_lines.append("name: lyxy-document-reader")
result_lines.append("metadata:")
result_lines.append(f" version: \"{version}\"")
result_lines.append(f" author: \"{author}\"")
result_lines.append("---")
result_lines.append("")
result_lines.extend(lines)
# 只有当有 .py 文件需要复制时才创建目录并复制
if has_py_files:
if not os.path.exists(target_root):
os.makedirs(target_root)
new_content = "\n".join(result_lines)
# 只复制 .py 文件
for file in files:
if file.endswith(".py"):
source_file = os.path.join(root, file)
target_file = os.path.join(target_root, file)
shutil.copy2(source_file, target_file)
file_count += 1
print(f"复制: {source_file} -> {target_file}")
with open(target_path, "w", encoding="utf-8") as f:
f.write(new_content)
return file_count
print(f"生成: {target_path} (version: {version}, author: {author})")
def obfuscate_scripts_dir(source_dir: str, target_dir: str) -> None:
@@ -113,9 +222,9 @@ def obfuscate_scripts_dir(source_dir: str, target_dir: str) -> None:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误: PyArmor 未安装
请使用以下命令启用混淆:
请使用以下命令:
uv run --with pyarmor python build.py --obfuscate
uv run --with pyarmor python build.py
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
""")
@@ -174,29 +283,10 @@ def obfuscate_scripts_dir(source_dir: str, target_dir: str) -> None:
def main() -> None:
"""
主函数:执行完整的打包流程
主函数:执行完整的混淆打包流程
"""
parser = argparse.ArgumentParser(
description="Skill 打包构建",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
# 开发模式 - 快速构建,不混淆
uv run python build.py
# 发布模式 - 完整构建PyArmor 混淆
uv run --with pyarmor python build.py --obfuscate
"""
)
parser.add_argument(
"--obfuscate",
action="store_true",
help="使用 PyArmor 混淆代码 (需: uv run --with pyarmor)"
)
args = parser.parse_args()
print("=" * 60)
print("Skill 打包构建")
print("Skill 打包构建 (混淆模式)")
print("=" * 60)
# 路径配置
@@ -205,40 +295,37 @@ def main() -> None:
scripts_source_dir = os.path.join(project_root, "scripts")
build_dir = os.path.join(project_root, "build")
# 生成时间戳
# 生成版本号
version = generate_timestamp()
print(f"版本号: {version}")
# 读取 git 用户信息
git_name, git_email = get_git_user_info()
author = f"{git_name} <{git_email}>"
print(f"作者: {author}")
print()
# 清理并创建 build 目录
clean_and_create_build_dir(build_dir)
print()
# 复制 SKILL.md
copy_skill_md(skill_md_path, build_dir)
# 复制 SKILL.md(动态注入元数据)
copy_skill_md(skill_md_path, build_dir, version, author)
print()
# 根据 --obfuscate 选择执行路径
if args.obfuscate:
print("────────────────────────────────────────")
print(" 使用 PyArmor 混淆代码 (Normal Mode)")
print("────────────────────────────────────────")
obfuscate_scripts_dir(scripts_source_dir, build_dir)
file_count = None
else:
scripts_target_dir = os.path.join(build_dir, "scripts")
print("复制 scripts/ 目录(仅 .py 文件):")
file_count = copy_scripts_dir(scripts_source_dir, scripts_target_dir)
# 混淆代码
print("────────────────────────────────────────")
print(" 使用 PyArmor 混淆代码 (Normal Mode)")
print("────────────────────────────────────────")
obfuscate_scripts_dir(scripts_source_dir, build_dir)
print()
# 完成信息
print("=" * 60)
print("构建完成!")
print(f"版本号: {version}")
if file_count is not None:
print(f"复制文件数: {file_count}")
else:
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
print(f"作者: {author}")
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
print(f"输出目录: {build_dir}")
print("=" * 60)