AutomationFlowsAI & RAG › Daily WeChat Chat Digest with AI

Daily WeChat Chat Digest with AI

Original n8n title: Wechat Daily Digest AI Cost Optimized

WeChat-Daily-Digest-AI-Cost-Optimized. Uses httpRequest, chainLlm, outputParserStructured, readWriteFile. Scheduled trigger; 26 nodes.

Cron / scheduled trigger★★★★☆ complexityAI-powered26 nodesHTTP RequestChain LlmOutput Parser StructuredRead Write FileOpenRouter ChatLm Chat Deep SeekGoogle Gemini Chat
AI & RAG Trigger: Cron / scheduled Nodes: 26 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow follows the Chainllm → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "WeChat-Daily-Digest-AI-Cost-Optimized",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "b0efabdd-948a-4e01-b233-68cd5f1906e8",
      "name": "Run every day at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        20,
        -540
      ],
      "timezone": "Asia/Shanghai"
    },
    {
      "parameters": {
        "url": "http://host.docker.internal:5030/api/v1/chatlog",
        "sendQuery": true,
        "specifyQuery": "json",
        "jsonQuery": "={\n  \"time\": \"{{ $json.date }}~{{ $json.date }}\",\n  \"talker\": \"{{ $json.group_name }}\"\n}",
        "options": {}
      },
      "id": "7341c497-4022-40e3-b8b2-0048700ccd10",
      "name": "Fetch Yesterday's Chatlog",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        60,
        -100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Node: Parse & Structure Data (Repaired)\n// Input: Raw chat log text from the previous node.\n// Output: A single, structured JSON object with correctly parsed messages.\n\nconst rawLog = $json.data;\nconst talkerName = $('ConfigureChatParameters').first().json.group_name;\n\nif (!rawLog) {\n  console.log(\"\u8f93\u5165\u6570\u636e\u4e3a\u7a7a\uff0c\u8bf7\u68c0\u67e5\u4e0a\u6e38\u8282\u70b9\u3002\");\n  return { error: \"Input data is empty or not found.\" };\n}\n\n// \u83b7\u53d6\u6628\u5929\u7684\u65e5\u671f\u4f5c\u4e3a\u6240\u6709\u6d88\u606f\u7684\u65e5\u671f\u524d\u7f00\nconst datePrefix = $('ConfigureChatParameters').first().json.date;\n\n\n// \u6309\u4e00\u4e2a\u6216\u591a\u4e2a\u7a7a\u884c\u6765\u5206\u5272\u6d88\u606f\u5757\uff0c\u8fd9\u66f4\u53ef\u9760\nconst messageBlocks = rawLog.trim().split(/\\n\\s*\\n/);\n\nconst messages = [];\nfor (const block of messageBlocks) {\n  if (block.trim() === \"\") continue;\n\n  const lines = block.trim().split('\\n');\n  const header = lines.shift(); // \u7b2c\u4e00\u884c\u662f \"\u53d1\u9001\u8005 HH:MM:SS\"\n\n  // \u6b63\u5219\u8868\u8fbe\u5f0f\uff0c\u7528\u4e8e\u4ece header \u4e2d\u6355\u83b7\u53d1\u9001\u8005\u548c\u65f6\u95f4\n  const match = header.match(/(.+) (\\d{2}:\\d{2}:\\d{2})$/);\n\n  // \u5982\u679c\u7b2c\u4e00\u884c\u4e0d\u7b26\u5408 \"\u53d1\u9001\u8005 \u65f6\u95f4\" \u7684\u683c\u5f0f\uff0c\u5219\u8df3\u8fc7\u8fd9\u4e2a\u5757\n  if (!match) {\n    console.log(\"\u8df3\u8fc7\u65e0\u6548\u7684\u6d88\u606f\u5757:\", block);\n    continue;\n  }\n\n  const senderName = match[1].trim();\n  const timeStr = match[2];\n  const content = lines.join('\\n').trim();\n\n  // \u7ec4\u5408\u65e5\u671f\u548c\u65f6\u95f4\uff0c\u521b\u5efa\u5b8c\u6574\u7684ISO\u65f6\u95f4\u6233\n  const isoTime = `${datePrefix}T${timeStr}+08:00`;\n\n  messages.push({\n    content: content,\n    contents: null,\n    sender_name: senderName,\n    talker_name: talkerName,\n    time: isoTime,\n    seq: messages.length + 1\n  });\n}\n\nconst now = new Date();\nconst result = {\n  step: \"1_structured_data\",\n  description: \"Parsed and structured data from raw text\",\n  date: $('ConfigureChatParameters').first().json.date,\n  timestamp: now.toISOString(),\n  sources: {\n    [talkerName]: {\n      message_count: messages.length,\n      messages: messages\n    }\n  },\n  statistics: {\n    total_sources: 1,\n    total_messages: messages.length,\n    sources_summary: {\n      [talkerName]: messages.length\n    }\n  }\n};\n\nconsole.log(\"\u6210\u529f\u83b7\u53d6 \" + messages.length +\" \u6761\u6d88\u606f from \" + talkerName)\n\nreturn [{ json: result }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        -100
      ],
      "id": "02d16e25-f519-4fdb-b7f1-b97edcbf4f6a",
      "name": "Parse & Structure Data"
    },
    {
      "parameters": {
        "jsCode": "// Node: Clean & Segment by Time (Repaired with Logs and Validation - Fixed)\n// Input: The structured JSON object from the \"Parse & Structure Data\" node.\n// Output: An array of time segments, with correct message counts.\n\nconst structuredData = $('Parse & Structure Data').first().json;\nconst sourceName = Object.keys(structuredData.sources)[0];\nconst allMessages = structuredData.sources[sourceName].messages;\n\nconsole.log(`\\n[blue]\u5f00\u59cb\u65f6\u95f4\u6bb5\u5206\u5272\u548c\u6e05\u6d17...[/blue]`);\nconsole.log(`[cyan]\u539f\u59cb\u6d88\u606f\u603b\u6570: ${allMessages.length} \u6761[/cyan]`);\n\nif (!allMessages || allMessages.length === 0) {\n  console.log(\"\u6ca1\u6709\u6d88\u606f\u9700\u8981\u5904\u7406\u3002\");\n  return [];\n}\n\n// 1. \u6e05\u6d17\u6d88\u606f\nconst cleanedMessages = allMessages.filter(msg => {\n  const hasSender = msg.sender_name && msg.sender_name.trim() !== \"\";\n  const hasContent = (msg.content && msg.content.trim() !== \"\") || (msg.contents && msg.contents !== null);\n  \n  // \u5f3a\u5316\u7cfb\u7edf\u6d88\u606f\u8fc7\u6ee4\u6761\u4ef6\n  const isSystemMessage = (msg.sender_name === \"\" || msg.sender_name === \"\u7cfb\u7edf\u6d88\u606f\") || \n                          (msg.content && (\n                            msg.content.includes(\"\u52a0\u5165\u4e86\u7fa4\u804a\") ||\n                            msg.content.includes(\"\u64a4\u56de\u4e86\u4e00\u6761\u6d88\u606f\") ||\n                            msg.content.includes(\"\u62cd\u4e86\u62cd\") ||\n                            msg.content.includes(\"\u4e0e\u7fa4\u91cc\u5176\u4ed6\u4eba\u90fd\u4e0d\u662f\u670b\u53cb\u5173\u7cfb\")\n                          ));\n\n  return hasSender && hasContent && !isSystemMessage;\n});\n\nconsole.log(`[cyan]\u6e05\u6d17\u540e\u6709\u6548\u6d88\u606f\u603b\u6570: ${cleanedMessages.length} \u6761 (\u8fc7\u6ee4\u6389\u7cfb\u7edf\u6d88\u606f\u7b49)[/cyan]`);\n\n// 2. \u5b9a\u4e49\u65f6\u95f4\u6bb5\nconst segmentsConfig = [\n  { name: \"\u51cc\u6668\", start_hour: 0, end_hour: 6, messages: [] },\n  { name: \"\u4e0a\u5348\", start_hour: 6, end_hour: 12, messages: [] },\n  { name: \"\u4e0b\u5348\", start_hour: 12, end_hour: 18, messages: [] },\n  { name: \"\u665a\u4e0a\", start_hour: 18, end_hour: 24, messages: [] }\n];\n\n// 3. \u5c06\u6e05\u6d17\u540e\u7684\u6d88\u606f\u653e\u5165\u65f6\u95f4\u6bb5\nfor (const message of cleanedMessages) {\n  try {\n    const msgHour = parseInt(message.time.substring(11, 13), 10);\n\n    if (isNaN(msgHour)) {\n        console.log(`[yellow]\u8b66\u544a: \u65e0\u6cd5\u89e3\u6790\u6d88\u606f\u65f6\u95f4\uff0c\u8df3\u8fc7: ${message.time}[/yellow]`);\n        continue; \n    }\n\n    let assigned = false;\n    for (const segment of segmentsConfig) {\n      if (msgHour >= segment.start_hour && msgHour < segment.end_hour) {\n        segment.messages.push(message);\n        assigned = true;\n        break;\n      }\n    }\n    if (!assigned) {\n        console.log(`[yellow]\u8b66\u544a: \u6d88\u606f\u672a\u5206\u914d\u5230\u4efb\u4f55\u65f6\u95f4\u6bb5\uff0c\u8df3\u8fc7: ${message.time} - ${message.sender_name}: ${message.content.substring(0, 50)}...[/yellow]`);\n    }\n  } catch (e) {\n    console.log(`[red]\u9519\u8bef: \u5904\u7406\u6d88\u606f\u65f6\u53d1\u751f\u5f02\u5e38: ${message.time} - ${e.message}[/red]`);\n  }\n}\n\n// 4. \u683c\u5f0f\u5316\u6700\u7ec8\u8f93\u51fa\nconst result = segmentsConfig\n  .filter(s => s.messages.length > 0)\n  .map(s => ({\n    json: {\n      name: s.name,\n      start_hour: s.start_hour,\n      end_hour: s.end_hour,\n      messages_log: s.messages.map(m => `${m.sender_name}: ${m.content}`).join('\\n---\\n'),\n      message_count: s.messages.length\n    }\n  }));\n\n// 5. \u6253\u5370\u6bcf\u4e2a\u65f6\u95f4\u6bb5\u7684\u65e5\u5fd7\nconsole.log(`\\n[blue]\u65f6\u95f4\u6bb5\u5206\u5272\u7ed3\u679c:[/blue]`);\nlet totalSegmentedMessages = 0;\nsegmentsConfig.forEach(s => {\n    if (s.messages.length > 0) {\n        console.log(`  \ud83d\udcc5 ${s.name} (${s.start_hour}-${s.end_hour}\u65f6): ${s.messages.length} \u6761\u6d88\u606f`);\n        totalSegmentedMessages += s.messages.length;\n    }\n});\n\n// 6. \u6570\u636e\u9a8c\u8bc1\nconsole.log(`\\n[blue]\u6570\u636e\u9a8c\u8bc1:[/blue]`);\nconsole.log(`  \u6e05\u6d17\u540e\u6d88\u606f\u603b\u6570: ${cleanedMessages.length} \u6761`);\nconsole.log(`  \u5206\u6bb5\u540e\u6d88\u606f\u603b\u6570: ${totalSegmentedMessages} \u6761`);\n\nif (cleanedMessages.length === totalSegmentedMessages) {\n  console.log(`[green]\u6570\u636e\u9a8c\u8bc1\u901a\u8fc7: \u6240\u6709\u6e05\u6d17\u540e\u7684\u6d88\u606f\u90fd\u5df2\u6210\u529f\u5206\u6bb5\u3002[/green]`);\n} else {\n  console.log(`[red]\u6570\u636e\u9a8c\u8bc1\u5931\u8d25: \u6e05\u6d17\u540e\u6d88\u606f (${cleanedMessages.length}) \u4e0e\u5206\u6bb5\u540e\u6d88\u606f (${totalSegmentedMessages}) \u6570\u91cf\u4e0d\u5339\u914d\u3002[/red]`);\n  // \u53ef\u4ee5\u9009\u62e9\u629b\u51fa\u9519\u8bef\u6765\u4e2d\u65ad\u5de5\u4f5c\u6d41\uff0c\u6216\u8005\u7ee7\u7eed\u6267\u884c\n  // throw new Error(\"\u6d88\u606f\u6570\u91cf\u4e0d\u5339\u914d\uff0c\u6570\u636e\u5904\u7406\u5f02\u5e38\u3002\");\n}\n\nconsole.log(`[green]\u65f6\u95f4\u6bb5\u5206\u5272\u548c\u6e05\u6d17\u5b8c\u6210[/green]`);\n\nreturn result;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        -100
      ],
      "id": "b9d5acd4-0559-47af-8419-dae86fb52fc7",
      "name": "Clean & Segment by Time"
    },
    {
      "parameters": {
        "content": "## \u7ed3\u6784\u5316\u521d\u59cb\u6570\u636e\n \n\u5929\u6570\u636e\u83b7\u53d6\u4e0e\u9884\u5904\u7406\uff08\u5305\u542b\u89e3\u6790\u3001\u6e05\u6d17\u3001\u5206\u6bb5\u3001\u94fe\u63a5\u63d0\u53d6\u3001\u6d3b\u8dc3\u7528\u6237\u7edf\u8ba1\u3001\u6d88\u606f\u6837\u672c\u51c6\u5907\uff09",
        "height": 260,
        "width": 860,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -200
      ],
      "id": "00d281ec-7a40-4921-a4e4-a034d921d47e",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## \u804a\u5929\u6570\u636e\u65f6\u95f4\u5206\u6bb5\n",
        "height": 260,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        -200
      ],
      "id": "3cef8b1c-c532-45c4-8c12-b28340a07cfc",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## \u8bdd\u9898\u5206\u6790\n   * \u5206\u800c\u6cbb\u4e4b: \u5c06\u957f\u65e5\u5fd7\u5206\u5272\u6210\u5c0f\u5757\uff0c\u9010\u4e2a\u5904\u7406\u3002\n   * \u6210\u672c\u63a7\u5236: \u5728\u7c97\u52a0\u5de5\u9636\u6bb5\u4f7f\u7528\u5ec9\u4ef7\u6a21\u578b\u3002\n   * \u7ed3\u6784\u5316\u8f93\u51fa: \u5f3a\u5236 AI \u8fd4\u56de JSON \u683c\u5f0f\uff0c\u4fbf\u4e8e\u540e\u7eed\u5904\u7406\u3002\n   * \u8bdd\u9898\u805a\u5408: \u5c06\u6240\u6709\u65f6\u95f4\u6bb5\u7684\u8bdd\u9898\u5408\u5e76\uff0c\u4e3a\u6700\u7ec8\u62a5\u544a\u505a\u51c6\u5907\u3002\n",
        "height": 420,
        "width": 1140,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        120
      ],
      "id": "ae96b3b5-25af-4379-b108-b83905e1c5b8",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "65cd41dc-69c0-47bb-b334-299a04eafc44",
      "name": "Split in Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 2,
      "position": [
        20,
        260
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\u4f60\u662f\u4e13\u4e1a\u7684\u804a\u5929\u8bb0\u5f55\u5206\u6790\u4e13\u5bb6\u3002\u4f60\u7684\u4efb\u52a1\u662f\u5206\u6790\u7ed9\u5b9a\u7684\u804a\u5929\u8bb0\u5f55\u7247\u6bb5\uff0c\u5e76\u4e25\u683c\u6309\u7167\u8981\u6c42\u63d0\u53d6\u6838\u5fc3\u8bdd\u9898\u3002\n\n### \u6838\u5fc3\u5206\u6790\u539f\u5219 (\u5fc5\u987b\u4e25\u683c\u9075\u5b88)\n\n1.  **\u8bdd\u9898\u8bc6\u522b\u6807\u51c6**:\n    *   **\u6570\u91cf\u95e8\u69db**: \u81f3\u5c11\u9700\u89813\u6761\u76f8\u5173\u7684\u6d88\u606f\u624d\u80fd\u6784\u6210\u4e00\u4e2a\u72ec\u7acb\u7684\u8bdd\u9898\u3002\n    *   **\u5185\u5bb9\u4ef7\u503c**: \u8bdd\u9898\u5185\u5bb9\u5fc5\u987b\u6709\u660e\u786e\u7684\u8ba8\u8bba\u4e3b\u9898\u548c\u4ef7\u503c\u3002\n    *   **\u65e0\u6548\u5185\u5bb9\u8fc7\u6ee4**: \u4e25\u7981\u5c06\u65e0\u610f\u4e49\u7684\u95ee\u5019\uff08\u5982\u201c\u65e9\u201d\u3001\u201c\u5728\u5417\u201d\uff09\u3001\u8868\u60c5\u7b26\u53f7\u3001\u6216\u7b80\u5355\u7684\u786e\u8ba4\uff08\u5982\u201c\u597d\u7684\u201d\u3001\u201c\u6536\u5230\u201d\uff09\u4f5c\u4e3a\u72ec\u7acb\u8bdd\u9898\u3002\n\n2.  **\u8bdd\u9898\u8fb9\u754c\u5224\u65ad**:\n    *   **\u4e3b\u9898\u5ef6\u7eed**: \u5982\u679c\u6709\u65b0\u6210\u5458\u52a0\u5165\u8ba8\u8bba\uff0c\u4f46\u8ba8\u8bba\u7684\u8fd8\u662f\u540c\u4e00\u4e2a\u6838\u5fc3\u4e3b\u9898\uff0c\u8fd9\u5c5e\u4e8e**\u540c\u4e00\u8bdd\u9898**\u3002\n    *   **\u4e3b\u9898\u8f6c\u53d8**: \u5f53\u8ba8\u8bba\u7684\u7126\u70b9\u53d1\u751f\u660e\u663e\u3001\u4e0d\u76f8\u5173\u7684\u8f6c\u53d8\u65f6\uff0c\u5fc5\u987b\u521b\u5efa**\u65b0\u8bdd\u9898**\u3002\n    *   **\u65f6\u95f4\u95f4\u9694**: \u5982\u679c\u4e24\u6761\u6d88\u606f\u4e4b\u95f4\u6ca1\u6709\u76f4\u63a5\u5173\u8054\uff0c\u4e14\u65f6\u95f4\u95f4\u9694\u8d85\u8fc7 **30\u5206\u949f**\uff0c\u5e94\u89c6\u4e3a**\u65b0\u8bdd\u9898**\u3002\n\n### \u5185\u5bb9\u751f\u6210\u8981\u6c42\n\n1.  **\u8bdd\u9898\u6807\u9898 (`topic_title`)**: \u751f\u6210\u4e00\u4e2a\u7b80\u6d01\u3001\u7cbe\u51c6\u3001\u80fd\u6982\u62ec\u8bdd\u9898\u6838\u5fc3\u7684\u6807\u9898\u3002\n2.  **\u8bdd\u9898\u63cf\u8ff0 (`topic_description`)**:\n    *   **\u98ce\u683c**: \u4f7f\u7528\u751f\u52a8\u3001\u63a5\u5730\u6c14\u3001\u7565\u5e26\u201c\u9a9a\u8bdd\u201d\u98ce\u683c\u7684\u5e74\u8f7b\u4eba\u7f51\u7edc\u7528\u8bed\u3002\n    *   **\u5185\u5bb9**: \u63cf\u8ff0\u5fc5\u987b\u51c6\u786e\u53cd\u6620\u8ba8\u8bba\u7684\u5b9e\u9645\u5185\u5bb9\u3001\u5173\u952e\u4fe1\u606f\u548c\u4e3b\u8981\u89c2\u70b9\u3002\n    *   **\u957f\u5ea6**: \u4e25\u683c\u63a7\u5236\u5728 100 \u5230 150 \u5b57\u4e4b\u95f4\u3002\n\n### \u7edd\u5bf9\u8f93\u51fa\u8981\u6c42 (\u6700\u91cd\u8981)\n\n1.  **\u7eafJSON**: **\u53ea\u5141\u8bb8**\u8f93\u51fa\u6709\u6548\u7684JSON\u5bf9\u8c61\u3002\n2.  **\u7981\u6b62Markdown**: **\u4e25\u7981**\u5728\u8f93\u51fa\u7684\u4efb\u4f55\u4f4d\u7f6e\u4f7f\u7528 ` ```json ` \u6216 ` ``` ` \u8fdb\u884c\u5305\u88f9\u3002\n3.  **\u7981\u6b62\u89e3\u91ca**: **\u4e25\u7981**\u5728JSON\u5bf9\u8c61\u524d\u540e\u6dfb\u52a0\u4efb\u4f55\u5f62\u5f0f\u7684\u89e3\u91ca\u6027\u6587\u5b57\u6216\u6ce8\u91ca\u3002\n4.  **\u7fa4\u7ec4\u540d**: {{ $('ConfigureChatParameters').item.json.group_name }}\n\n---\n### \u8f93\u51faJSON\u683c\u5f0f (\u4e25\u683c\u9075\u5b88\u6b64\u7ed3\u6784)\n\n1. \u5fc5\u987b\u8fd4\u56de\u6709\u6548\u7684 JSON \u683c\u5f0f\n2. \u4e0d\u8981\u5305\u542b\u4efb\u4f55\u89e3\u91ca\u6587\u5b57\u6216 markdown \u683c\u5f0f\n3. \u4e0d\u8981\u4f7f\u7528 ```json``` \u4ee3\u7801\u5757\n4. \u4e25\u683c\u6309\u7167\u4ee5\u4e0b\u683c\u5f0f\u8fd4\u56de\n\nJSON \u683c\u5f0f\u793a\u4f8b\uff1a\n{\n  \"analysis_id\": \"\u7fa4\u7ec4\u540d_\u65e5\u671f_\u65f6\u95f4\u6bb5_analysis\",\n  \"meta\": {\n    \"source\": \"\u7fa4\u7ec4\u540d\",\n    \"date\": \"\u65e5\u671f\", \n    \"segment\": \"\u65f6\u95f4\u6bb5\",\n    \"total_messages\": 50\n  },\n  \"topics\": [\n    {\n      \"topic_id\": \"\u8bdd\u98981\u7684ID\",\n      \"topic_title\": \"\u8bdd\u98981\u7684\u6807\u9898\",\n      \"topic_description\": \"\u8bdd\u98981\u7684\u8be6\u7ec6\u63cf\u8ff0\uff0c\u5305\u542b\u5173\u952e\u8bcd\u548c\u4e3b\u8981\u5185\u5bb9\uff0c\u81f3\u5c1150\u5b57\",\n      \"is_off_topic\": false,\n      \"topic_start_time\": \"\u8bdd\u9898\u5f00\u59cb\u65f6\u95f4 (ISO\u683c\u5f0f\uff0c\u4f8b\u5982: 2023-07-03T14:30:00+08:00)\",\n      \"topic_end_time\": \"\u8bdd\u9898\u7ed3\u675f\u65f6\u95f4 (ISO\u683c\u5f0f\uff0c\u4f8b\u5982: 2023-07-03T15:00:00+08:00)\",\n      \"message_seqs\": [\"\u6784\u6210\u8be5\u8bdd\u9898\u7684\u6240\u6709\u6d88\u606f\u7684seq\u5217\u8868\uff0c\u4f8b\u5982: 1, 5, 8\"]\n    }\n  ]\n}\n\n\u91cd\u8981\uff1a\u53ea\u8fd4\u56de JSON\uff0c\u4e0d\u8981\u4efb\u4f55\u5176\u4ed6\u5185\u5bb9\u3002\n\n---\n### \u5f85\u5206\u6790\u7684\u804a\u5929\u8bb0\u5f55\n\n{{ JSON.stringify($json) }}",
        "hasOutputParser": true,
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        560,
        240
      ],
      "id": "0c08e2ee-bb11-45c9-80ee-836b061bb6ef",
      "name": "Extract Topics (Low Cost AI)"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"required\": [\"analysis_id\", \"meta\", \"topics\"],\n  \"properties\": {\n    \"analysis_id\": {\n      \"type\": \"string\",\n      \"description\": \"\u552f\u4e00\u5206\u6790\u6807\u8bc6\u7b26\uff0c\u683c\u5f0f\u4e3a\u7fa4\u7ec4\u540d_\u65e5\u671f_\u65f6\u95f4\u6bb5_analysis\",\n      \"pattern\": \"^.+_.+_.+_analysis$\"\n    },\n    \"meta\": {\n      \"type\": \"object\",\n      \"required\": [\"source\", \"date\", \"segment\", \"total_messages\"],\n      \"properties\": {\n        \"source\": {\n          \"type\": \"string\",\n          \"description\": \"\u6570\u636e\u6765\u6e90\u7684\u7fa4\u7ec4\u540d\u79f0\"\n        },\n        \"date\": {\n          \"type\": \"string\",\n          \"description\": \"\u5206\u6790\u65e5\u671f\",\n          \"format\": \"date\"\n        },\n        \"segment\": {\n          \"type\": \"string\",\n          \"description\": \"\u65f6\u95f4\u6bb5\u6807\u8bc6\"\n        },\n        \"total_messages\": {\n          \"type\": \"integer\",\n          \"description\": \"\u6d88\u606f\u603b\u6570\u7edf\u8ba1\",\n          \"minimum\": 0\n        }\n      }\n    },\n    \"topics\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\"topic_id\", \"topic_title\", \"topic_description\", \"is_off_topic\", \"topic_start_time\", \"topic_end_time\", \"message_seqs\"],\n        \"properties\": {\n          \"topic_id\": {\n            \"type\": \"string\",\n            \"description\": \"\u8bdd\u9898\u552f\u4e00\u6807\u8bc6\u7b26\"\n          },\n          \"topic_title\": {\n            \"type\": \"string\",\n            \"description\": \"\u8bdd\u9898\u6807\u9898\"\n          },\n          \"topic_description\": {\n            \"type\": \"string\",\n            \"description\": \"\u8bdd\u9898\u8be6\u7ec6\u63cf\u8ff0\uff0c\u5305\u542b\u5173\u952e\u8bcd\u548c\u4e3b\u8981\u5185\u5bb9\uff0c\u81f3\u5c1150\u5b57\",\n            \"minLength\": 50\n          },\n          \"is_off_topic\": {\n            \"type\": \"boolean\",\n            \"description\": \"\u6807\u8bb0\u662f\u5426\u504f\u79bb\u4e3b\u9898\"\n          },\n          \"topic_start_time\": {\n            \"type\": \"string\",\n            \"description\": \"\u8bdd\u9898\u5f00\u59cb\u65f6\u95f4\",\n            \"format\": \"date-time\"\n          },\n          \"topic_end_time\": {\n            \"type\": \"string\",\n            \"description\": \"\u8bdd\u9898\u7ed3\u675f\u65f6\u95f4\",\n            \"format\": \"date-time\"\n          },\n          \"message_seqs\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"description\": \"\u6d88\u606f\u5e8f\u5217\u53f7\"\n            },\n            \"minItems\": 1\n          }\n        }\n      },\n      \"minItems\": 1\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        720,
        420
      ],
      "id": "3c1ff1c6-50d3-4b17-a925-56a188f4ba44",
      "name": "Structured Output Parser"
    },
    {
      "parameters": {
        "jsCode": "// Author: Gemini\n// Date: 2025-07-04\n// Description: This version corrects the data access path within the .map() function.\n// It now correctly reads `item.json.output` based on the actual output from the LLM node,\n// resolving the 'Cannot read properties of undefined' error.\n\n/**\n * Robustly maps a segment string to a Chinese time segment name.\n * @param {string} segmentInput - The segment string, e.g., \"19:30-20:00\" or \"\u508d\u665a\".\n * @returns {string} The corresponding Chinese segment name, e.g., \"\u665a\u4e0a\".\n */\nfunction mapTimeToChineseSegment(segmentInput) {\n  if (!segmentInput || typeof segmentInput !== 'string') {\n    return \"\u672a\u77e5\u65f6\u6bb5\";\n  }\n  const startHour = parseInt(segmentInput.split(':')[0], 10);\n  if (isNaN(startHour)) {\n    return segmentInput;\n  }\n  if (startHour >= 0 && startHour < 6) return \"\u6df1\u591c\";\n  if (startHour >= 6 && startHour < 9) return \"\u65e9\u6668\";\n  if (startHour >= 9 && startHour < 12) return \"\u4e0a\u5348\";\n  if (startHour >= 12 && startHour < 14) return \"\u4e2d\u5348\";\n  if (startHour >= 14 && startHour < 18) return \"\u4e0b\u5348\";\n  if (startHour >= 18 && startHour < 22) return \"\u665a\u4e0a\";\n  if (startHour >= 22 && startHour <= 23) return \"\u6df1\u591c\";\n  return \"\u672a\u77e5\u65f6\u6bb5\";\n}\n\n// --- Main Logic (Rebuilt) ---\n\n// **THE ONLY FIX IS ON THIS LINE**\n// Correctly access the data from the actual LLM output structure.\nconst analysisResults = items.map(item => item.json.output);\n\nif (!analysisResults || analysisResults.length === 0) {\n  return { json: { error: \"Input data is empty or invalid.\" } };\n}\n\nconst all_topics = [];\nconst sources_info = {};\nconst segments_info = {};\nlet topic_counter = 1;\n\n// A single, robust loop to process all inputs and aggregate correctly.\nfor (const result of analysisResults) {\n  if (!result || !result.topics || !Array.isArray(result.topics)) {\n    continue;\n  }\n\n  const source_name = result.meta.source;\n  const chinese_segment_name = mapTimeToChineseSegment(result.meta.segment);\n  const created_at = result.created_at || new Date().toISOString();\n  const segment_key = `${source_name}_${chinese_segment_name}`;\n\n  // --- Step 1: Aggregate Metadata in Real-time ---\n  if (!segments_info[segment_key]) {\n    segments_info[segment_key] = {\n      source_name: source_name,\n      segment_name: chinese_segment_name,\n      analysis_id: `${source_name}_${result.meta.date}_${chinese_segment_name}_analysis`,\n      created_at: created_at,\n      meta: {\n        source: source_name,\n        date: result.meta.date,\n        segment: chinese_segment_name,\n        total_messages: 0,\n      },\n      topics_count: 0,\n    };\n  }\n  segments_info[segment_key].topics_count += result.topics.length;\n  segments_info[segment_key].meta.total_messages += (result.meta.total_messages || 0);\n\n  // --- Step 2: Process Topics and Link to the *Correct* Aggregated Metadata ---\n  for (const topic of result.topics) {\n    const topic_seq = topic.topic_seq || [];\n    const message_count = topic_seq.length;\n\n    const topic_data = {\n      physical_id: `topic_${String(topic_counter).padStart(3, '0')}`,\n      original_topic_id: topic.topic_id,\n      topic_title: topic.topic_title,\n      topic_description: topic.topic_description,\n      is_off_topic: topic.is_off_topic,\n      topic_seq: topic_seq,\n      message_count: (topic.message_seqs || []).length,\n      chat_count:  (topic.message_seqs || []).length,\n      topic_start_time: topic.topic_start_time || new Date().toISOString(),\n      topic_end_time: topic.topic_end_time || new Date().toISOString(),\n      source_name: source_name,\n      segment_name: chinese_segment_name,\n      analysis_id: segments_info[segment_key].analysis_id,\n      created_at: created_at,\n      analysis_meta: segments_info[segment_key].meta,\n    };\n\n    all_topics.push(topic_data);\n    topic_counter++;\n  }\n}\n\n// --- Step 3: Final Aggregation for sources_metadata (can be done after the loop) ---\nall_topics.forEach(topic => {\n    if (!sources_info[topic.source_name]) {\n        sources_info[topic.source_name] = { source_name: topic.source_name, segments: [], total_topics: 0 };\n    }\n    if (!sources_info[topic.source_name].segments.includes(topic.segment_name)) {\n        sources_info[topic.source_name].segments.push(topic.segment_name);\n    }\n});\nObject.keys(sources_info).forEach(sourceName => {\n    sources_info[sourceName].total_topics = all_topics.filter(t => t.source_name === sourceName).length;\n});\n\n\n// --- Final Assembly ---\nconst on_topic_count = all_topics.filter(t => !t.is_off_topic).length;\nconst off_topic_count = all_topics.filter(t => t.is_off_topic).length;\nconst total_message_seqs = all_topics.reduce((sum, t) => sum + t.message_count, 0);\nconst total_original_messages = Object.values(segments_info).reduce((sum, s) => sum + s.meta.total_messages, 0);\nconst topics_by_source = Object.entries(sources_info).reduce((acc, [key, value]) => {\n  acc[key] = value.total_topics;\n  return acc;\n}, {});\n\nconst physical_merged = {\n  step: \"2_topic_merged\",\n  description: \"\u65f6\u95f4\u5206\u6bb5\u5206\u6790\u540e\u7684\u6240\u6709\u8bdd\u9898\u7269\u7406\u5408\u5e76\uff0c\u672a\u8fdb\u884c\u8bed\u4e49\u5408\u5e76\",\n  merge_timestamp: new Date().toISOString(),\n  merge_date: analysisResults[0]?.meta?.date || new Date().toISOString().split('T')[0],\n  merge_type: \"physical\",\n  total_topics: all_topics.length,\n  total_sources: Object.keys(sources_info).length,\n  total_segments: Object.keys(segments_info).length,\n  all_topics: all_topics,\n  sources_metadata: sources_info,\n  segments_metadata: segments_info,\n  statistics: {\n    on_topic_count,\n    off_topic_count,\n    total_message_seqs,\n    total_original_messages,\n    topics_by_source\n  }\n};\n\nreturn { json: physical_merged };"
      },
      "id": "1ea63cbf-cd4f-4f95-8876-99fbae4b4bd8",
      "name": "Merge & Deduplicate Topics",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        280
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=\u4f60\u662f\u804a\u5929\u8bdd\u9898\u667a\u80fd\u5408\u5e76\u4e13\u5bb6\u3002\u4f60\u7684\u6838\u5fc3\u4efb\u52a1\u662f\u5206\u6790\u5df2\u7269\u7406\u5408\u5e76\u7684\u8bdd\u9898\u5217\u8868\uff0c\u627e\u51fa\u8bed\u4e49\u76f8\u5173\u7684\u8bdd\u9898\uff0c\u5e76\u8f93\u51fa\u4e00\u4e2a\u5305\u542b\u5408\u5e76\u5173\u7cfb\u548cAI\u603b\u7ed3\u5185\u5bb9\u7684\u65b0JSON\u3002\n\n### \u6838\u5fc3\u5408\u5e76\u539f\u5219 (\u5fc5\u987b\u4e25\u683c\u9075\u5b88)\n\n1.  **\u8bed\u4e49\u4e3a\u738b**: \u5fc5\u987b\u57fa\u4e8e\u8bdd\u9898\u63cf\u8ff0 (`topic_description`) \u7684**\u6df1\u5c42\u8bed\u4e49**\u8fdb\u884c\u5408\u5e76\uff0c\u800c\u4e0d\u662f\u7b80\u5355\u7684\u5173\u952e\u8bcd\u5339\u914d\u3002\u4f8b\u5982\uff0c\u201cClaude\u8fde\u4e0d\u4e0a\u201d\u548c\u201c\u6211\u7684Claude\u4eca\u5929\u54cd\u5e94\u5f88\u6162\u201d\u5e94\u8be5\u5408\u5e76\uff0c\u4f46\u201cClaude\u4f7f\u7528\u6280\u5de7\u201d\u548c\u201cGemini\u4f7f\u7528\u6280\u5de7\u201d\u5219**\u7edd\u5bf9\u4e0d\u80fd**\u5408\u5e76\u3002\n2. **\u5408\u5e76\u6761\u4ef6**\uff1a\n   - \u8bed\u4e49\u76f8\u4f3c\u5ea6 > 0.7\n   - \u53c2\u4e0e\u8005\u91cd\u53e0\u5ea6 > 0.3\n   - \u65f6\u95f4\u8de8\u5ea6\u5408\u7406\uff08\u4e0d\u8d85\u8fc74\u5c0f\u65f6\uff09\n3.  **\u4fdd\u5b88\u7b56\u7565**: \n    - \u5b81\u53ef\u4fdd\u7559\u66f4\u591a\u72ec\u7acb\u7684\u3001\u6709\u4ef7\u503c\u7684\u5c0f\u8bdd\u9898\uff0c\u4e5f\u7edd\u4e0d\u9519\u8bef\u5730\u5c06\u4e0d\u76f8\u5173\u7684\u8bdd\u9898\u5408\u5e76\u6210\u4e00\u4e2a\u6a21\u7cca\u7684\u5927\u6742\u70e9\u3002\n    - \u5982\u679c\u4f60\u6ca1\u670970%\u4ee5\u4e0a\u7684\u628a\u63e1\uff0c\u5c31\u9009\u62e9\u4e0d\u5408\u5e76\u3002\n4.  **\u8d28\u91cf\u63d0\u5347**: \n    - \u5408\u5e76\u540e\u8bdd\u9898\u5e94\u8be5\u6bd4\u539f\u8bdd\u9898\u66f4\u6709\u4ef7\u503c\uff0c\u5176\u6807\u9898\u548c\u63cf\u8ff0\u5fc5\u987b\u6bd4\u539f\u59cb\u8bdd\u9898\u66f4\u5177\u4fe1\u606f\u91cf\u3001\u66f4\u7cbe\u70bc\u3001\u66f4\u80fd\u4f53\u73b0\u8ba8\u8bba\u7684\u7cbe\u534e\n    - \u907f\u514d\u4e3a\u4e86\u5408\u5e76\u800c\u5408\u5e76\uff0c\u4fdd\u6301\u8bdd\u9898\u7684\u72ec\u7acb\u6027\n    - \u5408\u5e76\u6570\u91cf\u63a7\u5236\u57282-4\u4e2a\u8bdd\u9898\u4e4b\u95f4\n\n### \u5185\u5bb9\u751f\u6210\u8981\u6c42\n\n1.  **\u5408\u5e76\u540e\u6807\u9898 (`merged_title`)**: \u751f\u6210\u4e00\u4e2a\u5168\u65b0\u7684\u3001\u9ad8\u5ea6\u6982\u62ec\u7684\u3001\u80fd\u4f53\u73b0\u8ba8\u8bba\u6838\u5fc3\u7684\u7cbe\u786e\u6807\u9898\u3002\n2.  **\u5408\u5e76\u540e\u63cf\u8ff0 (`merged_description`)**: **\u4e25\u683c\u57fa\u4e8e**\u6240\u6709\u88ab\u5408\u5e76\u8bdd\u9898\u7684**\u539f\u59cb\u5185\u5bb9**\u8fdb\u884c\u603b\u7ed3\u3002\u5fc5\u987b\u6e05\u6670\u5730\u8bf4\u660e\u8ba8\u8bba\u7684\u5b8c\u6574\u8109\u7edc\u3001\u6838\u5fc3\u89c2\u70b9\u3001\u4e3b\u8981\u95ee\u9898\u548c\u7ed3\u8bba\u3002**\u4e25\u7981**\u7f16\u9020\u3001\u81c6\u6d4b\u6216\u6dfb\u52a0\u4efb\u4f55\u539f\u59cb\u5bf9\u8bdd\u4e2d\u4e0d\u5b58\u5728\u7684\u4fe1\u606f\u3002**\u4e25\u7981**\u51fa\u73b0\u201c\u5408\u5e76\u4e86X\u4e2a\u8bdd\u9898\u201d\u8fd9\u79cd\u7a7a\u6d1e\u7684\u683c\u5f0f\u5316\u63cf\u8ff0\u3002\n3.  **\u8bdd\u9898\u6807\u7b7e (`topic_tags`)**: \u5229\u7528\u4f60\u6700\u65b0\u7684\u77e5\u8bc6\u5e93\uff0c\u4e3a\u6bcf\u4e2a\u5408\u5e76\u540e\u7684\u8bdd\u9898\u751f\u62102-4\u4e2a\u4e0e\u65f6\u4ff1\u8fdb\u7684\u3001\u4e1a\u5185\u8ba4\u53ef\u7684\u6807\u7b7e\u3002\u4f8b\u5982\uff1a`RAG`, `Fine-tuning`, `AI Agent`, `Claude 3.5 Sonnet`, `n8n` \u7b49\u3002\n\n### \u7edd\u5bf9\u8f93\u51fa\u8981\u6c42 (\u6700\u91cd\u8981)\n\n1.  **\u7eafJSON**: **\u53ea\u5141\u8bb8**\u8f93\u51fa\u6709\u6548\u7684JSON\u5bf9\u8c61\u3002\n2.  **\u7981\u6b62Markdown**: **\u4e25\u7981**\u5728\u8f93\u51fa\u7684\u4efb\u4f55\u4f4d\u7f6e\u4f7f\u7528 ` ```json ` \u6216 ` ``` ` \u8fdb\u884c\u5305\u88f9\u3002\n3.  **\u7981\u6b62\u89e3\u91ca**: **\u4e25\u7981**\u5728JSON\u5bf9\u8c61\u524d\u540e\u6dfb\u52a0\u4efb\u4f55\u5f62\u5f0f\u7684\u89e3\u91ca\u6027\u6587\u5b57\u6216\u6ce8\u91ca\u3002\n\n---\n### \u8f93\u51faJSON\u683c\u5f0f (\u4e25\u683c\u9075\u5b88\u6b64\u7ed3\u6784)\n\n\u4f60\u7684\u54cd\u5e94\u5fc5\u987b\u662f\u4e00\u4e2a\u6709\u6548\u7684 JSON \u5bf9\u8c61\uff0c\u4e14\u53ea\u80fd\u662f JSON \u5bf9\u8c61\u3002\u4e0d\u8981\u5305\u542b\u4efb\u4f55\u989d\u5916\u7684\u6587\u672c\u3001\u89e3\u91ca\u6216 Markdown \u4ee3\u7801\u5757\uff08\u4f8b\u5982\uff0c```json\uff09\u3002\n\n```json\n{\n  \"merged_topics\": [\n    {\n      \"semantic_topic_id\": \"merged_001\",\n      \"merged_title\": \"\u57fa\u4e8e\u8bed\u4e49\u5206\u6790\u7684\u7cbe\u786e\u6807\u9898\",\n      \"merged_description\": \"\u7efc\u5408\u63cf\u8ff0\uff1a\u8be6\u7ec6\u8bf4\u660e\u5408\u5e76\u540e\u8bdd\u9898\u7684\u5b8c\u6574\u5185\u5bb9\u8109\u7edc\u3001\u6838\u5fc3\u89c2\u70b9\u3001\u8ba8\u8bba\u8303\u56f4\u548c\u4e3b\u8981\u7ed3\u8bba\u3002\",\n      \"is_off_topic\": false,\n      \"semantic_category\": \"\u8ba8\u8bba\u7c7b\u578b (\u4f8b\u5982: '\u95ee\u9898\u6c42\u52a9', '\u7ecf\u9a8c\u5206\u4eab', '\u6280\u672f\u63a2\u8ba8')\",\n      \"topic_tags\": [\"AI\u751f\u6210\u6807\u7b7e1\", \"AI\u751f\u6210\u6807\u7b7e2\"],\n      \"merged_from\": [\n        {\n          \"physical_id\": \"\u88ab\u5408\u5e76\u7684\u539f\u59cb\u8bdd\u9898ID (\u4f8b\u5982: topic_001)\",\n          \"original_title\": \"\u88ab\u5408\u5e76\u7684\u539f\u59cb\u8bdd\u9898\u7684\u6807\u9898\"\n        }\n      ],\n      \"relevance_score\": 0.85,\n      \"confidence_level\": \"high\"\n    }\n  ],\n  \"merge_metadata\": {\n    \"merge_algorithm\": \"deep_semantic_analysis\",\n    \"total_merges_performed\": 1,\n    \"compression_achieved\": \"50%\"\n  }\n}\n```\n\n---\n### \u5f85\u5206\u6790\u7684\u7269\u7406\u5408\u5e76\u540e\u7684\u8bdd\u9898\u6570\u636e\n\n{{ JSON.stringify($json) }}",
        "hasOutputParser": true,
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        1220,
        80
      ],
      "id": "f091a273-6475-40a5-b7df-99d4fb12c1f1",
      "name": "AI Semantic Merge"
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\n  \"merged_topics\": [\n    {\n      \"semantic_topic_id\": \"merged_001\",\n      \"merged_title\": \"\u57fa\u4e8e\u8bed\u4e49\u5206\u6790\u7684\u7cbe\u786e\u6807\u9898\",\n      \"merged_description\": \"\u7efc\u5408\u63cf\u8ff0\uff1a\u8be6\u7ec6\u8bf4\u660e\u5408\u5e76\u540e\u8bdd\u9898\u7684\u5b8c\u6574\u5185\u5bb9\u8109\u7edc\u3001\u6838\u5fc3\u89c2\u70b9\u3001\u8ba8\u8bba\u8303\u56f4\u548c\u4e3b\u8981\u7ed3\u8bba\u3002\",\n      \"is_off_topic\": false,\n      \"semantic_category\": \"\u8ba8\u8bba\u7c7b\u578b (\u4f8b\u5982: '\u95ee\u9898\u6c42\u52a9', '\u7ecf\u9a8c\u5206\u4eab', '\u6280\u672f\u63a2\u8ba8')\",\n      \"topic_tags\": [\"AI\u751f\u6210\u6807\u7b7e1\", \"AI\u751f\u6210\u6807\u7b7e2\"],\n      \"merged_from\": [\n        {\n          \"physical_id\": \"\u88ab\u5408\u5e76\u7684\u539f\u59cb\u8bdd\u9898ID (\u4f8b\u5982: topic_001)\",\n          \"original_title\": \"\u88ab\u5408\u5e76\u7684\u539f\u59cb\u8bdd\u9898\u7684\u6807\u9898\"\n        }\n      ],\n      \"relevance_score\": 0.85,\n      \"confidence_level\": \"high\"\n    }\n  ],\n  \"merge_metadata\": {\n    \"merge_algorithm\": \"deep_semantic_analysis\",\n    \"total_merges_performed\": 1,\n    \"compression_achieved\": \"50%\"\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1420,
        400
      ],
      "id": "00c38755-66ab-4e1d-99f4-e096f8c1e2d7",
      "name": "Structured Output Parser1"
    },
    {
      "parameters": {
        "content": "## \u667a\u80fd\u8bdd\u9898\u5f52\u5e76\n\n- \u5c06\u76f8\u4f3c\u6216\u76f8\u5173\u7684\u8bdd\u9898\u8fdb\u884c\u667a\u80fd\u5408\u5e76\uff0c\u5f62\u6210\u66f4\u5b8f\u89c2\u3001\u6709\u610f\u4e49\u7684\u8bdd\u9898\u3002\n- \u5c06\u6240\u6709\u5904\u7406\u8fc7\u7684\u8bdd\u9898\u6570\u636e\u7ec4\u88c5\u6210\u6700\u7ec8\u62a5\u544a\u6240\u9700\u7684\u7ed3\u6784\u6216\u5185\u5bb9\u3002\n",
        "height": 740,
        "width": 620,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1160,
        -200
      ],
      "id": "dc05f681-cd26-4761-8823-41101dd488f9",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "jsCode": "// Author: Gemini\n// Date: 2025-07-05\n// Description: This version integrates all_links, active_users, and raw_message_sample\n// from upstream nodes into the final report structure.\n\n/**\n * Robustly maps a segment string to a Chinese time segment name.\n * This function is likely from an upstream node, but included for completeness if needed.\n * @param {string} segmentInput - The segment string, e.g., \"19:30-20:00\" or \"\u508d\u665a\".\n * @returns {string} The corresponding Chinese segment name, e.g., \"\u665a\u4e0a\".\n */\nfunction mapTimeToChineseSegment(segmentInput) {\n  if (!segmentInput || typeof segmentInput !== 'string') {\n    return \"\u672a\u77e5\u65f6\u6bb5\";\n  }\n  const startHour = parseInt(segmentInput.split(':')[0], 10);\n  if (isNaN(startHour)) {\n    return segmentInput;\n  }\n  if (startHour >= 0 && startHour < 6) return \"\u6df1\u591c\";\n  if (startHour >= 6 && startHour < 9) return \"\u65e9\u6668\";\n  if (startHour >= 9 && startHour < 12) return \"\u4e0a\u5348\";\n  if (startHour >= 12 && startHour < 14) return \"\u4e2d\u5348\";\n  if (startHour >= 14 && startHour < 18) return \"\u4e0b\u5348\";\n  if (startHour >= 18 && startHour < 22) return \"\u665a\u4e0a\";\n  if (startHour >= 22 && startHour <= 23) return \"\u6df1\u591c\";\n  return \"\u672a\u77e5\u65f6\u6bb5\";\n}\n\n// --- Main Logic ---\n\n// Step 1: Get the required data sources\n// $input.first() refers to the first input connected to this node (likely \"AI Semantic Merge\")\nconst aiMergeResult = $input.first().json.output;\n// $('Node Name') refers to data from a specific node by its name\nconst physicalMergeResult = $('Merge & Deduplicate Topics').first().json;\n\n// Get data from the newly added nodes\n// Ensure 'Extract Links & Active Users' is the exact name of your node in n8n\nconst linksAndUsersData = $('Extract Links & Active Users').first().json;\nconst all_links = linksAndUsersData.all_links;\nconst active_users = linksAndUsersData.active_users;\n\n// Ensure 'Prepare Raw Message Sample' is the exact name of your node in n8n\nconst rawSampleData = $('Prepare Raw Message Sample').first().json;\nconst raw_message_sample = rawSampleData.raw_message_sample;\n\n\nif (!aiMergeResult || !physicalMergeResult || !linksAndUsersData || !rawSampleData) {\n  return { json: { error: \"Missing one or more required inputs. Check node connections and names.\" } };\n}\n\n// Step 2: Create a fast lookup map for all original topics\nconst allTopicsMap = new Map(physicalMergeResult.all_topics.map(t => [t.physical_id, t]));\n\n// Step 3: Iterate through the AI's decisions and assemble the enriched topics\nconst enhanced_merged_topics = [];\nfor (const aiTopic of aiMergeResult.merged_topics) {\n  const combined_seqs_nested = [];\n  const seq_source_mapping = {};\n\n  for (const source of aiTopic.merged_from) {\n    const physical_id = source.physical_id;\n    const originalTopic = allTopicsMap.get(physical_id);\n\n    if (originalTopic) {\n      combined_seqs_nested.push(originalTopic.topic_seq);\n      for (const seq of originalTopic.topic_seq) {\n        if (!seq_source_mapping[seq]) {\n          seq_source_mapping[seq] = {\n            source_name: originalTopic.source_name,\n            segment_name: originalTopic.segment_name,\n            analysis_id: originalTopic.analysis_id,\n            topic_start_time: originalTopic.topic_start_time,\n            topic_end_time: originalTopic.topic_end_time\n          };\n        }\n      }\n    }\n  }\n\n  // Flatten combined_seqs_nested and remove duplicates\n  const combined_message_seqs = [...new Set(combined_seqs_nested.flat())].sort((a, b) => a - b);\n\n  // Calculate total_combined_messages and chat_count\n  const total_combined_messages = combined_message_seqs.length;\n  const chat_count = total_combined_messages; // Assuming chat_count is same as message count for now\n\n  // Determine message_time_range\n  let earliest_seq_time = null;\n  let latest_seq_time = null;\n\n  if (combined_message_seqs.length > 0) {\n    // Find the earliest and latest time from the original messages based on seq\n    // This requires access to the original messages from \"Parse & Structure Data\"\n    // For simplicity, we'll use the topic_start_time and topic_end_time from originalTopic\n    // as provided by \"Merge & Deduplicate Topics\" node.\n    // If more granular message-level time is needed, you'd need to pass the full\n    // original messages array or a lookup map from \"Parse & Structure Data\" node.\n\n    // For now, we'll use the min/max of the original topics' start/end times\n    let minTime = Infinity;\n    let maxTime = -Infinity;\n\n    for (const source of aiTopic.merged_from) {\n      const originalTopic = allTopicsMap.get(source.physical_id);\n      if (originalTopic) {\n        const startTime = new Date(originalTopic.topic_start_time).getTime();\n        const endTime = new Date(originalTopic.topic_end_time).getTime();\n        if (startTime < minTime) minTime = startTime;\n        if (endTime > maxTime) maxTime = endTime;\n      }\n    }\n    earliest_seq_time = minTime !== Infinity ? new Date(minTime).toISOString() : null;\n    latest_seq_time = maxTime !== -Infinity ? new Date(maxTime).toISOString() : null;\n  }\n\n\n  enhanced_merged_topics.push({\n    semantic_topic_id: aiTopic.semantic_topic_id,\n    merged_title: aiTopic.merged_title,\n    merged_description: aiTopic.merged_description,\n    is_off_topic: aiTopic.is_off_topic,\n    semantic_category: aiTopic.semantic_category,\n    topic_tags: aiTopic.topic_tags,\n    merged_from: aiTopic.merged_from,\n    relevance_score: aiTopic.relevance_score,\n    confidence_level: aiTopic.confidence_level,\n    combined_message_seqs: combined_message_seqs,\n    total_combined_messages: total_combined_messages,\n    chat_count: chat_count,\n    seq_source_mapping: seq_source_mapping,\n    message_time_range: {\n      earliest_seq: combined_message_seqs.length > 0 ? combined_message_seqs[0] : null,\n      latest_seq: combined_message_seqs.length > 0 ? combined_message_seqs[combined_message_seqs.length - 1] : null,\n      seq_count: combined_message_seqs.length,\n      earliest_time: earliest_seq_time, // Use calculated earliest time\n      latest_time: latest_seq_time // Use calculated latest time\n    }\n  });\n}\n\n// Prepare the final output\nconst now = new Date();\nreturn [{\n  json: {\n    merge_timestamp: now.toISOString(),\n    merge_date: now.toISOString().split('T')[0], // Just the date part\n    merge_type: \"semantic\",\n    original_topics_count: physicalMergeResult.all_topics.length,\n    merged_topics_count: enhanced_merged_topics.length,\n    compression_ratio: (1 - enhanced_merged_topics.length / physicalMergeResult.all_topics.length) * 100,\n    merged_topics: enhanced_merged_topics,\n    merge_metadata: aiMergeResult.merge_metadata,\n    original_statistics: physicalMergeResult.statistics,\n    sources_metadata: physicalMergeResult.sources_metadata,\n    segments_metadata: physicalMergeResult.segments_metadata,\n    global_seq_index: physicalMergeResult.global_seq_index, // Ensure this field exists and is passed\n\n    // Newly added fields\n    all_links: all_links,\n    active_users: active_users,\n    raw_message_sample: raw_message_sample\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        80
      ],
      "id": "0299a2da-cfe5-47ff-9b49-57c387020ae2",
      "name": "Assemble Final Report"
    },
    {
      "parameters": {
        "jsCode": "// \u83b7\u53d6AI Agent\u7684\u8f93\u51fa\nlet aiOutput;\ntry {\n  // \u9996\u5148\u68c0\u67e5\u8f93\u5165\u6570\u636e\u7ed3\u6784\n  const inputData = $input.first().json;\n  console.log('Input data structure:', JSON.stringify(inputData, null, 2));\n  \n  // \u5c1d\u8bd5\u4e0d\u540c\u7684\u6570\u636e\u7ed3\u6784\u8def\u5f84\n  if (inputData.choices && inputData.choices[0] && inputData.choices[0].message) {\n    // OpenAI API \u6807\u51c6\u683c\u5f0f\n    aiOutput = inputData.choices[0].message.content;\n  } else if (inputData.message && inputData.message.content) {\n    // \u7b80\u5316\u7684\u6d88\u606f\u683c\u5f0f\n    aiOutput = inputData.message.content;\n  } else if (inputData.content) {\n    // \u76f4\u63a5\u5185\u5bb9\u683c\u5f0f\n    aiOutput = inputData.content;\n  } else if (inputData.output) {\n    // \u8f93\u51fa\u5b57\u6bb5\u683c\u5f0f\n    aiOutput = inputData.output;\n  } else if (typeof inputData === 'string') {\n    // \u7eaf\u5b57\u7b26\u4e32\u683c\u5f0f\n    aiOutput = inputData;\n  } else {\n    // \u5982\u679c\u90fd\u627e\u4e0d\u5230\uff0c\u5c1d\u8bd5\u627e\u5230\u7b2c\u4e00\u4e2a\u5b57\u7b26\u4e32\u503c\n    const findStringValue = (obj) => {\n      if (typeof obj === 'string') return obj;\n      if (Array.isArray(obj)) {\n        for (const item of obj) {\n          const result = findStringValue(item);\n          if (result) return result;\n        }\n      } else if (typeof obj === 'object' && obj !== null) {\n        for (const key in obj) {\n          const result = findStringValue(obj[key]);\n          if (result) return result;\n        }\n      }\n      return null;\n    };\n    \n    aiOutput = findStringValue(inputData);\n    \n    if (!aiOutput) {\n      throw new Error('\u65e0\u6cd5\u5728\u8f93\u5165\u6570\u636e\u4e2d\u627e\u5230AI\u8f93\u51fa\u5185\u5bb9');\n    }\n  }\n  \n  console.log('Found AI output:', aiOutput.substring(0, 200) + '...');\n  \n} catch (error) {\n  console.error('\u83b7\u53d6AI\u8f93\u51fa\u65f6\u51fa\u9519:', error);\n  return [{\n    json: {\n      error: '\u65e0\u6cd5\u83b7\u53d6AI\u8f93\u51fa: ' + error.message,\n      inputData: $input.first().json,\n      status: 'error'\n    }\n  }];\n}\n\n// \u6e05\u7406\u53ef\u80fd\u7684markdown\u6807\u8bb0\nlet htmlContent = aiOutput;\nif (htmlContent.includes('```html')) {\n  htmlContent = htmlContent.replace(/```html\\n?/g, '').replace(/\\n?```/g, '');\n}\nif (htmlContent.includes('```')) {\n  htmlContent = htmlContent.replace(/```\\n?/g, '').replace(/\\n?```/g, '');\n}\n\n// \u83b7\u53d6\u7fa4\u804a\u540d\u79f0\u548c\u65e5\u671f\uff08\u4ece\u5de5\u4f5c\u6d41\u8f93\u5165\u6216\u8bbe\u7f6e\u9ed8\u8ba4\u503c\uff09\nconst groupName = $('ConfigureChatParameters').first().json.group_name || 'unknown';\nconst date = $('ConfigureChatParameters').first().json.date || new Date().toISOString().split('T')[0];\n\nconsole.log('\u7fa4\u804a\u4fe1\u606f:', { groupName, date });\n\n// \ud83d\udd25 \u6539\u8fdb\uff1a\u667a\u80fdHTML\u5185\u5bb9\u6e05\u7406 - \u4fdd\u62a4\u56fe\u8868\u548c\u811a\u672c\u5185\u5bb9\nconsole.log('\u5f00\u59cbHTML\u6e05\u7406\uff0c\u539f\u59cb\u957f\u5ea6:', htmlContent.length);\n\n// \u667a\u80fd\u6e05\u7406HTML\u5185\u5bb9\uff0c\u4fdd\u62a4\u91cd\u8981\u7684\u4ee3\u7801\u533a\u57df\nconst smartCleanHtmlForMCP = (html) => {\n  // 1. \u9996\u5148\u4fdd\u62a4\u91cd\u8981\u7684\u4ee3\u7801\u533a\u57df\n  const protectedSections = [];\n  let cleanedHtml = html;\n  \n  // \u4fdd\u62a4 script \u6807\u7b7e\u5185\u5bb9\uff08\u5305\u62ec mermaid, chart.js \u7b49\uff09\n  cleanedHtml = cleanedHtml.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, (match, offset) => {\n    const placeholder = `__PROTECTED_SCRIPT_${protectedSections.length}__`;\n    protectedSections.push(match);\n    return placeholder;\n  });\n  \n  // \u4fdd\u62a4 style \u6807\u7b7e\u5185\u5bb9\n  cleanedHtml = cleanedHtml.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, (match, offset) => {\n    const placeholder = `__PROTECTED_STYLE_${protectedSections.length}__`;\n    protectedSections.push(match);\n    return placeholder;\n  });\n  \n  // \u4fdd\u62a4 pre \u6807\u7b7e\u5185\u5bb9\uff08\u53ef\u80fd\u5305\u542b\u56fe\u8868\u5b9a\u4e49\uff09\n  cleanedHtml = cleanedHtml.replace(/<pre[^>]*>[\\s\\S]*?<\\/pre>/gi, (match, offset) => {\n    const placeholder = `__PROTECTED_PRE_${protectedSections.length}__`;\n    protectedSections.push(match);\n    return placeholder;\n  });\n  \n  // \u4fdd\u62a4\u5177\u6709\u7279\u6b8aclass\u7684div\uff08\u5982mermaid\u56fe\u8868\u5bb9\u5668\uff09\n  cleanedHtml = cleanedHtml.replace(/<div[^>]*class=\"[^\"]*(?:mermaid|chart|graph|diagram)[^\"]*\"[^>]*>[\\s\\S]*?<\\/div>/gi, (match, offset) => {\n    const placeholder = `__PROTECTED_CHART_${protectedSections.length}__`;\n    protectedSections.push(match);\n    return placeholder;\n  });\n  \n  // 2. \u5bf9\u5176\u4f59\u5185\u5bb9\u8fdb\u884c\u6e05\u7406\n  cleanedHtml = cleanedHtml\n    // \u53ea\u6e05\u7406\u666e\u901a\u7684\u6362\u884c\u7b26\u548c\u5236\u8868\u7b26\uff0c\u4f46\u4fdd\u7559\u5fc5\u8981\u7684\u683c\u5f0f\n    .replace(/[\\r]/g, '')           // \u79fb\u9664\u56de\u8f66\u7b26\n    .replace(/\\n\\s*\\n/g, '\\n')      // \u5c06\u591a\u4e2a\u6362\u884c\u5408\u5e76\u4e3a\u4e00\u4e2a\n    .replace(/\\t/g, '  ')           // \u5c06\u5236\u8868\u7b26\u8f6c\u6362\u4e3a\u4e24\u4e2a\u7a7a\u683c\n    // \u6e05\u7406HTML\u6807\u7b7e\u95f4\u7684\u591a\u4f59\u7a7a\u683c\uff0c\u4f46\u4e0d\u5f71\u54cd\u6587\u672c\u5185\u5bb9\n    .replace(/>\\s+</g, '><')\n    // \u6e05\u7406\u884c\u9996\u884c\u5c3e\u7a7a\u683c\n    .replace(/^\\s+|\\s+$/gm, '')\n    // \u5c06\u591a\u4e2a\u8fde\u7eed\u7a7a\u683c\u5408\u5e76\uff0c\u4f46\u4fdd\u7559\u5355\u4e2a\u6362\u884c\n    .replace(/ +/g, ' ');\n  \n  // 3. \u6062\u590d\u53d7\u4fdd\u62a4\u7684\u5185\u5bb9\n  protectedSections.forEach((section, index) => {\n    const scriptPlaceholder = `__PROTECTED_SCRIPT_${index}__`;\n    const stylePlaceholder = `__PROTECTED_STYLE_${index}__`;\n    const prePlaceholder = `__PROTECTED_PRE_${index}__`;\n    const chartPlaceholder = `__PROTECTED_CHART_${index}__`;\n    \n    cleanedHtml = cleanedHtml.replace(scriptPlaceholder, section);\n    cleanedHtml = cleanedHtml.replace(stylePlaceholder, section);\n    cleanedHtml = cleanedHtml.replace(prePlaceholder, section);\n    cleanedHtml = cleanedHtml.replace(chartPlaceholder, section);\n  });\n  \n  // 4. \u6700\u540e\u53ea\u5bf9JSON\u4f20\u8f93\u505a\u5fc5\u8981\u7684\u8f6c\u4e49\n  return cleanedHtml\n    .replace(/\\\\/g, '\\\\\\\\')    // \u8f6c\u4e49\u53cd\u659c\u6760\n    .replace(/\"/g, '\\\\\"');     // \u8f6c\u4e49\u53cc\u5f15\u53f7\n};\n\n// \u5e94\u7528\u667a\u80fd\u6e05\u7406\nconst originalHtmlContent = htmlContent;\nhtmlContent = smartCleanHtmlForMCP(htmlContent);\n\nconsole.log('\u667a\u80fdHTML\u6e05\u7406\u5b8c\u6210');\nconsole.log('- \u539f\u59cb\u957f\u5ea6:', originalHtmlContent.length);\nconsole.log('- \u6e05\u7406\u540e\u957f\u5ea6:', htmlContent.length);\nconsole.log('- \u8282\u7701\u7a7a\u95f4:', originalHtmlContent.length - htmlContent.length, 'bytes');\n\n// \u68c0\u67e5\u662f\u5426\u5305\u542b\u56fe\u8868\u76f8\u5173\u5185\u5bb9\nconst hasCharts = originalHtmlContent.match(/(mermaid|chart\\.js|echarts|d3\\.js|plotly|canvas|svg)/i);\nif (hasCharts) {\n  console.log('\u2705 \u68c0\u6d4b\u5230\u56fe\u8868\u5185\u5bb9\uff0c\u5df2\u5e94\u7528\u4fdd\u62a4\u6027\u6e05\u7406');\n} else {\n  console.log('\u2139\ufe0f \u672a\u68c0\u6d4b\u5230\u56fe\u8868\u5185\u5bb9');\n}\n\n// \u9a8c\u8bc1HTML\u6587\u6863\u5b8c\u6574\u6027\nif (!htmlContent.includes('<!DOCTYPE html>')) {\n  console.error('AI\u8f93\u51fa\u4e0d\u662f\u5b8c\u6574\u7684HTML\u6587\u6863');\n  return [{\n    json: {\n      error: 'AI\u8f93\u51fa\u4e0d\u662f\u5b8c\u6574\u7684HTML\u6587\u6863',\n      receivedContent: htmlContent.substring(0, 500),\n      status: 'error'\n    }\n  }];\n}\n\n// \u989d\u5916\u7684HTML\u5185\u5bb9\u9a8c\u8bc1\nconst validateHtml = (html) => {\n  const checks = {\n    hasDoctype: html.includes('<!DOCTYPE html>'),\n    hasHtmlTag: html.includes('<html') && html.includes('</html>'),\n    hasHeadTag: html.includes('<head') && html.includes('</head>'),\n    hasBodyTag: html.includes('<body') && html.includes('</body>'),\n    hasTitle: html.includes('<title'),\n    isNotEmpty: html.length > 100,\n    hasChartLibraries: /(?:mermaid|chart\\.js|echarts|d3\\.js|plotly)/i.test(html)\n  };\n  \n  const passed = Object.values(checks).filter(Boolean).length;\n  const total = Object.keys(checks).length;\n  \n  console.log('HTML\u9a8c\u8bc1\u7ed3\u679c:', checks);\n  console.log(`HTML\u8d28\u91cf\u8bc4\u5206: ${passed}/${total}`);\n  \n  return { checks, score: passed / total };\n};\n\nconst validation = validateHtml(htmlContent);\n\n// \u751f\u6210\u6587\u4ef6\u540d - \u4f7f\u7528\u7fa4\u804a\u540d\u79f0\u548c\u65e5\u671f\nconst filename = `\u7fa4\u804a\u65e5\u62a5-${groupName}-${date}.html`;\nconst safeFilename = filename.replace(/[^a-z0-9\\u4e00-\\u9fa5_\\-\\.]/gi, '_');\n\nconsole.log('\u6587\u4ef6\u540d\u751f\u6210:', { \n  original: filename, \n  safe: safeFilename,\n  groupName: groupName,\n  date: date \n});\n\n// \u751f\u6210\u6e05\u7406\u540e\u7684HTML\u7528\u4e8eMCP\u5de5\u5177\nconst mcpReadyHtml = htmlContent;\n\n// \u8ba1\u7b97\u5904\u7406\u65f6\u957f\nlet processDuration = null;\ntry {\n  const startTime = $('\u9a8c\u8bc1\u8f93\u5165\u53c2\u6570').item.json.process_start_time;\n  if (startTime) {\n    const startDate = new Date(startTime);\n    const endDate = new Date();\n    // \u8ba1\u7b97\u79d2\u6570\u5dee\n    processDuration = Math.round((endDate - startDate) / 1000);\n  }\n} catch (e) {\n  console.log('\u8ba1\u7b97\u5904\u7406\u65f6\u957f\u5931\u8d25:', e.message);\n  processDuration = null;\n}\n\n\n// \u8fd4\u56de\u5904\u7406\u7ed3\u679c - \u5305\u542b\u4e24\u4e2a\u7248\u672c\u7684HTML\nreturn [{\n  json: {\n    // \u539f\u59cbHTML\uff08\u7528\u4e8e\u6587\u4ef6\u4fdd\u5b58\uff09\n    htmlContent: originalHtmlContent,\n    // \u6e05\u7406\u540e\u7684HTML\uff08\u7528\u4e8eMCP\u5de5\u5177\u8c03\u7528\uff09\n    mcpHtmlContent: mcpReadyHtml,\n    // \u6587\u4ef6\u4fe1\u606f - \u4f7f\u7528\u7fa4\u804a\u540d\u79f0\n    filename: safeFilename,\n    originalFilename: filename,\n    groupName: groupName,\n    date: date,\n    process_end_time: $now.toISO(),\n    process_duration: processDuration,\n    timestamp: new Date().toISOString(),\n    status: 'success',\n    // \u7edf\u8ba1\u4fe1\u606f\n    originalContentLength: originalHtmlContent.length,\n    cleanedContentLength: mcpReadyHtml.length,\n    compressionRatio: ((originalHtmlContent.length - mcpReadyHtml.length) / originalHtmlContent.length * 100).toFixed(2) + '%',\n    // HTML\u8d28\u91cf\u4fe1\u606f\n    htmlValidation: validation,\n    hasChartContent: hasCharts !== null,\n    // \u8c03\u8bd5\u4fe1\u606f\n    processingSteps: [\n      '\u2705 AI\u8f93\u51fa\u89e3\u6790\u6210\u529f',\n      '\u2705 Markdown\u6e05\u7406\u5b8c\u6210', \n      '\u2705 \u667a\u80fdHTML\u6e05\u7406\u5b8c\u6210\uff08\u4fdd\u62a4\u56fe\u8868\uff09',\n      '\u2705 MCP\u683c\u5f0f\u4f18\u5316\u5b8c\u6210',\n      `\u2705 HTML\u9a8c\u8bc1\u5b8c\u6210 (${(validation.score * 100).toFixed(1)}%)`,\n      `\u2705 \u6587\u4ef6\u540d\u751f\u6210\u5b8c\u6210: ${safeFilename}`\n    ]\n  },\n  binary: {\n    data: {\n      data: Buffer.from(originalHtmlContent, 'utf8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: safeFilename,\n      fileExtension: 'html'\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        80
      ],
      "id": "699fd2d2-fdc8-4bd7-9a14-1db30edd2229",
      "name": "\u5904\u7406AI\u8f93\u51fa"
    },
    {
      "parameters": {
        "content": "## \u6700\u7ec8HTML\u5185\u5bb9\u5904\u7406\n",
        "height": 740,
        "width": 780,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1820,
        -200
      ],
      "id": "59446f38-5c4e-4778-a707-95c396cccc60",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "jsCode": "// Node: Extract Links & Active Users\n// Input: Output from \"Parse & Structure Data\" node (single JSON object with sources.messages)\n// Output: JSON with all_links array (objects with url, sender, time, context) and active_users array\n\nconst inputData = $input.first().json; // Get the single item from \"Parse & Structure Data\"\n\n\nif (!inputData || !inputData.sources) {\n  console.log(\"Input data is missing structuredData or sources field.\");\n  return { json: { all_links: [], active_users: [] } };\n}\n\n// Assuming only one source, get its messages list\nconst sourceName = Object.keys(inputData.sources)[0];\nconst allMessages = inputData.sources[sourceName].messages;\n\nif (!allMessages || allMessages.length === 0) {\n  console.log(\"No messages found in structured data.\");\n  return { json: { all_links: [], active_users: [] } };\n}\n\nconst allLinks = []; // Now an array of objects\nconst senderMessageCounts = {}; // Store message counts for active users\n\n// Regex for raw URLs (http or https)\nconst rawUrlRegex = /(https?:\\/\\/[^\\s]+)/g;\n// Regex for [\u94fe\u63a5|URL] format\nconst formattedLinkRegex = /\\[\u94fe\u63a5\\|(https?:\\/\\/[^\\]]+)\\]/g;\n\nconsole.log(`Processing ${allMessages.length} messages.`);\n\nfor (const message of allMessages) {\n  const senderName = message.sender_name;\n  const messageTime = message.time;\n  const messageContent = message.content;\n\n  //console.log(`--- Processing Message ---`);\n  //console.log(`Raw senderName: '${senderName}'`);\n  //console.log(`Message content: '${messageContent}'`);\n\n  // Check senderName validity for active users\n  const isValidSender = senderName && senderName.trim() !== '' && senderName !== '\u7cfb\u7edf\u6d88\u606f';\n  \n\n  if (messageContent) {\n    const extractedUrls = []; // Temporarily store all URLs found in this message\n    let linkMatch;\n    \n    // Extract raw URLs\n    rawUrlRegex.lastIndex = 0; // Reset regex lastIndex for global regex\n    while ((linkMatch = rawUrlRegex.exec(messageContent)) !== null) {\n      extractedUrls.push(linkMatch[0]);\n    }\n\n    // Extract formatted links [\u94fe\u63a5|URL]\n    formattedLinkRegex.lastIndex = 0; // Reset regex lastIndex for global regex\n    while ((linkMatch = formattedLinkRegex.exec(messageContent)) !== null) {\n      extractedUrls.push(linkMatch[1]);\n    }\n\n    for (const url of extractedUrls) {\n      // Check if this URL is part of an image or video markdown\n      const isImageOrVideoLink = messageContent.includes(`![\u56fe\u7247]`) || messageContent.includes(`![\u89c6\u9891]`);\n      if (!isImageOrVideoLink) {\n        //console.log(`Found valid link: ${url}`);\n        allLinks.push({\n          url: url,\n          sender: senderName,\n          time: messageTime,\n          context: messageContent\n        });\n      } else {\n        console.log(`Excluded image/video link: ${url}`);\n      }\n    }\n  }\n\n  // Count active users (only for valid senders)\n  if (isValidSender) {\n    //console.log(`Counting message for sender: ${senderName}`);\n    senderMessageCounts[senderName] = (senderMessageCounts[senderName] || 0) + 1;\n  }\n}\n\nconsole.log(`Final allLinks count: ${allLinks.length}`);\nconsole.log(`Final activeUsers count: ${Object.keys(senderMessageCounts).length}`);\n\n// Convert senderMessageCounts to active_users array and sort\nconst activeUsers = Object.entries(senderMessageCounts)\n  .map(([sender_name, message_count]) => ({ sender_name, message_count }))\n  .sort((a, b) => b.message_count - a.message_count); // Sort in descending order by message_count\n\nreturn [{\n  json: {\n    all_links: allLinks,\n    active_users: activeUsers\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        -100
      ],
      "id": "63f36a48-ba52-4392-9a16-e66a0d41ce6c",
      "name": "Extract Links & Active Users"
    },
    {
      "parameters": {
        "jsCode": "// Node: Prepare Raw Message Sample\n// Input: Output from \"Parse & Structure Data\" node (\u5355\u4e2a JSON \u5bf9\u8c61\uff0c\u5305\u542b sources.messages)\n// Output: JSON with raw_message_sample \u6570\u7ec4\n\nconst structuredData = $('Parse & Structure Data').first().json; // \u83b7\u53d6\u4e0a\u6e38\u8282\u70b9\u7684\u5355\u4e2a\u6570\u636e\u9879\n\nif (!structuredData || !structuredData.sources) {\n  console.log(\"\u8f93\u5165\u6570\u636e\u7f3a\u5c11 structuredData \u6216 sources \u5b57\u6bb5\u3002\");\n  return { json: { raw_message_sample: [] } };\n}\n\n// \u5047\u8bbe\u53ea\u6709\u4e00\u4e2a source\uff0c\u83b7\u53d6\u5176\u6d88\u606f\u5217\u8868\nconst sourceName = Object.keys(structuredData.sources)[0];\nconst allMessages = structuredData.sources[sourceName].messages;\n\nif (!allMessages || allMessages.length === 0) {\n  console.log(\"\u7ed3\u6784\u5316\u6570\u636e\u4e2d\u672a\u627e\u5230\u6d88\u606f\u3002\");\n  return { json: { raw_message_sample: [] } };\n}\n\n// \u63d0\u53d6\u6d88\u606f\u6837\u672c\u3002\n// \u4f60\u53ef\u4ee5\u6839\u636e\u9700\u8981\u8c03\u6574\u91c7\u6837\u903b\u8f91\uff08\u4f8b\u5982\uff0c\u968f\u673a\u91c7\u6837\uff0c\u66f4\u591a\u6d88\u606f\uff09\u3002\nconst rawMessageSample = allMessages.map(msg => ({\n  sender_name: msg.sender_name,\n  time: msg.time,\n  content: msg.content\n}));\n\nreturn [{\n  json: {\n    raw_message_sample: rawMessageSample\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        -100
      ],
      "id": "46a817d3-6fb4-4d3e-b991-3b098d0bb005",

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

WeChat-Daily-Digest-AI-Cost-Optimized. Uses httpRequest, chainLlm, outputParserStructured, readWriteFile. Scheduled trigger; 26 nodes.

Source: https://github.com/lqshow/awesome-n8n-workflows/blob/c78bab4cce4e943e25a95b71c7304427ca828ada/workflows/wechat-daily-report/wechat-daily-digest-ai-cost-optimized.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

My workflow 53. Uses formTrigger, httpRequest, lmChatOpenAi, form. Event-driven trigger; 74 nodes.

Form Trigger, HTTP Request, OpenAI Chat +15
AI & RAG

Episode 23: UGC with nanobanana. Uses lmChatOpenAi, lmChatOllama, lmChatDeepSeek, lmChatOpenRouter. Event-driven trigger; 74 nodes.

OpenAI Chat, Ollama Chat, Lm Chat Deep Seek +12
AI & RAG

Automate Blog Creation and Publishing with Ultra-Low Cost AI

Chain Llm, WordPress, HTTP Request +6
AI & RAG

Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing

WordPress, OpenAI, Perplexity +8
AI & RAG

A Telegram bot that converts natural-language work descriptions into detailed cost estimates using AI parsing, vector search, and the open-source DDC CWICR database with 55,000+ construction work item

HTTP Request, Telegram, Telegram Trigger +6