2
0

2 Commity 029e8e148f ... d7782e05b6

Autor SHA1 Správa Dátum
  dodo hold d7782e05b6 0813 3 týždňov pred
  dodo hold 762c75b6b5 fix(链路报表): 修复转链结果计数漏记 1 mesiac pred

+ 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/

+ 41 - 37
molilian.api/Controllers/admin/TaobaoController.cs

@@ -523,21 +523,22 @@ namespace molilian.api.Controllers
 #endif
                 //var tk = TkPoolCore.GetOne(item.accountId);
                 //if (tk != null) item.accountName = tk.company;
-                accountName = $"{TkChannelEnum.tb}_{item.accountId}";
-
-                if (item.total_count == 0)
-                {
-                    item.total_count = await TkLogCore.GetTotalAsync($":total:{accountName}:{item.report_date:yyyyMMdd}");
-                    if (item.total_count > 0)
-                    {
-                        //todo  过度用, 0613 跑两天就可以删掉
-                        accountName = item.accountName;
-                        item.total_count = await TkLogCore.GetTotalAsync($":total:{accountName}:{item.report_date:yyyyMMdd}");
-                    }
-                    if (item.total_count > 0)
-                    {
-                        item.success_count = await TkLogCore.GetTotalAsync($":total:{accountName}:success:{item.report_date:yyyyMMdd}");
-                        item.abandon_count = await TkLogCore.GetTotalAsync($":total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}");
+                string redisAccountName = $"{TkChannelEnum.tb}_{item.accountId}";
+
+                if (item.total_count == 0)
+                {
+                    string totalAccountName = redisAccountName;
+                    item.total_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:{item.report_date:yyyyMMdd}");
+                    if (item.total_count == 0 && !string.Equals(item.accountName, totalAccountName, StringComparison.Ordinal))
+                    {
+                        //todo  过度用, 0613 跑两天就可以删掉
+                        totalAccountName = item.accountName;
+                        item.total_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:{item.report_date:yyyyMMdd}");
+                    }
+                    if (item.total_count > 0)
+                    {
+                        item.success_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:success:{item.report_date:yyyyMMdd}");
+                        item.abandon_count = await TkLogCore.GetTotalAsync($":total:{totalAccountName}:放弃转链:{item.report_date:yyyyMMdd}");
                         if (item.total_count > 0)
                         {
                             item.success_percentage = $"{item.success_count / (double)item.total_count * 100:f2}%";
@@ -546,28 +547,31 @@ namespace molilian.api.Controllers
                     }
                 }
 
-                if (item.parse_total_count == 0)
-                {
-                    item.parse_total_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:{item.report_date:yyyyMMdd}");
-                    if (item.parse_total_count > 0)
-                    {
-                        item.parse_success_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:success:{item.report_date:yyyyMMdd}");
-                        item.parse_abandon_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}");
-                        if (item.parse_total_count > 0)
-                        {
-                            item.parse_success_percentage = $"{item.parse_success_count / (double)item.parse_total_count * 100:f2}%";
-                            item.parse_abandon_percentage = $"{item.parse_abandon_count / (double)item.parse_total_count * 100:f2}%";
-                        }
-                    }
-                }
-
-                if (item.coupon_total_count == 0)
-                {
-                    item.coupon_total_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:{item.report_date:yyyyMMdd}", false);
-                    if (item.coupon_total_count > 0)
-                    {
-                        item.coupon_success_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:success:{item.report_date:yyyyMMdd}", false);
-                        item.coupon_abandon_count = await TkLogCore.GetTotalAsync($":coupon_total:{accountName}:放弃转链:{item.report_date:yyyyMMdd}", false);
+                string parseAccountName = redisAccountName;
+                int parseTotalCount = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:{item.report_date:yyyyMMdd}");
+                if (parseTotalCount == 0 && !string.Equals(item.accountName, parseAccountName, StringComparison.Ordinal))
+                {
+                    parseAccountName = item.accountName;
+                    parseTotalCount = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:{item.report_date:yyyyMMdd}");
+                }
+                // Today's DB row may be an earlier snapshot. Prefer the live Redis values
+                // whenever they exist instead of only filling an initially-zero total.
+                if (parseTotalCount > 0)
+                {
+                    item.parse_total_count = parseTotalCount;
+                    item.parse_success_count = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:success:{item.report_date:yyyyMMdd}");
+                    item.parse_abandon_count = await TkLogCore.GetTotalAsync($":parse_total:{parseAccountName}:放弃转链:{item.report_date:yyyyMMdd}");
+                    item.parse_success_percentage = $"{item.parse_success_count / (double)item.parse_total_count * 100:f2}%";
+                    item.parse_abandon_percentage = $"{item.parse_abandon_count / (double)item.parse_total_count * 100:f2}%";
+                }
+
+                if (item.coupon_total_count == 0)
+                {
+                    item.coupon_total_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:{item.report_date:yyyyMMdd}", false);
+                    if (item.coupon_total_count > 0)
+                    {
+                        item.coupon_success_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:success:{item.report_date:yyyyMMdd}", false);
+                        item.coupon_abandon_count = await TkLogCore.GetTotalAsync($":coupon_total:{redisAccountName}:放弃转链:{item.report_date:yyyyMMdd}", false);
                         if (item.coupon_total_count > 0)
                         {
                             item.coupon_success_percentage = $"{item.coupon_success_count / (double)item.coupon_total_count * 100:f2}%";

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 86 - 25
molilian.core/Core/log/base.cs

@@ -321,11 +321,18 @@ namespace molilian.core
         }
 
 
-        private static async Task SaveParseCacheAsync(string channel, int accountId, string accountName,
-            bool success, string message, string reason, string deeplink)
-        {
-            string dp_flag = deeplink switch
-            {
+        private static async Task SaveParseCacheAsync(string channel, int accountId, string accountName,
+            bool success, string message, string reason, string deeplink)
+        {
+            // Account reports read this dimension directly. Record it first so failures in
+            // secondary aggregate dimensions cannot leave the account row incomplete.
+            if (accountId != 0)
+            {
+                await SaveParseAccountCacheAsync($"{channel}_{accountId}", success, message, reason);
+            }
+
+            string dp_flag = deeplink switch
+            {
                 "" => "none",
                 "tbopen://m.taobao.com/tbopen/index.html" or
                 "pinduoduo://com.xunmeng.pinduoduo/" or
@@ -354,11 +361,47 @@ namespace molilian.core
             {
                 await SaveParseAccountCacheAsync($"{accountName}", success, message, reason);
             }
-            if (accountId != 0)
+        }
+
+        private static async Task RecordParseMetricsAsync(
+            string channel,
+            int accountId,
+            string accountName,
+            bool success,
+            string message,
+            string reason,
+            string deeplink,
+            string riskStrategy,
+            int launchScene)
+        {
+            // The new parse_metric counters must not depend on the legacy Redis statistics
+            // completing successfully. The MySQL log queue is also independent of both.
+            await TracksCore.RecordParseResultAsync(
+                channel,
+                riskStrategy,
+                launchScene,
+                accountId,
+                success);
+
+            try
             {
-                await SaveParseAccountCacheAsync($"{channel}_{accountId}", success, message, reason);
+                await SaveParseCacheAsync(
+                    channel,
+                    accountId,
+                    accountName,
+                    success,
+                    message,
+                    reason,
+                    deeplink);
             }
-        }
+            catch (Exception ex)
+            {
+                _ = new LoggerLibrary("TkLogCore", "RecordParseMetrics")
+                    .Info($"channel={channel}, accountId={accountId}, riskStrategy={riskStrategy}, launchScene={launchScene}")
+                    .Info(ex.Message, ex.StackTrace)
+                    .SaveAsync();
+            }
+        }
 
         private static async Task SaveParseAccountCacheAsync(string accountName, bool success,
             string message, string reason)
@@ -441,26 +484,44 @@ namespace molilian.core
             string message, string reason)
         {
             var now = DateTime.Now;
-            string month = now.ToString("yyyyMM");
-            string day = now.ToString("yyyyMMdd");
-            string hour = now.ToString("yyyyMMddHH");
-
-            await SaveStatsCountAsync(prefix, accountName, month);
-            await SaveStatsCountAsync(prefix, accountName, day);
-            await SaveStatsCountAsync(prefix, accountName, hour);
-
             string result = success ? "success" : "fail";
-            await SaveStatsCountAsync(prefix, accountName, month, result);
-            await SaveStatsCountAsync(prefix, accountName, day, result);
-            await SaveStatsCountAsync(prefix, accountName, hour, result);
+            string[] timeKeys =
+            [
+                now.ToString("yyyyMM"),
+                now.ToString("yyyyMMdd"),
+                now.ToString("yyyyMMddHH")
+            ];
+            Exception? firstError = null;
+
+            async Task TrySaveAsync(Func<Task> action)
+            {
+                try
+                {
+                    await action();
+                }
+                catch (Exception ex)
+                {
+                    firstError ??= ex;
+                }
+            }
 
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, month);
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, day);
-            await SaveStatsDimensionAsync(prefix, accountName, "message", message, hour);
+            foreach (string timeKey in timeKeys)
+            {
+                // Keep total and result writes adjacent and isolate every dimension. A single
+                // transient Redis error must not suppress all success/fail counters that follow.
+                await TrySaveAsync(() => SaveStatsCountAsync(prefix, accountName, timeKey));
+                await TrySaveAsync(() => SaveStatsCountAsync(prefix, accountName, timeKey, result));
+                await TrySaveAsync(() => SaveStatsDimensionAsync(prefix, accountName, "message", message, timeKey));
+                await TrySaveAsync(() => SaveStatsDimensionAsync(prefix, accountName, "reason", reason, timeKey));
+            }
 
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, month);
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, day);
-            await SaveStatsDimensionAsync(prefix, accountName, "reason", reason, hour);
+            if (firstError != null)
+            {
+                _ = new LoggerLibrary("TkLogCore", "SaveStatsAccountCache")
+                    .Info($"prefix={prefix}, accountName={accountName}")
+                    .Info(firstError.Message, firstError.StackTrace)
+                    .SaveAsync();
+            }
         }
 
         private static void SaveStatsCount(string prefix, string accountName, string timeKey, string? dimension = null)

+ 4 - 4
molilian.core/Core/log/jd.cs

@@ -143,10 +143,10 @@ namespace molilian.core
 
                 if (!TestParseCore.InWhitelist(response.ip, response.oaid))
                 {
-                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
-                        response.accountName, response.success, response.message, response.reason,
-                        response.deeplink_url);
-                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
                 }
 
                 if (response.success) await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);

+ 5 - 5
molilian.core/Core/log/pdd.cs

@@ -122,12 +122,12 @@ namespace molilian.core
                 response.elapsedTime = (int)ts.TotalMilliseconds;
                 _ = RedisKit.RPushAsync(queue_parse_pdd_key, response);
 
-                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
+                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
                 {
-                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
-                          response.accountName, response.success, response.message, response.reason,
-                          response.deeplink_url);
-                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
                 }
 
                 //if (response.success) saveClientRequestTotal(response.channel, response.ip, response.oaid);

+ 12 - 12
molilian.core/Core/log/taobao.cs

@@ -99,10 +99,18 @@ namespace molilian.core
             try
             {
                 var ts = DateTime.Now - response.create_time;
-                response.elapsedTime = (int)ts.TotalMilliseconds;
-                _ = RedisKit.RPushAsync(queue_parse_tb_key, response);
-
-                if (response.success || response.message.Equals("转链失败") || response.message.Equals("放弃转链"))
+                response.elapsedTime = (int)ts.TotalMilliseconds;
+                _ = RedisKit.RPushAsync(queue_parse_tb_key, response);
+
+                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
+                {
+                    await RecordParseMetricsAsync(
+                        response.channel.ToString(), response.accountId, response.accountName,
+                        response.success, response.message, response.reason, response.deeplink_url,
+                        response.riskStrategy, response.launchScene);
+                }
+
+                if (response.success || response.message.Equals("转链失败") || response.message.Equals("放弃转链"))
                 {
                     await RiskControlCore.CallsIncrByAsync(response.channel, response.accountId);
                     await saveClientRequestTotalAsync(response.channel, response.ip, response.oaid);
@@ -111,14 +119,6 @@ namespace molilian.core
                 {
                     _ = saveUnionCouponParseCacheAsync(response);
                 }
-                if (!TestParseCore.InWhitelist(response.ip, response.oaid))
-                {
-                    await SaveParseCacheAsync(response.channel.ToString(), response.accountId,
-                        response.accountName, response.success, response.message, response.reason,
-                        response.deeplink_url);
-                    await TracksCore.RecordParseResultAsync(response.channel.ToString(), response.riskStrategy, response.launchScene, response.accountId, response.success);
-                }
-
                 var account = await TkPoolCore.ALLGetOneAsync(response.accountId);
                 if (!string.IsNullOrEmpty(response.itemId)) TkOrderTrackingCore.SaveLinkSummary(response, account);
 

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

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov