@jackwener/opencli 1.0.1 → 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 +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- 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/index.d.ts +3 -0
- package/dist/browser/index.js +4 -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 +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- 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.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- 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/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- 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/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- 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 +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- 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 +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- 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 +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -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 +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- 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/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- 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 +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
package/dist/clis/chatgpt/ask.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import { getVisibleChatMessages } from './ax.js';
|
|
3
4
|
export const askCommand = cli({
|
|
4
5
|
site: 'chatgpt',
|
|
5
6
|
name: 'ask',
|
|
@@ -21,6 +22,7 @@ export const askCommand = cli({
|
|
|
21
22
|
clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
|
|
22
23
|
}
|
|
23
24
|
catch { }
|
|
25
|
+
const messagesBefore = getVisibleChatMessages();
|
|
24
26
|
// Send the message
|
|
25
27
|
spawnSync('pbcopy', { input: text });
|
|
26
28
|
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
@@ -32,28 +34,27 @@ export const askCommand = cli({
|
|
|
32
34
|
"-e 'keystroke return' " +
|
|
33
35
|
"-e 'end tell'";
|
|
34
36
|
execSync(cmd);
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Restore clipboard after the prompt is sent.
|
|
38
|
+
if (clipBackup)
|
|
39
|
+
spawnSync('pbcopy', { input: clipBackup });
|
|
40
|
+
// Wait for response, then read the latest visible assistant message from the AX tree.
|
|
41
|
+
const pollInterval = 1;
|
|
39
42
|
const maxPolls = Math.ceil(timeout / pollInterval);
|
|
40
43
|
let response = '';
|
|
41
44
|
for (let i = 0; i < maxPolls; i++) {
|
|
42
|
-
// Wait
|
|
43
45
|
execSync(`sleep ${pollInterval}`);
|
|
44
|
-
// Try Cmd+Shift+C to copy the latest response
|
|
45
46
|
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
46
|
-
execSync("osascript -e '
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
execSync("osascript -e 'delay 0.2'");
|
|
48
|
+
const messagesNow = getVisibleChatMessages();
|
|
49
|
+
if (messagesNow.length <= messagesBefore.length)
|
|
50
|
+
continue;
|
|
51
|
+
const newMessages = messagesNow.slice(messagesBefore.length);
|
|
52
|
+
const candidate = [...newMessages].reverse().find((message) => message !== text);
|
|
53
|
+
if (candidate) {
|
|
54
|
+
response = candidate;
|
|
51
55
|
break;
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
|
-
// Restore clipboard
|
|
55
|
-
if (clipBackup)
|
|
56
|
-
spawnSync('pbcopy', { input: clipBackup });
|
|
57
58
|
if (!response) {
|
|
58
59
|
return [
|
|
59
60
|
{ Role: 'User', Text: text },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getVisibleChatMessages(): string[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
const AX_READ_SCRIPT = `
|
|
3
|
+
import Cocoa
|
|
4
|
+
import ApplicationServices
|
|
5
|
+
|
|
6
|
+
func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
|
|
7
|
+
var value: CFTypeRef?
|
|
8
|
+
guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
|
|
9
|
+
return value as AnyObject?
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func s(_ el: AXUIElement, _ name: String) -> String? {
|
|
13
|
+
if let v = attr(el, name) as? String, !v.isEmpty { return v }
|
|
14
|
+
return nil
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func children(_ el: AXUIElement) -> [AXUIElement] {
|
|
18
|
+
(attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func collectLists(_ el: AXUIElement, into out: inout [AXUIElement]) {
|
|
22
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
23
|
+
if role == kAXListRole as String { out.append(el) }
|
|
24
|
+
for c in children(el) { collectLists(c, into: &out) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func collectTexts(_ el: AXUIElement, into out: inout [String]) {
|
|
28
|
+
let role = s(el, kAXRoleAttribute as String) ?? ""
|
|
29
|
+
if role == kAXStaticTextRole as String {
|
|
30
|
+
if let text = s(el, kAXDescriptionAttribute as String), !text.isEmpty {
|
|
31
|
+
out.append(text)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for c in children(el) { collectTexts(c, into: &out) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
|
|
38
|
+
fputs("ChatGPT not running\\n", stderr)
|
|
39
|
+
exit(1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let axApp = AXUIElementCreateApplication(app.processIdentifier)
|
|
43
|
+
guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
|
|
44
|
+
fputs("No focused ChatGPT window\\n", stderr)
|
|
45
|
+
exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var lists: [AXUIElement] = []
|
|
49
|
+
collectLists(win, into: &lists)
|
|
50
|
+
|
|
51
|
+
var best: [String] = []
|
|
52
|
+
for list in lists {
|
|
53
|
+
var texts: [String] = []
|
|
54
|
+
collectTexts(list, into: &texts)
|
|
55
|
+
if texts.count > best.count {
|
|
56
|
+
best = texts
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let data = try! JSONSerialization.data(withJSONObject: best, options: [])
|
|
61
|
+
print(String(data: data, encoding: .utf8)!)
|
|
62
|
+
`;
|
|
63
|
+
export function getVisibleChatMessages() {
|
|
64
|
+
const output = execFileSync('swift', ['-'], {
|
|
65
|
+
input: AX_READ_SCRIPT,
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
68
|
+
}).trim();
|
|
69
|
+
if (!output)
|
|
70
|
+
return [];
|
|
71
|
+
const parsed = JSON.parse(output);
|
|
72
|
+
if (!Array.isArray(parsed))
|
|
73
|
+
return [];
|
|
74
|
+
return parsed
|
|
75
|
+
.filter((item) => typeof item === 'string')
|
|
76
|
+
.map((item) => item.replace(/[\uFFFC\u200B-\u200D\uFEFF]/g, '').trim())
|
|
77
|
+
.filter((item) => item.length > 0);
|
|
78
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import { getVisibleChatMessages } from './ax.js';
|
|
3
4
|
export const readCommand = cli({
|
|
4
5
|
site: 'chatgpt',
|
|
5
6
|
name: 'read',
|
|
@@ -12,14 +13,12 @@ export const readCommand = cli({
|
|
|
12
13
|
func: async (page) => {
|
|
13
14
|
try {
|
|
14
15
|
execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
|
|
15
|
-
execSync("osascript -e 'delay 0.5'");
|
|
16
|
-
execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
|
|
17
16
|
execSync("osascript -e 'delay 0.3'");
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
20
|
-
return [{ Role: 'System', Text: 'No
|
|
17
|
+
const messages = getVisibleChatMessages();
|
|
18
|
+
if (!messages.length) {
|
|
19
|
+
return [{ Role: 'System', Text: 'No visible chat messages were found in the current ChatGPT window.' }];
|
|
21
20
|
}
|
|
22
|
-
return [{ Role: 'Assistant', Text:
|
|
21
|
+
return [{ Role: 'Assistant', Text: messages[messages.length - 1] }];
|
|
23
22
|
}
|
|
24
23
|
catch (err) {
|
|
25
24
|
throw new Error("Failed to read from ChatGPT: " + err.message);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { CliError } from '../../errors.js';
|
|
3
|
+
function truncate(str, max = 60) {
|
|
4
|
+
return str.length > max ? str.slice(0, max - 3) + '...' : str;
|
|
5
|
+
}
|
|
6
|
+
function formatAuthors(authors, max = 3) {
|
|
7
|
+
const names = authors.map((a) => a.name);
|
|
8
|
+
if (names.length <= max)
|
|
9
|
+
return names.join(', ');
|
|
10
|
+
return names.slice(0, max).join(', ') + ' et al.';
|
|
11
|
+
}
|
|
12
|
+
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
13
|
+
function getMonthRange() {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`;
|
|
16
|
+
}
|
|
17
|
+
function getWeekRange() {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const day = now.getUTCDay(); // 0=Sun, 6=Sat
|
|
20
|
+
const daysToSat = day === 6 ? 0 : 6 - day;
|
|
21
|
+
const end = new Date(now);
|
|
22
|
+
end.setUTCDate(now.getUTCDate() + daysToSat);
|
|
23
|
+
const start = new Date(end);
|
|
24
|
+
start.setUTCDate(end.getUTCDate() - 6);
|
|
25
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
26
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
27
|
+
const sd = start.getUTCDate();
|
|
28
|
+
const ed = end.getUTCDate();
|
|
29
|
+
return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
30
|
+
}
|
|
31
|
+
cli({
|
|
32
|
+
site: 'hf',
|
|
33
|
+
name: 'top',
|
|
34
|
+
description: 'Top upvoted Hugging Face papers',
|
|
35
|
+
domain: 'huggingface.co',
|
|
36
|
+
strategy: Strategy.PUBLIC,
|
|
37
|
+
browser: false,
|
|
38
|
+
args: [
|
|
39
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of papers' },
|
|
40
|
+
{ name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' },
|
|
41
|
+
{ name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' },
|
|
42
|
+
{ name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' },
|
|
43
|
+
],
|
|
44
|
+
footerExtra: (kwargs) => {
|
|
45
|
+
if (kwargs._footerDate)
|
|
46
|
+
return kwargs._footerDate;
|
|
47
|
+
if (kwargs.period === 'monthly')
|
|
48
|
+
return getMonthRange();
|
|
49
|
+
if (kwargs.period === 'weekly')
|
|
50
|
+
return getWeekRange();
|
|
51
|
+
return kwargs.date ?? new Date().toISOString().slice(0, 10);
|
|
52
|
+
},
|
|
53
|
+
func: async (_page, kwargs) => {
|
|
54
|
+
const period = String(kwargs.period ?? 'daily');
|
|
55
|
+
const all = Boolean(kwargs.all);
|
|
56
|
+
const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';
|
|
57
|
+
if (period === 'weekly' || period === 'monthly') {
|
|
58
|
+
if (kwargs.date) {
|
|
59
|
+
throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`);
|
|
60
|
+
}
|
|
61
|
+
const url = `${endpoint}/api/papers?period=${period}`;
|
|
62
|
+
const res = await fetch(url);
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
if (!Array.isArray(body))
|
|
67
|
+
throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint');
|
|
68
|
+
const data = body;
|
|
69
|
+
const dates = data.map((d) => d.publishedAt).filter(Boolean).sort();
|
|
70
|
+
if (dates.length > 0) {
|
|
71
|
+
if (period === 'monthly') {
|
|
72
|
+
const d = new Date(dates[0]);
|
|
73
|
+
kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const start = new Date(dates[0]);
|
|
77
|
+
const end = new Date(dates[dates.length - 1]);
|
|
78
|
+
const sm = MONTH_ABBR[start.getUTCMonth()];
|
|
79
|
+
const em = MONTH_ABBR[end.getUTCMonth()];
|
|
80
|
+
const sd = start.getUTCDate();
|
|
81
|
+
const ed = end.getUTCDate();
|
|
82
|
+
kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0));
|
|
86
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
87
|
+
return items.map((item, i) => ({
|
|
88
|
+
rank: i + 1,
|
|
89
|
+
id: item.id ?? '',
|
|
90
|
+
title: truncate(item.title ?? ''),
|
|
91
|
+
upvotes: item.upvotes ?? 0,
|
|
92
|
+
authors: formatAuthors(item.authors ?? []),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
// daily
|
|
96
|
+
if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) {
|
|
97
|
+
throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD');
|
|
98
|
+
}
|
|
99
|
+
const url = kwargs.date
|
|
100
|
+
? `${endpoint}/api/daily_papers?date=${kwargs.date}`
|
|
101
|
+
: `${endpoint}/api/daily_papers`;
|
|
102
|
+
const res = await fetch(url);
|
|
103
|
+
if (!res.ok)
|
|
104
|
+
throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
|
|
105
|
+
const body = await res.json();
|
|
106
|
+
if (!Array.isArray(body))
|
|
107
|
+
throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint');
|
|
108
|
+
const data = body;
|
|
109
|
+
const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0));
|
|
110
|
+
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
|
|
111
|
+
return items.map((item, i) => ({
|
|
112
|
+
rank: i + 1,
|
|
113
|
+
id: item.paper?.id ?? '',
|
|
114
|
+
title: truncate(item.title ?? ''),
|
|
115
|
+
upvotes: item.paper?.upvotes ?? 0,
|
|
116
|
+
authors: formatAuthors(item.paper?.authors ?? []),
|
|
117
|
+
}));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* 评论即刻帖子
|
|
4
|
+
*
|
|
5
|
+
* 帖子详情页有评论输入框(contenteditable 或 textarea),
|
|
6
|
+
* 填入文本后点击"回复"或"发布"按钮提交。
|
|
7
|
+
*/
|
|
8
|
+
cli({
|
|
9
|
+
site: 'jike',
|
|
10
|
+
name: 'comment',
|
|
11
|
+
description: '评论即刻帖子',
|
|
12
|
+
domain: 'web.okjike.com',
|
|
13
|
+
strategy: Strategy.UI,
|
|
14
|
+
browser: true,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
17
|
+
{ name: 'text', type: 'string', required: true, help: '评论内容' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['status', 'message'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
22
|
+
await page.wait(5);
|
|
23
|
+
// 1. 找到评论输入框并填入文本
|
|
24
|
+
const inputResult = await page.evaluate(`(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
27
|
+
|
|
28
|
+
// 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器;
|
|
29
|
+
// 若评论区 class 名变更则回退到全页查找
|
|
30
|
+
const editor =
|
|
31
|
+
document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
|
|
32
|
+
document.querySelector('[contenteditable="true"]');
|
|
33
|
+
if (editor) {
|
|
34
|
+
editor.focus();
|
|
35
|
+
const dt = new DataTransfer();
|
|
36
|
+
dt.setData('text/plain', textToInsert);
|
|
37
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
38
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
39
|
+
}));
|
|
40
|
+
await new Promise(r => setTimeout(r, 800));
|
|
41
|
+
if (editor.textContent?.length > 0) {
|
|
42
|
+
return { ok: true, message: 'contenteditable' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 回退:textarea(带评论相关 placeholder)
|
|
47
|
+
const textareas = document.querySelectorAll('textarea');
|
|
48
|
+
for (const ta of textareas) {
|
|
49
|
+
const ph = ta.getAttribute('placeholder') || '';
|
|
50
|
+
if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) {
|
|
51
|
+
ta.focus();
|
|
52
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
53
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
54
|
+
)?.set;
|
|
55
|
+
setter?.call(ta, textToInsert);
|
|
56
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
57
|
+
await new Promise(r => setTimeout(r, 500));
|
|
58
|
+
return { ok: true, message: 'textarea' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 兜底:任意 textarea
|
|
63
|
+
if (textareas.length > 0) {
|
|
64
|
+
const ta = textareas[0];
|
|
65
|
+
ta.focus();
|
|
66
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
67
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
68
|
+
)?.set;
|
|
69
|
+
setter?.call(ta, textToInsert);
|
|
70
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
71
|
+
await new Promise(r => setTimeout(r, 500));
|
|
72
|
+
return { ok: true, message: 'textarea-fallback' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { ok: false, message: '未找到评论输入框' };
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { ok: false, message: e.toString() };
|
|
78
|
+
}
|
|
79
|
+
})()`);
|
|
80
|
+
if (!inputResult.ok) {
|
|
81
|
+
return [{ status: 'failed', message: inputResult.message }];
|
|
82
|
+
}
|
|
83
|
+
// 2. 点击"回复"或"发布"按钮
|
|
84
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
85
|
+
try {
|
|
86
|
+
await new Promise(r => setTimeout(r, 500));
|
|
87
|
+
const btns = Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
88
|
+
const text = btn.textContent?.trim() || '';
|
|
89
|
+
return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
|
|
90
|
+
});
|
|
91
|
+
if (btns.length === 0) {
|
|
92
|
+
return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' };
|
|
93
|
+
}
|
|
94
|
+
btns[0].click();
|
|
95
|
+
return { ok: true, message: '评论发布成功' };
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, message: e.toString() };
|
|
98
|
+
}
|
|
99
|
+
})()`);
|
|
100
|
+
if (submitResult.ok)
|
|
101
|
+
await page.wait(3);
|
|
102
|
+
return [{
|
|
103
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
104
|
+
message: submitResult.message,
|
|
105
|
+
}];
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* 发布即刻动态
|
|
4
|
+
*
|
|
5
|
+
* 即刻首页 /following 顶部有内联发帖框("分享你的想法..."),
|
|
6
|
+
* 直接在其中输入文本,点击"发送"按钮即可发布。
|
|
7
|
+
*/
|
|
8
|
+
cli({
|
|
9
|
+
site: 'jike',
|
|
10
|
+
name: 'create',
|
|
11
|
+
description: '发布即刻动态',
|
|
12
|
+
domain: 'web.okjike.com',
|
|
13
|
+
strategy: Strategy.UI,
|
|
14
|
+
browser: true,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'text', type: 'string', required: true, help: '动态正文内容' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['status', 'message'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
// 1. 导航到首页(有内联发帖框)
|
|
21
|
+
await page.goto('https://web.okjike.com');
|
|
22
|
+
await page.wait(5);
|
|
23
|
+
// 2. 在发帖框中输入文本
|
|
24
|
+
const textResult = await page.evaluate(`(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
27
|
+
|
|
28
|
+
// 首页发帖框在 _postForm_ 容器内,查找其中的 contenteditable
|
|
29
|
+
const form = document.querySelector('[class*="_postForm_"]');
|
|
30
|
+
const editor = form
|
|
31
|
+
? form.querySelector('[contenteditable="true"]')
|
|
32
|
+
: document.querySelector('[contenteditable="true"]');
|
|
33
|
+
|
|
34
|
+
if (editor) {
|
|
35
|
+
editor.focus();
|
|
36
|
+
// 用 ClipboardEvent paste 触发 React 状态更新
|
|
37
|
+
const dt = new DataTransfer();
|
|
38
|
+
dt.setData('text/plain', textToInsert);
|
|
39
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
40
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
41
|
+
}));
|
|
42
|
+
await new Promise(r => setTimeout(r, 800));
|
|
43
|
+
|
|
44
|
+
// 检查是否成功插入
|
|
45
|
+
const inserted = editor.textContent || '';
|
|
46
|
+
if (inserted.length > 0) {
|
|
47
|
+
return { ok: true, message: 'contenteditable' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 回退:textarea
|
|
52
|
+
const textarea = form
|
|
53
|
+
? form.querySelector('textarea')
|
|
54
|
+
: document.querySelector('textarea');
|
|
55
|
+
|
|
56
|
+
if (textarea) {
|
|
57
|
+
textarea.focus();
|
|
58
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
59
|
+
HTMLTextAreaElement.prototype, 'value'
|
|
60
|
+
)?.set;
|
|
61
|
+
setter?.call(textarea, textToInsert);
|
|
62
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
63
|
+
await new Promise(r => setTimeout(r, 500));
|
|
64
|
+
return { ok: true, message: 'textarea' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { ok: false, message: '未找到发帖输入框' };
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { ok: false, message: e.toString() };
|
|
70
|
+
}
|
|
71
|
+
})()`);
|
|
72
|
+
if (!textResult.ok) {
|
|
73
|
+
return [{ status: 'failed', message: textResult.message }];
|
|
74
|
+
}
|
|
75
|
+
// 3. 点击"发送"按钮
|
|
76
|
+
const submitResult = await page.evaluate(`(async () => {
|
|
77
|
+
try {
|
|
78
|
+
await new Promise(r => setTimeout(r, 500));
|
|
79
|
+
|
|
80
|
+
// 即刻首页发帖框的按钮文字为"发送"
|
|
81
|
+
const candidates = [
|
|
82
|
+
...Array.from(document.querySelectorAll('button')).filter(btn => {
|
|
83
|
+
const text = btn.textContent?.trim() || '';
|
|
84
|
+
return text === '发送' || text === '发布';
|
|
85
|
+
}),
|
|
86
|
+
].filter(el => el && !el.disabled);
|
|
87
|
+
|
|
88
|
+
if (candidates.length === 0) {
|
|
89
|
+
return { ok: false, message: '未找到可用的发送按钮(按钮可能因内容为空而禁用)' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
candidates[0].click();
|
|
93
|
+
return { ok: true, message: '动态发布成功' };
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return { ok: false, message: e.toString() };
|
|
96
|
+
}
|
|
97
|
+
})()`);
|
|
98
|
+
if (submitResult.ok) {
|
|
99
|
+
await page.wait(3);
|
|
100
|
+
}
|
|
101
|
+
return [{
|
|
102
|
+
status: submitResult.ok ? 'success' : 'failed',
|
|
103
|
+
message: submitResult.message,
|
|
104
|
+
}];
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { getPostDataJs } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* 即刻首页动态流适配器
|
|
5
|
+
*
|
|
6
|
+
* 策略:导航到 web.okjike.com/following(需登录),
|
|
7
|
+
* 通过 React fiber 树提取帖子数据。
|
|
8
|
+
*/
|
|
9
|
+
cli({
|
|
10
|
+
site: 'jike',
|
|
11
|
+
name: 'feed',
|
|
12
|
+
description: '即刻首页动态流',
|
|
13
|
+
domain: 'web.okjike.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
browser: true,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
18
|
+
],
|
|
19
|
+
columns: ['author', 'content', 'likes', 'comments', 'time', 'url'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const limit = kwargs.limit || 20;
|
|
22
|
+
// 1. 导航到即刻首页,等待 SPA 重定向到 /following
|
|
23
|
+
await page.goto('https://web.okjike.com');
|
|
24
|
+
await page.wait(5);
|
|
25
|
+
// 2. 通过 React fiber 提取帖子数据
|
|
26
|
+
const extract = async () => {
|
|
27
|
+
return (await page.evaluate(`(() => {
|
|
28
|
+
${getPostDataJs}
|
|
29
|
+
|
|
30
|
+
const results = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const elements = document.querySelectorAll('[class*="_post_"]');
|
|
33
|
+
|
|
34
|
+
for (const el of elements) {
|
|
35
|
+
const data = getPostData(el);
|
|
36
|
+
if (!data || !data.id || seen.has(data.id)) continue;
|
|
37
|
+
seen.add(data.id);
|
|
38
|
+
|
|
39
|
+
// 转发帖的正文可能为空,取 target(原帖)的内容作 fallback
|
|
40
|
+
const author = data.user?.screenName || data.target?.user?.screenName || '';
|
|
41
|
+
const content = data.content || data.target?.content || '';
|
|
42
|
+
|
|
43
|
+
// 跳过无内容且无作者的条目(如 PERSONAL_UPDATE)
|
|
44
|
+
if (!author && !content) continue;
|
|
45
|
+
|
|
46
|
+
results.push({
|
|
47
|
+
author,
|
|
48
|
+
content: content.replace(/\\n/g, ' ').slice(0, 120),
|
|
49
|
+
likes: data.likeCount || 0,
|
|
50
|
+
comments: data.commentCount || 0,
|
|
51
|
+
time: data.actionTime || data.createdAt || '',
|
|
52
|
+
url: 'https://web.okjike.com/originalPost/' + data.id,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
})()`));
|
|
58
|
+
};
|
|
59
|
+
let posts = await extract();
|
|
60
|
+
// 3. 如果数量不足,自动滚动加载更多
|
|
61
|
+
if (posts.length < limit) {
|
|
62
|
+
await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 2000 });
|
|
63
|
+
posts = await extract();
|
|
64
|
+
}
|
|
65
|
+
return posts.slice(0, limit);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* 点赞即刻帖子
|
|
4
|
+
*
|
|
5
|
+
* 即刻帖子详情页的操作栏是 div 元素(非 button),
|
|
6
|
+
* 点赞按钮可通过 class 前缀 _likeButton_ 定位。
|
|
7
|
+
*/
|
|
8
|
+
cli({
|
|
9
|
+
site: 'jike',
|
|
10
|
+
name: 'like',
|
|
11
|
+
description: '点赞即刻帖子',
|
|
12
|
+
domain: 'web.okjike.com',
|
|
13
|
+
strategy: Strategy.UI,
|
|
14
|
+
browser: true,
|
|
15
|
+
args: [
|
|
16
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['status', 'message'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
// 1. 导航到帖子详情页
|
|
21
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
22
|
+
await page.wait(5);
|
|
23
|
+
// 2. 找到点赞按钮并点击
|
|
24
|
+
const result = await page.evaluate(`(async () => {
|
|
25
|
+
try {
|
|
26
|
+
// 点赞按钮:class 包含 _likeButton_,在 _actions_ 容器内
|
|
27
|
+
const likeBtn = document.querySelector('[class*="_likeButton_"]');
|
|
28
|
+
if (!likeBtn) {
|
|
29
|
+
return { ok: false, message: '未找到点赞按钮' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 检查是否已点赞(已赞按钮带有 _liked_ 类)
|
|
33
|
+
const cls = likeBtn.className || '';
|
|
34
|
+
if (cls.includes('_liked')) {
|
|
35
|
+
return { ok: true, message: '该帖子已赞过' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 记录点击前的类名
|
|
39
|
+
const beforeCls = likeBtn.className;
|
|
40
|
+
|
|
41
|
+
likeBtn.click();
|
|
42
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
43
|
+
|
|
44
|
+
// 验证:类名变化表示点赞成功
|
|
45
|
+
const afterCls = likeBtn.className;
|
|
46
|
+
if (afterCls !== beforeCls) {
|
|
47
|
+
return { ok: true, message: '点赞成功' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 类名未变化,无法确认点赞是否成功
|
|
51
|
+
return { ok: false, message: '点赞状态未确认,请手动检查' };
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { ok: false, message: e.toString() };
|
|
54
|
+
}
|
|
55
|
+
})()`);
|
|
56
|
+
return [{
|
|
57
|
+
status: result.ok ? 'success' : 'failed',
|
|
58
|
+
message: result.message,
|
|
59
|
+
}];
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|