@jackwener/opencli 1.0.3 → 1.0.4
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/.github/workflows/build-extension.yml +21 -3
- package/.github/workflows/docs.yml +52 -0
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/browser/cdp.d.ts +16 -1
- package/dist/browser/cdp.js +124 -80
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +37 -100
- package/dist/browser.test.js +7 -0
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.test.js +11 -4
- package/dist/cli-manifest.json +635 -1
- package/dist/cli.js +45 -8
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/xiaohongshu/download.js +3 -3
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +5 -5
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +3 -3
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +92 -52
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +122 -51
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +154 -82
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +45 -100
- package/src/browser.test.ts +10 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +12 -4
- package/src/cli.ts +28 -8
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/xiaohongshu/download.ts +3 -3
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +4 -4
- package/src/explore.ts +4 -4
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/output.ts +3 -1
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +6 -2
- package/src/runtime.ts +3 -2
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
|
|
4
|
+
interface PaperAuthor {
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface DailyPaper {
|
|
9
|
+
paper: {
|
|
10
|
+
id: string;
|
|
11
|
+
upvotes: number;
|
|
12
|
+
authors: PaperAuthor[];
|
|
13
|
+
};
|
|
14
|
+
title: string;
|
|
15
|
+
numComments: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PeriodPaper {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
upvotes: number;
|
|
22
|
+
publishedAt: string;
|
|
23
|
+
authors: PaperAuthor[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function truncate(str: string, max = 60): string {
|
|
27
|
+
return str.length > max ? str.slice(0, max - 3) + '...' : str;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatAuthors(authors: PaperAuthor[], max = 3): string {
|
|
31
|
+
const names = authors.map((a) => a.name);
|
|
32
|
+
if (names.length <= max) return names.join(', ');
|
|
33
|
+
return names.slice(0, max).join(', ') + ' et al.';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
37
|
+
|
|
38
|
+
function getMonthRange(): string {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getWeekRange(): string {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const day = now.getUTCDay(); // 0=Sun, 6=Sat
|
|
46
|
+
const daysToSat = day === 6 ? 0 : 6 - day;
|
|
47
|
+
const end = new Date(now);
|
|
48
|
+
end.setUTCDate(now.getUTCDate() + daysToSat);
|
|
49
|
+
const start = new Date(end);
|
|
50
|
+
start.setUTCDate(end.getUTCDate() - 6);
|
|
51
|
+
|
|
52
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
53
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
54
|
+
const sd = start.getUTCDate();
|
|
55
|
+
const ed = end.getUTCDate();
|
|
56
|
+
return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cli({
|
|
60
|
+
site: 'hf',
|
|
61
|
+
name: 'top',
|
|
62
|
+
description: 'Top upvoted Hugging Face papers',
|
|
63
|
+
domain: 'huggingface.co',
|
|
64
|
+
strategy: Strategy.PUBLIC,
|
|
65
|
+
browser: false,
|
|
66
|
+
args: [
|
|
67
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of papers' },
|
|
68
|
+
{ name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' },
|
|
69
|
+
{ name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' },
|
|
70
|
+
{ name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' },
|
|
71
|
+
],
|
|
72
|
+
footerExtra: (kwargs) => {
|
|
73
|
+
if (kwargs._footerDate) return kwargs._footerDate;
|
|
74
|
+
if (kwargs.period === 'monthly') return getMonthRange();
|
|
75
|
+
if (kwargs.period === 'weekly') return getWeekRange();
|
|
76
|
+
return kwargs.date ?? new Date().toISOString().slice(0, 10);
|
|
77
|
+
},
|
|
78
|
+
func: async (_page, kwargs) => {
|
|
79
|
+
const period = String(kwargs.period ?? 'daily');
|
|
80
|
+
const all = Boolean(kwargs.all);
|
|
81
|
+
const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';
|
|
82
|
+
|
|
83
|
+
if (period === 'weekly' || period === 'monthly') {
|
|
84
|
+
if (kwargs.date) {
|
|
85
|
+
throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`);
|
|
86
|
+
}
|
|
87
|
+
const url = `${endpoint}/api/papers?period=${period}`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint');
|
|
92
|
+
const data: PeriodPaper[] = body;
|
|
93
|
+
const dates = data.map((d) => d.publishedAt).filter(Boolean).sort();
|
|
94
|
+
if (dates.length > 0) {
|
|
95
|
+
if (period === 'monthly') {
|
|
96
|
+
const d = new Date(dates[0]);
|
|
97
|
+
kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
98
|
+
} else {
|
|
99
|
+
const start = new Date(dates[0]);
|
|
100
|
+
const end = new Date(dates[dates.length - 1]);
|
|
101
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
102
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
103
|
+
const sd = start.getUTCDate();
|
|
104
|
+
const ed = end.getUTCDate();
|
|
105
|
+
kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0));
|
|
109
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
110
|
+
return items.map((item, i) => ({
|
|
111
|
+
rank: i + 1,
|
|
112
|
+
id: item.id ?? '',
|
|
113
|
+
title: truncate(item.title ?? ''),
|
|
114
|
+
upvotes: item.upvotes ?? 0,
|
|
115
|
+
authors: formatAuthors(item.authors ?? []),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// daily
|
|
120
|
+
if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) {
|
|
121
|
+
throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD');
|
|
122
|
+
}
|
|
123
|
+
const url = kwargs.date
|
|
124
|
+
? `${endpoint}/api/daily_papers?date=${kwargs.date}`
|
|
125
|
+
: `${endpoint}/api/daily_papers`;
|
|
126
|
+
const res = await fetch(url);
|
|
127
|
+
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
128
|
+
const body = await res.json();
|
|
129
|
+
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint');
|
|
130
|
+
const data: DailyPaper[] = body;
|
|
131
|
+
const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0));
|
|
132
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
133
|
+
return items.map((item, i) => ({
|
|
134
|
+
rank: i + 1,
|
|
135
|
+
id: item.paper?.id ?? '',
|
|
136
|
+
title: truncate(item.title ?? ''),
|
|
137
|
+
upvotes: item.paper?.upvotes ?? 0,
|
|
138
|
+
authors: formatAuthors(item.paper?.authors ?? []),
|
|
139
|
+
}));
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 评论即刻帖子
|
|
5
|
+
*
|
|
6
|
+
* 帖子详情页有评论输入框(contenteditable 或 textarea),
|
|
7
|
+
* 填入文本后点击"回复"或"发布"按钮提交。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'jike',
|
|
12
|
+
name: 'comment',
|
|
13
|
+
description: '评论即刻帖子',
|
|
14
|
+
domain: 'web.okjike.com',
|
|
15
|
+
strategy: Strategy.UI,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
19
|
+
{ name: 'text', type: 'string', required: true, help: '评论内容' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['status', 'message'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
|
|
26
|
+
// 1. 找到评论输入框并填入文本
|
|
27
|
+
const inputResult = await page.evaluate(`(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
30
|
+
|
|
31
|
+
// 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器;
|
|
32
|
+
// 若评论区 class 名变更则回退到全页查找
|
|
33
|
+
const editor =
|
|
34
|
+
document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
|
|
35
|
+
document.querySelector('[contenteditable="true"]');
|
|
36
|
+
if (editor) {
|
|
37
|
+
editor.focus();
|
|
38
|
+
const dt = new DataTransfer();
|
|
39
|
+
dt.setData('text/plain', textToInsert);
|
|
40
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
41
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
42
|
+
}));
|
|
43
|
+
await new Promise(r => setTimeout(r, 800));
|
|
44
|
+
if (editor.textContent?.length > 0) {
|
|
45
|
+
return { ok: true, message: 'contenteditable' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 回退:textarea(带评论相关 placeholder)
|
|
50
|
+
const textareas = document.querySelectorAll('textarea');
|
|
51
|
+
for (const ta of textareas) {
|
|
52
|
+
const ph = ta.getAttribute('placeholder') || '';
|
|
53
|
+
if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) {
|
|
54
|
+
ta.focus();
|
|
55
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
56
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
57
|
+
)?.set;
|
|
58
|
+
setter?.call(ta, textToInsert);
|
|
59
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
60
|
+
await new Promise(r => setTimeout(r, 500));
|
|
61
|
+
return { ok: true, message: 'textarea' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 兜底:任意 textarea
|
|
66
|
+
if (textareas.length > 0) {
|
|
67
|
+
const ta = textareas[0];
|
|
68
|
+
ta.focus();
|
|
69
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
70
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
71
|
+
)?.set;
|
|
72
|
+
setter?.call(ta, textToInsert);
|
|
73
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
74
|
+
await new Promise(r => setTimeout(r, 500));
|
|
75
|
+
return { ok: true, message: 'textarea-fallback' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: false, message: '未找到评论输入框' };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { ok: false, message: e.toString() };
|
|
81
|
+
}
|
|
82
|
+
})()`);
|
|
83
|
+
|
|
84
|
+
if (!inputResult.ok) {
|
|
85
|
+
return [{ status: 'failed', message: inputResult.message }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. 点击"回复"或"发布"按钮
|
|
89
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
90
|
+
try {
|
|
91
|
+
await new Promise(r => setTimeout(r, 500));
|
|
92
|
+
const btns = Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
93
|
+
const text = btn.textContent?.trim() || '';
|
|
94
|
+
return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
|
|
95
|
+
});
|
|
96
|
+
if (btns.length === 0) {
|
|
97
|
+
return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' };
|
|
98
|
+
}
|
|
99
|
+
btns[0].click();
|
|
100
|
+
return { ok: true, message: '评论发布成功' };
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return { ok: false, message: e.toString() };
|
|
103
|
+
}
|
|
104
|
+
})()`);
|
|
105
|
+
|
|
106
|
+
if (submitResult.ok) await page.wait(3);
|
|
107
|
+
|
|
108
|
+
return [{
|
|
109
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
110
|
+
message: submitResult.message,
|
|
111
|
+
}];
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 发布即刻动态
|
|
5
|
+
*
|
|
6
|
+
* 即刻首页 /following 顶部有内联发帖框("分享你的想法..."),
|
|
7
|
+
* 直接在其中输入文本,点击"发送"按钮即可发布。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'jike',
|
|
12
|
+
name: 'create',
|
|
13
|
+
description: '发布即刻动态',
|
|
14
|
+
domain: 'web.okjike.com',
|
|
15
|
+
strategy: Strategy.UI,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'text', type: 'string', required: true, help: '动态正文内容' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'message'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
// 1. 导航到首页(有内联发帖框)
|
|
23
|
+
await page.goto('https://web.okjike.com');
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
|
|
26
|
+
// 2. 在发帖框中输入文本
|
|
27
|
+
const textResult = await page.evaluate(`(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
30
|
+
|
|
31
|
+
// 首页发帖框在 _postForm_ 容器内,查找其中的 contenteditable
|
|
32
|
+
const form = document.querySelector('[class*="_postForm_"]');
|
|
33
|
+
const editor = form
|
|
34
|
+
? form.querySelector('[contenteditable="true"]')
|
|
35
|
+
: document.querySelector('[contenteditable="true"]');
|
|
36
|
+
|
|
37
|
+
if (editor) {
|
|
38
|
+
editor.focus();
|
|
39
|
+
// 用 ClipboardEvent paste 触发 React 状态更新
|
|
40
|
+
const dt = new DataTransfer();
|
|
41
|
+
dt.setData('text/plain', textToInsert);
|
|
42
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
43
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
44
|
+
}));
|
|
45
|
+
await new Promise(r => setTimeout(r, 800));
|
|
46
|
+
|
|
47
|
+
// 检查是否成功插入
|
|
48
|
+
const inserted = editor.textContent || '';
|
|
49
|
+
if (inserted.length > 0) {
|
|
50
|
+
return { ok: true, message: 'contenteditable' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 回退:textarea
|
|
55
|
+
const textarea = form
|
|
56
|
+
? form.querySelector('textarea')
|
|
57
|
+
: document.querySelector('textarea');
|
|
58
|
+
|
|
59
|
+
if (textarea) {
|
|
60
|
+
textarea.focus();
|
|
61
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
62
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
63
|
+
)?.set;
|
|
64
|
+
setter?.call(textarea, textToInsert);
|
|
65
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
66
|
+
await new Promise(r => setTimeout(r, 500));
|
|
67
|
+
return { ok: true, message: 'textarea' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { ok: false, message: '未找到发帖输入框' };
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { ok: false, message: e.toString() };
|
|
73
|
+
}
|
|
74
|
+
})()`);
|
|
75
|
+
|
|
76
|
+
if (!textResult.ok) {
|
|
77
|
+
return [{ status: 'failed', message: textResult.message }];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. 点击"发送"按钮
|
|
81
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await new Promise(r => setTimeout(r, 500));
|
|
84
|
+
|
|
85
|
+
// 即刻首页发帖框的按钮文字为"发送"
|
|
86
|
+
const candidates = [
|
|
87
|
+
...Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
88
|
+
const text = btn.textContent?.trim() || '';
|
|
89
|
+
return text === '发送' || text === '发布';
|
|
90
|
+
}),
|
|
91
|
+
].filter(el => el && !el.disabled);
|
|
92
|
+
|
|
93
|
+
if (candidates.length === 0) {
|
|
94
|
+
return { ok: false, message: '未找到可用的发送按钮(按钮可能因内容为空而禁用)' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
candidates[0].click();
|
|
98
|
+
return { ok: true, message: '动态发布成功' };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, message: e.toString() };
|
|
101
|
+
}
|
|
102
|
+
})()`);
|
|
103
|
+
|
|
104
|
+
if (submitResult.ok) {
|
|
105
|
+
await page.wait(3);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [{
|
|
109
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
110
|
+
message: submitResult.message,
|
|
111
|
+
}];
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { JikePost, getPostDataJs } from './shared.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 即刻首页动态流适配器
|
|
6
|
+
*
|
|
7
|
+
* 策略:导航到 web.okjike.com/following(需登录),
|
|
8
|
+
* 通过 React fiber 树提取帖子数据。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
cli({
|
|
12
|
+
site: 'jike',
|
|
13
|
+
name: 'feed',
|
|
14
|
+
description: '即刻首页动态流',
|
|
15
|
+
domain: 'web.okjike.com',
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
20
|
+
],
|
|
21
|
+
columns: ['author', 'content', 'likes', 'comments', 'time', 'url'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const limit = kwargs.limit || 20;
|
|
24
|
+
|
|
25
|
+
// 1. 导航到即刻首页,等待 SPA 重定向到 /following
|
|
26
|
+
await page.goto('https://web.okjike.com');
|
|
27
|
+
await page.wait(5);
|
|
28
|
+
|
|
29
|
+
// 2. 通过 React fiber 提取帖子数据
|
|
30
|
+
const extract = async (): Promise<JikePost[]> => {
|
|
31
|
+
return (await page.evaluate(`(() => {
|
|
32
|
+
${getPostDataJs}
|
|
33
|
+
|
|
34
|
+
const results = [];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const elements = document.querySelectorAll('[class*="_post_"]');
|
|
37
|
+
|
|
38
|
+
for (const el of elements) {
|
|
39
|
+
const data = getPostData(el);
|
|
40
|
+
if (!data || !data.id || seen.has(data.id)) continue;
|
|
41
|
+
seen.add(data.id);
|
|
42
|
+
|
|
43
|
+
// 转发帖的正文可能为空,取 target(原帖)的内容作 fallback
|
|
44
|
+
const author = data.user?.screenName || data.target?.user?.screenName || '';
|
|
45
|
+
const content = data.content || data.target?.content || '';
|
|
46
|
+
|
|
47
|
+
// 跳过无内容且无作者的条目(如 PERSONAL_UPDATE)
|
|
48
|
+
if (!author && !content) continue;
|
|
49
|
+
|
|
50
|
+
results.push({
|
|
51
|
+
author,
|
|
52
|
+
content: content.replace(/\\n/g, ' ').slice(0, 120),
|
|
53
|
+
likes: data.likeCount || 0,
|
|
54
|
+
comments: data.commentCount || 0,
|
|
55
|
+
time: data.actionTime || data.createdAt || '',
|
|
56
|
+
url: 'https://web.okjike.com/originalPost/' + data.id,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
})()`)) as JikePost[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let posts = await extract();
|
|
65
|
+
|
|
66
|
+
// 3. 如果数量不足,自动滚动加载更多
|
|
67
|
+
if (posts.length < limit) {
|
|
68
|
+
await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 2000 });
|
|
69
|
+
posts = await extract();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return posts.slice(0, limit);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 点赞即刻帖子
|
|
5
|
+
*
|
|
6
|
+
* 即刻帖子详情页的操作栏是 div 元素(非 button),
|
|
7
|
+
* 点赞按钮可通过 class 前缀 _likeButton_ 定位。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'jike',
|
|
12
|
+
name: 'like',
|
|
13
|
+
description: '点赞即刻帖子',
|
|
14
|
+
domain: 'web.okjike.com',
|
|
15
|
+
strategy: Strategy.UI,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'message'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
// 1. 导航到帖子详情页
|
|
23
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
|
|
26
|
+
// 2. 找到点赞按钮并点击
|
|
27
|
+
const result = await page.evaluate(`(async () => {
|
|
28
|
+
try {
|
|
29
|
+
// 点赞按钮:class 包含 _likeButton_,在 _actions_ 容器内
|
|
30
|
+
const likeBtn = document.querySelector('[class*="_likeButton_"]');
|
|
31
|
+
if (!likeBtn) {
|
|
32
|
+
return { ok: false, message: '未找到点赞按钮' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 检查是否已点赞(已赞按钮带有 _liked_ 类)
|
|
36
|
+
const cls = likeBtn.className || '';
|
|
37
|
+
if (cls.includes('_liked')) {
|
|
38
|
+
return { ok: true, message: '该帖子已赞过' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 记录点击前的类名
|
|
42
|
+
const beforeCls = likeBtn.className;
|
|
43
|
+
|
|
44
|
+
likeBtn.click();
|
|
45
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
46
|
+
|
|
47
|
+
// 验证:类名变化表示点赞成功
|
|
48
|
+
const afterCls = likeBtn.className;
|
|
49
|
+
if (afterCls !== beforeCls) {
|
|
50
|
+
return { ok: true, message: '点赞成功' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 类名未变化,无法确认点赞是否成功
|
|
54
|
+
return { ok: false, message: '点赞状态未确认,请手动检查' };
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { ok: false, message: e.toString() };
|
|
57
|
+
}
|
|
58
|
+
})()`);
|
|
59
|
+
|
|
60
|
+
return [{
|
|
61
|
+
status: result.ok ? 'success' : 'failed',
|
|
62
|
+
message: result.message,
|
|
63
|
+
}];
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 即刻通知适配器
|
|
5
|
+
*
|
|
6
|
+
* 策略:直接导航到 web.okjike.com/notification,
|
|
7
|
+
* 优先用 React fiber 树提取通知数据,失败时回退到 DOM 文本提取。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// 即刻通知的通用字段
|
|
11
|
+
interface JikeNotification {
|
|
12
|
+
type: string;
|
|
13
|
+
user: string;
|
|
14
|
+
content: string;
|
|
15
|
+
time: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 将通知类型代码映射为中文标签
|
|
19
|
+
function resolveActionLabel(type: string): string {
|
|
20
|
+
if (!type) return '通知';
|
|
21
|
+
const upper = type.toUpperCase();
|
|
22
|
+
if (upper.includes('LIKE')) return '赞了你';
|
|
23
|
+
if (upper.includes('COMMENT')) return '评论了你';
|
|
24
|
+
if (upper.includes('FOLLOW')) return '关注了你';
|
|
25
|
+
if (upper.includes('REPOST')) return '转发了你';
|
|
26
|
+
if (upper.includes('MENTION')) return '提到了你';
|
|
27
|
+
if (upper.includes('REPLY')) return '回复了你';
|
|
28
|
+
return type;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cli({
|
|
32
|
+
site: 'jike',
|
|
33
|
+
name: 'notifications',
|
|
34
|
+
description: '即刻通知',
|
|
35
|
+
domain: 'web.okjike.com',
|
|
36
|
+
strategy: Strategy.COOKIE,
|
|
37
|
+
browser: true,
|
|
38
|
+
args: [
|
|
39
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
40
|
+
],
|
|
41
|
+
columns: ['type', 'user', 'content', 'time'],
|
|
42
|
+
func: async (page, kwargs) => {
|
|
43
|
+
const limit = (kwargs.limit as number) || 20;
|
|
44
|
+
|
|
45
|
+
// 1. 直接导航到通知页
|
|
46
|
+
await page.goto('https://web.okjike.com/notification');
|
|
47
|
+
await page.wait(5);
|
|
48
|
+
|
|
49
|
+
// 3. 优先用 React fiber 提取通知数据
|
|
50
|
+
// 通知 fiber 数据结构与帖子不同,需查找含 type + user 字段的 props
|
|
51
|
+
const fiberResults = (await page.evaluate(`(() => {
|
|
52
|
+
// 从 React fiber 树中提取通知数据,向上最多走 15 层
|
|
53
|
+
function getNotificationData(element) {
|
|
54
|
+
for (const key of Object.keys(element)) {
|
|
55
|
+
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
56
|
+
let fiber = element[key];
|
|
57
|
+
for (let i = 0; i < 15 && fiber; i++) {
|
|
58
|
+
const props = fiber.memoizedProps || fiber.pendingProps;
|
|
59
|
+
if (props && props.data) {
|
|
60
|
+
const d = props.data;
|
|
61
|
+
// 通知条目特征:含 type/actionType 字段,以及来源用户字段
|
|
62
|
+
if (d.type || d.actionType || d.notificationType) return d;
|
|
63
|
+
}
|
|
64
|
+
fiber = fiber.return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const results = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
|
|
74
|
+
// 通知页使用 _item_ 类前缀作为条目容器
|
|
75
|
+
const elements = Array.from(document.querySelectorAll('[class*="_item_"]'));
|
|
76
|
+
|
|
77
|
+
for (const el of elements) {
|
|
78
|
+
const data = getNotificationData(el);
|
|
79
|
+
if (!data) continue;
|
|
80
|
+
|
|
81
|
+
const actionType = data.actionType || data.type || data.notificationType || '';
|
|
82
|
+
const fromUser =
|
|
83
|
+
(data.sourceUser && data.sourceUser.screenName) ||
|
|
84
|
+
(data.user && data.user.screenName) ||
|
|
85
|
+
(data.actionUser && data.actionUser.screenName) ||
|
|
86
|
+
(data.actor && data.actor.screenName) ||
|
|
87
|
+
'';
|
|
88
|
+
const targetContent =
|
|
89
|
+
(data.targetPost && data.targetPost.content) ||
|
|
90
|
+
(data.post && data.post.content) ||
|
|
91
|
+
'';
|
|
92
|
+
const commentContent =
|
|
93
|
+
(data.comment && data.comment.content) ||
|
|
94
|
+
data.commentContent ||
|
|
95
|
+
'';
|
|
96
|
+
const content = commentContent || targetContent;
|
|
97
|
+
const time = data.createdAt || data.updatedAt || '';
|
|
98
|
+
|
|
99
|
+
// 用 user+time+type 去重,避免同一用户同一时间不同类型通知被合并
|
|
100
|
+
const key = fromUser + '\x00' + time + '\x00' + (data.type || data.actionType || '');
|
|
101
|
+
if (seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
|
|
104
|
+
if (!fromUser && !content) continue;
|
|
105
|
+
|
|
106
|
+
results.push({ actionType, fromUser, content, time });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return results;
|
|
110
|
+
})()`)) as Array<{ actionType: string; fromUser: string; content: string; time: string }>;
|
|
111
|
+
|
|
112
|
+
// 4. fiber 提取成功,映射类型标签后返回
|
|
113
|
+
if (fiberResults.length > 0) {
|
|
114
|
+
const notifications: JikeNotification[] = fiberResults.map((r) => ({
|
|
115
|
+
type: resolveActionLabel(r.actionType),
|
|
116
|
+
user: r.fromUser,
|
|
117
|
+
content: r.content.replace(/\n/g, ' ').slice(0, 100),
|
|
118
|
+
time: r.time,
|
|
119
|
+
}));
|
|
120
|
+
return notifications.slice(0, limit);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5. 回退:解析通知条目的 innerText(格式: 用户名\n操作描述\n日期)
|
|
124
|
+
await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 2000 });
|
|
125
|
+
|
|
126
|
+
const domResults = (await page.evaluate(`(() => {
|
|
127
|
+
const results = [];
|
|
128
|
+
const items = document.querySelectorAll('[class*="_item_"]');
|
|
129
|
+
|
|
130
|
+
// 动作关键词,用于定位通知类型行
|
|
131
|
+
// 长模式优先匹配,避免"赞了你"截断"赞了你的动态"
|
|
132
|
+
const actionPatterns = [
|
|
133
|
+
'赞了你的动态', '赞了你的评论', '赞了你的转发',
|
|
134
|
+
'评论了你的动态', '评论了你的转发',
|
|
135
|
+
'回复了你的评论', '回复了你',
|
|
136
|
+
'转发了你的动态',
|
|
137
|
+
'提到了你', '关注了你',
|
|
138
|
+
'赞了你', '评论了你', '转发了你',
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const item of items) {
|
|
142
|
+
const text = item.innerText || '';
|
|
143
|
+
const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
|
|
144
|
+
if (lines.length < 2) continue;
|
|
145
|
+
|
|
146
|
+
// 策略:在全文中搜索动作关键词来定位类型
|
|
147
|
+
let type = '';
|
|
148
|
+
let user = '';
|
|
149
|
+
let time = '';
|
|
150
|
+
let content = '';
|
|
151
|
+
|
|
152
|
+
const fullText = lines.join(' ');
|
|
153
|
+
for (const pattern of actionPatterns) {
|
|
154
|
+
const idx = fullText.indexOf(pattern);
|
|
155
|
+
if (idx >= 0) {
|
|
156
|
+
// 关键词前面是用户名(可能多人用、分隔)
|
|
157
|
+
user = fullText.slice(0, idx).replace(/[、,]/g, ' ').trim();
|
|
158
|
+
type = pattern;
|
|
159
|
+
// 关键词后面可能是时间和原帖内容
|
|
160
|
+
const rest = fullText.slice(idx + pattern.length).trim();
|
|
161
|
+
// 时间通常以数字或"刚刚"/"分钟前"等开头
|
|
162
|
+
const timeMatch = rest.match(/^[\\S]*?(?:刚刚|\\d+.*?前|\\d{4}\\/\\d{2}\\/\\d{2})/);
|
|
163
|
+
time = timeMatch ? timeMatch[0] : '';
|
|
164
|
+
content = rest.slice(time.length).trim().slice(0, 100);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 没匹配到动作关键词,回退到简单行序
|
|
170
|
+
if (!type) {
|
|
171
|
+
user = lines[0] || '';
|
|
172
|
+
type = lines[1] || '';
|
|
173
|
+
time = lines[2] || '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!user && !type) continue;
|
|
177
|
+
results.push({ type, user, content, time });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return results;
|
|
181
|
+
})()`)) as JikeNotification[];
|
|
182
|
+
|
|
183
|
+
return domResults.slice(0, limit);
|
|
184
|
+
},
|
|
185
|
+
});
|