#!/usr/bin/env node import { randomBytes } from 'node:crypto'; const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main'; const MASTER_KEY = process.env.XIAOSHU_MASTER_KEY || ''; const PARSE_URL = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, ''); const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || `${new URL(PARSE_URL).origin}/api/functions`).replace(/\/$/, ''); const COMPANY_ID = '7pIbDBJmKx'; if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY'); const appHeaders = { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' }; const masterHeaders = { ...appHeaders, 'X-Parse-Master-Key': MASTER_KEY }; const company = { __type: 'Pointer', className: 'Company', objectId: COMPANY_ID }; const userIds = []; async function request(url, init = {}, expected = 200) { const response = await fetch(url, init); const payload = await response.json().catch(() => ({})); if (response.status !== expected) throw new Error(`${url} 期望 ${expected},实际 ${response.status}: ${payload.retmsg || payload.error || ''}`); return payload; } async function master(path, init = {}, expected = 200) { return request(`${PARSE_URL}${path}`, { ...init, headers: { ...masterHeaders, ...(init.headers || {}) } }, expected); } async function cloud(token, action, params = {}, expected = 200) { return request(`${FUNCTION_URL}/xiaoshu/app/gateway`, { method: 'POST', headers: appHeaders, body: JSON.stringify({ ...(token ? { token } : {}), params: { action, ...params } }), }, expected); } async function first(className, where, keys) { const query = new URLSearchParams({ where: JSON.stringify(where), limit: '1', keys }); return (await master(`/classes/${className}?${query}`)).results?.[0] || null; } try { const memory = await first('MemoryPracticeRecord', { company, yhid: { $gt: 0 }, plid: { $gt: 0 } }, 'yhid,plid'); const study = await first('DailyStudyRecord', { company, userId: { $gt: 0 } }, 'userId'); const studyCommon = await first('CommonModel', { company, modelId: 56 }, 'generalId'); const orderCommon = await first('CommonModel', { company, modelId: 54 }, 'generalId'); const practice = await first('PracticeRecord', { company, yhid: { $gt: 0 } }, 'yhid'); if (!memory?.yhid || !study?.userId || !studyCommon?.generalId || !orderCommon?.generalId || !practice?.yhid) throw new Error('生产学习数据样本不完整'); const username = `auth_learning_smoke_${Date.now()}`; const mobile = `139${String(Date.now()).slice(-8)}`; const password = `${randomBytes(24).toString('base64url')}Aa9!`; const created = await master('/users', { method: 'POST', body: JSON.stringify({ username, password, mobile, company, isAdmin: true, role: 'admin', roles: ['admin'], legacyUserId: 990000001, legacyGroupId: 1, testCreatedBy: 'smoke-app-auth-learning' }), }, 201); userIds.push(created.objectId); const login = await cloud('', 'user_login_passwd', { name: mobile, passwd: password }); const sessionToken = String(login.result?.sessionToken || ''); if (!sessionToken.startsWith('r:')) throw new Error('手机号映射登录没有返回有效 Parse 会话'); await cloud('', 'e_get_21list', { uid: memory.yhid, cpage: 1, psize: 1 }, 401); const memoryList = await cloud(sessionToken, 'e_get_21list', { uid: memory.yhid, cpage: 1, psize: 20 }); if (!Array.isArray(memoryList.result) || memoryList.result.length < 1 || Number(memoryList.page?.itemCount) < 1) throw new Error('抗遗忘记录读取为空'); const studyList = await cloud(sessionToken, 'content_list', { modelId: 56, myfield2: `UserId=${study.userId}`, cpage: 1, psize: 20 }); if (!Array.isArray(studyList.result) || studyList.result.length < 1 || Number(studyList.page?.itemCount) < 1) throw new Error('学习记录列表读取为空'); const studyDetail = await cloud(sessionToken, 'e_record_detail', { id: studyCommon.generalId }); if (!Array.isArray(studyDetail.result)) throw new Error('学习记录详情响应格式错误'); const orderDetail = await cloud(sessionToken, 'e_order_detail', { id: orderCommon.generalId }); if (!Array.isArray(orderDetail.result) || !orderDetail.result.length) throw new Error('预约详情读取为空'); const newWords = await cloud(sessionToken, 'e_words_list', { uid: practice.yhid, cpage: 1, psize: 20 }); if (!Array.isArray(newWords.result)) throw new Error('生词本响应格式错误'); const overviewStartedAt = performance.now(); const learningOverview = await cloud(sessionToken, 'app_learning_overview', { uid: study.userId, studentId: study.userId, date: '' }); const learningOverviewMs = Math.round(performance.now() - overviewStartedAt); if (!Array.isArray(learningOverview.result?.records) || !learningOverview.result.records.length) throw new Error('学习记录轻量总览为空'); if (!learningOverview.result?.recordDateCounts || !Array.isArray(learningOverview.result?.courses) || !Array.isArray(learningOverview.result?.schedules)) throw new Error('学习记录轻量总览结构错误'); const companionStartedAt = performance.now(); const companionOverview = await cloud(sessionToken, 'app_companion_overview', { uid: study.userId, mode: 'student', history: false, limit: 500 }); const companionOverviewMs = Math.round(performance.now() - companionStartedAt); if (!Array.isArray(companionOverview.result?.courses) || !Array.isArray(companionOverview.result?.schedules)) throw new Error('伴学轻量总览结构错误'); const accountStartedAt = performance.now(); const accountOverview = await cloud(sessionToken, 'app_account_overview', { uid: memory.yhid }); const accountOverviewMs = Math.round(performance.now() - accountStartedAt); if (!accountOverview.result?.profile || !accountOverview.result?.antiForgetSummary || !accountOverview.result?.companionSummary) throw new Error('我的页面轻量总览结构错误'); const coachUsername = `coach_learning_smoke_${Date.now()}`; const coachPassword = `${randomBytes(24).toString('base64url')}Bb8!`; const coach = await master('/users', { method: 'POST', body: JSON.stringify({ username: coachUsername, password: coachPassword, company, legacyUserId: Number(memory.plid), legacyGroupId: 3, testCreatedBy: 'smoke-app-auth-learning' }), }, 201); userIds.push(coach.objectId); const coachLogin = await cloud('', 'user_login_passwd', { name: coachUsername, passwd: coachPassword }); const coachToken = String(coachLogin.result?.sessionToken || ''); const coachMemory = await cloud(coachToken, 'content_list', { modelId: 60, myfield2: `plid=${memory.plid}`, cpage: 1, psize: 20 }); if (!Array.isArray(coachMemory.result) || coachMemory.result.length < 1) throw new Error('陪练本人抗遗忘任务读取为空'); const coachCompanionStartedAt = performance.now(); const coachCompanionOverview = await cloud(coachToken, 'app_companion_overview', { uid: memory.plid, mode: 'coach', history: false, limit: 500 }); const coachCompanionOverviewMs = Math.round(performance.now() - coachCompanionStartedAt); if (!Array.isArray(coachCompanionOverview.result?.schedules)) throw new Error('陪练排课轻量总览结构错误'); const coachAccountStartedAt = performance.now(); const coachAccountOverview = await cloud(coachToken, 'app_account_overview', { uid: memory.plid }); const coachAccountOverviewMs = Math.round(performance.now() - coachAccountStartedAt); if (!coachAccountOverview.result?.profile || !coachAccountOverview.result?.coachPanel) throw new Error('陪练我的页面轻量总览结构错误'); console.log(JSON.stringify({ passed: true, parseSession: true, privateSessionRequired: true, coachOwnership: true, memoryRecords: memoryList.page?.itemCount, studyList: studyList.page?.itemCount, studyDetail: true, orderDetail: true, newWords: newWords.page?.itemCount, overviewLatencyMs: { learning: learningOverviewMs, companion: companionOverviewMs, account: accountOverviewMs, coachCompanion: coachCompanionOverviewMs, coachAccount: coachAccountOverviewMs } }, null, 2)); } finally { for (const userId of userIds) { const pointer = { __type: 'Pointer', className: '_User', objectId: userId }; const sessions = await master(`/classes/_Session?where=${encodeURIComponent(JSON.stringify({ user: pointer }))}&limit=1000&keys=objectId`).catch(() => ({ results: [] })); for (const session of sessions.results || []) await master(`/classes/_Session/${session.objectId}`, { method: 'DELETE' }).catch(() => undefined); await master(`/users/${userId}`, { method: 'DELETE' }).catch(() => undefined); } }