@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.
- package/AGENTS.md +167 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/agent-proxy.d.ts +24 -0
- package/dist/agent-proxy.js +140 -0
- package/dist/agent-tools.d.ts +736 -0
- package/dist/agent-tools.js +409 -0
- package/dist/context/api.d.ts +5 -0
- package/dist/context/api.js +164 -0
- package/dist/context/cli.d.ts +19 -0
- package/dist/context/cli.js +108 -0
- package/dist/context/extractor.d.ts +12 -0
- package/dist/context/extractor.js +43 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +11 -0
- package/dist/context/matcher.d.ts +39 -0
- package/dist/context/matcher.js +246 -0
- package/dist/context/parser.d.ts +28 -0
- package/dist/context/parser.js +157 -0
- package/dist/context/pipeline.d.ts +26 -0
- package/dist/context/pipeline.js +56 -0
- package/dist/context/prompts.d.ts +6 -0
- package/dist/context/prompts.js +60 -0
- package/dist/context/providers.d.ts +6 -0
- package/dist/context/providers.js +106 -0
- package/dist/context/redactor.d.ts +10 -0
- package/dist/context/redactor.js +68 -0
- package/dist/context/server.d.ts +5 -0
- package/dist/context/server.js +134 -0
- package/dist/context/store.d.ts +12 -0
- package/dist/context/store.js +82 -0
- package/dist/context/types.d.ts +79 -0
- package/dist/context/types.js +4 -0
- package/dist/context/user-state.d.ts +40 -0
- package/dist/context/user-state.js +144 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +385 -0
- package/dist/insights/browser/BrowserPool.d.ts +87 -0
- package/dist/insights/browser/BrowserPool.js +266 -0
- package/dist/insights/browser/RequestInterceptor.d.ts +46 -0
- package/dist/insights/browser/RequestInterceptor.js +115 -0
- package/dist/insights/cli.d.ts +8 -0
- package/dist/insights/cli.js +206 -0
- package/dist/insights/core/base/BaseAdapter.d.ts +37 -0
- package/dist/insights/core/base/BaseAdapter.js +123 -0
- package/dist/insights/core/health/HealthMonitor.d.ts +75 -0
- package/dist/insights/core/health/HealthMonitor.js +171 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.d.ts +125 -0
- package/dist/insights/core/interfaces/SocialMediaPlatform.js +42 -0
- package/dist/insights/core/utils/DataNormalizer.d.ts +53 -0
- package/dist/insights/core/utils/DataNormalizer.js +349 -0
- package/dist/insights/core/utils/InstagramUrlUtils.d.ts +11 -0
- package/dist/insights/core/utils/InstagramUrlUtils.js +60 -0
- package/dist/insights/core/utils/TikTokUrlUtils.d.ts +10 -0
- package/dist/insights/core/utils/TikTokUrlUtils.js +57 -0
- package/dist/insights/handlers.d.ts +157 -0
- package/dist/insights/handlers.js +246 -0
- package/dist/insights/index.d.ts +437 -0
- package/dist/insights/index.js +426 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.d.ts +34 -0
- package/dist/insights/platforms/instagram/InstagramAdapter.js +342 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.d.ts +31 -0
- package/dist/insights/platforms/moltbook/MoltbookAdapter.js +227 -0
- package/dist/insights/platforms/reddit/RedditAdapter.d.ts +21 -0
- package/dist/insights/platforms/reddit/RedditAdapter.js +212 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.d.ts +34 -0
- package/dist/insights/platforms/tiktok/TikTokAdapter.js +269 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.d.ts +23 -0
- package/dist/insights/platforms/twitter/TwitterAdapter.js +211 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.d.ts +35 -0
- package/dist/insights/platforms/xiaohongshu/XiaohongshuAdapter.js +258 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.d.ts +22 -0
- package/dist/insights/platforms/youtube/YouTubeAdapter.js +254 -0
- package/dist/insights/service-config.d.ts +7 -0
- package/dist/insights/service-config.js +60 -0
- package/dist/insights/services/UnifiedSocialMediaService.d.ts +94 -0
- package/dist/insights/services/UnifiedSocialMediaService.js +259 -0
- package/dist/insights/vision/VisionExtractor.d.ts +46 -0
- package/dist/insights/vision/VisionExtractor.js +236 -0
- package/dist/learnings.d.ts +50 -0
- package/dist/learnings.js +130 -0
- package/dist/openapi.d.ts +29 -0
- package/dist/openapi.js +169 -0
- package/dist/server-factory.d.ts +20 -0
- package/dist/server-factory.js +41 -0
- package/dist/suggestions.d.ts +16 -0
- package/dist/suggestions.js +72 -0
- package/dist/telemetry.d.ts +44 -0
- package/dist/telemetry.js +93 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.js +256 -0
- package/dist/tools.d.ts +2433 -0
- package/dist/tools.js +2294 -0
- package/dist/transport/http.d.ts +15 -0
- package/dist/transport/http.js +154 -0
- package/package.json +76 -0
- package/skills/catalog.json +272 -0
- package/skills/community-catalog.json +4202 -0
- package/skills/competitive-analysis/SKILL.md +174 -0
- package/skills/content-creator/SKILL.md +256 -0
- package/skills/content-strategy/SKILL.md +222 -0
- package/skills/data-storytelling/SKILL.md +248 -0
- package/skills/heuristic-evaluation/SKILL.md +201 -0
- package/skills/market-research-reports/SKILL.md +184 -0
- package/skills/user-stories/SKILL.md +178 -0
- package/skills/ux-researcher/SKILL.md +239 -0
- package/web-dist/assets/index-B1b25lNd.css +1 -0
- package/web-dist/assets/index-CDWHwHbl.js +64 -0
- package/web-dist/index.html +16 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base adapter class providing common functionality for all social media platforms
|
|
3
|
+
* Implements shared features like rate limiting, error handling, and logging
|
|
4
|
+
*
|
|
5
|
+
* Analysis (clustering, enrichment) has been moved to the CrowdListen API.
|
|
6
|
+
* This class now only handles data retrieval.
|
|
7
|
+
*/
|
|
8
|
+
import { SocialMediaError, RateLimitError } from '../interfaces/SocialMediaPlatform.js';
|
|
9
|
+
export class BaseAdapter {
|
|
10
|
+
config;
|
|
11
|
+
isInitialized = false;
|
|
12
|
+
lastRequestTime = 0;
|
|
13
|
+
requestCount = 0;
|
|
14
|
+
rateLimitWindow = 60000; // 1 minute window
|
|
15
|
+
maxRequestsPerWindow = 30; // Max 30 requests per minute
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
async enforceRateLimit() {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
22
|
+
// Reset request count if window has passed
|
|
23
|
+
if (timeSinceLastRequest > this.rateLimitWindow) {
|
|
24
|
+
this.requestCount = 0;
|
|
25
|
+
}
|
|
26
|
+
// If we've hit the rate limit, wait
|
|
27
|
+
if (this.requestCount >= this.maxRequestsPerWindow) {
|
|
28
|
+
const waitTime = this.rateLimitWindow - timeSinceLastRequest;
|
|
29
|
+
if (waitTime > 0) {
|
|
30
|
+
this.log(`Rate limit reached. Waiting ${waitTime}ms...`);
|
|
31
|
+
await this.sleep(waitTime);
|
|
32
|
+
this.requestCount = 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.requestCount++;
|
|
36
|
+
this.lastRequestTime = Date.now();
|
|
37
|
+
}
|
|
38
|
+
async sleep(ms) {
|
|
39
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
handleError(error, operation) {
|
|
42
|
+
this.log(`Error in ${operation}: ${error.message || error}`, 'error');
|
|
43
|
+
if (this.isRateLimitError(error)) {
|
|
44
|
+
throw new RateLimitError(this.getPlatformName(), error);
|
|
45
|
+
}
|
|
46
|
+
if (this.isAuthError(error)) {
|
|
47
|
+
throw new SocialMediaError(`Authentication failed during ${operation}`, 'authentication_error', this.getPlatformName(), error.statusCode);
|
|
48
|
+
}
|
|
49
|
+
if (this.isNotFoundError(error)) {
|
|
50
|
+
throw new SocialMediaError(`Resource not found during ${operation}`, 'not_found', this.getPlatformName(), error.statusCode);
|
|
51
|
+
}
|
|
52
|
+
throw new SocialMediaError(`Operation ${operation} failed: ${error.message || error}`, 'unknown_error', this.getPlatformName(), error.statusCode);
|
|
53
|
+
}
|
|
54
|
+
isRateLimitError(error) {
|
|
55
|
+
return error.statusCode === 429 || error.code === 'rate_limit_exceeded';
|
|
56
|
+
}
|
|
57
|
+
isAuthError(error) {
|
|
58
|
+
return error.statusCode === 401 || error.statusCode === 403;
|
|
59
|
+
}
|
|
60
|
+
isNotFoundError(error) {
|
|
61
|
+
return error.statusCode === 404;
|
|
62
|
+
}
|
|
63
|
+
log(message, level = 'info') {
|
|
64
|
+
const timestamp = new Date().toISOString();
|
|
65
|
+
const platformName = this.getPlatformName();
|
|
66
|
+
const prefix = `[${timestamp}] [${platformName.toUpperCase()}]`;
|
|
67
|
+
switch (level) {
|
|
68
|
+
case 'error':
|
|
69
|
+
console.error(`${prefix} ERROR: ${message}`);
|
|
70
|
+
break;
|
|
71
|
+
case 'warn':
|
|
72
|
+
console.warn(`${prefix} WARN: ${message}`);
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
console.log(`${prefix} INFO: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
validateUserId(userId) {
|
|
79
|
+
if (!userId || userId.trim().length === 0) {
|
|
80
|
+
throw new SocialMediaError('User ID cannot be empty', 'validation_error', this.getPlatformName());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
validateContentId(contentId) {
|
|
84
|
+
if (!contentId || contentId.trim().length === 0) {
|
|
85
|
+
throw new SocialMediaError('Content ID cannot be empty', 'validation_error', this.getPlatformName());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
validateLimit(limit) {
|
|
89
|
+
if (limit < 1 || limit > 1000) {
|
|
90
|
+
throw new SocialMediaError('Limit must be between 1 and 1000', 'validation_error', this.getPlatformName());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async analyzeContent(contentId, enableClustering = true) {
|
|
94
|
+
this.validateContentId(contentId);
|
|
95
|
+
try {
|
|
96
|
+
await this.enforceRateLimit();
|
|
97
|
+
const comments = await this.getContentComments(contentId, 200);
|
|
98
|
+
// Return basic post metadata only — analysis is handled by the CrowdListen API
|
|
99
|
+
const analysis = {
|
|
100
|
+
postId: contentId,
|
|
101
|
+
platform: this.getPlatformName(),
|
|
102
|
+
sentiment: 'neutral',
|
|
103
|
+
themes: ['general'],
|
|
104
|
+
summary: `Retrieved ${comments.length} comments for ${contentId}. Use analyze_content with CROWDLISTEN_API_KEY for full analysis.`,
|
|
105
|
+
commentCount: comments.length,
|
|
106
|
+
topComments: comments.slice(0, 5),
|
|
107
|
+
};
|
|
108
|
+
return analysis;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.handleError(error, 'analyzeContent');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
ensureInitialized() {
|
|
115
|
+
if (!this.isInitialized) {
|
|
116
|
+
throw new SocialMediaError('Adapter not initialized. Call initialize() first.', 'initialization_error', this.getPlatformName());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async cleanup() {
|
|
120
|
+
this.isInitialized = false;
|
|
121
|
+
this.log('Adapter cleaned up');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive Health Monitor
|
|
3
|
+
*
|
|
4
|
+
* Periodically checks platform health in the background, tracks response
|
|
5
|
+
* times, consecutive failures, and degradation state. The MCP health_check
|
|
6
|
+
* tool reads cached state from here instead of doing a live probe every
|
|
7
|
+
* time, making it fast and non-blocking.
|
|
8
|
+
*
|
|
9
|
+
* Three-state model: healthy -> degraded -> down
|
|
10
|
+
* - healthy: last check succeeded with results
|
|
11
|
+
* - degraded: last check returned 0 results, or 1-2 consecutive failures
|
|
12
|
+
* - down: 3+ consecutive failures
|
|
13
|
+
*/
|
|
14
|
+
import { PlatformType } from '../interfaces/SocialMediaPlatform.js';
|
|
15
|
+
export type HealthStatus = 'healthy' | 'degraded' | 'down';
|
|
16
|
+
export interface PlatformHealthState {
|
|
17
|
+
platform: PlatformType;
|
|
18
|
+
status: HealthStatus;
|
|
19
|
+
lastChecked: Date;
|
|
20
|
+
lastHealthy: Date | null;
|
|
21
|
+
consecutiveFailures: number;
|
|
22
|
+
responseTimeMs: number | null;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface HealthSummary {
|
|
26
|
+
overall: HealthStatus;
|
|
27
|
+
platforms: Record<string, PlatformHealthState>;
|
|
28
|
+
lastFullCheck: Date | null;
|
|
29
|
+
}
|
|
30
|
+
export type HealthCheckFn = (platform: PlatformType) => Promise<{
|
|
31
|
+
success: boolean;
|
|
32
|
+
resultCount: number;
|
|
33
|
+
}>;
|
|
34
|
+
export declare class HealthMonitor {
|
|
35
|
+
private checkFn;
|
|
36
|
+
private platforms;
|
|
37
|
+
private intervalMs;
|
|
38
|
+
private states;
|
|
39
|
+
private intervalId;
|
|
40
|
+
private startupTimeoutId;
|
|
41
|
+
private lastFullCheck;
|
|
42
|
+
constructor(checkFn: HealthCheckFn, platforms: PlatformType[], intervalMs?: number);
|
|
43
|
+
/**
|
|
44
|
+
* Start background health checks.
|
|
45
|
+
* The first check runs after a short delay so it does not block MCP
|
|
46
|
+
* server startup.
|
|
47
|
+
*/
|
|
48
|
+
start(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Stop background health checks and clean up timers.
|
|
51
|
+
*/
|
|
52
|
+
stop(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Check all platforms in parallel. One failing platform does not block
|
|
55
|
+
* or affect the others.
|
|
56
|
+
*/
|
|
57
|
+
checkAll(): Promise<HealthSummary>;
|
|
58
|
+
/**
|
|
59
|
+
* Check a single platform and update its state.
|
|
60
|
+
*/
|
|
61
|
+
checkPlatform(platform: PlatformType): Promise<PlatformHealthState>;
|
|
62
|
+
/**
|
|
63
|
+
* Get a snapshot summary of all platform health states.
|
|
64
|
+
*/
|
|
65
|
+
getSummary(): HealthSummary;
|
|
66
|
+
/**
|
|
67
|
+
* Get health state for a single platform. Returns undefined if the
|
|
68
|
+
* platform is not being monitored.
|
|
69
|
+
*/
|
|
70
|
+
getState(platform: PlatformType): PlatformHealthState | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the monitor has recent data (checked within the given ms).
|
|
73
|
+
*/
|
|
74
|
+
hasRecentData(withinMs?: number): boolean;
|
|
75
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proactive Health Monitor
|
|
3
|
+
*
|
|
4
|
+
* Periodically checks platform health in the background, tracks response
|
|
5
|
+
* times, consecutive failures, and degradation state. The MCP health_check
|
|
6
|
+
* tool reads cached state from here instead of doing a live probe every
|
|
7
|
+
* time, making it fast and non-blocking.
|
|
8
|
+
*
|
|
9
|
+
* Three-state model: healthy -> degraded -> down
|
|
10
|
+
* - healthy: last check succeeded with results
|
|
11
|
+
* - degraded: last check returned 0 results, or 1-2 consecutive failures
|
|
12
|
+
* - down: 3+ consecutive failures
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Default check interval: 5 minutes.
|
|
16
|
+
* Default per-platform timeout: 15 seconds.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
|
|
19
|
+
const CHECK_TIMEOUT_MS = 15_000;
|
|
20
|
+
const STARTUP_DELAY_MS = 10_000;
|
|
21
|
+
const CONSECUTIVE_FAILURE_THRESHOLD = 3;
|
|
22
|
+
export class HealthMonitor {
|
|
23
|
+
checkFn;
|
|
24
|
+
platforms;
|
|
25
|
+
intervalMs;
|
|
26
|
+
states = new Map();
|
|
27
|
+
intervalId = null;
|
|
28
|
+
startupTimeoutId = null;
|
|
29
|
+
lastFullCheck = null;
|
|
30
|
+
constructor(checkFn, platforms, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
31
|
+
this.checkFn = checkFn;
|
|
32
|
+
this.platforms = platforms;
|
|
33
|
+
this.intervalMs = intervalMs;
|
|
34
|
+
for (const p of platforms) {
|
|
35
|
+
this.states.set(p, {
|
|
36
|
+
platform: p,
|
|
37
|
+
status: 'healthy', // optimistic start
|
|
38
|
+
lastChecked: new Date(0),
|
|
39
|
+
lastHealthy: null,
|
|
40
|
+
consecutiveFailures: 0,
|
|
41
|
+
responseTimeMs: null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start background health checks.
|
|
47
|
+
* The first check runs after a short delay so it does not block MCP
|
|
48
|
+
* server startup.
|
|
49
|
+
*/
|
|
50
|
+
start() {
|
|
51
|
+
if (this.intervalId)
|
|
52
|
+
return;
|
|
53
|
+
this.startupTimeoutId = setTimeout(() => {
|
|
54
|
+
this.checkAll().catch((err) => console.error('[HealthMonitor] Initial check error:', err));
|
|
55
|
+
}, STARTUP_DELAY_MS);
|
|
56
|
+
this.intervalId = setInterval(() => {
|
|
57
|
+
this.checkAll().catch((err) => console.error('[HealthMonitor] Periodic check error:', err));
|
|
58
|
+
}, this.intervalMs);
|
|
59
|
+
console.error(`[HealthMonitor] Started — checking ${this.platforms.length} platforms every ${Math.round(this.intervalMs / 1000)}s`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Stop background health checks and clean up timers.
|
|
63
|
+
*/
|
|
64
|
+
stop() {
|
|
65
|
+
if (this.startupTimeoutId) {
|
|
66
|
+
clearTimeout(this.startupTimeoutId);
|
|
67
|
+
this.startupTimeoutId = null;
|
|
68
|
+
}
|
|
69
|
+
if (this.intervalId) {
|
|
70
|
+
clearInterval(this.intervalId);
|
|
71
|
+
this.intervalId = null;
|
|
72
|
+
}
|
|
73
|
+
console.error('[HealthMonitor] Stopped');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check all platforms in parallel. One failing platform does not block
|
|
77
|
+
* or affect the others.
|
|
78
|
+
*/
|
|
79
|
+
async checkAll() {
|
|
80
|
+
await Promise.allSettled(this.platforms.map((p) => this.checkPlatform(p)));
|
|
81
|
+
this.lastFullCheck = new Date();
|
|
82
|
+
return this.getSummary();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check a single platform and update its state.
|
|
86
|
+
*/
|
|
87
|
+
async checkPlatform(platform) {
|
|
88
|
+
const state = this.states.get(platform);
|
|
89
|
+
if (!state) {
|
|
90
|
+
throw new Error(`Unknown platform: ${platform}`);
|
|
91
|
+
}
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
const result = await Promise.race([
|
|
95
|
+
this.checkFn(platform),
|
|
96
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Health check timeout')), CHECK_TIMEOUT_MS)),
|
|
97
|
+
]);
|
|
98
|
+
const elapsed = Date.now() - start;
|
|
99
|
+
if (result.success && result.resultCount > 0) {
|
|
100
|
+
state.status = 'healthy';
|
|
101
|
+
state.lastHealthy = new Date();
|
|
102
|
+
state.consecutiveFailures = 0;
|
|
103
|
+
state.error = undefined;
|
|
104
|
+
}
|
|
105
|
+
else if (result.success && result.resultCount === 0) {
|
|
106
|
+
// Responded but returned nothing — might be a transient issue
|
|
107
|
+
state.consecutiveFailures++;
|
|
108
|
+
state.status =
|
|
109
|
+
state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD
|
|
110
|
+
? 'down'
|
|
111
|
+
: 'degraded';
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
state.consecutiveFailures++;
|
|
115
|
+
state.status =
|
|
116
|
+
state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD
|
|
117
|
+
? 'down'
|
|
118
|
+
: 'degraded';
|
|
119
|
+
}
|
|
120
|
+
state.responseTimeMs = elapsed;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
state.consecutiveFailures++;
|
|
124
|
+
state.status =
|
|
125
|
+
state.consecutiveFailures >= CONSECUTIVE_FAILURE_THRESHOLD
|
|
126
|
+
? 'down'
|
|
127
|
+
: 'degraded';
|
|
128
|
+
state.error =
|
|
129
|
+
error instanceof Error ? error.message : String(error);
|
|
130
|
+
state.responseTimeMs = Date.now() - start;
|
|
131
|
+
}
|
|
132
|
+
state.lastChecked = new Date();
|
|
133
|
+
return { ...state };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get a snapshot summary of all platform health states.
|
|
137
|
+
*/
|
|
138
|
+
getSummary() {
|
|
139
|
+
const platforms = {};
|
|
140
|
+
let hasDown = false;
|
|
141
|
+
let hasDegraded = false;
|
|
142
|
+
for (const [key, state] of this.states) {
|
|
143
|
+
platforms[key] = { ...state };
|
|
144
|
+
if (state.status === 'down')
|
|
145
|
+
hasDown = true;
|
|
146
|
+
if (state.status === 'degraded')
|
|
147
|
+
hasDegraded = true;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
overall: hasDown ? 'down' : hasDegraded ? 'degraded' : 'healthy',
|
|
151
|
+
platforms,
|
|
152
|
+
lastFullCheck: this.lastFullCheck,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get health state for a single platform. Returns undefined if the
|
|
157
|
+
* platform is not being monitored.
|
|
158
|
+
*/
|
|
159
|
+
getState(platform) {
|
|
160
|
+
const state = this.states.get(platform);
|
|
161
|
+
return state ? { ...state } : undefined;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Whether the monitor has recent data (checked within the given ms).
|
|
165
|
+
*/
|
|
166
|
+
hasRecentData(withinMs = DEFAULT_INTERVAL_MS) {
|
|
167
|
+
if (!this.lastFullCheck)
|
|
168
|
+
return false;
|
|
169
|
+
return Date.now() - this.lastFullCheck.getTime() < withinMs;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core interface that all social media platform adapters must implement
|
|
3
|
+
* Provides a unified API for content retrieval across different platforms
|
|
4
|
+
*
|
|
5
|
+
* Analysis types (EnrichedComment, OpinionUnit, etc.) have been moved to
|
|
6
|
+
* the CrowdListen API. This file now only contains retrieval types.
|
|
7
|
+
*/
|
|
8
|
+
export interface Post {
|
|
9
|
+
id: string;
|
|
10
|
+
platform: PlatformType;
|
|
11
|
+
author: User;
|
|
12
|
+
content: string;
|
|
13
|
+
mediaUrl?: string;
|
|
14
|
+
engagement: EngagementMetrics;
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
url: string;
|
|
17
|
+
hashtags?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface User {
|
|
20
|
+
id: string;
|
|
21
|
+
username: string;
|
|
22
|
+
displayName?: string;
|
|
23
|
+
followerCount?: number;
|
|
24
|
+
verified?: boolean;
|
|
25
|
+
profileImageUrl?: string;
|
|
26
|
+
bio?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface EngagementMetrics {
|
|
29
|
+
likes: number;
|
|
30
|
+
comments: number;
|
|
31
|
+
shares?: number;
|
|
32
|
+
views?: number;
|
|
33
|
+
engagementRate?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface Comment {
|
|
36
|
+
id: string;
|
|
37
|
+
author: User;
|
|
38
|
+
text: string;
|
|
39
|
+
timestamp: Date;
|
|
40
|
+
likes: number;
|
|
41
|
+
replies?: Comment[];
|
|
42
|
+
engagement?: {
|
|
43
|
+
upvotes?: number;
|
|
44
|
+
downvotes?: number;
|
|
45
|
+
shares?: number;
|
|
46
|
+
views?: number;
|
|
47
|
+
score?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface ContentAnalysis {
|
|
51
|
+
postId: string;
|
|
52
|
+
platform: PlatformType;
|
|
53
|
+
sentiment?: 'positive' | 'negative' | 'neutral';
|
|
54
|
+
themes?: string[];
|
|
55
|
+
summary?: string;
|
|
56
|
+
commentCount: number;
|
|
57
|
+
topComments: Comment[];
|
|
58
|
+
analysisMetadata?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
export interface CommentCluster {
|
|
61
|
+
id: number;
|
|
62
|
+
theme: string;
|
|
63
|
+
sentiment: 'positive' | 'negative' | 'neutral' | 'mixed';
|
|
64
|
+
comments: Comment[];
|
|
65
|
+
summary: string;
|
|
66
|
+
size: number;
|
|
67
|
+
}
|
|
68
|
+
export interface TrendingHashtag {
|
|
69
|
+
hashtag: string;
|
|
70
|
+
postCount: number;
|
|
71
|
+
engagementScore: number;
|
|
72
|
+
}
|
|
73
|
+
export type PlatformType = 'tiktok' | 'twitter' | 'reddit' | 'instagram' | 'youtube' | 'moltbook' | 'xiaohongshu';
|
|
74
|
+
export interface PlatformCapabilities {
|
|
75
|
+
supportsTrending: boolean;
|
|
76
|
+
supportsUserContent: boolean;
|
|
77
|
+
supportsSearch: boolean;
|
|
78
|
+
supportsComments: boolean;
|
|
79
|
+
supportsAnalysis: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Main interface that all platform adapters must implement
|
|
83
|
+
*/
|
|
84
|
+
export interface SocialMediaPlatform {
|
|
85
|
+
getTrendingContent(limit?: number): Promise<Post[]>;
|
|
86
|
+
getUserContent(userId: string, limit?: number): Promise<Post[]>;
|
|
87
|
+
searchContent(query: string, limit?: number): Promise<Post[]>;
|
|
88
|
+
getContentComments(contentId: string, limit?: number): Promise<Comment[]>;
|
|
89
|
+
analyzeContent(contentId: string, enableClustering?: boolean): Promise<ContentAnalysis>;
|
|
90
|
+
getPlatformName(): PlatformType;
|
|
91
|
+
getSupportedFeatures(): PlatformCapabilities;
|
|
92
|
+
initialize(): Promise<boolean>;
|
|
93
|
+
cleanup(): Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Configuration interface for platform adapters
|
|
97
|
+
*/
|
|
98
|
+
export interface PlatformConfig {
|
|
99
|
+
platform: PlatformType;
|
|
100
|
+
credentials?: Record<string, string>;
|
|
101
|
+
options?: {
|
|
102
|
+
rateLimit?: number;
|
|
103
|
+
timeout?: number;
|
|
104
|
+
retries?: number;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Error types for unified error handling
|
|
109
|
+
*/
|
|
110
|
+
export declare class SocialMediaError extends Error {
|
|
111
|
+
code: string;
|
|
112
|
+
platform: PlatformType;
|
|
113
|
+
statusCode?: number | undefined;
|
|
114
|
+
originalError?: Error | undefined;
|
|
115
|
+
constructor(message: string, code: string, platform: PlatformType, statusCode?: number | undefined, originalError?: Error | undefined);
|
|
116
|
+
}
|
|
117
|
+
export declare class AuthenticationError extends SocialMediaError {
|
|
118
|
+
constructor(platform: PlatformType, originalError?: Error);
|
|
119
|
+
}
|
|
120
|
+
export declare class RateLimitError extends SocialMediaError {
|
|
121
|
+
constructor(platform: PlatformType, originalError?: Error);
|
|
122
|
+
}
|
|
123
|
+
export declare class NotFoundError extends SocialMediaError {
|
|
124
|
+
constructor(platform: PlatformType, resource: string, originalError?: Error);
|
|
125
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core interface that all social media platform adapters must implement
|
|
3
|
+
* Provides a unified API for content retrieval across different platforms
|
|
4
|
+
*
|
|
5
|
+
* Analysis types (EnrichedComment, OpinionUnit, etc.) have been moved to
|
|
6
|
+
* the CrowdListen API. This file now only contains retrieval types.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Error types for unified error handling
|
|
10
|
+
*/
|
|
11
|
+
export class SocialMediaError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
platform;
|
|
14
|
+
statusCode;
|
|
15
|
+
originalError;
|
|
16
|
+
constructor(message, code, platform, statusCode, originalError) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.platform = platform;
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
this.originalError = originalError;
|
|
22
|
+
this.name = 'SocialMediaError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class AuthenticationError extends SocialMediaError {
|
|
26
|
+
constructor(platform, originalError) {
|
|
27
|
+
super(`Authentication failed for ${platform}`, 'AUTH_ERROR', platform, undefined, originalError);
|
|
28
|
+
this.name = 'AuthenticationError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class RateLimitError extends SocialMediaError {
|
|
32
|
+
constructor(platform, originalError) {
|
|
33
|
+
super(`Rate limit exceeded for ${platform}`, 'RATE_LIMIT', platform, undefined, originalError);
|
|
34
|
+
this.name = 'RateLimitError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class NotFoundError extends SocialMediaError {
|
|
38
|
+
constructor(platform, resource, originalError) {
|
|
39
|
+
super(`${resource} not found on ${platform}`, 'NOT_FOUND', platform, undefined, originalError);
|
|
40
|
+
this.name = 'NotFoundError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data normalization utilities for converting platform-specific data
|
|
3
|
+
* into standardized formats
|
|
4
|
+
*/
|
|
5
|
+
import { Post, User, Comment, PlatformType } from '../interfaces/SocialMediaPlatform.js';
|
|
6
|
+
/**
|
|
7
|
+
* Normalize user data from different platforms
|
|
8
|
+
*/
|
|
9
|
+
export declare class DataNormalizer {
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a post from any platform to standard format
|
|
12
|
+
*/
|
|
13
|
+
static normalizePost(rawData: any, platform: PlatformType, baseUrl?: string): Post;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize user data from any platform
|
|
16
|
+
*/
|
|
17
|
+
static normalizeUser(rawData: any, platform: PlatformType): User;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize comment data from any platform
|
|
20
|
+
*/
|
|
21
|
+
static normalizeComment(rawData: any, platform: PlatformType): Comment;
|
|
22
|
+
private static normalizeTikTokPost;
|
|
23
|
+
private static normalizeTikTokUser;
|
|
24
|
+
private static normalizeTikTokComment;
|
|
25
|
+
private static normalizeTwitterPost;
|
|
26
|
+
private static normalizeTwitterUser;
|
|
27
|
+
private static normalizeTwitterComment;
|
|
28
|
+
private static normalizeRedditPost;
|
|
29
|
+
private static normalizeRedditUser;
|
|
30
|
+
private static normalizeRedditComment;
|
|
31
|
+
private static normalizeInstagramPost;
|
|
32
|
+
private static normalizeInstagramUser;
|
|
33
|
+
private static normalizeInstagramComment;
|
|
34
|
+
private static normalizeYouTubePost;
|
|
35
|
+
private static normalizeYouTubeUser;
|
|
36
|
+
private static normalizeYouTubeComment;
|
|
37
|
+
/**
|
|
38
|
+
* Extract hashtags from text
|
|
39
|
+
*/
|
|
40
|
+
private static extractHashtags;
|
|
41
|
+
/**
|
|
42
|
+
* Calculate engagement rate
|
|
43
|
+
*/
|
|
44
|
+
static calculateEngagementRate(likes: number, comments: number, shares: number | undefined, followerCount: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* Sanitize and validate text content
|
|
47
|
+
*/
|
|
48
|
+
static sanitizeText(text: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Normalize timestamp from various formats
|
|
51
|
+
*/
|
|
52
|
+
static normalizeTimestamp(timestamp: any): Date;
|
|
53
|
+
}
|