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.
Files changed (46) hide show
  1. package/README.md +132 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +691 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +45 -0
  6. package/sites/36kr/newsflash.js +76 -0
  7. package/sites/baidu/search.js +62 -0
  8. package/sites/bilibili/comments.js +74 -0
  9. package/sites/bilibili/feed.js +79 -0
  10. package/sites/bilibili/history.js +43 -0
  11. package/sites/bilibili/me.js +45 -0
  12. package/sites/bilibili/popular.js +44 -0
  13. package/sites/bilibili/ranking.js +42 -0
  14. package/sites/bilibili/search.js +46 -0
  15. package/sites/bilibili/trending.js +32 -0
  16. package/sites/bilibili/video.js +73 -0
  17. package/sites/bing/search.js +40 -0
  18. package/sites/boss/detail.js +38 -0
  19. package/sites/boss/search.js +44 -0
  20. package/sites/cnblogs/search.js +68 -0
  21. package/sites/csdn/search.js +51 -0
  22. package/sites/douban/comments.js +58 -0
  23. package/sites/douban/movie-hot.js +64 -0
  24. package/sites/douban/movie-top.js +65 -0
  25. package/sites/douban/movie.js +117 -0
  26. package/sites/douban/search.js +90 -0
  27. package/sites/douban/top250.js +73 -0
  28. package/sites/github/fork.js +38 -0
  29. package/sites/github/issue-create.js +42 -0
  30. package/sites/github/issues.js +32 -0
  31. package/sites/github/me.js +22 -0
  32. package/sites/github/pr-create.js +55 -0
  33. package/sites/github/repo.js +27 -0
  34. package/sites/google/search.js +54 -0
  35. package/sites/toutiao/hot.js +107 -0
  36. package/sites/toutiao/search.js +146 -0
  37. package/sites/xiaohongshu/comments.js +56 -0
  38. package/sites/xiaohongshu/feed.js +50 -0
  39. package/sites/xiaohongshu/me.js +49 -0
  40. package/sites/xiaohongshu/note.js +63 -0
  41. package/sites/xiaohongshu/search.js +56 -0
  42. package/sites/xiaohongshu/user_posts.js +53 -0
  43. package/sites/zhihu/hot.js +36 -0
  44. package/sites/zhihu/me.js +43 -0
  45. package/sites/zhihu/question.js +62 -0
  46. 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(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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
+ }