диздоки БС, БСС, БС контейнеров #23
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Universal Lore Checker | |
on: | |
pull_request_target: | |
paths: | |
- '01_Вселенная/**' | |
- '02_Объекты/**' | |
- '03_Государства/**' | |
- '04_Корпорации/**' | |
- '05_Организации/**' | |
- '06_Расы/**' | |
- '07_Существа/**' | |
- '08_Технологии/**' | |
- '09_Вооружение/**' | |
- '10_Явления/**' | |
- '11_Товары/**' | |
- '12_Другое/**' | |
- '13_Истории/**' | |
- '14_Повседневность/**' | |
jobs: | |
universal_lore_checker: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v3 | |
with: | |
fetch-depth: 0 | |
- name: Fetch PR head branch | |
run: | | |
git remote add pr "${{ github.event.pull_request.head.repo.clone_url }}" | |
git fetch pr "${{ github.event.pull_request.head.ref }}" | |
- name: Set up Python | |
uses: actions/setup-python@v4 | |
with: | |
python-version: '3.9' | |
- name: Install dependencies | |
run: | | |
pip install --upgrade pip | |
pip cache purge | |
pip install google-generativeai==0.4.0 | |
pip install requests | |
pip install google-ai-generativelanguage==0.4.0 | |
pip install google-auth>=2.15.0 | |
pip install google-api-core | |
pip install protobuf | |
pip install pydantic | |
pip install tqdm | |
pip install typing-extensions | |
pip install "proto-plus>=1.22.3" | |
pip install "googleapis-common-protos>=1.56.2" | |
pip install requests>=2.18.0 | |
pip install cachetools>=2.0.0 | |
pip install pyasn1-modules>=0.2.1 | |
pip install rsa>=3.1.4 | |
pip install annotated-types>=0.6.0 | |
pip install pydantic-core | |
pip install grpcio>=1.33.2 | |
pip install grpcio-status>=1.33.2 | |
pip install pyasn1>=0.4.6 | |
- name: Analyze Markdown Changes and Compare with Templates, Rules, and other articles | |
env: | |
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
GITHUB_REPOSITORY: ${{ github.repository }} | |
BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
run: | | |
python <<'EOF' | |
import os, subprocess, requests, re | |
import google.generativeai as genai | |
# Настройка модели Gemini | |
genai.configure(api_key=os.getenv("GEMINI_API_KEY")) | |
model = genai.GenerativeModel("gemini-2.0-flash") | |
def get_changed_files(): | |
""" | |
Вычисляет общий предок между базовой веткой и веткой PR, | |
а затем получает список изменённых файлов с указанием статуса (A/M) через git diff. | |
Возвращает список кортежей (status, filename) для файлов с расширением .md. | |
""" | |
base_sha = os.getenv("BASE_SHA") | |
head_ref = os.getenv("HEAD_REF") | |
head_sha = subprocess.check_output(["git", "rev-parse", f"pr/{head_ref}"]).strip().decode() | |
merge_base = subprocess.check_output(["git", "merge-base", base_sha, head_sha]).strip().decode() | |
diff_output = subprocess.check_output( | |
["git", "diff", "--name-status", "--diff-filter=ACMRT", merge_base, head_sha] | |
).decode().splitlines() | |
changed = [] | |
for line in diff_output: | |
parts = line.split() | |
if len(parts) >= 2 and parts[1].endswith(".md"): | |
status, filename = parts[0], parts[1] | |
changed.append((status, filename)) | |
return changed | |
def get_file_content(filepath): | |
""" | |
Получает содержимое файла из ветки PR с помощью git show. | |
""" | |
head_ref = os.getenv("HEAD_REF") | |
try: | |
result = subprocess.run( | |
["git", "show", f"pr/{head_ref}:{filepath}"], | |
capture_output=True, text=True, check=True | |
) | |
return result.stdout | |
except Exception as e: | |
print(f"Error getting content for {filepath}: {e}") | |
return None | |
def create_comment(body): | |
repo = os.getenv("GITHUB_REPOSITORY") | |
pr_number = os.getenv("GITHUB_REF").split('/')[-2] | |
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" | |
headers = { | |
"Authorization": f"Bearer {os.getenv('GITHUB_TOKEN')}", | |
"Accept": "application/vnd.github+json", | |
} | |
payload = {"body": body} | |
response = requests.post(url, json=payload, headers=headers) | |
if response.status_code != 201: | |
print(f"Failed to post comment: {response.status_code}, {response.text}") | |
else: | |
print("Comment posted successfully") | |
def get_lore_files(root_dir, exclude_dirs): | |
lore_files = {} | |
for root, _, files in os.walk(root_dir): | |
if any(ex_dir in root for ex_dir in exclude_dirs): | |
continue | |
for file in files: | |
if file.endswith('.md') and file not in ["README.md", "TEMPLATE.md", "design_document.md"]: | |
full_path = os.path.join(root, file) | |
try: | |
with open(full_path, 'r', encoding='utf-8') as f: | |
lore_files[full_path] = f.read() | |
except Exception as e: | |
print(f"Error reading {full_path}: {e}") | |
return lore_files | |
changed_files = get_changed_files() | |
for status, filepath in changed_files: | |
comments = [] | |
dirname = os.path.dirname(filepath) | |
root_dirname = dirname.split(os.sep)[0] | |
file_name = os.path.basename(filepath) | |
# Если файл design_document.md обрабатывается отдельно | |
if file_name == "design_document.md": | |
if status == 'A': | |
try: | |
file_content = get_file_content(filepath) | |
rules_path = "00_Правила_оформления/Общие_правила.md" | |
with open(rules_path, 'r', encoding='utf-8') as r: | |
rules_content = r.read() | |
prompt_rules = f""" | |
Проанализируй следующий текст на соответствие общим правилам оформления. | |
Укажи, чего не хватает в тексте по сравнению с правилами. | |
Сформулируй рекомендации на русском языке. | |
Текст файла: | |
{file_content} | |
Общие правила: | |
{rules_content} | |
""" | |
response_rules = model.generate_content(prompt_rules) | |
recommendations_rules_text = (response_rules.candidates[0].content.parts[0].text.strip() | |
if response_rules.candidates and response_rules.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Проверка design_document.md на соответствие общим правилам</summary>\n\n{recommendations_rules_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка проверки design_document.md</summary>\n\n{e}\n</details>") | |
if comments: | |
comment_body = f"### Анализ файла `{file_name}` в папке `{root_dirname}`:\n\n" + "\n".join(comments) | |
create_comment(comment_body) | |
continue | |
# 0. Краткий пересказ файла | |
try: | |
file_content = get_file_content(filepath) | |
summary = model.generate_content( | |
f"Опиши содержание следующего текста на русском языке, укажи все нелогичные моменты и слабые места:\n\n{file_content}" | |
) | |
summary_text = (summary.candidates[0].content.parts[0].text.strip() | |
if summary.candidates and summary.candidates[0].content.parts | |
else "Нет описания.") | |
comments.append(f"<details><summary>Краткий пересказ для `{filepath}`</summary>\n\n{summary_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка краткого пересказа для `{filepath}`</summary>\n\n{e}</details>") | |
# 1. Теги для новых файлов (status 'A') | |
if status == 'A': | |
try: | |
file_content = get_file_content(filepath) | |
tags_response = model.generate_content( | |
f"На основе содержания, предложи 3-5 ключевых тега на русском языке в формате '#тег'. Только теги:\n\n{file_content}" | |
) | |
tags_text = (tags_response.candidates[0].content.parts[0].text.strip() | |
if tags_response.candidates and tags_response.candidates[0].content.parts | |
else "Не удалось получить теги.") | |
comments.append(f"<details><summary>Теги для `{filepath}`</summary>\n\n{tags_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка получения тегов для `{filepath}`</summary>\n\n{e}</details>") | |
# 2. Сравнение с design_document.md (если он существует в той же папке) | |
design_doc_path = os.path.join(dirname, "design_document.md") | |
if os.path.exists(design_doc_path): | |
try: | |
file_content = get_file_content(filepath) | |
result = subprocess.run(["git", "show", f"HEAD~1:{filepath}"], capture_output=True, text=True) | |
old_text = result.stdout if result.returncode == 0 else "" | |
with open(design_doc_path, 'r', encoding='utf-8') as t: | |
design_doc_content = t.read() | |
prompt_design_doc = f""" | |
Проанализируй следующий текст на соответствие дизайн-документу. | |
Укажи, чего не хватает в тексте по сравнению с дизайн-документом. | |
Текст файла: | |
{file_content} | |
Текст дизайн-документа: | |
{design_doc_content} | |
""" | |
response_design_doc = model.generate_content(prompt_design_doc) | |
recommendations_design_doc_text = (response_design_doc.candidates[0].content.parts[0].text.strip() | |
if response_design_doc.candidates and response_design_doc.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Сравнение дизайн-документа для `{filepath}`</summary>\n\n{recommendations_design_doc_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка сравнения с дизайн-документом для `{filepath}`</summary>\n\n{e}</details>") | |
else: | |
comments.append(f"<details><summary>Сравнение дизайн-документа для `{filepath}`</summary>\n\nФайл design_document.md не найден.\n</details>") | |
# 3. Сравнение с README.md и TEMPLATE.md из категории | |
template_path = os.path.join(root_dirname, "TEMPLATE.md") | |
readme_path = os.path.join(root_dirname, "README.md") | |
try: | |
file_content = get_file_content(filepath) | |
if os.path.exists(template_path): | |
with open(template_path, 'r', encoding='utf-8') as t: | |
template_content = t.read() | |
else: | |
template_content = "Шаблон не найден." | |
if os.path.exists(readme_path): | |
with open(readme_path, 'r', encoding='utf-8') as r: | |
readme_content = r.read() | |
else: | |
readme_content = "README не найден." | |
prompt_template_readme = f""" | |
Проанализируй следующий текст на соответствие шаблону и README из той же папки. | |
Укажи, чего не хватает в тексте по сравнению с шаблоном и README. | |
Текст файла: | |
{file_content} | |
Текст шаблона: | |
{template_content} | |
Текст README: | |
{readme_content} | |
""" | |
response_template_readme = model.generate_content(prompt_template_readme) | |
recommendations_template_readme_text = (response_template_readme.candidates[0].content.parts[0].text.strip() | |
if response_template_readme.candidates and response_template_readme.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Проверка соответствия условиям категории для `{filepath}`</summary>\n\n{recommendations_template_readme_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка проверки соответствия условиям категории для `{filepath}`</summary>\n\n{e}</details>") | |
# 4. Сравнение с Общими_правилами.md | |
rules_path = "00_Правила_оформления/Общие_правила.md" | |
try: | |
file_content = get_file_content(filepath) | |
with open(rules_path, 'r', encoding='utf-8') as r: | |
rules_content = r.read() | |
prompt_rules = f""" | |
Проанализируй следующий текст на соответствие общим правилам оформления. | |
Укажи, чего не хватает в тексте по сравнению с правилами. | |
Текст файла: | |
{file_content} | |
Общие правила: | |
{rules_content} | |
""" | |
response_rules = model.generate_content(prompt_rules) | |
recommendations_rules_text = (response_rules.candidates[0].content.parts[0].text.strip() | |
if response_rules.candidates and response_rules.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Проверка на соответствие общим правилам для `{filepath}`</summary>\n\n{recommendations_rules_text}\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка проверки на соответствие общим правилам для `{filepath}`</summary>\n\n{e}</details>") | |
# 5. Общее сравнение с другими статьями (все лор-файлы) | |
exclude_dirs = ['00_Правила_оформления', '01_Вселенная/Хронология'] | |
all_lore_files = get_lore_files('01_Вселенная', exclude_dirs) | |
all_lore_files.update(get_lore_files('02_Объекты', exclude_dirs)) | |
all_lore_files.update(get_lore_files('03_Государства', exclude_dirs)) | |
all_lore_files.update(get_lore_files('04_Корпорации', exclude_dirs)) | |
all_lore_files.update(get_lore_files('05_Организации', exclude_dirs)) | |
all_lore_files.update(get_lore_files('06_Расы', exclude_dirs)) | |
all_lore_files.update(get_lore_files('07_Существа', exclude_dirs)) | |
all_lore_files.update(get_lore_files('08_Технологии', exclude_dirs)) | |
all_lore_files.update(get_lore_files('09_Вооружение', exclude_dirs)) | |
all_lore_files.update(get_lore_files('10_Явления', exclude_dirs)) | |
all_lore_files.update(get_lore_files('11_Товары', exclude_dirs)) | |
all_lore_files.update(get_lore_files('12_Другое', exclude_dirs)) | |
all_lore_files.update(get_lore_files('13_Истории', exclude_dirs)) | |
all_lore_files.update(get_lore_files('14_Повседневность', exclude_dirs)) | |
try: | |
file_content = get_file_content(filepath) | |
context_content = "" | |
for path, content in all_lore_files.items(): | |
context_content += f"\nФайл: {path}\n{content}\n" | |
if context_content: | |
prompt_all_lore = f""" | |
Проанализируй текст в файле `{filepath}` на предмет неточностей и противоречий относительно контекста (содержимого остальных .md файлов, кроме README.md, TEMPLATE.md и design_document.md). | |
Текст файла: | |
{file_content} | |
Контекст: | |
{context_content} | |
""" | |
response_all_lore = model.generate_content(prompt_all_lore) | |
recommendations_all_lore_text = (response_all_lore.candidates[0].content.parts[0].text.strip() | |
if response_all_lore.candidates and response_all_lore.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Общее сравнение для `{filepath}`</summary>\n\n{recommendations_all_lore_text}\n</details>") | |
else: | |
comments.append(f"<details><summary>Общее сравнение для `{filepath}`</summary>\n\nНет других статей для сравнения.\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка общего сравнения для `{filepath}`</summary>\n\n{e}</details>") | |
# 6. Сравнение с другими файлами в той же категории | |
exclude_dirs = ['00_Правила_оформления'] | |
same_category_lore_files = get_lore_files(root_dirname, exclude_dirs) | |
try: | |
file_content = get_file_content(filepath) | |
context_category_content = "" | |
for path, content in same_category_lore_files.items(): | |
if path != filepath: | |
context_category_content += f"\nФайл: {path}\n{content}\n" | |
if context_category_content: | |
prompt_same_category = f""" | |
Проанализируй текст в файле `{filepath}` на предмет неточностей относительно контекста (остальные файлы в папке, кроме README.md, TEMPLATE.md и design_document.md). | |
Текст файла: | |
{file_content} | |
Контекст: | |
{context_category_content} | |
""" | |
response_same_category = model.generate_content(prompt_same_category) | |
recommendations_same_category_text = (response_same_category.candidates[0].content.parts[0].text.strip() | |
if response_same_category.candidates and response_same_category.candidates[0].content.parts | |
else "Не удалось получить рекомендации.") | |
comments.append(f"<details><summary>Сравнение по категории для `{filepath}`</summary>\n\n{recommendations_same_category_text}\n</details>") | |
else: | |
comments.append(f"<details><summary>Сравнение по категории для `{filepath}`</summary>\n\nНет других статей в этой категории.\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка сравнения по категории для `{filepath}`</summary>\n\n{e}</details>") | |
# 7. Сравнение с предыдущей версией (если статус 'M') | |
try: | |
if status == 'M': | |
result = subprocess.run(["git", "show", f"HEAD~1:{filepath}"], capture_output=True, text=True) | |
old_text = result.stdout | |
file_content = get_file_content(filepath) | |
diff_summary = model.generate_content( | |
f"Кратко опиши изменения между предыдущей и текущей версиями текста.\n\nСтарый текст:\n{old_text}\n\nНовый текст:\n{file_content}" | |
) | |
diff_summary_text = (diff_summary.candidates[0].content.parts[0].text.strip() | |
if diff_summary.candidates and diff_summary.candidates[0].content.parts | |
else "Нет описания изменений.") | |
comments.append(f"<details><summary>Изменения в статье для `{filepath}`</summary>\n\n**Описание изменений:**\n{diff_summary_text}\n</details>") | |
elif status == 'A': | |
comments.append(f"<details><summary>Изменения в статье для `{filepath}`</summary>\n\nФайл впервые создан.\n</details>") | |
except Exception as e: | |
comments.append(f"<details><summary>Ошибка сравнения с предыдущей версией для `{filepath}`</summary>\n\n{e}</details>") | |
if comments: | |
comment_body = f"### Анализ файла `{file_name}` в папке `{root_dirname}`:\n\n" + "\n".join(comments) | |
create_comment(comment_body) | |
EOF |