APPdoc/Form1.cs

433 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.IO;
using System.IO.Compression; // Добавлено (или уже было) для ZipFile
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows.Forms;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Markdig;
using HtmlToOpenXml; // Для конвертера HTML в DOCX
using System.Collections.Generic; // Для IList<>
using DocumentFormat.OpenXml; // Для OpenXmlCompositeElement
using System.Diagnostics; // <--- ДОБАВЛЕНО для Debug.WriteLine
namespace GiteaDocGenerator
{
public partial class Form1 : Form
{
private string _tempDir = string.Empty; // Для хранения скачанного кода
public Form1()
{
InitializeComponent();
}
// --- Обработчики UI ---
private void btnSelectSample_Click(object sender, EventArgs e)
{
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Filter = "Word Documents (*.docx)|*.docx";
ofd.Title = "Выберите файл-пример DOCX";
if (ofd.ShowDialog() == DialogResult.OK)
{
txtSampleDocx.Text = ofd.FileName;
}
}
}
private async void btnGenerate_Click(object sender, EventArgs e)
{
string repoUrl = txtRepoUrl.Text.Trim();
string sampleDocxPath = txtSampleDocx.Text;
string apiKey = txtApiKey.Text.Trim(); // **НЕБЕЗОПАСНО!**
if (string.IsNullOrWhiteSpace(repoUrl) || !Uri.IsWellFormedUriString(repoUrl, UriKind.Absolute))
{
MessageBox.Show("Пожалуйста, введите корректный URL репозитория Gitea.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (string.IsNullOrWhiteSpace(sampleDocxPath) || !File.Exists(sampleDocxPath))
{
MessageBox.Show("Пожалуйста, выберите существующий файл-пример DOCX.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (string.IsNullOrWhiteSpace(apiKey))
{
MessageBox.Show("Пожалуйста, введите ваш Google AI API Key.", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// Блокируем UI на время работы
SetUiEnabled(false);
lblStatus.Text = "Статус: Скачивание репозитория...";
try
{
// 1. Скачивание и распаковка репозитория
_tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempDir);
string zipPath = await DownloadRepoAsync(repoUrl, _tempDir);
if (string.IsNullOrEmpty(zipPath)) return; // Ошибка уже показана в DownloadRepoAsync
lblStatus.Text = "Статус: Распаковка архива...";
string extractedPath = UnzipRepo(zipPath, _tempDir);
File.Delete(zipPath); // Удаляем zip после распаковки
// Находим папку с кодом внутри (обычно имя_репо-ветка)
string repoSourcePath = Directory.GetDirectories(extractedPath).FirstOrDefault();
if (repoSourcePath == null)
{
throw new DirectoryNotFoundException("Не удалось найти папку с исходным кодом после распаковки.");
}
// 2. Подготовка данных для ИИ
lblStatus.Text = "Статус: Чтение кода и структуры примера...";
string codeContent = ReadCodeFiles(repoSourcePath); // Читаем код
string sampleStructure = ExtractStructureFromDocx(sampleDocxPath); // Читаем структуру DOCX
if (string.IsNullOrWhiteSpace(codeContent))
{
MessageBox.Show("Не удалось прочитать файлы кода из репозитория. Возможно, репозиторий пуст или содержит неподдерживаемые файлы.", "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
// 3. Взаимодействие с Gemini API
lblStatus.Text = "Статус: Обращение к Google Gemini API...";
string markdownResult = await CallGeminiApiAsync(apiKey, sampleStructure, codeContent);
if (string.IsNullOrWhiteSpace(markdownResult))
{
lblStatus.Text = "Статус: Ошибка API.";
MessageBox.Show("Получен пустой ответ от Gemini API.", "Ошибка API", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 4. Создание DOCX из Markdown
lblStatus.Text = "Статус: Создание DOCX файла...";
string outputDocxPath = ShowSaveDialog();
if (!string.IsNullOrEmpty(outputDocxPath))
{
await CreateDocxFromMarkdown(markdownResult, outputDocxPath, sampleDocxPath);
lblStatus.Text = $"Статус: Готово! Файл сохранен в {outputDocxPath}";
MessageBox.Show($"Документация успешно сгенерирована и сохранена:\n{outputDocxPath}", "Успех", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
lblStatus.Text = "Статус: Генерация отменена.";
}
}
catch (Exception ex)
{
lblStatus.Text = "Статус: Ошибка!";
string errorMessage = $"Произошла ошибка: {ex.Message}";
if (ex.InnerException != null)
{
errorMessage += $"\n\nВнутренняя ошибка: {ex.InnerException.Message}\n\n{ex.InnerException.StackTrace}";
}
errorMessage += $"\n\nStackTrace:\n{ex.StackTrace}";
// Если в сообщении уже есть лог Markdown/HTML (добавленный в CreateDocxFromMarkdown),
// он будет включен здесь автоматически.
MessageBox.Show(errorMessage, "Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetUiEnabled(true);
CleanUpTempDir();
}
}
private void SetUiEnabled(bool enabled)
{
txtRepoUrl.Enabled = enabled;
txtSampleDocx.Enabled = enabled;
btnSelectSample.Enabled = enabled;
btnGenerate.Enabled = enabled;
txtApiKey.Enabled = enabled;
}
private void CleanUpTempDir()
{
if (!string.IsNullOrEmpty(_tempDir) && Directory.Exists(_tempDir))
{
try { Directory.Delete(_tempDir, true); }
catch (Exception ex) { Console.WriteLine($"Не удалось удалить временную папку {_tempDir}: {ex.Message}"); }
_tempDir = string.Empty;
}
}
// --- Логика работы с Gitea ---
private async Task<string> DownloadRepoAsync(string repoUrl, string downloadDir)
{
Uri repoUri = new Uri(repoUrl);
string zipUrlMaster = $"{repoUri.GetLeftPart(UriPartial.Path).TrimEnd('/')}/archive/master.zip";
string zipUrlMain = $"{repoUri.GetLeftPart(UriPartial.Path).TrimEnd('/')}/archive/main.zip";
string zipPath = Path.Combine(downloadDir, "repo.zip");
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "GiteaDocGeneratorApp");
HttpResponseMessage response = null;
try
{
response = await client.GetAsync(zipUrlMain);
if (!response.IsSuccessStatusCode)
{
lblStatus.Text = "Статус: Не удалось скачать main.zip, пробую master.zip...";
response.Dispose();
response = await client.GetAsync(zipUrlMaster);
}
response.EnsureSuccessStatusCode();
using (var fs = new FileStream(zipPath, FileMode.CreateNew))
{
await response.Content.CopyToAsync(fs);
}
return zipPath;
}
catch (HttpRequestException httpEx)
{
string message = $"Ошибка скачивания репозитория: {httpEx.Message}";
if (response?.StatusCode == System.Net.HttpStatusCode.NotFound)
{
message += "\nУбедитесь, что URL верный, репозиторий публичный и содержит ветку 'main' или 'master'.";
}
else if (response != null)
{
message += $"\nКод ответа: {response.StatusCode}";
}
MessageBox.Show(message, "Ошибка скачивания", MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
finally
{
response?.Dispose();
}
}
}
private string UnzipRepo(string zipPath, string extractPath)
{
ZipFile.ExtractToDirectory(zipPath, extractPath);
return extractPath;
}
// --- Подготовка данных ---
private string ReadCodeFiles(string sourcePath, int maxChars = 15000)
{
StringBuilder content = new StringBuilder();
string[] extensions = { ".cs", ".md", ".txt", ".py", ".js", ".html", ".css", ".java", ".go", ".php", "Dockerfile", ".yaml", ".yml", ".json" };
string basePathWithTrailingSlash = sourcePath.EndsWith(Path.DirectorySeparatorChar.ToString()) ? sourcePath : sourcePath + Path.DirectorySeparatorChar;
Uri sourceUri = new Uri(basePathWithTrailingSlash);
var readmeFiles = Directory.GetFiles(sourcePath, "README.md", SearchOption.AllDirectories).Concat(Directory.GetFiles(sourcePath, "README.txt", SearchOption.AllDirectories));
foreach (var file in readmeFiles)
{
if (content.Length >= maxChars) break;
try
{
Uri fileUri = new Uri(file);
string relativePath = Uri.UnescapeDataString(sourceUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar);
content.AppendLine($"--- Файл: {relativePath} ---");
content.AppendLine(File.ReadAllText(file));
content.AppendLine("--- Конец файла ---");
content.AppendLine();
}
catch (Exception ex) { Console.WriteLine($"Ошибка чтения файла {file}: {ex.Message}"); }
}
var codeFiles = Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories).Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()) && !Path.GetFileName(f).StartsWith("README.", StringComparison.OrdinalIgnoreCase));
foreach (var file in codeFiles)
{
if (content.Length >= maxChars) { content.AppendLine("\n[...Остальные файлы кода пропущены из-за ограничения размера...]"); break; }
try
{
string fileContent = File.ReadAllText(file);
if (!string.IsNullOrWhiteSpace(fileContent))
{
Uri fileUri = new Uri(file);
string relativePath = Uri.UnescapeDataString(sourceUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar);
content.AppendLine($"--- Файл: {relativePath} ---");
content.AppendLine(fileContent);
content.AppendLine("--- Конец файла ---");
content.AppendLine();
}
}
catch (Exception ex) { Console.WriteLine($"Ошибка чтения файла {file}: {ex.Message}"); }
}
return content.ToString();
}
private string ExtractStructureFromDocx(string docxPath)
{
StringBuilder structure = new StringBuilder();
try
{
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(docxPath, false))
{
Body body = wordDoc.MainDocumentPart.Document.Body;
foreach (var element in body.Elements<Paragraph>())
{
string styleId = element.ParagraphProperties?.ParagraphStyleId?.Val?.Value;
bool isHeading = styleId != null && styleId.StartsWith("Heading", StringComparison.OrdinalIgnoreCase) || styleId == "Заголовок";
if (isHeading) { structure.AppendLine($"{(isHeading ? "## " : "")}{element.InnerText}"); }
else if (!string.IsNullOrWhiteSpace(element.InnerText)) { if (structure.Length < 2000) { structure.AppendLine(element.InnerText); } }
}
if (structure.Length >= 2000) { structure.AppendLine("\n[...Структура примера обрезана...]"); }
}
}
catch (Exception ex)
{
MessageBox.Show($"Не удалось прочитать структуру из файла DOCX: {ex.Message}. Будет использован стандартный промпт.", "Ошибка чтения DOCX", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return "Заголовок 1\n\nВведение\n\nОсновная часть\n\nЗаключение";
}
if (string.IsNullOrWhiteSpace(structure.ToString())) { return "Заголовок 1\n\nВведение\n\nОсновная часть\n\nЗаключение"; }
return structure.ToString();
}
// --- Взаимодействие с Gemini API ---
private async Task<string> CallGeminiApiAsync(string apiKey, string sampleStructure, string codeContent)
{
string apiUrl = $"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key={apiKey}";
string prompt = $@"Сгенерируй документацию для программного продукта, исходный код которого приведен ниже.
Документация должна строго следовать структуре, представленной в следующем примере (используй заголовки и порядок разделов как в примере):
--- НАЧАЛО СТРУКТУРЫ ПРИМЕРА ---
{sampleStructure}
--- КОНЕЦ СТРУКТУРЫ ПРИМЕРА ---
Содержимое документации должно описывать код продукта. Если кода нет, напиши общую документацию по указанной структуре.
ВЕСЬ ТВОЙ ОТВЕТ ДОЛЖЕН БЫТЬ ТОЛЬКО В ФОРМАТЕ MARKDOWN. Не добавляй никаких пояснений до или после markdown.
--- НАЧАЛО КОДА ПРОДУКТА ---
{codeContent}
--- КОНЕЦ КОДА ПРОДУКТА ---
Еще раз: ВЕСЬ ТВОЙ ОТВЕТ должен быть строго в формате Markdown.";
var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = 0.6 } };
string jsonPayload = JsonSerializer.Serialize(requestBody);
using (HttpClient client = new HttpClient())
{
try
{
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(apiUrl, content);
string responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) { string errorDetails = TryExtractErrorFromJson(responseBody); throw new Exception($"Ошибка API Gemini: {response.StatusCode}. {errorDetails}"); }
using (JsonDocument document = JsonDocument.Parse(responseBody))
{
if (document.RootElement.TryGetProperty("candidates", out JsonElement candidates) && candidates.GetArrayLength() > 0)
{
if (candidates[0].TryGetProperty("content", out JsonElement responseContent) && responseContent.TryGetProperty("parts", out JsonElement parts) && parts.GetArrayLength() > 0)
{
if (parts[0].TryGetProperty("text", out JsonElement textElement)) { return textElement.GetString(); }
}
if (candidates[0].TryGetProperty("finishReason", out JsonElement reason))
{
string reasonStr = reason.GetString(); string safetyInfo = "";
if (reasonStr == "SAFETY" && candidates[0].TryGetProperty("safetyRatings", out JsonElement ratings)) { safetyInfo = $" Safety Ratings: {ratings.ToString()}"; }
throw new Exception($"Ответ API Gemini не содержит текст. Причина завершения: {reasonStr}.{safetyInfo}");
}
}
throw new Exception($"Не удалось извлечь текст из ответа API Gemini. Ответ:\n{responseBody}");
}
}
catch (JsonException jsonEx) { throw new Exception($"Ошибка парсинга JSON ответа от API Gemini: {jsonEx.Message}. Ответ:\n{jsonPayload}", jsonEx); }
catch (Exception ex) { throw new Exception($"Ошибка при вызове Gemini API: {ex.Message}", ex); }
}
}
private string TryExtractErrorFromJson(string jsonResponse)
{
if (string.IsNullOrWhiteSpace(jsonResponse)) return "(пустой ответ)";
try { using (JsonDocument document = JsonDocument.Parse(jsonResponse)) { if (document.RootElement.TryGetProperty("error", out JsonElement errorElement)) { if (errorElement.TryGetProperty("message", out JsonElement messageElement)) { return messageElement.GetString(); } return errorElement.ToString(); } } }
catch (JsonException) { }
return jsonResponse;
}
// --- Создание DOCX ---
private string ShowSaveDialog()
{
using (SaveFileDialog sfd = new SaveFileDialog())
{
sfd.Filter = "Word Document (*.docx)|*.docx"; sfd.Title = "Сохранить документацию как..."; sfd.FileName = "Generated_Documentation.docx";
if (sfd.ShowDialog() == DialogResult.OK) { return sfd.FileName; }
return null;
}
}
// Метод остается асинхронным (async Task)
// Метод остается асинхронным (async Task)
private async Task CreateDocxFromMarkdown(string markdown, string outputPath, string sampleDocxPath)
{
// --- Логирование Markdown ---
Debug.WriteLine("--- ИСХОДНЫЙ MARKDOWN ---");
Debug.WriteLine(markdown ?? "[NULL]");
Debug.WriteLine("--- КОНЕЦ MARKDOWN ---");
// ---------------------------
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
string html = "[NULL]";
if (markdown != null)
{
html = Markdown.ToHtml(markdown, pipeline);
}
// --- Логирование HTML ---
Debug.WriteLine("--- ПОЛУЧЕННЫЙ HTML ---");
Debug.WriteLine(html);
Debug.WriteLine("--- КОНЕЦ HTML ---");
// ------------------------
try
{
// Шаг 1: Клонирование шаблона. Шаблон будет закрыт сразу после клонирования.
using (WordprocessingDocument templateDoc = WordprocessingDocument.Open(sampleDocxPath, false))
{
// Клонируем шаблон в ВЫХОДНОЙ файл и открываем для редактирования
using (WordprocessingDocument wordDoc = (WordprocessingDocument)templateDoc.Clone(outputPath, true))
{
// Шаг 2: Работа с клонированным документом (wordDoc)
MainDocumentPart mainPart = wordDoc.MainDocumentPart;
Body body = mainPart.Document.Body;
// !!! ОЧИЩАЕМ ТЕЛО ДОКУМЕНТА !!!
body.RemoveAllChildren<Paragraph>();
body.RemoveAllChildren<Table>();
// Шаг 3: Используем HtmlConverter для преобразования HTML и добавления в пустое тело
HtmlConverter converter = new HtmlConverter(mainPart);
await converter.ParseHtml(html); // ParseHtml добавляет элементы в mainPart.Document.Body
// Шаг 4: Плейсхолдер НЕ НУЖЕН и его поиск/удаление НЕ НУЖНЫ
// Сохранение происходит автоматически при выходе из using (wordDoc)
} // wordDoc закрывается и сохраняется здесь
} // templateDoc закрывается здесь
}
catch (IOException ioEx)
{
string logInfo = $"\n\n--- Markdown Log ---\n{markdown ?? "[NULL]"}\n--- HTML Log ---\n{html}";
throw new IOException($"Ошибка доступа к файлу при клонировании/создании DOCX '{outputPath}': {ioEx.Message}. Убедитесь, что файл не открыт в другой программе.{logInfo}", ioEx);
}
catch (Exception ex)
{
// Убираем проверку на InvalidOperationException, так как плейсхолдера нет
string logInfo = $"\n\n--- Markdown Log ---\n{markdown ?? "[NULL]"}\n--- HTML Log ---\n{html}";
throw new Exception($"Ошибка при создании DOCX файла из Markdown/HTML:\n{ex.Message}{logInfo}", ex);
}
}
}
}