MyBook/Assets/script/DeepSeekReasonerStreamManager.cs
2025-03-17 17:38:32 +08:00

287 lines
9.1 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 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 = "";
while (!streamingCompleted || displayed.Length < reasoningBuffer.Length)
{
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);
//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>
{
{"什么是潜艇?","一种潜水用具"},
{"什么是航母?","一种船"}
}
);
}
}
// 解析 [] 内的内容,转换成 <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();
}
}