browser-web-search 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +691 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/sites/36kr/newsflash.js +76 -0
- package/sites/baidu/search.js +62 -0
- package/sites/bilibili/comments.js +74 -0
- package/sites/bilibili/feed.js +79 -0
- package/sites/bilibili/history.js +43 -0
- package/sites/bilibili/me.js +45 -0
- package/sites/bilibili/popular.js +44 -0
- package/sites/bilibili/ranking.js +42 -0
- package/sites/bilibili/search.js +46 -0
- package/sites/bilibili/trending.js +32 -0
- package/sites/bilibili/video.js +73 -0
- package/sites/bing/search.js +40 -0
- package/sites/boss/detail.js +38 -0
- package/sites/boss/search.js +44 -0
- package/sites/cnblogs/search.js +68 -0
- package/sites/csdn/search.js +51 -0
- package/sites/douban/comments.js +58 -0
- package/sites/douban/movie-hot.js +64 -0
- package/sites/douban/movie-top.js +65 -0
- package/sites/douban/movie.js +117 -0
- package/sites/douban/search.js +90 -0
- package/sites/douban/top250.js +73 -0
- package/sites/github/fork.js +38 -0
- package/sites/github/issue-create.js +42 -0
- package/sites/github/issues.js +32 -0
- package/sites/github/me.js +22 -0
- package/sites/github/pr-create.js +55 -0
- package/sites/github/repo.js +27 -0
- package/sites/google/search.js +54 -0
- package/sites/toutiao/hot.js +107 -0
- package/sites/toutiao/search.js +146 -0
- package/sites/xiaohongshu/comments.js +56 -0
- package/sites/xiaohongshu/feed.js +50 -0
- package/sites/xiaohongshu/me.js +49 -0
- package/sites/xiaohongshu/note.js +63 -0
- package/sites/xiaohongshu/search.js +56 -0
- package/sites/xiaohongshu/user_posts.js +53 -0
- package/sites/zhihu/hot.js +36 -0
- package/sites/zhihu/me.js +43 -0
- package/sites/zhihu/question.js +62 -0
- package/sites/zhihu/search.js +58 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "toutiao/search",
|
|
4
|
+
"description": "今日头条搜索",
|
|
5
|
+
"domain": "so.toutiao.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"query": {"required": true, "description": "搜索关键词"},
|
|
8
|
+
"count": {"required": false, "description": "返回结果数量 (默认 10, 最多 20)"}
|
|
9
|
+
},
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site toutiao/search AI"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.query) return {error: 'Missing argument: query', hint: 'Provide a search keyword'};
|
|
17
|
+
const count = Math.min(parseInt(args.count) || 10, 20);
|
|
18
|
+
|
|
19
|
+
const url = 'https://so.toutiao.com/search?keyword=' + encodeURIComponent(args.query) + '&pd=information&dvpf=pc';
|
|
20
|
+
const resp = await fetch(url, {credentials: 'include'});
|
|
21
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Open so.toutiao.com in bb-browser first'};
|
|
22
|
+
|
|
23
|
+
const html = await resp.text();
|
|
24
|
+
const parser = new DOMParser();
|
|
25
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
26
|
+
|
|
27
|
+
const results = [];
|
|
28
|
+
|
|
29
|
+
// Helper: extract clean article URL from jump redirect chain
|
|
30
|
+
function extractArticleUrl(href) {
|
|
31
|
+
if (!href) return '';
|
|
32
|
+
try {
|
|
33
|
+
// Decode nested jump URLs to find the real toutiao article URL
|
|
34
|
+
let decoded = href;
|
|
35
|
+
for (let i = 0; i < 5; i++) {
|
|
36
|
+
const match = decoded.match(/toutiao\.com(?:%2F|\/)+a?(\d{15,})/);
|
|
37
|
+
if (match) return 'https://www.toutiao.com/article/' + match[1] + '/';
|
|
38
|
+
const groupMatch = decoded.match(/group(?:%2F|\/)(\d{15,})/);
|
|
39
|
+
if (groupMatch) return 'https://www.toutiao.com/article/' + groupMatch[1] + '/';
|
|
40
|
+
decoded = decodeURIComponent(decoded);
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
return href;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Strategy 1: SSR HTML uses cs-card containers
|
|
47
|
+
const cards = doc.querySelectorAll('.cs-card');
|
|
48
|
+
for (const card of cards) {
|
|
49
|
+
const titleLink = card.querySelector('a[href*="search/jump"]');
|
|
50
|
+
if (!titleLink) continue;
|
|
51
|
+
|
|
52
|
+
const title = (titleLink.textContent || '').trim();
|
|
53
|
+
if (!title || title.length < 2) continue;
|
|
54
|
+
// Skip non-result links like "去西瓜搜" / "去抖音搜"
|
|
55
|
+
if (title.includes('去西瓜搜') || title.includes('去抖音搜')) continue;
|
|
56
|
+
|
|
57
|
+
const articleUrl = extractArticleUrl(titleLink.getAttribute('href') || '');
|
|
58
|
+
|
|
59
|
+
// Extract snippet & source & time from card text
|
|
60
|
+
const fullText = (card.textContent || '').trim();
|
|
61
|
+
// Remove the title (may appear twice) to get the rest
|
|
62
|
+
let rest = fullText;
|
|
63
|
+
const titleIdx = rest.indexOf(title);
|
|
64
|
+
if (titleIdx >= 0) rest = rest.substring(titleIdx + title.length);
|
|
65
|
+
// Remove second occurrence of title if present
|
|
66
|
+
const titleIdx2 = rest.indexOf(title);
|
|
67
|
+
if (titleIdx2 >= 0) rest = rest.substring(titleIdx2 + title.length);
|
|
68
|
+
rest = rest.trim();
|
|
69
|
+
|
|
70
|
+
let snippet = '';
|
|
71
|
+
let source = '';
|
|
72
|
+
let time = '';
|
|
73
|
+
|
|
74
|
+
// Remove trailing comment count like "1评论" or "23评论" first
|
|
75
|
+
rest = rest.replace(/\d+评论/g, '').trim();
|
|
76
|
+
|
|
77
|
+
// Extract time from the tail first
|
|
78
|
+
// Time patterns: "3天前", "12小时前", "5分钟前", "前天17:23", "昨天08:00", "2024-01-01"
|
|
79
|
+
// The number-based patterns (N天前 etc.) must NOT be preceded by a digit
|
|
80
|
+
const timeMatch = rest.match(/((?<=[^\d])|^)(\d{1,2}(?:小时|分钟|天)前|前天[\d:]*|昨天[\d:]*|\d{4}[-/.]\d{2}[-/.]\d{2}.*)$/);
|
|
81
|
+
if (timeMatch) {
|
|
82
|
+
time = timeMatch[2] ? timeMatch[2].trim() : timeMatch[0].trim();
|
|
83
|
+
rest = rest.substring(0, rest.length - timeMatch[0].length).trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Source is the short text at the end (author/media name, typically 2-20 chars)
|
|
87
|
+
// Pattern: "...snippet content...SourceName"
|
|
88
|
+
const sourceMatch = rest.match(/^([\s\S]+?)([\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z0-9_\s]{1,19})$/);
|
|
89
|
+
if (sourceMatch && sourceMatch[1].length > 10) {
|
|
90
|
+
snippet = sourceMatch[1].trim().substring(0, 300);
|
|
91
|
+
source = sourceMatch[2].trim();
|
|
92
|
+
} else {
|
|
93
|
+
snippet = rest.substring(0, 300);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
results.push({title, snippet, source, time, url: articleUrl});
|
|
97
|
+
if (results.length >= count) break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Strategy 2: Fallback to finding jump links with article IDs
|
|
101
|
+
if (results.length === 0) {
|
|
102
|
+
const links = doc.querySelectorAll('a[href*="search/jump"]');
|
|
103
|
+
for (const link of links) {
|
|
104
|
+
const text = (link.textContent || '').trim();
|
|
105
|
+
if (!text || text.length < 4) continue;
|
|
106
|
+
// Skip navigation/promo links
|
|
107
|
+
if (text.includes('去西瓜搜') || text.includes('去抖音搜') || text.includes('APP')) continue;
|
|
108
|
+
|
|
109
|
+
const href = link.getAttribute('href') || '';
|
|
110
|
+
// Only include links that point to actual articles
|
|
111
|
+
if (!href.match(/toutiao\.com|group|a\d{10,}/)) continue;
|
|
112
|
+
|
|
113
|
+
const articleUrl = extractArticleUrl(href);
|
|
114
|
+
if (results.some(r => r.title === text)) continue;
|
|
115
|
+
|
|
116
|
+
// Try to get snippet from sibling/parent context
|
|
117
|
+
let snippet = '';
|
|
118
|
+
const container = link.closest('[class*="card"]') || link.parentElement?.parentElement;
|
|
119
|
+
if (container) {
|
|
120
|
+
const containerText = (container.textContent || '').trim();
|
|
121
|
+
const afterTitle = containerText.indexOf(text);
|
|
122
|
+
if (afterTitle >= 0) {
|
|
123
|
+
const rest = containerText.substring(afterTitle + text.length).trim();
|
|
124
|
+
if (rest.length > 10) snippet = rest.substring(0, 300);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
results.push({title: text, snippet, source: '', time: '', url: articleUrl});
|
|
129
|
+
if (results.length >= count) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (results.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
error: 'No results found',
|
|
136
|
+
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',
|
|
137
|
+
query: args.query
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
query: args.query,
|
|
143
|
+
count: results.length,
|
|
144
|
+
results
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/comments",
|
|
4
|
+
"description": "获取小红书笔记的评论列表",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"note_id": {"required": true, "description": "Note ID"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site xiaohongshu/comments 69aa7160000000001b01634d"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.note_id) return {error: 'Missing argument: note_id'};
|
|
17
|
+
|
|
18
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
19
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
20
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Not logged in?'};
|
|
21
|
+
|
|
22
|
+
// 先通过 note store 设置当前笔记(触发评论加载)
|
|
23
|
+
const noteStore = pinia._s.get('note');
|
|
24
|
+
if (!noteStore) return {error: 'Note store not found', hint: 'Not logged in?'};
|
|
25
|
+
|
|
26
|
+
let captured = null;
|
|
27
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
28
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
29
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
30
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
31
|
+
if (this.__url?.includes('comment/page') && this.__url?.includes(args.note_id)) {
|
|
32
|
+
const x = this;
|
|
33
|
+
const orig = x.onreadystatechange;
|
|
34
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
35
|
+
}
|
|
36
|
+
return origSend.apply(this, arguments);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// 设置当前 noteId,触发评论加载
|
|
41
|
+
noteStore.setCurrentNoteId(args.note_id);
|
|
42
|
+
await noteStore.getNoteDetailByNoteId(args.note_id);
|
|
43
|
+
await new Promise(r => setTimeout(r, 800));
|
|
44
|
+
} finally {
|
|
45
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
46
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!captured?.success) return {error: captured?.msg || 'Comments fetch failed', hint: 'Not logged in?'};
|
|
50
|
+
const comments = (captured.data?.comments || []).map(c => ({
|
|
51
|
+
id: c.id, author: c.user_info?.nickname, author_id: c.user_info?.user_id,
|
|
52
|
+
content: c.content, likes: c.like_count,
|
|
53
|
+
sub_comment_count: c.sub_comment_count, created_time: c.create_time
|
|
54
|
+
}));
|
|
55
|
+
return {note_id: args.note_id, count: comments.length, has_more: captured.data?.has_more, comments};
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/feed",
|
|
4
|
+
"description": "获取小红书首页推荐 Feed",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {},
|
|
7
|
+
"capabilities": ["network"],
|
|
8
|
+
"readOnly": true
|
|
9
|
+
}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
async function(args) {
|
|
13
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
14
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
15
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Ensure xiaohongshu.com is fully loaded'};
|
|
16
|
+
|
|
17
|
+
const feedStore = pinia._s.get('feed');
|
|
18
|
+
if (!feedStore) return {error: 'Feed store not found', hint: 'Not logged in?'};
|
|
19
|
+
|
|
20
|
+
let captured = null;
|
|
21
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
22
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
23
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
24
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
25
|
+
if (this.__url?.includes('homefeed') && !this.__url?.includes('category')) {
|
|
26
|
+
const x = this;
|
|
27
|
+
const orig = x.onreadystatechange;
|
|
28
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
29
|
+
}
|
|
30
|
+
return origSend.apply(this, arguments);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await feedStore.fetchFeeds();
|
|
35
|
+
await new Promise(r => setTimeout(r, 500));
|
|
36
|
+
} finally {
|
|
37
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
38
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!captured?.success) return {error: captured?.msg || 'Feed fetch failed', hint: 'Not logged in?'};
|
|
42
|
+
const notes = (captured.data.items || []).map(item => ({
|
|
43
|
+
id: item.id, xsec_token: item.xsec_token,
|
|
44
|
+
title: item.note_card?.display_title, type: item.note_card?.type,
|
|
45
|
+
url: 'https://www.xiaohongshu.com/explore/' + item.id,
|
|
46
|
+
author: item.note_card?.user?.nickname, author_id: item.note_card?.user?.user_id,
|
|
47
|
+
likes: item.note_card?.interact_info?.liked_count
|
|
48
|
+
}));
|
|
49
|
+
return {count: notes.length, has_more: !!captured.data.cursor_score, notes};
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/me",
|
|
4
|
+
"description": "获取当前小红书登录用户信息",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {},
|
|
7
|
+
"capabilities": ["network"],
|
|
8
|
+
"readOnly": true
|
|
9
|
+
}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
async function(args) {
|
|
13
|
+
// 通过 pinia store 的 user action 获取,走页面完整签名链路
|
|
14
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
15
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
16
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Ensure xiaohongshu.com is fully loaded'};
|
|
17
|
+
|
|
18
|
+
// 拦截 user/me 的 response
|
|
19
|
+
let captured = null;
|
|
20
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
21
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
22
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
23
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
24
|
+
if (this.__url?.includes('/user/me')) {
|
|
25
|
+
const x = this;
|
|
26
|
+
const orig = x.onreadystatechange;
|
|
27
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
28
|
+
}
|
|
29
|
+
return origSend.apply(this, arguments);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const userStore = pinia._s.get('user');
|
|
34
|
+
if (userStore?.getUserInfo) await userStore.getUserInfo();
|
|
35
|
+
else {
|
|
36
|
+
// fallback: 直接触发 user/me 请求
|
|
37
|
+
const feedStore = pinia._s.get('feed');
|
|
38
|
+
if (feedStore) await feedStore.fetchFeeds();
|
|
39
|
+
}
|
|
40
|
+
await new Promise(r => setTimeout(r, 500));
|
|
41
|
+
} finally {
|
|
42
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
43
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!captured?.success) return {error: captured?.msg || 'Failed to get user info', hint: 'Not logged in?'};
|
|
47
|
+
const u = captured.data;
|
|
48
|
+
return {nickname: u.nickname, red_id: u.red_id, desc: u.desc, gender: u.gender, userid: u.user_id, url: 'https://www.xiaohongshu.com/user/profile/' + u.user_id};
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/note",
|
|
4
|
+
"description": "获取小红书笔记详情(标题、正文、互动数据)",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"note_id": {"required": true, "description": "Note ID or URL"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site xiaohongshu/note 69aa7160000000001b01634d"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.note_id) return {error: 'Missing argument: note_id'};
|
|
17
|
+
|
|
18
|
+
let noteId = args.note_id;
|
|
19
|
+
const urlMatch = noteId.match(/explore\/([a-f0-9]+)/);
|
|
20
|
+
if (urlMatch) noteId = urlMatch[1];
|
|
21
|
+
|
|
22
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
23
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
24
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Not logged in?'};
|
|
25
|
+
|
|
26
|
+
const noteStore = pinia._s.get('note');
|
|
27
|
+
if (!noteStore) return {error: 'Note store not found', hint: 'Not logged in?'};
|
|
28
|
+
|
|
29
|
+
let captured = null;
|
|
30
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
31
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
32
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
33
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
34
|
+
if (this.__url?.includes('/feed') && b?.includes?.(noteId)) {
|
|
35
|
+
const x = this;
|
|
36
|
+
const orig = x.onreadystatechange;
|
|
37
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
38
|
+
}
|
|
39
|
+
return origSend.apply(this, arguments);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await noteStore.getNoteDetailByNoteId(noteId);
|
|
44
|
+
await new Promise(r => setTimeout(r, 500));
|
|
45
|
+
} finally {
|
|
46
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
47
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!captured?.success) return {error: captured?.msg || 'Note fetch failed', hint: 'Note may be deleted or restricted'};
|
|
51
|
+
const note = captured.data?.items?.[0]?.note_card;
|
|
52
|
+
if (!note) return {error: 'Note not found in response'};
|
|
53
|
+
return {
|
|
54
|
+
note_id: noteId, title: note.title, desc: note.desc, type: note.type,
|
|
55
|
+
url: 'https://www.xiaohongshu.com/explore/' + noteId,
|
|
56
|
+
author: note.user?.nickname, author_id: note.user?.user_id,
|
|
57
|
+
likes: note.interact_info?.liked_count, comments: note.interact_info?.comment_count,
|
|
58
|
+
collects: note.interact_info?.collected_count, shares: note.interact_info?.share_count,
|
|
59
|
+
tags: note.tag_list?.map(t => t.name),
|
|
60
|
+
images: note.image_list?.map(img => img.info_list?.[0]?.url),
|
|
61
|
+
created_time: note.time
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/search",
|
|
4
|
+
"description": "搜索小红书笔记",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"keyword": {"required": true, "description": "Search keyword"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site xiaohongshu/search 美食"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.keyword) return {error: 'Missing argument: keyword'};
|
|
17
|
+
|
|
18
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
19
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
20
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Not logged in?'};
|
|
21
|
+
|
|
22
|
+
const searchStore = pinia._s.get('search');
|
|
23
|
+
if (!searchStore) return {error: 'Search store not found', hint: 'Not logged in?'};
|
|
24
|
+
|
|
25
|
+
let captured = null;
|
|
26
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
27
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
28
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
29
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
30
|
+
if (this.__url?.includes('search/notes')) {
|
|
31
|
+
const x = this;
|
|
32
|
+
const orig = x.onreadystatechange;
|
|
33
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
34
|
+
}
|
|
35
|
+
return origSend.apply(this, arguments);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
searchStore.mutateSearchValue(args.keyword);
|
|
40
|
+
await searchStore.loadMore();
|
|
41
|
+
await new Promise(r => setTimeout(r, 500));
|
|
42
|
+
} finally {
|
|
43
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
44
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!captured?.success) return {error: captured?.msg || 'Search failed', hint: 'Not logged in?'};
|
|
48
|
+
const notes = (captured.data?.items || []).map(i => ({
|
|
49
|
+
id: i.id, xsec_token: i.xsec_token,
|
|
50
|
+
title: i.note_card?.display_title, type: i.note_card?.type,
|
|
51
|
+
url: 'https://www.xiaohongshu.com/explore/' + i.id,
|
|
52
|
+
author: i.note_card?.user?.nickname,
|
|
53
|
+
likes: i.note_card?.interact_info?.liked_count
|
|
54
|
+
}));
|
|
55
|
+
return {keyword: args.keyword, count: notes.length, has_more: captured.data?.has_more, notes};
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "xiaohongshu/user_posts",
|
|
4
|
+
"description": "获取小红书用户的笔记列表",
|
|
5
|
+
"domain": "www.xiaohongshu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"user_id": {"required": true, "description": "User ID"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site xiaohongshu/user_posts 5a927d8411be10720ae9e1e4"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
if (!args.user_id) return {error: 'Missing argument: user_id'};
|
|
17
|
+
|
|
18
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
19
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
20
|
+
if (!pinia?._s) return {error: 'Page not ready', hint: 'Not logged in?'};
|
|
21
|
+
|
|
22
|
+
const userStore = pinia._s.get('user');
|
|
23
|
+
if (!userStore) return {error: 'User store not found', hint: 'Not logged in?'};
|
|
24
|
+
|
|
25
|
+
let captured = null;
|
|
26
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
27
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
28
|
+
XMLHttpRequest.prototype.open = function(m, u) { this.__url = u; return origOpen.apply(this, arguments); };
|
|
29
|
+
XMLHttpRequest.prototype.send = function(b) {
|
|
30
|
+
if (this.__url?.includes('user_posted') && this.__url?.includes(args.user_id)) {
|
|
31
|
+
const x = this;
|
|
32
|
+
const orig = x.onreadystatechange;
|
|
33
|
+
x.onreadystatechange = function() { if (x.readyState === 4 && !captured) { try { captured = JSON.parse(x.responseText); } catch {} } if (orig) orig.apply(this, arguments); };
|
|
34
|
+
}
|
|
35
|
+
return origSend.apply(this, arguments);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await userStore.fetchNotes({userId: args.user_id});
|
|
40
|
+
await new Promise(r => setTimeout(r, 500));
|
|
41
|
+
} finally {
|
|
42
|
+
XMLHttpRequest.prototype.open = origOpen;
|
|
43
|
+
XMLHttpRequest.prototype.send = origSend;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!captured?.success) return {error: captured?.msg || 'User posts fetch failed', hint: 'Not logged in?'};
|
|
47
|
+
const notes = (captured.data?.notes || []).map(n => ({
|
|
48
|
+
note_id: n.note_id, title: n.display_title, type: n.type,
|
|
49
|
+
url: 'https://www.xiaohongshu.com/explore/' + n.note_id,
|
|
50
|
+
likes: n.interact_info?.liked_count, time: n.last_update_time
|
|
51
|
+
}));
|
|
52
|
+
return {user_id: args.user_id, count: notes.length, has_more: captured.data?.has_more, notes};
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "zhihu/hot",
|
|
4
|
+
"description": "Get Zhihu hot list (trending topics)",
|
|
5
|
+
"domain": "www.zhihu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"count": {"required": false, "description": "Number of items to return (default: 20, max: 50)"}
|
|
8
|
+
},
|
|
9
|
+
"capabilities": ["network"],
|
|
10
|
+
"readOnly": true,
|
|
11
|
+
"example": "bb-browser site zhihu/hot 10"
|
|
12
|
+
}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
async function(args) {
|
|
16
|
+
const count = Math.min(parseInt(args.count) || 20, 50);
|
|
17
|
+
const resp = await fetch('https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50', {credentials: 'include'});
|
|
18
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
19
|
+
const d = await resp.json();
|
|
20
|
+
const items = (d.data || []).slice(0, count).map((item, i) => {
|
|
21
|
+
const t = item.target || {};
|
|
22
|
+
return {
|
|
23
|
+
rank: i + 1,
|
|
24
|
+
id: t.id,
|
|
25
|
+
title: t.title,
|
|
26
|
+
url: 'https://www.zhihu.com/question/' + t.id,
|
|
27
|
+
excerpt: t.excerpt || '',
|
|
28
|
+
answer_count: t.answer_count,
|
|
29
|
+
follower_count: t.follower_count,
|
|
30
|
+
heat: item.detail_text || '',
|
|
31
|
+
trend: item.trend === 0 ? 'stable' : item.trend > 0 ? 'up' : 'down',
|
|
32
|
+
is_new: item.debut || false
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
return {count: items.length, items};
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "zhihu/me",
|
|
4
|
+
"description": "Get current logged-in Zhihu user info",
|
|
5
|
+
"domain": "www.zhihu.com",
|
|
6
|
+
"args": {},
|
|
7
|
+
"capabilities": ["network"],
|
|
8
|
+
"readOnly": true,
|
|
9
|
+
"example": "bb-browser site zhihu/me"
|
|
10
|
+
}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
async function(args) {
|
|
14
|
+
const resp = await fetch('https://www.zhihu.com/api/v4/me', {credentials: 'include'});
|
|
15
|
+
if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'Not logged in?'};
|
|
16
|
+
const u = await resp.json();
|
|
17
|
+
return {
|
|
18
|
+
id: u.id,
|
|
19
|
+
uid: u.uid,
|
|
20
|
+
name: u.name,
|
|
21
|
+
url: 'https://www.zhihu.com/people/' + u.url_token,
|
|
22
|
+
url_token: u.url_token,
|
|
23
|
+
headline: u.headline,
|
|
24
|
+
gender: u.gender === 1 ? 'male' : u.gender === 0 ? 'female' : 'unknown',
|
|
25
|
+
ip_info: u.ip_info,
|
|
26
|
+
avatar_url: u.avatar_url,
|
|
27
|
+
is_vip: u.vip_info?.is_vip || false,
|
|
28
|
+
answer_count: u.answer_count,
|
|
29
|
+
question_count: u.question_count,
|
|
30
|
+
articles_count: u.articles_count,
|
|
31
|
+
columns_count: u.columns_count,
|
|
32
|
+
favorite_count: u.favorite_count,
|
|
33
|
+
voteup_count: u.voteup_count,
|
|
34
|
+
thanked_count: u.thanked_count,
|
|
35
|
+
creation_count: u.creation_count,
|
|
36
|
+
notifications: {
|
|
37
|
+
default: u.default_notifications_count,
|
|
38
|
+
follow: u.follow_notifications_count,
|
|
39
|
+
vote_thank: u.vote_thank_notifications_count,
|
|
40
|
+
messages: u.messages_count
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* @meta
|
|
2
|
+
{
|
|
3
|
+
"name": "zhihu/question",
|
|
4
|
+
"description": "Get a Zhihu question and its top answers",
|
|
5
|
+
"domain": "www.zhihu.com",
|
|
6
|
+
"args": {
|
|
7
|
+
"id": {"required": true, "description": "Question ID (numeric)"},
|
|
8
|
+
"count": {"required": false, "description": "Number of answers to fetch (default: 5, max: 20)"}
|
|
9
|
+
},
|
|
10
|
+
"capabilities": ["network"],
|
|
11
|
+
"readOnly": true,
|
|
12
|
+
"example": "bb-browser site zhihu/question 34816524"
|
|
13
|
+
}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
async function(args) {
|
|
17
|
+
if (!args.id) return {error: 'Missing argument: id'};
|
|
18
|
+
const qid = args.id;
|
|
19
|
+
const count = Math.min(parseInt(args.count) || 5, 20);
|
|
20
|
+
|
|
21
|
+
// Fetch question detail and answers in parallel
|
|
22
|
+
const [qResp, aResp] = await Promise.all([
|
|
23
|
+
fetch('https://www.zhihu.com/api/v4/questions/' + qid + '?include=data[*].detail,excerpt,answer_count,follower_count,visit_count,comment_count,topics', {credentials: 'include'}),
|
|
24
|
+
fetch('https://www.zhihu.com/api/v4/questions/' + qid + '/answers?limit=' + count + '&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author', {credentials: 'include'})
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (!qResp.ok) return {error: 'HTTP ' + qResp.status + ' fetching question', hint: qResp.status === 404 ? 'Question not found' : 'Not logged in?'};
|
|
28
|
+
if (!aResp.ok) return {error: 'HTTP ' + aResp.status + ' fetching answers', hint: 'Not logged in?'};
|
|
29
|
+
|
|
30
|
+
const q = await qResp.json();
|
|
31
|
+
const aData = await aResp.json();
|
|
32
|
+
|
|
33
|
+
// Strip HTML tags helper
|
|
34
|
+
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
35
|
+
|
|
36
|
+
const answers = (aData.data || []).map((a, i) => ({
|
|
37
|
+
rank: i + 1,
|
|
38
|
+
id: a.id,
|
|
39
|
+
author: a.author?.name || 'anonymous',
|
|
40
|
+
author_headline: a.author?.headline || '',
|
|
41
|
+
voteup_count: a.voteup_count,
|
|
42
|
+
comment_count: a.comment_count,
|
|
43
|
+
content: strip(a.content).substring(0, 800),
|
|
44
|
+
created_time: a.created_time,
|
|
45
|
+
updated_time: a.updated_time
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
id: q.id,
|
|
50
|
+
title: q.title,
|
|
51
|
+
url: 'https://www.zhihu.com/question/' + qid,
|
|
52
|
+
detail: strip(q.detail) || '',
|
|
53
|
+
excerpt: q.excerpt || '',
|
|
54
|
+
answer_count: q.answer_count,
|
|
55
|
+
follower_count: q.follower_count,
|
|
56
|
+
visit_count: q.visit_count,
|
|
57
|
+
comment_count: q.comment_count,
|
|
58
|
+
topics: (q.topics || []).map(t => t.name),
|
|
59
|
+
answers_total: aData.paging?.totals || answers.length,
|
|
60
|
+
answers
|
|
61
|
+
};
|
|
62
|
+
}
|