294 lines
11 KiB
C#
294 lines
11 KiB
C#
using Newtonsoft.Json;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.Networking;
|
||
using UnityEngine.UI;
|
||
|
||
public class DeepSeekReasonerStreamManager : MonoBehaviour
|
||
{
|
||
// API 配置
|
||
[Header("API Settings")]
|
||
[SerializeField] private string apiKey = "sk-c89fd3342dda4a8eb53cb533607e2e89"; // 替换成你的 API Key
|
||
[SerializeField] private string modelName = "deepseek-reasoner";
|
||
[SerializeField] private string apiUrl = "https://api.deepseek.com/v1/chat/completions";
|
||
|
||
// 推理参数
|
||
[Header("Inference Settings")]
|
||
[Range(0, 2)] public float temperature = 0.7f;
|
||
[Range(1, 1000)] public int maxTokens = 150;
|
||
|
||
// UI 控件(请在 Inspector 中指定,只需一个 Text 节点)
|
||
[Header("UI Elements")]
|
||
public TextMeshProUGUI dialogueText; // 显示推理过程及最终回复
|
||
|
||
public InfoShow infoShow;
|
||
|
||
// 内部缓冲区
|
||
private StringBuilder reasoningBuffer = new StringBuilder();
|
||
private StringBuilder contentBuffer = new StringBuilder();
|
||
private bool streamingCompleted = false;
|
||
|
||
/// <summary>
|
||
/// 开始流式推理请求,传入对话消息列表(例如第一轮对话)
|
||
/// </summary>
|
||
/// <param name="messages">对话消息列表,格式参见 DeepSeekMessage</param>
|
||
public void SendStreamReasonerRequest(List<DeepSeekMessage> messages)
|
||
{
|
||
// 清空之前的缓冲区及 UI 显示
|
||
reasoningBuffer.Clear();
|
||
contentBuffer.Clear();
|
||
dialogueText.text = "";
|
||
streamingCompleted = false;
|
||
|
||
StartCoroutine(ProcessStreamReasonerRequest(messages));
|
||
// 开启打字机效果协程,先显示推理过程
|
||
StartCoroutine(AnimateReasoningText());
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 流式请求的协程:发送请求并通过自定义下载处理器实时处理返回块
|
||
/// </summary>
|
||
private IEnumerator ProcessStreamReasonerRequest(List<DeepSeekMessage> messages)
|
||
{
|
||
ChatStreamRequest requestBody = new ChatStreamRequest
|
||
{
|
||
model = modelName,
|
||
messages = messages,
|
||
stream = true
|
||
};
|
||
GameManager.Instance.infoTGo.text = "智慧正在酝酿中,请稍候…";
|
||
string jsonBody = JsonUtility.ToJson(requestBody);
|
||
Debug.Log("Sending streaming JSON: " + jsonBody);
|
||
|
||
// 使用自定义 StreamingDownloadHandler 逐块处理响应
|
||
StreamingDownloadHandler downloadHandler = new StreamingDownloadHandler();
|
||
downloadHandler.onChunkReceived = ProcessChunk;
|
||
|
||
using (UnityWebRequest request = new UnityWebRequest(apiUrl, "POST"))
|
||
{
|
||
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
|
||
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
|
||
request.downloadHandler = downloadHandler;
|
||
request.SetRequestHeader("Content-Type", "application/json");
|
||
request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
|
||
request.SetRequestHeader("Accept", "application/json");
|
||
|
||
yield return request.SendWebRequest();
|
||
|
||
if (IsRequestError(request))
|
||
{
|
||
Debug.LogError($"API Error: {request.responseCode}\n{request.downloadHandler.text}");
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
streamingCompleted = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对每个返回块进行解析,如果存在推理内容则追加到 reasoningBuffer,
|
||
/// 否则将最终结果追加到 contentBuffer(不直接更新 UI)。
|
||
/// </summary>
|
||
private void ProcessChunk(string chunk)
|
||
{
|
||
chunk = chunk.Trim();
|
||
if (string.IsNullOrEmpty(chunk))
|
||
return;
|
||
|
||
if (chunk.Contains("keep-alive"))
|
||
return;
|
||
|
||
if (chunk.StartsWith("data:"))
|
||
{
|
||
chunk = chunk.Substring("data:".Length).Trim();
|
||
if (chunk == "[DONE]")
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
ChatStreamResponseChunk chunkData = JsonUtility.FromJson<ChatStreamResponseChunk>(chunk);
|
||
if (chunkData != null && chunkData.choices != null && chunkData.choices.Length > 0)
|
||
{
|
||
StreamingDelta delta = chunkData.choices[0].delta;
|
||
if (!string.IsNullOrEmpty(delta.reasoning_content))
|
||
{
|
||
reasoningBuffer.Append(delta.reasoning_content);
|
||
}
|
||
else if (!string.IsNullOrEmpty(delta.content))
|
||
{
|
||
contentBuffer.Append(delta.content);
|
||
// 此处不直接更新 dialogueText,待流式完成后统一显示最终结果
|
||
}
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError("解析返回块失败: " + e.Message + "\nChunk content: " + chunk);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 实现打字机效果:不断检查 reasoningBuffer 中的新字符,并逐个显示到 dialogueText,
|
||
/// 流式加载结束后,替换显示最终回复内容。
|
||
/// </summary>
|
||
private IEnumerator AnimateReasoningText()
|
||
{
|
||
string displayed = "";
|
||
GameManager.Instance.stopAnimation = false; // 允许执行
|
||
while (!streamingCompleted || displayed.Length < reasoningBuffer.Length)
|
||
{
|
||
if (GameManager.Instance.stopAnimation) break; // 立即停止
|
||
if (displayed.Length < reasoningBuffer.Length)
|
||
{
|
||
displayed = reasoningBuffer.ToString().Substring(0, displayed.Length + 1);
|
||
dialogueText.text = displayed;
|
||
}
|
||
yield return new WaitForSeconds(0.05f);
|
||
}
|
||
// 推理过程已显示完毕,等待片刻后替换为最终结果
|
||
yield return new WaitForSeconds(0.5f);
|
||
|
||
|
||
if (!GameManager.Instance.stopAnimation)
|
||
{
|
||
yield return new WaitForSeconds(0.5f);
|
||
linkidResponse(contentBuffer.ToString());
|
||
}
|
||
//dialogueText.text = contentBuffer.ToString();
|
||
// linkidResponse(contentBuffer.ToString());
|
||
}
|
||
|
||
private void linkidResponse(string response)
|
||
{
|
||
try
|
||
{
|
||
Dictionary<string, string> dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||
infoShow.ShowInfo(dictionary);
|
||
}
|
||
catch (System.Exception)
|
||
{
|
||
infoShow.ShowInfo(
|
||
new Dictionary<string, string>
|
||
{
|
||
{"什么是潜艇?","潜艇是一种能够在水下航行的特种舰艇,主要用于军事侦察、战斗巡逻、秘密运输等任务。潜艇利用内部的压载舱调整自身浮力,实现浮出水面或下潜水中的能力。现代潜艇通常配备柴油发动机或核动力系统,能够长时间潜伏在水下执行任务。军事潜艇通常携带鱼雷、导弹或水雷等武器,能够对敌方舰船实施打击。此外,潜艇还可用于海洋科学研究、深海探测和搜救任务。在历史上,潜艇在第一次和第二次世界大战中都发挥了重要作用,尤其是德国的“U型潜艇”对海战产生了深远影响。如今,各国海军普遍装备不同类型的潜艇,如战略核潜艇、攻击型潜艇和常规动力潜艇,以执行各种军事和非军事任务。"},
|
||
{"什么是航母?","航空母舰,简称航母,是一种专门用于搭载和起降战斗机的大型水面舰艇。航母通常配备巨大的甲板作为飞机的跑道,并设有升降机、机库等辅助设施,使舰载机能够高效起降和维护。现代航母一般由核动力或常规动力提供动力,拥有强大的续航能力。航母不仅是一种海上军事基地,还是国家海军力量的象征,能够在远离本土的海域实施空中作战任务。其舰载机编队能够执行制空、对海打击、反潜作战、电子战等多种任务。航母战斗群通常由驱逐舰、护卫舰、补给舰和潜艇等护航舰艇组成,形成强大的作战体系。世界主要海军强国,如美国、中国、英国、法国等,都装备有不同级别的航母,并持续发展新型舰载机和航母战斗体系。"}
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
// 解析 [] 内的内容,转换成 <link> 形式
|
||
public string ParseClickableText(string rawText)
|
||
{
|
||
return Regex.Replace(rawText, @"\[(.*?)\]", "<link=$1><color=yellow><u>$1</u></color></link>");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查请求是否出现错误
|
||
/// </summary>
|
||
private bool IsRequestError(UnityWebRequest request)
|
||
{
|
||
return request.result == UnityWebRequest.Result.ConnectionError ||
|
||
request.result == UnityWebRequest.Result.ProtocolError ||
|
||
request.result == UnityWebRequest.Result.DataProcessingError;
|
||
}
|
||
|
||
// 可序列化的请求和响应数据结构
|
||
|
||
[Serializable]
|
||
public class DeepSeekMessage
|
||
{
|
||
public string role; // "user", "assistant", "system" 等
|
||
public string content; // 消息内容
|
||
}
|
||
|
||
[Serializable]
|
||
private class ChatStreamRequest
|
||
{
|
||
public string model;
|
||
public List<DeepSeekMessage> messages;
|
||
public bool stream;
|
||
}
|
||
|
||
[Serializable]
|
||
private class StreamingDelta
|
||
{
|
||
public string reasoning_content;
|
||
public string content;
|
||
}
|
||
|
||
[Serializable]
|
||
private class StreamingChoice
|
||
{
|
||
public StreamingDelta delta;
|
||
}
|
||
|
||
[Serializable]
|
||
private class ChatStreamResponseChunk
|
||
{
|
||
public StreamingChoice[] choices;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 自定义流式下载处理器:将接收到的数据按行分割,并调用回调处理每一行 JSON 数据
|
||
/// </summary>
|
||
public class StreamingDownloadHandler : DownloadHandlerScript
|
||
{
|
||
public Action<string> onChunkReceived;
|
||
private StringBuilder buffer = new StringBuilder();
|
||
|
||
public StreamingDownloadHandler() : base(new byte[1024])
|
||
{
|
||
}
|
||
|
||
protected override bool ReceiveData(byte[] data, int dataLength)
|
||
{
|
||
if (data == null || dataLength <= 0)
|
||
return false;
|
||
|
||
string chunk = Encoding.UTF8.GetString(data, 0, dataLength);
|
||
buffer.Append(chunk);
|
||
|
||
// 假设每个 JSON 块以换行符结尾
|
||
string fullText = buffer.ToString();
|
||
string[] lines = fullText.Split(new[] { "\n" }, StringSplitOptions.None);
|
||
// 处理除最后一行外的所有完整行
|
||
for (int i = 0; i < lines.Length - 1; i++)
|
||
{
|
||
string line = lines[i].Trim();
|
||
if (!string.IsNullOrEmpty(line))
|
||
{
|
||
onChunkReceived?.Invoke(line);
|
||
}
|
||
}
|
||
// 保留最后可能不完整的那一行
|
||
buffer.Clear();
|
||
buffer.Append(lines[lines.Length - 1]);
|
||
|
||
return true;
|
||
}
|
||
|
||
protected override void CompleteContent()
|
||
{
|
||
string remaining = buffer.ToString().Trim();
|
||
if (!string.IsNullOrEmpty(remaining))
|
||
{
|
||
onChunkReceived?.Invoke(remaining);
|
||
}
|
||
base.CompleteContent();
|
||
}
|
||
}
|