@xbrowser/quora 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.
- package/index.ts +241 -0
- package/package.json +23 -0
package/index.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
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: 'quora',
|
|
8
|
+
url: 'https://www.quora.com',
|
|
9
|
+
description: 'Quora SEO 外链 - 问答平台 (DA 92, nofollow, 136M 月流量)',
|
|
10
|
+
requiresLogin: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
site.command('login', {
|
|
14
|
+
description: '登录 Quora(Google / 邮箱)',
|
|
15
|
+
scope: 'browser',
|
|
16
|
+
parameters: z.object({}),
|
|
17
|
+
examples: [{ cmd: 'xbrowser quora login', description: '登录 Quora' }],
|
|
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://www.quora.com/login', {
|
|
24
|
+
waitUntil: 'domcontentloaded',
|
|
25
|
+
timeout: 15000,
|
|
26
|
+
});
|
|
27
|
+
await page.waitForTimeout(2000);
|
|
28
|
+
|
|
29
|
+
await ctx.waitForHuman?.({
|
|
30
|
+
reason: 'Complete Quora login (Google OAuth or email)',
|
|
31
|
+
timeout: 300,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await page.goto('https://www.quora.com/', {
|
|
35
|
+
waitUntil: 'domcontentloaded',
|
|
36
|
+
timeout: 15000,
|
|
37
|
+
});
|
|
38
|
+
await page.waitForTimeout(2000);
|
|
39
|
+
|
|
40
|
+
const loggedIn = await page
|
|
41
|
+
.locator(
|
|
42
|
+
'a[href*="/logout"], button[aria-label*="Profile"], [class*="avatar"], [data-testid="user-menu"], img[class*="profile"]'
|
|
43
|
+
)
|
|
44
|
+
.first()
|
|
45
|
+
.isVisible()
|
|
46
|
+
.catch(() => false);
|
|
47
|
+
|
|
48
|
+
await ctx.storage.set('quora_login', { loggedIn, at: Date.now() });
|
|
49
|
+
|
|
50
|
+
return ok({ loggedIn, url: page.url() }, [loggedIn ? 'Quora 登录成功' : '登录可能未完成,请检查页面']);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
site.command('answer', {
|
|
55
|
+
description: '回答问题(含外链)',
|
|
56
|
+
scope: 'page',
|
|
57
|
+
parameters: z.object({
|
|
58
|
+
questionUrl: z.string().describe('问题页面 URL'),
|
|
59
|
+
content: z.string().describe('回答内容(含外链)'),
|
|
60
|
+
}),
|
|
61
|
+
examples: [
|
|
62
|
+
{
|
|
63
|
+
cmd: 'xbrowser quora answer --questionUrl "https://www.quora.com/What-is-the-best-tool" --content "I recommend https://example.com - it\'s the best tool"',
|
|
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(params.questionUrl, {
|
|
73
|
+
waitUntil: 'domcontentloaded',
|
|
74
|
+
timeout: 20000,
|
|
75
|
+
});
|
|
76
|
+
await page.waitForLoadState('networkidle');
|
|
77
|
+
await page.waitForTimeout(3000);
|
|
78
|
+
|
|
79
|
+
const answerBtn = page.locator(
|
|
80
|
+
'button:has-text("Answer"), a:has-text("Answer"), [data-testid="answer-button"], button[class*="Answer"]'
|
|
81
|
+
).first();
|
|
82
|
+
if (await answerBtn.isVisible().catch(() => false)) {
|
|
83
|
+
await answerBtn.click();
|
|
84
|
+
await page.waitForTimeout(2000);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const editor = page.locator(
|
|
88
|
+
'div[contenteditable="true"][class*="editor"], div[contenteditable="true"][data-testid*="answer"], div[class*="quill"], div[contenteditable="true"][role="textbox"]'
|
|
89
|
+
).first();
|
|
90
|
+
if (await editor.isVisible().catch(() => false)) {
|
|
91
|
+
await editor.click();
|
|
92
|
+
await page.keyboard.insertText(params.content);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await ctx.waitForHuman?.({
|
|
96
|
+
reason: 'Review answer and submit (resolve CAPTCHA if present)',
|
|
97
|
+
timeout: 120,
|
|
98
|
+
autoDetect: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const submitBtn = page.locator(
|
|
102
|
+
'button:has-text("Submit"), button:has-text("Post"), button:has-text("提交"), button[type="submit"]'
|
|
103
|
+
).first();
|
|
104
|
+
if (await submitBtn.isVisible().catch(() => false)) {
|
|
105
|
+
await submitBtn.click();
|
|
106
|
+
await page.waitForTimeout(3000);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return ok({
|
|
110
|
+
questionUrl: params.questionUrl,
|
|
111
|
+
answered: true,
|
|
112
|
+
url: page.url(),
|
|
113
|
+
}, [`回答已发布在 ${params.questionUrl}`]);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
site.command('publish-article', {
|
|
118
|
+
description: '创建 Quora 文章(Space 帖子)',
|
|
119
|
+
scope: 'page',
|
|
120
|
+
parameters: z.object({
|
|
121
|
+
title: z.string().describe('文章标题'),
|
|
122
|
+
content: z.string().describe('文章内容(含外链)'),
|
|
123
|
+
}),
|
|
124
|
+
examples: [
|
|
125
|
+
{
|
|
126
|
+
cmd: 'xbrowser quora publish-article --title "My Guide" --content "Check out https://example.com for details"',
|
|
127
|
+
description: '在 Quora 发布文章',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
result: z.any(),
|
|
131
|
+
handler: async (params, ctx) => {
|
|
132
|
+
const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
|
|
133
|
+
if (!page) throw new Error('需要浏览器页面上下文');
|
|
134
|
+
|
|
135
|
+
await page.goto('https://www.quora.com/content', {
|
|
136
|
+
waitUntil: 'domcontentloaded',
|
|
137
|
+
timeout: 20000,
|
|
138
|
+
});
|
|
139
|
+
await page.waitForLoadState('networkidle');
|
|
140
|
+
await page.waitForTimeout(3000);
|
|
141
|
+
|
|
142
|
+
const titleInput = page.locator(
|
|
143
|
+
'input[name*="title"], input[placeholder*="title"], input[placeholder*="Title"], input[class*="title"]'
|
|
144
|
+
).first();
|
|
145
|
+
if (await titleInput.isVisible().catch(() => false)) {
|
|
146
|
+
await titleInput.fill(params.title);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const editor = page.locator(
|
|
150
|
+
'div[contenteditable="true"][class*="editor"], div[contenteditable="true"][role="textbox"], div[class*="quill"]'
|
|
151
|
+
).first();
|
|
152
|
+
if (await editor.isVisible().catch(() => false)) {
|
|
153
|
+
await editor.click();
|
|
154
|
+
await page.keyboard.insertText(params.content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await ctx.waitForHuman?.({
|
|
158
|
+
reason: 'Review article and publish (resolve CAPTCHA if present)',
|
|
159
|
+
timeout: 120,
|
|
160
|
+
autoDetect: true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const publishBtn = page.locator(
|
|
164
|
+
'button:has-text("Publish"), button:has-text("Post"), button:has-text("发布"), button[type="submit"]'
|
|
165
|
+
).first();
|
|
166
|
+
if (await publishBtn.isVisible().catch(() => false)) {
|
|
167
|
+
await publishBtn.click();
|
|
168
|
+
await page.waitForTimeout(3000);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return ok({
|
|
172
|
+
title: params.title,
|
|
173
|
+
url: page.url(),
|
|
174
|
+
}, [`文章 "${params.title}" 已在 Quora 发布`]);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
site.command('update-profile', {
|
|
179
|
+
description: '更新 Quora 个人资料(添加外链)',
|
|
180
|
+
scope: 'browser',
|
|
181
|
+
parameters: z.object({
|
|
182
|
+
url: z.string().describe('要添加到 Profile 的网站 URL'),
|
|
183
|
+
bio: z.string().optional().describe('个人简介文本'),
|
|
184
|
+
}),
|
|
185
|
+
examples: [
|
|
186
|
+
{
|
|
187
|
+
cmd: 'xbrowser quora update-profile --url "https://example.com" --bio "Software developer"',
|
|
188
|
+
description: '更新 Profile 添加外链',
|
|
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://www.quora.com/settings', {
|
|
197
|
+
waitUntil: 'domcontentloaded',
|
|
198
|
+
timeout: 15000,
|
|
199
|
+
});
|
|
200
|
+
await page.waitForLoadState('networkidle');
|
|
201
|
+
await page.waitForTimeout(2000);
|
|
202
|
+
|
|
203
|
+
if (params.bio) {
|
|
204
|
+
const bioInput = page.locator(
|
|
205
|
+
'textarea[name*="bio"], textarea[aria-label*="Bio"], textarea[placeholder*="bio"], textarea[placeholder*="About"]'
|
|
206
|
+
).first();
|
|
207
|
+
if (await bioInput.isVisible().catch(() => false)) {
|
|
208
|
+
await bioInput.fill(`${params.bio}\n\n${params.url}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const webInput = page.locator(
|
|
213
|
+
'input[name*="url"], input[aria-label*="Website"], input[placeholder*="website"], input[placeholder*="URL"]'
|
|
214
|
+
).first();
|
|
215
|
+
if (await webInput.isVisible().catch(() => false)) {
|
|
216
|
+
await webInput.fill(params.url);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const submitBtn = page.locator(
|
|
220
|
+
'button:has-text("Save"), button:has-text("Update"), button[type="submit"]'
|
|
221
|
+
).first();
|
|
222
|
+
if (await submitBtn.isVisible().catch(() => false)) {
|
|
223
|
+
await submitBtn.click();
|
|
224
|
+
await page.waitForTimeout(2000);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return ok({ url: params.url, updated: true }, ['Profile 已更新,包含外链']);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
site.login(async (ctx) => {
|
|
232
|
+
const page = (ctx as Record<string, unknown>).page as import('playwright').Page | undefined;
|
|
233
|
+
if (!page) return;
|
|
234
|
+
await page.goto('https://www.quora.com/login');
|
|
235
|
+
await ctx.storage.set('quora_login', { at: Date.now() });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
site.logout(async (ctx) => {
|
|
239
|
+
await ctx.storage.delete('quora_login');
|
|
240
|
+
});
|
|
241
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xbrowser/quora",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Quora SEO 外链 - 问答平台 (DA 92, nofollow, 136M 月流量)",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"xbrowser",
|
|
8
|
+
"xbrowser-plugin",
|
|
9
|
+
"quora.com"
|
|
10
|
+
],
|
|
11
|
+
"author": "dyyz1993",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"xbrowser": {
|
|
14
|
+
"site": "https://www.quora.com",
|
|
15
|
+
"requiresLogin": true,
|
|
16
|
+
"commands": [
|
|
17
|
+
"login",
|
|
18
|
+
"answer",
|
|
19
|
+
"publish-article",
|
|
20
|
+
"update-profile"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|