{"url_pattern":"^https?://mp\\.toutiao\\.com/.*$","site_name":"toutiao","allowed_domains":["so.toutiao.com","toutiao.com"],"tools":[{"name":"insert_article","description":"Insert content into Toutiao (头条号) article editor","inputSchema":{"type":"object","properties":{"title":{"type":"string","description":"Article title"},"content":{"type":"string","description":"Article content (HTML)"}},"required":["content"]},"handler":"(params) => {\n  // Set title\n  if (params.title) {\n    const titleInput = document.querySelector('input[placeholder*=\"标题\"]') || document.querySelector('.title-input input');\n    if (titleInput) {\n      titleInput.value = params.title;\n      titleInput.dispatchEvent(new Event('input', { bubbles: true }));\n    }\n  }\n  // Find editor\n  const editor = document.querySelector('.ql-editor') || document.querySelector('[contenteditable=\"true\"]');\n  if (!editor) {\n    return { success: false, message: 'Editor not found. Open mp.toutiao.com article editor first' };\n  }\n  editor.innerHTML = params.content;\n  editor.dispatchEvent(new Event('input', { bubbles: true }));\n  return { success: true, message: 'Content inserted into Toutiao editor' };\n}"},{"name":"toutiao_hot","description":"今日头条热榜","inputSchema":{"type":"object","properties":{"count":{"type":"string","description":"返回条数 (默认 20, 最多 50)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const count = Math.min(parseInt(args.count) || 20, 50);\n\n      const resp = await fetch('https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc', {credentials: 'include'});\n      if (!resp.ok) {\n        // Fallback: parse hot search from homepage\n        return await fallbackFromHomepage(count);\n      }\n\n      let data;\n      try {\n        data = await resp.json();\n      } catch (e) {\n        return await fallbackFromHomepage(count);\n      }\n\n      if (!data || !data.data) {\n        return await fallbackFromHomepage(count);\n      }\n\n      const items = (data.data || data.fixed_top_data || []).slice(0, count).map((item, i) => ({\n        rank: i + 1,\n        title: item.Title || item.title || '',\n        hot_value: item.HotValue || item.hot_value || 0,\n        label: item.Label || item.label || '',\n        url: item.Url || item.url || '',\n        cluster_id: item.ClusterId || item.cluster_id || ''\n      }));\n\n      return {count: items.length, items};\n\n      async function fallbackFromHomepage(limit) {\n        const homeResp = await fetch('https://www.toutiao.com/', {credentials: 'include'});\n        if (!homeResp.ok) return {error: 'HTTP ' + homeResp.status, hint: 'Open www.toutiao.com in bb-browser first'};\n\n        const html = await homeResp.text();\n        const parser = new DOMParser();\n        const doc = parser.parseFromString(html, 'text/html');\n\n        // Try to extract hot search data from SSR HTML\n        const items = [];\n\n        // Method 1: Look for hot search region text\n        const allText = doc.body?.textContent || '';\n\n        // Method 2: Parse script tags for embedded data\n        const scripts = doc.querySelectorAll('script:not([src])');\n        for (const script of scripts) {\n          const text = script.textContent || '';\n          if (text.includes('hotBoard') || text.includes('hot_board') || text.includes('HotValue')) {\n            try {\n              const match = text.match(/\\[.*\"Title\".*\\]/s) || text.match(/\\[.*\"title\".*\"hot_value\".*\\]/s);\n              if (match) {\n                const hotData = JSON.parse(match[0]);\n                hotData.slice(0, limit).forEach((item, i) => {\n                  items.push({\n                    rank: i + 1,\n                    title: item.Title || item.title || '',\n                    hot_value: item.HotValue || item.hot_value || 0,\n                    label: item.Label || item.label || '',\n                    url: item.Url || item.url || '',\n                    cluster_id: item.ClusterId || item.cluster_id || ''\n                  });\n                });\n                if (items.length > 0) return {count: items.length, source: 'homepage_script', items};\n              }\n            } catch (e) {}\n          }\n        }\n\n        // Method 3: Parse hot search links from DOM\n        const hotLinks = doc.querySelectorAll('a[href*=\"search\"], [class*=\"hot\"] a, [class*=\"Hot\"] a');\n        for (const link of hotLinks) {\n          const title = (link.textContent || '').trim();\n          if (!title || title.length < 2 || title.length > 100) continue;\n          if (items.some(it => it.title === title)) continue;\n          items.push({\n            rank: items.length + 1,\n            title,\n            hot_value: 0,\n            label: '',\n            url: link.getAttribute('href') || '',\n            cluster_id: ''\n          });\n          if (items.length >= limit) break;\n        }\n\n        if (items.length === 0) {\n          return {error: 'Could not extract hot topics', hint: 'Open www.toutiao.com in bb-browser first and make sure you are logged in'};\n        }\n\n        return {count: items.length, source: 'homepage_dom', items};\n      }\n  };\n  return run(params || {});\n}"},{"name":"toutiao_search","description":"今日头条搜索","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"搜索关键词"},"count":{"type":"string","description":"返回结果数量 (默认 10, 最多 20)"}},"required":["query"]},"handler":"(params) => {\n  const run = async function(args) {\n\n      if (!args.query) return {error: 'Missing argument: query', hint: 'Provide a search keyword'};\n      const count = Math.min(parseInt(args.count) || 10, 20);\n\n      const url = 'https://so.toutiao.com/search?keyword=' + encodeURIComponent(args.query) + '&pd=information&dvpf=pc';\n      const resp = await fetch(url, {credentials: 'include'});\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Open so.toutiao.com in bb-browser first'};\n\n      const html = await resp.text();\n      const parser = new DOMParser();\n      const doc = parser.parseFromString(html, 'text/html');\n\n      const results = [];\n\n      // Helper: extract clean article URL from jump redirect chain\n      function extractArticleUrl(href) {\n        if (!href) return '';\n        try {\n          // Decode nested jump URLs to find the real toutiao article URL\n          let decoded = href;\n          for (let i = 0; i < 5; i++) {\n            const match = decoded.match(/toutiao\\.com(?:%2F|\\/)+a?(\\d{15,})/);\n            if (match) return 'https://www.toutiao.com/article/' + match[1] + '/';\n            const groupMatch = decoded.match(/group(?:%2F|\\/)(\\d{15,})/);\n            if (groupMatch) return 'https://www.toutiao.com/article/' + groupMatch[1] + '/';\n            decoded = decodeURIComponent(decoded);\n          }\n        } catch (e) {}\n        return href;\n      }\n\n      // Strategy 1: SSR HTML uses cs-card containers\n      const cards = doc.querySelectorAll('.cs-card');\n      for (const card of cards) {\n        const titleLink = card.querySelector('a[href*=\"search/jump\"]');\n        if (!titleLink) continue;\n\n        const title = (titleLink.textContent || '').trim();\n        if (!title || title.length < 2) continue;\n        // Skip non-result links like \"去西瓜搜\" / \"去抖音搜\"\n        if (title.includes('去西瓜搜') || title.includes('去抖音搜')) continue;\n\n        const articleUrl = extractArticleUrl(titleLink.getAttribute('href') || '');\n\n        // Extract snippet & source & time from card text\n        const fullText = (card.textContent || '').trim();\n        // Remove the title (may appear twice) to get the rest\n        let rest = fullText;\n        const titleIdx = rest.indexOf(title);\n        if (titleIdx >= 0) rest = rest.substring(titleIdx + title.length);\n        // Remove second occurrence of title if present\n        const titleIdx2 = rest.indexOf(title);\n        if (titleIdx2 >= 0) rest = rest.substring(titleIdx2 + title.length);\n        rest = rest.trim();\n\n        let snippet = '';\n        let source = '';\n        let time = '';\n\n        // Remove trailing comment count like \"1评论\" or \"23评论\" first\n        rest = rest.replace(/\\d+评论/g, '').trim();\n\n        // Extract time from the tail first\n        // Time patterns: \"3天前\", \"12小时前\", \"5分钟前\", \"前天17:23\", \"昨天08:00\", \"2024-01-01\"\n        // The number-based patterns (N天前 etc.) must NOT be preceded by a digit\n        const timeMatch = rest.match(/((?<=[^\\d])|^)(\\d{1,2}(?:小时|分钟|天)前|前天[\\d:]*|昨天[\\d:]*|\\d{4}[-/.]\\d{2}[-/.]\\d{2}.*)$/);\n        if (timeMatch) {\n          time = timeMatch[2] ? timeMatch[2].trim() : timeMatch[0].trim();\n          rest = rest.substring(0, rest.length - timeMatch[0].length).trim();\n        }\n\n        // Source is the short text at the end (author/media name, typically 2-20 chars)\n        // Pattern: \"...snippet content...SourceName\"\n        const sourceMatch = rest.match(/^([\\s\\S]+?)([\\u4e00-\\u9fa5A-Za-z][\\u4e00-\\u9fa5A-Za-z0-9_\\s]{1,19})$/);\n        if (sourceMatch && sourceMatch[1].length > 10) {\n          snippet = sourceMatch[1].trim().substring(0, 300);\n          source = sourceMatch[2].trim();\n        } else {\n          snippet = rest.substring(0, 300);\n        }\n\n        results.push({title, snippet, source, time, url: articleUrl});\n        if (results.length >= count) break;\n      }\n\n      // Strategy 2: Fallback to finding jump links with article IDs\n      if (results.length === 0) {\n        const links = doc.querySelectorAll('a[href*=\"search/jump\"]');\n        for (const link of links) {\n          const text = (link.textContent || '').trim();\n          if (!text || text.length < 4) continue;\n          // Skip navigation/promo links\n          if (text.includes('去西瓜搜') || text.includes('去抖音搜') || text.includes('APP')) continue;\n\n          const href = link.getAttribute('href') || '';\n          // Only include links that point to actual articles\n          if (!href.match(/toutiao\\.com|group|a\\d{10,}/)) continue;\n\n          const articleUrl = extractArticleUrl(href);\n          if (results.some(r => r.title === text)) continue;\n\n          // Try to get snippet from sibling/parent context\n          let snippet = '';\n          const container = link.closest('[class*=\"card\"]') || link.parentElement?.parentElement;\n          if (container) {\n            const containerText = (container.textContent || '').trim();\n            const afterTitle = containerText.indexOf(text);\n            if (afterTitle >= 0) {\n              const rest = containerText.substring(afterTitle + text.length).trim();\n              if (rest.length > 10) snippet = rest.substring(0, 300);\n            }\n          }\n\n          results.push({title: text, snippet, source: '', time: '', url: articleUrl});\n          if (results.length >= count) break;\n        }\n      }\n\n      if (results.length === 0) {\n        return {\n          error: 'No results found',\n          hint: 'Toutiao may require login or has anti-scraping protection. Try: 1) Open so.toutiao.com in bb-browser first, 2) Log in to toutiao, 3) Use toutiao/hot instead',\n          query: args.query\n        };\n      }\n\n      return {\n        query: args.query,\n        count: results.length,\n        results\n      };\n  };\n  return run(params || {});\n}"}]}