import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors'; export function stripHtml(html) { if (!html) return ''; const decoded = html .replace(/\\u003c/g, '<') .replace(/\\u003e/g, '>') .replace(/\\n/g, '\n') .replace(/\\r/g, ''); return decoded.replace(/<[^>]+>/g, '').trim(); } export function decodeHtmlEntities(html) { if (!html) return ''; return html.replace(/ /g, ' ') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'"); } export function getHupuThreadUrl(tid) { return `https://bbs.hupu.com/${encodeURIComponent(String(tid))}-1.html`; } export function getHupuSearchUrl(query, page, forum, sort) { const searchParams = new URLSearchParams(); searchParams.append('q', String(query)); searchParams.append('page', String(page)); if (forum) { searchParams.append('topicId', String(forum)); } if (sort) { searchParams.append('sortby', String(sort)); } return `https://bbs.hupu.com/search?${searchParams.toString()}`; } export async function readHupuNextData(page, url, actionLabel, options = {}) { await page.goto(url); const result = await page.evaluate(` (async () => { const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const expectedTid = ${JSON.stringify(options.expectedTid || '')}; const timeoutMs = ${JSON.stringify(options.timeoutMs ?? 5000)}; let lastSeenTid = ''; let lastSeenHref = ''; const waitFor = async (predicate, limitMs = timeoutMs) => { const start = Date.now(); while (Date.now() - start < limitMs) { if (predicate()) return true; await wait(100); } return false; }; const ready = await waitFor(() => { const script = document.getElementById('__NEXT_DATA__'); if (!script?.textContent) return false; lastSeenHref = location.href; try { const parsed = JSON.parse(script.textContent); const threadTid = parsed?.props?.pageProps?.detail?.thread?.tid; lastSeenTid = typeof threadTid === 'string' ? threadTid : ''; if (!expectedTid) return true; return threadTid === expectedTid; } catch { return false; } }); if (!ready) { return { ok: false, error: expectedTid ? \`帖子数据未就绪或tid不匹配(expected=\${expectedTid}, actual=\${lastSeenTid || 'unknown'}, href=\${lastSeenHref || location.href})\` : '无法找到帖子数据' }; } try { const text = document.getElementById('__NEXT_DATA__')?.textContent || ''; return { ok: true, data: JSON.parse(text) }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } })() `); if (!result || typeof result !== 'object' || !result.ok) { throw new CommandExecutionError(`${actionLabel} failed: ${result?.error || 'invalid browser response'}`); } return result.data; } export async function readHupuSearchData(page, url, actionLabel) { await page.goto(url); const result = await page.evaluate(` (async () => { const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const waitFor = async (predicate, timeoutMs = 5000) => { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (predicate()) return true; await wait(100); } return false; }; const extractFromScript = () => { const marker = 'window.$$data='; for (const script of Array.from(document.scripts)) { const text = script.textContent || ''; const dataIndex = text.indexOf(marker); if (dataIndex === -1) continue; const jsonStart = dataIndex + marker.length; let braceCount = 0; let jsonEnd = jsonStart; let inString = false; let escapeNext = false; for (let i = jsonStart; i < text.length; i++) { const char = text[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (!inString) { if (char === '{') { braceCount++; } else if (char === '}') { braceCount--; if (braceCount === 0) { jsonEnd = i; break; } } } } if (jsonEnd > jsonStart) { return text.substring(jsonStart, jsonEnd + 1); } } return ''; }; const ready = await waitFor(() => { return typeof window.$$data !== 'undefined' || Boolean(extractFromScript()); }); if (!ready) { return { ok: false, error: '无法找到搜索数据' }; } try { if (typeof window.$$data !== 'undefined') { return { ok: true, data: JSON.parse(JSON.stringify(window.$$data)) }; } const jsonString = extractFromScript(); return { ok: true, data: JSON.parse(jsonString) }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } })() `); if (!result || typeof result !== 'object' || !result.ok) { throw new CommandExecutionError(`${actionLabel} failed: ${result?.error || 'invalid browser response'}`); } return result.data; } function buildBrowserJsonPostScript(apiUrl, body, mode) { return ` (async () => { const url = ${JSON.stringify(apiUrl)}; const payload = ${JSON.stringify(body)}; const mode = ${JSON.stringify(mode)}; const getCookie = (name) => document.cookie .split('; ') .find((item) => item.startsWith(name + '=')) ?.slice(name.length + 1) || ''; const findThumbcacheValue = () => { const rawEntry = document.cookie .split('; ') .find((item) => item.startsWith('.thumbcache_')); if (rawEntry && rawEntry.includes('=')) { const rawValue = rawEntry.slice(rawEntry.indexOf('=') + 1); try { return decodeURIComponent(rawValue); } catch { return rawValue; } } const storageKey = Object.keys(localStorage).find((key) => key.startsWith('.thumbcache_')); if (!storageKey) return ''; return localStorage.getItem(storageKey) || ''; }; const resolveDefaultPayload = (input) => { const next = { ...input }; const sensorsRaw = decodeURIComponent(getCookie('sensorsdata2015jssdkcross') || ''); let deviceid = ''; try { const sensors = JSON.parse(sensorsRaw); deviceid = sensors?.props?.['$device_id'] || sensors?.distinct_id || ''; } catch {} if ((next.puid === '' || next.puid == null) && getCookie('ua')) { next.puid = getCookie('ua'); } if ((next.shumei_id === '' || next.shumei_id == null) && getCookie('smidV2')) { next.shumei_id = getCookie('smidV2'); } if ((next.deviceid === '' || next.deviceid == null) && deviceid) { next.deviceid = deviceid; } return next; }; const resolveReplyPayload = (input) => { const next = { ...input }; const thumbcache = findThumbcacheValue(); if ((next.shumeiId === '' || next.shumeiId == null) && thumbcache) { next.shumeiId = thumbcache; } if ((next.deviceid === '' || next.deviceid == null) && thumbcache) { next.deviceid = thumbcache; } return next; }; const resolvedPayload = mode === 'reply' ? resolveReplyPayload(payload) : resolveDefaultPayload(payload); try { const response = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(resolvedPayload) }); const text = await response.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = text ? { message: text } : null; } return { ok: response.ok, status: response.status, data }; } catch (error) { return { ok: false, error: error instanceof Error ? error.message : String(error) }; } })() `; } /** * Execute authenticated Hupu JSON requests inside the browser page so * cookies and the thread referer come from the live logged-in session. */ export async function postHupuJson(page, tid, apiUrl, body, actionLabel, mode = 'default') { const referer = getHupuThreadUrl(tid); await page.goto(referer); const result = await page.evaluate(buildBrowserJsonPostScript(apiUrl, body, mode)); if (!result || typeof result !== 'object') { throw new CommandExecutionError(`${actionLabel} failed: invalid browser response`); } if (result.status === 401 || result.status === 403) { throw new AuthRequiredError('bbs.hupu.com', `${actionLabel} failed: please log in to Hupu first`); } if (result.error) { throw new CommandExecutionError(`${actionLabel} failed: ${result.error}`); } if (!result.ok) { const detail = result.data?.msg || result.data?.message || `HTTP ${result.status ?? 'unknown'}`; throw new CommandExecutionError(`${actionLabel} failed: ${detail}`); } return result.data ?? {}; }