|
|
@@ -0,0 +1,352 @@
|
|
|
+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<TkDataDTO> 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");
|
|
|
+
|
|
|
+ if (business.ValueKind != JsonValueKind.Object)
|
|
|
+ {
|
|
|
+ string reason = inner.Read("message", "接口响应缺少转链结果");
|
|
|
+ LogAiOpenFailure(content, reason, responseBody);
|
|
|
+ return SetAiOpenFailure(result, "fail", reason, TkSubCodeEnum.Other);
|
|
|
+ }
|
|
|
+
|
|
|
+ bool success = business.Read<bool>("success", false);
|
|
|
+ string message = business.Read("message", string.Empty);
|
|
|
+ if (!success)
|
|
|
+ {
|
|
|
+ message = string.IsNullOrEmpty(message) ? "转链失败" : message;
|
|
|
+ TkSubCodeEnum subCode = message.Contains("不支持", StringComparison.Ordinal)
|
|
|
+ || message.Contains("商品ID", StringComparison.Ordinal)
|
|
|
+ || message.Contains("有效推广链接", StringComparison.Ordinal)
|
|
|
+ ? TkSubCodeEnum.NoConvert
|
|
|
+ : TkSubCodeEnum.Other;
|
|
|
+ LogAiOpenFailure(content, message, responseBody);
|
|
|
+ return SetAiOpenFailure(result, message, message, 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;
|
|
|
+
|
|
|
+ // 完整淘口令格式:m.tb.cn 链接 + 商品标题 + 两位字母和 3~4 位数字。
|
|
|
+ Match titleMatch = Regex.Match(
|
|
|
+ fullTpwd,
|
|
|
+ @"https?://m\.tb\.cn/\S+\s+(?<title>.+?)\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 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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|