Explorar o código

增加 搜同款的采样(已关闭)

dodo hold hai 2 meses
pai
achega
12b62f17ce

+ 111 - 0
molilian.api/Controllers/admin/DeeplinkReportController.cs

@@ -0,0 +1,111 @@
+using dodohold.core;
+using Microsoft.AspNetCore.Mvc;
+using molilian.core;
+using System.Text.Json;
+
+namespace molilian.api.Controllers
+{
+    [ApiController]
+    [MyAuthorize("admin")]
+    [Route("api/[controller]/[action]")]
+    public class DeeplinkReportController : ControllerBase
+    {
+        private const int MaxRangeDays = 90;
+
+        readonly IAuthorizationProvider provider = new AdminProvider();
+        protected IHttpContextAccessor _accessor;
+
+        public DeeplinkReportController(IHttpContextAccessor accessor)
+        {
+            _accessor = accessor;
+        }
+
+        [HttpPost]
+        public async Task<ActionResult> daily([FromBody] JsonElement form)
+        {
+            _ = provider.Get(_accessor.HttpContext);
+
+            var queryDate = form.PathReadArray<string>("query_date[]");
+            var channelName = form.Read("channel_name", string.Empty).Trim();
+
+            DateTime start = DateTime.Now.Date.AddDays(-6);
+            DateTime end = DateTime.Now.Date;
+
+            if (queryDate.Count == 2)
+            {
+                if (DateTime.TryParse(queryDate[0], out var parsedStart))
+                {
+                    start = parsedStart.Date;
+                }
+
+                if (DateTime.TryParse(queryDate[1], out var parsedEnd))
+                {
+                    end = parsedEnd.Date;
+                }
+            }
+
+            if (end < start)
+            {
+                (start, end) = (end, start);
+            }
+
+            if ((end - start).TotalDays >= MaxRangeDays)
+            {
+                end = start.AddDays(MaxRangeDays - 1);
+            }
+
+            var list = new List<DeeplinkDailyReportRow>();
+            var accountName = string.IsNullOrWhiteSpace(channelName)
+                ? "tool"
+                : channelName;
+
+            for (var date = start; date <= end; date = date.AddDays(1))
+            {
+                var dateKey = date.ToString("yyyyMMdd");
+                var row = new DeeplinkDailyReportRow
+                {
+                    report_date = date.ToString("yyyy-MM-dd"),
+                    total_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:{dateKey}"),
+                    success_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:success:{dateKey}"),
+                    fail_count = await TkLogCore.GetTotalAsync($":parse_total:{accountName}:fail:{dateKey}")
+                };
+                list.Add(row);
+            }
+
+            list = list.OrderByDescending(item => item.report_date).ToList();
+
+            var summary = new DeeplinkDailyReportSummary
+            {
+                total_count = list.Sum(item => item.total_count),
+                success_count = list.Sum(item => item.success_count),
+                fail_count = list.Sum(item => item.fail_count)
+            };
+
+            return new APIResult(new
+            {
+                data = new
+                {
+                    list,
+                    count = list.Count,
+                    summary,
+                    maxRangeDays = MaxRangeDays
+                }
+            });
+        }
+    }
+
+    public class DeeplinkDailyReportRow
+    {
+        public string report_date { get; set; } = string.Empty;
+        public long total_count { get; set; } = 0;
+        public long success_count { get; set; } = 0;
+        public long fail_count { get; set; } = 0;
+    }
+
+    public class DeeplinkDailyReportSummary
+    {
+        public long total_count { get; set; } = 0;
+        public long success_count { get; set; } = 0;
+        public long fail_count { get; set; } = 0;
+    }
+}

+ 43 - 1
molilian.api/Controllers/public/ApiController.cs

@@ -190,5 +190,47 @@ namespace molilian.api.Controllers
 
         }
 
+        [HttpGet]
+        public async Task<ActionResult> PromotionQuerySampleLogs([FromQuery] int n = 20)
+        {
+            int count = Math.Clamp(n, 1, 100);
+            string logDir = Path.Combine(AppContext.BaseDirectory, "log", "promotion_query_samples");
+
+            if (!Directory.Exists(logDir))
+            {
+                return new APIResult(new
+                {
+                    success = true,
+                    count = 0,
+                    logs = Array.Empty<object>()
+                });
+            }
+
+            var files = Directory.EnumerateFiles(logDir, "*.log", SearchOption.TopDirectoryOnly)
+                .Where(file => !Path.GetFileName(file).StartsWith("write_error_", StringComparison.OrdinalIgnoreCase))
+                .Select(file => new FileInfo(file))
+                .OrderByDescending(file => file.LastWriteTime)
+                .Take(count)
+                .ToList();
+
+            var logs = new List<object>();
+            foreach (var file in files)
+            {
+                logs.Add(new
+                {
+                    fileName = file.Name,
+                    writeTime = file.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+                    content = await System.IO.File.ReadAllTextAsync(file.FullName)
+                });
+            }
+
+            return new APIResult(new
+            {
+                success = true,
+                count = logs.Count,
+                logs
+            });
+        }
+
     }
-}
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
molilian.api/Properties/PublishProfiles/latest.pubxml.user


+ 1 - 1
molilian.api/Properties/launchSettings.json

@@ -16,7 +16,7 @@
         "CenterDB": "",
         "CenterRedis": ""
       },
-      "environmentVariables22": {
+      "environmentVariables222": {
         "ASPNETCORE_ENVIRONMENT": "Development",
         "EndPoint": "sh1",
         "NtfyServer": "https://ntfy.yunhui800.com/similar",

+ 887 - 0
promotion-query-logs.html

@@ -0,0 +1,887 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>PromotionQuery Logs</title>
+  <style>
+    :root {
+      color-scheme: light;
+      --bg: #f6f7f9;
+      --panel: #ffffff;
+      --text: #1f2328;
+      --muted: #68707d;
+      --border: #d8dee4;
+      --primary: #0969da;
+      --primary-hover: #0759b8;
+      --danger: #b42318;
+      --code-bg: #f0f3f6;
+    }
+
+    * {
+      box-sizing: border-box;
+    }
+
+    body {
+      margin: 0;
+      background: var(--bg);
+      color: var(--text);
+      font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+    }
+
+    main {
+      max-width: 1180px;
+      margin: 0 auto;
+      padding: 24px;
+    }
+
+    h1 {
+      margin: 0 0 16px;
+      font-size: 22px;
+      font-weight: 650;
+      letter-spacing: 0;
+    }
+
+    .toolbar {
+      display: grid;
+      grid-template-columns: minmax(260px, 1fr) 120px auto auto;
+      gap: 10px;
+      align-items: end;
+      margin-bottom: 14px;
+      padding: 14px;
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 8px;
+    }
+
+    label {
+      display: grid;
+      gap: 5px;
+      color: var(--muted);
+      font-size: 12px;
+      font-weight: 600;
+    }
+
+    input {
+      width: 100%;
+      height: 36px;
+      padding: 7px 9px;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      color: var(--text);
+      background: #fff;
+      font: inherit;
+    }
+
+    button {
+      height: 36px;
+      padding: 0 14px;
+      border: 1px solid var(--primary);
+      border-radius: 6px;
+      background: var(--primary);
+      color: #fff;
+      font: inherit;
+      font-weight: 600;
+      cursor: pointer;
+      white-space: nowrap;
+    }
+
+    button:hover {
+      background: var(--primary-hover);
+      border-color: var(--primary-hover);
+    }
+
+    button.secondary {
+      background: #fff;
+      color: var(--primary);
+    }
+
+    button.secondary:hover {
+      background: #f1f6fd;
+    }
+
+    .status {
+      margin: 0 0 14px;
+      color: var(--muted);
+    }
+
+    .status.error {
+      color: var(--danger);
+    }
+
+    .logs {
+      display: grid;
+      gap: 12px;
+    }
+
+    .log-item {
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 8px;
+      overflow: hidden;
+    }
+
+    .log-head {
+      display: grid;
+      grid-template-columns: minmax(0, 1fr) auto;
+      gap: 12px;
+      align-items: center;
+      padding: 12px 14px;
+      border-bottom: 1px solid var(--border);
+      background: #fbfcfd;
+    }
+
+    .log-title {
+      min-width: 0;
+    }
+
+    .file-name {
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-weight: 650;
+    }
+
+    .time {
+      color: var(--muted);
+      font-size: 12px;
+    }
+
+    .image-url {
+      display: block;
+      margin-top: 4px;
+      overflow: hidden;
+      color: var(--primary);
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      text-decoration: none;
+    }
+
+    .source-pill {
+      display: inline-block;
+      margin-top: 6px;
+      padding: 2px 7px;
+      border: 1px solid var(--border);
+      border-radius: 999px;
+      color: var(--muted);
+      background: #fff;
+      font-size: 12px;
+    }
+
+    .image-preview {
+      display: grid;
+      grid-template-columns: 112px minmax(0, 1fr);
+      gap: 12px;
+      align-items: center;
+      padding: 12px 14px;
+      border-bottom: 1px solid var(--border);
+      background: #fff;
+    }
+
+    .image-preview img {
+      width: 112px;
+      height: 112px;
+      object-fit: contain;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      background: #f8fafc;
+    }
+
+    .image-meta {
+      min-width: 0;
+      color: var(--muted);
+    }
+
+    .image-meta strong {
+      display: block;
+      margin-bottom: 4px;
+      color: var(--text);
+      font-weight: 650;
+    }
+
+    .response-view {
+      padding: 14px;
+      border-bottom: 1px solid var(--border);
+      background: #fff;
+    }
+
+    .response-title {
+      display: flex;
+      gap: 10px;
+      align-items: baseline;
+      justify-content: space-between;
+      margin-bottom: 12px;
+    }
+
+    .response-title strong {
+      font-size: 15px;
+    }
+
+    .response-title span {
+      color: var(--muted);
+      font-size: 12px;
+    }
+
+    .response-message {
+      margin: 0;
+      padding: 10px 12px;
+      color: var(--muted);
+      background: #f8fafc;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+    }
+
+    .product-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(285px, 1fr));
+      gap: 10px;
+    }
+
+    .product-card {
+      display: grid;
+      grid-template-columns: 92px minmax(0, 1fr);
+      gap: 10px;
+      min-width: 0;
+      padding: 10px;
+      border: 1px solid var(--border);
+      border-radius: 8px;
+      background: #fff;
+    }
+
+    .product-card img,
+    .product-image-empty {
+      width: 92px;
+      height: 92px;
+      object-fit: contain;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      background: #f8fafc;
+    }
+
+    .product-image-empty {
+      display: grid;
+      place-items: center;
+      color: var(--muted);
+      font-size: 12px;
+    }
+
+    .product-info {
+      min-width: 0;
+    }
+
+    .product-name {
+      display: -webkit-box;
+      overflow: hidden;
+      color: var(--text);
+      font-weight: 650;
+      line-height: 1.4;
+      text-decoration: none;
+      -webkit-line-clamp: 2;
+      -webkit-box-orient: vertical;
+    }
+
+    .product-name:hover {
+      color: var(--primary);
+    }
+
+    .product-price {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      align-items: baseline;
+      margin-top: 7px;
+    }
+
+    .sale-price {
+      color: var(--danger);
+      font-size: 16px;
+      font-weight: 700;
+    }
+
+    .origin-price {
+      color: var(--muted);
+      text-decoration: line-through;
+    }
+
+    .coupon {
+      padding: 1px 6px;
+      color: var(--danger);
+      border: 1px solid #f0b8b2;
+      border-radius: 999px;
+      background: #fff5f3;
+      font-size: 12px;
+    }
+
+    .product-meta {
+      margin-top: 6px;
+      color: var(--muted);
+      font-size: 12px;
+    }
+
+    .product-actions {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      margin-top: 8px;
+    }
+
+    .product-actions a {
+      padding: 3px 8px;
+      color: var(--primary);
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      text-decoration: none;
+      font-size: 12px;
+    }
+
+    .product-actions a:hover {
+      background: #f1f6fd;
+    }
+
+    .raw-log {
+      border-bottom: 1px solid var(--border);
+      background: var(--code-bg);
+    }
+
+    .raw-log summary {
+      padding: 10px 14px;
+      color: var(--muted);
+      background: #fbfcfd;
+      cursor: pointer;
+      user-select: none;
+    }
+
+    .log-body {
+      margin: 0;
+      max-height: 520px;
+      overflow: auto;
+      padding: 14px;
+      background: var(--code-bg);
+      font: 12px/1.55 "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+      white-space: pre-wrap;
+      word-break: break-word;
+    }
+
+    .empty {
+      padding: 32px 18px;
+      color: var(--muted);
+      text-align: center;
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 8px;
+    }
+
+    @media (max-width: 760px) {
+      main {
+        padding: 14px;
+      }
+
+      .toolbar {
+        grid-template-columns: 1fr;
+      }
+
+      .log-head {
+        grid-template-columns: 1fr;
+      }
+
+      .image-preview {
+        grid-template-columns: 84px minmax(0, 1fr);
+      }
+
+      .image-preview img {
+        width: 84px;
+        height: 84px;
+      }
+
+      .product-grid {
+        grid-template-columns: 1fr;
+      }
+    }
+  </style>
+</head>
+<body>
+  <main>
+    <h1>PromotionQuery 日志</h1>
+
+    <div class="toolbar">
+      <label>
+        API 地址
+        <input id="apiBase" autocomplete="off">
+      </label>
+      <label>
+        条数
+        <input id="count" type="number" min="1" max="100" value="20">
+      </label>
+      <button id="loadBtn" type="button">查询</button>
+      <button id="copyUrlBtn" class="secondary" type="button">复制接口</button>
+    </div>
+
+    <p id="status" class="status"></p>
+    <section id="logs" class="logs"></section>
+  </main>
+
+  <script>
+    const endpointPath = "/api/PromotionQuerySampleLogs";
+    const apiBaseInput = document.getElementById("apiBase");
+    const countInput = document.getElementById("count");
+    const loadBtn = document.getElementById("loadBtn");
+    const copyUrlBtn = document.getElementById("copyUrlBtn");
+    const statusEl = document.getElementById("status");
+    const logsEl = document.getElementById("logs");
+
+    apiBaseInput.value = "http://similar.molilian.com:5005";
+
+    function buildUrl() {
+      const base = apiBaseInput.value.replace(/\/+$/, "");
+      const count = Math.min(Math.max(Number(countInput.value) || 20, 1), 100);
+      countInput.value = String(count);
+      return `${base}${endpointPath}?n=${encodeURIComponent(count)}`;
+    }
+
+    function setStatus(text, isError = false) {
+      statusEl.textContent = text;
+      statusEl.classList.toggle("error", isError);
+    }
+
+    function getLogSection(content, name) {
+      const marker = new RegExp(`\\t${name}\\r?\\n`);
+      const match = marker.exec(content);
+      if (!match) {
+        return "";
+      }
+
+      const start = match.index + match[0].length;
+      const rest = content.slice(start);
+      const next = rest.search(/\r?\n\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\t/);
+      return (next >= 0 ? rest.slice(0, next) : rest).trim();
+    }
+
+    function normalizeImageUrl(value) {
+      const text = (value || "").trim();
+      if (text.startsWith("//")) {
+        return `https:${text}`;
+      }
+      return text;
+    }
+
+    function detectImageMime(base64) {
+      if (base64.startsWith("/9j/")) {
+        return "image/jpeg";
+      }
+      if (base64.startsWith("iVBOR")) {
+        return "image/png";
+      }
+      if (base64.startsWith("R0lGOD")) {
+        return "image/gif";
+      }
+      if (base64.startsWith("UklGR")) {
+        return "image/webp";
+      }
+      return "image/jpeg";
+    }
+
+    function buildImageSrcFromImg(value) {
+      const text = (value || "").trim();
+      if (!text) {
+        return "";
+      }
+      if (/^data:image\//i.test(text)) {
+        return text;
+      }
+      if (/^https?:\/\//i.test(text) || text.startsWith("//")) {
+        return normalizeImageUrl(text);
+      }
+
+      const base64 = text.replace(/\s/g, "");
+      if (base64.length < 32 || !/^[A-Za-z0-9+/=]+$/.test(base64)) {
+        return "";
+      }
+      return `data:${detectImageMime(base64)};base64,${base64}`;
+    }
+
+    function getClientPost(content) {
+      const text = getLogSection(content, "clientPost");
+      if (!text) {
+        return {};
+      }
+
+      try {
+        return JSON.parse(text);
+      } catch {
+        return {};
+      }
+    }
+
+    function getImageInfo(content) {
+      const loggedUrl = normalizeImageUrl(getLogSection(content, "imageUrl"));
+      if (loggedUrl) {
+        return {
+          source: "url参数",
+          src: loggedUrl,
+          href: loggedUrl,
+          label: loggedUrl
+        };
+      }
+
+      const post = getClientPost(content);
+      const requestUrl = normalizeImageUrl(post.url || post.pic || post.imageUrl || post.pictureUrl || "");
+      if (requestUrl) {
+        return {
+          source: "url参数",
+          src: requestUrl,
+          href: requestUrl,
+          label: requestUrl
+        };
+      }
+
+      const imgValue = post.img || post.image || "";
+      const imgSrc = buildImageSrcFromImg(imgValue);
+      if (!imgSrc) {
+        return null;
+      }
+
+      const isUrl = /^https?:\/\//i.test(imgSrc);
+      return {
+        source: isUrl ? "img参数URL" : "img参数base64",
+        src: imgSrc,
+        href: isUrl ? imgSrc : "",
+        label: isUrl ? imgSrc : `base64 图片,约 ${Math.round(String(imgValue).length / 1024)} KB`
+      };
+    }
+
+    function createImagePreview(imageInfo) {
+      if (!imageInfo) {
+        return null;
+      }
+
+      const preview = document.createElement("div");
+      preview.className = "image-preview";
+
+      const img = document.createElement("img");
+      img.src = imageInfo.src;
+      img.alt = imageInfo.source;
+      img.loading = "lazy";
+      img.referrerPolicy = "no-referrer";
+
+      const meta = document.createElement("div");
+      meta.className = "image-meta";
+
+      const title = document.createElement("strong");
+      title.textContent = imageInfo.source;
+      meta.appendChild(title);
+
+      if (imageInfo.href) {
+        const link = document.createElement("a");
+        link.className = "image-url";
+        link.href = imageInfo.href;
+        link.target = "_blank";
+        link.rel = "noreferrer";
+        link.textContent = imageInfo.label;
+        meta.appendChild(link);
+      } else {
+        const text = document.createElement("span");
+        text.textContent = imageInfo.label;
+        meta.appendChild(text);
+      }
+
+      preview.append(img, meta);
+      return preview;
+    }
+
+    function parseJson(text) {
+      if (!text) {
+        return null;
+      }
+
+      try {
+        return JSON.parse(text);
+      } catch {
+        return null;
+      }
+    }
+
+    function getResponseData(content) {
+      return parseJson(getLogSection(content, "responseJson"));
+    }
+
+    function getProducts(response) {
+      if (Array.isArray(response)) {
+        return response.filter((item) => item && typeof item === "object");
+      }
+
+      if (!response || typeof response !== "object") {
+        return [];
+      }
+
+      const containers = [
+        response,
+        response.data,
+        response.result,
+        response.response
+      ].filter((item) => item && typeof item === "object");
+
+      const keys = ["promotionImg", "PromotionImg", "items", "Items", "list", "List"];
+      for (const container of containers) {
+        if (Array.isArray(container)) {
+          return container.filter((item) => item && typeof item === "object");
+        }
+
+        for (const key of keys) {
+          if (Array.isArray(container[key])) {
+            return container[key].filter((item) => item && typeof item === "object");
+          }
+        }
+      }
+
+      return [];
+    }
+
+    function textNode(tag, className, text) {
+      const node = document.createElement(tag);
+      if (className) {
+        node.className = className;
+      }
+      node.textContent = text || "";
+      return node;
+    }
+
+    function money(value) {
+      const text = String(value ?? "").trim();
+      return text ? `¥${text}` : "";
+    }
+
+    function appendAction(container, href, text) {
+      const url = normalizeImageUrl(href || "");
+      if (!url) {
+        return;
+      }
+
+      const link = document.createElement("a");
+      link.href = url;
+      link.target = "_blank";
+      link.rel = "noreferrer";
+      link.textContent = text;
+      container.appendChild(link);
+    }
+
+    function createProductCard(product, index) {
+      const card = document.createElement("div");
+      card.className = "product-card";
+
+      const pic = normalizeImageUrl(product.pic || product.imageUrl || product.pictUrl || product.itemPic || "");
+      if (pic) {
+        const image = document.createElement("img");
+        image.src = pic;
+        image.alt = product.itemName || product.title || `商品 ${index + 1}`;
+        image.loading = "lazy";
+        image.referrerPolicy = "no-referrer";
+        card.appendChild(image);
+      } else {
+        card.appendChild(textNode("div", "product-image-empty", "无图"));
+      }
+
+      const info = document.createElement("div");
+      info.className = "product-info";
+
+      const nameText = product.itemName || product.title || product.goodsName || `商品 ${index + 1}`;
+      const name = document.createElement(product.h5_url ? "a" : "div");
+      name.className = "product-name";
+      name.textContent = nameText;
+      if (product.h5_url) {
+        name.href = normalizeImageUrl(product.h5_url);
+        name.target = "_blank";
+        name.rel = "noreferrer";
+      }
+      info.appendChild(name);
+
+      const prices = document.createElement("div");
+      prices.className = "product-price";
+      const sale = money(product.promotionPrice || product.zkFinalPrice || product.price);
+      if (sale) {
+        prices.appendChild(textNode("span", "sale-price", sale));
+      }
+      const origin = money(product.price);
+      if (origin && origin !== sale) {
+        prices.appendChild(textNode("span", "origin-price", origin));
+      }
+      const couponAmount = Number(product.couponAmount || 0);
+      if (couponAmount > 0) {
+        prices.appendChild(textNode("span", "coupon", `券 ${couponAmount}`));
+      }
+      info.appendChild(prices);
+
+      const metaParts = [
+        product.shopTitle,
+        product.provcity,
+        product.sellCountStr ? `销量 ${product.sellCountStr}` : "",
+        product.itemId ? `ID ${product.itemId}` : ""
+      ].filter(Boolean);
+      if (metaParts.length) {
+        info.appendChild(textNode("div", "product-meta", metaParts.join(" / ")));
+      }
+
+      const actions = document.createElement("div");
+      actions.className = "product-actions";
+      appendAction(actions, product.h5_url, "H5");
+      appendAction(actions, product.deeplink_url, "Deeplink");
+      appendAction(actions, product.clickTracks, "ClickTrack");
+      if (actions.children.length) {
+        info.appendChild(actions);
+      }
+
+      card.appendChild(info);
+      return card;
+    }
+
+    function createResponseView(response) {
+      const section = document.createElement("section");
+      section.className = "response-view";
+
+      const products = getProducts(response);
+      const title = document.createElement("div");
+      title.className = "response-title";
+      title.append(
+        textNode("strong", "", "响应商品"),
+        textNode("span", "", `${products.length} 个商品`)
+      );
+      section.appendChild(title);
+
+      if (!products.length) {
+        const message = [
+          response?.message,
+          response?.reason,
+          response?.code ? `code: ${response.code}` : ""
+        ].filter(Boolean).join(" / ");
+        section.appendChild(textNode("p", "response-message", message || "响应中没有商品列表"));
+        return section;
+      }
+
+      const grid = document.createElement("div");
+      grid.className = "product-grid";
+      products.forEach((product, index) => grid.appendChild(createProductCard(product, index)));
+      section.appendChild(grid);
+      return section;
+    }
+
+    function createRawLog(content) {
+      const details = document.createElement("details");
+      details.className = "raw-log";
+
+      const summary = document.createElement("summary");
+      summary.textContent = "Raw 日志";
+
+      const body = document.createElement("pre");
+      body.className = "log-body";
+      body.textContent = content || "";
+
+      details.append(summary, body);
+      return details;
+    }
+
+    function renderLogs(logs) {
+      logsEl.replaceChildren();
+
+      if (!logs.length) {
+        const empty = document.createElement("div");
+        empty.className = "empty";
+        empty.textContent = "暂无日志";
+        logsEl.appendChild(empty);
+        return;
+      }
+
+      logs.forEach((item) => {
+        const article = document.createElement("article");
+        article.className = "log-item";
+
+        const head = document.createElement("div");
+        head.className = "log-head";
+
+        const title = document.createElement("div");
+        title.className = "log-title";
+
+        const fileName = document.createElement("span");
+        fileName.className = "file-name";
+        fileName.textContent = item.fileName || "";
+
+        const time = document.createElement("span");
+        time.className = "time";
+        time.textContent = item.writeTime || "";
+
+        title.append(fileName, time);
+
+        const imageInfo = getImageInfo(item.content || "");
+        if (imageInfo) {
+          const source = document.createElement("span");
+          source.className = "source-pill";
+          source.textContent = imageInfo.source;
+          title.appendChild(source);
+        }
+
+        if (imageInfo?.href) {
+          const link = document.createElement("a");
+          link.className = "image-url";
+          link.href = imageInfo.href;
+          link.target = "_blank";
+          link.rel = "noreferrer";
+          link.textContent = imageInfo.label;
+          title.appendChild(link);
+        }
+
+        const copyBtn = document.createElement("button");
+        copyBtn.className = "secondary";
+        copyBtn.type = "button";
+        copyBtn.textContent = "复制 Raw";
+        copyBtn.addEventListener("click", async () => {
+          await navigator.clipboard.writeText(item.content || "");
+          copyBtn.textContent = "已复制";
+          setTimeout(() => copyBtn.textContent = "复制 Raw", 1200);
+        });
+
+        const preview = createImagePreview(imageInfo);
+        const response = getResponseData(item.content || "");
+        head.append(title, copyBtn);
+        article.append(head);
+        if (preview) {
+          article.appendChild(preview);
+        }
+        article.appendChild(createResponseView(response));
+        article.appendChild(createRawLog(item.content || ""));
+        logsEl.appendChild(article);
+      });
+    }
+
+    async function loadLogs() {
+      const url = buildUrl();
+      loadBtn.disabled = true;
+      setStatus(`请求中:${url}`);
+
+      try {
+        const response = await fetch(url, { cache: "no-store" });
+        if (!response.ok) {
+          throw new Error(`HTTP ${response.status}`);
+        }
+
+        const data = await response.json();
+        const logs = Array.isArray(data.logs) ? data.logs : [];
+        renderLogs(logs);
+        setStatus(`已加载 ${logs.length} 条,${new Date().toLocaleString()}`);
+      } catch (error) {
+        renderLogs([]);
+        setStatus(`查询失败:${error.message}`, true);
+      } finally {
+        loadBtn.disabled = false;
+      }
+    }
+
+    loadBtn.addEventListener("click", loadLogs);
+    copyUrlBtn.addEventListener("click", async () => {
+      await navigator.clipboard.writeText(buildUrl());
+      copyUrlBtn.textContent = "已复制";
+      setTimeout(() => copyUrlBtn.textContent = "复制接口", 1200);
+    });
+
+    loadLogs();
+  </script>
+</body>
+</html>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio