422 lines
21 KiB
C#
422 lines
21 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|