Compare commits
3 Commits
65c746c639
...
e67ec24dfd
| Author | SHA1 | Date | |
|---|---|---|---|
| e67ec24dfd | |||
| aa1f0a9e94 | |||
| a8af3cc6c4 |
39
README.md
39
README.md
@@ -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**(本文档):面向项目开发者
|
||||||
|
|||||||
58
SKILL.md
58
SKILL.md
@@ -2,29 +2,28 @@
|
|||||||
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。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 统一文档解析 Skill
|
# 统一文档解析 Skill
|
||||||
|
|
||||||
## Purpose
|
## 🔴 重要:执行路径优先级(必须遵守)
|
||||||
|
|
||||||
**第一步:获取执行建议**
|
### 执行路径选择(按优先级顺序)
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/lyxy_document_reader.py --advice <文件路径或URL>
|
|
||||||
```
|
|
||||||
|
|
||||||
这会输出准确的执行命令,包含所需的依赖配置。
|
|
||||||
|
|
||||||
**执行路径优先级**
|
|
||||||
1. **lyxy-runner-python skill(首选)** - 自动管理依赖
|
1. **lyxy-runner-python skill(首选)** - 自动管理依赖
|
||||||
2. **uv run --with** - 按需加载依赖
|
2. **uv run --with** - 按需加载依赖
|
||||||
3. **主机 Python + pip install** - 手动安装依赖
|
3. **主机 Python + pip install** - 手动安装依赖
|
||||||
|
|
||||||
|
### 第一步:获取执行建议
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py --advice <文件路径或URL>
|
||||||
|
```
|
||||||
|
这会输出准确的执行命令,包含所需的依赖配置。
|
||||||
|
|
||||||
|
*也可以使用:`python scripts/lyxy_document_reader.py --advice <文件路径或URL>`*
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
**支持格式**
|
**支持格式**
|
||||||
- DOCX(Word 文档)
|
- DOCX(Word 文档)
|
||||||
- XLSX(Excel 表格)
|
- XLSX(Excel 表格)
|
||||||
@@ -60,49 +59,36 @@ python scripts/lyxy_document_reader.py --advice <文件路径或URL>
|
|||||||
| `-s <pattern>` | 正则表达式搜索 |
|
| `-s <pattern>` | 正则表达式搜索 |
|
||||||
| `-n <num>/--context <num>` | 与 `-s` 配合,指定上下文行数(默认 2) |
|
| `-n <num>/--context <num>` | 与 `-s` 配合,指定上下文行数(默认 2) |
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. **获取执行建议**
|
|
||||||
```bash
|
|
||||||
python scripts/lyxy_document_reader.py --advice <文件>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **选择执行方式**
|
|
||||||
- 优先使用 lyxy-runner-python skill
|
|
||||||
- 其次使用建议中的 uv 命令
|
|
||||||
- 最后使用建议中的 pip + python 命令
|
|
||||||
|
|
||||||
3. **添加需要的参数**
|
|
||||||
- 如 `-c`、`-t`、`-s` 等
|
|
||||||
|
|
||||||
## 参数使用示例
|
## 参数使用示例
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 获取执行建议
|
# 获取执行建议
|
||||||
python scripts/lyxy_document_reader.py --advice document.docx
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py --advice document.docx
|
||||||
|
|
||||||
# 读取全文
|
# 读取全文
|
||||||
python scripts/lyxy_document_reader.py document.docx
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx
|
||||||
|
|
||||||
# 统计字数
|
# 统计字数
|
||||||
python scripts/lyxy_document_reader.py document.docx -c
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -c
|
||||||
|
|
||||||
# 提取标题
|
# 提取标题
|
||||||
python scripts/lyxy_document_reader.py document.docx -t
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -t
|
||||||
|
|
||||||
# 提取指定章节
|
# 提取指定章节
|
||||||
python scripts/lyxy_document_reader.py document.docx -tc "第三章"
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -tc "第三章"
|
||||||
|
|
||||||
# 搜索内容
|
# 搜索内容
|
||||||
python scripts/lyxy_document_reader.py document.docx -s "关键词"
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -s "关键词"
|
||||||
|
|
||||||
# 正则搜索
|
# 正则搜索
|
||||||
python scripts/lyxy_document_reader.py document.docx -s "\d{4}-\d{2}-\d{2}"
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -s "\d{4}-\d{2}-\d{2}"
|
||||||
|
|
||||||
# 指定搜索上下文行数
|
# 指定搜索上下文行数
|
||||||
python scripts/lyxy_document_reader.py document.docx -s "关键词" -n 5
|
PYTHONPATH=. uv run --with pyarmor python scripts/lyxy_document_reader.py document.docx -s "关键词" -n 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*也可以使用纯 python 命令:`python scripts/lyxy_document_reader.py ...`*
|
||||||
|
|
||||||
## 错误处理
|
## 错误处理
|
||||||
|
|
||||||
| 错误 | 原因 | 解决 |
|
| 错误 | 原因 | 解决 |
|
||||||
|
|||||||
257
build.py
257
build.py
@@ -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 rel_path == ".":
|
|
||||||
target_root = target_dir
|
|
||||||
else:
|
|
||||||
target_root = os.path.join(target_dir, rel_path)
|
|
||||||
|
|
||||||
# 检查此目录下是否有 .py 文件需要复制
|
if frontmatter_start >= 0 and frontmatter_end > frontmatter_start:
|
||||||
has_py_files = any(file.endswith(".py") for file in files)
|
# 有 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 文件需要复制时才创建目录并复制
|
new_content = "\n".join(result_lines)
|
||||||
if has_py_files:
|
|
||||||
if not os.path.exists(target_root):
|
|
||||||
os.makedirs(target_root)
|
|
||||||
|
|
||||||
# 只复制 .py 文件
|
with open(target_path, "w", encoding="utf-8") as f:
|
||||||
for file in files:
|
f.write(new_content)
|
||||||
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:
|
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,40 +295,37 @@ 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}")
|
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
|
||||||
else:
|
|
||||||
print("混淆模式: 已生成 .pyx 和 pyarmor_runtime")
|
|
||||||
print(f"输出目录: {build_dir}")
|
print(f"输出目录: {build_dir}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
|||||||
@@ -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** 系统依次执行混淆构建和发布
|
||||||
|
|||||||
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 "============================================"
|
||||||
@@ -106,11 +106,14 @@ def generate_uv_command(
|
|||||||
Returns:
|
Returns:
|
||||||
uv run 命令字符串
|
uv run 命令字符串
|
||||||
"""
|
"""
|
||||||
parts = ["uv run"]
|
parts = ["PYTHONPATH=. uv run"]
|
||||||
|
|
||||||
if python_version:
|
if python_version:
|
||||||
parts.append(f"--python {python_version}")
|
parts.append(f"--python {python_version}")
|
||||||
|
|
||||||
|
# 始终添加 pyarmor 依赖(混淆后脚本需要)
|
||||||
|
parts.append("--with pyarmor")
|
||||||
|
|
||||||
for dep in dependencies:
|
for dep in dependencies:
|
||||||
# 处理包含空格的依赖(如 unstructured[pdf]),需要加引号
|
# 处理包含空格的依赖(如 unstructured[pdf]),需要加引号
|
||||||
if "[" in dep or " " in dep:
|
if "[" in dep or " " in dep:
|
||||||
@@ -141,8 +144,8 @@ def generate_python_command(
|
|||||||
"""
|
"""
|
||||||
python_cmd = f"python {script_path} {input_path}"
|
python_cmd = f"python {script_path} {input_path}"
|
||||||
|
|
||||||
# 构建 pip install 命令,处理带引号的依赖
|
# 构建 pip install 命令,处理带引号的依赖,始终包含 pyarmor
|
||||||
pip_parts = ["pip install"]
|
pip_parts = ["pip install", "pyarmor"]
|
||||||
for dep in dependencies:
|
for dep in dependencies:
|
||||||
pip_parts.append(dep)
|
pip_parts.append(dep)
|
||||||
pip_cmd = " ".join(pip_parts)
|
pip_cmd = " ".join(pip_parts)
|
||||||
|
|||||||
Reference in New Issue
Block a user