287 lines
9.1 KiB
C#
287 lines
9.1 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 = "";
|
|||
|
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();
|
|||
|
}
|
|||
|
}
|