@crowdlisten/harness 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 (109) hide show
  1. package/AGENTS.md +167 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/dist/agent-proxy.d.ts +24 -0
  5. package/dist/agent-proxy.js +140 -0
  6. package/dist/agent-tools.d.ts +736 -0
  7. package/dist/agent-tools.js +409 -0
  8. package/dist/context/api.d.ts +5 -0
  9. package/dist/context/api.js +164 -0
  10. package/dist/context/cli.d.ts +19 -0
  11. package/dist/context/cli.js +108 -0
  12. package/dist/context/extractor.d.ts +12 -0
  13. package/dist/context/extractor.js +43 -0
  14. package/dist/context/index.d.ts +12 -0
  15. package/dist/context/index.js +11 -0
  16. package/dist/context/matcher.d.ts +39 -0
  17. package/dist/context/matcher.js +246 -0
  18. package/dist/context/parser.d.ts +28 -0
  19. package/dist/context/parser.js +157 -0
  20. package/dist/context/pipeline.d.ts +26 -0
  21. package/dist/context/pipeline.js +56 -0
  22. package/dist/context/prompts.d.ts +6 -0
  23. package/dist/context/prompts.js +60 -0
  24. package/dist/context/providers.d.ts +6 -0
  25. package/dist/context/providers.js +106 -0
  26. package/dist/context/redactor.d.ts +10 -0
  27. package/dist/context/redactor.js +68 -0
  28. package/dist/context/server.d.ts +5 -0
  29. package/dist/context/server.js +134 -0
  30. package/dist/context/store.d.ts +12 -0
  31. package/dist/context/store.js +82 -0
  32. package/dist/context/types.d.ts +79 -0
  33. package/dist/context/types.js +4 -0
  34. package/dist/context/user-state.d.ts +40 -0
  35. package/dist/context/user-state.js +144 -0
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.js +385 -0
  38. package/dist/insights/browser/BrowserPool.d.ts +87 -0
  39. package/dist/insights/browser/BrowserPool.js +266 -0
  40. package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
  41. package/dist/insights/browser/RequestInterceptor.js +115 -0
  42. package/dist/insights/cli.d.ts +8 -0
  43. package/dist/insights/cli.js +206 -0
  44. package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
  45. package/dist/insights/core/base/BaseAdapter.js +123 -0
  46. package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
  47. package/dist/insights/core/health/HealthMonitor.js +171 -0
  48. package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
  49. package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
  50. package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
  51. package/dist/insights/core/utils/DataNormalizer.js +349 -0
  52. package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
  53. package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
  54. package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
  55. package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
  56. package/dist/insights/handlers.d.ts +157 -0
  57. package/dist/insights/handlers.js +246 -0
  58. package/dist/insights/index.d.ts +437 -0
  59. package/dist/insights/index.js +426 -0
  60. package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
  61. package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
  62. package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
  63. package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
  64. package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
  65. package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
  66. package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
  67. package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
  68. package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
  69. package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
  70. package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
  71. package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
  72. package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
  73. package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
  74. package/dist/insights/service-config.d.ts +7 -0
  75. package/dist/insights/service-config.js +60 -0
  76. package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
  77. package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
  78. package/dist/insights/vision/VisionExtractor.d.ts +46 -0
  79. package/dist/insights/vision/VisionExtractor.js +236 -0
  80. package/dist/learnings.d.ts +50 -0
  81. package/dist/learnings.js +130 -0
  82. package/dist/openapi.d.ts +29 -0
  83. package/dist/openapi.js +169 -0
  84. package/dist/server-factory.d.ts +20 -0
  85. package/dist/server-factory.js +41 -0
  86. package/dist/suggestions.d.ts +16 -0
  87. package/dist/suggestions.js +72 -0
  88. package/dist/telemetry.d.ts +44 -0
  89. package/dist/telemetry.js +93 -0
  90. package/dist/tools/registry.d.ts +65 -0
  91. package/dist/tools/registry.js +256 -0
  92. package/dist/tools.d.ts +2433 -0
  93. package/dist/tools.js +2294 -0
  94. package/dist/transport/http.d.ts +15 -0
  95. package/dist/transport/http.js +154 -0
  96. package/package.json +76 -0
  97. package/skills/catalog.json +272 -0
  98. package/skills/community-catalog.json +4202 -0
  99. package/skills/competitive-analysis/SKILL.md +174 -0
  100. package/skills/content-creator/SKILL.md +256 -0
  101. package/skills/content-strategy/SKILL.md +222 -0
  102. package/skills/data-storytelling/SKILL.md +248 -0
  103. package/skills/heuristic-evaluation/SKILL.md +201 -0
  104. package/skills/market-research-reports/SKILL.md +184 -0
  105. package/skills/user-stories/SKILL.md +178 -0
  106. package/skills/ux-researcher/SKILL.md +239 -0
  107. package/web-dist/assets/index-B1b25lNd.css +1 -0
  108. package/web-dist/assets/index-CDWHwHbl.js +64 -0
  109. package/web-dist/index.html +16 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Twitter/X Platform Adapter
3
+ * Uses @the-convocation/twitter-scraper for HTTP-only access to Twitter's
4
+ * internal API via cookie auth. No browser needed.
5
+ *
6
+ * Auth: TWITTER_USERNAME + TWITTER_PASSWORD env vars, or stored cookies.
7
+ */
8
+ import { Scraper, SearchMode } from '@the-convocation/twitter-scraper';
9
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
10
+ export class TwitterAdapter extends BaseAdapter {
11
+ scraper;
12
+ constructor(config) {
13
+ super(config);
14
+ this.scraper = new Scraper();
15
+ this.maxRequestsPerWindow = 20;
16
+ }
17
+ async initialize() {
18
+ try {
19
+ const username = process.env.TWITTER_USERNAME;
20
+ const password = process.env.TWITTER_PASSWORD;
21
+ if (username && password) {
22
+ await this.scraper.login(username, password);
23
+ }
24
+ const loggedIn = await this.scraper.isLoggedIn();
25
+ if (!loggedIn) {
26
+ this.log('Twitter scraper not logged in — some features may be limited', 'warn');
27
+ // Still usable for public content without login
28
+ }
29
+ this.isInitialized = true;
30
+ this.log(`Twitter adapter initialized (logged in: ${loggedIn})`);
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ this.log(`Failed to initialize Twitter adapter: ${error.message}`, 'error');
35
+ // Initialize anyway — scraper can still fetch some public content
36
+ this.isInitialized = true;
37
+ return true;
38
+ }
39
+ }
40
+ async searchContent(query, limit = 10) {
41
+ this.ensureInitialized();
42
+ this.validateLimit(limit);
43
+ if (!query || query.trim().length === 0) {
44
+ throw new Error('Search query cannot be empty');
45
+ }
46
+ await this.enforceRateLimit();
47
+ const posts = [];
48
+ const tweets = this.scraper.searchTweets(query, limit, SearchMode.Latest);
49
+ for await (const tweet of tweets) {
50
+ if (posts.length >= limit)
51
+ break;
52
+ const post = this.normalizeTweet(tweet);
53
+ if (post)
54
+ posts.push(post);
55
+ }
56
+ this.log(`Found ${posts.length} tweets for query: ${query}`);
57
+ return posts;
58
+ }
59
+ async getTrendingContent(limit = 10) {
60
+ this.ensureInitialized();
61
+ this.validateLimit(limit);
62
+ await this.enforceRateLimit();
63
+ try {
64
+ const trends = await this.scraper.getTrends();
65
+ if (!trends || trends.length === 0) {
66
+ return [];
67
+ }
68
+ // Search for the top trend to get actual posts
69
+ const topTrend = trends[0];
70
+ return this.searchContent(topTrend, limit);
71
+ }
72
+ catch (error) {
73
+ this.handleError(error, 'getTrendingContent');
74
+ }
75
+ }
76
+ async getUserContent(userId, limit = 10) {
77
+ this.ensureInitialized();
78
+ this.validateUserId(userId);
79
+ this.validateLimit(limit);
80
+ await this.enforceRateLimit();
81
+ const username = userId.replace(/^@/, '');
82
+ const posts = [];
83
+ const tweets = this.scraper.getTweets(username, limit);
84
+ for await (const tweet of tweets) {
85
+ if (posts.length >= limit)
86
+ break;
87
+ const post = this.normalizeTweet(tweet);
88
+ if (post)
89
+ posts.push(post);
90
+ }
91
+ this.log(`Retrieved ${posts.length} tweets from user @${username}`);
92
+ return posts;
93
+ }
94
+ async getContentComments(contentId, limit = 20) {
95
+ this.ensureInitialized();
96
+ this.validateContentId(contentId);
97
+ this.validateLimit(limit);
98
+ await this.enforceRateLimit();
99
+ // Extract tweet ID from URL if needed
100
+ let tweetId = contentId;
101
+ const idMatch = contentId.match(/\/status\/(\d+)/);
102
+ if (idMatch)
103
+ tweetId = idMatch[1];
104
+ try {
105
+ const tweet = await this.scraper.getTweet(tweetId);
106
+ if (!tweet) {
107
+ this.log(`Tweet ${tweetId} not found`, 'warn');
108
+ return [];
109
+ }
110
+ // Get replies by searching for replies to the tweet
111
+ const comments = [];
112
+ const username = tweet.username || '';
113
+ const replyQuery = `to:${username} conversation_id:${tweetId}`;
114
+ const replies = this.scraper.searchTweets(replyQuery, limit, SearchMode.Latest);
115
+ for await (const reply of replies) {
116
+ if (comments.length >= limit)
117
+ break;
118
+ const comment = this.normalizeTweetAsComment(reply);
119
+ if (comment)
120
+ comments.push(comment);
121
+ }
122
+ this.log(`Retrieved ${comments.length} replies for tweet ${tweetId}`);
123
+ return comments;
124
+ }
125
+ catch (error) {
126
+ this.handleError(error, `getContentComments(${tweetId})`);
127
+ }
128
+ }
129
+ // ── Normalization ──────────────────────────────────────────────────────
130
+ normalizeTweet(tweet) {
131
+ try {
132
+ const id = tweet.id || '';
133
+ if (!id)
134
+ return null;
135
+ return {
136
+ id,
137
+ platform: 'twitter',
138
+ author: {
139
+ id: tweet.userId || tweet.username || '',
140
+ username: tweet.username || '',
141
+ displayName: tweet.name || tweet.username || '',
142
+ followerCount: tweet.followersCount,
143
+ verified: tweet.isVerified || tweet.isBlueVerified,
144
+ profileImageUrl: tweet.profileImageUrl,
145
+ },
146
+ content: tweet.text || '',
147
+ mediaUrl: tweet.photos?.[0]?.url || tweet.videos?.[0]?.preview || '',
148
+ engagement: {
149
+ likes: tweet.likes || 0,
150
+ comments: tweet.replies || 0,
151
+ shares: tweet.retweets || 0,
152
+ views: tweet.views || 0,
153
+ },
154
+ timestamp: tweet.timeParsed ? new Date(tweet.timeParsed) : new Date(),
155
+ url: tweet.permanentUrl || `https://x.com/${tweet.username || 'user'}/status/${id}`,
156
+ hashtags: tweet.hashtags || [],
157
+ };
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ normalizeTweetAsComment(tweet) {
164
+ try {
165
+ const id = tweet.id || '';
166
+ if (!id)
167
+ return null;
168
+ return {
169
+ id,
170
+ author: {
171
+ id: tweet.userId || tweet.username || '',
172
+ username: tweet.username || '',
173
+ displayName: tweet.name || tweet.username || '',
174
+ verified: tweet.isVerified || tweet.isBlueVerified,
175
+ },
176
+ text: tweet.text || '',
177
+ timestamp: tweet.timeParsed ? new Date(tweet.timeParsed) : new Date(),
178
+ likes: tweet.likes || 0,
179
+ engagement: {
180
+ shares: tweet.retweets || 0,
181
+ views: tweet.views || 0,
182
+ },
183
+ };
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ // ── Platform identity ──────────────────────────────────────────────────
190
+ getPlatformName() {
191
+ return 'twitter';
192
+ }
193
+ getSupportedFeatures() {
194
+ return {
195
+ supportsTrending: true,
196
+ supportsUserContent: true,
197
+ supportsSearch: true,
198
+ supportsComments: true,
199
+ supportsAnalysis: true,
200
+ };
201
+ }
202
+ async cleanup() {
203
+ try {
204
+ await this.scraper.logout();
205
+ }
206
+ catch {
207
+ // Ignore logout errors
208
+ }
209
+ await super.cleanup();
210
+ }
211
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Xiaohongshu (RedNote) Platform Adapter — flat browser adapter using API interception.
3
+ *
4
+ * Uses BrowserPool + RequestInterceptor to capture XHS's internal API responses.
5
+ * Preserves mobile viewport, zh-CN locale, and conservative anti-detection delays.
6
+ * No tiers, no fallback chains.
7
+ *
8
+ * API targets:
9
+ * - /api/sns/web/v1/search/notes — search
10
+ * - /api/sns/web/v2/comment/page — comments
11
+ * - /api/sns/web/v1/feed — explore/trending
12
+ * - /api/sns/web/v1/user_posted — user posts
13
+ */
14
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
15
+ import { Post, Comment, PlatformCapabilities, PlatformType, PlatformConfig } from '../../core/interfaces/SocialMediaPlatform.js';
16
+ export declare class XiaohongshuAdapter extends BaseAdapter {
17
+ constructor(config: PlatformConfig);
18
+ initialize(): Promise<boolean>;
19
+ searchContent(query: string, limit?: number): Promise<Post[]>;
20
+ getTrendingContent(limit?: number): Promise<Post[]>;
21
+ getUserContent(userId: string, limit?: number): Promise<Post[]>;
22
+ getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
23
+ private interceptPosts;
24
+ /**
25
+ * XHS needs longer waits and slower scrolling due to anti-bot measures.
26
+ */
27
+ private waitAndScrollSlow;
28
+ private humanDelay;
29
+ private structurePosts;
30
+ private structureComments;
31
+ private normalizeNote;
32
+ private normalizeXHSComment;
33
+ getPlatformName(): PlatformType;
34
+ getSupportedFeatures(): PlatformCapabilities;
35
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Xiaohongshu (RedNote) Platform Adapter — flat browser adapter using API interception.
3
+ *
4
+ * Uses BrowserPool + RequestInterceptor to capture XHS's internal API responses.
5
+ * Preserves mobile viewport, zh-CN locale, and conservative anti-detection delays.
6
+ * No tiers, no fallback chains.
7
+ *
8
+ * API targets:
9
+ * - /api/sns/web/v1/search/notes — search
10
+ * - /api/sns/web/v2/comment/page — comments
11
+ * - /api/sns/web/v1/feed — explore/trending
12
+ * - /api/sns/web/v1/user_posted — user posts
13
+ */
14
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
15
+ import { getBrowserPool } from '../../browser/BrowserPool.js';
16
+ import { RequestInterceptor } from '../../browser/RequestInterceptor.js';
17
+ const API_PATTERNS = [
18
+ '/api/sns/web/v1/search/notes',
19
+ '/api/sns/web/v2/comment/page',
20
+ '/api/sns/web/v1/feed',
21
+ '/api/sns/web/v1/user_posted',
22
+ '/api/sns/web/v1/note/',
23
+ '/api/sns/web/v2/note/',
24
+ ];
25
+ export class XiaohongshuAdapter extends BaseAdapter {
26
+ constructor(config) {
27
+ super(config);
28
+ // Browser platforms risk IP blocks above ~1/min sustained; 3/min allows interactive bursts
29
+ this.maxRequestsPerWindow = 3;
30
+ }
31
+ async initialize() {
32
+ this.isInitialized = true;
33
+ this.log('Xiaohongshu adapter initialized (API interception mode, mobile profile)');
34
+ return true;
35
+ }
36
+ async searchContent(query, limit = 10) {
37
+ this.ensureInitialized();
38
+ await this.enforceRateLimit();
39
+ const url = `https://www.xiaohongshu.com/search_result?keyword=${encodeURIComponent(query)}&type=1`;
40
+ return this.interceptPosts(url, limit);
41
+ }
42
+ async getTrendingContent(limit = 10) {
43
+ this.ensureInitialized();
44
+ await this.enforceRateLimit();
45
+ return this.interceptPosts('https://www.xiaohongshu.com/explore', limit);
46
+ }
47
+ async getUserContent(userId, limit = 10) {
48
+ this.ensureInitialized();
49
+ this.validateUserId(userId);
50
+ await this.enforceRateLimit();
51
+ return this.interceptPosts(`https://www.xiaohongshu.com/user/profile/${userId}`, limit);
52
+ }
53
+ async getContentComments(contentId, limit = 20) {
54
+ this.ensureInitialized();
55
+ this.validateContentId(contentId);
56
+ await this.enforceRateLimit();
57
+ const url = contentId.includes('xiaohongshu.com/')
58
+ ? contentId
59
+ : `https://www.xiaohongshu.com/explore/${contentId}`;
60
+ const pool = getBrowserPool();
61
+ const page = await pool.acquire('xiaohongshu');
62
+ const interceptor = new RequestInterceptor();
63
+ try {
64
+ await interceptor.setup(page, API_PATTERNS);
65
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
66
+ await this.waitAndScrollSlow(page);
67
+ const apiData = interceptor.getAllData();
68
+ if (apiData.length === 0)
69
+ return [];
70
+ return this.structureComments(apiData).slice(0, limit);
71
+ }
72
+ catch (error) {
73
+ this.handleError(error, `getContentComments(${contentId})`);
74
+ }
75
+ finally {
76
+ interceptor.stop();
77
+ await pool.release(page);
78
+ }
79
+ }
80
+ // ── Interception pipeline ──────────────────────────────────────────────
81
+ async interceptPosts(url, limit) {
82
+ const pool = getBrowserPool();
83
+ const page = await pool.acquire('xiaohongshu');
84
+ const interceptor = new RequestInterceptor();
85
+ try {
86
+ await interceptor.setup(page, API_PATTERNS);
87
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
88
+ await this.waitAndScrollSlow(page);
89
+ const apiData = interceptor.getAllData();
90
+ if (apiData.length === 0)
91
+ return [];
92
+ return this.structurePosts(apiData).slice(0, limit);
93
+ }
94
+ catch (error) {
95
+ this.handleError(error, `interceptPosts(${url})`);
96
+ }
97
+ finally {
98
+ interceptor.stop();
99
+ await pool.release(page);
100
+ }
101
+ }
102
+ /**
103
+ * XHS needs longer waits and slower scrolling due to anti-bot measures.
104
+ */
105
+ async waitAndScrollSlow(page) {
106
+ // Human-like initial wait
107
+ await this.humanDelay(page, 2000, 4000);
108
+ try {
109
+ await page.waitForLoadState('networkidle', { timeout: 20000 });
110
+ }
111
+ catch {
112
+ // Non-fatal
113
+ }
114
+ // Slow scrolling with random delays (70% viewport height)
115
+ for (let i = 0; i < 3; i++) {
116
+ await page.evaluate(() => window.scrollBy(0, window.innerHeight * 0.7));
117
+ await this.humanDelay(page, 2000, 5000);
118
+ }
119
+ }
120
+ async humanDelay(page, minMs, maxMs) {
121
+ const delay = minMs + Math.random() * (maxMs - minMs);
122
+ await page.waitForTimeout(delay);
123
+ }
124
+ // ── Data normalization (preserved from VisualXiaohongshuAdapter) ───────
125
+ structurePosts(interceptedData) {
126
+ const posts = [];
127
+ const seenIds = new Set();
128
+ for (const data of interceptedData) {
129
+ try {
130
+ // Search results shape
131
+ const items = data?.data?.items || data?.data?.notes || [];
132
+ if (Array.isArray(items)) {
133
+ for (const item of items) {
134
+ const noteCard = item.note_card || item;
135
+ const post = this.normalizeNote(noteCard, item.id || noteCard.note_id);
136
+ if (post && !seenIds.has(post.id)) {
137
+ seenIds.add(post.id);
138
+ posts.push(post);
139
+ }
140
+ }
141
+ }
142
+ // Single note detail
143
+ if (data?.data?.note_id || data?.data?.id) {
144
+ const post = this.normalizeNote(data.data, data.data.note_id || data.data.id);
145
+ if (post && !seenIds.has(post.id)) {
146
+ seenIds.add(post.id);
147
+ posts.push(post);
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ // Skip malformed response
153
+ }
154
+ }
155
+ return posts;
156
+ }
157
+ structureComments(interceptedData) {
158
+ const comments = [];
159
+ const seenIds = new Set();
160
+ for (const data of interceptedData) {
161
+ try {
162
+ const commentList = data?.data?.comments || [];
163
+ if (Array.isArray(commentList)) {
164
+ for (const item of commentList) {
165
+ const comment = this.normalizeXHSComment(item);
166
+ if (comment && !seenIds.has(comment.id)) {
167
+ seenIds.add(comment.id);
168
+ comments.push(comment);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ // Skip malformed response
175
+ }
176
+ }
177
+ return comments;
178
+ }
179
+ normalizeNote(note, noteId) {
180
+ try {
181
+ const id = noteId || note.note_id || note.id || '';
182
+ if (!id)
183
+ return null;
184
+ const user = note.user || note.author || {};
185
+ const interactInfo = note.interact_info || {};
186
+ return {
187
+ id: String(id),
188
+ platform: 'xiaohongshu',
189
+ author: {
190
+ id: user.user_id || user.uid || '',
191
+ username: user.nickname || user.nick_name || '',
192
+ displayName: user.nickname || user.nick_name || '',
193
+ profileImageUrl: user.avatar || user.images,
194
+ },
195
+ content: note.title || note.desc || note.display_title || '',
196
+ mediaUrl: note.cover?.url || note.cover?.url_default || note.image_list?.[0]?.url || '',
197
+ engagement: {
198
+ likes: interactInfo.liked_count || note.liked_count || 0,
199
+ comments: interactInfo.comment_count || note.comment_count || 0,
200
+ shares: interactInfo.share_count || note.share_count || 0,
201
+ views: interactInfo.view_count || 0,
202
+ },
203
+ timestamp: note.time
204
+ ? new Date(note.time)
205
+ : note.create_time
206
+ ? new Date(note.create_time * 1000)
207
+ : new Date(),
208
+ url: `https://www.xiaohongshu.com/explore/${id}`,
209
+ hashtags: (note.tag_list || []).map((t) => t.name || t),
210
+ };
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ normalizeXHSComment(item) {
217
+ try {
218
+ const id = item.id || item.comment_id || '';
219
+ if (!id)
220
+ return null;
221
+ const user = item.user_info || item.user || {};
222
+ const subComments = (item.sub_comments || [])
223
+ .map((s) => this.normalizeXHSComment(s))
224
+ .filter(Boolean);
225
+ return {
226
+ id: String(id),
227
+ author: {
228
+ id: user.user_id || '',
229
+ username: user.nickname || '',
230
+ displayName: user.nickname || '',
231
+ profileImageUrl: user.image || user.avatar,
232
+ },
233
+ text: item.content || '',
234
+ timestamp: item.create_time
235
+ ? new Date(item.create_time * 1000)
236
+ : new Date(),
237
+ likes: item.like_count || 0,
238
+ replies: subComments.length > 0 ? subComments : undefined,
239
+ };
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ }
245
+ // ── Platform identity ──────────────────────────────────────────────────
246
+ getPlatformName() {
247
+ return 'xiaohongshu';
248
+ }
249
+ getSupportedFeatures() {
250
+ return {
251
+ supportsTrending: true,
252
+ supportsUserContent: true,
253
+ supportsSearch: true,
254
+ supportsComments: true,
255
+ supportsAnalysis: true,
256
+ };
257
+ }
258
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * YouTube Platform Adapter
3
+ * Uses YouTube Data API v3 for video search, trending, comments, and channel content
4
+ */
5
+ import { BaseAdapter } from '../../core/base/BaseAdapter.js';
6
+ import { Post, Comment, PlatformCapabilities, PlatformType, PlatformConfig } from '../../core/interfaces/SocialMediaPlatform.js';
7
+ export declare class YouTubeAdapter extends BaseAdapter {
8
+ private client;
9
+ private apiKey;
10
+ constructor(config: PlatformConfig);
11
+ initialize(): Promise<boolean>;
12
+ getTrendingContent(limit?: number): Promise<Post[]>;
13
+ getUserContent(userId: string, limit?: number): Promise<Post[]>;
14
+ searchContent(query: string, limit?: number): Promise<Post[]>;
15
+ getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
16
+ getPlatformName(): PlatformType;
17
+ getSupportedFeatures(): PlatformCapabilities;
18
+ protected isRateLimitError(error: any): boolean;
19
+ protected isAuthError(error: any): boolean;
20
+ protected isNotFoundError(error: any): boolean;
21
+ cleanup(): Promise<void>;
22
+ }