@xbrowser/medium 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.ts +296 -0
  2. package/package.json +24 -0
package/index.ts ADDED
@@ -0,0 +1,296 @@
1
+ import { z } from 'zod';
2
+ import type { XCLIAPI } from '@dyyz1993/xcli-core';
3
+ import { ok, fail } from '@dyyz1993/xcli-core';
4
+
5
+ export default function (xcli: XCLIAPI): void {
6
+ const site = xcli.createSite({
7
+ name: 'medium',
8
+ url: 'https://medium.com',
9
+ description: 'Medium SEO 外链 - 内容平台 (DA 96, nofollow, 241M 月流量)',
10
+ requiresLogin: true,
11
+ });
12
+
13
+ site.command('login', {
14
+ description: '登录 Medium(Google / 邮箱)',
15
+ scope: 'browser',
16
+ parameters: z.object({}),
17
+ examples: [{ cmd: 'xbrowser medium login', description: '登录 Medium' }],
18
+ result: z.any(),
19
+ handler: async (params, ctx) => {
20
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
21
+ if (!page) throw new Error('需要浏览器页面上下文');
22
+
23
+ await page.goto('https://medium.com/m/signin', {
24
+ waitUntil: 'domcontentloaded',
25
+ timeout: 15000,
26
+ });
27
+ await page.waitForTimeout(2000);
28
+
29
+ await ctx.waitForHuman?.({
30
+ reason: 'Complete Medium login (Google OAuth or email)',
31
+ timeout: 300,
32
+ });
33
+
34
+ await page.goto('https://medium.com/', {
35
+ waitUntil: 'domcontentloaded',
36
+ timeout: 15000,
37
+ });
38
+ await page.waitForTimeout(2000);
39
+
40
+ const loggedIn = await page
41
+ .locator(
42
+ 'a[href*="/me"], [data-testid="header-user-menu"], button[aria-label*="User"], img[alt*="avatar"]'
43
+ )
44
+ .first()
45
+ .isVisible()
46
+ .catch(() => false);
47
+
48
+ await ctx.storage.set('medium_login', { loggedIn, at: Date.now() });
49
+
50
+ return ok({ loggedIn, url: page.url() }, [loggedIn ? 'Medium 登录成功' : '登录可能未完成,请检查页面']);
51
+ },
52
+ });
53
+
54
+ site.command('publish', {
55
+ description: '在 Medium 发布文章(含外链)',
56
+ scope: 'page',
57
+ parameters: z.object({
58
+ title: z.string().describe('文章标题'),
59
+ content: z.string().describe('文章内容(纯文本或 Markdown)'),
60
+ }),
61
+ examples: [
62
+ {
63
+ cmd: 'xbrowser medium publish --title "My Guide" --content "Check out https://example.com for more"',
64
+ description: '发布带外链的文章',
65
+ },
66
+ ],
67
+ result: z.any(),
68
+ handler: async (params, ctx) => {
69
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
70
+ if (!page) throw new Error('需要浏览器页面上下文');
71
+
72
+ await page.goto('https://medium.com/new-story', {
73
+ waitUntil: 'domcontentloaded',
74
+ timeout: 20000,
75
+ });
76
+ await page.waitForLoadState('networkidle');
77
+ await page.waitForTimeout(4000);
78
+
79
+ await page.waitForSelector('[contenteditable="true"]', { timeout: 10000 }).catch(() => {});
80
+
81
+ const titleSection = page.locator(
82
+ 'section [contenteditable="true"]'
83
+ ).first();
84
+ if (await titleSection.isVisible().catch(() => false)) {
85
+ await titleSection.click();
86
+ await page.keyboard.type(params.title);
87
+ await page.keyboard.press('Enter');
88
+ }
89
+
90
+ await page.waitForTimeout(500);
91
+
92
+ await page.keyboard.insertText(params.content);
93
+
94
+ await ctx.waitForHuman?.({
95
+ reason: 'Review and publish article (resolve CAPTCHA if present)',
96
+ timeout: 120,
97
+ autoDetect: true,
98
+ });
99
+
100
+ const publishBtn = page.locator(
101
+ 'button:has-text("Publish"), button[data-testid="publish-button"], button[aria-label="Publish"]'
102
+ ).first();
103
+ if (await publishBtn.isVisible().catch(() => false)) {
104
+ await publishBtn.click();
105
+ await page.waitForTimeout(2000);
106
+
107
+ const confirmBtn = page.locator(
108
+ 'button:has-text("Publish now"), button:has-text("Publish")'
109
+ ).last();
110
+ if (await confirmBtn.isVisible().catch(() => false)) {
111
+ await confirmBtn.click();
112
+ await page.waitForTimeout(3000);
113
+ }
114
+ }
115
+
116
+ return ok({
117
+ title: params.title,
118
+ url: page.url(),
119
+ }, [`文章 "${params.title}" 已在 Medium 发布`]);
120
+ },
121
+ });
122
+
123
+ site.command('draft', {
124
+ description: '在 Medium 保存草稿',
125
+ scope: 'page',
126
+ parameters: z.object({
127
+ title: z.string().describe('文章标题'),
128
+ content: z.string().describe('文章内容'),
129
+ }),
130
+ examples: [
131
+ {
132
+ cmd: 'xbrowser medium draft --title "Draft" --content "Draft content"',
133
+ description: '保存为草稿',
134
+ },
135
+ ],
136
+ result: z.any(),
137
+ handler: async (params, ctx) => {
138
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
139
+ if (!page) throw new Error('需要浏览器页面上下文');
140
+
141
+ await page.goto('https://medium.com/new-story', {
142
+ waitUntil: 'domcontentloaded',
143
+ timeout: 20000,
144
+ });
145
+ await page.waitForLoadState('networkidle');
146
+ await page.waitForTimeout(4000);
147
+
148
+ await page.waitForSelector('[contenteditable="true"]', { timeout: 10000 }).catch(() => {});
149
+
150
+ const titleSection = page.locator(
151
+ 'section [contenteditable="true"]'
152
+ ).first();
153
+ if (await titleSection.isVisible().catch(() => false)) {
154
+ await titleSection.click();
155
+ await page.keyboard.type(params.title);
156
+ await page.keyboard.press('Enter');
157
+ }
158
+
159
+ await page.waitForTimeout(500);
160
+
161
+ await page.keyboard.insertText(params.content);
162
+
163
+ const saveBtn = page.locator(
164
+ 'button:has-text("Save"), button[aria-label="Save"]'
165
+ ).first();
166
+ if (await saveBtn.isVisible().catch(() => false)) {
167
+ await saveBtn.click();
168
+ await page.waitForTimeout(2000);
169
+ }
170
+
171
+ return ok({
172
+ title: params.title,
173
+ saved: true,
174
+ url: page.url(),
175
+ }, [`草稿 "${params.title}" 已保存`]);
176
+ },
177
+ });
178
+
179
+ site.command('import', {
180
+ description: '在 Medium 导入文章(设置 canonical URL)',
181
+ scope: 'page',
182
+ parameters: z.object({
183
+ url: z.string().describe('要导入的文章 URL(设置 canonical)'),
184
+ }),
185
+ examples: [
186
+ {
187
+ cmd: 'xbrowser medium import --url "https://example.com/my-article"',
188
+ description: '导入文章并设置 canonical',
189
+ },
190
+ ],
191
+ result: z.any(),
192
+ handler: async (params, ctx) => {
193
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
194
+ if (!page) throw new Error('需要浏览器页面上下文');
195
+
196
+ await page.goto('https://medium.com/p/import', {
197
+ waitUntil: 'domcontentloaded',
198
+ timeout: 15000,
199
+ });
200
+ await page.waitForLoadState('networkidle');
201
+ await page.waitForTimeout(3000);
202
+
203
+ const urlInput = page.locator(
204
+ 'input[placeholder*="URL"], input[placeholder*="url"], input[name*="url"], input[type="url"]'
205
+ ).first();
206
+ if (await urlInput.isVisible().catch(() => false)) {
207
+ await urlInput.fill(params.url);
208
+ await page.keyboard.press('Enter');
209
+ await page.waitForTimeout(5000);
210
+ }
211
+
212
+ await ctx.waitForHuman?.({
213
+ reason: 'Review imported article and publish',
214
+ timeout: 120,
215
+ autoDetect: true,
216
+ });
217
+
218
+ const publishBtn = page.locator(
219
+ 'button:has-text("Publish"), button:has-text("Publish now")'
220
+ ).first();
221
+ if (await publishBtn.isVisible().catch(() => false)) {
222
+ await publishBtn.click();
223
+ await page.waitForTimeout(3000);
224
+ }
225
+
226
+ return ok({
227
+ importedFrom: params.url,
228
+ url: page.url(),
229
+ }, [`文章已从 ${params.url} 导入到 Medium(canonical 已设置)`]);
230
+ },
231
+ });
232
+
233
+ site.command('update-profile', {
234
+ description: '更新 Medium 个人资料(添加外链)',
235
+ scope: 'browser',
236
+ parameters: z.object({
237
+ url: z.string().describe('要添加到 Profile 的 URL'),
238
+ bio: z.string().optional().describe('个人简介文本'),
239
+ }),
240
+ examples: [
241
+ {
242
+ cmd: 'xbrowser medium update-profile --url "https://example.com" --bio "Developer"',
243
+ description: '更新 Profile 添加外链',
244
+ },
245
+ ],
246
+ result: z.any(),
247
+ handler: async (params, ctx) => {
248
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
249
+ if (!page) throw new Error('需要浏览器页面上下文');
250
+
251
+ await page.goto('https://medium.com/me/settings', {
252
+ waitUntil: 'domcontentloaded',
253
+ timeout: 15000,
254
+ });
255
+ await page.waitForLoadState('networkidle');
256
+ await page.waitForTimeout(2000);
257
+
258
+ if (params.bio) {
259
+ const bioInput = page.locator(
260
+ 'textarea[name*="bio"], textarea[aria-label*="Bio"], textarea[placeholder*="bio"], textarea[placeholder*="About"]'
261
+ ).first();
262
+ if (await bioInput.isVisible().catch(() => false)) {
263
+ await bioInput.fill(`${params.bio}\n\n${params.url}`);
264
+ }
265
+ }
266
+
267
+ const webInput = page.locator(
268
+ 'input[name*="url"], input[aria-label*="Website"], input[placeholder*="website"], input[placeholder*="URL"]'
269
+ ).first();
270
+ if (await webInput.isVisible().catch(() => false)) {
271
+ await webInput.fill(params.url);
272
+ }
273
+
274
+ const submitBtn = page.locator(
275
+ 'button:has-text("Save"), button:has-text("保存"), button[type="submit"]'
276
+ ).first();
277
+ if (await submitBtn.isVisible().catch(() => false)) {
278
+ await submitBtn.click();
279
+ await page.waitForTimeout(2000);
280
+ }
281
+
282
+ return ok({ url: params.url, updated: true }, ['Profile 已更新,包含外链']);
283
+ },
284
+ });
285
+
286
+ site.login(async (ctx) => {
287
+ const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
288
+ if (!page) return;
289
+ await page.goto('https://medium.com/m/signin');
290
+ await ctx.storage.set('medium_login', { at: Date.now() });
291
+ });
292
+
293
+ site.logout(async (ctx) => {
294
+ await ctx.storage.delete('medium_login');
295
+ });
296
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@xbrowser/medium",
3
+ "version": "1.0.0",
4
+ "description": "Medium SEO 外链 - 内容平台 (DA 96, nofollow, 241M 月流量)",
5
+ "main": "index.ts",
6
+ "keywords": [
7
+ "xbrowser",
8
+ "xbrowser-plugin",
9
+ "medium.com"
10
+ ],
11
+ "author": "dyyz1993",
12
+ "license": "MIT",
13
+ "xbrowser": {
14
+ "site": "https://medium.com",
15
+ "requiresLogin": true,
16
+ "commands": [
17
+ "login",
18
+ "publish",
19
+ "draft",
20
+ "import",
21
+ "update-profile"
22
+ ]
23
+ }
24
+ }