#!/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 || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, ''); if (!MASTER_KEY) throw new Error('缺少 XIAOSHU_MASTER_KEY'); async function request(url, init = {}, master = false) { const response = await fetch(url, { ...init, headers: { 'X-Parse-Application-Id': APP_ID, ...(master ? { 'X-Parse-Master-Key': MASTER_KEY } : {}), 'Content-Type': 'application/json', ...(init.headers || {}) } }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(`${response.status}: ${payload.message || payload.error || JSON.stringify(payload)}`); return payload; } async function call(path, token, params, expectedStatus = 200) { const response = await fetch(`${FUNCTION_URL}/${path}`, { method: 'POST', headers: { 'X-Parse-Application-Id': APP_ID, 'Content-Type': 'application/json' }, body: JSON.stringify({ token, params }) }); const payload = await response.json().catch(() => ({})); if (response.status !== expectedStatus) throw new Error(`${path} 期望 ${expectedStatus},实际 ${response.status}: ${payload.message || payload.error || ''}`); if (expectedStatus === 200 && payload.success !== true) throw new Error(payload.message || `${path} 返回失败`); return payload; } let userId = ''; try { const companies = await request(`${PARSE_URL}/classes/Company?limit=1&keys=objectId`, {}, true); const companyId = companies.results?.[0]?.objectId; if (!companyId) throw new Error('没有可用于运营网关测试的帐套'); const company = { __type: 'Pointer', className: 'Company', objectId: companyId }; const username = `codex_ops_smoke_${Date.now()}`; const password = randomBytes(24).toString('base64url'); const created = await request(`${PARSE_URL}/users`, { method: 'POST', body: JSON.stringify({ username, password, isAdmin: true, roles: ['admin', 'ops-manager'], company }) }, true); userId = created.objectId; const login = await request(`${PARSE_URL}/login`, { method: 'POST', body: JSON.stringify({ username, password }) }); const token = login.sessionToken; if (!token) throw new Error('临时运营管理员未返回会话'); const dashboard = (await call('xiaoshu/ops/gateway', token, { operation: 'ops/dashboard/summary' })).data; if (dashboard.identity?.operationsRole !== 'ops-manager' || !Array.isArray(dashboard.metrics) || dashboard.metrics.length < 4) throw new Error('运营工作台返回结构无效'); const users = (await call('xiaoshu/ops/gateway', token, { operation: 'ops/users/list', page: 1, pageSize: 20, search: '' })).data; if (!Array.isArray(users.items) || !users.items.length || users.items.some((item) => 'legacyUserData' in item || 'legacyPasswordHash' in item || 'ACL' in item)) throw new Error('用户业务投影为空或泄漏技术字段'); const named = (await call('xiaoshu/ops/gateway', token, { operation: 'ops/users/list', page: 1, pageSize: 20, search: '万宇昊' })).data; if (!named.items?.some((item) => item.displayName === '万宇昊')) throw new Error('用户显示名称兜底未找到“万宇昊”'); for (const operation of ['ops/relations/list', 'ops/course-bindings/list', 'ops/appointments/list', 'ops/lessons/list', 'ops/learning-records/list', 'ops/finance/list', 'ops/money-logs/list']) { const page = (await call('xiaoshu/ops/gateway', token, { operation, page: 1, pageSize: 5, search: '' })).data; if (!Array.isArray(page.items) || typeof page.total !== 'number') throw new Error(`${operation} 未返回统一分页结构`); } const denied = await call('xiaoshu/admin/gateway', token, { operation: 'catalog' }, 403); if (denied.success !== false) throw new Error('普通运营管理员仍可访问技术数据目录'); console.log(`Operations smoke passed: users=${users.total}, metrics=${dashboard.metrics.length}.`); } finally { if (userId) { const sessions = await request(`${PARSE_URL}/classes/_Session?where=${encodeURIComponent(JSON.stringify({ user: { __type: 'Pointer', className: '_User', objectId: userId } }))}&limit=1000`, {}, true).catch(() => ({ results: [] })); for (const session of sessions.results || []) await request(`${PARSE_URL}/classes/_Session/${session.objectId}`, { method: 'DELETE' }, true).catch(() => undefined); await request(`${PARSE_URL}/users/${userId}`, { method: 'DELETE' }, true).catch(() => undefined); } }