feat: social-mcp 初始实现

多平台社交自动化 MCP 服务,首批支持小红书。

- 13 个 MCP 工具:登录管理、内容浏览、发布、互动
- 13 个 REST API 端点,支持 Bearer token 认证和限流
- BrowserManager:串行队列、背压、崩溃恢复
- Cookie 持久化:原子写入、0600 权限
- 安全:DNS rebinding 防御、错误脱敏、深层日志 redact
- Docker 部署支持
- 28 个单元测试全部通过
This commit is contained in:
2026-02-28 22:57:22 +08:00
commit 8da5f40c9f
38 changed files with 11273 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
import os from 'node:os';
import path from 'node:path';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function envString(key: string, fallback: string): string {
return process.env[key] ?? fallback;
}
function envInt(key: string, fallback: number): number {
const raw = process.env[key];
if (raw === undefined) return fallback;
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) {
// eslint-disable-next-line no-console
console.error(`[config] Invalid integer for ${key}="${raw}", using default ${fallback}`);
return fallback;
}
return parsed;
}
function envBool(key: string, fallback: boolean): boolean {
const raw = process.env[key];
if (raw === undefined) return fallback;
// Accept common truthy / falsy strings
if (['true', '1', 'yes'].includes(raw.toLowerCase())) return true;
if (['false', '0', 'no'].includes(raw.toLowerCase())) return false;
return fallback;
}
// ---------------------------------------------------------------------------
// HOST safety check — must run before exporting config
// ---------------------------------------------------------------------------
const host = envString('HOST', '127.0.0.1');
if (host === '0.0.0.0' || host === '::') {
const allow = process.env['ALLOW_REMOTE'];
if (allow !== 'yes-i-understand-the-risk') {
// Use console.error directly — the logger module depends on config,
// so it is not available yet at this point.
// eslint-disable-next-line no-console
console.error(
`[FATAL] HOST is set to "${host}" which exposes the service to the network.\n` +
`If you really intend to do this, set ALLOW_REMOTE=yes-i-understand-the-risk\n` +
`Refusing to start.`,
);
process.exit(1);
}
}
// ---------------------------------------------------------------------------
// Operation timeouts (milliseconds)
// Matches the tiers described in PLAN.md section 6.1
// ---------------------------------------------------------------------------
const operationTimeouts: Record<string, number> = {
like: 15_000, // 15s — quick interactions
favorite: 15_000, // 15s
comment: 20_000, // 20s
reply: 20_000, // 20s
feed_list: 30_000, // 30s — page load + extraction
search: 30_000, // 30s
feed_detail: 60_000, // 60s — includes scroll loading
user_profile: 60_000, // 60s
publish: 300_000, // 5min — upload may be slow
login: 300_000, // 5min — user interaction
default: 60_000, // 1min — fallback
};
// ---------------------------------------------------------------------------
// Config type
// ---------------------------------------------------------------------------
export interface AppConfig {
/** HTTP port */
port: number;
/** HTTP bind address */
host: string;
/** Run browser in headless mode */
headless: boolean;
/** Custom browser executable path (optional) */
browserBin: string | undefined;
/** Pino log level */
logLevel: string;
/** NODE_ENV */
nodeEnv: string;
/** Directory for per-platform cookie storage */
cookieDir: string;
/** Max pending operations per platform queue */
maxQueueDepth: number;
/** Per-operation-type timeout in ms */
operationTimeouts: Record<string, number>;
}
// ---------------------------------------------------------------------------
// Exported config singleton
// ---------------------------------------------------------------------------
export const config: AppConfig = {
port: envInt('PORT', 3000),
host,
headless: envBool('HEADLESS', true),
browserBin: process.env['BROWSER_BIN'] || undefined,
logLevel: envString('LOG_LEVEL', 'info'),
nodeEnv: envString('NODE_ENV', 'development'),
cookieDir: envString('COOKIE_DIR', path.join(os.homedir(), '.social-mcp')),
maxQueueDepth: envInt('MAX_QUEUE_DEPTH', 10),
operationTimeouts,
};