import { ArrowDown, ArrowUp, ArrowUpDown, Eye, RefreshCcw, Search, X, } from 'lucide-react'; import {FormEvent, useEffect, useMemo, useState} from 'react'; import { getAdminDatabaseTableRows, getAdminDatabaseTables, } from '../api/adminApiClient'; import type { AdminDatabaseTableRowPayload, AdminDatabaseTableRowsResponse, } from '../api/adminApiTypes'; import {handlePageError} from './pageUtils'; interface AdminDatabaseTablesPageProps { token: string; onUnauthorized: (message?: string) => void; } type SortDirection = 'asc' | 'desc'; export function AdminDatabaseTablesPage({ token, onUnauthorized, }: AdminDatabaseTablesPageProps) { const [tables, setTables] = useState([]); const [tableName, setTableName] = useState(() => readHashTableName()); const [search, setSearch] = useState(''); const [filters, setFilters] = useState(''); const [limit, setLimit] = useState('100'); const [result, setResult] = useState(null); const [detailRow, setDetailRow] = useState(null); const [errorMessage, setErrorMessage] = useState(''); const [copyMessage, setCopyMessage] = useState(''); const [sortColumn, setSortColumn] = useState(''); const [sortDirection, setSortDirection] = useState('asc'); const [isLoadingTables, setIsLoadingTables] = useState(false); const [isLoadingRows, setIsLoadingRows] = useState(false); useEffect(() => { void loadTables(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); useEffect(() => { const nextTableName = readHashTableName(); if (nextTableName) { setTableName(nextTableName); } const handleHashChange = () => { const tableFromHash = readHashTableName(); if (tableFromHash) { setTableName(tableFromHash); void refreshRows(tableFromHash); } }; window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (tables.length && !tableName) { setTableName(tables[0] ?? ''); } }, [tableName, tables]); useEffect(() => { if (tableName) { void refreshRows(tableName); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableName]); const visibleColumns = useMemo(() => { const columns = result?.columns ?? []; if (columns.length) { return columns; } const firstRow = result?.rows[0]; return firstRow ? Object.keys(firstRow.cells) : []; }, [result]); const tableOptions = useMemo(() => { const optionNames = tableName && !tables.includes(tableName) ? [tableName, ...tables] : tables; return optionNames.map(getDatabaseTableHeader); }, [tableName, tables]); const selectedTableHeader = useMemo( () => getDatabaseTableHeader(tableName), [tableName], ); const resultTableHeader = useMemo( () => getDatabaseTableHeader(result?.tableName || tableName), [result?.tableName, tableName], ); const columnHeaders = useMemo( () => visibleColumns.map((column) => getDatabaseTableColumnHeader(tableName, column), ), [tableName, visibleColumns], ); const sortedRows = useMemo(() => { const rows = result?.rows ?? []; if (!sortColumn || !visibleColumns.includes(sortColumn)) { return rows; } return [...rows] .map((row, index) => ({index, row})) .sort((left, right) => { const comparison = compareTableCellValues( left.row.cells[sortColumn], right.row.cells[sortColumn], sortDirection, ); if (comparison !== 0) { return comparison; } return left.index - right.index; }) .map(({row}) => row); }, [result, sortColumn, sortDirection, visibleColumns]); async function loadTables() { setIsLoadingTables(true); setErrorMessage(''); try { const response = await getAdminDatabaseTables(token); setTables(response.tables); if (response.fetchErrors.length) { setErrorMessage(response.fetchErrors.join(';')); } } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setIsLoadingTables(false); } } async function refreshRows( nextTableName = tableName, options: { search?: string; filters?: string; limit?: string; } = {}, ) { const normalizedTableName = nextTableName.trim(); if (!normalizedTableName) { return; } const querySearch = options.search ?? search; const queryFilters = options.filters ?? filters; const queryLimit = options.limit ?? limit; setIsLoadingRows(true); setErrorMessage(''); try { const response = await getAdminDatabaseTableRows(token, normalizedTableName, { search: querySearch, filters: queryFilters, limit: parseLimit(queryLimit), }); setResult(response); setCopyMessage(''); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setIsLoadingRows(false); } } function handleSearch(event: FormEvent) { event.preventDefault(); void refreshRows(); } function handleTableChange(nextTableName: string) { setTableName(nextTableName); const nextHash = `#tables?table=${encodeURIComponent(nextTableName)}`; if (window.location.hash !== nextHash) { window.location.hash = nextHash; } } function handleResetQuery() { setSearch(''); setFilters(''); setLimit('100'); void refreshRows(tableName, {search: '', filters: '', limit: '100'}); } function handleSortColumn(column: string) { if (sortColumn === column) { setSortDirection((currentDirection) => currentDirection === 'asc' ? 'desc' : 'asc', ); return; } setSortColumn(column); setSortDirection('asc'); } async function handleCopyDetailJson() { if (!detailRow) { return; } const copiedText = JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2); try { await navigator.clipboard.writeText(copiedText); setCopyMessage('已复制 JSON'); } catch { setCopyMessage('复制失败,请手动选中后复制'); } } return (

表查询

SpacetimeDB 行数据

已选表:{selectedTableHeader.optionLabel} 显示列:{visibleColumns.length}
{errorMessage ? (
{errorMessage}
) : null}

{resultTableHeader.label}

{result?.totalReturned ?? 0} 条
{columnHeaders.map(({column, label, description}) => { const isSorted = sortColumn === column; return ( ); })} {sortedRows.length ? ( sortedRows.map((row, rowIndex) => ( setDetailRow(row)} > {visibleColumns.map((column) => { const cellValue = formatCellValue(row.cells[column]); return ( ); })} )) ) : ( )}
详情
{cellValue.content}
暂无数据
{detailRow ? (

行详情

{copyMessage ?
{copyMessage}
: null}
              {JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2)}
            
) : null}
); } function readHashTableName() { const hash = window.location.hash; const queryIndex = hash.indexOf('?'); if (queryIndex < 0) { return ''; } return new URLSearchParams(hash.slice(queryIndex + 1)).get('table')?.trim() ?? ''; } function parseLimit(value: string) { const parsed = Number.parseInt(value.trim(), 10); return Number.isFinite(parsed) ? parsed : 100; } function getDatabaseTableHeader(tableName: string): DatabaseTableHeader { const normalizedName = tableName.trim(); if (!normalizedName) { return { description: '尚未选择数据库表。', label: '数据行', name: '', optionLabel: '-', }; } const label = getDatabaseTableLabel(normalizedName); const description = getDatabaseTableDescription(normalizedName, label); return { description: `原始表名:${normalizedName}。${description}。`, label, name: normalizedName, optionLabel: `${label}(${normalizedName})`, }; } function getDatabaseTableLabel(tableName: string) { const exactLabel = databaseTableLabelMap[tableName]; if (exactLabel) { return exactLabel; } const segments = tableName.split('_').filter(Boolean); if (segments.length) { return segments.map(formatDatabaseTableColumnSegment).join(''); } return tableName; } function getDatabaseTableDescription(tableName: string, label: string) { return databaseTableDescriptionMap[tableName] ?? `当前 SpacetimeDB 中的 ${label} 表`; } function getDatabaseTableColumnHeader(tableName: string, column: string) { const normalizedColumn = column.trim(); const label = getDatabaseTableColumnLabel(normalizedColumn); const description = getDatabaseTableColumnDescription( tableName, normalizedColumn, label, ); return {column: normalizedColumn, label, description}; } function getDatabaseTableColumnLabel(column: string) { const normalizedColumn = column.trim(); const exactLabel = databaseTableColumnLabelMap[normalizedColumn]; if (exactLabel) { return exactLabel; } const segments = normalizedColumn.split('_').filter(Boolean); if (segments.length) { return segments.map(formatDatabaseTableColumnSegment).join(''); } return '字段'; } function formatDatabaseTableColumnSegment(segment: string) { return databaseTableColumnSegmentLabelMap[segment] ?? segment; } function getDatabaseTableColumnDescription( tableName: string, column: string, label: string, ) { const exactDescription = databaseTableColumnDescriptionMap[column]; const description = exactDescription ?? `当前表 ${tableName || '未知'} 中的 ${label} 字段`; return `原始字段名:${column}。${description}。点击可按此列排序。`; } function compareTableCellValues( leftValue: unknown, rightValue: unknown, sortDirection: SortDirection, ) { const direction = sortDirection === 'asc' ? 1 : -1; const left = normalizeTableCellSortValue(leftValue); const right = normalizeTableCellSortValue(rightValue); if (left.kind === 'empty' && right.kind === 'empty') { return 0; } if (left.kind === 'empty') { return 1; } if (right.kind === 'empty') { return -1; } if (left.kind !== right.kind) { return direction * (getSortKindOrder(left.kind) - getSortKindOrder(right.kind)); } let comparison = 0; switch (left.kind) { case 'number': comparison = left.value - getSortableNumberValue(right); break; case 'boolean': comparison = Number(left.value) - Number(getSortableBooleanValue(right)); break; case 'text': comparison = tableSortCollator.compare(left.value, getSortableTextValue(right)); break; } return direction * comparison; } function normalizeTableCellSortValue(value: unknown): SortableTableCellValue { if (value === null || typeof value === 'undefined' || value === '') { return {kind: 'empty'}; } if (typeof value === 'number' && Number.isFinite(value)) { return {kind: 'number', value}; } if (typeof value === 'boolean') { return {kind: 'boolean', value}; } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) { return {kind: 'empty'}; } return {kind: 'text', value: trimmed}; } return {kind: 'text', value: stringifyUnknownValue(value)}; } function buildRowKey(row: AdminDatabaseTableRowPayload, rowIndex: number) { const firstValue = Object.values(row.cells)[0]; return `${rowIndex}-${String(firstValue ?? '')}`; } function formatCellValue(value: unknown): FormattedTableCellValue { if (value === null || typeof value === 'undefined' || value === '') { return {content: '-', fullText: '-'}; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { const text = String(value); return {content: text, fullText: text}; } return { content: stringifyUnknownValue(value), fullText: stringifyPrettyUnknownValue(value), }; } function stringifyPrettyUnknownValue(value: unknown) { try { const serialized = JSON.stringify(value, null, 2); return serialized ?? String(value); } catch { return String(value); } } function stringifyUnknownValue(value: unknown) { try { const serialized = JSON.stringify(value); return serialized ?? String(value); } catch { return String(value); } } function getSortKindOrder(kind: SortableTableCellValue['kind']): number { switch (kind) { case 'number': return 0; case 'boolean': return 1; case 'text': return 2; case 'empty': return 3; default: return 3; } } type SortableTableCellValue = | {kind: 'empty'} | {kind: 'number'; value: number} | {kind: 'boolean'; value: boolean} | {kind: 'text'; value: string}; interface DatabaseTableHeader { name: string; label: string; optionLabel: string; description: string; } interface FormattedTableCellValue { content: string; fullText: string; } const tableSortCollator = new Intl.Collator('zh-CN', { numeric: true, sensitivity: 'base', }); const databaseTableColumnLabelMap: Record = { id: 'ID', table_name: '表名', row_count: '行数', error_message: '错误信息', fetch_errors: '读取异常', total_returned: '返回总数', columns: '列名列表', rows: '行列表', raw: '原始值', user_id: '用户ID', referee_user_id: '被邀请人ID', invitee_user_id: '被邀请人ID', owner_user_id: '归属用户ID', profile_id: '档案ID', module_key: '模块键', event_id: '事件ID', event_key: '事件键', event_title: '事件名称', scope_kind: '范围类型', scope_id: '范围ID', day_key: '日期键', occurred_at: '发生时间', created_at: '创建时间', updated_at: '更新时间', starts_at: '开始时间', expires_at: '到期时间', last_played_at: '最近游玩时间', published_at: '发布时间', sort_order: '排序值', reward_points: '奖励积分', threshold: '阈值', enabled: '启用状态', status: '状态', description: '说明', title: '标题', name: '名称', points: '积分', total: '总数', count: '数量', metadata_json: '元数据JSON', data_json: '数据JSON', config_json: '配置JSON', draft_json: '草稿JSON', snapshot_json: '快照JSON', payload_json: '载荷JSON', request_payload_json: '请求载荷JSON', latest_structured_payload_json: '最新结构化载荷JSON', result_payload_json: '结果载荷JSON', evidence_json: '凭证JSON', source_asset_ids_json: '源资产ID列表', available_choices_json: '可选项JSON', visible_character_ids_json: '可见角色ID列表', played_profile_ids_json: '已玩档案列表', tags_json: '标签JSON', theme_tags_json: '主题标签JSON', cover_image_src: '封面图', cover_asset_id: '封面资产ID', public_user_code: '陶泥号', public_work_code: '公开作品号', author_public_user_code: '作者陶泥号', author_display_name: '作者昵称', display_name: '显示名', avatar_url: '头像地址', phone_number_masked: '手机号掩码', phone_number_e164: '手机号E164', login_method: '登录方式', binding_status: '绑定状态', token_version: '令牌版本', wallet_balance: '钱包余额', amount_delta: '变动金额', balance_after: '变动后余额', source_type: '来源类型', source_module: '来源模块', source_entity_id: '来源实体ID', request_label: '请求标签', task_kind: '任务类型', failure_message: '失败信息', latest_text_output: '最新文本输出', world_key: '世界键', world_type: '世界类型', world_name: '世界名称', world_title: '世界标题', world_subtitle: '世界副标题', subtitle: '副标题', summary_text: '摘要文本', bottom_tab: '底部Tab', saved_at: '存档时间', visited_at: '访问时间', claimed_at: '领取时间', paid_at: '支付时间', started_at: '开始时间', completed_at: '完成时间', finished_at_ms: '完成时间毫秒', started_at_ms: '开始时间毫秒', elapsed_ms: '耗时毫秒', duration_limit_ms: '时限毫秒', play_count: '游玩次数', clear_count: '通关次数', like_id: '点赞ID', liked_at: '点赞时间', invite_code: '邀请码', inviter_user_id: '邀请人ID', inviter_reward_granted: '邀请人奖励发放状态', invitee_reward_granted: '被邀请人奖励发放状态', bound_at: '绑定时间', search: '关键词', filters: '筛选条件', limit: '条数上限', value: '值', type: '类型', kind: '类型', key: '键', url: '链接地址', path: '路径', message: '消息', content: '内容', text: '文本', source: '来源', target: '目标', asset_id: '资产ID', file_id: '文件ID', task_id: '任务ID', session_id: '会话ID', record_id: '记录ID', created_by: '创建人', updated_by: '更新人', total_count: '总数', max_uses: '最大使用次数', global_used_count: '全局使用次数', allowed_user_ids: '允许用户列表', metadata: '元数据', }; const databaseTableColumnDescriptionMap: Record = { id: '当前记录的唯一标识', table_name: '当前查询的表名', row_count: '当前统计到的行数', error_message: '读取表统计或行数据时返回的错误信息', fetch_errors: '读取表列表时积累的异常信息', total_returned: '本次接口实际返回的行数', columns: '当前结果集返回的字段名列表', rows: '当前结果集返回的行列表', raw: '当前行的原始值', user_id: '当前记录所属用户的标识', referee_user_id: '被邀请人的用户标识', invitee_user_id: '被邀请人的用户标识', owner_user_id: '当前记录归属用户的标识', profile_id: '当前用户档案的标识', module_key: '当前记录所属模块的业务键', event_id: '当前埋点事件的唯一标识', event_key: '埋点事件键', event_title: '埋点事件展示名称', scope_kind: '埋点统计范围类型', scope_id: '埋点统计范围标识', day_key: '按天聚合时使用的日期键', occurred_at: '事件实际发生时间', created_at: '记录创建时间', updated_at: '记录更新时间', starts_at: '生效开始时间', expires_at: '失效或到期时间', last_played_at: '最近一次游玩时间', published_at: '公开发布时间', sort_order: '列表或配置项使用的排序值', reward_points: '完成任务后发放的奖励积分', threshold: '触发完成条件的阈值', enabled: '是否启用', status: '当前状态', description: '字段说明', title: '展示标题', name: '名称', points: '积分或点数', metadata_json: '存放结构化附加信息的 JSON 文本', data_json: '存放业务数据的 JSON 文本', config_json: '当前记录的配置 JSON', draft_json: '当前记录的草稿 JSON', snapshot_json: '当前运行态快照 JSON', payload_json: '事件或任务携带的载荷 JSON', request_payload_json: 'AI 任务请求载荷 JSON', latest_structured_payload_json: 'AI 任务最新结构化输出 JSON', result_payload_json: '生成或编排完成后的结果载荷 JSON', evidence_json: '用户反馈提交时附带的凭证元数据 JSON', source_asset_ids_json: '当前记录引用的源资产 ID 列表', available_choices_json: '当前运行态可选项 JSON', visible_character_ids_json: '当前运行态可见角色 ID 列表', played_profile_ids_json: '当前运行态已串联游玩的档案 ID 列表', tags_json: '标签列表 JSON', theme_tags_json: '主题标签列表 JSON', cover_image_src: '封面图片地址或平台资产引用', cover_asset_id: '封面对应的平台资产 ID', public_user_code: '用户对外展示的陶泥号', public_work_code: '作品公开后对外展示的作品号', author_public_user_code: '作者对外展示的陶泥号', author_display_name: '作者展示昵称', display_name: '用户展示名称', avatar_url: '用户头像地址', phone_number_masked: '脱敏后的手机号', phone_number_e164: 'E.164 格式手机号', login_method: '账号最近或主要登录方式', binding_status: '账号绑定状态', token_version: '用于令牌吊销和刷新控制的版本号', wallet_balance: '当前钱包余额', amount_delta: '本次钱包流水的增减值', balance_after: '本次变更后的钱包余额', source_type: '当前记录的来源类型', source_module: '触发 AI 任务的来源模块', source_entity_id: '触发 AI 任务的来源实体 ID', request_label: 'AI 任务请求展示标签', task_kind: 'AI 任务类型', failure_message: '失败时记录的错误信息', latest_text_output: 'AI 任务最近一次文本输出', world_key: '世界或作品在运行态中的稳定键', world_type: '世界或作品类型', world_name: '世界名称', world_title: '世界标题', world_subtitle: '世界副标题', subtitle: '副标题', summary_text: '摘要文本', bottom_tab: '保存快照时所在的底部 Tab', saved_at: '用户保存存档的时间', visited_at: '用户访问该作品或世界的时间', claimed_at: '奖励领取时间', paid_at: '订单支付时间', started_at: '任务或流程开始时间', completed_at: '任务或流程完成时间', finished_at_ms: '运行态完成时的毫秒时间戳', started_at_ms: '运行态开始时的毫秒时间戳', elapsed_ms: '运行态已消耗毫秒数', duration_limit_ms: '运行态时间限制毫秒数', play_count: '作品累计游玩次数', clear_count: '累计通关次数', like_id: '点赞记录唯一标识', liked_at: '点赞发生时间', invite_code: '完成绑定时使用的邀请码', inviter_user_id: '邀请人的用户标识', inviter_reward_granted: '邀请人奖励是否已经发放', invitee_reward_granted: '被邀请人奖励是否已经发放', bound_at: '邀请关系绑定时间', search: '用于本地过滤的关键词', filters: '用于等值筛选的 JSON 条件', limit: '本次查询请求的条数上限', value: '当前字段值', type: '当前字段类型', kind: '当前字段种类', key: '用于稳定识别记录或业务项的键', url: '链接地址或资源地址', path: '资源路径或业务路径', message: '消息内容或错误信息', content: '主要内容', text: '文本内容', source: '来源字段', target: '目标字段', asset_id: '平台资产对象标识', file_id: '文件标识', task_id: '任务标识', session_id: '会话标识', record_id: '记录标识', created_by: '创建该记录的主体', updated_by: '最后更新该记录的主体', total_count: '累计总数', max_uses: '允许的最大使用次数', global_used_count: '当前已使用次数', allowed_user_ids: '被允许使用的用户 ID 列表', metadata: '结构化附加信息', }; const databaseTableColumnSegmentLabelMap: Record = { account: '账户', action: '动作', active: '启用', actor: '参与者', added: '新增', address: '地址', after: '后', agent: 'Agent', ai: 'AI', amount: '金额', anchor: '锚点', archive: '存档', assistant: '助手', at: '时间', asset: '资产', attach: '附加', attachment: '附件', author: '作者', available: '可用', balance: '余额', before: '前', best: '最佳', binding: '绑定', blockers: '阻断项', body: '正文', bottom: '底部', built: '构建', bucket: 'Bucket', cache: '缓存', card: '卡片', cents: '分', channel: '渠道', chapter: '章节', character: '角色', chat: '聊天', choice: '选项', claim: '领取', claimed: '领取', claimant: '领取人', cleared: '已清除', client: '客户端', code: '编码', completed: '完成', content: '内容', count: '数量', create: '创建', created: '创建', current: '当前', custom: '自定义', data: '数据', date: '日期', day: '日期', default: '默认', delta: '变动', delete: '删除', deleted: '删除', description: '说明', detail: '详情', device: '设备', difficulty: '难度', display: '展示', draft: '草稿', elapsed: '耗时', enabled: '启用', entity: '实体', entry: '记录', error: '错误', evidence: '凭证', event: '事件', expires: '到期', expire: '到期', expired: '到期', failure: '失败', file: '文件', filter: '筛选', first: '首个', flags: '标记', flow: '流程', form: '表单', game: '游戏', granted: '发放', global: '全局', grid: '网格', group: '分组', hash: '哈希', history: '历史', host: '主机', id: 'ID', image: '图片', info: '信息', initial: '初始', input: '输入', invite: '邀请', invitee: '被邀请人', inviter: '邀请人', issued: '签发', item: '物品', job: '任务', json: 'JSON', key: '键', kind: '类型', last: '最近', ledger: '流水', level: '关卡', liked: '点赞', limit: '条数上限', line: '行', link: '链接', list: '列表', login: '登录', main: '主', max: '最大', membership: '会员', message: '消息', metadata: '元数据', metrics: '指标', module: '模块', mode: '模式', name: '名称', next: '下一个', npc: 'NPC', note: '备注', number: '编号', object: '对象', observed: '观察', order: '订单', owner: '归属', pack: '包', paid: '支付', password: '密码', payment: '支付', percent: '百分比', phone: '手机', phase: '阶段', path: '路径', pending: '待处理', picture: '图片', platform: '平台', points: '积分', policy: '策略', product: '商品', profile: '档案', progress: '进度', prompt: '提示词', provider: '提供方', publication: '发布', publish: '发布', payload: '载荷', public: '公开', query: '查询', ready: '就绪', raw: '原始', record: '记录', reference: '引用', referral: '邀请关系', refresh: '刷新', register: '注册', relation: '关系', reply: '回复', request: '请求', response: '响应', reward: '奖励', row: '行', rows: '行', run: '运行', runtime: '运行态', save: '存档', saved: '存档', search: '搜索', seed: '种子', seen: '可见', sequence: '序号', session: '会话', shape: '形状', share: '分享', slot: '槽位', snapshot: '快照', sort: '排序', status: '状态', source: '来源', starts: '开始', started: '开始', state: '状态', step: '步骤', structured: '结构化', summary: '摘要', task: '任务', target: '目标', text: '文本', time: '时间', title: '标题', total: '总数', type: '类型', unique: '唯一', update: '更新', updated: '更新', url: '链接', user: '用户', value: '值', version: '版本', visible: '可见', wallet: '钱包', wechat: '微信', work: '作品', world: '世界', xp: '经验', occurred: '发生', played: '游玩', published: '发布时间', returned: '返回', rewardpoints: '奖励积分', result: '结果', read: '读取', write: '写入', totalreturned: '返回总数', }; const databaseTableLabelMap: Record = { database_migration_operator: '数据库迁移操作员', database_migration_import_chunk: '数据库迁移分片', auth_store_snapshot: '认证仓储快照', user_account: '用户账号', auth_identity: '身份绑定', refresh_session: '刷新会话', runtime_setting: '运行时设置', runtime_snapshot: '运行时快照', user_browse_history: '浏览历史', profile_dashboard_state: '个人主页状态', profile_wallet_ledger: '钱包流水', analytics_date_dimension: '日期维表', tracking_event: '埋点事件', tracking_daily_stat: '埋点日统计', profile_task_config: '个人任务配置', profile_task_progress: '个人任务进度', profile_task_reward_claim: '个人任务领奖', profile_redeem_code: '兑换码', profile_redeem_code_usage: '兑换码使用记录', profile_invite_code: '邀请码', profile_referral_relation: '邀请关系', profile_played_world: '已玩世界', public_work_play_daily_stat: '公开作品日游玩统计', public_work_like: '公开作品点赞', profile_membership: '会员状态', profile_recharge_order: '充值订单', profile_feedback_submission: '反馈提交', profile_save_archive: '存档记录', story_session: '剧情会话', story_event: '剧情事件', npc_state: 'NPC 状态', inventory_slot: '背包槽位', battle_state: '战斗状态', treasure_record: '宝藏记录', quest_record: '任务记录', quest_log: '任务日志', player_progression: '玩家进度', chapter_progression: '章节进度', custom_world_profile: '自定义世界档案', custom_world_session: '自定义世界会话', custom_world_agent_session: '自定义世界 Agent 会话', custom_world_agent_message: '自定义世界 Agent 消息', custom_world_agent_operation: '自定义世界 Agent 操作', custom_world_draft_card: '自定义世界草稿卡片', custom_world_gallery_entry: '自定义世界画廊条目', puzzle_agent_session: '拼图 Agent 会话', puzzle_agent_message: '拼图 Agent 消息', puzzle_work_profile: '拼图作品档案', puzzle_event: '拼图事件', puzzle_runtime_run: '拼图运行记录', puzzle_leaderboard_entry: '拼图排行榜条目', match3d_agent_session: '抓大鹅 Agent 会话', match3d_agent_message: '抓大鹅 Agent 消息', match3d_work_profile: '抓大鹅作品档案', match3d_runtime_run: '抓大鹅运行记录', square_hole_agent_session: '方洞挑战 Agent 会话', square_hole_agent_message: '方洞挑战 Agent 消息', square_hole_work_profile: '方洞挑战作品档案', square_hole_runtime_run: '方洞挑战运行记录', visual_novel_agent_session: '视觉小说 Agent 会话', visual_novel_agent_message: '视觉小说 Agent 消息', visual_novel_work_profile: '视觉小说作品档案', visual_novel_runtime_run: '视觉小说运行记录', visual_novel_runtime_history_entry: '视觉小说历史条目', visual_novel_runtime_event: '视觉小说运行事件', big_fish_creation_session: '大鱼吃小鱼创建会话', big_fish_agent_message: '大鱼吃小鱼 Agent 消息', big_fish_asset_slot: '大鱼吃小鱼资产槽位', big_fish_event: '大鱼吃小鱼事件', big_fish_runtime_run: '大鱼吃小鱼运行记录', asset_object: '资产对象', asset_entity_binding: '资产实体绑定', asset_event: '资产事件', ai_task: 'AI 任务', ai_task_stage: 'AI 任务阶段', ai_text_chunk: 'AI 文本分片', ai_result_reference: 'AI 结果引用', ai_task_event: 'AI 任务事件', }; const databaseTableDescriptionMap: Record = { database_migration_operator: '管理数据库迁移导出、导入和增量导入权限的操作员表', database_migration_import_chunk: '大迁移 JSON 分片导入的临时表', auth_store_snapshot: '旧认证仓储的整份 JSON 快照表', user_account: '用户账号主表', auth_identity: '第三方或手机号身份绑定表', refresh_session: '刷新令牌会话表', runtime_setting: '用户运行时设置表', runtime_snapshot: '用户当前运行时快照表', user_browse_history: '用户浏览历史表', profile_dashboard_state: '个人主页聚合状态表', profile_wallet_ledger: '钱包流水账表', analytics_date_dimension: '分析日期维表', tracking_event: '埋点原始事件表', tracking_daily_stat: '埋点按自然日聚合表', profile_task_config: '个人任务配置表', profile_task_progress: '个人任务进度表', profile_task_reward_claim: '个人任务领奖记录表', profile_redeem_code: '运营兑换码表', profile_redeem_code_usage: '兑换码使用记录表', profile_invite_code: '用户邀请中心邀请码表', profile_referral_relation: '邀请关系记录表', profile_played_world: '用户已玩世界记录表', public_work_play_daily_stat: '公开作品日游玩统计表', public_work_like: '公开作品点赞表', profile_membership: '会员状态表', profile_recharge_order: '充值订单表', profile_feedback_submission: '反馈提交记录表', profile_save_archive: '用户存档记录表', story_session: '剧情会话表', story_event: '剧情事件表', npc_state: 'NPC 状态表', inventory_slot: '背包槽位表', battle_state: '战斗状态表', treasure_record: '宝藏记录表', quest_record: '任务记录表', quest_log: '任务日志表', player_progression: '玩家进度表', chapter_progression: '章节进度表', custom_world_profile: '自定义世界档案表', custom_world_session: '自定义世界会话表', custom_world_agent_session: '自定义世界 Agent 会话表', custom_world_agent_message: '自定义世界 Agent 消息表', custom_world_agent_operation: '自定义世界 Agent 操作表', custom_world_draft_card: '自定义世界草稿卡片表', custom_world_gallery_entry: '自定义世界画廊条目表', puzzle_agent_session: '拼图 Agent 会话表', puzzle_agent_message: '拼图 Agent 消息表', puzzle_work_profile: '拼图作品档案表', puzzle_event: '拼图事件表', puzzle_runtime_run: '拼图运行记录表', puzzle_leaderboard_entry: '拼图排行榜条目表', match3d_agent_session: '抓大鹅 Agent 会话表', match3d_agent_message: '抓大鹅 Agent 消息表', match3d_work_profile: '抓大鹅作品档案表', match3d_runtime_run: '抓大鹅运行记录表', square_hole_agent_session: '方洞挑战 Agent 会话表', square_hole_agent_message: '方洞挑战 Agent 消息表', square_hole_work_profile: '方洞挑战作品档案表', square_hole_runtime_run: '方洞挑战运行记录表', visual_novel_agent_session: '视觉小说 Agent 会话表', visual_novel_agent_message: '视觉小说 Agent 消息表', visual_novel_work_profile: '视觉小说作品档案表', visual_novel_runtime_run: '视觉小说运行记录表', visual_novel_runtime_history_entry: '视觉小说历史条目表', visual_novel_runtime_event: '视觉小说运行事件表', big_fish_creation_session: '大鱼吃小鱼创建会话表', big_fish_agent_message: '大鱼吃小鱼 Agent 消息表', big_fish_asset_slot: '大鱼吃小鱼资产槽位表', big_fish_event: '大鱼吃小鱼事件表', big_fish_runtime_run: '大鱼吃小鱼运行记录表', asset_object: '资产对象表', asset_entity_binding: '资产实体绑定表', asset_event: '资产事件表', ai_task: 'AI 任务表', ai_task_stage: 'AI 任务阶段表', ai_text_chunk: 'AI 文本分片表', ai_result_reference: 'AI 结果引用表', ai_task_event: 'AI 任务事件表', }; function getSortableNumberValue(value: SortableTableCellValue) { return isSortableNumberValue(value) ? value.value : 0; } function getSortableBooleanValue(value: SortableTableCellValue) { return isSortableBooleanValue(value) ? value.value : false; } function getSortableTextValue(value: SortableTableCellValue) { return isSortableTextValue(value) ? value.value : stringifyUnknownValue(value); } function isSortableNumberValue( value: SortableTableCellValue, ): value is Extract { return value.kind === 'number'; } function isSortableBooleanValue( value: SortableTableCellValue, ): value is Extract { return value.kind === 'boolean'; } function isSortableTextValue( value: SortableTableCellValue, ): value is Extract { return value.kind === 'text'; }