const VOC_SOCIAL_API_ROOT = 'https://server.fmode.cn/api/voc-social'; const XIAOHONGSHU_APP_BASE_URL = `${VOC_SOCIAL_API_ROOT}/xiaohongshu/app`; function buildUrl(baseUrl, path, query = {}) { const url = new URL(`${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`); Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { url.searchParams.set(key, String(value)); } }); return url.toString(); } function unwrapData(json) { if (!json || typeof json !== 'object') return {}; return json.data?.data || json.data || json; } function pickBusinessStatus(json) { const topCode = Number(json?.code); if (Number.isFinite(topCode) && topCode !== 0 && topCode !== 200) { return topCode; } const nestedCode = Number(json?.data?.status_code); if (Number.isFinite(nestedCode) && nestedCode !== 0 && nestedCode !== 200) { return nestedCode; } return 0; } function redactSecret(value) { return String(value || '') .replace(/Bearer\s+[^"'\s,}]+/gi, 'Bearer [REDACTED]') .replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, 'sk-[REDACTED]') .replace(/\br:[A-Za-z0-9]{4,}\b/g, 'r:[REDACTED]') .replace(/("Authorization"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3') .replace(/("api[_-]?token"\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3'); } async function requestJson(url, token) { const response = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }); const text = await response.text(); let json; try { json = JSON.parse(text); } catch { json = undefined; } const businessStatus = pickBusinessStatus(json); if (!response.ok || businessStatus) { const message = json?.message || json?.msg || json?.data?.msg || text || `HTTP ${response.status}`; const error = new Error(redactSecret(message)); error.httpStatus = businessStatus || response.status || 500; error.responseBody = redactSecret(text); throw error; } return { json, data: unwrapData(json), rawText: text }; } class XiaohongshuApi { constructor(options = {}) { this.baseUrl = options.baseUrl || process.env.XIAOHONGSHU_API_BASE_URL || XIAOHONGSHU_APP_BASE_URL; this.token = options.token; } async searchNotes({ keyword, page = 1, sort = 'popularity_descending', noteType = '_0' }) { const url = buildUrl(this.baseUrl, 'search_notes', { keyword, page, sort, note_type: noteType }); return requestJson(url, this.token); } async getNoteDetail({ noteId }) { const url = buildUrl(this.baseUrl, 'get_note_info', { note_id: noteId }); return requestJson(url, this.token); } async getNoteComments({ noteId, cursor = '' }) { const url = buildUrl(this.baseUrl, 'get_note_comments', { note_id: noteId, cursor }); return requestJson(url, this.token); } } module.exports = { VOC_SOCIAL_API_ROOT, XIAOHONGSHU_APP_BASE_URL, XiaohongshuApi };