@jackwener/opencli 1.7.7 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -14
- package/README.zh-CN.md +30 -10
- package/cli-manifest.json +782 -55
- package/clis/36kr/news.js +1 -1
- package/clis/amazon/discussion.js +37 -6
- package/clis/amazon/discussion.test.js +147 -32
- package/clis/apple-podcasts/commands.test.js +4 -4
- package/clis/apple-podcasts/episodes.js +1 -1
- package/clis/apple-podcasts/search.js +1 -1
- package/clis/apple-podcasts/top.js +1 -1
- package/clis/arxiv/paper.js +1 -1
- package/clis/arxiv/search.js +1 -1
- package/clis/band/mentions.js +3 -3
- package/clis/bbc/news.js +1 -1
- package/clis/bilibili/subtitle.js +2 -2
- package/clis/bloomberg/businessweek.js +1 -1
- package/clis/bloomberg/economics.js +1 -1
- package/clis/bloomberg/industries.js +1 -1
- package/clis/bloomberg/main.js +1 -1
- package/clis/bloomberg/markets.js +1 -1
- package/clis/bloomberg/opinions.js +1 -1
- package/clis/bloomberg/politics.js +1 -1
- package/clis/bloomberg/tech.js +1 -1
- package/clis/boss/search.js +49 -8
- package/clis/boss/search.test.js +78 -0
- package/clis/boss/send.js +3 -3
- package/clis/chatgpt/image.js +37 -8
- package/clis/chatgpt/image.test.js +92 -0
- package/clis/chatgpt/utils.js +39 -6
- package/clis/chatgpt/utils.test.js +63 -0
- package/clis/chatgpt-app/ask.js +4 -20
- package/clis/chatgpt-app/ax.js +135 -2
- package/clis/chatgpt-app/ax.test.js +35 -0
- package/clis/chatgpt-app/model.js +1 -1
- package/clis/chatgpt-app/new.js +1 -1
- package/clis/chatgpt-app/read.js +1 -1
- package/clis/chatgpt-app/send.js +3 -22
- package/clis/chatgpt-app/status.js +1 -1
- package/clis/chatwise/ask.js +2 -2
- package/clis/chatwise/model.js +2 -2
- package/clis/chatwise/send.js +2 -2
- package/clis/claude/ask.js +128 -0
- package/clis/claude/ask.test.js +338 -0
- package/clis/claude/commands.test.js +118 -0
- package/clis/claude/detail.js +29 -0
- package/clis/claude/history.js +31 -0
- package/clis/claude/new.js +21 -0
- package/clis/claude/read.js +24 -0
- package/clis/claude/send.js +41 -0
- package/clis/claude/status.js +24 -0
- package/clis/claude/utils.js +440 -0
- package/clis/claude/utils.test.js +148 -0
- package/clis/codex/ask.js +2 -2
- package/clis/codex/send.js +2 -2
- package/clis/ctrip/search.js +1 -1
- package/clis/ctrip/search.test.js +4 -4
- package/clis/cursor/ask.js +2 -2
- package/clis/cursor/composer.js +2 -2
- package/clis/cursor/send.js +2 -2
- package/clis/deepseek/ask.js +49 -10
- package/clis/deepseek/ask.test.js +150 -3
- package/clis/deepseek/utils.js +60 -22
- package/clis/deepseek/utils.test.js +124 -5
- package/clis/doubao/utils.js +53 -11
- package/clis/doubao/utils.test.js +22 -2
- package/clis/eastmoney/announcement.js +1 -1
- package/clis/eastmoney/convertible.js +1 -1
- package/clis/eastmoney/etf.js +1 -1
- package/clis/eastmoney/holders.js +1 -1
- package/clis/eastmoney/index-board.js +1 -1
- package/clis/eastmoney/kline.js +1 -1
- package/clis/eastmoney/kuaixun.js +1 -1
- package/clis/eastmoney/longhu.js +1 -1
- package/clis/eastmoney/money-flow.js +1 -1
- package/clis/eastmoney/northbound.js +1 -1
- package/clis/eastmoney/quote.js +1 -1
- package/clis/eastmoney/rank.js +1 -1
- package/clis/eastmoney/sectors.js +1 -1
- package/clis/facebook/marketplace-inbox.js +83 -0
- package/clis/facebook/marketplace-listings.js +83 -0
- package/clis/facebook/marketplace.test.js +91 -0
- package/clis/google/news.js +1 -1
- package/clis/google/suggest.js +1 -1
- package/clis/google/trends.js +1 -1
- package/clis/google-scholar/cite.js +74 -0
- package/clis/google-scholar/cite.test.js +47 -0
- package/clis/google-scholar/profile.js +92 -0
- package/clis/google-scholar/profile.test.js +49 -0
- package/clis/google-scholar/search.js +1 -1
- package/clis/google-scholar/search.test.js +15 -0
- package/clis/hf/top.js +1 -1
- package/clis/jd/item.js +679 -47
- package/clis/jd/item.test.js +318 -7
- package/clis/jd/item.test.ts +517 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/tags.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/lesswrong/user-posts.js +1 -1
- package/clis/lesswrong/user.js +1 -1
- package/clis/paperreview/commands.test.js +6 -6
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/powerchina/search.js +250 -0
- package/clis/powerchina/search.test.js +67 -0
- package/clis/producthunt/posts.js +1 -1
- package/clis/producthunt/today.js +1 -1
- package/clis/sinablog/search.js +1 -1
- package/clis/sinafinance/news.js +1 -1
- package/clis/sinafinance/stock.js +6 -3
- package/clis/sinafinance/stock.test.js +59 -0
- package/clis/spotify/spotify.js +6 -6
- package/clis/substack/search.js +1 -1
- package/clis/toutiao/articles.js +80 -0
- package/clis/toutiao/articles.test.js +30 -0
- package/clis/twitter/followers.js +2 -2
- package/clis/twitter/following.js +224 -73
- package/clis/twitter/following.test.js +277 -0
- package/clis/twitter/post.js +184 -47
- package/clis/twitter/post.test.js +114 -34
- package/clis/uiverse/_shared.js +63 -4
- package/clis/uiverse/_shared.test.js +7 -0
- package/clis/uiverse/code.js +1 -0
- package/clis/uiverse/navigation.test.js +12 -0
- package/clis/uiverse/preview.js +1 -0
- package/clis/web/read.js +319 -81
- package/clis/web/read.test.js +221 -5
- package/clis/weibo/favorites.js +169 -0
- package/clis/weibo/favorites.test.js +114 -0
- package/clis/weibo/publish.js +282 -0
- package/clis/weibo/publish.test.js +183 -0
- package/clis/weixin/create-draft.js +225 -0
- package/clis/weixin/drafts.js +65 -0
- package/clis/weixin/drafts.test.js +65 -0
- package/clis/weread/ranking.js +1 -1
- package/clis/weread/search-regression.test.js +8 -8
- package/clis/weread/search.js +1 -1
- package/clis/wikipedia/random.js +1 -1
- package/clis/wikipedia/search.js +1 -1
- package/clis/wikipedia/summary.js +1 -1
- package/clis/wikipedia/trending.js +1 -1
- package/clis/xianyu/chat.js +3 -3
- package/clis/xianyu/item.js +2 -2
- package/clis/xianyu/item.test.js +3 -3
- package/clis/xiaohongshu/search.js +17 -2
- package/clis/xiaohongshu/search.test.js +37 -1
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/download.test.js +3 -3
- package/clis/xiaoyuzhou/episode.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
- package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
- package/clis/xiaoyuzhou/podcast.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/xiaoyuzhou/transcript.test.js +5 -5
- package/clis/yollomi/models.js +1 -1
- package/clis/youtube/channel.js +24 -1
- package/clis/youtube/channel.test.js +59 -0
- package/clis/zhihu/answer.js +21 -162
- package/clis/zhihu/answer.test.js +26 -53
- package/clis/zhihu/collection.js +197 -0
- package/clis/zhihu/collection.test.js +290 -0
- package/clis/zhihu/collections.js +127 -0
- package/clis/zhihu/collections.test.js +182 -0
- package/clis/zhihu/comment.js +24 -305
- package/clis/zhihu/comment.test.js +31 -35
- package/clis/zhihu/favorite.js +44 -182
- package/clis/zhihu/favorite.test.js +30 -167
- package/clis/zhihu/follow.js +25 -56
- package/clis/zhihu/follow.test.js +20 -23
- package/clis/zhihu/like.js +22 -67
- package/clis/zhihu/like.test.js +19 -42
- package/clis/zhihu/search.js +3 -2
- package/clis/zhihu/write-shared.js +8 -1
- package/clis/zhihu/write-shared.test.js +1 -0
- package/clis/zlibrary/commands.test.js +75 -0
- package/clis/zlibrary/info.js +47 -0
- package/clis/zlibrary/search.js +46 -0
- package/clis/zlibrary/utils.js +136 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/browser/base-page.d.ts +3 -1
- package/dist/src/browser/base-page.js +76 -1
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +74 -0
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +36 -9
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +3 -3
- package/dist/src/browser/daemon-client.d.ts +38 -4
- package/dist/src/browser/daemon-client.js +24 -7
- package/dist/src/browser/daemon-client.test.js +49 -0
- package/dist/src/browser/errors.js +3 -0
- package/dist/src/browser/errors.test.js +3 -0
- package/dist/src/browser/network-cache.d.ts +1 -0
- package/dist/src/browser/page.d.ts +3 -1
- package/dist/src/browser/page.js +10 -2
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +85 -0
- package/dist/src/build-manifest.d.ts +2 -0
- package/dist/src/build-manifest.js +13 -3
- package/dist/src/build-manifest.test.js +20 -2
- package/dist/src/cli.d.ts +6 -0
- package/dist/src/cli.js +462 -32
- package/dist/src/cli.test.js +209 -2
- package/dist/src/commanderAdapter.js +29 -9
- package/dist/src/commanderAdapter.test.js +78 -2
- package/dist/src/commands/daemon.js +6 -0
- package/dist/src/completion-shared.js +1 -2
- package/dist/src/completion.test.js +3 -2
- package/dist/src/daemon.js +125 -41
- package/dist/src/doctor.d.ts +4 -6
- package/dist/src/doctor.js +80 -22
- package/dist/src/doctor.test.js +82 -0
- package/dist/src/engine.test.js +6 -5
- package/dist/src/errors.d.ts +14 -8
- package/dist/src/errors.js +36 -30
- package/dist/src/errors.test.js +5 -5
- package/dist/src/execution.d.ts +4 -0
- package/dist/src/execution.js +173 -25
- package/dist/src/execution.test.js +171 -1
- package/dist/src/main.js +10 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/pipeline/steps/fetch.js +1 -21
- package/dist/src/pipeline/steps/fetch.test.js +6 -12
- package/dist/src/plugin-scaffold.js +1 -1
- package/dist/src/plugin-scaffold.test.js +1 -1
- package/dist/src/registry.d.ts +40 -9
- package/dist/src/registry.js +3 -1
- package/dist/src/runtime-detect.d.ts +10 -0
- package/dist/src/runtime-detect.js +19 -0
- package/dist/src/runtime-detect.test.js +12 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/update-check.d.ts +31 -1
- package/dist/src/update-check.js +62 -16
- package/dist/src/update-check.test.js +86 -1
- package/package.json +1 -1
- package/dist/src/diagnostic.d.ts +0 -63
- package/dist/src/diagnostic.js +0 -292
- package/dist/src/diagnostic.test.js +0 -302
- /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weibo publish — post a new Weibo update via browser UI automation.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Navigate to weibo.com and wait for the feed
|
|
6
|
+
* 2. Check login state (getSelfUid)
|
|
7
|
+
* 3. Click "发微博" button to open the inline compose editor
|
|
8
|
+
* 4. Wait for textarea editor to appear
|
|
9
|
+
* 5. Fill text content via CDP type
|
|
10
|
+
* 6. Optionally upload images via CDP setFileInput
|
|
11
|
+
* 7. Click the publish button
|
|
12
|
+
* 8. Poll for success/failure feedback
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* opencli weibo publish "Hello from OpenCLI! #opencli" # publishes immediately
|
|
16
|
+
* opencli weibo publish "Check this out" --images /path/a.jpg,/path/b.jpg
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
21
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
22
|
+
import { getSelfUid } from './utils.js';
|
|
23
|
+
|
|
24
|
+
const MAX_IMAGES = 9;
|
|
25
|
+
const UPLOAD_POLL_MS = 1500;
|
|
26
|
+
const UPLOAD_TIMEOUT_MS = 30_000;
|
|
27
|
+
const COMPOSE_POLL_MS = 300;
|
|
28
|
+
const COMPOSE_TIMEOUT_MS = 10_000;
|
|
29
|
+
const SUBMIT_POLL_MS = 500;
|
|
30
|
+
const SUBMIT_TIMEOUT_MS = 20_000;
|
|
31
|
+
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
|
|
32
|
+
|
|
33
|
+
// Weibo PC UI selectors
|
|
34
|
+
const TEXTAREA_SELECTOR = 'textarea._input_13iqr_8';
|
|
35
|
+
const FILE_INPUT_SELECTOR = 'input[type="file"][class*="_file_"]';
|
|
36
|
+
|
|
37
|
+
function validateText(text) {
|
|
38
|
+
const t = String(text ?? '').trim();
|
|
39
|
+
if (!t) throw new ArgumentError('weibo publish text cannot be empty');
|
|
40
|
+
if (t.length > 2000) throw new ArgumentError('weibo publish text exceeds 2000 characters');
|
|
41
|
+
return t;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateImagePaths(raw) {
|
|
45
|
+
if (!raw) return [];
|
|
46
|
+
const paths = raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
47
|
+
if (paths.length > MAX_IMAGES) {
|
|
48
|
+
throw new ArgumentError(`Too many images: ${paths.length} (max ${MAX_IMAGES})`);
|
|
49
|
+
}
|
|
50
|
+
return paths.map(p => {
|
|
51
|
+
const absPath = path.resolve(p);
|
|
52
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
53
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
54
|
+
throw new ArgumentError(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
|
|
55
|
+
}
|
|
56
|
+
const stat = fs.statSync(absPath, { throwIfNoEntry: false });
|
|
57
|
+
if (!stat || !stat.isFile()) {
|
|
58
|
+
throw new ArgumentError(`Not a valid file: ${absPath}`);
|
|
59
|
+
}
|
|
60
|
+
return absPath;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cli({
|
|
65
|
+
site: 'weibo',
|
|
66
|
+
name: 'publish',
|
|
67
|
+
description: 'Publish a new Weibo post immediately',
|
|
68
|
+
domain: 'weibo.com',
|
|
69
|
+
strategy: Strategy.UI,
|
|
70
|
+
browser: true,
|
|
71
|
+
args: [
|
|
72
|
+
{
|
|
73
|
+
name: 'text',
|
|
74
|
+
type: 'string',
|
|
75
|
+
required: true,
|
|
76
|
+
positional: true,
|
|
77
|
+
help: 'Weibo text content (max 2000 chars)',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'images',
|
|
81
|
+
type: 'string',
|
|
82
|
+
required: false,
|
|
83
|
+
help: `Image paths, comma-separated, max ${MAX_IMAGES} (jpg/png/gif/webp)`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
columns: ['status', 'message', 'text'],
|
|
87
|
+
func: async (page, kwargs) => {
|
|
88
|
+
if (!page) throw new CommandExecutionError('Browser session required for weibo publish');
|
|
89
|
+
|
|
90
|
+
const text = validateText(kwargs.text);
|
|
91
|
+
const absPaths = validateImagePaths(kwargs.images);
|
|
92
|
+
|
|
93
|
+
// Step 1: Navigate to weibo.com and wait for feed to load
|
|
94
|
+
await page.goto('https://weibo.com', { waitUntil: 'load', settleMs: 2000 });
|
|
95
|
+
await page.wait({ time: 2 });
|
|
96
|
+
|
|
97
|
+
// Step 2: Check login
|
|
98
|
+
try {
|
|
99
|
+
await getSelfUid(page);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
102
|
+
throw new CommandExecutionError('Not logged into Weibo. Please login at weibo.com in your Chrome browser.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 3: Click "发微博" button to open inline compose editor
|
|
106
|
+
const clickResult = await page.evaluate(`
|
|
107
|
+
() => {
|
|
108
|
+
const visible = el => !!el && el.offsetParent !== null && !el.disabled;
|
|
109
|
+
const buttons = document.querySelectorAll('button[title="发微博"], button[title="写微博"]');
|
|
110
|
+
for (const btn of buttons) {
|
|
111
|
+
if (visible(btn)) {
|
|
112
|
+
btn.click();
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { ok: false, message: 'Could not find 发微博 button' };
|
|
117
|
+
}
|
|
118
|
+
`);
|
|
119
|
+
if (!clickResult?.ok) {
|
|
120
|
+
throw new CommandExecutionError(clickResult?.message ?? 'Could not open compose editor.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 4: Wait for the textarea editor to appear (visible, not just in DOM)
|
|
124
|
+
let editorFound = false;
|
|
125
|
+
for (let i = 0; i < Math.ceil(COMPOSE_TIMEOUT_MS / COMPOSE_POLL_MS); i++) {
|
|
126
|
+
const result = await page.evaluate(`
|
|
127
|
+
() => {
|
|
128
|
+
const ta = document.querySelector('textarea._input_13iqr_8');
|
|
129
|
+
if (!ta) return { found: false };
|
|
130
|
+
const visible = ta.offsetParent !== null;
|
|
131
|
+
return { found: true, visible, rectTop: visible ? ta.getBoundingClientRect().top : -1 };
|
|
132
|
+
}
|
|
133
|
+
`);
|
|
134
|
+
if (result?.found && result.visible && result.rectTop >= 0) {
|
|
135
|
+
editorFound = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
await page.wait({ time: COMPOSE_POLL_MS / 1000 });
|
|
139
|
+
}
|
|
140
|
+
if (!editorFound) {
|
|
141
|
+
throw new CommandExecutionError('Weibo compose editor did not appear');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 5: Upload images first (before text to avoid editor reset)
|
|
145
|
+
if (absPaths.length > 0) {
|
|
146
|
+
if (!page.setFileInput) {
|
|
147
|
+
throw new CommandExecutionError('Browser extension does not support file upload. Please update the extension.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Find the file input
|
|
151
|
+
const fileInputFound = await page.evaluate(`
|
|
152
|
+
() => {
|
|
153
|
+
const input = document.querySelector('input[type="file"][class*="_file_"]');
|
|
154
|
+
return !!input;
|
|
155
|
+
}
|
|
156
|
+
`);
|
|
157
|
+
if (!fileInputFound) {
|
|
158
|
+
throw new CommandExecutionError('Could not find image file input on Weibo compose page. UI may have changed.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await page.setFileInput(absPaths, FILE_INPUT_SELECTOR);
|
|
162
|
+
|
|
163
|
+
// Wait for upload to complete
|
|
164
|
+
let uploadResult = null;
|
|
165
|
+
for (let i = 0; i < Math.ceil(UPLOAD_TIMEOUT_MS / UPLOAD_POLL_MS); i++) {
|
|
166
|
+
await page.wait({ time: UPLOAD_POLL_MS / 1000 });
|
|
167
|
+
uploadResult = await page.evaluateWithArgs(`
|
|
168
|
+
(() => {
|
|
169
|
+
const expectedCount = expected;
|
|
170
|
+
const uploading = document.querySelector('[class*="upload"], [class*="progress"]');
|
|
171
|
+
if (uploading && uploading.offsetParent !== null) return null;
|
|
172
|
+
const pics = document.querySelectorAll('img[class*="pic"], [class*="imgItem"], [class*="picture"] img');
|
|
173
|
+
if (pics.length >= expectedCount) return { ok: true, count: pics.length };
|
|
174
|
+
return null;
|
|
175
|
+
})()
|
|
176
|
+
`, { expected: absPaths.length });
|
|
177
|
+
if (uploadResult !== null) break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!uploadResult?.ok) {
|
|
181
|
+
throw new CommandExecutionError(uploadResult?.message ?? 'Image upload did not complete before timeout');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 6: Insert text using native DOM setter (preserves Weibo internal state)
|
|
186
|
+
// IMPORTANT: Using nativeSetter preserves the textarea's reactive/internal state.
|
|
187
|
+
// Direct ta.value= assignment bypasses Weibo's Vue reactivity and causes "undefined" content.
|
|
188
|
+
const insertResult = await page.evaluateWithArgs(`
|
|
189
|
+
(() => {
|
|
190
|
+
const ta = document.querySelector('textarea._input_13iqr_8');
|
|
191
|
+
if (!ta || ta.offsetParent === null) return { ok: false, message: 'textarea not visible' };
|
|
192
|
+
ta.focus();
|
|
193
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
194
|
+
if (nativeSetter) {
|
|
195
|
+
nativeSetter.call(ta, textContent);
|
|
196
|
+
} else {
|
|
197
|
+
ta.value = textContent;
|
|
198
|
+
}
|
|
199
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
200
|
+
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
201
|
+
return { ok: true, valueLength: ta.value.length };
|
|
202
|
+
})()
|
|
203
|
+
`, { textContent: text });
|
|
204
|
+
|
|
205
|
+
if (!insertResult?.ok) {
|
|
206
|
+
throw new CommandExecutionError(insertResult?.message ?? 'Could not insert text.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
// Step 7: Click the send button inside the compose editor
|
|
211
|
+
// Try 发送 first (compose editor's submit), then 发布 (fallback)
|
|
212
|
+
await page.wait({ time: 0.5 });
|
|
213
|
+
const publishResult = await page.evaluate(`
|
|
214
|
+
() => {
|
|
215
|
+
const visible = el => !!el && el.offsetParent !== null && !el.disabled;
|
|
216
|
+
const labels = ['发送', '发布'];
|
|
217
|
+
for (const label of labels) {
|
|
218
|
+
const allBtns = document.querySelectorAll('button, [role="button"]');
|
|
219
|
+
for (const btn of allBtns) {
|
|
220
|
+
const t = (btn.innerText || btn.textContent || '').trim();
|
|
221
|
+
if (t === label && visible(btn)) {
|
|
222
|
+
btn.click();
|
|
223
|
+
return { ok: true, label };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { ok: false, message: 'Could not find send button' };
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
if (!publishResult?.ok) {
|
|
231
|
+
throw new CommandExecutionError(publishResult?.message ?? 'Could not click publish.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Step 8: Wait for success/failure result
|
|
235
|
+
let finalResult = null;
|
|
236
|
+
for (let i = 0; i < Math.ceil(SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS); i++) {
|
|
237
|
+
await page.wait({ time: SUBMIT_POLL_MS / 1000 });
|
|
238
|
+
finalResult = await page.evaluateWithArgs(`
|
|
239
|
+
(() => {
|
|
240
|
+
const successMarkers = ['发布成功', '已发布', '发送成功'];
|
|
241
|
+
const errorMarkers = ['发布失败', '发送失败', '内容违规', '请稍后再试', '频繁'];
|
|
242
|
+
for (const el of document.querySelectorAll('*')) {
|
|
243
|
+
if (el.children.length > 3) continue;
|
|
244
|
+
const txt = (el.innerText || '').trim();
|
|
245
|
+
if (!txt || txt.length > 100) continue;
|
|
246
|
+
for (const m of successMarkers) {
|
|
247
|
+
if (txt.includes(m) && (txt.includes('成功') || txt.includes('微博'))) {
|
|
248
|
+
return { ok: true, message: txt };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const m of errorMarkers) {
|
|
252
|
+
if (txt.includes(m)) {
|
|
253
|
+
return { ok: false, message: txt };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
})()
|
|
259
|
+
`, { maxIterations: Math.ceil(SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS), currentIndex: i });
|
|
260
|
+
if (finalResult !== null) break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!finalResult) {
|
|
264
|
+
throw new CommandExecutionError('Publish button clicked but result was unclear. Check Weibo manually.');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!finalResult.ok) {
|
|
268
|
+
throw new CommandExecutionError(finalResult.message || 'Weibo publish failed');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return [{
|
|
272
|
+
status: 'success',
|
|
273
|
+
message: finalResult.message || 'Published successfully',
|
|
274
|
+
text,
|
|
275
|
+
}];
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export const __test__ = {
|
|
280
|
+
validateText,
|
|
281
|
+
validateImagePaths,
|
|
282
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
|
|
5
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
statSync: vi.fn((p) => {
|
|
10
|
+
if (String(p).includes('missing')) return undefined;
|
|
11
|
+
return { isFile: () => !String(p).includes('directory') };
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('node:path', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
resolve: vi.fn((p) => `/abs/${p}`),
|
|
21
|
+
extname: vi.fn((p) => {
|
|
22
|
+
const m = String(p).match(/\.[^.]+$/);
|
|
23
|
+
return m ? m[0] : '';
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
import './publish.js';
|
|
29
|
+
|
|
30
|
+
function makePage({ evaluateResults = [], evaluateWithArgsResults = [], overrides = {} } = {}) {
|
|
31
|
+
const evaluate = vi.fn();
|
|
32
|
+
for (const result of evaluateResults) {
|
|
33
|
+
evaluate.mockResolvedValueOnce(result);
|
|
34
|
+
}
|
|
35
|
+
evaluate.mockResolvedValue({ ok: true });
|
|
36
|
+
|
|
37
|
+
const evaluateWithArgs = vi.fn();
|
|
38
|
+
for (const result of evaluateWithArgsResults) {
|
|
39
|
+
evaluateWithArgs.mockResolvedValueOnce(result);
|
|
40
|
+
}
|
|
41
|
+
evaluateWithArgs.mockResolvedValue(null);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
evaluate,
|
|
47
|
+
evaluateWithArgs,
|
|
48
|
+
setFileInput: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('weibo publish command', () => {
|
|
54
|
+
const getCommand = () => getRegistry().get('weibo/publish');
|
|
55
|
+
|
|
56
|
+
it('publishes a text-only post when the UI reports success', async () => {
|
|
57
|
+
const command = getCommand();
|
|
58
|
+
const page = makePage({
|
|
59
|
+
evaluateResults: [
|
|
60
|
+
'123456',
|
|
61
|
+
{ ok: true },
|
|
62
|
+
{ found: true, visible: true, rectTop: 100 },
|
|
63
|
+
{ ok: true, label: '发送' },
|
|
64
|
+
],
|
|
65
|
+
evaluateWithArgsResults: [
|
|
66
|
+
{ ok: true, valueLength: 5 },
|
|
67
|
+
{ ok: true, message: '发送成功' },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = await command.func(page, { text: 'hello' });
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual([{ status: 'success', message: '发送成功', text: 'hello' }]);
|
|
74
|
+
expect(page.goto).toHaveBeenCalledWith('https://weibo.com', { waitUntil: 'load', settleMs: 2000 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('uploads up to nine images before publishing', async () => {
|
|
78
|
+
const command = getCommand();
|
|
79
|
+
const page = makePage({
|
|
80
|
+
evaluateResults: [
|
|
81
|
+
'123456',
|
|
82
|
+
{ ok: true },
|
|
83
|
+
{ found: true, visible: true, rectTop: 100 },
|
|
84
|
+
true,
|
|
85
|
+
{ ok: true, label: '发送' },
|
|
86
|
+
],
|
|
87
|
+
evaluateWithArgsResults: [
|
|
88
|
+
{ ok: true, count: 2 },
|
|
89
|
+
{ ok: true, valueLength: 11 },
|
|
90
|
+
{ ok: true, message: '发送成功' },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await command.func(page, { text: 'with images', images: 'a.png,b.webp' });
|
|
95
|
+
|
|
96
|
+
expect(page.setFileInput).toHaveBeenCalledWith(
|
|
97
|
+
['/abs/a.png', '/abs/b.webp'],
|
|
98
|
+
'input[type="file"][class*="_file_"]',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('maps auth failures to AuthRequiredError', async () => {
|
|
103
|
+
const command = getCommand();
|
|
104
|
+
const page = makePage({ evaluateResults: [null, null] });
|
|
105
|
+
|
|
106
|
+
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('validates text and image arguments before navigation', async () => {
|
|
110
|
+
const command = getCommand();
|
|
111
|
+
const page = makePage();
|
|
112
|
+
|
|
113
|
+
await expect(command.func(page, { text: ' ' })).rejects.toBeInstanceOf(ArgumentError);
|
|
114
|
+
await expect(command.func(page, { text: 'hi', images: 'a.bmp' })).rejects.toBeInstanceOf(ArgumentError);
|
|
115
|
+
await expect(command.func(page, { text: 'hi', images: 'missing.png' })).rejects.toBeInstanceOf(ArgumentError);
|
|
116
|
+
await expect(command.func(page, { text: 'hi', images: '1.png,2.png,3.png,4.png,5.png,6.png,7.png,8.png,9.png,10.png' })).rejects.toBeInstanceOf(ArgumentError);
|
|
117
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('throws CommandExecutionError when compose cannot be opened', async () => {
|
|
121
|
+
const command = getCommand();
|
|
122
|
+
const page = makePage({
|
|
123
|
+
evaluateResults: ['123456', { ok: false, message: 'Could not find 发微博 button' }],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('throws CommandExecutionError when upload readiness is not proven', async () => {
|
|
130
|
+
const command = getCommand();
|
|
131
|
+
const page = makePage({
|
|
132
|
+
evaluateResults: [
|
|
133
|
+
'123456',
|
|
134
|
+
{ ok: true },
|
|
135
|
+
{ found: true, visible: true, rectTop: 100 },
|
|
136
|
+
true,
|
|
137
|
+
],
|
|
138
|
+
evaluateWithArgsResults: [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await expect(command.func(page, { text: 'hello', images: 'a.png' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('throws CommandExecutionError when publish result is unclear or failed', async () => {
|
|
145
|
+
const command = getCommand();
|
|
146
|
+
const page = makePage({
|
|
147
|
+
evaluateResults: [
|
|
148
|
+
'123456',
|
|
149
|
+
{ ok: true },
|
|
150
|
+
{ found: true, visible: true, rectTop: 100 },
|
|
151
|
+
{ ok: true, label: '发送' },
|
|
152
|
+
],
|
|
153
|
+
evaluateWithArgsResults: [
|
|
154
|
+
{ ok: true, valueLength: 5 },
|
|
155
|
+
{ ok: false, message: '内容违规' },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does not treat editor close as positive publish proof', async () => {
|
|
163
|
+
const command = getCommand();
|
|
164
|
+
const page = makePage({
|
|
165
|
+
evaluateResults: [
|
|
166
|
+
'123456',
|
|
167
|
+
{ ok: true },
|
|
168
|
+
{ found: true, visible: true, rectTop: 100 },
|
|
169
|
+
{ ok: true, label: '发送' },
|
|
170
|
+
],
|
|
171
|
+
evaluateWithArgsResults: [
|
|
172
|
+
{ ok: true, valueLength: 5 },
|
|
173
|
+
null,
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
178
|
+
|
|
179
|
+
const submitScript = page.evaluateWithArgs.mock.calls.at(-1)[0];
|
|
180
|
+
expect(submitScript).not.toContain('Editor closed after publish');
|
|
181
|
+
expect(submitScript).toContain('发布成功');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
|
|
4
|
+
const WEIXIN_DOMAIN = 'mp.weixin.qq.com';
|
|
5
|
+
const WEIXIN_HOME = 'https://mp.weixin.qq.com/';
|
|
6
|
+
|
|
7
|
+
async function getToken(page) {
|
|
8
|
+
return page.evaluate(`(window.location.href.match(/token=(\\d+)/)||[])[1]`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function navigateToEditor(page) {
|
|
12
|
+
await page.goto(WEIXIN_HOME);
|
|
13
|
+
await page.wait(3);
|
|
14
|
+
const token = await getToken(page);
|
|
15
|
+
if (!token) {
|
|
16
|
+
throw new CommandExecutionError('Could not extract session token. Please log in to mp.weixin.qq.com');
|
|
17
|
+
}
|
|
18
|
+
await page.goto(`https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=77&token=${token}&lang=zh_CN`);
|
|
19
|
+
await page.wait(4);
|
|
20
|
+
const hasTitle = await page.evaluate('!!document.querySelector("textarea#title")');
|
|
21
|
+
if (!hasTitle) {
|
|
22
|
+
throw new CommandExecutionError('Article editor did not load. Session may have expired');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fillField(page, selector, value) {
|
|
27
|
+
return page.evaluate(`(() => {
|
|
28
|
+
var el = document.querySelector('${selector}');
|
|
29
|
+
if (!el) return { ok: false, reason: 'not found: ${selector}' };
|
|
30
|
+
el.focus();
|
|
31
|
+
var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
32
|
+
var setter = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
33
|
+
if (setter && setter.set) setter.set.call(el, ${JSON.stringify(value)});
|
|
34
|
+
else el.value = ${JSON.stringify(value)};
|
|
35
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${JSON.stringify(value)} }));
|
|
36
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
37
|
+
el.blur();
|
|
38
|
+
return { ok: true };
|
|
39
|
+
})()`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fillContent(page, text) {
|
|
43
|
+
return page.evaluate(`(() => {
|
|
44
|
+
var editors = document.querySelectorAll('div[contenteditable="true"]');
|
|
45
|
+
var editor = editors[editors.length - 1];
|
|
46
|
+
if (!editor) return { ok: false, reason: 'content editor not found' };
|
|
47
|
+
editor.focus();
|
|
48
|
+
if (editor.querySelector('[contenteditable="false"]')) editor.innerHTML = '';
|
|
49
|
+
document.execCommand('selectAll', false, null);
|
|
50
|
+
document.execCommand('insertText', false, ${JSON.stringify(text)});
|
|
51
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
52
|
+
return { ok: true };
|
|
53
|
+
})()`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function uploadContentImage(page, imagePath) {
|
|
57
|
+
const fs = await import('node:fs');
|
|
58
|
+
const path = await import('node:path');
|
|
59
|
+
const absPath = path.default.resolve(imagePath);
|
|
60
|
+
if (!fs.default.existsSync(absPath)) {
|
|
61
|
+
throw new CommandExecutionError(`Image not found: ${absPath}`);
|
|
62
|
+
}
|
|
63
|
+
if (!page.setFileInput) {
|
|
64
|
+
throw new CommandExecutionError('Image upload requires Browser Bridge with CDP support');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await page.evaluate(`(() => {
|
|
68
|
+
var li = document.querySelector('#js_editor_insertimage');
|
|
69
|
+
if (li) li.click();
|
|
70
|
+
})()`);
|
|
71
|
+
await page.wait(1);
|
|
72
|
+
await page.evaluate(`(() => {
|
|
73
|
+
var items = document.querySelectorAll('.js_img_dropdown_menu .tpl_dropdown_menu_item');
|
|
74
|
+
if (items[0]) items[0].click();
|
|
75
|
+
})()`);
|
|
76
|
+
await page.wait(1);
|
|
77
|
+
|
|
78
|
+
await page.setFileInput([absPath], 'input[type="file"][name="file"]');
|
|
79
|
+
await page.wait(8);
|
|
80
|
+
|
|
81
|
+
const cdnCount = await page.evaluate(`(() => {
|
|
82
|
+
var editor = document.querySelector('#ueditor_0');
|
|
83
|
+
return editor ? editor.querySelectorAll('img[src*="mmbiz"]').length : 0;
|
|
84
|
+
})()`);
|
|
85
|
+
if (cdnCount === 0) {
|
|
86
|
+
throw new CommandExecutionError('Image did not upload to WeChat CDN');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function selectCoverFromContent(page) {
|
|
91
|
+
await page.evaluate('document.querySelector("#js_cover_description_area")?.scrollIntoView()');
|
|
92
|
+
await page.wait(1);
|
|
93
|
+
|
|
94
|
+
await page.evaluate('document.querySelector(".js_cover_btn_area")?.click()');
|
|
95
|
+
await page.wait(1);
|
|
96
|
+
|
|
97
|
+
await page.evaluate(`(() => {
|
|
98
|
+
var links = document.querySelectorAll('a.pop-opr__button');
|
|
99
|
+
for (var i = 0; i < links.length; i++) {
|
|
100
|
+
if (links[i].textContent.trim() === '从正文选择') { links[i].click(); return; }
|
|
101
|
+
}
|
|
102
|
+
})()`);
|
|
103
|
+
await page.wait(2);
|
|
104
|
+
|
|
105
|
+
await page.evaluate(`(() => {
|
|
106
|
+
var img = document.querySelector('.weui-desktop-dialog_img-picker .appmsg_content_img');
|
|
107
|
+
if (img) img.click();
|
|
108
|
+
})()`);
|
|
109
|
+
await page.wait(1);
|
|
110
|
+
|
|
111
|
+
await page.evaluate(`(() => {
|
|
112
|
+
var btns = document.querySelectorAll('.weui-desktop-dialog_img-picker button');
|
|
113
|
+
for (var i = 0; i < btns.length; i++) {
|
|
114
|
+
if (btns[i].textContent.trim() === '下一步' && !btns[i].disabled) { btns[i].click(); return; }
|
|
115
|
+
}
|
|
116
|
+
})()`);
|
|
117
|
+
|
|
118
|
+
// Crop dialog image rendering can be slow
|
|
119
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
120
|
+
await page.wait(2);
|
|
121
|
+
const ready = await page.evaluate(`(() => {
|
|
122
|
+
var btns = document.querySelectorAll('button');
|
|
123
|
+
for (var i = 0; i < btns.length; i++) {
|
|
124
|
+
if (btns[i].textContent.trim() === '确认' && btns[i].offsetHeight > 0 && !btns[i].disabled) return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
})()`);
|
|
128
|
+
if (ready) break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await page.evaluate(`(() => {
|
|
132
|
+
var btns = document.querySelectorAll('button');
|
|
133
|
+
for (var i = 0; i < btns.length; i++) {
|
|
134
|
+
if (btns[i].textContent.trim() === '确认' && btns[i].offsetHeight > 0 && !btns[i].disabled) { btns[i].click(); return; }
|
|
135
|
+
}
|
|
136
|
+
})()`);
|
|
137
|
+
await page.wait(2);
|
|
138
|
+
const hasCover = await page.evaluate(`(() => {
|
|
139
|
+
var area = document.querySelector('#js_cover_area');
|
|
140
|
+
if (!area) return false;
|
|
141
|
+
var found = false;
|
|
142
|
+
area.querySelectorAll('*').forEach(function(el) {
|
|
143
|
+
var bg = window.getComputedStyle(el).backgroundImage;
|
|
144
|
+
if (bg && bg.includes('mmbiz')) found = true;
|
|
145
|
+
});
|
|
146
|
+
return found;
|
|
147
|
+
})()`);
|
|
148
|
+
return hasCover;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function clickSaveDraft(page) {
|
|
152
|
+
const result = await page.evaluate(`(() => {
|
|
153
|
+
var btns = document.querySelectorAll('span, button, a');
|
|
154
|
+
for (var i = 0; i < btns.length; i++) {
|
|
155
|
+
if ((btns[i].textContent || '').trim() === '保存为草稿') { btns[i].click(); return { ok: true }; }
|
|
156
|
+
}
|
|
157
|
+
return { ok: false };
|
|
158
|
+
})()`);
|
|
159
|
+
if (!result?.ok) throw new CommandExecutionError('Save draft button not found');
|
|
160
|
+
|
|
161
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
162
|
+
await page.wait(2);
|
|
163
|
+
const saved = await page.evaluate(`(() => {
|
|
164
|
+
var el = document.querySelector('#js_save_success');
|
|
165
|
+
if (el && window.getComputedStyle(el).display !== 'none') return true;
|
|
166
|
+
return document.body.innerText.includes('已保存');
|
|
167
|
+
})()`);
|
|
168
|
+
if (saved) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const createDraftCommand = cli({
|
|
174
|
+
site: 'weixin',
|
|
175
|
+
name: 'create-draft',
|
|
176
|
+
description: '创建微信公众号图文草稿',
|
|
177
|
+
domain: WEIXIN_DOMAIN,
|
|
178
|
+
strategy: Strategy.COOKIE,
|
|
179
|
+
browser: true,
|
|
180
|
+
navigateBefore: false,
|
|
181
|
+
timeoutSeconds: 180,
|
|
182
|
+
args: [
|
|
183
|
+
{ name: 'title', required: true, help: '文章标题 (最长64字)' },
|
|
184
|
+
{ name: 'content', required: true, positional: true, help: '文章正文' },
|
|
185
|
+
{ name: 'author', help: '作者名 (最长8字)' },
|
|
186
|
+
{ name: 'cover-image', help: '封面图片路径 (会先上传到正文再设为封面)' },
|
|
187
|
+
{ name: 'summary', help: '文章摘要' },
|
|
188
|
+
],
|
|
189
|
+
columns: ['status', 'detail'],
|
|
190
|
+
|
|
191
|
+
func: async (page, kwargs) => {
|
|
192
|
+
await navigateToEditor(page);
|
|
193
|
+
|
|
194
|
+
const titleResult = await fillField(page, 'textarea#title', kwargs.title);
|
|
195
|
+
if (!titleResult?.ok) throw new CommandExecutionError('Failed to fill title');
|
|
196
|
+
|
|
197
|
+
if (kwargs.author) {
|
|
198
|
+
const authorResult = await fillField(page, 'input#author', kwargs.author);
|
|
199
|
+
if (!authorResult?.ok) throw new CommandExecutionError('Failed to fill author');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const contentResult = await fillContent(page, kwargs.content);
|
|
203
|
+
if (!contentResult?.ok) throw new CommandExecutionError('Failed to fill content');
|
|
204
|
+
|
|
205
|
+
if (kwargs['cover-image']) {
|
|
206
|
+
await uploadContentImage(page, kwargs['cover-image']);
|
|
207
|
+
const coverSet = await selectCoverFromContent(page);
|
|
208
|
+
if (!coverSet) {
|
|
209
|
+
// Non-fatal: draft can be saved without cover
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (kwargs.summary) {
|
|
214
|
+
await fillField(page, 'textarea#js_description', kwargs.summary);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await page.wait(1);
|
|
218
|
+
const success = await clickSaveDraft(page);
|
|
219
|
+
|
|
220
|
+
return [{
|
|
221
|
+
status: success ? 'draft saved' : 'save attempted, check browser to confirm',
|
|
222
|
+
detail: `"${kwargs.title}"${kwargs.author ? ` by ${kwargs.author}` : ''}${kwargs['cover-image'] ? ' (with cover)' : ''}`,
|
|
223
|
+
}];
|
|
224
|
+
},
|
|
225
|
+
});
|