dodo hold 3 weken geleden
bovenliggende
commit
d7782e05b6

+ 5 - 2
.gitignore

@@ -20,6 +20,7 @@ stats.html
 *.njsproj
 *.sln
 *.sw?
+latest.pubxml.user
 
 # Misc
 .DS_Store
@@ -27,6 +28,8 @@ stats.html
 **/bin/
 **/obj/
 **/obj/Debug/
-.claude/*
-.ace-tool/
+.claude/*
+.ace-tool/
 /.codex-build
+
+/scripts/

File diff suppressed because it is too large
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 3 - 0
molilian.core/Plus/Alimama/parse.cs

@@ -187,6 +187,9 @@ namespace molilian.core
                     case "tbk_dg":
                         result = await endpoint_tbk_dg(result.content, result, cancellationToken);
                         break;
+                    case "aiopen":
+                        result = await endpoint_aiopen(result.content, result, cancellationToken);
+                        break;
                     case "veapi":
                     case "ddx":
                     case "other_api":

+ 352 - 0
molilian.core/Plus/Alimama/parse_endpoint/endpoint_aiopen.cs

@@ -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();
+        }
+    }
+}

Some files were not shown because too many files changed in this diff