xhs-mcp 0.6.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +122 -0
  3. package/README.md +180 -0
  4. package/dist/cli/xhs-cli.d.ts +6 -0
  5. package/dist/cli/xhs-cli.d.ts.map +1 -0
  6. package/dist/cli/xhs-cli.js +300 -0
  7. package/dist/cli/xhs-cli.js.map +1 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +58 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/lib/auth/auth.service.d.ts +12 -0
  13. package/dist/lib/auth/auth.service.d.ts.map +1 -0
  14. package/dist/lib/auth/auth.service.js +138 -0
  15. package/dist/lib/auth/auth.service.js.map +1 -0
  16. package/dist/lib/auth/auth.types.d.ts +17 -0
  17. package/dist/lib/auth/auth.types.d.ts.map +1 -0
  18. package/dist/lib/auth/auth.types.js +5 -0
  19. package/dist/lib/auth/auth.types.js.map +1 -0
  20. package/dist/lib/auth/index.d.ts +6 -0
  21. package/dist/lib/auth/index.d.ts.map +1 -0
  22. package/dist/lib/auth/index.js +6 -0
  23. package/dist/lib/auth/index.js.map +1 -0
  24. package/dist/lib/browser/browser.manager.d.ts +22 -0
  25. package/dist/lib/browser/browser.manager.d.ts.map +1 -0
  26. package/dist/lib/browser/browser.manager.js +196 -0
  27. package/dist/lib/browser/browser.manager.js.map +1 -0
  28. package/dist/lib/browser/browser.types.d.ts +23 -0
  29. package/dist/lib/browser/browser.types.d.ts.map +1 -0
  30. package/dist/lib/browser/browser.types.js +5 -0
  31. package/dist/lib/browser/browser.types.js.map +1 -0
  32. package/dist/lib/browser/index.d.ts +6 -0
  33. package/dist/lib/browser/index.d.ts.map +1 -0
  34. package/dist/lib/browser/index.js +6 -0
  35. package/dist/lib/browser/index.js.map +1 -0
  36. package/dist/lib/feeds/feed.service.d.ts +13 -0
  37. package/dist/lib/feeds/feed.service.d.ts.map +1 -0
  38. package/dist/lib/feeds/feed.service.js +224 -0
  39. package/dist/lib/feeds/feed.service.js.map +1 -0
  40. package/dist/lib/feeds/feed.types.d.ts +26 -0
  41. package/dist/lib/feeds/feed.types.d.ts.map +1 -0
  42. package/dist/lib/feeds/feed.types.js +5 -0
  43. package/dist/lib/feeds/feed.types.js.map +1 -0
  44. package/dist/lib/feeds/index.d.ts +6 -0
  45. package/dist/lib/feeds/index.d.ts.map +1 -0
  46. package/dist/lib/feeds/index.js +6 -0
  47. package/dist/lib/feeds/index.js.map +1 -0
  48. package/dist/lib/index.d.ts +9 -0
  49. package/dist/lib/index.d.ts.map +1 -0
  50. package/dist/lib/index.js +11 -0
  51. package/dist/lib/index.js.map +1 -0
  52. package/dist/lib/publishing/index.d.ts +6 -0
  53. package/dist/lib/publishing/index.d.ts.map +1 -0
  54. package/dist/lib/publishing/index.js +6 -0
  55. package/dist/lib/publishing/index.js.map +1 -0
  56. package/dist/lib/publishing/publish.service.d.ts +39 -0
  57. package/dist/lib/publishing/publish.service.d.ts.map +1 -0
  58. package/dist/lib/publishing/publish.service.js +942 -0
  59. package/dist/lib/publishing/publish.service.js.map +1 -0
  60. package/dist/lib/publishing/publish.types.d.ts +37 -0
  61. package/dist/lib/publishing/publish.types.d.ts.map +1 -0
  62. package/dist/lib/publishing/publish.types.js +5 -0
  63. package/dist/lib/publishing/publish.types.js.map +1 -0
  64. package/dist/lib/shared/base.service.d.ts +22 -0
  65. package/dist/lib/shared/base.service.d.ts.map +1 -0
  66. package/dist/lib/shared/base.service.js +28 -0
  67. package/dist/lib/shared/base.service.js.map +1 -0
  68. package/dist/lib/shared/config.d.ts +17 -0
  69. package/dist/lib/shared/config.d.ts.map +1 -0
  70. package/dist/lib/shared/config.js +133 -0
  71. package/dist/lib/shared/config.js.map +1 -0
  72. package/dist/lib/shared/cookies.d.ts +10 -0
  73. package/dist/lib/shared/cookies.d.ts.map +1 -0
  74. package/dist/lib/shared/cookies.js +79 -0
  75. package/dist/lib/shared/cookies.js.map +1 -0
  76. package/dist/lib/shared/errors.d.ts +51 -0
  77. package/dist/lib/shared/errors.d.ts.map +1 -0
  78. package/dist/lib/shared/errors.js +93 -0
  79. package/dist/lib/shared/errors.js.map +1 -0
  80. package/dist/lib/shared/index.d.ts +11 -0
  81. package/dist/lib/shared/index.d.ts.map +1 -0
  82. package/dist/lib/shared/index.js +18 -0
  83. package/dist/lib/shared/index.js.map +1 -0
  84. package/dist/lib/shared/logger.d.ts +16 -0
  85. package/dist/lib/shared/logger.d.ts.map +1 -0
  86. package/dist/lib/shared/logger.js +41 -0
  87. package/dist/lib/shared/logger.js.map +1 -0
  88. package/dist/lib/shared/types.d.ts +175 -0
  89. package/dist/lib/shared/types.d.ts.map +1 -0
  90. package/dist/lib/shared/types.js +6 -0
  91. package/dist/lib/shared/types.js.map +1 -0
  92. package/dist/lib/shared/utils.d.ts +78 -0
  93. package/dist/lib/shared/utils.d.ts.map +1 -0
  94. package/dist/lib/shared/utils.js +137 -0
  95. package/dist/lib/shared/utils.js.map +1 -0
  96. package/dist/server/handlers/index.d.ts +6 -0
  97. package/dist/server/handlers/index.d.ts.map +1 -0
  98. package/dist/server/handlers/index.js +6 -0
  99. package/dist/server/handlers/index.js.map +1 -0
  100. package/dist/server/handlers/resource.handlers.d.ts +18 -0
  101. package/dist/server/handlers/resource.handlers.d.ts.map +1 -0
  102. package/dist/server/handlers/resource.handlers.js +113 -0
  103. package/dist/server/handlers/resource.handlers.js.map +1 -0
  104. package/dist/server/handlers/tool.handlers.d.ts +64 -0
  105. package/dist/server/handlers/tool.handlers.d.ts.map +1 -0
  106. package/dist/server/handlers/tool.handlers.js +123 -0
  107. package/dist/server/handlers/tool.handlers.js.map +1 -0
  108. package/dist/server/http.server.d.ts +21 -0
  109. package/dist/server/http.server.d.ts.map +1 -0
  110. package/dist/server/http.server.js +310 -0
  111. package/dist/server/http.server.js.map +1 -0
  112. package/dist/server/index.d.ts +7 -0
  113. package/dist/server/index.d.ts.map +1 -0
  114. package/dist/server/index.js +7 -0
  115. package/dist/server/index.js.map +1 -0
  116. package/dist/server/mcp.server.d.ts +15 -0
  117. package/dist/server/mcp.server.d.ts.map +1 -0
  118. package/dist/server/mcp.server.js +61 -0
  119. package/dist/server/mcp.server.js.map +1 -0
  120. package/dist/server/schemas/tool.schemas.d.ts +20 -0
  121. package/dist/server/schemas/tool.schemas.d.ts.map +1 -0
  122. package/dist/server/schemas/tool.schemas.js +178 -0
  123. package/dist/server/schemas/tool.schemas.js.map +1 -0
  124. package/dist/utils/index.d.ts +5 -0
  125. package/dist/utils/index.d.ts.map +1 -0
  126. package/dist/utils/index.js +5 -0
  127. package/dist/utils/index.js.map +1 -0
  128. package/dist/utils/xhs.utils.d.ts +14 -0
  129. package/dist/utils/xhs.utils.d.ts.map +1 -0
  130. package/dist/utils/xhs.utils.js +84 -0
  131. package/dist/utils/xhs.utils.js.map +1 -0
  132. package/docs/HTTP_TRANSPORTS.md +269 -0
  133. package/package.json +59 -0
@@ -0,0 +1,942 @@
1
+ /**
2
+ * Publishing service for XHS MCP Server
3
+ */
4
+ import { PublishError, InvalidImageError } from '../shared/errors.js';
5
+ import { BaseService } from '../shared/base.service.js';
6
+ import { existsSync, statSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { logger } from '../shared/logger.js';
9
+ import { sleep } from '../shared/utils.js';
10
+ // Constants for video publishing
11
+ const VIDEO_TIMEOUTS = {
12
+ PAGE_LOAD: 3000,
13
+ TAB_SWITCH: 2000,
14
+ VIDEO_PROCESSING: 10000,
15
+ CONTENT_WAIT: 1000,
16
+ UPLOAD_READY: 1000,
17
+ UPLOAD_START: 3000,
18
+ PROCESSING_CHECK: 3000,
19
+ COMPLETION_CHECK: 2000,
20
+ PROCESSING_TIMEOUT: 120000, // 2 minutes
21
+ COMPLETION_TIMEOUT: 300000, // 5 minutes
22
+ };
23
+ const SELECTORS = {
24
+ FILE_INPUT: [
25
+ 'input[type=file]',
26
+ '.upload-input',
27
+ 'input[accept*="video"]',
28
+ 'input[accept*="mp4"]',
29
+ 'input[class*="upload"]',
30
+ 'input[class*="file"]'
31
+ ],
32
+ SUCCESS_INDICATORS: [
33
+ '.success-message',
34
+ '.publish-success',
35
+ '[data-testid="publish-success"]',
36
+ '.toast-success',
37
+ '.upload-success',
38
+ '.video-upload-success',
39
+ '.video-processing-complete',
40
+ '.upload-complete'
41
+ ],
42
+ ERROR_INDICATORS: [
43
+ '.error-message',
44
+ '.publish-error',
45
+ '[data-testid="publish-error"]',
46
+ '.toast-error',
47
+ '.error-toast',
48
+ '.upload-error',
49
+ '.video-upload-error'
50
+ ],
51
+ PROCESSING_INDICATORS: [
52
+ '.video-processing',
53
+ '.upload-progress',
54
+ '.processing-indicator',
55
+ '[class*="processing"]',
56
+ '[class*="uploading"]',
57
+ '.progress-bar',
58
+ '.upload-status'
59
+ ],
60
+ COMPLETION_INDICATORS: [
61
+ '.upload-complete',
62
+ '.processing-complete',
63
+ '.video-ready',
64
+ '[class*="complete"]',
65
+ '[class*="ready"]'
66
+ ],
67
+ TOAST_SELECTORS: [
68
+ '.toast',
69
+ '.message',
70
+ '.notification',
71
+ '[role="alert"]',
72
+ '.ant-message',
73
+ '.el-message'
74
+ ],
75
+ PUBLISH_PAGE_INDICATORS: [
76
+ 'div.upload-content',
77
+ 'div.submit',
78
+ '.creator-editor',
79
+ '.video-upload-container',
80
+ 'input[type="file"]'
81
+ ]
82
+ };
83
+ const TEXT_PATTERNS = {
84
+ SUCCESS: ['成功', 'success', '完成'],
85
+ ERROR: ['失败', 'error', '错误'],
86
+ PROCESSING: ['处理中', '上传中', 'processing', 'uploading', '进度']
87
+ };
88
+ export class PublishService extends BaseService {
89
+ constructor(config) {
90
+ super(config);
91
+ }
92
+ // Helper methods for element detection and text matching
93
+ async findElementBySelectors(page, selectors) {
94
+ for (const selector of selectors) {
95
+ const element = await page.$(selector);
96
+ if (element) {
97
+ logger.debug(`Found element with selector: ${selector}`);
98
+ return element;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ async getElementText(element) {
104
+ try {
105
+ return await element.page().evaluate((el) => el.textContent, element);
106
+ }
107
+ catch (error) {
108
+ logger.warn(`Failed to get element text: ${error}`);
109
+ return null;
110
+ }
111
+ }
112
+ async checkTextPatterns(text, patterns) {
113
+ if (!text)
114
+ return false;
115
+ return patterns.some(pattern => text.includes(pattern));
116
+ }
117
+ async checkElementForPatterns(page, selectors, patterns) {
118
+ for (const selector of selectors) {
119
+ const element = await page.$(selector);
120
+ if (element) {
121
+ const text = await this.getElementText(element);
122
+ if (text && await this.checkTextPatterns(text, patterns)) {
123
+ return { found: true, text, element };
124
+ }
125
+ }
126
+ }
127
+ return { found: false };
128
+ }
129
+ async waitForCondition(condition, timeout, checkInterval = 1000, errorMessage) {
130
+ const startTime = Date.now();
131
+ while (Date.now() - startTime < timeout) {
132
+ if (await condition()) {
133
+ return;
134
+ }
135
+ await sleep(checkInterval);
136
+ }
137
+ throw new PublishError(errorMessage);
138
+ }
139
+ async publishNote(title, content, imagePaths, tags = '', browserPath) {
140
+ // Validate inputs
141
+ if (!title?.trim()) {
142
+ throw new PublishError('Note title cannot be empty');
143
+ }
144
+ if (!content?.trim()) {
145
+ throw new PublishError('Note content cannot be empty');
146
+ }
147
+ if (!imagePaths || imagePaths.length === 0) {
148
+ throw new PublishError('At least one image is required');
149
+ }
150
+ // Validate and resolve image paths
151
+ const resolvedPaths = this.validateAndResolveImagePaths(imagePaths);
152
+ // Wait for upload container selector
153
+ const uploadSelector = 'div.upload-content';
154
+ try {
155
+ const page = await this.getBrowserManager().createPage(false, browserPath, true);
156
+ try {
157
+ await this.getBrowserManager().navigateWithRetry(page, this.getConfig().xhs.creatorPublishUrl);
158
+ // Wait for page to load
159
+ await sleep(3000);
160
+ // First, try to switch to the image/text upload tab
161
+ await this.clickUploadTab(page);
162
+ // Wait for tab switch to complete
163
+ await sleep(2000);
164
+ let hasUploadContainer = await this.getBrowserManager().waitForSelectorSafe(page, uploadSelector, 30000);
165
+ if (!hasUploadContainer) {
166
+ // Try alternative selectors for upload container
167
+ const alternativeSelectors = [
168
+ 'div.upload-content',
169
+ '.upload-content',
170
+ 'div[class*="upload"]',
171
+ 'div[class*="image"]',
172
+ 'input[type="file"]'
173
+ ];
174
+ for (const selector of alternativeSelectors) {
175
+ hasUploadContainer = await this.getBrowserManager().waitForSelectorSafe(page, selector, 10000);
176
+ if (hasUploadContainer) {
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ if (!hasUploadContainer) {
182
+ throw new PublishError('Could not find upload container on publish page');
183
+ }
184
+ // Upload images
185
+ await this.uploadImages(page, resolvedPaths);
186
+ // Wait for images to be processed
187
+ await sleep(3000);
188
+ // Wait a bit for the page to settle after image upload
189
+ await sleep(2000);
190
+ // Fill in title
191
+ await this.fillTitle(page, title);
192
+ // Wait a bit more for content area to appear
193
+ await sleep(1000);
194
+ // Fill in content
195
+ await this.fillContent(page, content);
196
+ // Add tags if provided
197
+ if (tags) {
198
+ await this.addTags(page, tags);
199
+ }
200
+ // Submit the note
201
+ await this.submitPublish(page);
202
+ // Wait for completion and check result
203
+ await this.waitForPublishCompletion(page);
204
+ // Save cookies
205
+ await this.getBrowserManager().saveCookiesFromPage(page);
206
+ return {
207
+ success: true,
208
+ message: 'Note published successfully',
209
+ title,
210
+ content,
211
+ imageCount: resolvedPaths.length,
212
+ tags,
213
+ url: this.getConfig().xhs.creatorPublishUrl
214
+ };
215
+ }
216
+ finally {
217
+ await page.close();
218
+ }
219
+ }
220
+ catch (error) {
221
+ logger.error(`Publish error: ${error}`);
222
+ throw error;
223
+ }
224
+ }
225
+ validateAndResolveImagePaths(imagePaths) {
226
+ const resolvedPaths = [];
227
+ for (const imagePath of imagePaths) {
228
+ const resolvedPath = join(process.cwd(), imagePath);
229
+ if (!existsSync(resolvedPath)) {
230
+ throw new InvalidImageError(`Image file not found: ${imagePath}`);
231
+ }
232
+ const stats = statSync(resolvedPath);
233
+ if (!stats.isFile()) {
234
+ throw new InvalidImageError(`Path is not a file: ${imagePath}`);
235
+ }
236
+ // Check file extension
237
+ const ext = imagePath.toLowerCase().split('.').pop();
238
+ const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
239
+ if (!ext || !allowedExtensions.includes(ext)) {
240
+ throw new InvalidImageError(`Unsupported image format: ${imagePath}. Supported: ${allowedExtensions.join(', ')}`);
241
+ }
242
+ resolvedPaths.push(resolvedPath);
243
+ }
244
+ if (resolvedPaths.length > 18) {
245
+ throw new PublishError('Maximum 18 images allowed');
246
+ }
247
+ return resolvedPaths;
248
+ }
249
+ async clickUploadTab(page) {
250
+ try {
251
+ // Try multiple selectors for tabs
252
+ const tabSelectors = [
253
+ 'div.creator-tab',
254
+ '.creator-tab',
255
+ '[role="tab"]',
256
+ '.tab',
257
+ 'div[class*="tab"]'
258
+ ];
259
+ let tabs = [];
260
+ for (const selector of tabSelectors) {
261
+ const foundTabs = await page.$$(selector);
262
+ if (foundTabs.length > 0) {
263
+ tabs = foundTabs;
264
+ break;
265
+ }
266
+ }
267
+ if (tabs.length === 0) {
268
+ logger.warn('No tabs found');
269
+ return;
270
+ }
271
+ // Look for the "上传图文" (upload image/text) tab specifically
272
+ let imageTextTab = null;
273
+ for (let i = 0; i < tabs.length; i++) {
274
+ const tab = tabs[i];
275
+ try {
276
+ const isVisible = await tab.isIntersectingViewport();
277
+ if (!isVisible)
278
+ continue;
279
+ const text = await page.evaluate(el => el.textContent, tab);
280
+ // Check if this is the image/text upload tab
281
+ if (text && (text.includes('上传图文') || text.includes('图文') || text.includes('图片'))) {
282
+ imageTextTab = tab;
283
+ break;
284
+ }
285
+ }
286
+ catch (error) {
287
+ // Ignore individual tab errors
288
+ }
289
+ }
290
+ if (imageTextTab) {
291
+ await imageTextTab.click();
292
+ await sleep(2000); // Wait for tab switch
293
+ }
294
+ else {
295
+ // Fallback: click the second tab (usually image/text upload)
296
+ const visibleTabs = [];
297
+ for (const tab of tabs) {
298
+ const isVisible = await tab.isIntersectingViewport();
299
+ if (isVisible) {
300
+ visibleTabs.push(tab);
301
+ }
302
+ }
303
+ if (visibleTabs.length > 1) {
304
+ await visibleTabs[1].click(); // Usually the second tab is image/text
305
+ await sleep(2000);
306
+ }
307
+ else if (visibleTabs.length > 0) {
308
+ await visibleTabs[0].click();
309
+ await sleep(2000);
310
+ }
311
+ }
312
+ }
313
+ catch (error) {
314
+ logger.warn(`Failed to click upload tab: ${error}`);
315
+ }
316
+ }
317
+ async uploadImages(page, imagePaths) {
318
+ // Try primary file input selector
319
+ let fileInput = await page.$('input[type=file]');
320
+ if (!fileInput) {
321
+ // Fallback to alternative selector
322
+ fileInput = await page.$('.upload-input');
323
+ if (!fileInput) {
324
+ throw new PublishError('Could not find file upload input on page');
325
+ }
326
+ }
327
+ // Upload each image
328
+ for (const imagePath of imagePaths) {
329
+ try {
330
+ await fileInput.uploadFile(imagePath);
331
+ await sleep(1000); // Wait between uploads
332
+ }
333
+ catch (error) {
334
+ throw new PublishError(`Failed to upload image ${imagePath}: ${error}`);
335
+ }
336
+ }
337
+ }
338
+ async fillTitle(page, title) {
339
+ const titleSelectors = [
340
+ 'input[placeholder*="标题"]',
341
+ 'input[placeholder*="title"]',
342
+ 'input[data-placeholder*="标题"]',
343
+ '.title-input input',
344
+ 'input[type="text"]',
345
+ 'input[placeholder*="请输入标题"]',
346
+ 'input[placeholder*="标题"]',
347
+ 'input[name="title"]',
348
+ 'input[id*="title"]',
349
+ 'input[class*="title"]',
350
+ 'div[contenteditable="true"]',
351
+ 'textarea[placeholder*="标题"]',
352
+ 'textarea[placeholder*="title"]'
353
+ ];
354
+ for (const selector of titleSelectors) {
355
+ try {
356
+ const titleInput = await page.$(selector);
357
+ if (titleInput) {
358
+ const isVisible = await titleInput.isIntersectingViewport();
359
+ if (isVisible) {
360
+ await titleInput.click();
361
+ await sleep(500); // Wait for focus
362
+ await titleInput.type(title);
363
+ return;
364
+ }
365
+ }
366
+ }
367
+ catch (error) {
368
+ // Continue to next selector
369
+ }
370
+ }
371
+ // If no input found, try to find any input or textarea on the page
372
+ try {
373
+ const allInputs = await page.$$('input, textarea, [contenteditable="true"]');
374
+ for (let i = 0; i < allInputs.length; i++) {
375
+ const input = allInputs[i];
376
+ try {
377
+ const isVisible = await input.isIntersectingViewport();
378
+ const tagName = await page.evaluate(el => el.tagName, input);
379
+ if (isVisible && (tagName === 'INPUT' || tagName === 'TEXTAREA')) {
380
+ await input.click();
381
+ await sleep(500);
382
+ await input.type(title);
383
+ return;
384
+ }
385
+ }
386
+ catch (error) {
387
+ // Continue to next input
388
+ }
389
+ }
390
+ }
391
+ catch (error) {
392
+ // Fall through to error
393
+ }
394
+ throw new PublishError('Could not find title input field');
395
+ }
396
+ async findContentElement(page) {
397
+ // Strategy 1: Try div.ql-editor (primary selector)
398
+ const qlEditor = await page.$('div.ql-editor');
399
+ if (qlEditor) {
400
+ return qlEditor;
401
+ }
402
+ // Strategy 2: Try to find textarea or contenteditable
403
+ const contentSelectors = [
404
+ 'textarea[placeholder*="正文"]',
405
+ 'div[contenteditable="true"]',
406
+ 'div[data-placeholder*="正文"]',
407
+ '.content-editor',
408
+ 'div[role="textbox"]'
409
+ ];
410
+ for (const selector of contentSelectors) {
411
+ const element = await page.$(selector);
412
+ if (element) {
413
+ return element;
414
+ }
415
+ }
416
+ return null;
417
+ }
418
+ async findTextboxByPlaceholder(page) {
419
+ try {
420
+ // Find all p elements
421
+ const pElements = await page.$$('p');
422
+ // Look for element with data-placeholder containing "输入正文描述"
423
+ for (const p of pElements) {
424
+ const placeholder = await page.evaluate(el => el.getAttribute('data-placeholder'), p);
425
+ if (placeholder?.includes('输入正文描述')) {
426
+ return p;
427
+ }
428
+ }
429
+ return null;
430
+ }
431
+ catch (error) {
432
+ return null;
433
+ }
434
+ }
435
+ async findTextboxParent(page, element) {
436
+ try {
437
+ return await page.evaluateHandle(el => el.parentElement, element);
438
+ }
439
+ catch (error) {
440
+ return null;
441
+ }
442
+ }
443
+ async fillContent(page, content) {
444
+ // Wait for content area to appear
445
+ try {
446
+ await page.waitForSelector('div[contenteditable="true"], textarea, [role="textbox"], .ql-editor', { timeout: 10000 });
447
+ }
448
+ catch (error) {
449
+ // Continue without waiting
450
+ }
451
+ let contentElement = await this.findContentElement(page);
452
+ if (!contentElement) {
453
+ // Try alternative approach
454
+ const textboxElement = await this.findTextboxByPlaceholder(page);
455
+ if (textboxElement) {
456
+ contentElement = await this.findTextboxParent(page, textboxElement);
457
+ }
458
+ }
459
+ if (!contentElement) {
460
+ // Try to find any contenteditable or textarea element
461
+ try {
462
+ const allContentElements = await page.$$('div[contenteditable="true"], textarea, [role="textbox"], .ql-editor, p[contenteditable="true"]');
463
+ for (let i = 0; i < allContentElements.length; i++) {
464
+ const element = allContentElements[i];
465
+ try {
466
+ const isVisible = await element.isIntersectingViewport();
467
+ const tagName = await page.evaluate(el => el.tagName, element);
468
+ const contentEditable = await page.evaluate(el => el.getAttribute('contenteditable'), element);
469
+ const role = await page.evaluate(el => el.getAttribute('role'), element);
470
+ const className = await page.evaluate(el => el.className, element);
471
+ if (isVisible && (contentEditable === 'true' || tagName === 'TEXTAREA' || role === 'textbox' || className.includes('ql-editor'))) {
472
+ contentElement = element;
473
+ break;
474
+ }
475
+ }
476
+ catch (error) {
477
+ // Continue to next element
478
+ }
479
+ }
480
+ }
481
+ catch (error) {
482
+ // Continue to next strategy
483
+ }
484
+ }
485
+ if (!contentElement) {
486
+ // Last resort: try to find any element that might be for content
487
+ try {
488
+ const allElements = await page.$$('*');
489
+ for (let i = 0; i < Math.min(allElements.length, 50); i++) { // Limit to first 50 elements
490
+ const element = allElements[i];
491
+ try {
492
+ const isVisible = await element.isIntersectingViewport();
493
+ const tagName = await page.evaluate(el => el.tagName, element);
494
+ const contentEditable = await page.evaluate(el => el.getAttribute('contenteditable'), element);
495
+ const className = await page.evaluate(el => el.className, element);
496
+ const placeholder = await page.evaluate(el => el.getAttribute('placeholder'), element);
497
+ if (isVisible && (contentEditable === 'true' || tagName === 'TEXTAREA' ||
498
+ className.includes('content') || className.includes('editor') ||
499
+ placeholder?.includes('内容') || placeholder?.includes('正文'))) {
500
+ contentElement = element;
501
+ break;
502
+ }
503
+ }
504
+ catch (error) {
505
+ // Ignore errors for individual elements
506
+ }
507
+ }
508
+ }
509
+ catch (error) {
510
+ // Fall through to error
511
+ }
512
+ }
513
+ if (!contentElement) {
514
+ throw new PublishError('Could not find content input field');
515
+ }
516
+ try {
517
+ await contentElement.click();
518
+ await sleep(500); // Wait for focus
519
+ await contentElement.type(content);
520
+ }
521
+ catch (error) {
522
+ throw new PublishError(`Failed to fill content: ${error}`);
523
+ }
524
+ }
525
+ async inputTags(contentElement, tags) {
526
+ const tagList = tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
527
+ for (const tag of tagList) {
528
+ await this.inputTag(contentElement, tag);
529
+ }
530
+ }
531
+ async inputTag(contentElement, tag) {
532
+ try {
533
+ // Try to find topic suggestion container
534
+ const page = contentElement.page();
535
+ const topicContainer = await page.$('#creator-editor-topic-container');
536
+ if (topicContainer) {
537
+ const firstItem = await topicContainer.$('.item');
538
+ if (firstItem) {
539
+ await firstItem.click();
540
+ await sleep(500);
541
+ }
542
+ }
543
+ // Type the tag
544
+ await contentElement.type(`#${tag}`);
545
+ await sleep(500);
546
+ // Press Enter to confirm the tag
547
+ await contentElement.press('Enter');
548
+ await sleep(500);
549
+ }
550
+ catch (error) {
551
+ logger.warn(`Failed to add tag ${tag}: ${error}`);
552
+ }
553
+ }
554
+ async addTags(page, tags) {
555
+ try {
556
+ const contentElement = await this.findContentElement(page);
557
+ if (contentElement) {
558
+ await this.inputTags(contentElement, tags);
559
+ }
560
+ }
561
+ catch (error) {
562
+ logger.warn(`Failed to add tags: ${error}`);
563
+ }
564
+ }
565
+ async submitPublish(page) {
566
+ const submitSelector = 'div.submit div.d-button-content';
567
+ const submitButton = await page.$(submitSelector);
568
+ if (!submitButton) {
569
+ throw new PublishError('Could not find submit button');
570
+ }
571
+ // Wait for submit button to be visible
572
+ await page.waitForSelector(submitSelector, { visible: true, timeout: 10000 });
573
+ try {
574
+ await submitButton.click();
575
+ await sleep(2000);
576
+ }
577
+ catch (error) {
578
+ throw new PublishError(`Failed to click submit button: ${error}`);
579
+ }
580
+ }
581
+ async isElementVisible(element) {
582
+ try {
583
+ return await element.isIntersectingViewport();
584
+ }
585
+ catch (error) {
586
+ return false;
587
+ }
588
+ }
589
+ async waitForPublishCompletion(page) {
590
+ const maxWaitTime = 60000; // 60 seconds
591
+ const startTime = Date.now();
592
+ while (Date.now() - startTime < maxWaitTime) {
593
+ // Check for success indicators
594
+ const successIndicators = [
595
+ '.success-message',
596
+ '.publish-success',
597
+ '[data-testid="publish-success"]',
598
+ '.toast-success'
599
+ ];
600
+ for (const selector of successIndicators) {
601
+ const element = await page.$(selector);
602
+ if (element) {
603
+ await sleep(2000); // Wait a bit more for any final processing
604
+ return;
605
+ }
606
+ }
607
+ // Check for error indicators
608
+ const errorIndicators = [
609
+ '.error-message',
610
+ '.publish-error',
611
+ '[data-testid="publish-error"]',
612
+ '.toast-error',
613
+ '.error-toast'
614
+ ];
615
+ for (const selector of errorIndicators) {
616
+ const element = await page.$(selector);
617
+ if (element) {
618
+ const errorText = await page.evaluate(el => el.textContent, element);
619
+ throw new PublishError(`Publish failed with error: ${errorText}`);
620
+ }
621
+ }
622
+ // Check if we're still on the publish page
623
+ const publishPageIndicators = [
624
+ 'div.upload-content',
625
+ 'div.submit',
626
+ '.creator-editor'
627
+ ];
628
+ let stillOnPublishPage = false;
629
+ for (const selector of publishPageIndicators) {
630
+ const element = await page.$(selector);
631
+ if (element) {
632
+ stillOnPublishPage = true;
633
+ break;
634
+ }
635
+ }
636
+ if (!stillOnPublishPage) {
637
+ // We've left the publish page, likely successful
638
+ logger.debug('Left publish page, assuming success');
639
+ return;
640
+ }
641
+ // Check for toast messages
642
+ const toastSelectors = [
643
+ '.toast',
644
+ '.message',
645
+ '.notification',
646
+ '[role="alert"]'
647
+ ];
648
+ for (const selector of toastSelectors) {
649
+ const element = await page.$(selector);
650
+ if (element) {
651
+ const toastText = await page.evaluate(el => el.textContent, element);
652
+ if (toastText) {
653
+ if (toastText.includes('成功') || toastText.includes('success')) {
654
+ logger.debug(`Found success toast: ${toastText}`);
655
+ return;
656
+ }
657
+ else if (toastText.includes('失败') || toastText.includes('error') || toastText.includes('错误')) {
658
+ throw new PublishError(`Publish failed: ${toastText}`);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ await sleep(1000); // Wait before next check
664
+ }
665
+ throw new PublishError('Publish completion timeout - could not determine result');
666
+ }
667
+ async waitForVideoPublishCompletion(page) {
668
+ logger.debug('Waiting for video publish completion...');
669
+ let isProcessing = false;
670
+ await this.waitForCondition(async () => {
671
+ // Check for success indicators
672
+ const successResult = await this.checkElementForPatterns(page, SELECTORS.SUCCESS_INDICATORS, TEXT_PATTERNS.SUCCESS);
673
+ if (successResult.found) {
674
+ logger.debug(`Found success indicator: ${successResult.text}`);
675
+ await sleep(VIDEO_TIMEOUTS.COMPLETION_CHECK);
676
+ return true;
677
+ }
678
+ // Check for error indicators
679
+ const errorResult = await this.checkElementForPatterns(page, SELECTORS.ERROR_INDICATORS, TEXT_PATTERNS.ERROR);
680
+ if (errorResult.found) {
681
+ throw new PublishError(`Video publish failed with error: ${errorResult.text}`);
682
+ }
683
+ // Check if we've left the publish page (likely success)
684
+ const stillOnPage = await this.findElementBySelectors(page, SELECTORS.PUBLISH_PAGE_INDICATORS);
685
+ if (!stillOnPage) {
686
+ logger.debug('Left publish page, assuming video publish success');
687
+ return true;
688
+ }
689
+ // Check for processing status
690
+ const processingResult = await this.checkElementForPatterns(page, SELECTORS.PROCESSING_INDICATORS, TEXT_PATTERNS.PROCESSING);
691
+ isProcessing = processingResult.found;
692
+ if (isProcessing) {
693
+ logger.debug(`Video still processing: ${processingResult.text}`);
694
+ }
695
+ // Check for toast messages
696
+ const toastResult = await this.checkElementForPatterns(page, SELECTORS.TOAST_SELECTORS, TEXT_PATTERNS.SUCCESS);
697
+ if (toastResult.found) {
698
+ logger.debug(`Found success toast: ${toastResult.text}`);
699
+ return true;
700
+ }
701
+ const errorToastResult = await this.checkElementForPatterns(page, SELECTORS.TOAST_SELECTORS, TEXT_PATTERNS.ERROR);
702
+ if (errorToastResult.found) {
703
+ throw new PublishError(`Video publish failed: ${errorToastResult.text}`);
704
+ }
705
+ return false;
706
+ }, VIDEO_TIMEOUTS.COMPLETION_TIMEOUT, isProcessing ? 5000 : VIDEO_TIMEOUTS.COMPLETION_CHECK, 'Video publish completion timeout - could not determine result after 5 minutes');
707
+ }
708
+ // Unified publish method for both images and videos
709
+ async publishContent(type, title, content, mediaPaths, tags = '', browserPath) {
710
+ // Validate inputs
711
+ this.validateContentInputs(type, title, content, mediaPaths);
712
+ if (type === 'image') {
713
+ return await this.publishNote(title, content, mediaPaths, tags, browserPath);
714
+ }
715
+ else {
716
+ // For videos, only take the first path
717
+ const videoPath = mediaPaths[0];
718
+ return await this.publishVideo(title, content, videoPath, tags, browserPath);
719
+ }
720
+ }
721
+ async publishVideo(title, content, videoPath, tags = '', browserPath) {
722
+ // Validate inputs
723
+ this.validateVideoInputs(title, content, videoPath);
724
+ // Validate and resolve video path
725
+ const resolvedVideoPath = this.validateAndResolveVideoPath(videoPath);
726
+ try {
727
+ const page = await this.getBrowserManager().createPage(false, browserPath, true);
728
+ try {
729
+ await this.executeVideoPublishWorkflow(page, title, content, resolvedVideoPath, tags);
730
+ // Save cookies
731
+ await this.getBrowserManager().saveCookiesFromPage(page);
732
+ return {
733
+ success: true,
734
+ message: 'Video published successfully',
735
+ title,
736
+ content,
737
+ imageCount: 0, // Videos don't have image count
738
+ tags,
739
+ url: this.getConfig().xhs.creatorVideoPublishUrl
740
+ };
741
+ }
742
+ finally {
743
+ await page.close();
744
+ }
745
+ }
746
+ catch (error) {
747
+ logger.error(`Video publish error: ${error}`);
748
+ throw error;
749
+ }
750
+ }
751
+ validateContentInputs(type, title, content, mediaPaths) {
752
+ if (!title?.trim()) {
753
+ throw new PublishError(`${type === 'image' ? 'Image' : 'Video'} title cannot be empty`);
754
+ }
755
+ if (!content?.trim()) {
756
+ throw new PublishError(`${type === 'image' ? 'Image' : 'Video'} content cannot be empty`);
757
+ }
758
+ if (!mediaPaths || mediaPaths.length === 0) {
759
+ throw new PublishError(`${type === 'image' ? 'Image' : 'Video'} paths are required`);
760
+ }
761
+ if (type === 'image' && mediaPaths.length > 18) {
762
+ throw new PublishError('Maximum 18 images allowed for image posts');
763
+ }
764
+ if (type === 'video' && mediaPaths.length !== 1) {
765
+ throw new PublishError('Video publishing requires exactly one video file');
766
+ }
767
+ }
768
+ validateVideoInputs(title, content, videoPath) {
769
+ if (!title?.trim()) {
770
+ throw new PublishError('Video title cannot be empty');
771
+ }
772
+ if (!content?.trim()) {
773
+ throw new PublishError('Video content cannot be empty');
774
+ }
775
+ if (!videoPath?.trim()) {
776
+ throw new PublishError('Video path is required');
777
+ }
778
+ }
779
+ async executeVideoPublishWorkflow(page, title, content, videoPath, tags) {
780
+ // Navigate to video upload page
781
+ await this.getBrowserManager().navigateWithRetry(page, this.getConfig().xhs.creatorVideoPublishUrl);
782
+ // Wait for page to load
783
+ await sleep(VIDEO_TIMEOUTS.PAGE_LOAD);
784
+ // Switch to video upload tab if needed
785
+ await this.clickVideoUploadTab(page);
786
+ // Wait for tab switch to complete
787
+ await sleep(VIDEO_TIMEOUTS.TAB_SWITCH);
788
+ // Upload video
789
+ await this.uploadVideo(page, videoPath);
790
+ // Wait for video to be processed (videos take longer than images)
791
+ await sleep(VIDEO_TIMEOUTS.VIDEO_PROCESSING);
792
+ // Fill in title
793
+ await this.fillTitle(page, title);
794
+ // Wait a bit for content area to appear
795
+ await sleep(VIDEO_TIMEOUTS.CONTENT_WAIT);
796
+ // Fill in content
797
+ await this.fillContent(page, content);
798
+ // Add tags if provided
799
+ if (tags) {
800
+ await this.addTags(page, tags);
801
+ }
802
+ // Submit the video
803
+ await this.submitPublish(page);
804
+ // Wait for completion and check result (videos need longer timeout)
805
+ await this.waitForVideoPublishCompletion(page);
806
+ }
807
+ validateAndResolveVideoPath(videoPath) {
808
+ const resolvedPath = join(process.cwd(), videoPath);
809
+ if (!existsSync(resolvedPath)) {
810
+ throw new PublishError(`Video file not found: ${videoPath}`);
811
+ }
812
+ const stats = statSync(resolvedPath);
813
+ if (!stats.isFile()) {
814
+ throw new PublishError(`Path is not a file: ${videoPath}`);
815
+ }
816
+ // Check file extension
817
+ const ext = videoPath.toLowerCase().split('.').pop();
818
+ const allowedExtensions = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv'];
819
+ if (!ext || !allowedExtensions.includes(ext)) {
820
+ throw new PublishError(`Unsupported video format: ${videoPath}. Supported: ${allowedExtensions.join(', ')}`);
821
+ }
822
+ // Check file size (XHS typically has limits)
823
+ const maxSizeInMB = 500; // 500MB limit
824
+ const fileSizeInMB = stats.size / (1024 * 1024);
825
+ if (fileSizeInMB > maxSizeInMB) {
826
+ throw new PublishError(`Video file too large: ${fileSizeInMB.toFixed(2)}MB. Maximum allowed: ${maxSizeInMB}MB`);
827
+ }
828
+ return resolvedPath;
829
+ }
830
+ async clickVideoUploadTab(page) {
831
+ try {
832
+ // Try multiple selectors for video tab
833
+ const videoTabSelectors = [
834
+ 'div.creator-tab',
835
+ '.creator-tab',
836
+ '[role="tab"]',
837
+ '.tab',
838
+ 'div[class*="tab"]'
839
+ ];
840
+ let tabs = [];
841
+ for (const selector of videoTabSelectors) {
842
+ const foundTabs = await page.$$(selector);
843
+ if (foundTabs.length > 0) {
844
+ tabs = foundTabs;
845
+ break;
846
+ }
847
+ }
848
+ if (tabs.length === 0) {
849
+ logger.warn('No tabs found for video upload');
850
+ return;
851
+ }
852
+ // Look for the video upload tab specifically
853
+ let videoTab = null;
854
+ for (let i = 0; i < tabs.length; i++) {
855
+ const tab = tabs[i];
856
+ try {
857
+ const isVisible = await tab.isIntersectingViewport();
858
+ if (!isVisible)
859
+ continue;
860
+ const text = await page.evaluate(el => el.textContent, tab);
861
+ // Check if this is the video upload tab
862
+ if (text && (text.includes('上传视频') || text.includes('视频') || text.includes('video'))) {
863
+ videoTab = tab;
864
+ break;
865
+ }
866
+ }
867
+ catch (error) {
868
+ // Ignore individual tab errors
869
+ }
870
+ }
871
+ if (videoTab) {
872
+ await videoTab.click();
873
+ await sleep(2000); // Wait for tab switch
874
+ }
875
+ else {
876
+ // Fallback: click the first tab (usually video upload)
877
+ const visibleTabs = [];
878
+ for (const tab of tabs) {
879
+ const isVisible = await tab.isIntersectingViewport();
880
+ if (isVisible) {
881
+ visibleTabs.push(tab);
882
+ }
883
+ }
884
+ if (visibleTabs.length > 0) {
885
+ await visibleTabs[0].click(); // Usually the first tab is video upload
886
+ await sleep(2000);
887
+ }
888
+ }
889
+ }
890
+ catch (error) {
891
+ logger.warn(`Failed to click video upload tab: ${error}`);
892
+ }
893
+ }
894
+ async uploadVideo(page, videoPath) {
895
+ logger.debug(`Uploading video: ${videoPath}`);
896
+ // Find file input element
897
+ const fileInput = await this.findElementBySelectors(page, SELECTORS.FILE_INPUT);
898
+ if (!fileInput) {
899
+ throw new PublishError('Could not find file upload input on video upload page');
900
+ }
901
+ try {
902
+ // Wait for the input to be ready
903
+ await sleep(VIDEO_TIMEOUTS.UPLOAD_READY);
904
+ // Upload the video file
905
+ await fileInput.uploadFile(videoPath);
906
+ logger.debug('Video file uploaded, waiting for processing...');
907
+ // Wait for upload to start and show progress
908
+ await sleep(VIDEO_TIMEOUTS.UPLOAD_START);
909
+ // Wait for video processing to complete (this can take a while)
910
+ await this.waitForVideoProcessing(page);
911
+ }
912
+ catch (error) {
913
+ throw new PublishError(`Failed to upload video ${videoPath}: ${error}`);
914
+ }
915
+ }
916
+ async waitForVideoProcessing(page) {
917
+ logger.debug('Waiting for video processing to complete...');
918
+ try {
919
+ await this.waitForCondition(async () => {
920
+ // Check if processing is complete
921
+ const completeResult = await this.checkElementForPatterns(page, SELECTORS.COMPLETION_INDICATORS, TEXT_PATTERNS.SUCCESS);
922
+ if (completeResult.found) {
923
+ logger.debug(`Video processing complete: ${completeResult.text}`);
924
+ return true;
925
+ }
926
+ // Check if still processing
927
+ const processingResult = await this.checkElementForPatterns(page, SELECTORS.PROCESSING_INDICATORS, TEXT_PATTERNS.PROCESSING);
928
+ if (processingResult.found) {
929
+ logger.debug(`Video processing: ${processingResult.text}`);
930
+ return false; // Still processing, continue waiting
931
+ }
932
+ // If not processing and no completion indicator, assume it's done
933
+ logger.debug('No processing indicators found, assuming video processing complete');
934
+ return true;
935
+ }, VIDEO_TIMEOUTS.PROCESSING_TIMEOUT, VIDEO_TIMEOUTS.PROCESSING_CHECK, 'Video processing timeout');
936
+ }
937
+ catch (error) {
938
+ logger.warn('Video processing timeout, continuing anyway...');
939
+ }
940
+ }
941
+ }
942
+ //# sourceMappingURL=publish.service.js.map