Skip to content

Commit 26d0a38

Browse files
author
2lipan
committed
完成合并
2 parents 4f1d6f2 + 267d262 commit 26d0a38

13 files changed

Lines changed: 562 additions & 5 deletions

File tree

.github/scripts/validate_pr.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import sys
2+
import os
3+
import yaml
4+
import re
5+
6+
VALID_PREFIXES = ['tool_', 'app_', 'db_', 'kbwf_']
7+
REQUIRED_YAML_FIELDS = ['name', 'tags', 'title', 'description']
8+
VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
9+
10+
# 每种前缀允许的文件扩展名
11+
PREFIX_EXT = {
12+
'app_': '.mk',
13+
'db_': '.tool',
14+
'tool_': '.tool',
15+
'kbwf_': '.kbwf',
16+
}
17+
18+
errors = []
19+
warnings = []
20+
21+
def check(cond, msg, is_warn=False):
22+
if not cond:
23+
(warnings if is_warn else errors).append(msg)
24+
25+
def get_valid_tags():
26+
root = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../..')
27+
yaml_path = os.path.join(root, 'tools/data.yaml')
28+
if not os.path.isfile(yaml_path):
29+
print(f"⚠️ 未找到 tools/data.yaml,跳过 tags 校验")
30+
return set()
31+
try:
32+
with open(yaml_path, encoding='utf-8') as f:
33+
data = yaml.safe_load(f)
34+
tags = data.get('additionalProperties', {}).get('tags', [])
35+
return {tag['name'] for tag in tags if 'name' in tag}
36+
except yaml.YAMLError as e:
37+
print(f"⚠️ tools/data.yaml 解析失败: {e}")
38+
return set()
39+
40+
def get_changed_tool_dirs(changed_files_path):
41+
dirs = set()
42+
with open(changed_files_path) as f:
43+
for line in f:
44+
parts = line.strip().split('/')
45+
if len(parts) >= 2 and parts[0] == 'tools':
46+
tool_dir = parts[1]
47+
if any(tool_dir.startswith(p) for p in VALID_PREFIXES):
48+
dirs.add(os.path.join('tools', tool_dir))
49+
return dirs
50+
51+
def get_prefix(tool_name):
52+
for p in VALID_PREFIXES:
53+
if tool_name.startswith(p):
54+
return p
55+
return None
56+
57+
def validate_tool_dir(tool_path, valid_tags):
58+
tool_name = os.path.basename(tool_path)
59+
print(f"\n🔍 校验: {tool_path}")
60+
61+
# 1. 目录前缀校验
62+
prefix = get_prefix(tool_name)
63+
check(prefix is not None,
64+
f"[{tool_name}] 目录名前缀无效,必须以 {VALID_PREFIXES} 之一开头")
65+
if prefix is None:
66+
return
67+
68+
# 2. 根目录必要文件校验
69+
for fname in ['data.yaml', 'logo.png', 'README.md']:
70+
check(
71+
os.path.isfile(os.path.join(tool_path, fname)),
72+
f"[{tool_name}] 缺少必要文件: {fname}"
73+
)
74+
75+
# 3. 至少一个版本目录
76+
if not os.path.isdir(tool_path):
77+
check(False, f"[{tool_name}] 工具目录不存在")
78+
return
79+
80+
version_dirs = [
81+
d for d in os.listdir(tool_path)
82+
if os.path.isdir(os.path.join(tool_path, d)) and VERSION_RE.match(d)
83+
]
84+
check(len(version_dirs) > 0, f"[{tool_name}] 缺少版本目录(如 1.0.0)")
85+
86+
# 4. 每个版本目录校验
87+
expected_ext = PREFIX_EXT[prefix]
88+
for version in version_dirs:
89+
version_path = os.path.join(tool_path, version)
90+
files_in_version = os.listdir(version_path)
91+
92+
# 4.1 只能有一个对应扩展名的文件
93+
matched = [f for f in files_in_version if f.endswith(expected_ext)]
94+
check(
95+
len(matched) == 1,
96+
f"[{tool_name}/{version}] 版本目录下应有且仅有 1 个 {expected_ext} 文件,"
97+
f"当前找到 {len(matched)} 个: {matched}"
98+
)
99+
100+
# 4.2 版本目录下不应有其他多余文件
101+
extra = [f for f in files_in_version if not f.endswith(expected_ext)]
102+
check(
103+
len(extra) == 0,
104+
f"[{tool_name}/{version}] 版本目录下存在多余文件: {extra}",
105+
is_warn=True
106+
)
107+
108+
# 5. data.yaml 内容校验
109+
yaml_path = os.path.join(tool_path, 'data.yaml')
110+
if os.path.isfile(yaml_path):
111+
try:
112+
with open(yaml_path, encoding='utf-8') as f:
113+
data = yaml.safe_load(f)
114+
for field in REQUIRED_YAML_FIELDS:
115+
check(
116+
field in data and data[field],
117+
f"[{tool_name}] data.yaml 缺少字段或为空: {field}"
118+
)
119+
if valid_tags and 'tags' in data and isinstance(data['tags'], list):
120+
for tag in data['tags']:
121+
check(
122+
tag in valid_tags,
123+
f"[{tool_name}] data.yaml 中 tag '{tag}' 未在 tools/data.yaml 中定义,"
124+
f"可选值: {sorted(valid_tags)}"
125+
)
126+
except yaml.YAMLError as e:
127+
errors.append(f"[{tool_name}] data.yaml 解析失败: {e}")
128+
129+
# 6. README.md 非空
130+
readme_path = os.path.join(tool_path, 'README.md')
131+
if os.path.isfile(readme_path):
132+
content = open(readme_path, encoding='utf-8').read().strip()
133+
check(len(content) > 50, f"[{tool_name}] README.md 内容过少,请补充工具说明")
134+
if tool_name.startswith('tool_') or tool_name.startswith('db_'):
135+
check(
136+
'参数' in content or 'parameter' in content.lower(),
137+
f"[{tool_name}] README.md 建议包含参数说明",
138+
is_warn=True
139+
)
140+
141+
# 7. logo.png 大小
142+
logo_path = os.path.join(tool_path, 'logo.png')
143+
if os.path.isfile(logo_path):
144+
size_kb = os.path.getsize(logo_path) / 1024
145+
check(size_kb <= 500,
146+
f"[{tool_name}] logo.png 过大 ({size_kb:.1f}KB),建议不超过 500KB",
147+
is_warn=True)
148+
149+
def main():
150+
changed_files_path = sys.argv[1]
151+
valid_tags = get_valid_tags()
152+
tool_dirs = get_changed_tool_dirs(changed_files_path)
153+
154+
if not tool_dirs:
155+
print("ℹ️ 本次 PR 未涉及 tools/ 目录下的工具,跳过校验。")
156+
sys.exit(0)
157+
158+
print(f"本次 PR 涉及 {len(tool_dirs)} 个工具目录:")
159+
for d in sorted(tool_dirs):
160+
validate_tool_dir(d, valid_tags)
161+
162+
print("\n" + "=" * 50)
163+
if warnings:
164+
print(f"⚠️ 警告 ({len(warnings)} 条):")
165+
for w in warnings:
166+
print(f" - {w}")
167+
168+
if errors:
169+
print(f"\n❌ 错误 ({len(errors)} 条):")
170+
for e in errors:
171+
print(f" - {e}")
172+
print("\n请修复以上问题后重新提交 PR。")
173+
sys.exit(1)
174+
else:
175+
print("✅ 所有校验通过!")
176+
sys.exit(0)
177+
178+
if __name__ == '__main__':
179+
main()

.github/workflows/pr-validate.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: PR Validation
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
paths:
7+
- 'tools/**'
8+
workflow_dispatch:
9+
inputs:
10+
pr_number:
11+
description: 'PR number to validate'
12+
required: true
13+
14+
jobs:
15+
validate:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
pull-requests: write
19+
contents: read
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Checkout PR branch
28+
if: github.event_name == 'workflow_dispatch'
29+
run: |
30+
cp .github/scripts/validate_pr.py /tmp/validate_pr.py
31+
gh pr checkout ${{ inputs.pr_number }}
32+
mkdir -p .github/scripts
33+
cp /tmp/validate_pr.py .github/scripts/validate_pr.py
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
37+
- name: Get changed files
38+
run: |
39+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40+
gh pr diff ${{ inputs.pr_number }} --name-only > changed_files.txt
41+
else
42+
git diff --name-only origin/main...HEAD > changed_files.txt
43+
fi
44+
env:
45+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: '3.11'
51+
52+
- name: Install dependencies
53+
run: pip install pyyaml
54+
55+
- name: Run validation
56+
id: validate
57+
working-directory: ${{ github.workspace }}
58+
run: |
59+
python .github/scripts/validate_pr.py changed_files.txt > result.txt 2>&1
60+
echo "exit_code=$?" >> $GITHUB_OUTPUT
61+
cat result.txt
62+
63+
- name: Comment on PR
64+
if: always() && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')
65+
uses: actions/github-script@v7
66+
with:
67+
script: |
68+
const fs = require('fs');
69+
const result = fs.readFileSync('result.txt', 'utf8');
70+
const exitCode = '${{ steps.validate.outputs.exit_code }}';
71+
const icon = exitCode === '0' ? '✅' : '❌';
72+
const status = exitCode === '0' ? '校验通过' : '校验失败,请修复后重新提交';
73+
const prNumber = context.eventName === 'workflow_dispatch'
74+
? parseInt('${{ inputs.pr_number }}')
75+
: context.payload.pull_request.number;
76+
github.rest.issues.createComment({
77+
issue_number: prNumber,
78+
owner: context.repo.owner,
79+
repo: context.repo.repo,
80+
body: `## ${icon} PR 自动校验结果\n\n**状态**: ${status}\n\n\`\`\`\n${result}\n\`\`\``
81+
});
82+
83+
- name: Fail if validation failed
84+
if: steps.validate.outputs.exit_code != '0'
85+
run: exit 1

tools/data.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ additionalProperties:
7474
zh-hant: "模型記憶"
7575
zh: "模型记忆"
7676
- key: skill
77-
name: 技能
77+
name: Skills
7878
sort: 110
7979
locales:
80-
en: "SKILL"
81-
zh-hant: "技能"
82-
zh: "技能"
80+
en: "Skills"
81+
zh-hant: "Skills"
82+
zh: "Skills"

tools/skill_mysql/data.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: MySQL数据库技能
22
tags:
3-
- 技能
3+
- Skills
44
title: MySQL数据库技能
55
description: MySQL数据库技能
8.79 KB
Binary file not shown.

0 commit comments

Comments
 (0)