{"url_pattern":"^https?://(www\\.)?qidian\\.com(/.*)?$","site_name":"qidian","allowed_domains":["book.qidian.com","qidian.com"],"tools":[{"name":"qidian_get_current_book","description":"Extract book metadata from the current Qidian detail page","inputSchema":{"type":"object","properties":{},"required":null},"handler":"() => {\n  const title = document.querySelector('.book-info h1 em, .book-information h1 em, h1 em')?.textContent?.trim() || document.title;\n  const author = document.querySelector('.book-info h1 a.writer, .book-information .writer, a.writer')?.textContent?.trim() || '';\n  const intro = document.querySelector('.book-intro p, .book-intro-detail, .intro')?.textContent?.replace(/\\s+/g, ' ').trim() || '';\n  const category = Array.from(document.querySelectorAll('.tag span, .book-info .tag a, .book-information .tag a')).map((node) => node.textContent?.trim()).filter(Boolean);\n  return {\n    success: true,\n    book: {\n      title,\n      author,\n      intro,\n      category,\n      url: window.location.href\n    }\n  };\n}"},{"name":"qidian_open_book","description":"Open a Qidian book detail page by book ID","inputSchema":{"type":"object","properties":{"bookId":{"type":"string","description":"Qidian book ID"}},"required":["bookId"]},"handler":"(params) => {\n  window.location.href = 'https://book.qidian.com/info/' + params.bookId + '/';\n  return { success: true, message: 'Opening book detail...', bookId: params.bookId };\n}"},{"name":"qidian_search","description":"起点中文网小说搜索","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"Search keyword (e.g. 仙侠, 系统流)"},"page":{"type":"string","description":"Page number (default 1)"}},"required":["query"]},"handler":"(params) => {\n  const run = async function(args) {\n\n      if (!args.query) return {error: 'Missing argument: query'};\n      const page = parseInt(args.page) || 1;\n      const kw = encodeURIComponent(args.query);\n\n      const url = 'https://www.qidian.com/so/' + kw + '.html' + (page > 1 ? '?page=' + page : '');\n      const resp = await fetch(url, {credentials: 'include'});\n      if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Make sure a qidian.com tab is open'};\n\n      const html = await resp.text();\n      const parser = new DOMParser();\n      const doc = parser.parseFromString(html, 'text/html');\n\n      // Try embedded JSON data first\n      let jsonData = null;\n      for (const s of doc.querySelectorAll('script')) {\n        const text = s.textContent || '';\n        for (const pat of [\n          /g_search_data\\s*=\\s*(\\{[\\s\\S]*?\\});/,\n          /window\\.__INITIAL_STATE__\\s*=\\s*(\\{[\\s\\S]*?\\});/,\n          /\"bookList\"\\s*:\\s*(\\[[\\s\\S]*?\\])\\s*[,}]/\n        ]) {\n          const m = text.match(pat);\n          if (m) { try { jsonData = JSON.parse(m[1]); } catch(e) {} }\n          if (jsonData) break;\n        }\n        if (jsonData) break;\n      }\n\n      if (jsonData) {\n        const bookList = jsonData.bookList || jsonData.books || jsonData.data?.bookList || [];\n        if (Array.isArray(bookList) && bookList.length > 0) {\n          const books = bookList.map((b, i) => ({\n            rank: i + 1,\n            id: b.bookId || b.bid || b.id,\n            title: b.bookName || b.bName || b.title || b.name,\n            author: b.authorName || b.author || b.aName,\n            category: b.catName || b.category || b.cat,\n            description: (b.desc || b.description || b.intro || '').substring(0, 300),\n            wordCount: b.totalWordCount || b.wordCount || b.cnt || null,\n            status: b.isFinish === 1 ? '完结' : b.isFinish === 0 ? '连载' : b.state || null,\n            cover: b.bookCoverUrl || b.cover || null,\n            url: 'https://book.qidian.com/info/' + (b.bookId || b.bid || b.id) + '/'\n          }));\n          return {query: args.query, page, count: books.length, books};\n        }\n      }\n\n      // DOM parsing: Qidian uses <li class=\"res-book-item\"> for each result\n      const resultItems = doc.querySelectorAll(\n        '.res-book-item, .book-result-item, li[data-bid], ' +\n        '.search-result-page .book-img-text li'\n      );\n\n      if (resultItems.length > 0) {\n        const seen = new Set();\n        const books = [];\n        resultItems.forEach((el) => {\n          // Title: first meaningful link\n          const titleEl = el.querySelector('h2 a, h3 a, h4 a, [class*=\"title\"] a');\n          const title = titleEl ? titleEl.textContent.trim() : '';\n          if (!title) return;\n\n          // Book ID from link\n          const href = titleEl ? (titleEl.getAttribute('href') || '') : '';\n          const idMatch = href.match(/\\/(?:info|book)\\/(\\d+)/);\n          const bookId = idMatch ? idMatch[1] : (el.getAttribute('data-bid') || null);\n          if (bookId && seen.has(bookId)) return;\n          if (bookId) seen.add(bookId);\n\n          // Parse the full text content to extract structured fields\n          // Typical format: \"Title Author |Category|Status Description... 最新更新 Chapter·Time WordCount\"\n          const fullText = (el.textContent || '').replace(/\\s+/g, ' ').trim();\n\n          // Extract \"author |category|status\" metadata\n          let author = '';\n          let category = '';\n          let status = null;\n          const metaMatch = fullText.match(/([^\\s|]{2,})\\s*\\|([^|]+)\\|(\\S+)/);\n          if (metaMatch) {\n            author = metaMatch[1].trim();\n            category = metaMatch[2].trim();\n            const st = metaMatch[3].trim();\n            if (st === '连载' || st === '完结') status = st;\n          }\n\n          // Extract description: text between \"status\" and \"最新更新\"\n          let description = '';\n          const descMatch = fullText.match(/\\|(连载|完结)\\s+(.+?)(?:\\s+最新更新|$)/);\n          if (descMatch && descMatch[2]) {\n            description = descMatch[2].trim().substring(0, 300);\n          }\n\n          // Word count: \"XX.XX万总字数\" or \"XX万字\"\n          let wordCount = null;\n          const wcMatch = fullText.match(/([\\d,.]+)\\s*万(?:总字数|字)/);\n          if (wcMatch) wordCount = wcMatch[1] + '万字';\n\n          // Last update\n          let lastUpdate = null;\n          const updateMatch = fullText.match(/最新更新\\s*(.+?)(?:\\s+[\\d,.]+万|$)/);\n          if (updateMatch) lastUpdate = updateMatch[1].trim();\n\n          // Cover image\n          const imgEl = el.querySelector('img[src*=\"bookcover\"], img[src*=\"qdbimg\"], img[data-src*=\"bookcover\"]');\n          let cover = imgEl ? (imgEl.getAttribute('src') || imgEl.getAttribute('data-src')) : null;\n          if (cover && cover.startsWith('//')) cover = 'https:' + cover;\n\n          const bookUrl = bookId\n            ? 'https://book.qidian.com/info/' + bookId + '/'\n            : (href.startsWith('http') ? href : 'https://www.qidian.com' + href);\n\n          books.push({\n            rank: books.length + 1, id: bookId, title, author, category,\n            description, wordCount, status, lastUpdate, cover, url: bookUrl\n          });\n        });\n\n        if (books.length > 0) {\n          const pageInfo = doc.querySelector('[class*=\"pagination\"], [class*=\"page\"]');\n          const hasMore = pageInfo ? pageInfo.textContent.includes('下一页') : false;\n          return {query: args.query, page, count: books.length, hasMore, books};\n        }\n      }\n\n      // Last resort: collect unique book links\n      const allBookLinks = doc.querySelectorAll('a[href*=\"book.qidian.com/info/\"], a[href*=\"/info/\"], a[href*=\"/book/\"]');\n      if (allBookLinks.length > 0) {\n        const seen = new Set();\n        const books = [];\n        allBookLinks.forEach((a) => {\n          const href = a.getAttribute('href') || '';\n          const idMatch = href.match(/\\/(?:info|book)\\/(\\d+)/);\n          const bookId = idMatch ? idMatch[1] : null;\n          if (!bookId || seen.has(bookId)) return;\n          seen.add(bookId);\n          const title = a.textContent.trim();\n          if (!title || title.length > 100) return;\n          books.push({\n            rank: books.length + 1, id: bookId, title,\n            url: 'https://book.qidian.com/info/' + bookId + '/'\n          });\n        });\n        if (books.length > 0) {\n          return {query: args.query, page, count: books.length, books, note: 'Partial results'};\n        }\n      }\n\n      return {\n        error: 'No results found',\n        hint: 'Navigate to www.qidian.com first. The page may require login or the HTML structure has changed.',\n        debug: { htmlLength: html.length, title: doc.title || '' }\n      };\n  };\n  return run(params || {});\n}"},{"name":"qidian_search_books","description":"Search novels on Qidian and return structured results","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"Search keyword"},"page":{"type":"number","description":"Page number, default 1"},"limit":{"type":"number","description":"Maximum results to return, default 10"}},"required":["query"]},"handler":"(params) => {\n  if (!params.query) {\n    return { success: false, message: 'query is required' };\n  }\n  const page = Number(params.page || 1) || 1;\n  const kw = encodeURIComponent(params.query);\n  const url = 'https://www.qidian.com/so/' + kw + '.html' + (page > 1 ? '?page=' + page : '');\n  return fetch(url, { credentials: 'include' })\n    .then((resp) => resp.ok ? resp.text() : Promise.reject(new Error('HTTP ' + resp.status)))\n    .then((html) => {\n      const parser = new DOMParser();\n      const doc = parser.parseFromString(html, 'text/html');\n      const books = [];\n      const seen = new Set();\n      doc.querySelectorAll('.res-book-item, .book-result-item, li[data-bid], .search-result-page .book-img-text li').forEach((el) => {\n        const titleEl = el.querySelector('h2 a, h3 a, h4 a, [class*=\"title\"] a');\n        const title = titleEl ? titleEl.textContent.trim() : '';\n        if (!title) return;\n        const href = titleEl ? (titleEl.getAttribute('href') || '') : '';\n        const idMatch = href.match(/\\/(?:info|book)\\/(\\d+)/);\n        const bookId = idMatch ? idMatch[1] : (el.getAttribute('data-bid') || null);\n        if (bookId && seen.has(bookId)) return;\n        if (bookId) seen.add(bookId);\n        const text = (el.textContent || '').replace(/\\s+/g, ' ').trim();\n        const metaMatch = text.match(/([^\\s|]{2,})\\s*\\|([^|]+)\\|(\\S+)/);\n        const wordCountMatch = text.match(/([\\d,.]+)\\s*万(?:总字数|字)/);\n        books.push({\n          rank: books.length + 1,\n          bookId,\n          title,\n          author: metaMatch ? metaMatch[1].trim() : '',\n          category: metaMatch ? metaMatch[2].trim() : '',\n          status: metaMatch ? metaMatch[3].trim() : null,\n          wordCount: wordCountMatch ? wordCountMatch[1] + '万字' : null,\n          url: bookId ? 'https://book.qidian.com/info/' + bookId + '/' : (href.startsWith('http') ? href : 'https://www.qidian.com' + href)\n        });\n      });\n      return { success: true, query: params.query, page, count: books.length, books: books.slice(0, Number(params.limit || 10) || 10) };\n    })\n    .catch((error) => ({ success: false, message: error.message }));\n}"}]}