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

View File

@@ -40,6 +40,9 @@ tests/ # 测试套件
│ └── fixtures/ # 静态测试文件Git LFS 管理) │ └── fixtures/ # 静态测试文件Git LFS 管理)
│ └── xls/ # XLS 旧格式测试文件 │ └── xls/ # XLS 旧格式测试文件
openspec/ # OpenSpec 规范文档 openspec/ # OpenSpec 规范文档
build.py # 构建脚本(混淆模式)
publish.py # 发布脚本
publish.sh # 一键构建+发布
README.md # 本文档(开发者文档) README.md # 本文档(开发者文档)
SKILL.md # AI Skill 文档 SKILL.md # AI Skill 文档
``` ```
@@ -266,6 +269,42 @@ uv run \
- 错误处理:自定义异常 + 清晰信息 + 位置上下文 - 错误处理:自定义异常 + 清晰信息 + 位置上下文
- Git 提交:`类型: 简短描述`feat/fix/refactor/docs/style/test/chore - 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**(本文档):面向项目开发者 - **README.md**(本文档):面向项目开发者

View File

@@ -2,9 +2,6 @@
name: lyxy-document-reader name: lyxy-document-reader
description: 统一文档解析工具 - 将 DOCX、XLSX、PPTX、PDF、HTML/URL 转换为 Markdown。支持全文输出、字数统计、行数统计、标题提取、章节提取、正则搜索。当用户要求"读取/解析/打开文档"、上传 .docx/.xlsx/.pptx/.pdf/.html 文件、或提供 URL 时使用。 description: 统一文档解析工具 - 将 DOCX、XLSX、PPTX、PDF、HTML/URL 转换为 Markdown。支持全文输出、字数统计、行数统计、标题提取、章节提取、正则搜索。当用户要求"读取/解析/打开文档"、上传 .docx/.xlsx/.pptx/.pdf/.html 文件、或提供 URL 时使用。
license: MIT license: MIT
metadata:
version: "1.0"
author: lyxy
compatibility: Requires Python 3.11+。优先使用 lyxy-runner-python skill次选 uv run --with降级到主机 Python。 compatibility: Requires Python 3.11+。优先使用 lyxy-runner-python skill次选 uv run --with降级到主机 Python。
--- ---

247
build.py
View File

@@ -1,20 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Skill 打包构建脚本 Skill 打包构建脚本(混淆模式)
使用方式: 使用方式:
# 开发模式 - 快速构建,不混淆 uv run --with pyarmor python build.py
uv run python build.py
# 发布模式 - 完整构建PyArmor 混淆
uv run --with pyarmor python build.py --obfuscate
""" """
import os import os
import sys import sys
import shutil import shutil
import subprocess import subprocess
import argparse
from datetime import datetime from datetime import datetime
@@ -28,6 +23,67 @@ def generate_timestamp() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S") 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: def clean_and_create_build_dir(build_dir: str) -> None:
""" """
删除旧 build 目录并创建新的空目录 删除旧 build 目录并创建新的空目录
@@ -42,59 +98,112 @@ def clean_and_create_build_dir(build_dir: str) -> None:
print(f"创建构建目录: {build_dir}") 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: Args:
source_path: 源 SKILL.md 路径 source_path: 源 SKILL.md 路径
target_dir: 目标目录 target_dir: 目标目录
version: 版本号
author: 作者信息 (格式: "Name <email>")
""" """
target_path = os.path.join(target_dir, "SKILL.md") 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: lines = content.split("\n")
"""
递归复制 scripts/ 目录,仅复制 .py 文件
Args: # 解析 frontmatter
source_dir: 源目录 frontmatter_start = -1
target_dir: 目标目录 frontmatter_end = -1
frontmatter_count = 0
has_metadata = False
metadata_idx = -1
Returns: for i, line in enumerate(lines):
复制的文件数量 stripped = line.rstrip()
""" if stripped == "---":
file_count = 0 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): result_lines = []
# 计算相对路径
rel_path = os.path.relpath(root, source_dir) if frontmatter_start >= 0 and frontmatter_end > frontmatter_start:
# 处理相对路径为 "." 的情况 # 有 frontmatter
if rel_path == ".": i = 0
target_root = target_dir while i < len(lines):
if i < frontmatter_start or i >= frontmatter_end:
result_lines.append(lines[i])
i += 1
else: else:
target_root = os.path.join(target_dir, rel_path) # 在 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 文件需要复制 new_content = "\n".join(result_lines)
has_py_files = any(file.endswith(".py") for file in files)
# 只有当有 .py 文件需要复制时才创建目录并复制 with open(target_path, "w", encoding="utf-8") as f:
if has_py_files: f.write(new_content)
if not os.path.exists(target_root):
os.makedirs(target_root)
# 只复制 .py 文件 print(f"生成: {target_path} (version: {version}, author: {author})")
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}")
return file_count
def obfuscate_scripts_dir(source_dir: str, target_dir: str) -> None: 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 未安装 错误: 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: 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("=" * 60)
print("Skill 打包构建") print("Skill 打包构建 (混淆模式)")
print("=" * 60) print("=" * 60)
# 路径配置 # 路径配置
@@ -205,39 +295,36 @@ def main() -> None:
scripts_source_dir = os.path.join(project_root, "scripts") scripts_source_dir = os.path.join(project_root, "scripts")
build_dir = os.path.join(project_root, "build") build_dir = os.path.join(project_root, "build")
# 生成时间戳 # 生成版本号
version = generate_timestamp() version = generate_timestamp()
print(f"版本号: {version}") print(f"版本号: {version}")
# 读取 git 用户信息
git_name, git_email = get_git_user_info()
author = f"{git_name} <{git_email}>"
print(f"作者: {author}")
print() print()
# 清理并创建 build 目录 # 清理并创建 build 目录
clean_and_create_build_dir(build_dir) clean_and_create_build_dir(build_dir)
print() print()
# 复制 SKILL.md # 复制 SKILL.md(动态注入元数据)
copy_skill_md(skill_md_path, build_dir) copy_skill_md(skill_md_path, build_dir, version, author)
print() print()
# 根据 --obfuscate 选择执行路径 # 混淆代码
if args.obfuscate:
print("────────────────────────────────────────") print("────────────────────────────────────────")
print(" 使用 PyArmor 混淆代码 (Normal Mode)") print(" 使用 PyArmor 混淆代码 (Normal Mode)")
print("────────────────────────────────────────") print("────────────────────────────────────────")
obfuscate_scripts_dir(scripts_source_dir, build_dir) 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()
# 完成信息 # 完成信息
print("=" * 60) print("=" * 60)
print("构建完成!") print("构建完成!")
print(f"版本号: {version}") print(f"版本号: {version}")
if file_count is not None: print(f"作者: {author}")
print(f"复制文件数: {file_count}")
else:
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime") print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
print(f"输出目录: {build_dir}") print(f"输出目录: {build_dir}")
print("=" * 60) print("=" * 60)

View File

@@ -8,7 +8,7 @@
系统 SHALL 提供 build.py 脚本,运行后完成 skill 的完整打包流程。 系统 SHALL 提供 build.py 脚本,运行后完成 skill 的完整打包流程。
#### Scenario: 运行 build.py 成功 #### Scenario: 运行 build.py 成功
- **WHEN** 用户执行 `uv run python build.py` - **WHEN** 用户执行 `uv run --with pyarmor python build.py`
- **THEN** 脚本完成所有打包步骤并输出成功信息 - **THEN** 脚本完成所有打包步骤并输出成功信息
### Requirement: 构建目录清理重建 ### Requirement: 构建目录清理重建
@@ -18,26 +18,31 @@
- **WHEN** 构建开始 - **WHEN** 构建开始
- **THEN** 脚本删除整个 build 目录(如有),然后创建新的空 build 目录 - **THEN** 脚本删除整个 build 目录(如有),然后创建新的空 build 目录
### Requirement: SKILL.md 复制 ### Requirement: SKILL.md 动态生成
系统 SHALL 将 skill/SKILL.md 直接复制到 build/SKILL.md不保留 skill 这一级目录 系统 SHALL 读取 SKILL.md 模板,动态注入 version 和 author 字段后写入 build/SKILL.md
#### Scenario: SKILL.md 成功复制 #### Scenario: SKILL.md 包含动态元数据
- **WHEN** 构建执行 - **WHEN** 构建执行
- **THEN** build/SKILL.md 文件存在且内容与 skill/SKILL.md 一致 - **THEN** build/SKILL.md 的 metadata 包含 version 和 author 字段
### Requirement: scripts 目录复制 #### Scenario: version 是时间戳
系统 SHALL 将 scripts/ 目录完整复制到 build/scripts/,保持目录结构。 - **WHEN** 构建在 2026年3月11日 14点30分22秒执行
- **THEN** build/SKILL.md 中 `metadata.version` 值为 "20260311_143022"
#### Scenario: scripts 目录结构保留 #### Scenario: author 来自 git 配置
- **WHEN** 构建执行 - **WHEN** git config user.name 是 "Your Name"git config user.email 是 "your@email.com"
- **THEN** build/scripts/ 下的子目录结构与原 scripts/ 一致 - **THEN** build/SKILL.md 中 `metadata.author` 值为 "Your Name <your@email.com>"
### Requirement: 仅复制 Python 文件 ### Requirement: git 配置读取
系统 SHALL 只复制 .py 扩展名的文件,其他文件类型自然被过滤 系统 SHALL 从 git config 读取 user.name 和 user.email
#### Scenario: 只保留 py 文件 #### Scenario: git config 读取成功
- **WHEN** 原目录包含多种文件类型 - **WHEN** git config 已设置 user.name 和 user.email
- **THEN** build/scripts/ 中只存在 .py 文件 - **THEN** 系统读取到正确的值
#### Scenario: git config 未设置
- **WHEN** git config user.name 或 user.email 未设置
- **THEN** 系统显示错误信息并退出
### Requirement: 时间戳版本号 ### Requirement: 时间戳版本号
系统 SHALL 生成 YYYYMMDD_HHMMSS 格式的时间戳作为构建版本标识。 系统 SHALL 生成 YYYYMMDD_HHMMSS 格式的时间戳作为构建版本标识。
@@ -51,47 +56,43 @@
#### Scenario: 显示构建信息 #### Scenario: 显示构建信息
- **WHEN** 构建成功完成 - **WHEN** 构建成功完成
- **THEN** 控制台输出版本号和构建文件清单 - **THEN** 控制台输出版本号和作者信息
### Requirement: --obfuscate 参数支持 ### Requirement: 仅混淆构建
系统 SHALL 支持 `--obfuscate` 命令行参数,用于启用代码混淆功能 系统 SHALL 仅提供混淆构建模式,移除非混淆选项
#### Scenario: 使用 --obfuscate 参数 #### Scenario: build.py 始终混淆
- **WHEN** 用户执行 `uv run --with pyarmor python build.py --obfuscate` - **WHEN** 用户执行 `uv run --with pyarmor python build.py`
- **THEN** 系统使用 PyArmor scripts 目录代码进行混淆 - **THEN** 系统使用 PyArmor 混淆 scripts 目录代码
#### Scenario: 不使用 --obfuscate 参数 #### Scenario: --obfuscate 参数
- **WHEN** 用户执行 `uv run python build.py`(不带 --obfuscate - **WHEN** 用户运行 build.py
- **THEN** 系统执行原有的复制行为,不进行混淆 - **THEN** 系统不需要 --obfuscate 参数,直接执行混淆构建
### Requirement: PyArmor 混淆执行 ### Requirement: PyArmor 混淆执行
系统 SHALL `--obfuscate` 模式下调用 PyArmor 工具对 scripts 目录进行混淆。 系统 SHALL 调用 PyArmor 工具对 scripts 目录进行混淆。
#### Scenario: PyArmor 成功执行 #### Scenario: PyArmor 成功执行
- **WHEN** 启用 --obfuscate 且 PyArmor 可用 - **WHEN** PyArmor 可用
- **THEN** 系统执行 pyarmor gen --recursive 命令 - **THEN** 系统执行 pyarmor gen --recursive 命令
#### Scenario: 混淆后文件输出 #### Scenario: 混淆后文件输出
- **WHEN** PyArmor 混淆完成 - **WHEN** PyArmor 混淆完成
- **THEN** build/scripts/ 目录包含混淆后的文件 - **THEN** build/ 目录包含混淆后的文件和 pyarmor_runtime 子目录
#### Scenario: pyarmor_runtime 包含
- **WHEN** PyArmor 混淆完成
- **THEN** build/scripts/ 目录包含 pyarmor_runtime_xxxxxx 子目录
### Requirement: PyArmor 未安装友好提示 ### Requirement: PyArmor 未安装友好提示
系统 SHALL 在 PyArmor 未安装时提供清晰的错误提示,引导用户正确使用 `uv run --with pyarmor` 系统 SHALL 在 PyArmor 未安装时提供清晰的错误提示,引导用户正确使用 `uv run --with pyarmor`
#### Scenario: PyArmor ImportError #### Scenario: PyArmor ImportError
- **WHEN** 启用 --obfuscate 但未通过 --with pyarmor 加载 - **WHEN** 未通过 --with pyarmor 加载
- **THEN** 系统显示友好错误信息,提示正确命令 - **THEN** 系统显示友好错误信息,提示正确命令
### Requirement: SKILL.md 保持明文 ### Requirement: SKILL.md 保持明文
系统 SHALL 在混淆模式下仍然将 SKILL.md 作为明文文件复制,不进行混淆。 系统 SHALL 在混淆模式下仍然将 SKILL.md 作为明文文件复制,不进行混淆。
#### Scenario: SKILL.md 保持明文 #### Scenario: SKILL.md 保持明文
- **WHEN** 启用 --obfuscate 执行构建 - **WHEN** 启用混淆执行构建
- **THEN** build/SKILL.md 文件为明文,内容与原文件一致 - **THEN** build/SKILL.md 文件为明文,内容包含动态注入的元数据
### Requirement: 混淆错误处理 ### Requirement: 混淆错误处理
系统 SHALL 在 PyArmor 混淆失败时捕获错误并显示详细信息。 系统 SHALL 在 PyArmor 混淆失败时捕获错误并显示详细信息。
@@ -99,3 +100,10 @@
#### Scenario: PyArmor 命令失败 #### Scenario: PyArmor 命令失败
- **WHEN** pyarmor 命令执行返回非零退出码 - **WHEN** pyarmor 命令执行返回非零退出码
- **THEN** 系统显示退出码、标准输出和错误输出信息 - **THEN** 系统显示退出码、标准输出和错误输出信息
### Requirement: 一键发布脚本
系统 SHALL 提供 publish.sh 脚本,一键执行混淆构建并发布。
#### Scenario: publish.sh 执行成功
- **WHEN** 用户执行 `./publish.sh`
- **THEN** 系统依次执行混淆构建和发布

View File

@@ -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 <version>`
#### 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** 脚本显示错误信息并退出

300
publish.py Normal file
View File

@@ -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()

30
publish.sh Executable file
View File

@@ -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 "============================================"