2025-03-17 17:27:32 +08:00
|
|
|
|
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 = "";
|
2025-03-17 20:43:54 +08:00
|
|
|
|
GameManager.Instance.stopAnimation = false; // 允许执行
|
2025-03-17 17:27:32 +08:00
|
|
|
|
while (!streamingCompleted || displayed.Length < reasoningBuffer.Length)
|
|
|
|
|
{
|
2025-03-17 20:43:54 +08:00
|
|
|
|
if (GameManager.Instance.stopAnimation) break; // 立即停止
|
2025-03-17 17:27:32 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
2025-03-17 20:43:54 +08:00
|
|
|
|
if (!GameManager.Instance.stopAnimation)
|
|
|
|
|
{
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
linkidResponse(contentBuffer.ToString());
|
|
|
|
|
}
|
2025-03-17 17:27:32 +08:00
|
|
|
|
//dialogueText.text = contentBuffer.ToString();
|
2025-03-17 20:43:54 +08:00
|
|
|
|
// linkidResponse(contentBuffer.ToString());
|
2025-03-17 17:27:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
{
|
2025-03-17 20:43:54 +08:00
|
|
|
|
{"什么是潜艇?","潜艇是一种能够在水下航行的特种舰艇,主要用于军事侦察、战斗巡逻、秘密运输等任务。潜艇利用内部的压载舱调整自身浮力,实现浮出水面或下潜水中的能力。现代潜艇通常配备柴油发动机或核动力系统,能够长时间潜伏在水下执行任务。军事潜艇通常携带鱼雷、导弹或水雷等武器,能够对敌方舰船实施打击。此外,潜艇还可用于海洋科学研究、深海探测和搜救任务。在历史上,潜艇在第一次和第二次世界大战中都发挥了重要作用,尤其是德国的“U型潜艇”对海战产生了深远影响。如今,各国海军普遍装备不同类型的潜艇,如战略核潜艇、攻击型潜艇和常规动力潜艇,以执行各种军事和非军事任务。"},
|
|
|
|
|
{"什么是航母?","航空母舰,简称航母,是一种专门用于搭载和起降战斗机的大型水面舰艇。航母通常配备巨大的甲板作为飞机的跑道,并设有升降机、机库等辅助设施,使舰载机能够高效起降和维护。现代航母一般由核动力或常规动力提供动力,拥有强大的续航能力。航母不仅是一种海上军事基地,还是国家海军力量的象征,能够在远离本土的海域实施空中作战任务。其舰载机编队能够执行制空、对海打击、反潜作战、电子战等多种任务。航母战斗群通常由驱逐舰、护卫舰、补给舰和潜艇等护航舰艇组成,形成强大的作战体系。世界主要海军强国,如美国、中国、英国、法国等,都装备有不同级别的航母,并持续发展新型舰载机和航母战斗体系。"}
|
2025-03-17 17:27:32 +08:00
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 [] 内的内容,转换成 <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();
|
|
|
|
|
}
|
|
|
|
|
}
|