using dodohold.core; using System.Diagnostics; using System.Net; using System.Text.Json; using System.Text.RegularExpressions; namespace molilian.core { public partial class AlimamaPlus { private const string AiOpenMcpUrl = "https://tmcp.taobao.com/mcp/union-ai-platform-server/mcp"; private const string AiOpenSkillId = "R1LWxXhl.VRFe"; private const string AiOpenTqlId = "tql_obSb3eZV"; public async Task endpoint_aiopen(string content, TkDataDTO result, CancellationToken cancellationToken = default) { Stopwatch stopwatch = Stopwatch.StartNew(); string responseBody = string.Empty; result.channel = TkChannelEnum.tb; try { string accessKey = _account.appkey?.Trim() ?? string.Empty; string pid = _account.open_pid?.Trim() ?? string.Empty; #if DEBUG accessKey = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJUTUNQIiwic3ViIjoiTExNX0FHRU5UOlBMQVRGT1JNOnVuaW9uLWFpLWdhdGV3YXk6VU5HV185NjkyNTIyNzIzX3VJQ1RsOERHIiwiaWF0IjoxNzg2MzY5NTc0LCJqdGkiOiIyMjUwZWMyOC04MDg2LTQwNTktODVjZi02NTYxNmZhZDU4MDUifQ.8ThEiO3aXMfzCS8gSk0d22hlK8rpqMxhUNMpQU6DZn0"; pid = "mm_9692522723_3372850009_116175250382"; #endif if (string.IsNullOrEmpty(accessKey) || string.IsNullOrEmpty(pid)) { string reason = string.IsNullOrEmpty(accessKey) ? "Access Key 未配置" : "PID 未配置"; LogAiOpenFailure(content, reason, string.Empty); return SetAiOpenFailure(result, "放弃转链", reason, TkSubCodeEnum.Other); } if (!pid.StartsWith("mm_", StringComparison.Ordinal)) { const string reason = "PID 格式不合法"; LogAiOpenFailure(content, reason, string.Empty); return SetAiOpenFailure(result, reason, reason, TkSubCodeEnum.Other); } TimeSpan timeout = GetRequestTimeout2(result.elapsedTime2); #if DEBUG timeout = TimeSpan.FromSeconds(60); _proxy = null; #endif var initializePayload = new { jsonrpc = "2.0", id = $"init-{Guid.NewGuid():N}", method = "initialize", @params = new { protocolVersion = "2025-03-26", capabilities = new { }, clientInfo = new { name = "openai-codex-agent", version = "1.0.0" } } }; var initializeResponse = await AiOpenPostAsync( initializePayload.Convert2Json(true), accessKey, string.Empty, timeout, cancellationToken); if (!initializeResponse.success) { string reason = $"初始化 HTTP {(int)initializeResponse.statusCode}: {initializeResponse.body}"; LogAiOpenFailure(content, reason, initializeResponse.body); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.NetError); } if (string.IsNullOrEmpty(initializeResponse.sessionId)) { const string reason = "初始化响应缺少 Mcp-Session-Id"; LogAiOpenFailure(content, reason, initializeResponse.body); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.Other); } var callPayload = new { jsonrpc = "2.0", id = $"call-{Guid.NewGuid():N}", method = "tools/call", @params = new { name = "entry", arguments = new { arg0 = AiOpenSkillId, arg1 = "query", arg2 = AiOpenTqlId, arg3 = new { material = content, pid } } } }; var callResponse = await AiOpenPostAsync( callPayload.Convert2Json(true), accessKey, initializeResponse.sessionId, timeout, cancellationToken); responseBody = callResponse.body; result.remote_response = responseBody; if (!callResponse.success) { string reason = $"HTTP {(int)callResponse.statusCode}: {responseBody}"; LogAiOpenFailure(content, reason, responseBody); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.NetError); } JsonElement rpc = GetAiOpenRpcJson(responseBody); string rpcError = rpc.PathRead("error.message", string.Empty); if (!string.IsNullOrEmpty(rpcError)) { LogAiOpenFailure(content, rpcError, responseBody); return SetAiOpenFailure(result, "fail", rpcError, TkSubCodeEnum.Other); } string businessJson = rpc.PathRead("result.content[0].text", string.Empty); if (string.IsNullOrEmpty(businessJson)) { const string reason = "JSON-RPC 响应缺少业务数据"; LogAiOpenFailure(content, reason, responseBody); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.Other); } JsonElement inner = businessJson.Convert2JsonElement(); JsonElement data = inner.ElementRead("data"); JsonElement business = data.ValueKind == JsonValueKind.Object ? data.ElementRead("taokeMaterialUniversalLinkConvert") : inner.ElementRead("taokeMaterialUniversalLinkConvert"); // Error responses such as bizErrorCode=403 are returned directly in text, // rather than under data.taokeMaterialUniversalLinkConvert. if (business.ValueKind != JsonValueKind.Object && IsAiOpenBusinessPayload(inner)) { business = inner; } if (business.ValueKind != JsonValueKind.Object) { string reason = ReadAiOpenErrorDescription(inner, "接口响应缺少转链结果"); LogAiOpenFailure(content, reason, responseBody); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.Other); } bool success = business.Read("success", false); string message = business.Read("message", string.Empty); string businessReason = ReadAiOpenErrorDescription(business, message); if (!success) { message = string.IsNullOrEmpty(message) ? "转链失败" : message; businessReason = string.IsNullOrEmpty(businessReason) ? message : businessReason; int bizErrorCode = business.Read("bizErrorCode", 0); int resultCode = business.Read("resultCode", 0); bool remoteDailyLimit = bizErrorCode == 403 && resultCode == 400 && businessReason.Contains("调用已达上限", StringComparison.Ordinal); TkSubCodeEnum subCode = remoteDailyLimit ? TkSubCodeEnum.RemoteDailyLimit : businessReason.Contains("不支持", StringComparison.Ordinal) || businessReason.Contains("商品ID", StringComparison.Ordinal) || businessReason.Contains("有效推广链接", StringComparison.Ordinal) ? TkSubCodeEnum.NoConvert : TkSubCodeEnum.Other; LogAiOpenFailure(content, businessReason, responseBody); if (remoteDailyLimit && AiOpenRiskControlCore.IsApplicableAccount(_account)) { await AiOpenRiskControlCore.RecordDailyLimitAndSuspendAsync( _account, businessReason, bizErrorCode, resultCode); } return SetAiOpenFailure(result, message, businessReason, subCode); } string cpsShortUrl = business.Read("cpsShortUrl", string.Empty); string cpsLongUrl = business.Read("cpsLongUrl", string.Empty); string cpsShortTpwd = business.Read("cpsShortTpwd", string.Empty); string cpsFullTpwd = business.Read("cpsFullTpwd", string.Empty); string couponShortUrl = business.Read("couponShortUrl", string.Empty); string couponLongUrl = business.Read("couponLongUrl", string.Empty); string couponShortTpwd = business.Read("couponShortTpwd", string.Empty); string couponFullTpwd = business.Read("couponFullTpwd", string.Empty); string cpsUrl = string.IsNullOrEmpty(cpsShortUrl) ? cpsLongUrl : cpsShortUrl; string couponUrl = string.IsNullOrEmpty(couponShortUrl) ? couponLongUrl : couponShortUrl; string shortLinkurl = _account.useCouponLinkFirst ? couponUrl : cpsUrl; if (string.IsNullOrEmpty(shortLinkurl)) { shortLinkurl = _account.useCouponLinkFirst ? cpsUrl : couponUrl; } if (string.IsNullOrEmpty(shortLinkurl)) { const string reason = "转链结果缺少有效推广链接"; LogAiOpenFailure(content, reason, responseBody); return SetAiOpenFailure(result, reason, reason, TkSubCodeEnum.NoConvert); } string fullTpwd = _account.useCouponLinkFirst ? couponFullTpwd : cpsFullTpwd; string shortTpwd = _account.useCouponLinkFirst ? couponShortTpwd : cpsShortTpwd; if (string.IsNullOrEmpty(fullTpwd)) { fullTpwd = _account.useCouponLinkFirst ? cpsFullTpwd : couponFullTpwd; } if (string.IsNullOrEmpty(shortTpwd)) { shortTpwd = _account.useCouponLinkFirst ? cpsShortTpwd : couponShortTpwd; } result.success = true; result.message = string.Empty; result.reason = string.Empty; result.subCode = TkSubCodeEnum.Success; result.content = ReplaceUrls(content, shortLinkurl); result.shortLinkurl = shortLinkurl; result.deeplink_url = GetDeeplink(shortLinkurl); result.taoToken = string.IsNullOrEmpty(fullTpwd) ? $"{shortTpwd} {shortLinkurl}".Trim() : fullTpwd; result.item_url = string.IsNullOrEmpty(cpsUrl) ? shortLinkurl : cpsUrl; result.item_deeplink_url = GetDeeplink(result.item_url); result.itemId = ExtractItemId(content) ?? result.itemId; result.pic = _config.tk_parse_default_icon; // "http://doc.molilian.com/20260817142801_2822_122.png?x-oss-process=image/resize,w_200"; // 完整淘口令格式:m.tb.cn 链接 + 商品标题 + 两位字母和 3~4 位数字。 Match titleMatch = Regex.Match( fullTpwd, @"https?://m\.tb\.cn/\S+\s+(?.+?)\s+[A-Za-z]{2}\d{3,4}\s*$", RegexOptions.IgnoreCase); if (titleMatch.Success) { result.itemName = titleMatch.Groups["title"].Value.Trim(); } switch (result.link_type) { case LinkTypeEnum.profile: case LinkTypeEnum.goods: case LinkTypeEnum.video: case LinkTypeEnum.live: break; default: result.link_type = LinkTypeEnum.goods; break; } return result; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { string reason = $"上游接口异常: {ex.Message}"; LogAiOpenFailure(content, reason, responseBody); return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.NetError); } finally { stopwatch.Stop(); result.elapsedTime3 = (int)stopwatch.ElapsedMilliseconds; } } private async Task<(bool success, HttpStatusCode statusCode, string body, string sessionId)> AiOpenPostAsync( string post, string accessKey, string sessionId, TimeSpan timeout, CancellationToken cancellationToken) { var client = new WebClientUtility() .SetContentType("application/json") .AddHeaders("Accept", "application/json, text/event-stream") .AddHeaders("Authorization", $"Bearer {accessKey}"); if (!string.IsNullOrEmpty(sessionId)) { client.AddHeaders("Mcp-Session-Id", sessionId); } client.Proxy = _proxy; client.Timeout = timeout; if (!string.IsNullOrEmpty(_user_agent)) client.UserAgent = _user_agent; var response = await client.Post(post).RequestAsync(AiOpenMcpUrl, "POST", cancellationToken); if (response.ResponseMessage == null) { throw response.ResponseException ?? new Exception("上游接口未返回 HTTP 响应"); } string body = response.Body(); string responseSessionId = response.ResponseMessage.Headers.TryGetValues("Mcp-Session-Id", out IEnumerable<string>? values) ? values.FirstOrDefault() ?? string.Empty : string.Empty; bool success = response.Successed && (int)response.ResponseMessage.StatusCode is >= 200 and <= 299; return (success, response.ResponseMessage.StatusCode, body, responseSessionId); } private static JsonElement GetAiOpenRpcJson(string body) { string json = body.Trim(); if (!json.StartsWith('{')) { json = body.Split('\n') .Select(line => line.Trim()) .Where(line => line.StartsWith("data:", StringComparison.Ordinal)) .Select(line => line[5..].Trim()) .LastOrDefault(value => value.StartsWith('{')) ?? string.Empty; } if (string.IsNullOrEmpty(json)) { throw new JsonException("响应中没有可解析的 JSON-RPC data"); } return json.Convert2JsonElement(); } private static TkDataDTO SetAiOpenFailure( TkDataDTO result, string message, string reason, TkSubCodeEnum subCode) { result.channel = TkChannelEnum.tb; result.link_type = LinkTypeEnum.unknown; result.success = false; result.message = message; result.reason = reason; result.subCode = subCode; return result; } private static bool IsAiOpenBusinessPayload(JsonElement value) { return value.ValueKind == JsonValueKind.Object && (value.TryGetProperty("success", out _) || value.TryGetProperty("bizErrorCode", out _) || value.TryGetProperty("resultCode", out _)); } private static string ReadAiOpenErrorDescription(JsonElement value, string fallback) { string description = value.Read("bizErrorDesc", string.Empty); if (!string.IsNullOrWhiteSpace(description)) { return description.Trim(); } string message = value.Read("message", string.Empty); return string.IsNullOrWhiteSpace(message) ? fallback : message.Trim(); } private static void LogAiOpenFailure(string content, string reason, string body) { if (string.IsNullOrEmpty(body)) { _ = new LoggerLibrary("endpoint", "aiopen") .Info(content, reason) .SaveAsync(); return; } _ = new LoggerLibrary("endpoint", "aiopen") .Info(content, reason) .Info("response", body) .SaveAsync(); } } }