From a8af3cc6c4e26948274feaaf1f73b8a1d6269a4e Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 11 Mar 2026 12:22:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20skill=20=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=8A=9F=E8=83=BD=E5=92=8C=E6=B7=B7=E6=B7=86=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.py: 移除 --obfuscate 参数,默认混淆模式;从 git config 读取 author,动态注入 SKILL.md - publish.py: 新增发布脚本,自动 clone 目标仓库、同步 build/ 内容、git commit+push - publish.sh: 新增一键构建+发布脚本 - skill-publishing spec: 新增发布规范 - skill-packaging spec: 更新构建规范 --- README.md | 39 +++ SKILL.md | 3 - build.py | 257 +++++++++++++------- openspec/specs/skill-packaging/spec.md | 76 +++--- openspec/specs/skill-publishing/spec.md | 82 +++++++ publish.py | 300 ++++++++++++++++++++++++ publish.sh | 30 +++ 7 files changed, 665 insertions(+), 122 deletions(-) create mode 100644 openspec/specs/skill-publishing/spec.md create mode 100644 publish.py create mode 100755 publish.sh diff --git a/README.md b/README.md index 600cfef..e6fb8f9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ tests/ # 测试套件 │ └── fixtures/ # 静态测试文件(Git LFS 管理) │ └── xls/ # XLS 旧格式测试文件 openspec/ # OpenSpec 规范文档 +build.py # 构建脚本(混淆模式) +publish.py # 发布脚本 +publish.sh # 一键构建+发布 README.md # 本文档(开发者文档) SKILL.md # AI Skill 文档 ``` @@ -266,6 +269,42 @@ uv run \ - 错误处理:自定义异常 + 清晰信息 + 位置上下文 - Git 提交:`类型: 简短描述`(feat/fix/refactor/docs/style/test/chore) +## 构建与发布 + +### 构建脚本 + +项目提供 `build.py` 用于构建 Skill 包,使用 PyArmor 进行代码混淆: + +```bash +uv run --with pyarmor python build.py +``` + +构建产物输出到 `build/` 目录,包含: +- `SKILL.md`(动态注入 version 和 author) +- `scripts/`(混淆后的代码) + +### 发布脚本 + +提供 `publish.py` 用于自动发布到目标仓库: + +```bash +uv run python publish.py +``` + +发布流程: +1. 在临时目录 clone `https://github.com/lanyuanxiaoyao/skills.git`(--depth 1) +2. 清空 `skills/lyxy-document-reader/` 目录 +3. 复制 `build/` 内容到目标路径 +4. Git 提交并推送 + +### 一键发布 + +使用 `publish.sh` 一键完成构建+发布: + +```bash +./publish.sh +``` + ## 文档说明 - **README.md**(本文档):面向项目开发者 diff --git a/SKILL.md b/SKILL.md index bb19f32..c38aefb 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,9 +2,6 @@ name: lyxy-document-reader description: 统一文档解析工具 - 将 DOCX、XLSX、PPTX、PDF、HTML/URL 转换为 Markdown。支持全文输出、字数统计、行数统计、标题提取、章节提取、正则搜索。当用户要求"读取/解析/打开文档"、上传 .docx/.xlsx/.pptx/.pdf/.html 文件、或提供 URL 时使用。 license: MIT -metadata: - version: "1.0" - author: lyxy compatibility: Requires Python 3.11+。优先使用 lyxy-runner-python skill,次选 uv run --with,降级到主机 Python。 --- diff --git a/build.py b/build.py index cc19206..94b952b 100644 --- a/build.py +++ b/build.py @@ -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 ") """ 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) diff --git a/openspec/specs/skill-packaging/spec.md b/openspec/specs/skill-packaging/spec.md index 1c914c8..db8da33 100644 --- a/openspec/specs/skill-packaging/spec.md +++ b/openspec/specs/skill-packaging/spec.md @@ -8,7 +8,7 @@ 系统 SHALL 提供 build.py 脚本,运行后完成 skill 的完整打包流程。 #### Scenario: 运行 build.py 成功 -- **WHEN** 用户执行 `uv run python build.py` +- **WHEN** 用户执行 `uv run --with pyarmor python build.py` - **THEN** 脚本完成所有打包步骤并输出成功信息 ### Requirement: 构建目录清理重建 @@ -18,26 +18,31 @@ - **WHEN** 构建开始 - **THEN** 脚本删除整个 build 目录(如有),然后创建新的空 build 目录 -### Requirement: SKILL.md 复制 -系统 SHALL 将 skill/SKILL.md 直接复制到 build/SKILL.md,不保留 skill 这一级目录。 +### Requirement: SKILL.md 动态生成 +系统 SHALL 读取 SKILL.md 模板,动态注入 version 和 author 字段后写入 build/SKILL.md。 -#### Scenario: SKILL.md 成功复制 +#### Scenario: SKILL.md 包含动态元数据 - **WHEN** 构建执行 -- **THEN** build/SKILL.md 文件存在且内容与 skill/SKILL.md 一致 +- **THEN** build/SKILL.md 的 metadata 包含 version 和 author 字段 -### Requirement: scripts 目录复制 -系统 SHALL 将 scripts/ 目录完整复制到 build/scripts/,保持目录结构。 +#### Scenario: version 是时间戳 +- **WHEN** 构建在 2026年3月11日 14点30分22秒执行 +- **THEN** build/SKILL.md 中 `metadata.version` 值为 "20260311_143022" -#### Scenario: scripts 目录结构保留 -- **WHEN** 构建执行 -- **THEN** build/scripts/ 下的子目录结构与原 scripts/ 一致 +#### Scenario: author 来自 git 配置 +- **WHEN** git config user.name 是 "Your Name",git config user.email 是 "your@email.com" +- **THEN** build/SKILL.md 中 `metadata.author` 值为 "Your Name " -### Requirement: 仅复制 Python 文件 -系统 SHALL 只复制 .py 扩展名的文件,其他文件类型自然被过滤。 +### Requirement: git 配置读取 +系统 SHALL 从 git config 读取 user.name 和 user.email。 -#### Scenario: 只保留 py 文件 -- **WHEN** 原目录包含多种文件类型 -- **THEN** build/scripts/ 中只存在 .py 文件 +#### Scenario: git config 读取成功 +- **WHEN** git config 已设置 user.name 和 user.email +- **THEN** 系统读取到正确的值 + +#### Scenario: git config 未设置 +- **WHEN** git config user.name 或 user.email 未设置 +- **THEN** 系统显示错误信息并退出 ### Requirement: 时间戳版本号 系统 SHALL 生成 YYYYMMDD_HHMMSS 格式的时间戳作为构建版本标识。 @@ -51,47 +56,43 @@ #### Scenario: 显示构建信息 - **WHEN** 构建成功完成 -- **THEN** 控制台输出版本号和构建文件清单 +- **THEN** 控制台输出版本号和作者信息 -### Requirement: --obfuscate 参数支持 -系统 SHALL 支持 `--obfuscate` 命令行参数,用于启用代码混淆功能。 +### Requirement: 仅混淆构建 +系统 SHALL 仅提供混淆构建模式,移除非混淆选项。 -#### Scenario: 使用 --obfuscate 参数 -- **WHEN** 用户执行 `uv run --with pyarmor python build.py --obfuscate` -- **THEN** 系统使用 PyArmor 对 scripts 目录代码进行混淆 +#### Scenario: build.py 始终混淆 +- **WHEN** 用户执行 `uv run --with pyarmor python build.py` +- **THEN** 系统使用 PyArmor 混淆 scripts 目录代码 -#### Scenario: 不使用 --obfuscate 参数 -- **WHEN** 用户执行 `uv run python build.py`(不带 --obfuscate) -- **THEN** 系统执行原有的复制行为,不进行混淆 +#### Scenario: 无 --obfuscate 参数 +- **WHEN** 用户运行 build.py +- **THEN** 系统不需要 --obfuscate 参数,直接执行混淆构建 ### Requirement: PyArmor 混淆执行 -系统 SHALL 在 `--obfuscate` 模式下调用 PyArmor 工具对 scripts 目录进行混淆。 +系统 SHALL 调用 PyArmor 工具对 scripts 目录进行混淆。 #### Scenario: PyArmor 成功执行 -- **WHEN** 启用 --obfuscate 且 PyArmor 可用 +- **WHEN** PyArmor 可用 - **THEN** 系统执行 pyarmor gen --recursive 命令 #### Scenario: 混淆后文件输出 - **WHEN** PyArmor 混淆完成 -- **THEN** build/scripts/ 目录包含混淆后的文件 - -#### Scenario: pyarmor_runtime 包含 -- **WHEN** PyArmor 混淆完成 -- **THEN** build/scripts/ 目录包含 pyarmor_runtime_xxxxxx 子目录 +- **THEN** build/ 目录包含混淆后的文件和 pyarmor_runtime 子目录 ### Requirement: PyArmor 未安装友好提示 系统 SHALL 在 PyArmor 未安装时提供清晰的错误提示,引导用户正确使用 `uv run --with pyarmor`。 #### Scenario: PyArmor ImportError -- **WHEN** 启用 --obfuscate 但未通过 --with pyarmor 加载 +- **WHEN** 未通过 --with pyarmor 加载 - **THEN** 系统显示友好错误信息,提示正确命令 ### Requirement: SKILL.md 保持明文 系统 SHALL 在混淆模式下仍然将 SKILL.md 作为明文文件复制,不进行混淆。 #### Scenario: SKILL.md 保持明文 -- **WHEN** 启用 --obfuscate 执行构建 -- **THEN** build/SKILL.md 文件为明文,内容与原文件一致 +- **WHEN** 启用混淆执行构建 +- **THEN** build/SKILL.md 文件为明文,内容包含动态注入的元数据 ### Requirement: 混淆错误处理 系统 SHALL 在 PyArmor 混淆失败时捕获错误并显示详细信息。 @@ -99,3 +100,10 @@ #### Scenario: PyArmor 命令失败 - **WHEN** pyarmor 命令执行返回非零退出码 - **THEN** 系统显示退出码、标准输出和错误输出信息 + +### Requirement: 一键发布脚本 +系统 SHALL 提供 publish.sh 脚本,一键执行混淆构建并发布。 + +#### Scenario: publish.sh 执行成功 +- **WHEN** 用户执行 `./publish.sh` +- **THEN** 系统依次执行混淆构建和发布 diff --git a/openspec/specs/skill-publishing/spec.md b/openspec/specs/skill-publishing/spec.md new file mode 100644 index 0000000..e75e36f --- /dev/null +++ b/openspec/specs/skill-publishing/spec.md @@ -0,0 +1,82 @@ +## Purpose + +提供 skill 发布到目标 GitHub 仓库的能力,自动化将 build/ 目录内容同步到 skills 仓库。 + +## Requirements + +### Requirement: publish.py 一键发布 +系统 SHALL 提供 publish.py 脚本,运行后将 build/ 目录内容发布到目标仓库。 + +#### Scenario: 运行 publish.py 成功 +- **WHEN** 用户执行 `uv run python publish.py` +- **THEN** 脚本完成所有发布步骤并输出成功信息 + +### Requirement: 使用临时目录 clone +系统 SHALL 在系统临时目录创建临时文件夹,用于 clone 目标仓库。 + +#### Scenario: 临时目录自动清理 +- **WHEN** 发布完成或失败 +- **THEN** 临时目录被自动清理 + +### Requirement: shallow clone +系统 SHALL 使用 `--depth 1` 参数 clone 目标仓库,加快 clone 速度。 + +#### Scenario: clone 参数正确 +- **WHEN** 执行 git clone +- **THEN** 命令包含 `--depth 1` 参数 + +### Requirement: 目标仓库配置 +系统 SHALL 硬编码目标仓库 URL 为 `https://github.com/lanyuanxiaoyao/skills.git`。 + +#### Scenario: 目标仓库正确 +- **WHEN** publish.py 执行 clone +- **THEN** clone 的仓库地址是 `https://github.com/lanyuanxiaoyao/skills.git` + +### Requirement: 目标路径配置 +系统 SHALL 将内容发布到目标仓库的 `skills/lyxy-document-reader/` 路径。 + +#### Scenario: 目标路径正确 +- **WHEN** 文件同步完成 +- **THEN** 文件位于 `skills/lyxy-document-reader/` 目录下 + +### Requirement: 清空目标路径 +系统 SHALL 在复制前清空 `skills/lyxy-document-reader/` 目录内容。 + +#### Scenario: 旧文件被清理 +- **WHEN** 开始同步文件 +- **THEN** 目标目录下的旧文件被删除 + +### Requirement: 从 SKILL.md 读取版本号 +系统 SHALL 解析 build/SKILL.md 的 YAML frontmatter 获取 version 字段。 + +#### Scenario: 版本号读取成功 +- **WHEN** build/SKILL.md 包含 `metadata.version: "20260311_143022"` +- **THEN** publish.py 读取到版本号 "20260311_143022" + +### Requirement: git 提交信息 +系统 SHALL 使用包含版本号的 commit message,格式为 `publish: lyxy-document-reader `。 + +#### Scenario: commit message 正确 +- **WHEN** 版本号是 20260311_143022 +- **THEN** commit message 是 `publish: lyxy-document-reader 20260311_143022` + +### Requirement: git 提交并推送 +系统 SHALL 执行 git add、git commit 和 git push 操作。 + +#### Scenario: git 操作成功 +- **WHEN** 文件同步完成 +- **THEN** 系统执行 git add .、git commit 和 git push + +### Requirement: build 目录存在检查 +系统 SHALL 在开始前检查 build/ 目录是否存在,不存在则提示错误。 + +#### Scenario: build 目录不存在 +- **WHEN** build/ 目录不存在 +- **THEN** 脚本显示错误信息并退出 + +### Requirement: SKILL.md 存在检查 +系统 SHALL 检查 build/SKILL.md 是否存在,不存在则提示错误。 + +#### Scenario: build/SKILL.md 不存在 +- **WHEN** build/SKILL.md 不存在 +- **THEN** 脚本显示错误信息并退出 diff --git a/publish.py b/publish.py new file mode 100644 index 0000000..bf8ac12 --- /dev/null +++ b/publish.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +""" +Skill 发布脚本 + +使用方式: + uv run python publish.py +""" + +import os +import sys +import shutil +import subprocess +import tempfile + + +TARGET_REPO_URL = "https://github.com/lanyuanxiaoyao/skills.git" +TARGET_PATH = "skills/lyxy-document-reader" + + +def check_build_dir(build_dir: str) -> None: + """ + 检查 build/ 目录是否存在 + + Args: + build_dir: build 目录路径 + + Raises: + SystemExit: 目录不存在时退出 + """ + if not os.path.exists(build_dir): + print(""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 错误: build/ 目录不存在 + + 请先运行 build.py: + uv run python build.py +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + """) + sys.exit(1) + + +def check_build_skill_md(build_skill_md_path: str) -> None: + """ + 检查 build/SKILL.md 是否存在 + + Args: + build_skill_md_path: build/SKILL.md 路径 + + Raises: + SystemExit: 文件不存在时退出 + """ + if not os.path.exists(build_skill_md_path): + print(""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 错误: build/SKILL.md 不存在 + + 请先运行 build.py: + uv run python build.py +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + """) + sys.exit(1) + + +def parse_version_from_skill_md(skill_md_path: str) -> str: + """ + 从 SKILL.md 解析出版本号 + + Args: + skill_md_path: SKILL.md 路径 + + Returns: + 版本号字符串 + + Raises: + SystemExit: 解析失败时退出 + """ + with open(skill_md_path, "r", encoding="utf-8") as f: + content = f.read() + + # 简单解析 YAML frontmatter 中的 version + lines = content.split("\n") + in_frontmatter = False + in_metadata = False + for line in lines: + stripped = line.strip() + if stripped == "---": + if not in_frontmatter: + in_frontmatter = True + else: + break + continue + if in_frontmatter: + if stripped == "metadata:": + in_metadata = True + elif in_metadata and stripped.startswith("version:"): + # 提取版本号,去掉引号 + version_part = stripped.split(":", 1)[1].strip() + version = version_part.strip('"').strip("'") + return version + elif in_metadata and stripped and not stripped.startswith(" "): + # metadata 块结束 + in_metadata = False + + print(""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 错误: 无法从 build/SKILL.md 解析版本号 + + 请检查 build/SKILL.md 是否包含 metadata.version 字段 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + """) + sys.exit(1) + + +def run_git_command(repo_dir: str, args: list[str]) -> subprocess.CompletedProcess: + """ + 在指定目录运行 git 命令 + + Args: + repo_dir: 仓库目录 + args: git 命令参数列表 + + Returns: + subprocess.CompletedProcess + + Raises: + subprocess.CalledProcessError: 命令失败时 + """ + cmd = ["git"] + args + return subprocess.run( + cmd, + cwd=repo_dir, + check=True, + capture_output=True, + text=True + ) + + +def clone_repo(temp_dir: str) -> str: + """ + 在临时目录 clone 目标仓库 + + Args: + temp_dir: 临时目录路径 + + Returns: + 仓库目录路径 + + Raises: + SystemExit: clone 失败时退出 + """ + repo_dir = os.path.join(temp_dir, "skills-repo") + print(f"Clone 仓库: {TARGET_REPO_URL}") + print(f" 到: {repo_dir}") + + try: + run_git_command(temp_dir, ["clone", "--depth", "1", TARGET_REPO_URL, "skills-repo"]) + except subprocess.CalledProcessError as e: + print(f""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 错误: Clone 仓库失败 + + 返回码: {e.returncode} + 标准输出: {e.stdout} + 错误输出: {e.stderr} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + """) + sys.exit(1) + + return repo_dir + + +def clear_target_dir(repo_dir: str) -> str: + """ + 清空目标路径目录 + + Args: + repo_dir: 仓库目录 + + Returns: + 目标目录路径 + """ + target_dir = os.path.join(repo_dir, TARGET_PATH) + + if os.path.exists(target_dir): + print(f"清空目标目录: {target_dir}") + shutil.rmtree(target_dir) + + os.makedirs(target_dir, exist_ok=True) + return target_dir + + +def copy_build_contents(build_dir: str, target_dir: str) -> None: + """ + 复制 build/ 内容到目标目录 + + Args: + build_dir: build 源目录 + target_dir: 目标目录 + """ + print(f"复制 build/ 内容 -> {target_dir}") + + for item in os.listdir(build_dir): + src = os.path.join(build_dir, item) + dst = os.path.join(target_dir, item) + + if os.path.isdir(src): + shutil.copytree(src, dst) + print(f" 目录: {item}") + else: + shutil.copy2(src, dst) + print(f" 文件: {item}") + + +def git_commit_and_push(repo_dir: str, version: str) -> None: + """ + 执行 git add / commit / push + + Args: + repo_dir: 仓库目录 + version: 版本号 + + Raises: + SystemExit: git 操作失败时退出 + """ + commit_message = f"publish: lyxy-document-reader {version}" + + print(f"Git 提交: {commit_message}") + + try: + run_git_command(repo_dir, ["add", "."]) + run_git_command(repo_dir, ["commit", "-m", commit_message]) + print(" 推送中...") + run_git_command(repo_dir, ["push"]) + except subprocess.CalledProcessError as e: + print(f""" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 错误: Git 操作失败 + + 返回码: {e.returncode} + 标准输出: {e.stdout} + 错误输出: {e.stderr} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + """) + sys.exit(1) + + +def main() -> None: + """ + 主函数:执行完整的发布流程 + """ + print("=" * 60) + print("Skill 发布") + print("=" * 60) + + # 路径配置 + project_root = os.path.dirname(os.path.abspath(__file__)) + build_dir = os.path.join(project_root, "build") + build_skill_md_path = os.path.join(build_dir, "SKILL.md") + + # 检查 build/ 目录 + check_build_dir(build_dir) + check_build_skill_md(build_skill_md_path) + + # 解析版本号 + version = parse_version_from_skill_md(build_skill_md_path) + print(f"版本号: {version}") + print() + + # 使用临时目录 + with tempfile.TemporaryDirectory(prefix="lyxy-publish-") as temp_dir: + print(f"临时目录: {temp_dir}") + print() + + # Clone 仓库 + repo_dir = clone_repo(temp_dir) + print() + + # 清空目标路径 + target_dir = clear_target_dir(repo_dir) + print() + + # 复制内容 + copy_build_contents(build_dir, target_dir) + print() + + # Git 提交并推送 + git_commit_and_push(repo_dir, version) + print() + + # 完成信息 + print("=" * 60) + print("发布完成!") + print(f"版本号: {version}") + print(f"目标仓库: {TARGET_REPO_URL}") + print(f"目标路径: {TARGET_PATH}") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..c0bdb2f --- /dev/null +++ b/publish.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# 混淆构建并发布脚本 +# +# 使用方式: +# ./publish.sh +# + +set -e + +cd "$(dirname "$0")" + +echo "============================================" +echo "Skill 混淆构建 + 发布" +echo "============================================" +echo + +# 1. 混淆构建 +echo "[1/2] 执行混淆构建..." +uv run --with pyarmor python build.py +echo + +# 2. 发布 +echo "[2/2] 执行发布..." +uv run python publish.py +echo + +echo "============================================" +echo "完成!" +echo "============================================"