@jackwener/opencli 1.0.5 → 1.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 (97) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +36 -10
  3. package/README.zh-CN.md +3 -0
  4. package/SKILL.md +7 -2
  5. package/dist/bilibili.js +4 -2
  6. package/dist/cli-manifest.json +506 -6
  7. package/dist/cli.js +51 -1
  8. package/dist/clis/antigravity/serve.js +296 -47
  9. package/dist/clis/arxiv/paper.d.ts +1 -0
  10. package/dist/clis/arxiv/paper.js +21 -0
  11. package/dist/clis/arxiv/search.d.ts +1 -0
  12. package/dist/clis/arxiv/search.js +24 -0
  13. package/dist/clis/arxiv/utils.d.ts +18 -0
  14. package/dist/clis/arxiv/utils.js +49 -0
  15. package/dist/clis/boss/batchgreet.d.ts +1 -0
  16. package/dist/clis/boss/batchgreet.js +147 -0
  17. package/dist/clis/boss/exchange.d.ts +1 -0
  18. package/dist/clis/boss/exchange.js +111 -0
  19. package/dist/clis/boss/greet.d.ts +1 -0
  20. package/dist/clis/boss/greet.js +175 -0
  21. package/dist/clis/boss/invite.d.ts +1 -0
  22. package/dist/clis/boss/invite.js +158 -0
  23. package/dist/clis/boss/joblist.d.ts +1 -0
  24. package/dist/clis/boss/joblist.js +55 -0
  25. package/dist/clis/boss/mark.d.ts +1 -0
  26. package/dist/clis/boss/mark.js +141 -0
  27. package/dist/clis/boss/recommend.d.ts +1 -0
  28. package/dist/clis/boss/recommend.js +83 -0
  29. package/dist/clis/boss/stats.d.ts +1 -0
  30. package/dist/clis/boss/stats.js +116 -0
  31. package/dist/clis/sinafinance/news.d.ts +7 -0
  32. package/dist/clis/sinafinance/news.js +61 -0
  33. package/dist/clis/wikipedia/search.d.ts +1 -0
  34. package/dist/clis/wikipedia/search.js +30 -0
  35. package/dist/clis/wikipedia/summary.d.ts +1 -0
  36. package/dist/clis/wikipedia/summary.js +28 -0
  37. package/dist/clis/wikipedia/utils.d.ts +8 -0
  38. package/dist/clis/wikipedia/utils.js +18 -0
  39. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +64 -5
  40. package/dist/clis/xiaohongshu/creator-note-detail.js +258 -69
  41. package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
  42. package/dist/clis/xiaohongshu/creator-note-detail.test.js +211 -0
  43. package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
  44. package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
  45. package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
  46. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
  47. package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
  48. package/dist/clis/xiaohongshu/creator-notes.js +159 -71
  49. package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
  50. package/dist/clis/xiaohongshu/creator-notes.test.js +162 -0
  51. package/dist/external.d.ts +20 -0
  52. package/dist/external.js +159 -0
  53. package/docs/.vitepress/config.mts +1 -0
  54. package/docs/public/CNAME +1 -0
  55. package/package.json +1 -1
  56. package/src/bilibili.ts +4 -2
  57. package/src/browser/cdp.ts +3 -3
  58. package/src/cli.ts +56 -1
  59. package/src/clis/antigravity/README.md +3 -46
  60. package/src/clis/antigravity/serve.ts +323 -50
  61. package/src/clis/arxiv/paper.ts +21 -0
  62. package/src/clis/arxiv/search.ts +24 -0
  63. package/src/clis/arxiv/utils.ts +63 -0
  64. package/src/clis/boss/batchgreet.ts +167 -0
  65. package/src/clis/boss/exchange.ts +126 -0
  66. package/src/clis/boss/greet.ts +198 -0
  67. package/src/clis/boss/invite.ts +177 -0
  68. package/src/clis/boss/joblist.ts +63 -0
  69. package/src/clis/boss/mark.ts +155 -0
  70. package/src/clis/boss/recommend.ts +94 -0
  71. package/src/clis/boss/stats.ts +130 -0
  72. package/src/clis/chaoxing/README.md +2 -24
  73. package/src/clis/chatgpt/README.md +3 -42
  74. package/src/clis/chatwise/README.md +3 -36
  75. package/src/clis/codex/README.md +3 -32
  76. package/src/clis/cursor/README.md +3 -31
  77. package/src/clis/discord-app/README.md +2 -25
  78. package/src/clis/feishu/README.md +2 -17
  79. package/src/clis/neteasemusic/README.md +3 -29
  80. package/src/clis/notion/README.md +2 -26
  81. package/src/clis/sinafinance/news.ts +76 -0
  82. package/src/clis/wechat/README.md +2 -25
  83. package/src/clis/wikipedia/search.ts +32 -0
  84. package/src/clis/wikipedia/summary.ts +28 -0
  85. package/src/clis/wikipedia/utils.ts +20 -0
  86. package/src/clis/xiaohongshu/creator-note-detail.test.ts +223 -0
  87. package/src/clis/xiaohongshu/creator-note-detail.ts +340 -72
  88. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
  89. package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
  90. package/src/clis/xiaohongshu/creator-notes.test.ts +178 -0
  91. package/src/clis/xiaohongshu/creator-notes.ts +215 -75
  92. package/src/daemon.ts +3 -3
  93. package/src/external-clis.yaml +39 -0
  94. package/src/external.ts +182 -0
  95. package/CDP.md +0 -103
  96. package/CDP.zh-CN.md +0 -103
  97. package/CLI-ELECTRON.md +0 -125
@@ -0,0 +1,141 @@
1
+ /**
2
+ * BOSS直聘 mark — label/mark a candidate.
3
+ *
4
+ * Uses /wapi/zprelation/friend/label/addMark to add a label to a candidate,
5
+ * and /wapi/zprelation/friend/label/deleteMark to remove one.
6
+ *
7
+ * Available labels (from /wapi/zprelation/friend/label/get):
8
+ * 1=新招呼, 2=沟通中, 3=已约面, 4=已获取简历, 5=已交换电话,
9
+ * 6=已交换微信, 7=不合适, 8=牛人发起, 11=收藏
10
+ */
11
+ import { cli, Strategy } from '../../registry.js';
12
+ const LABEL_MAP = {
13
+ '新招呼': 1, '沟通中': 2, '已约面': 3, '已获取简历': 4,
14
+ '已交换电话': 5, '已交换微信': 6, '不合适': 7, '牛人发起': 8, '收藏': 11,
15
+ };
16
+ cli({
17
+ site: 'boss',
18
+ name: 'mark',
19
+ description: 'BOSS直聘给候选人添加标签',
20
+ domain: 'www.zhipin.com',
21
+ strategy: Strategy.COOKIE,
22
+ browser: true,
23
+ args: [
24
+ { name: 'uid', required: true, help: 'Encrypted UID of the candidate' },
25
+ { name: 'label', required: true, help: 'Label name (新招呼/沟通中/已约面/已获取简历/已交换电话/已交换微信/不合适/收藏) or label ID' },
26
+ { name: 'remove', type: 'boolean', default: false, help: 'Remove the label instead of adding' },
27
+ ],
28
+ columns: ['status', 'detail'],
29
+ func: async (page, kwargs) => {
30
+ if (!page)
31
+ throw new Error('Browser page required');
32
+ const uid = kwargs.uid;
33
+ const labelInput = kwargs.label;
34
+ const remove = kwargs.remove || false;
35
+ // Resolve label to ID
36
+ let labelId;
37
+ if (LABEL_MAP[labelInput]) {
38
+ labelId = LABEL_MAP[labelInput];
39
+ }
40
+ else if (!isNaN(Number(labelInput))) {
41
+ labelId = Number(labelInput);
42
+ }
43
+ else {
44
+ // Try partial match
45
+ const entry = Object.entries(LABEL_MAP).find(([k]) => k.includes(labelInput));
46
+ if (entry) {
47
+ labelId = entry[1];
48
+ }
49
+ else {
50
+ throw new Error(`未知标签: ${labelInput}。可用标签: ${Object.keys(LABEL_MAP).join(', ')}`);
51
+ }
52
+ }
53
+ if (process.env.OPENCLI_VERBOSE) {
54
+ console.error(`[opencli:boss] ${remove ? 'Removing' : 'Adding'} label ${labelId} for ${uid}...`);
55
+ }
56
+ await page.goto('https://www.zhipin.com/web/chat/index');
57
+ await page.wait({ time: 2 });
58
+ // First get numeric UID from friend list
59
+ const friendData = await page.evaluate(`
60
+ async () => {
61
+ return new Promise((resolve, reject) => {
62
+ const xhr = new XMLHttpRequest();
63
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
64
+ xhr.withCredentials = true;
65
+ xhr.timeout = 15000;
66
+ xhr.setRequestHeader('Accept', 'application/json');
67
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
68
+ xhr.onerror = () => reject(new Error('Network Error'));
69
+ xhr.send();
70
+ });
71
+ }
72
+ `);
73
+ if (friendData.code !== 0) {
74
+ if (friendData.code === 7 || friendData.code === 37) {
75
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
76
+ }
77
+ throw new Error(`获取好友列表失败: ${friendData.message}`);
78
+ }
79
+ // Find in friend list (check multiple pages)
80
+ let friend = null;
81
+ let allFriends = friendData.zpData?.friendList || [];
82
+ friend = allFriends.find((f) => f.encryptUid === uid);
83
+ if (!friend) {
84
+ // Also check greetRecSortList
85
+ const greetData = await page.evaluate(`
86
+ async () => {
87
+ return new Promise((resolve, reject) => {
88
+ const xhr = new XMLHttpRequest();
89
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList', true);
90
+ xhr.withCredentials = true;
91
+ xhr.timeout = 15000;
92
+ xhr.setRequestHeader('Accept', 'application/json');
93
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
94
+ xhr.onerror = () => reject(new Error('Network Error'));
95
+ xhr.send();
96
+ });
97
+ }
98
+ `);
99
+ if (greetData.code === 0) {
100
+ friend = (greetData.zpData?.friendList || []).find((f) => f.encryptUid === uid);
101
+ }
102
+ }
103
+ if (!friend) {
104
+ throw new Error('未找到该候选人');
105
+ }
106
+ const numericUid = friend.uid;
107
+ const friendName = friend.name || '候选人';
108
+ const friendSource = friend.friendSource ?? 0;
109
+ const action = remove ? 'deleteMark' : 'addMark';
110
+ const targetUrl = `https://www.zhipin.com/wapi/zprelation/friend/label/${action}`;
111
+ // The API uses friendId + friendSource + labelId (discovered from JS bundles)
112
+ const params = new URLSearchParams({
113
+ friendId: String(numericUid),
114
+ friendSource: String(friendSource),
115
+ labelId: String(labelId),
116
+ });
117
+ // Try GET first (the N() wrapper in boss JS uses GET with query params)
118
+ const data = await page.evaluate(`
119
+ async () => {
120
+ return new Promise((resolve, reject) => {
121
+ const xhr = new XMLHttpRequest();
122
+ xhr.open('GET', '${targetUrl}?${params.toString()}', true);
123
+ xhr.withCredentials = true;
124
+ xhr.timeout = 15000;
125
+ xhr.setRequestHeader('Accept', 'application/json');
126
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
127
+ xhr.onerror = () => reject(new Error('Network Error'));
128
+ xhr.send();
129
+ });
130
+ }
131
+ `);
132
+ if (data.code !== 0) {
133
+ throw new Error(`标签操作失败: ${data.message} (code=${data.code})`);
134
+ }
135
+ const labelName = Object.entries(LABEL_MAP).find(([, v]) => v === labelId)?.[0] || String(labelId);
136
+ return [{
137
+ status: remove ? '✅ 标签已移除' : '✅ 标签已添加',
138
+ detail: `${friendName}: ${remove ? '移除' : '添加'}标签「${labelName}」`,
139
+ }];
140
+ },
141
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * BOSS直聘 recommend — view recommended candidates (新招呼/greet sort list).
3
+ *
4
+ * Uses /wapi/zprelation/friend/greetRecSortList to get system-recommended candidates.
5
+ * These are candidates who have greeted or been recommended by the system.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ cli({
9
+ site: 'boss',
10
+ name: 'recommend',
11
+ description: 'BOSS直聘查看推荐候选人(新招呼列表)',
12
+ domain: 'www.zhipin.com',
13
+ strategy: Strategy.COOKIE,
14
+ browser: true,
15
+ args: [
16
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
17
+ ],
18
+ columns: ['name', 'job_name', 'last_time', 'labels', 'encrypt_uid', 'security_id', 'encrypt_job_id'],
19
+ func: async (page, kwargs) => {
20
+ if (!page)
21
+ throw new Error('Browser page required');
22
+ const limit = kwargs.limit || 20;
23
+ if (process.env.OPENCLI_VERBOSE) {
24
+ console.error('[opencli:boss] Fetching recommended candidates...');
25
+ }
26
+ await page.goto('https://www.zhipin.com/web/chat/index');
27
+ await page.wait({ time: 2 });
28
+ // Get label definitions for mapping
29
+ const labelData = await page.evaluate(`
30
+ async () => {
31
+ return new Promise((resolve, reject) => {
32
+ const xhr = new XMLHttpRequest();
33
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/label/get', true);
34
+ xhr.withCredentials = true;
35
+ xhr.timeout = 10000;
36
+ xhr.setRequestHeader('Accept', 'application/json');
37
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
38
+ xhr.onerror = () => resolve({});
39
+ xhr.send();
40
+ });
41
+ }
42
+ `);
43
+ const labelMap = {};
44
+ if (labelData.code === 0 && labelData.zpData?.labels) {
45
+ for (const l of labelData.zpData.labels) {
46
+ labelMap[l.labelId] = l.label;
47
+ }
48
+ }
49
+ // Get recommended candidates
50
+ const targetUrl = 'https://www.zhipin.com/wapi/zprelation/friend/greetRecSortList';
51
+ const data = await page.evaluate(`
52
+ async () => {
53
+ return new Promise((resolve, reject) => {
54
+ const xhr = new XMLHttpRequest();
55
+ xhr.open('GET', '${targetUrl}', true);
56
+ xhr.withCredentials = true;
57
+ xhr.timeout = 15000;
58
+ xhr.setRequestHeader('Accept', 'application/json');
59
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
60
+ xhr.onerror = () => reject(new Error('Network Error'));
61
+ xhr.ontimeout = () => reject(new Error('Timeout'));
62
+ xhr.send();
63
+ });
64
+ }
65
+ `);
66
+ if (data.code !== 0) {
67
+ if (data.code === 7 || data.code === 37) {
68
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
69
+ }
70
+ throw new Error(`API error: ${data.message} (code=${data.code})`);
71
+ }
72
+ const friends = (data.zpData?.friendList || []).slice(0, limit);
73
+ return friends.map((f) => ({
74
+ name: f.name || '',
75
+ job_name: f.jobName || '',
76
+ last_time: f.lastTime || '',
77
+ labels: (f.relationLabelList || []).map((id) => labelMap[id] || String(id)).join(', '),
78
+ encrypt_uid: f.encryptUid || '',
79
+ security_id: f.securityId || '',
80
+ encrypt_job_id: f.encryptJobId || '',
81
+ }));
82
+ },
83
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ /**
2
+ * BOSS直聘 stats — job statistics overview.
3
+ *
4
+ * Uses /wapi/zpchat/chatHelper/statistics for total friend count,
5
+ * and /wapi/zpjob/job/chatted/jobList for per-job info.
6
+ * Since BOSS doesn't expose detailed per-job stats via API,
7
+ * we show what's available: job status, chat info, and total stats.
8
+ */
9
+ import { cli, Strategy } from '../../registry.js';
10
+ cli({
11
+ site: 'boss',
12
+ name: 'stats',
13
+ description: 'BOSS直聘职位数据统计',
14
+ domain: 'www.zhipin.com',
15
+ strategy: Strategy.COOKIE,
16
+ browser: true,
17
+ args: [
18
+ { name: 'job_id', default: '', help: 'Encrypted job ID (show all if empty)' },
19
+ ],
20
+ columns: ['job_name', 'salary', 'city', 'status', 'total_chats', 'encrypt_job_id'],
21
+ func: async (page, kwargs) => {
22
+ if (!page)
23
+ throw new Error('Browser page required');
24
+ const filterJobId = kwargs.job_id || '';
25
+ if (process.env.OPENCLI_VERBOSE) {
26
+ console.error('[opencli:boss] Fetching job statistics...');
27
+ }
28
+ await page.goto('https://www.zhipin.com/web/chat/index');
29
+ await page.wait({ time: 2 });
30
+ // Get job list
31
+ const jobData = await page.evaluate(`
32
+ async () => {
33
+ return new Promise((resolve, reject) => {
34
+ const xhr = new XMLHttpRequest();
35
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zpjob/job/chatted/jobList', true);
36
+ xhr.withCredentials = true;
37
+ xhr.timeout = 15000;
38
+ xhr.setRequestHeader('Accept', 'application/json');
39
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(new Error('JSON parse failed')); } };
40
+ xhr.onerror = () => reject(new Error('Network Error'));
41
+ xhr.ontimeout = () => reject(new Error('Timeout'));
42
+ xhr.send();
43
+ });
44
+ }
45
+ `);
46
+ if (jobData.code !== 0) {
47
+ if (jobData.code === 7 || jobData.code === 37) {
48
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
49
+ }
50
+ throw new Error(`API error: ${jobData.message} (code=${jobData.code})`);
51
+ }
52
+ // Get total chat stats
53
+ const chatStats = await page.evaluate(`
54
+ async () => {
55
+ return new Promise((resolve, reject) => {
56
+ const xhr = new XMLHttpRequest();
57
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zpchat/chatHelper/statistics', true);
58
+ xhr.withCredentials = true;
59
+ xhr.timeout = 10000;
60
+ xhr.setRequestHeader('Accept', 'application/json');
61
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
62
+ xhr.onerror = () => resolve({});
63
+ xhr.send();
64
+ });
65
+ }
66
+ `);
67
+ const totalFriends = chatStats.zpData?.totalFriendCount || 0;
68
+ // Get per-job chat counts from friend list
69
+ const friendData = await page.evaluate(`
70
+ async () => {
71
+ return new Promise((resolve, reject) => {
72
+ const xhr = new XMLHttpRequest();
73
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
74
+ xhr.withCredentials = true;
75
+ xhr.timeout = 15000;
76
+ xhr.setRequestHeader('Accept', 'application/json');
77
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({}); } };
78
+ xhr.onerror = () => resolve({});
79
+ xhr.send();
80
+ });
81
+ }
82
+ `);
83
+ // Count chats per job
84
+ const jobChatCounts = {};
85
+ if (friendData.code === 0) {
86
+ for (const f of (friendData.zpData?.friendList || [])) {
87
+ const jobName = f.jobName || 'unknown';
88
+ jobChatCounts[jobName] = (jobChatCounts[jobName] || 0) + 1;
89
+ }
90
+ }
91
+ let jobs = jobData.zpData || [];
92
+ if (filterJobId) {
93
+ jobs = jobs.filter((j) => j.encryptJobId === filterJobId);
94
+ }
95
+ const results = jobs.map((j) => ({
96
+ job_name: j.jobName || '',
97
+ salary: j.salaryDesc || '',
98
+ city: j.address || '',
99
+ status: j.jobOnlineStatus === 1 ? '在线' : '已关闭',
100
+ total_chats: String(jobChatCounts[j.jobName] || 0),
101
+ encrypt_job_id: j.encryptJobId || '',
102
+ }));
103
+ // Add summary row
104
+ if (!filterJobId && results.length > 0) {
105
+ results.push({
106
+ job_name: '--- 总计 ---',
107
+ salary: '',
108
+ city: '',
109
+ status: `${jobs.length} 个职位`,
110
+ total_chats: String(totalFriends),
111
+ encrypt_job_id: '',
112
+ });
113
+ }
114
+ return results;
115
+ },
116
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sina Finance 7x24 live news feed.
3
+ *
4
+ * Uses the public CJ API — no key or browser required.
5
+ * https://app.cj.sina.com.cn/api/news/pc
6
+ */
7
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Sina Finance 7x24 live news feed.
3
+ *
4
+ * Uses the public CJ API — no key or browser required.
5
+ * https://app.cj.sina.com.cn/api/news/pc
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import { CliError } from '../../errors.js';
9
+ // User-facing type (0-9) → Sina API tag ID
10
+ const TYPE_MAP = [
11
+ 0, // 0: 全部
12
+ 10, // 1: A股
13
+ 1, // 2: 宏观
14
+ 3, // 3: 公司
15
+ 4, // 4: 数据
16
+ 5, // 5: 市场
17
+ 102, // 6: 国际
18
+ 6, // 7: 观点
19
+ 6, // 8: 央行
20
+ 8, // 9: 其它
21
+ ];
22
+ function stripHtml(html) {
23
+ return html.replace(/<[^>]+>/g, '').trim();
24
+ }
25
+ cli({
26
+ site: 'sinafinance',
27
+ name: 'news',
28
+ description: '新浪财经 7x24 小时实时快讯',
29
+ domain: 'app.cj.sina.com.cn',
30
+ strategy: Strategy.PUBLIC,
31
+ browser: false,
32
+ args: [
33
+ { name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
34
+ { name: 'type', type: 'int', default: 0, help: 'News type: 0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它' },
35
+ ],
36
+ columns: ['id', 'time', 'content', 'views'],
37
+ func: async (_page, args) => {
38
+ const limit = Math.max(1, Math.min(Number(args.limit), 50));
39
+ const apiTag = TYPE_MAP[args.type] ?? 0;
40
+ const params = new URLSearchParams({
41
+ page: '1',
42
+ size: String(limit),
43
+ tag: String(apiTag),
44
+ });
45
+ const res = await fetch(`https://app.cj.sina.com.cn/api/news/pc?${params}`);
46
+ if (!res.ok) {
47
+ throw new CliError('FETCH_ERROR', `Sina Finance API HTTP ${res.status}`, 'Check your network connection');
48
+ }
49
+ const json = await res.json();
50
+ const list = json?.result?.data?.feed?.list ?? [];
51
+ if (!list.length) {
52
+ throw new CliError('NOT_FOUND', 'No news found', 'Try a different type or increase limit');
53
+ }
54
+ return list.map((item) => ({
55
+ id: item.id ?? '',
56
+ time: item.create_time ?? '',
57
+ content: stripHtml(item.rich_text ?? ''),
58
+ views: item.view_num ?? 0,
59
+ }));
60
+ },
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { wikiFetch } from './utils.js';
4
+ cli({
5
+ site: 'wikipedia',
6
+ name: 'search',
7
+ description: 'Search Wikipedia articles',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
12
+ { name: 'limit', type: 'int', default: 10, help: 'Max results' },
13
+ { name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
14
+ ],
15
+ columns: ['title', 'snippet', 'url'],
16
+ func: async (_page, args) => {
17
+ const limit = Math.max(1, Math.min(Number(args.limit), 50));
18
+ const lang = args.lang || 'en';
19
+ const q = encodeURIComponent(args.keyword);
20
+ const data = await wikiFetch(lang, `/w/api.php?action=query&list=search&srsearch=${q}&srlimit=${limit}&format=json&utf8=1`);
21
+ const results = data?.query?.search;
22
+ if (!results?.length)
23
+ throw new CliError('NOT_FOUND', 'No articles found', 'Try a different keyword');
24
+ return results.map((r) => ({
25
+ title: r.title,
26
+ snippet: r.snippet.replace(/<[^>]+>/g, '').slice(0, 120),
27
+ url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`,
28
+ }));
29
+ },
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { wikiFetch } from './utils.js';
4
+ cli({
5
+ site: 'wikipedia',
6
+ name: 'summary',
7
+ description: 'Get Wikipedia article summary',
8
+ strategy: Strategy.PUBLIC,
9
+ browser: false,
10
+ args: [
11
+ { name: 'title', positional: true, required: true, help: 'Article title (e.g. "Transformer (machine learning model)")' },
12
+ { name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
13
+ ],
14
+ columns: ['title', 'description', 'extract', 'url'],
15
+ func: async (_page, args) => {
16
+ const lang = args.lang || 'en';
17
+ const title = encodeURIComponent(args.title.replace(/ /g, '_'));
18
+ const data = await wikiFetch(lang, `/api/rest_v1/page/summary/${title}`);
19
+ if (!data?.title)
20
+ throw new CliError('NOT_FOUND', `Article "${args.title}" not found`, 'Try searching first: opencli wikipedia search <keyword>');
21
+ return [{
22
+ title: data.title,
23
+ description: data.description ?? '-',
24
+ extract: (data.extract ?? '').slice(0, 300),
25
+ url: data.content_urls?.desktop?.page ?? `https://${lang}.wikipedia.org/wiki/${title}`,
26
+ }];
27
+ },
28
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wikipedia adapter utilities.
3
+ *
4
+ * Uses the public MediaWiki REST API and Action API — no key required.
5
+ * REST API: https://en.wikipedia.org/api/rest_v1/
6
+ * Action API: https://en.wikipedia.org/w/api.php
7
+ */
8
+ export declare function wikiFetch(lang: string, path: string): Promise<unknown>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Wikipedia adapter utilities.
3
+ *
4
+ * Uses the public MediaWiki REST API and Action API — no key required.
5
+ * REST API: https://en.wikipedia.org/api/rest_v1/
6
+ * Action API: https://en.wikipedia.org/w/api.php
7
+ */
8
+ import { CliError } from '../../errors.js';
9
+ export async function wikiFetch(lang, path) {
10
+ const url = `https://${lang}.wikipedia.org${path}`;
11
+ const resp = await fetch(url, {
12
+ headers: { 'User-Agent': 'opencli/1.0 (https://github.com/jackwener/opencli)' },
13
+ });
14
+ if (!resp.ok) {
15
+ throw new CliError('FETCH_ERROR', `Wikipedia API HTTP ${resp.status}`, `Check your title or search term`);
16
+ }
17
+ return resp.json();
18
+ }
@@ -1,10 +1,69 @@
1
1
  /**
2
- * Xiaohongshu Creator Note Detail — per-note analytics breakdown.
2
+ * Xiaohongshu Creator Note Detail — per-note analytics from the creator detail page.
3
3
  *
4
- * Uses the creator.xiaohongshu.com internal API (cookie auth).
5
- * Returns total reads, engagement, likes, collects, comments, shares
6
- * for a specific note, split by channel (organic vs promoted vs video).
4
+ * The current creator center no longer serves stable single-note metrics from the legacy
5
+ * `/api/galaxy/creator/data/note_detail` endpoint. The real note detail page loads data
6
+ * through the newer `datacenter/note/*` API family, so this command navigates to the
7
+ * detail page and parses the rendered metrics that are backed by those APIs.
7
8
  *
8
9
  * Requires: logged into creator.xiaohongshu.com in Chrome.
9
10
  */
10
- export {};
11
+ import type { IPage } from '../../types.js';
12
+ type CreatorNoteDetailRow = {
13
+ section: string;
14
+ metric: string;
15
+ value: string;
16
+ extra: string;
17
+ };
18
+ export type { CreatorNoteDetailRow };
19
+ type AudienceSourceItem = {
20
+ title?: string;
21
+ value_with_double?: number;
22
+ info?: {
23
+ imp_count?: number;
24
+ view_count?: number;
25
+ interaction_count?: number;
26
+ };
27
+ };
28
+ type AudiencePortraitItem = {
29
+ title?: string;
30
+ value?: number;
31
+ };
32
+ type NoteTrendPoint = {
33
+ date?: number;
34
+ count?: number;
35
+ count_with_double?: number;
36
+ };
37
+ type NoteTrendBucket = {
38
+ imp_list?: NoteTrendPoint[];
39
+ view_list?: NoteTrendPoint[];
40
+ view_time_list?: NoteTrendPoint[];
41
+ like_list?: NoteTrendPoint[];
42
+ comment_list?: NoteTrendPoint[];
43
+ collect_list?: NoteTrendPoint[];
44
+ share_list?: NoteTrendPoint[];
45
+ rise_fans_list?: NoteTrendPoint[];
46
+ };
47
+ type NoteDetailApiPayload = {
48
+ noteBase?: {
49
+ hour?: NoteTrendBucket;
50
+ day?: NoteTrendBucket;
51
+ };
52
+ audienceTrend?: {
53
+ no_data?: boolean;
54
+ no_data_tip_msg?: string;
55
+ };
56
+ audienceSource?: {
57
+ source?: AudienceSourceItem[];
58
+ };
59
+ audienceSourceDetail?: {
60
+ gender?: AudiencePortraitItem[];
61
+ age?: AudiencePortraitItem[];
62
+ city?: AudiencePortraitItem[];
63
+ interest?: AudiencePortraitItem[];
64
+ };
65
+ };
66
+ export declare function parseCreatorNoteDetailText(bodyText: string, noteId: string): CreatorNoteDetailRow[];
67
+ export declare function appendAudienceRows(rows: CreatorNoteDetailRow[], payload?: NoteDetailApiPayload): CreatorNoteDetailRow[];
68
+ export declare function appendTrendRows(rows: CreatorNoteDetailRow[], payload?: NoteDetailApiPayload): CreatorNoteDetailRow[];
69
+ export declare function fetchCreatorNoteDetailRows(page: IPage, noteId: string): Promise<CreatorNoteDetailRow[]>;