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:
39
README.md
39
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**(本文档):面向项目开发者
|
||||
|
||||
3
SKILL.md
3
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。
|
||||
---
|
||||
|
||||
|
||||
247
build.py
247
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 <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
|
||||
result_lines = []
|
||||
|
||||
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:
|
||||
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 文件需要复制
|
||||
has_py_files = any(file.endswith(".py") for file in files)
|
||||
new_content = "\n".join(result_lines)
|
||||
|
||||
# 只有当有 .py 文件需要复制时才创建目录并复制
|
||||
if has_py_files:
|
||||
if not os.path.exists(target_root):
|
||||
os.makedirs(target_root)
|
||||
with open(target_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
|
||||
# 只复制 .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}")
|
||||
|
||||
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,39 +295,36 @@ 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("=" * 60)
|
||||
print("构建完成!")
|
||||
print(f"版本号: {version}")
|
||||
if file_count is not None:
|
||||
print(f"复制文件数: {file_count}")
|
||||
else:
|
||||
print(f"作者: {author}")
|
||||
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
|
||||
print(f"输出目录: {build_dir}")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -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 <your@email.com>"
|
||||
|
||||
### 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** 系统依次执行混淆构建和发布
|
||||
|
||||
82
openspec/specs/skill-publishing/spec.md
Normal file
82
openspec/specs/skill-publishing/spec.md
Normal 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
300
publish.py
Normal 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
30
publish.sh
Executable 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 "============================================"
|
||||
Reference in New Issue
Block a user