{"url_pattern":"^https?://([a-z0-9-]+\\.)?douban\\.com(/.*)?$","site_name":"douban","allowed_domains":["douban.com","example.com","movie.douban.com","search.douban.com"],"tools":[{"name":"douban_search","description":"Search Douban across movies, books, and music","inputSchema":{"type":"object","properties":{"keyword":{"type":"string","description":"Search keyword (Chinese or English)"}},"required":["keyword"]},"handler":"(params) => {\n  const run = async function(args) {\n\n      if (!args.keyword) return {error: 'Missing argument: keyword'};\n      const q = encodeURIComponent(args.keyword);\n\n      // Try the rich search_suggest endpoint (requires www.douban.com origin)\n      var resp;\n      var usedFallback = false;\n      try {\n        resp = await fetch('https://www.douban.com/j/search_suggest?q=' + q, {credentials: 'include'});\n        if (!resp.ok) throw new Error('HTTP ' + resp.status);\n      } catch (e) {\n        // Fallback: use movie.douban.com subject_suggest (works cross-subdomain via same eTLD+1 cookies)\n        try {\n          resp = await fetch('/j/subject_suggest?q=' + q, {credentials: 'include'});\n          usedFallback = true;\n        } catch (e2) {\n          return {error: 'Search failed: ' + e2.message, hint: 'Not logged in? Navigate to www.douban.com first.'};\n        }\n      }\n\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};\n      var d = await resp.json();\n\n      if (usedFallback) {\n        // subject_suggest returns an array directly\n        var items = (Array.isArray(d) ? d : []).map(function(c, i) {\n          return {\n            rank: i + 1,\n            id: c.id,\n            type: c.type === 'movie' ? 'movie' : c.type === 'b' ? 'book' : c.type || 'unknown',\n            title: c.title,\n            subtitle: c.sub_title || '',\n            rating: null,\n            info: '',\n            year: c.year || null,\n            cover: c.img || c.pic || null,\n            url: c.url\n          };\n        });\n        return {\n          keyword: args.keyword,\n          count: items.length,\n          results: items,\n          suggestions: [],\n          note: 'Limited results (movie/book only). For richer results, navigate to www.douban.com first.'\n        };\n      }\n\n      // Rich search_suggest response with cards\n      var cards = (d.cards || []).map(function(c, i) {\n        var id = c.url && c.url.match(/subject\\/(\\d+)/);\n        id = id ? id[1] : null;\n        var ratingMatch = c.card_subtitle && c.card_subtitle.match(/([\\d.]+)分/);\n        return {\n          rank: i + 1,\n          id: id,\n          type: c.type || 'unknown',\n          title: c.title,\n          subtitle: c.abstract || '',\n          rating: ratingMatch ? parseFloat(ratingMatch[1]) : null,\n          info: c.card_subtitle || '',\n          year: c.year || null,\n          cover: c.cover_url || null,\n          url: c.url\n        };\n      });\n\n      var suggestions = d.words || [];\n\n      return {\n        keyword: args.keyword,\n        count: cards.length,\n        results: cards,\n        suggestions: suggestions\n      };\n  };\n  return run(params || {});\n}"},{"name":"douban_search_books","description":"Navigate to Douban book search results","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"Book search keywords"}},"required":["query"]},"handler":"(params) => {\n  window.location.href = 'https://search.douban.com/book/subject_search?search_text=' + encodeURIComponent(params.query);\n  return { success: true, message: 'Opening Douban book search...', query: params.query };\n}"},{"name":"douban_get_book_details","description":"Extract book details from the current Douban subject page","inputSchema":{"type":"object","properties":{},"required":null},"handler":"() => {\n  const title = document.querySelector('#wrapper h1 span')?.textContent?.trim() || document.title;\n  const ratingText = document.querySelector('strong[property=\"v:average\"]')?.textContent?.trim() || '';\n  const introBlocks = Array.from(document.querySelectorAll('#link-report .intro, .related_info .intro'))\n    .map((node) => node.textContent?.trim())\n    .filter(Boolean);\n  const infoText = document.querySelector('#info')?.textContent?.replace(/\\s+/g, ' ').trim() || '';\n  const cover = document.querySelector('#mainpic img')?.src || null;\n  const metadata = {};\n  infoText.split(/\\s{2,}|\\n/).forEach((line) => {\n    const parts = line.split(':');\n    if (parts.length >= 2) {\n      const key = parts[0].trim();\n      const value = parts.slice(1).join(':').trim();\n      if (key && value) metadata[key] = value;\n    }\n  });\n  return {\n    success: true,\n    book: {\n      title,\n      rating: ratingText ? Number(ratingText) : null,\n      metadata,\n      intro: introBlocks.join('\\n\\n'),\n      cover,\n      url: window.location.href\n    }\n  };\n}"},{"name":"douban_movie","description":"Get detailed movie/TV info with rating, cast, and hot reviews from Douban","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Douban subject ID (e.g. 1292052 for The Shawshank Redemption)"}},"required":["id"]},"handler":"(params) => {\n  const run = async function(args) {\n\n      if (!args.id) return {error: 'Missing argument: id'};\n      const id = String(args.id).trim();\n\n      // Fetch structured data from the JSON API\n      const apiResp = await fetch('https://movie.douban.com/j/subject_abstract?subject_id=' + id, {credentials: 'include'});\n      if (!apiResp.ok) return {error: 'HTTP ' + apiResp.status, hint: 'Not logged in or invalid ID?'};\n      const apiData = await apiResp.json();\n      if (apiData.r !== 0 || !apiData.subject) return {error: 'Subject not found', hint: 'Check the ID'};\n\n      const s = apiData.subject;\n\n      // Also fetch the HTML page for richer data (summary, rating distribution, hot comments)\n      const pageResp = await fetch('https://movie.douban.com/subject/' + id + '/', {credentials: 'include'});\n      let summary = '', ratingDist = {}, hotComments = [], recommendations = [], votes = null, info = '';\n\n      if (pageResp.ok) {\n        const html = await pageResp.text();\n        const doc = new DOMParser().parseFromString(html, 'text/html');\n\n        // Summary\n        const summaryEl = doc.querySelector('[property=\"v:summary\"]');\n        summary = summaryEl ? summaryEl.textContent.trim() : '';\n\n        // Vote count\n        const votesEl = doc.querySelector('[property=\"v:votes\"]');\n        votes = votesEl ? parseInt(votesEl.textContent) : null;\n\n        // Info block\n        const infoEl = doc.querySelector('#info');\n        info = infoEl ? infoEl.innerText || infoEl.textContent.trim() : '';\n\n        // Rating distribution\n        doc.querySelectorAll('.ratings-on-weight .item').forEach(function(el) {\n          var star = el.querySelector('span:first-child');\n          var pct = el.querySelector('.rating_per');\n          if (star && pct) ratingDist[star.textContent.trim()] = pct.textContent.trim();\n        });\n\n        // Hot comments\n        doc.querySelectorAll('#hot-comments .comment-item').forEach(function(el) {\n          var author = el.querySelector('.comment-info a');\n          var rating = el.querySelector('.comment-info .rating');\n          var content = el.querySelector('.short');\n          var voteCount = el.querySelector('.vote-count');\n          var date = el.querySelector('.comment-time');\n          hotComments.push({\n            author: author ? author.textContent.trim() : '',\n            rating: rating ? rating.title : '',\n            content: content ? content.textContent.trim() : '',\n            votes: voteCount ? parseInt(voteCount.textContent) || 0 : 0,\n            date: date ? date.textContent.trim() : ''\n          });\n        });\n\n        // Recommendations\n        doc.querySelectorAll('.recommendations-bd dl').forEach(function(dl) {\n          var a = dl.querySelector('dd a');\n          if (a) {\n            var recId = a.href?.match(/subject\\/(\\d+)/)?.[1];\n            recommendations.push({title: a.textContent.trim(), id: recId, url: a.href});\n          }\n        });\n      }\n\n      // Parse info block for structured fields\n      const parseInfo = function(text) {\n        const result = {};\n        const lines = text.split('\\n').map(function(l) { return l.trim(); }).filter(Boolean);\n        lines.forEach(function(line) {\n          var m = line.match(/^(.+?):\\s*(.+)$/);\n          if (m) result[m[1].trim()] = m[2].trim();\n        });\n        return result;\n      };\n      const infoFields = parseInfo(info);\n\n      return {\n        id: s.id,\n        title: s.title,\n        subtype: s.subtype,\n        is_tv: s.is_tv,\n        rating: parseFloat(s.rate) || null,\n        votes: votes,\n        rating_distribution: ratingDist,\n        directors: s.directors,\n        actors: s.actors,\n        types: s.types,\n        region: s.region,\n        duration: s.duration,\n        release_year: s.release_year,\n        episodes_count: s.episodes_count || null,\n        imdb: infoFields['IMDb'] || null,\n        alias: infoFields['又名'] || null,\n        language: infoFields['语言'] || null,\n        release_date: infoFields['上映日期'] || infoFields['首播'] || null,\n        summary: summary,\n        playable: s.playable,\n        url: s.url,\n        hot_comments: hotComments,\n        recommendations: recommendations\n      };\n  };\n  return run(params || {});\n}"},{"name":"douban_movie_hot","description":"Get hot/trending movies or TV shows on Douban by tag","inputSchema":{"type":"object","properties":{"type":{"type":"string","description":"Type: movie (default) or tv"},"tag":{"type":"string","description":"Tag filter (default: 热门). Movies: 热门/最新/豆瓣高分/冷门佳片/华语/欧美/韩国/日本. TV: 热门/国产剧/综艺/美剧/日剧/韩剧/日本动画/纪录片"},"count":{"type":"string","description":"Number of results (default: 20, max: 50)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const type = (args.type || 'movie').toLowerCase();\n      if (type !== 'movie' && type !== 'tv') return {error: 'Invalid type. Use \"movie\" or \"tv\"'};\n\n      const tag = args.tag || '热门';\n      const count = Math.min(parseInt(args.count) || 20, 50);\n\n      const url = 'https://movie.douban.com/j/search_subjects?type=' + type\n        + '&tag=' + encodeURIComponent(tag)\n        + '&page_limit=' + count\n        + '&page_start=0';\n\n      const resp = await fetch(url, {credentials: 'include'});\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};\n      const d = await resp.json();\n\n      if (!d.subjects) return {error: 'No data returned', hint: 'Invalid tag or not logged in?'};\n\n      const items = d.subjects.map(function(s, i) {\n        return {\n          rank: i + 1,\n          id: s.id,\n          title: s.title,\n          rating: s.rate ? parseFloat(s.rate) : null,\n          cover: s.cover,\n          url: s.url,\n          playable: s.playable,\n          is_new: s.is_new,\n          episodes_info: s.episodes_info || null\n        };\n      });\n\n      // Also fetch available tags for reference\n      var tagsResp = await fetch('https://movie.douban.com/j/search_tags?type=' + type + '&source=index', {credentials: 'include'});\n      var availableTags = [];\n      if (tagsResp.ok) {\n        var tagsData = await tagsResp.json();\n        availableTags = tagsData.tags || [];\n      }\n\n      return {\n        type: type,\n        tag: tag,\n        count: items.length,\n        available_tags: availableTags,\n        items: items\n      };\n  };\n  return run(params || {});\n}"},{"name":"douban_subject","description":"获取电影详情","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"电影 ID"}},"required":["id"]},"handler":"(params) => {\n  const args = Object.assign({}, params || {});\n  let data = null;\n  let __wbContextUrl = globalThis.location?.href || '';\n  let __wbContextDocument = globalThis.document || null;\n    const __wbDefault = (value, fallback) => (value === undefined || value === null || value === '' ? fallback : value);\n    const __wbJoin = (value, separator) => Array.isArray(value) ? value.join(separator) : (value ?? '');\n    const __wbGet = (value, path) => {\n      if (!path) return value;\n      return String(path).split('.').reduce((acc, part) => (acc == null ? undefined : acc[part]), value);\n    };\n    const __wbBaseUrl = () => (__wbContextUrl || globalThis.location?.href || 'https://example.com/');\n    const __wbResolve = (target, base) => {\n      if (target == null) return '';\n      const text = String(target);\n      try {\n        return /^https?:/i.test(text) ? text : new URL(text, base || __wbBaseUrl()).toString();\n      } catch (_error) {\n        return text;\n      }\n    };\n    const __wbFetch = async (target, options) => {\n      const url = __wbResolve(target, __wbBaseUrl());\n      const resp = await globalThis.fetch(url, Object.assign({ credentials: 'include' }, options || {}));\n      if (!resp.ok) {\n        throw new Error(`HTTP ${resp.status} for ${url}`);\n      }\n      const text = await resp.text();\n      try {\n        return { url, text, data: JSON.parse(text) };\n      } catch (_error) {\n        return { url, text, data: text };\n      }\n    };\n  return (async () => {\n    {\n      const __wbResp = await __wbFetch(`https://movie.douban.com/subject/${args.id}`);\n      __wbContextUrl = __wbResp.url;\n      __wbContextDocument = new DOMParser().parseFromString(__wbResp.text, 'text/html');\n      data = __wbContextDocument;\n    }\n    {\n      const document = __wbContextDocument || globalThis.document;\n      const location = new URL(__wbBaseUrl());\n      const window = { document, location };\n      const fetch = (target, options) => globalThis.fetch(__wbResolve(target, __wbBaseUrl()), Object.assign({ credentials: 'include' }, options || {}));\n      data = await (((async () => {\n  const id = '(args.id)';\n  \n  // Wait for page to load\n  await new Promise(r => setTimeout(r, 2000));\n  \n  // Extract title\n  const titleEl = document.querySelector('span[property=\"v:itemreviewed\"]');\n  const title = titleEl?.textContent?.trim() || '';\n  \n  // Extract original title\n  const ogTitleEl = document.querySelector('span[property=\"v:originalTitle\"]');\n  const originalTitle = ogTitleEl?.textContent?.trim() || '';\n  \n  // Extract year\n  const yearEl = document.querySelector('.year');\n  const year = yearEl?.textContent?.trim() || '';\n  \n  // Extract rating\n  const ratingEl = document.querySelector('strong[property=\"v:average\"]');\n  const rating = parseFloat(ratingEl?.textContent || '0');\n  \n  // Extract rating count\n  const ratingCountEl = document.querySelector('span[property=\"v:votes\"]');\n  const ratingCount = parseInt(ratingCountEl?.textContent || '0', 10);\n  \n  // Extract genres\n  const genreEls = document.querySelectorAll('span[property=\"v:genre\"]');\n  const genres = Array.from(genreEls).map(el => el.textContent?.trim()).filter(Boolean).join(',');\n  \n  // Extract directors\n  const directorEls = document.querySelectorAll('a[rel=\"v:directedBy\"]');\n  const directors = Array.from(directorEls).map(el => el.textContent?.trim()).filter(Boolean).join(',');\n  \n  // Extract casts\n  const castEls = document.querySelectorAll('a[rel=\"v:starring\"]');\n  const casts = Array.from(castEls).slice(0, 5).map(el => el.textContent?.trim()).filter(Boolean).join(',');\n  \n  // Extract summary\n  const summaryEl = document.querySelector('span[property=\"v:summary\"]');\n  const summary = summaryEl?.textContent?.trim() || '';\n  \n  return [{\n    id,\n    title,\n    originalTitle,\n    year,\n    rating,\n    ratingCount,\n    genres,\n    directors,\n    casts,\n    summary: summary.substring(0, 200),\n    url: `https://movie.douban.com/subject/${id}`\n  }];\n})())());\n    }\n    return data;\n  })();\n}"},{"name":"douban_top250","description":"Get Douban Top 250 movies list","inputSchema":{"type":"object","properties":{"start":{"type":"string","description":"Start position (default: 0, step by 25). Use 0 for #1-25, 25 for #26-50, etc."}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const start = parseInt(args.start) || 0;\n\n      const resp = await fetch('https://movie.douban.com/top250?start=' + start, {credentials: 'include'});\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};\n      const html = await resp.text();\n      const doc = new DOMParser().parseFromString(html, 'text/html');\n\n      const items = [];\n      doc.querySelectorAll('.grid_view .item').forEach(function(el) {\n        var rank = el.querySelector('.pic em');\n        var titleEl = el.querySelector('.hd a .title');\n        var otherTitleEl = el.querySelector('.hd a .other');\n        var ratingEl = el.querySelector('.rating_num');\n        var link = el.querySelector('.hd a');\n        var quoteEl = el.querySelector('.quote .inq') || el.querySelector('.quote span');\n        var infoEl = el.querySelector('.bd p');\n\n        // Vote count is in a span like \"3268455人评价\"\n        var voteSpans = el.querySelectorAll('.bd div span');\n        var votes = null;\n        voteSpans.forEach(function(sp) {\n          var m = sp.textContent.match(/(\\d+)人评价/);\n          if (m) votes = parseInt(m[1]);\n        });\n\n        var id = link?.href?.match(/subject\\/(\\d+)/)?.[1];\n\n        // Parse info line for director, year, region, genre\n        var infoText = infoEl ? infoEl.textContent.trim() : '';\n        var lines = infoText.split('\\n').map(function(l) { return l.trim(); }).filter(Boolean);\n        var directorLine = lines[0] || '';\n        var metaLine = lines[1] || '';\n        var metaParts = metaLine.split('/').map(function(p) { return p.trim(); });\n\n        items.push({\n          rank: rank ? parseInt(rank.textContent) : null,\n          id: id,\n          title: titleEl ? titleEl.textContent.trim() : '',\n          other_title: otherTitleEl ? otherTitleEl.textContent.trim().replace(/^\\s*\\/\\s*/, '') : '',\n          rating: ratingEl ? parseFloat(ratingEl.textContent) : null,\n          votes: votes,\n          quote: quoteEl ? quoteEl.textContent.trim() : '',\n          year: metaParts[0] || '',\n          region: metaParts[1] || '',\n          genre: metaParts[2] || '',\n          url: link ? link.href : ''\n        });\n      });\n\n      return {\n        start: start,\n        count: items.length,\n        total: 250,\n        has_more: start + items.length < 250,\n        next_start: start + items.length < 250 ? start + 25 : null,\n        items: items\n      };\n  };\n  return run(params || {});\n}"},{"name":"douban_comments","description":"Get short reviews/comments for a Douban movie or TV show","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Douban subject ID (e.g. 1292052)"},"sort":{"type":"string","description":"Sort order: new_score (default, hot), time (newest first)"},"count":{"type":"string","description":"Number of comments (default: 20, max: 50)"}},"required":["id"]},"handler":"(params) => {\n  const run = async function(args) {\n\n      if (!args.id) return {error: 'Missing argument: id'};\n      const id = String(args.id).trim();\n      const sort = args.sort || 'new_score';\n      const count = Math.min(parseInt(args.count) || 20, 50);\n\n      if (sort !== 'new_score' && sort !== 'time') {\n        return {error: 'Invalid sort. Use \"new_score\" (hot) or \"time\" (newest)'};\n      }\n\n      const url = 'https://movie.douban.com/j/subject/' + id + '/comments?start=0&limit=' + count + '&status=P&sort=' + sort;\n\n      const resp = await fetch(url, {credentials: 'include'});\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};\n      const d = await resp.json();\n\n      if (d.retcode !== 1 || !d.result) return {error: 'Failed to fetch comments', hint: 'Invalid ID or not logged in?'};\n\n      const ratingMap = {'1': '很差', '2': '较差', '3': '还行', '4': '推荐', '5': '力荐'};\n\n      const comments = (d.result.normal || []).map(function(c) {\n        var userId = c.user?.path?.match(/people\\/([^/]+)/)?.[1];\n        return {\n          id: c.id,\n          author: c.user?.name || '',\n          author_id: userId || '',\n          rating: c.rating ? parseInt(c.rating) : null,\n          rating_label: c.rating_word || ratingMap[c.rating] || '',\n          content: c.content || '',\n          votes: c.votes || 0,\n          date: c.time || ''\n        };\n      });\n\n      return {\n        subject_id: id,\n        sort: sort,\n        total: d.result.total_num || 0,\n        count: comments.length,\n        comments: comments\n      };\n  };\n  return run(params || {});\n}"}]}