Vibe coding :P

This commit is contained in:
2025-08-20 21:59:11 +02:00
parent 7ef1502249
commit 28685780f3
2 changed files with 344 additions and 183 deletions
+196 -58
View File
@@ -1,22 +1,47 @@
# latex_commands.py
# -*- coding: utf-8 -*-
from __future__ import annotations
import io
from typing import Optional, List
import logging
from typing import Optional
import discord
from discord import app_commands
from discord.ext import commands
from constants import (
LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, LATEX_MAX_ATTACH_MB, LATEX_MAX_ZIP_MB,
OPENAI_MODEL, ALLOWED_ROLES, GUILD_ID
ALLOWED_ROLES,
GUILD_ID,
LATEX_MAX_ATTACH_MB,
LATEX_MAX_COMPILE_SECONDS,
LATEX_MAX_ZIP_MB,
LATEX_TEX_ENGINE,
OPENAI_MODEL,
)
from latex_functions import (
is_tex_attachment, compile_single_tex_bytes, compile_zip_to_zip, ask_openai_diagnosis
compile_single_tex_bytes,
compile_zip_to_zip,
is_safe_asset_name,
is_safe_tex_name,
)
async def _collect_attachments_bytes(attachments, max_mb: int):
"""
Zwraca słownik {filename: bytes} dla wszystkich załączników, do limitu MB.
Nie podbija wyjątków z .read() — zostawiamy to wyżej; tu zwracamy, co się udało.
"""
out = {}
for a in attachments or []:
# limit rozmiaru
if (getattr(a, "size", 0) or 0) > max_mb * 1024 * 1024:
continue
data = await a.read()
out[a.filename] = data
return out
class LatexModule(commands.Cog):
def __init__(self, bot: commands.Bot, logger_name: str):
self.bot = bot
@@ -31,47 +56,94 @@ class LatexModule(commands.Cog):
guild=None if GUILD_ID is None else discord.Object(id=GUILD_ID),
)
@commands.has_any_role(*ALLOWED_ROLES)
async def tex(self, ctx: commands.Context, include_source_excerpt: Optional[bool] = True):
async def tex(
self, ctx: commands.Context, include_source_excerpt: Optional[bool] = True
):
self.logger.info("LaTeX command invoked by %s", ctx.author)
ch = ctx.message.channel
# ZBIERZ WSZYSTKIE ZAŁĄCZNIKI
async with ch.typing():
atts = [a for a in (ctx.message.attachments or []) if is_tex_attachment(a, LATEX_MAX_ATTACH_MB)]
if not atts:
return await ctx.reply(f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False)
self.logger.debug("LaTeX attachments: %s", ", ".join(a.filename for a in atts))
files: List[discord.File] = []
parts: List[str] = []
for att in atts:
all_bytes = await _collect_attachments_bytes(
ctx.message.attachments, LATEX_MAX_ATTACH_MB
)
if not all_bytes:
return await ctx.reply(
f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False
)
# PODZIEL NA GŁÓWNE .TEX i WSPIERAJĄCE
tex_names = [
n
for n in all_bytes.keys()
if n.lower().endswith(".tex") and is_safe_tex_name(n)
]
if not tex_names:
return await ctx.reply(
f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False
)
support_names = [
n
for n in all_bytes.keys()
if n not in tex_names and is_safe_asset_name(n)
]
files_to_send = []
parts = []
for tex_name in tex_names:
try:
data = await att.read()
self.logger.info("LaTeX read %s bytes from %s", len(data), att.filename)
res = await compile_single_tex_bytes(data, att.filename, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name)
self.logger.info("LaTeX result for %s: %s", att.filename, res)
self.logger.info("LaTeX log (%s)\n%s", att.filename, (res["log_text"] or "")[-2000:])
main_bytes = all_bytes[tex_name]
support = [
(n, all_bytes[n])
for n in support_names
if n != tex_name and is_safe_asset_name(n)
]
# (ważne) do supportów dopuszczamy również **inne .tex** dla \input/\include
other_tex = [
(n, all_bytes[n])
for n in tex_names
if n != tex_name and is_safe_tex_name(n)
]
support.extend(other_tex)
res = await compile_single_tex_bytes(
main_bytes,
tex_name,
LATEX_TEX_ENGINE,
LATEX_MAX_COMPILE_SECONDS,
logger="discord",
support_files=support,
)
# Twoja dotychczasowa obsługa success/fail przykładowo:
if res["ok"] and res["pdf_bytes"]:
b = io.BytesIO(res["pdf_bytes"])
b.seek(0)
b.name = res["pdf_name"]
files.append(discord.File(b, filename=res["pdf_name"]))
parts.append(f"✅ `{att.filename}` → `{res['pdf_name']}`")
files_to_send.append(discord.File(b, filename=res["pdf_name"]))
parts.append(f"✅ `{tex_name}` → `{res['pdf_name']}`")
else:
self.logger.info("LaTeX compile failed for %s", att.filename)
excerpt = ""
if include_source_excerpt:
try:
excerpt = data.decode("utf-8", errors="ignore")[:4000]
except Exception:
excerpt = ""
advice = await ask_openai_diagnosis(res["log_text"] or "", excerpt, OPENAI_MODEL, logger=self.logger_name)
parts.append(f"❌ `{att.filename}` — błąd kompilacji.\nDiagnoza:\n{advice}")
raise
except Exception as e:
self.logger.info("Exception %s: %s", att.filename, e)
parts.append(f"⚠️ `{att.filename}` — wyjątek (szczegóły w logu).")
parts.append(
f"❌ `{tex_name}` — błąd kompilacji. (log: { (res['log_text'] or '')[-400] })"
)
# Tutaj wywołujesz swoją ścieżkę do OpenAI diagnozy, którą ogarnęliśmy wcześniej.
except Exception:
# let it crash — pełny stack w logu i re-raise (Discord.py to pokaże sensownie)
logging.getLogger("discord").exception(
"Unhandled exception while compiling %s", tex_name
)
raise
content = f"**LaTeX** — {len(atts)} plik(ów). Model: `{OPENAI_MODEL}`\n\n" + "\n\n".join(parts)
await ctx.reply(content if len(content)<1900 else content[:1900]+"", files=files, mention_author=False)
content = (
f"**LaTeX** — {len(tex_names)} plik(ów). Model: `{OPENAI_MODEL}`\n\n"
+ "\n\n".join(parts)
)
await ctx.reply(
content if len(content) < 1900 else content[:1900] + "",
files=parts,
mention_author=False,
)
# -------- /latexclean (PDF only; log -> debug logger) --------
@commands.hybrid_command(
@@ -84,57 +156,123 @@ class LatexModule(commands.Cog):
async def latexclean(self, ctx: commands.Context):
ch = ctx.message.channel
async with ch.typing():
atts = [a for a in (ctx.message.attachments or []) if is_tex_attachment(a, LATEX_MAX_ATTACH_MB)]
if not atts:
return await ctx.reply(f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False)
all_bytes = await _collect_attachments_bytes(
ctx.message.attachments, LATEX_MAX_ATTACH_MB
)
if not all_bytes:
return await ctx.reply(
f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False
)
files: List[discord.File] = []
parts: List[str] = []
for att in atts:
# PODZIEL NA GŁÓWNE .TEX i WSPIERAJĄCE
tex_names = [
n
for n in all_bytes.keys()
if n.lower().endswith(".tex") and is_safe_tex_name(n)
]
if not tex_names:
return await ctx.reply(
f"Dołącz .tex (≤{LATEX_MAX_ATTACH_MB} MiB).", mention_author=False
)
support_names = [
n
for n in all_bytes.keys()
if n not in tex_names and is_safe_asset_name(n)
]
files_to_send = []
parts = []
for tex_name in tex_names:
try:
data = await att.read()
res = await compile_single_tex_bytes(data, att.filename, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name)
self.logger.debug("LaTeX CLEAN (%s)\n%s", att.filename, (res["log_text"] or "")[-2000:])
main_bytes = all_bytes[tex_name]
support = [
(n, all_bytes[n])
for n in support_names
if n != tex_name and is_safe_asset_name(n)
]
# (ważne) do supportów dopuszczamy również **inne .tex** dla \input/\include
other_tex = [
(n, all_bytes[n])
for n in tex_names
if n != tex_name and is_safe_tex_name(n)
]
support.extend(other_tex)
res = await compile_single_tex_bytes(
main_bytes,
tex_name,
LATEX_TEX_ENGINE,
LATEX_MAX_COMPILE_SECONDS,
logger="discord",
support_files=support,
)
# Twoja dotychczasowa obsługa success/fail przykładowo:
if res["ok"] and res["pdf_bytes"]:
b = io.BytesIO(res["pdf_bytes"])
b.seek(0)
b.name = res["pdf_name"]
files.append(discord.File(b, filename=res["pdf_name"]))
parts.append(f"✅ `{att.filename}`")
files_to_send.append(discord.File(b, filename=res["pdf_name"]))
parts.append(f"✅ `{tex_name}` → `{res['pdf_name']}`")
else:
parts.append(f"❌ `{att.filename}` — błąd (log w debug).")
except Exception as e:
self.logger.debug("CLEAN exception %s: %s", att.filename, e)
parts.append(f"⚠️ `{att.filename}` — wyjątek (log w debug).")
parts.append(
f"❌ `{tex_name}` — błąd kompilacji. (log: { (res['log_text'] or '')[-400] })"
)
# Tutaj wywołujesz swoją ścieżkę do OpenAI diagnozy, którą ogarnęliśmy wcześniej.
except Exception:
# let it crash — pełny stack w logu i re-raise (Discord.py to pokaże sensownie)
logging.getLogger("discord").exception(
"Unhandled exception while compiling %s", tex_name
)
raise
await ctx.reply("**LaTeX clean** — " + ", ".join(parts), files=files, mention_author=False)
await ctx.reply(
"**LaTeX clean** — " + ", ".join(parts),
files=tex_names,
mention_author=False,
)
# -------- /texbatch (ZIP -> ZIP) --------
@app_commands.command(
name="texbatch",
description="Wyślij ZIP z .tex → odeślę ZIP z PDF-ami (tylko udane)."
description="Wyślij ZIP z .tex → odeślę ZIP z PDF-ami (tylko udane).",
)
@app_commands.describe(archive=f"Archiwum .zip (max ~{LATEX_MAX_ZIP_MB} MiB)")
@app_commands.checks.has_any_role(*ALLOWED_ROLES)
async def texbatch(self, interaction: discord.Interaction, archive: discord.Attachment):
async def texbatch(
self, interaction: discord.Interaction, archive: discord.Attachment
):
await interaction.response.defer()
if not archive or not archive.filename.lower().endswith(".zip"):
return await interaction.followup.send("Dołącz ZIP (.zip) z plikami .tex.", ephemeral=True)
return await interaction.followup.send(
"Dołącz ZIP (.zip) z plikami .tex.", ephemeral=True
)
if archive.size > LATEX_MAX_ZIP_MB * 1024 * 1024:
return await interaction.followup.send(f"ZIP > {LATEX_MAX_ZIP_MB} MiB — za duży.", ephemeral=True)
return await interaction.followup.send(
f"ZIP > {LATEX_MAX_ZIP_MB} MiB — za duży.", ephemeral=True
)
data = await archive.read()
out_zip_bytes, failed = await compile_zip_to_zip(data, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name)
out_zip_bytes, failed = await compile_zip_to_zip(
data, LATEX_TEX_ENGINE, LATEX_MAX_COMPILE_SECONDS, logger=self.logger_name
)
if not out_zip_bytes and failed:
return await interaction.followup.send("Brak PDF-ów. " + "; ".join(failed[:10]), ephemeral=True)
return await interaction.followup.send(
"Brak PDF-ów. " + "; ".join(failed[:10]), ephemeral=True
)
bio = io.BytesIO(out_zip_bytes)
bio.seek(0)
bio.name = "compiled_pdfs.zip"
if failed:
self.logger.debug("BATCH failed: %s", ", ".join(failed))
await interaction.followup.send("✅ Gotowe (ZIP w załączniku)." + (f" ❌ Błędy: {len(failed)}" if failed else ""), files=[discord.File(bio, filename=bio.name)])
await interaction.followup.send(
"✅ Gotowe (ZIP w załączniku)."
+ (f" ❌ Błędy: {len(failed)}" if failed else ""),
files=[discord.File(bio, filename=bio.name)],
)
async def setup(bot):
logger = logging.getLogger("discord")