// 临时脚本:给本地后台灌入假数据的假 API(仅用于人工查看渲染效果与截图)。 // 用法:node scripts/tmp-admin-fake-api.mjs [port] import http from 'node:http'; const port = Number(process.argv[2] ?? 8099); const admin = { subject: 'admin:local', username: 'owner', displayName: '本地体验账号', roles: ['admin', 'owner'], accountRole: 'owner', tabPermissions: [], actionPermissions: [], issuedAt: new Date(Date.now() - 60_000).toISOString(), expiresAt: new Date(Date.now() + 86_400_000).toISOString(), }; const avatar = (text, background) => `data:image/svg+xml;utf8,${encodeURIComponent( `${text}`, )}`; const cover = (label, background) => `data:image/svg+xml;utf8,${encodeURIComponent( `${label}`, )}`; const now = Date.now(); const iso = (offsetMinutes) => new Date(now - offsetMinutes * 60_000).toISOString(); /** 素材查询列表里的假素材。 */ function editorAsset(assetId, label, ownerUserId, authorDisplayName, code) { return { assetId, ownerUserId, authorDisplayName, authorPublicUserCode: code, folderId: 'folder_local', label, assetObjectId: `${assetId}-object`, imageSrc: cover(label, '#b6623f'), objectKey: `assets/${assetId}.png`, width: 1024, height: 1024, sourceType: 'generated', prompt: 'a warm clay-style illustration', actualPrompt: 'a warm clay-style illustration, soft light', model: 'seedream-4', provider: 'vectorengine', taskId: `task_${assetId}`, groupTaskId: null, assetKind: 'image', generationInputs: { size: '1024x1024' }, sourceResourceId: null, sourceImageSrc: null, generationCostMudPoints: 12, refundMudPoints: 0, createdAt: iso(60), updatedAt: iso(30), }; } /** 精选审核列表里的假素材。 */ function showcaseAsset( showcaseId, label, ownerUserId, authorDisplayName, code, ) { return { showcaseId, assetId: `${showcaseId}_asset`, ownerUserId, authorDisplayName, authorPublicUserCode: code, label, imageSrc: cover(label, '#8f4b26'), objectKey: `showcase/${showcaseId}.png`, width: 1024, height: 1536, prompt: 'steampunk town at dusk', actualPrompt: 'steampunk town at dusk, warm clay palette', model: 'seedream-4', provider: 'vectorengine', taskId: `task_${showcaseId}`, sourceResourceId: null, sourceImageSrc: null, sourceObjectKey: null, sourceAssetObjectId: null, sourceLabel: null, assetKind: 'image', generationInputs: { size: '1024x1536' }, thumbnailSrc: null, generationCostMudPoints: 16, refundMudPoints: 0, reviewStatus: 'pending', displayEnabled: false, likeCount: 12, assetDeletedWhilePending: false, reviewedByAdminUserId: null, reviewNote: null, refundLedgerId: null, refundCompletedAt: null, submittedAt: iso(45), }; } const tableStats = [ ['account_profile', 1284, null], ['editor_asset', 8632, null], ['project_snapshot', 412, null], ['tracking_event', 92_431, null], ['wallet_ledger', 5281, null], ['ip_blacklist', null, '不可统计(private 或当前身份不可见)'], ]; const trackingEntries = [ { eventId: 'evt_01HQ8Z3K7M4N2P', eventKey: 'daily_login', eventTitle: '每日登录', scopeKind: 'user', scopeId: 'u_1001', dayKey: 20260929, userId: 'u_1001', ownerUserId: null, profileId: 'p_1001', moduleKey: 'profile', metadataJson: '{"channel":"wechat","days":3}', occurredAt: iso(12), }, { eventId: 'evt_01HQ8Z3K7M4N3Q', eventKey: 'editor_generate_image', eventTitle: '画板生成图片', scopeKind: 'project', scopeId: 'proj_7788', dayKey: 20260929, userId: 'u_1002', // 事件用户与项目归属人通常不是同一个人,假数据按这个口径给。 ownerUserId: 'u_1005', profileId: null, moduleKey: 'editor', metadataJson: '{"model":"seedream-4","size":"2048x2048","cost":12,"ok":true,"seed":null}', occurredAt: iso(48), }, { eventId: 'evt_01HQ8Z3K7M4N4R', eventKey: 'agc_project_publish', eventTitle: '发布独立 App 游戏', scopeKind: 'game', scopeId: 'game_9001', dayKey: 20260928, userId: 'u_1003', ownerUserId: null, profileId: null, moduleKey: 'ai-game-creator', metadataJson: '{"version":7,"platform":"windows"}', occurredAt: iso(600), }, { // 作者在自己项目里生成,事件用户与归属人相同:不该出现第二个重复按钮。 eventId: 'evt_01HQ8Z3K7M4N5S', eventKey: 'editor_generate_video', eventTitle: '画板生成视频', scopeKind: 'project', scopeId: 'proj_9001', dayKey: 20260928, userId: 'u_1003', ownerUserId: 'u_1003', profileId: 'p_1003', moduleKey: 'editor', metadataJson: '{"model":"video-1","seconds":5,"cost":40}', occurredAt: iso(900), }, ]; const inviteEntries = [ { userId: 'u_8801', inviteCode: 'TAONIER-VIP-2026', metadata: { userTags: ['vip', '内测'] }, startsAt: iso(60 * 24 * 7), expiresAt: null, status: 'active', createdAt: iso(60 * 24 * 7), updatedAt: iso(60 * 24 * 2), }, { userId: 'u_8802', inviteCode: 'BETA-CREATOR', metadata: { userTags: ['creator'] }, startsAt: null, expiresAt: iso(-60 * 24 * 3), status: 'active', createdAt: iso(60 * 24 * 20), updatedAt: iso(60 * 24 * 19), }, { userId: 'u_8803', inviteCode: 'EXPIRED-CODE', metadata: {}, startsAt: iso(60 * 24 * 40), expiresAt: iso(60 * 24 * 10), status: 'expired', createdAt: iso(60 * 24 * 40), updatedAt: iso(60 * 24 * 10), }, ]; const inviteOperations = [ { operationId: 'op_1', codeKind: 'invite', code: 'TAONIER-VIP-2026', action: 'create', operatorUserId: 'u_admin', operatorDisplayName: '本地体验账号', createdAt: iso(60 * 24 * 7), }, { operationId: 'op_2', codeKind: 'invite', code: 'BETA-CREATOR', action: 'update', operatorUserId: 'u_admin', operatorDisplayName: '本地体验账号', createdAt: iso(60 * 24 * 19), }, ]; const gates = [ { gateKey: 'agc:template-library', enabled: true, rolloutPercent: 40, allowUserIds: ['u_1001', 'u_1002'], allowUserTags: ['vip'], denyUserIds: [], description: 'AGC 模板库灰度', updatedAt: iso(120), }, { gateKey: 'editor:showcase', enabled: false, rolloutPercent: 0, allowUserIds: [], allowUserTags: [], denyUserIds: ['u_9001'], description: '精选作品投放开关', updatedAt: iso(60 * 30), }, ]; const snapshotItems = [ { userId: 'u_1001', projectId: 'proj_7788', projectName: '陶泥小世界', syncRevision: 12, syncedAtMs: now - 1000 * 60 * 26, fileCount: 184, totalBytes: 24_318_712, status: 'ready', channel: 'dev', authorDisplayName: '阿陶', authorPublicUserCode: 'TN-1001', }, { userId: 'u_1002', projectId: 'proj_8100', projectName: '纸片拼图计划', syncRevision: 3, syncedAtMs: now - 1000 * 60 * 180, fileCount: 59, totalBytes: 5_120_004, status: 'partial', channel: 'dev', authorDisplayName: '小满', authorPublicUserCode: 'TN-1002', }, { userId: 'u_1003', projectId: 'proj_4412', projectName: '三消工坊', syncRevision: 27, syncedAtMs: now - 1000 * 60 * 60 * 30, fileCount: 402, totalBytes: 88_120_330, status: 'unverified', channel: 'dev', authorDisplayName: '老周', authorPublicUserCode: 'TN-1003', }, ]; const errorReports = [ { batchId: 'batch_20260929_01', eventCount: 6, logCount: 2, fingerprint: 'fp_9f2a', source: 'agc-desktop', status: 'new', userId: 'u_1002', createdAt: iso(45), updatedAt: iso(45), }, { batchId: 'batch_20260928_07', eventCount: 3, logCount: 1, fingerprint: 'fp_4b71', source: 'web', status: 'in-progress', userId: 'u_1001', createdAt: iso(60 * 20), updatedAt: iso(60 * 4), }, { batchId: 'batch_20260927_02', eventCount: 11, logCount: 4, fingerprint: 'fp_1c05', source: 'agc-mobile', status: 'resolved', userId: 'u_1004', createdAt: iso(60 * 40), updatedAt: iso(60 * 36), }, ]; const errorReportDetail = { ...errorReports[0], firstFingerprint: 'fp_9f2a', firstSource: 'agc-desktop', note: '已定位到随包 Codex 版本过旧,等待渠道重新发布。', events: [ { level: 'error', message: 'Codex app-server JSON-RPC 失败:items must not be empty', }, { level: 'warn', message: 'provider request retried once' }, ], logNames: ['agc-main.log', 'agc-provider.log'], }; const dashboard = { range: { granularity: 'day', anchorDate: '2026-09-29', periodStartDate: '2026-09-23', periodEndDate: '2026-09-29', periodLabel: '2026-09-23 ~ 2026-09-29', }, metrics: { generatedAssets: 438, consumedMudPoints: 96_400, refundedMudPoints: 3_120, totalRegisteredUsers: 12_480, newRegisteredUsers: 128, newUserPaymentConversion: { paidUsers: 21, newRegisteredUsers: 128, rateBasisPoints: 1_641, }, day1Retention: { eligibleUsers: 1_240, retainedUsers: 508, rateBasisPoints: 4_096, }, day7Retention: { eligibleUsers: 1_102, retainedUsers: 198, rateBasisPoints: 1_796, }, visitUsers: 942, totalVisitUsers: 31_204, visitCount: 5_180, totalVisitCount: 184_902, currentUsers: 63, }, charts: [ { id: 'new_users', title: '新增注册', unit: '人', total: 128, buckets: [ { key: '2026-09-23', label: '09-23', value: 12 }, { key: '2026-09-24', label: '09-24', value: 18 }, { key: '2026-09-25', label: '09-25', value: 15 }, { key: '2026-09-26', label: '09-26', value: 22 }, { key: '2026-09-27', label: '09-27', value: 24 }, { key: '2026-09-28', label: '09-28', value: 21 }, { key: '2026-09-29', label: '09-29', value: 16 }, ], }, { id: 'generated_assets', title: '生成素材', unit: '个', total: 438, buckets: [ { key: '2026-09-23', label: '09-23', value: 52 }, { key: '2026-09-24', label: '09-24', value: 61 }, { key: '2026-09-25', label: '09-25', value: 58 }, { key: '2026-09-26', label: '09-26', value: 70 }, { key: '2026-09-27', label: '09-27', value: 66 }, { key: '2026-09-28', label: '09-28', value: 74 }, { key: '2026-09-29', label: '09-29', value: 57 }, ], }, ], operations: { cards: [ { id: 'paid_orders', label: '支付订单', value: 37, unit: '单' }, { id: 'refund_orders', label: '退款订单', value: 4, unit: '单' }, { id: 'published_games', label: '公开游戏', value: 12, unit: '个' }, ], assetKindBreakdown: [ { key: 'image', label: '图片', value: 268 }, { key: 'video', label: '视频', value: 74 }, { key: 'audio', label: '音频', value: 58 }, { key: 'text', label: '文本', value: 38 }, ], moduleVisitBreakdown: [ { key: 'image-editor', label: '图片画板', value: 2_180 }, { key: 'agc', label: '独立 App', value: 1_640 }, { key: 'profile', label: '个人中心', value: 1_360 }, ], }, warnings: [], generatedAt: new Date().toISOString(), }; const userDetail = { userId: 'u_1001', publicUserCode: 'TN-1001', displayName: '阿陶', avatarUrl: avatar('陶', '#b6623f'), phoneBound: true, createdAt: iso(60 * 24 * 120), lastLoginAt: iso(30), wallet: { balance: 1860, permanentPoints: 1200, spendableBalance: 1660, heldPoints: 200, walletFrozen: false, manualFrozen: false, refundDebtFrozen: false, }, rechargeOrders: [], restrictions: {}, tags: ['vip', '内测'], }; function route(pathname, method, url) { if (pathname === '/admin/api/login' && method === 'POST') { return { token: 'local-fake-token', admin }; } if (pathname === '/admin/api/me') { return { admin }; } if (pathname === '/admin/api/overview') { return { service: { bindHost: '127.0.0.1', bindPort: 8082, jwtIssuer: 'genarrative-local', adminEnabled: true, spacetimeServerUrl: 'http://127.0.0.1:3101', spacetimeDatabase: 'genarrative-dev', }, database: { databaseIdentity: 'local-identity-8f21', ownerIdentity: 'owner-identity-4c90', hostType: 'standalone', schemaTableNames: tableStats.map(([name]) => name), tableStats: tableStats.map(([tableName, rowCount, errorMessage]) => ({ tableName, rowCount, errorMessage, })), fetchErrors: [], }, }; } if (pathname === '/admin/api/dashboard') { return dashboard; } if (pathname === '/admin/api/tracking/events') { return { entries: trackingEntries, total: trackingEntries.length }; } if (pathname === '/admin/api/tracking/event-keys') { return { eventKeys: [ { eventKey: 'daily_login', eventTitle: '每日登录', scopeKinds: ['user'], }, { eventKey: 'editor_generate_image', eventTitle: '画板生成图片', scopeKinds: ['project'], }, { eventKey: 'agc_project_publish', eventTitle: '发布独立 App 游戏', scopeKinds: ['game'], }, ], }; } if (pathname === '/admin/api/profile/invite-codes') { return { entries: inviteEntries, operations: inviteOperations }; } if (pathname === '/admin/api/profile/tasks') { return { entries: [ { taskId: 'task_daily_login', title: '每日登录', description: '登录后到任务中心领取', eventKey: 'daily_login', cycle: 'daily', scopeKind: 'user', threshold: 1, rewardPoints: 20, enabled: true, sortOrder: 1, createdBy: 'u_admin', createdByDisplayName: '本地体验账号', createdAt: iso(60 * 24 * 30), updatedBy: 'u_admin', updatedByDisplayName: '本地体验账号', updatedAt: iso(60 * 6), }, { taskId: 'task_generate_image', title: '生成 3 张图片', description: '画板生成成功后计数', eventKey: 'editor_generate_image', cycle: 'daily', scopeKind: 'project', threshold: 3, rewardPoints: 60, enabled: true, sortOrder: 2, createdBy: 'u_admin', createdByDisplayName: '本地体验账号', createdAt: iso(60 * 24 * 12), updatedBy: 'u_admin', updatedByDisplayName: '本地体验账号', updatedAt: iso(60 * 20), }, ], }; } if (pathname === '/admin/api/agc/tracking-events') { return { entries: [ { eventId: 'agc_evt_01', schemaVersion: 1, eventName: 'workspace_opened', eventTime: iso(25), receivedAt: iso(24), userId: 'u_1001', editorSessionId: 'session_8801', projectId: 'proj_7788', creativeTaskId: null, agentRunId: null, agentTurnId: null, status: 'ok', errorCode: null, source: 'desktop', clientVersion: '0.1.158', properties: { workspace: 'project-development' }, batchId: 'batch_agc_01', }, { eventId: 'agc_evt_02', schemaVersion: 1, eventName: 'codex_runner_started', eventTime: iso(90), receivedAt: iso(89), userId: 'u_1002', editorSessionId: 'session_8802', projectId: 'proj_8100', creativeTaskId: 'task_5566', agentRunId: 'run_778', agentTurnId: 'turn_1', status: 'failed', errorCode: 'RUNNER_SPAWN_FAILED', source: 'desktop', clientVersion: '0.1.154', properties: { exitCode: -1073741502 }, batchId: 'batch_agc_02', }, ], nextCursor: 'agc-cursor-2', }; } if (pathname === '/admin/api/feature-gates') { return { gates }; } if (pathname === '/admin/api/profile/recharge-orders') { const wallet = (overrides) => ({ userId: 'u_1001', totalBalance: 1_860, spendableBalance: 1_660, dailyFreePoints: 0, membershipLimitedPoints: 0, permanentPoints: 1_200, heldPoints: 200, refundDebtPoints: 0, manualFrozen: false, refundDebtFrozen: false, walletFrozen: false, manualRestriction: null, ...overrides, }); const orders = [ { orderId: 'order_20260929_01', userId: 'user-1', user: { userId: 'user-1', publicUserCode: 'TN-1001', displayName: '阿陶', avatarUrl: null, }, productId: 'prod_points_600', productTitle: '600 泥点', productKind: 'points', amountCents: 3_000, paidAmountCents: 3_000, status: 'paid', paymentChannel: 'wechat_native', paidAtMicros: 1_790_640_000_000_000, providerTransactionId: 'wx_42000012342026092901', createdAtMicros: 1_790_640_000_000_000, pointsDelta: 600, cumulativeSuccessRefundCents: 0, targetRecoveryPoints: 0, recoveredPoints: 0, unrecoveredPoints: 0, recoveryStatus: null, refunds: [], activeHold: null, remainingRefundableCents: 3_000, refundEligible: true, refundBlockReasonCode: null, wallet: wallet({}), }, { orderId: 'order_20260928_07', userId: 'user-2', user: { userId: 'user-2', publicUserCode: 'TN-1002', displayName: '小满', avatarUrl: null, }, productId: 'prod_points_1200', productTitle: '1200 泥点 + 赠送 100', productKind: 'points', amountCents: 6_000, paidAmountCents: 6_000, status: 'refunded', paymentChannel: 'wechat_mp', paidAtMicros: 1_790_560_000_000_000, providerTransactionId: 'wx_42000012342026092807', createdAtMicros: 1_790_560_000_000_000, pointsDelta: 1_300, cumulativeSuccessRefundCents: 6_000, targetRecoveryPoints: 1_300, recoveredPoints: 980, unrecoveredPoints: 320, recoveryStatus: 'partial', refunds: [ { refundId: 'refund_01', outRefundNo: 'out_refund_01', status: 'succeeded', amountCents: 6_000, reason: '用户申请退款', createdAtMicros: 1_790_570_000_000_000, updatedAtMicros: 1_790_580_000_000_000, providerRefundId: 'wx_refund_01', lastErrorCode: null, operatorDisplayName: '本地体验账号', }, ], activeHold: null, remainingRefundableCents: 0, refundEligible: false, refundBlockReasonCode: 'already_refunded', wallet: wallet({ userId: 'user-2', totalBalance: 320, spendableBalance: 0, permanentPoints: 0, heldPoints: 320, refundDebtPoints: 320, refundDebtFrozen: true, walletFrozen: true, }), }, ]; return { orders, total: orders.length, limit: 50, offset: 0, scannedCount: orders.length, }; } if (pathname === '/admin/api/profile/recharge-products') { const product = ( productId, title, priceCents, pointsAmount, bonusPoints, ) => ({ productId, title, priceCents, kind: 'points', pointsAmount, bonusPoints, durationDays: 0, badgeLabel: bonusPoints ? `赠送 ${bonusPoints}` : '', description: `${pointsAmount} 泥点`, tier: 'none', membershipPeriodPoints: 0, membershipPeriodDays: 0, membershipQueueLimit: 0, membershipDiscountBps: 0, enabled: true, sortOrder: 1, createdBy: 'u_admin', createdAt: iso(60 * 24 * 30), updatedBy: 'u_admin', updatedAt: iso(60 * 24 * 2), }); return { entries: [ product('prod_points_600', '600 泥点', 3_000, 600, 0), product('prod_points_1200', '1200 泥点 + 赠送 100', 6_000, 1_200, 100), ], }; } if (pathname === '/admin/api/profile/redeem-codes') { return { entries: [ { userId: 'u_9001', code: 'WELCOME-600', rewardPoints: 600, mode: 'once', metadata: { campaign: '2026-09 新玩家' }, startsAt: iso(60 * 24 * 30), expiresAt: null, status: 'active', createdAt: iso(60 * 24 * 30), updatedAt: iso(60 * 24 * 3), }, { userId: 'u_9002', code: 'SUMMER-200', rewardPoints: 200, mode: 'once', metadata: {}, startsAt: null, expiresAt: iso(-60 * 24 * 5), status: 'expired', createdAt: iso(60 * 24 * 90), updatedAt: iso(60 * 24 * 88), }, ], operations: [ { operationId: 'op_redeem_1', codeKind: 'redeem', code: 'WELCOME-600', action: 'create', operatorUserId: 'u_admin', operatorDisplayName: '本地体验账号', createdAt: iso(60 * 24 * 30), }, ], }; } if (pathname === '/admin/api/accounts') { return { accounts: [ { accountId: 'acc_owner', username: 'owner', displayName: '本地体验账号', accountRole: 'owner', enabled: true, tabPermissions: [], createdAt: iso(60 * 24 * 200), updatedAt: iso(60 * 24 * 2), }, { accountId: 'acc_ops', username: 'ops_chen', displayName: '陈运营', accountRole: 'member', enabled: true, tabPermissions: ['tracking', 'error-reports', 'invite'], createdAt: iso(60 * 24 * 90), updatedAt: iso(60 * 24 * 5), }, { accountId: 'acc_review', username: 'review_liu', displayName: '刘审核', accountRole: 'member', enabled: false, tabPermissions: ['editor-showcase', 'game-distribution'], createdAt: iso(60 * 24 * 45), updatedAt: iso(60 * 24 * 30), }, ], }; } if (pathname === '/admin/api/database/tables') { return { tables: [...tableStats.map(([name]) => name), 'external_api_key'], fetchErrors: [], }; } if (pathname === '/admin/api/external-api-keys') { const keys = [ { keyId: 'key_01', ownerUserId: 'u_1001', publicUserCode: 'TN-1001', name: '画板自动化', keyPrefix: 'gnr_live_9f2a', scopes: ['canvas.write', 'asset.read'], createdAt: iso(60 * 24 * 30), lastUsedAt: iso(90), revokedAt: null, updatedAt: iso(90), status: 'active', }, { keyId: 'key_02', ownerUserId: 'u_1002', publicUserCode: 'TN-1002', name: '外部编辑器(已撤销)', keyPrefix: 'gnr_live_4b71', scopes: ['canvas.read'], createdAt: iso(60 * 24 * 60), lastUsedAt: iso(60 * 24 * 20), revokedAt: iso(60 * 24 * 19), updatedAt: iso(60 * 24 * 19), status: 'revoked', }, ]; const sortColumn = url.searchParams.get('sortColumn') ?? 'createdAt'; const sortDirection = (url.searchParams.get('sortDirection') ?? 'desc') === 'asc' ? 1 : -1; const sorted = [...keys].sort( (left, right) => String(left[sortColumn] ?? '').localeCompare( String(right[sortColumn] ?? ''), 'zh-CN', ) * sortDirection, ); return { keys: sorted, total: sorted.length, limit: Number(url.searchParams.get('limit') ?? 50) || 50, offset: Number(url.searchParams.get('offset') ?? 0) || 0, scannedCount: sorted.length, scanLimit: 500, scanLimitReached: false, }; } if (/^\/admin\/api\/database\/tables\/[^/]+\/rows$/.test(pathname)) { const columns = [ 'invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at', ]; const allRows = [ { key: 'row_1', cells: { invitee_user_id: 'u_1001', inviter_user_id: 'u_1002', invite_code: 'TAONIER-VIP-2026', bound_at: iso(120), }, }, { key: 'row_2', cells: { invitee_user_id: 'u_1003', inviter_user_id: 'u_1002', invite_code: 'TAONIER-VIP-2026', bound_at: iso(600), }, }, { key: 'row_3', cells: { invitee_user_id: 'u_1004', inviter_user_id: 'u_1005', invite_code: 'BETA-CREATOR', bound_at: iso(2880), }, }, ]; // 后台的排序是服务端排序,假 API 也按 sortColumn / sortDirection 重排, // 否则本地点表头看不出任何变化。 const sortColumn = url.searchParams.get('sortColumn') ?? ''; const sortDirection = url.searchParams.get('sortDirection') === 'desc' ? -1 : 1; const sortedRows = sortColumn ? [...allRows].sort((left, right) => { const l = String(left.cells[sortColumn] ?? ''); const r = String(right.cells[sortColumn] ?? ''); return l.localeCompare(r, 'zh-CN') * sortDirection; }) : allRows; const limit = Number(url.searchParams.get('limit') ?? 100) || 100; const page = Number(url.searchParams.get('page') ?? 1) || 1; const pageRows = sortedRows.slice((page - 1) * limit, page * limit); return { tableName: 'profile_referral_relation', columns, rows: pageRows, totalReturned: pageRows.length, limit, page, totalMatched: sortedRows.length, scannedCount: sortedRows.length, scanLimit: 50_000, scanLimitReached: false, }; } if (pathname === '/admin/api/agc-templates') { return { revision: 'rev_local_7', writable: true, templates: [ { id: 'tpl_platformer', title: '平台跳跃模板', summary: '横版跳跃基础关卡、金币与终点判定,可直接改名改封面。', tags: ['横版', '内置美术'], runtime: 'web', engine: 'Genarrative Runtime', engineVersion: '1.4.0', templateVersion: '3', enabled: true, coverUrl: cover('平台跳跃', '#b6623f'), zipSizeBytes: 4_318_712, }, { id: 'tpl_puzzle', title: '拼图关模板', summary: '带参考图的拼图关卡,支持难度与提示次数配置。', tags: ['拼图', '休闲'], runtime: 'web', engine: 'Genarrative Runtime', engineVersion: '1.4.0', templateVersion: '2', enabled: false, coverUrl: cover('拼图关', '#8f4b26'), zipSizeBytes: 2_004_120, }, ], }; } if (pathname === '/admin/api/agc-models') { return { revision: 7, defaultModelId: 'model_sol', models: [ { id: 'model_sol', alias: '标准模型', modelId: 'gpt-5.6-sol', enabled: true, }, { id: 'model_terra', alias: '快速模型', modelId: 'gpt-5.6-terra', enabled: true, }, { id: 'model_legacy', alias: '旧版模型', modelId: 'gpt-5.4', enabled: false, }, ], }; } if (pathname === '/admin/api/game-distribution/games') { const game = (gameId, title, authorName, status, playCount, versions) => ({ gameId, title, author: { id: `u_${gameId}`, name: authorName, avatarUrl: null }, status, versionCount: versions.length, playCount, activeVersionId: versions[0]?.versionId ?? null, publicationRevision: versions[0]?.versionNumber ?? 0, createdAt: iso(60 * 24 * 20), updatedAt: iso(60 * 3), versions, }); const version = (versionId, versionNumber, status, bytes) => ({ versionId, versionNumber, status, packageBytes: bytes, packageSha256: `${versionId}0f4e9d1c7b3a5e8f2b6d4c9a1e7f3b5d8c2a4e6f`, createdAt: iso(60 * 24 * 5), reviewedAt: iso(60 * 24 * 4), publishedAt: iso(60 * 24 * 3), reviewReason: null, entryUrl: `https://games.example.com/${versionId}/index.html`, }); return { games: [ game('game_1001', '陶泥小世界', '阿陶', 'published', 345, [ version('ver_1001_7', 7, 'published', 18_400_000), version('ver_1001_6', 6, 'published', 17_900_000), ]), game('game_1002', '纸片拼图计划', '小满', 'suspended', 96, [ version('ver_1002_3', 3, 'suspended', 5_120_004), ]), game('game_1003', '三消工坊', '老周', 'unpublished', 12, []), ], }; } if (pathname === '/admin/api/editor-assets') { return { entries: [ editorAsset('asset_01', '红发精灵立绘', 'u_1001', '阿陶', 'TN-1001'), editorAsset('asset_02', '森林场景背景', 'u_1002', '小满', 'TN-1002'), ], nextCursor: null, }; } if (pathname === '/admin/api/editor-showcase/assets') { return { entries: [ showcaseAsset( 'showcase_01', '蒸汽朋克小镇', 'u_1002', '小满', 'TN-1002', ), showcaseAsset('showcase_02', '纸片森林', 'u_1003', '老周', 'TN-1003'), ], nextCursor: null, }; } if (pathname === '/admin/api/project-snapshots/channels') { return { defaultChannel: 'dev', channels: ['dev', 'release'] }; } if (pathname === '/admin/api/project-snapshots') { return { items: snapshotItems, nextCursor: 'cursor-2' }; } if (pathname === '/admin/api/error-reports') { return { reports: errorReports, total: errorReports.length, offset: 0, limit: 20, hasMore: false, }; } if (pathname.startsWith('/admin/api/error-reports/')) { return errorReportDetail; } if (pathname === '/admin/api/profile/users/detail') { return userDetail; } return {}; } const server = http.createServer((request, response) => { const url = new URL(request.url ?? '/', `http://127.0.0.1:${port}`); const payload = route(url.pathname, request.method ?? 'GET', url); const body = JSON.stringify({ data: payload }); response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'x-genarrative-response-envelope': 'v1', }); response.end(body); }); server.listen(port, '127.0.0.1', () => { console.log(`fake admin api on http://127.0.0.1:${port}`); });