Преглед изворни кода

feat(mode_workflow): 新增多平台图标并支持图标静态服务

- 新增小红书、公众号、视频号等多平台SVG图标文件
- 后端新增`/icons/`静态路由,安全提供图标文件访问
- 前端重构平台logo渲染逻辑,优化统计栏布局与样式
- 完善工序via字段的无效占位符过滤规则
刘文武 пре 1 дан
родитељ
комит
6523eefa3a

+ 1 - 0
examples/mode_workflow/icons/X.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781529810324" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3661" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 1024a512 512 0 1 1 512-512 512 512 0 0 1-512 512z m215.04-768h-56.32L535.654 431.718l5.632 8.295L414.976 256H256l196.096 284.826-174.387 227.02h55.705l145.05-188.825-9.728-14.439L608.41 767.846h158.976l-204.8-297.881zM626.176 712.5L350.208 311.346h47.411L673.587 712.5h-47.411z" p-id="3662"></path></svg>

+ 1 - 0
examples/mode_workflow/icons/bili.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781529371716" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11657" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M769.856 348.928H261.248a27.84 27.84 0 0 0-28.48 27.584v337.152a27.712 27.712 0 0 0 28.48 27.456h508.608a26.496 26.496 0 0 0 27.072-27.456V376.512a26.688 26.688 0 0 0-27.072-27.584zM295.872 473.536l143.36-27.456 10.816 53.76-141.888 27.456zM516.8 637.44c-44.032 48-90.24-15.168-90.24-15.168l23.488-15.168s31.424 56.704 66.432-18.432c33.6 72.96 70.784 19.2 70.784 19.2l21.312 13.696s-39.68 63.872-91.712 15.872z m212.672-110.144L587.2 499.84l11.2-53.824 142.976 27.456z" fill="#53D4F4" p-id="11658"></path><path d="M512 0a512 512 0 1 0 512 512 512 512 0 0 0-512-512z m269.632 809.408a462.4 462.4 0 0 0-47.872 0s-2.624 41.088-37.696 41.792a39.552 39.552 0 0 1-41.792-39.552c-21.44 0-279.552 1.152-279.552 1.152s-4.544 38.08-39.552 38.08a39.68 39.68 0 0 1-39.552-38.08c-22.976 0-53.888-0.768-53.888-0.768a123.52 123.52 0 0 1-87.808-117.184c1.152-100.992 0-300.8 0-300.8a117.376 117.376 0 0 1 85.504-119.808 5388.8 5388.8 0 0 1 161.984-1.536l-65.92-64s-10.176-12.8 7.168-27.136 18.432-8.512 24.512-4.352 98.304 95.104 98.304 95.104h-12.416c35.392 0 71.936 0.576 107.008 0.576 13.568-13.568 90.816-89.216 96-92.928s7.168-10.112 24.512 4.16 7.168 27.136 7.168 27.136l-64.448 62.144c88.512 0.768 156.736 1.152 156.736 1.152a120.896 120.896 0 0 1 89.6 119.424c-1.152 100.224 0.384 301.76 0.384 301.76s-4.864 97.92-88.512 113.408z" fill="#53D4F4" p-id="11659"></path></svg>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
examples/mode_workflow/icons/douyin.svg


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
examples/mode_workflow/icons/github.svg


+ 1 - 0
examples/mode_workflow/icons/gzh.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781528987227" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2933" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 0a512 512 0 0 1 512 512l-0.256 16A512 512 0 0 1 0 512l0.256-16A512 512 0 0 1 512 0z m132.224 429.184c-111.168 0-198.656 75.136-198.656 167.232 0 92.48 87.488 167.36 198.656 167.36 23.296 0 46.848-5.888 70.144-11.52l64.192 34.752-17.6-57.728c46.72-34.752 81.664-80.96 81.664-132.864 0-92.16-93.376-167.232-198.4-167.232zM416.448 256c-128.384 0-233.6 86.336-233.6 196.16 0 63.36 34.88 115.456 93.376 155.84l-23.36 69.44 81.728-40.64c29.184 5.632 52.736 11.52 81.728 11.52l10.88-0.256 10.88-0.768a164.736 164.736 0 0 1-7.168-48.192c0-100.736 87.552-182.464 198.656-182.464 7.488 0 14.912 0.64 22.464 1.28C631.936 324.928 531.264 256 416.512 256z m163.456 265.664c17.6 0 29.184 11.52 29.184 22.976 0 11.52-11.712 22.976-29.184 22.976-11.712 0-23.296-11.52-23.296-23.04 0-11.456 11.584-22.912 23.296-22.912z m128.576 0c17.472 0 29.184 11.52 29.184 22.976 0 11.52-11.968 22.976-29.184 22.976h-0.064c-11.648 0-23.232-11.52-23.232-23.04 0-11.456 11.584-22.912 23.296-22.912zM340.288 354.176c17.536 0 29.184 11.456 29.184 28.8 0 17.088-11.52 28.8-29.184 28.8-17.6 0-35.2-11.52-35.2-28.8 0-17.344 17.6-28.8 35.2-28.8z m163.52-0.128c17.536 0 29.184 11.52 29.184 28.8s-11.648 28.8-29.184 28.8c-17.664 0-35.2-11.52-35.2-28.8 0.32-17.28 17.856-28.8 35.2-28.8z" fill="#08A128" p-id="2934"></path></svg>

+ 1 - 0
examples/mode_workflow/icons/sph.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781529050518" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4873" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M298.2 334.7c-31.3-36-44.2-40.1-50.3-14.3-20.4 80.2 62.5 402.9 103.3 403.6 14.3 0 33.3-14.9 50.3-40.8 5.4-8.8 17-23.8 25.8-34 8.2-10.2 14.9-21.7 14.9-25.8 0-3.4 4.8-10.2 10.2-14.9 19.7-16.3 14.9-29.2-57.7-146.1-29.9-47.6-44.2-65.9-96.5-127.7zM779.2 326.5c-8.2-41.4-33.3-28.5-85.6 44.8-39.4 54.4-119.6 184.1-125 201.8-4.1 11.5 2 27.2 25.8 65.2 34.6 56.4 61.8 85.6 78.1 85.6 17.7 0 42.1-58.4 82.9-193.6 11.6-38.7 27.9-180 23.8-203.8z" fill="#FD8515" p-id="4874"></path><path d="M512 0C229.2 0 0 229.2 0 512s229.2 512 512 512 512-229.2 512-512S794.8 0 512 0z m296.4 635c-27.2 85.6-43.5 118.9-74.1 150.8-29.2 31.3-58.4 34.6-101.2 12.9-27.2-13.6-85.6-73.4-97.8-99.9-13.6-29.9-24.5-26.5-59.1 19-51.6 67.9-89 94.4-134.5 94.4-46.9 0-87-56.4-121.6-169.9-74.7-248-71.3-410.4 8.8-425.3 31.3-6.1 59.1 0.7 85.6 19 25.8 19 63.9 59.8 87 93.1 8.8 13.6 19 27.2 21.7 31.3 6.1 6.8 34 50.3 66.6 103.9 9.5 15.6 19 28.5 21.1 28.5 5.4 0 40.1-44.8 40.1-52.3 0-4.8 46.2-76.1 54.4-83.6 2-2 10.2-12.9 17-24.5 31.9-50.3 89.7-103.3 129.1-116.9 42.8-14.3 79.5 6.8 95.8 55.7 22.9 69.6 7.3 216.3-38.9 363.8z" fill="#FD8515" p-id="4875"></path></svg>

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
examples/mode_workflow/icons/weibo.svg


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
examples/mode_workflow/icons/xhs.svg


+ 1 - 0
examples/mode_workflow/icons/youtube.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781529388576" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12721" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512.254947 959.556658c247.190491 0 447.583279-200.392788 447.583279-447.569969 0-247.161822-200.392788-447.544371-447.583279-447.544371-247.188443 0-447.556658 200.382549-447.556657 447.544371 0 247.178204 200.368215 447.569968 447.556657 447.569969" fill="#E9644A" p-id="12722"></path><path d="M599.154143 512.218088l-146.531301 86.062681V426.14312l146.531301 86.074968z m136.892444 79.792407V431.989505s0-77.134401-77.14464-77.134401H365.584399s-77.094469 0-77.09447 77.134401v160.019966s0 77.12109 77.09447 77.12109h293.318572c-0.001024 0 77.143616 0 77.143616-77.120066" fill="#FFFFFF" p-id="12723"></path></svg>

+ 1 - 0
examples/mode_workflow/icons/zhihu.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781529106200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8324" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m-90.7 477.8l-0.1 1.5c-1.5 20.4-6.3 43.9-12.9 67.6l24-18.1 71 80.7c9.2 33-3.3 63.1-3.3 63.1l-95.7-111.9v-0.1c-8.9 29-20.1 57.3-33.3 84.7-22.6 45.7-55.2 54.7-89.5 57.7-34.4 3-23.3-5.3-23.3-5.3 68-55.5 78-87.8 96.8-123.1 11.9-22.3 20.4-64.3 25.3-96.8H264.1s4.8-31.2 19.2-41.7h101.6c0.6-15.3-1.3-102.8-2-131.4h-49.4c-9.2 45-41 56.7-48.1 60.1-7 3.4-23.6 7.1-21.1 0 2.6-7.1 27-46.2 43.2-110.7 16.3-64.6 63.9-62 63.9-62-12.8 22.5-22.4 73.6-22.4 73.6h159.7c10.1 0 10.6 39 10.6 39h-90.8c-0.7 22.7-2.8 83.8-5 131.4H519s12.2 15.4 12.2 41.7H421.3z m346.5 167h-87.6l-69.5 46.6-16.4-46.6h-40.1V321.5h213.6v387.3zM408.2 611s0-0.1 0 0z m216 94.3l56.8-38.1h45.6-0.1V364.7H596.7v302.5h14.1z" fill="#1296DB" p-id="8325"></path></svg>

+ 43 - 27
examples/mode_workflow/index.html

@@ -375,9 +375,22 @@
         margin-top: 7px;
       }
       .stat .sub.plat-break {
-        margin-top: 3px;
+        margin-top: 5px;
         color: var(--ink-soft);
         font-weight: 600;
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        gap: 4px 12px;
+      }
+      .stat .sub.plat-break .pb-item {
+        display: inline-flex;
+        align-items: center;
+        gap: 5px;
+      }
+      .stat .sub.plat-break .pb-item b {
+        font-size: 12px;
+        color: var(--ink);
       }
       .ring-row {
         display: flex;
@@ -684,7 +697,8 @@
         line-height: 0;
         flex: none;
       }
-      .plat-logo svg {
+      .plat-logo svg,
+      .plat-logo img {
         display: block;
       }
       .done-dot {
@@ -2263,32 +2277,28 @@
         ({ xhs: "xhs", gzh: "gzh", zhihu: "zhihu", douyin: "douyin", sph: "sph", youtube: "youtube", x: "x" })[p] ||
         "other";
       const PLAT_NAME = (p) =>
-        ({ xhs: "小红书", gzh: "公众号", zhihu: "知乎", douyin: "抖音", sph: "视频号", youtube: "YouTube", x: "X" })[
-          p
-        ] ||
+        ({
+          xhs: "小红书",
+          gzh: "公众号",
+          sph: "视频号",
+          github: "GitHub",
+          toutiao: "头条",
+          douyin: "抖音",
+          bili: "哔哩哔哩",
+          zhihu: "知乎",
+          weibo: "微博",
+          youtube: "YouTube",
+          x: "X",
+        })[p] ||
         p ||
         "?";
       /* 渠道 logo 徽标(品牌色圆角方块,hover 显示渠道名) */
-      const PLAT_LOGO = (p, size = 18) => {
-        const glyph = (bg, ch) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24">
-    <rect width="24" height="24" rx="5.5" fill="${bg}"/>
-    <text x="12" y="16.6" font-size="12" font-weight="700" fill="#fff" text-anchor="middle"
-          font-family="'Noto Sans SC',sans-serif">${ch}</text></svg>`;
-        const svgs = {
-          xhs: glyph("#ff2442", "红"),
-          zhihu: glyph("#0084ff", "知"),
-          gzh: glyph("#07c160", "公"),
-          douyin: glyph("#161823", "抖"),
-          sph: glyph("#fa6d20", "视"),
-          x: glyph("#15202b", "X"),
-          youtube: `<svg width="${size}" height="${size}" viewBox="0 0 24 24">
-      <rect width="24" height="24" rx="5.5" fill="#f00"/>
-      <polygon points="9.8,7.8 17,12 9.8,16.2" fill="#fff"/></svg>`,
-        };
-        return svgs[p]
-          ? `<span class="plat-logo" title="${PLAT_NAME(p)}">${svgs[p]}</span>`
+      /* 有对应 SVG 图标的渠道(文件位于 /icons/<key>.svg) */
+      const PLAT_ICONS = new Set(["xhs", "gzh", "sph", "github", "douyin", "bili", "zhihu", "weibo", "youtube"]);
+      const PLAT_LOGO = (p, size = 18) =>
+        PLAT_ICONS.has(p)
+          ? `<span class="plat-logo" title="${PLAT_NAME(p)}"><img src="/icons/${p}.svg" width="${size}" height="${size}" alt="${PLAT_NAME(p)}" loading="lazy"></span>`
           : `<span class="plat other">${esc(PLAT_NAME(p))}</span>`;
-      };
       const MODELS_PROC = ["anthropic/claude-sonnet-4-6", "google/gemini-3.1-flash-lite"];
       const MODELS_TOOL = ["google/gemini-3.1-flash-lite", "anthropic/claude-sonnet-4-6"];
       const scoreCls = (v) => (v == null ? "s0" : v >= 9 ? "s9" : v >= 8 ? "s8" : v >= 6 ? "s6" : "s0");
@@ -2324,12 +2334,18 @@
           p = d.process_data;
         // 进度百分比向下取整:200/201=99.5% 显示 99%,未真正做完不会显示 100%
         const pct = (a, b) => (b ? Math.floor((a / b) * 100) : 0);
-        const platBreak = (arr) => (arr || []).map(([k, n]) => `${PLAT_NAME(k)} ${n}`).join(" · ") || "—";
+        const platBreak = (arr) =>
+          (arr || [])
+            .map(
+              ([k, n]) =>
+                `<span class="pb-item">${PLAT_LOGO(k, 15)}<b class="num">${n}</b></span>`,
+            )
+            .join("") || "—";
         v.innerHTML = `
   <div class="dash-section"><h2>结果数据</h2><div class="rule"></div><span class="tag">RESULTS</span></div>
   <div class="cards">
-    <div class="card stat"><div class="lbl">采集帖子数量</div><div class="val num">${r.post_count}</div><div class="sub">${platBreak(r.collected_by_platform)}</div></div>
-    <div class="card stat t"><div class="lbl">解构帖子数量</div><div class="val num">${r.extracted_post_count}</div><div class="sub">${platBreak(r.extracted_by_platform)}</div></div>
+    <div class="card stat"><div class="lbl">采集帖子数量</div><div class="val num">${r.post_count}</div><div class="sub plat-break">${platBreak(r.collected_by_platform)}</div></div>
+    <div class="card stat t"><div class="lbl">解构帖子数量</div><div class="val num">${r.extracted_post_count}</div><div class="sub plat-break">${platBreak(r.extracted_by_platform)}</div></div>
     <div class="card stat a"><div class="lbl">工具数量</div><div class="val num">${r.tool_count}</div><div class="sub">mode_tools 去重工具名</div></div>
     <div class="card stat r"><div class="lbl">内容树覆盖节点</div><div class="val num">0</div>
       <div class="sub">0%</div></div>

+ 17 - 1
examples/mode_workflow/server.py

@@ -36,6 +36,8 @@ import db
 PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8772
 MATRIX_FILE = HERE / "reference" / "judged_matrix.json"
 LOG_DIR = HERE / "runs" / "logs"
+# 工序步骤 via 字段的「无具体工具」占位符,不计入工序提及工具 TOP 榜
+_VIA_PLACEHOLDERS = {"-", "—", "-", "--", "/", "无", "n/a", "none"}
 
 # 知识检索后端地址:从 .env 的 KNOWLEDGE_API_BASE 读取(db.py 已 load_dotenv)。
 # 注意:不能把它注入到 search.html 让浏览器直连——后端是明文 http://,而页面
@@ -154,7 +156,8 @@ def _dashboard():
                     if tp in t_idx and (a_idx[leaf], t_idx[tp]) in valid:
                         covered.add((a_idx[leaf], t_idx[tp]))
             via = (s.get("via") or "").strip()
-            if via:
+            # "-" / "—" / "无" 等是「无具体工具」占位符,不计入工具 TOP 榜
+            if via and via.lower() not in _VIA_PLACEHOLDERS:
                 via_counter[via] += 1
             for v in _split_values(s.get("substance")):
                 substance_counter[v] += 1
@@ -351,6 +354,19 @@ class Handler(BaseHTTPRequestHandler):
                 self._json(r) if r else self._err("未知 task_id", 404)
             elif u.path == "/api/img":
                 self._proxy_image(qs.get("u", ""))
+            elif u.path.startswith("/icons/") and u.path.endswith(".svg"):
+                # 渠道 logo 静态 SVG;按 basename 取文件,杜绝路径穿越
+                name = Path(u.path).name
+                f = HERE / "icons" / name
+                if not f.is_file():
+                    return self._err("not found", 404)
+                body = f.read_bytes()
+                self.send_response(200)
+                self.send_header("Content-Type", "image/svg+xml; charset=utf-8")
+                self.send_header("Content-Length", str(len(body)))
+                self.send_header("Cache-Control", "public, max-age=86400")
+                self.end_headers()
+                self.wfile.write(body)
             elif u.path.startswith("/api/v1/knowledge"):
                 self._proxy_knowledge()
             else:

Неке датотеке нису приказане због велике количине промена