Files
aiData/WordAddIn/WordAddIn/AiService.cs
HuangHai d9bb1d1f83 'commit'
2026-01-26 09:15:45 +08:00

422 lines
21 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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WordAddIn.Models;
namespace WordAddIn
{
/// <summary>
/// AI 服务核心类。
/// 职责:
/// 1. 封装与后端大模型 API (如 Gemini, OpenAI) 的 HTTP 通信。
/// 2. 构造 System Prompt 和 User Prompt,处理上下文注入。
/// 3. 解析 API 返回的 JSON 数据,并处理异常。
/// 4. 提供具体的业务接口:扩写、润色、升华、大纲生成、结构分析、配图生成。
/// </summary>
public class AiService
{
// 兔子平台配置 (兼容 OpenAI 接口格式)
private const string ApiKey = "sk-FCwlaMANKdSlXlY7HkzZncSp0N5gecfpQdk0iR059Hfk4dQ1";
private const string BaseUrl = "https://api.tu-zi.com/v1";
// 模型配置
private const string TextModel = "gemini-3-flash-preview"; // 默认文本模型,速度快,成本低
private const string ImageModel = "gemini-3-pro-image-preview"; // 默认生图模型,支持高质量绘图
private static readonly HttpClient _httpClient;
static AiService()
{
// 关键设置:必须强制启用 TLS 1.2,否则在 .NET Framework 4.7.2 下无法连接现代 HTTPS 站点
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromSeconds(120); // 设置 120 秒超时,防止生图或长文本生成时连接断开
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ApiKey}");
}
/// <summary>
/// 润色加工:优化文本的语言表达,提升流畅度和专业性。
/// </summary>
/// <param name="originalText">原文</param>
/// <param name="documentContext">文档全文上下文</param>
/// <returns>润色后的文本</returns>
public async Task<string> PolishText(string originalText, string documentContext = null)
{
string systemPrompt = "你是一个专业的文字编辑和润色专家。请对用户提供的文本进行深度润色和梳理。目标是:1. 提升语言的流畅度和专业性;2. 修正语法错误和用词不当;3. 优化逻辑结构,使表达更清晰;4. 保持原意不变。请直接输出润色后的内容,不要包含任何解释或客套话。请直接输出纯文本,不要使用任何Markdown标记。";
if (!string.IsNullOrWhiteSpace(documentContext))
{
systemPrompt += $"\n\n【全文背景与排版规则】:你已通读全文,以下是文档的整体上下文信息和排版规则,请在润色时保持与全文主题和风格的一致性,并参考排版规则生成对应的层级结构:\n{documentContext}";
}
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = $"请润色以下内容:\n\n{originalText}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 深度升华:挖掘文本思想深度,提升价值和高度。
/// </summary>
/// <param name="originalText">原文</param>
/// <param name="documentContext">文档全文上下文</param>
/// <returns>升华后的文本</returns>
public async Task<string> ElevateText(string originalText, string documentContext = null)
{
string systemPrompt = "你是一个资深的领域专家和思想家。用户提供了一段文本,请你根据自己的广博知识和深刻理解,对其进行深度升华。目标是:1. 挖掘深度:透过现象看本质,补充更有见地的分析;2. 拓展广度:引入相关的行业背景、前沿趋势或跨学科视角;3. 提升价值:将普通的描述提升为具有指导意义或启发性的洞见;4. 高于原文:不要局限于原文的字面意思,要大胆进行思想层面的拔高。请直接输出升华后的纯文本内容,保持逻辑连贯,不要使用任何Markdown标记。";
if (!string.IsNullOrWhiteSpace(documentContext))
{
systemPrompt += $"\n\n【全文背景与排版规则】:你已通读全文,以下是文档的整体上下文信息和排版规则,请在升华时保持与全文主题和风格的一致性(但在思想深度上要超越它),并参考排版规则生成对应的层级结构:\n{documentContext}";
}
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = $"请对以下内容进行深度升华和价值提升:\n\n{originalText}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 素材写作:根据参考资料生成新的内容。
/// </summary>
/// <param name="originalText">目标文本/上下文</param>
/// <param name="referenceContent">参考资料内容</param>
/// <param name="wordLimit">字数限制</param>
/// <param name="documentContext">文档全文上下文</param>
/// <returns>生成的内容</returns>
public async Task<string> GenerateWithReference(string originalText, string referenceContent, int wordLimit, string documentContext = null)
{
string systemPrompt = "你是一个专业的文档写作助手。请根据提供的参考资料,撰写或重写目标文本。请直接输出纯文本内容,不要使用任何Markdown标记。";
if (!string.IsNullOrWhiteSpace(documentContext))
{
systemPrompt += $"\n\n【全文背景与排版规则】:你已通读全文,以下是文档的整体上下文信息和排版规则,请在生成时保持与全文主题和风格的一致性,并参考排版规则生成对应的层级结构:\n{documentContext}";
}
StringBuilder userPrompt = new StringBuilder();
userPrompt.AppendLine("请根据以下信息生成内容:");
userPrompt.AppendLine();
userPrompt.AppendLine($"【字数要求】:{wordLimit}字左右");
userPrompt.AppendLine();
if (!string.IsNullOrWhiteSpace(referenceContent))
{
userPrompt.AppendLine("【参考资料】:");
userPrompt.AppendLine(referenceContent);
userPrompt.AppendLine();
}
if (!string.IsNullOrWhiteSpace(originalText))
{
userPrompt.AppendLine("【目标文本/上下文】:");
userPrompt.AppendLine(originalText);
userPrompt.AppendLine();
}
userPrompt.AppendLine("请开始生成:");
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = userPrompt.ToString() }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 总结大纲:将文本总结为层级分明的大纲文本。
/// </summary>
/// <param name="originalText">原文</param>
/// <returns>大纲文本</returns>
public async Task<string> SummarizeToOutline(string originalText)
{
string systemPrompt = "你是一个专业的文档总结助手。请将用户提供的长文本总结为一个层级分明的大纲。要求:1. 结构清晰,使用缩进或序号表示层级;2. 语言精练,概括重点;3. 适合用于制作PPT或架构图。请直接输出大纲文本,不要包含Markdown代码块标记。";
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = $"请将以下内容总结为大纲:\n\n{originalText}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 生成大纲:根据主题生成结构化的 JSON 大纲。
/// 功能描述:
/// 1. 构造 Prompt,要求 AI 返回特定格式的 JSON 数组 (OutlineItem)。
/// 2. 调用 AI 接口。
/// 3. 清理 AI 可能返回的 Markdown 代码块标记 (```json ... ```)。
/// 4. 反序列化 JSON 为对象列表。
/// </summary>
/// <param name="topic">大纲主题或简要描述</param>
/// <returns>大纲项列表 (包含标题和层级)</returns>
public async Task<List<OutlineItem>> GenerateOutline(string topic)
{
string systemPrompt = @"你是一个专业的文档大纲生成助手。请根据用户提供的主题或描述,生成一个结构化的文档大纲。
要求包含:
1. 主标题 (Level 0)
2. 一级目录 (Level 1)
3. 二级目录 (Level 2)
4. 三级目录 (Level 3)
请只返回一个JSON数组,数组中的每个元素包含 'Title' (标题文本) 和 'Level' (整数层级,0-3)。
例如:[{""Title"": ""关于人工智能的发展报告"", ""Level"": 0}, {""Title"": ""第一章 绪论"", ""Level"": 1}, {""Title"": ""1.1 研究背景"", ""Level"": 2}]
请确保返回的是合法的JSON格式,不包含Markdown标记。";
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = topic }
};
string jsonResponse = await GetChatCompletion(messages);
// 清理可能存在的 Markdown 代码块标记
// 某些模型即使被要求不返回 Markdown,仍可能习惯性地加上 ```json
jsonResponse = jsonResponse.Replace("```json", "").Replace("```", "").Trim();
try
{
return JsonConvert.DeserializeObject<List<OutlineItem>>(jsonResponse);
}
catch (Exception ex)
{
Console.WriteLine("JSON Parsing Failed: " + ex.Message);
// 这里可以考虑返回一个空列表,或者抛出异常让上层处理
// 目前选择返回空列表,由 UI 层提示失败
return new List<OutlineItem>();
}
}
/// <summary>
/// 结构分析:分析文档文本,提取标题及其层级结构。
/// 功能描述:
/// 1. 发送文档内容给 AI。
/// 2. AI 识别出所有的标题行及其层级 (1-5)。
/// 3. 返回 JSON 格式的标题列表,供格式美化功能使用。
/// </summary>
/// <param name="text">文档全文内容 (或截断后的部分内容)</param>
/// <returns>标题信息列表 (包含标题文本和层级)</returns>
public async Task<List<HeadingInfo>> AnalyzeStructure(string text)
{
string systemPrompt = @"你是一个文档格式专家。请分析用户提供的文档内容,识别出所有的标题及其对应的层级(1级、2级、3级...)。
请只返回一个JSON数组,数组中的每个元素包含 'text' (标题所在行的完整纯文本内容,包含序号) 和 'level' (整数层级)。
忽略正文内容,只提取标题。
例如:[{""text"": ""第一章 绪论"", ""level"": 1}, {""text"": ""1.1 研究背景"", ""level"": 2}]
请确保返回的是合法的JSON格式,不包含Markdown标记。";
var messages = new List<Message>
{
new Message { role = "system", content = systemPrompt },
new Message { role = "user", content = text }
};
string jsonResponse = await GetChatCompletion(messages);
// 清理可能存在的 Markdown 代码块标记
jsonResponse = jsonResponse.Replace("```json", "").Replace("```", "").Trim();
try
{
return JsonConvert.DeserializeObject<List<HeadingInfo>>(jsonResponse);
}
catch (Exception ex)
{
Console.WriteLine("JSON Parsing Failed: " + ex.Message);
return new List<HeadingInfo>();
}
}
public class HeadingInfo
{
public string text { get; set; }
public int level { get; set; }
}
/// <summary>
/// 通读全文:分析文档全文,生成摘要和风格画像。
/// </summary>
/// <param name="fullText">全文内容</param>
/// <returns>文档画像描述</returns>
public async Task<string> AnalyzeDocument(string fullText)
{
var messages = new List<Message>
{
new Message { role = "system", content = "你是一个文档分析专家。请阅读提供的文档全文,并提取关键的上下文信息,包括:1. 核心主题摘要;2. 写作风格与语调;3. 关键术语或背景设定。请生成一段简明扼要的“文档画像”,用于在后续写作任务中作为System Prompt的一部分,帮助AI保持上下文一致性。请直接输出这段描述性文字,不要包含其他客套话。" },
new Message { role = "user", content = $"请分析以下文档全文:\n\n{fullText}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 格式化文本(保留接口,目前逻辑主要在 AnalyzeStructure 中实现)。
/// </summary>
public async Task<string> FormatText(string originalText)
{
var messages = new List<Message>
{
new Message { role = "system", content = "你是一个文档排版专家。请直接输出处理后的纯文本,严禁使用Markdown格式,不要包含任何解释性文字。" },
new Message { role = "user", content = $"请对以下文本进行格式化和润色:修正错别字、规范标点符号、优化分段,使其看起来更专业:\n\n{originalText}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 生成图片 URL。
/// 流程:
/// 1. 优化提示词:将用户简单的中文描述转换为详细的英文绘图 Prompt。
/// 2. 调用生图 API (如 DALL-E 3 或 Gemini Image)。
/// 3. 解析 API 响应,提取图片 URL。
/// </summary>
/// <param name="prompt">用户的原始描述</param>
/// <param name="style">图片风格 (如"水墨画", "赛博朋克"等)</param>
/// <param name="shouldOptimize">是否需要 AI 优化提示词(默认为 true)</param>
/// <returns>生成的图片 URL (有效期通常较短)</returns>
public async Task<string> GenerateImageUrl(string prompt, string style = null, bool shouldOptimize = true)
{
try
{
// 1. 优化 Prompt:根据 flag 决定是否调用 LLM 进行翻译和优化
string optimizedPrompt = prompt;
if (shouldOptimize)
{
optimizedPrompt = await OptimizePromptForImage(prompt, style);
}
// 使用匿名对象构造请求体,以获得最大灵活性
var requestBody = new
{
prompt = optimizedPrompt,
model = ImageModel,
size = "1024x1024", // 标准分辨率
response_format = "url" // 显式请求 URL 格式
};
var json = JsonConvert.SerializeObject(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}/images/generations", content);
var responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API Error ({response.StatusCode}): {responseString}");
}
// 使用 JObject 动态解析响应,以兼容不同厂商的 API 格式差异
var jsonResponse = JObject.Parse(responseString);
// 1. 尝试解析 OpenAI 标准格式: data[0].url
var data = jsonResponse["data"];
if (data != null)
{
if (data.Type == JTokenType.Array && data.HasValues)
{
var firstItem = data[0];
// Gemini 3 image preview 可能会返回 revised_prompt 和 url
if (firstItem["url"] != null) return firstItem["url"].ToString();
// 有时它会返回 NO_IMAGE,这可能是模型拒绝了,或者格式不对
if (firstItem["revised_prompt"] != null && firstItem["revised_prompt"].ToString() == "NO_IMAGE")
{
// 尝试从其他字段找,或者这就是个错误
}
// 某些 API 可能直接在数组里放 URL 字符串
if (firstItem.Type == JTokenType.String) return firstItem.ToString();
}
// 2. 尝试解析 data 为直接字符串的情况
else if (data.Type == JTokenType.String)
{
return data.ToString();
}
}
// 3. 尝试解析根节点 url (部分非标准 API)
if (jsonResponse["url"] != null)
{
return jsonResponse["url"].ToString();
}
throw new Exception($"无法从响应中解析图片 URL。原始响应: {responseString}");
}
catch (Exception ex)
{
throw new Exception($"生图请求失败: {ex.Message}", ex);
}
}
/// <summary>
/// 优化图片生成提示词。
/// 将用户的中文描述翻译成英文,并加入风格指令。
/// </summary>
private async Task<string> OptimizePromptForImage(string originalPrompt, string style = null)
{
// 注意:架构图的特殊模板处理已移动到 UI 层 (ImageOptionsForm),
// 这里主要处理需要 AI 翻译和润色的艺术风格。
string styleInstruction = string.IsNullOrEmpty(style) ? "" : $"6. 图片风格必须严格遵循:{style}。";
var messages = new List<Message>
{
new Message { role = "system", content = $"你是一个专业的AI绘图提示词专家。你的任务是将用户的输入转化为一段高质量的、英文的DALL-E 3绘图提示词。请遵循以下规则:\n1. 描述画面的部分必须翻译成英文。\n2. 提炼核心意象,去除无关的修饰语或敏感词。\n3. 增加艺术风格、光影、构图等细节描述。\n4. 直接输出最终的英文Prompt,不要包含任何解释或前言后语。\n5. 确保内容安全,符合通用内容审核标准。\n{styleInstruction}\n7. 【重要约束】如果画面中需要包含任何文字(Text/Label/Title),必须严格保留中文(Simplified Chinese),并明确指示 'text in Chinese',绝对禁止在画面中生成英文文字。" },
new Message { role = "user", content = $"请根据以下描述生成一段绘图Prompt:\n{originalPrompt}" }
};
return await GetChatCompletion(messages);
}
/// <summary>
/// 通用聊天补全请求方法。
/// </summary>
private async Task<string> GetChatCompletion(List<Message> messages)
{
try
{
var requestBody = new ChatCompletionRequest
{
messages = messages,
model = TextModel
};
var json = JsonConvert.SerializeObject(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{BaseUrl}/chat/completions", content);
var responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API Error ({response.StatusCode}): {responseString}");
}
var result = JsonConvert.DeserializeObject<ChatCompletionResponse>(responseString);
return result?.choices?.FirstOrDefault()?.message?.content;
}
catch (Exception ex)
{
// 抛出更详细的错误
throw new Exception($"AI 请求失败: {ex.Message}", ex);
}
}
}
}