引入 StreamProcessResult 统一封装流式处理结果,补充各 API 类型下用户输入与系统输出内容的提取与累计,用于会话消息持久化与用量统计;同时增强 Gemini 请求与响应内容解析能力,确保流式场景下消息与 token 使用数据完整一致。
154 lines
4.9 KiB
C#
154 lines
4.9 KiB
C#
using System.Text.Json;
|
||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||
using Yi.Framework.AiHub.Domain.Shared.Extensions;
|
||
|
||
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.Gemini;
|
||
|
||
public static class GeminiGenerateContentAcquirer
|
||
{
|
||
/// <summary>
|
||
/// 从请求体中提取用户最后一条消息内容
|
||
/// 路径: contents[last].parts[last].text
|
||
/// </summary>
|
||
public static string GetLastUserContent(JsonElement request)
|
||
{
|
||
var contents = request.GetPath("contents");
|
||
if (!contents.HasValue || contents.Value.ValueKind != JsonValueKind.Array)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var contentsArray = contents.Value.EnumerateArray().ToList();
|
||
if (contentsArray.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var lastContent = contentsArray[^1];
|
||
var parts = lastContent.GetPath("parts");
|
||
if (!parts.HasValue || parts.Value.ValueKind != JsonValueKind.Array)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var partsArray = parts.Value.EnumerateArray().ToList();
|
||
if (partsArray.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
// 获取最后一个 part 的 text
|
||
var lastPart = partsArray[^1];
|
||
return lastPart.GetPath("text").GetString() ?? string.Empty;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从响应中提取文本内容(非 thought 类型)
|
||
/// 路径: candidates[0].content.parts[].text (where thought != true)
|
||
/// </summary>
|
||
public static string GetTextContent(JsonElement response)
|
||
{
|
||
var candidates = response.GetPath("candidates");
|
||
if (!candidates.HasValue || candidates.Value.ValueKind != JsonValueKind.Array)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var candidatesArray = candidates.Value.EnumerateArray().ToList();
|
||
if (candidatesArray.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var parts = candidatesArray[0].GetPath("content", "parts");
|
||
if (!parts.HasValue || parts.Value.ValueKind != JsonValueKind.Array)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
// 遍历所有 parts,只取非 thought 的 text
|
||
foreach (var part in parts.Value.EnumerateArray())
|
||
{
|
||
var isThought = part.GetPath("thought").GetBool();
|
||
if (!isThought)
|
||
{
|
||
var text = part.GetPath("text").GetString();
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
return text;
|
||
}
|
||
}
|
||
}
|
||
|
||
return string.Empty;
|
||
}
|
||
|
||
public static ThorUsageResponse? GetUsage(JsonElement response)
|
||
{
|
||
var usage = response.GetPath("usageMetadata");
|
||
if (!usage.HasValue)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var inputTokens = usage.Value.GetPath("promptTokenCount").GetInt();
|
||
var outputTokens = usage.Value.GetPath("candidatesTokenCount").GetInt()
|
||
+ usage.Value.GetPath("cachedContentTokenCount").GetInt()
|
||
+ usage.Value.GetPath("thoughtsTokenCount").GetInt()
|
||
+ usage.Value.GetPath("toolUsePromptTokenCount").GetInt();
|
||
|
||
return new ThorUsageResponse
|
||
{
|
||
PromptTokens = inputTokens,
|
||
InputTokens = inputTokens,
|
||
OutputTokens = outputTokens,
|
||
CompletionTokens = outputTokens,
|
||
TotalTokens = inputTokens + outputTokens,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取图片 base64(包含 data:image 前缀)
|
||
/// 优先从 inlineData.data 中获取,其次从 markdown text 中解析
|
||
/// </summary>
|
||
public static string GetImagePrefixBase64(JsonElement response)
|
||
{
|
||
// Step 1: 优先尝试从 candidates[0].content.parts[0].inlineData.data 获取
|
||
var inlineBase64 = response
|
||
.GetPath("candidates", 0, "content", "parts", 0, "inlineData", "data")
|
||
.GetString();
|
||
|
||
if (!string.IsNullOrEmpty(inlineBase64))
|
||
{
|
||
// 默认按 png 格式拼接前缀
|
||
return $"data:image/png;base64,{inlineBase64}";
|
||
}
|
||
|
||
// Step 2: fallback,从 candidates[0].content.parts[0].text 中解析 markdown 图片
|
||
var text = response
|
||
.GetPath("candidates", 0, "content", "parts", 0, "text")
|
||
.GetString();
|
||
|
||
if (string.IsNullOrEmpty(text))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
// markdown 图片格式: 
|
||
var startMarker = "(data:image/";
|
||
var startIndex = text.IndexOf(startMarker, StringComparison.Ordinal);
|
||
if (startIndex < 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
startIndex += 1; // 跳过 "("
|
||
var endIndex = text.IndexOf(')', startIndex);
|
||
if (endIndex <= startIndex)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return text.Substring(startIndex, endIndex - startIndex);
|
||
}
|
||
} |