1362 lines
42 KiB
TypeScript
1362 lines
42 KiB
TypeScript
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<string[]>([]);
|
||
const [tableName, setTableName] = useState(() => readHashTableName());
|
||
const [search, setSearch] = useState('');
|
||
const [filters, setFilters] = useState('');
|
||
const [limit, setLimit] = useState('100');
|
||
const [result, setResult] = useState<AdminDatabaseTableRowsResponse | null>(null);
|
||
const [detailRow, setDetailRow] = useState<AdminDatabaseTableRowPayload | null>(null);
|
||
const [errorMessage, setErrorMessage] = useState('');
|
||
const [copyMessage, setCopyMessage] = useState('');
|
||
const [sortColumn, setSortColumn] = useState('');
|
||
const [sortDirection, setSortDirection] = useState<SortDirection>('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<HTMLFormElement>) {
|
||
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 (
|
||
<section className="admin-page admin-page-wide">
|
||
<div className="admin-page-heading">
|
||
<div>
|
||
<h2>表查询</h2>
|
||
<p>SpacetimeDB 行数据</p>
|
||
</div>
|
||
<div className="admin-action-row">
|
||
<button
|
||
className="admin-secondary-button"
|
||
disabled={isLoadingTables}
|
||
type="button"
|
||
onClick={() => void loadTables()}
|
||
>
|
||
<RefreshCcw size={17} aria-hidden="true" />
|
||
<span>{isLoadingTables ? '刷新中' : '刷新表'}</span>
|
||
</button>
|
||
<button
|
||
className="admin-primary-button"
|
||
disabled={!tableName || isLoadingRows}
|
||
type="button"
|
||
onClick={() => void refreshRows()}
|
||
>
|
||
<Search size={17} aria-hidden="true" />
|
||
<span>{isLoadingRows ? '查询中' : '查询'}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<form className="admin-panel admin-form" onSubmit={handleSearch}>
|
||
<div className="admin-table-query-grid">
|
||
<label className="admin-field">
|
||
<span>表</span>
|
||
<select
|
||
value={tableName}
|
||
onChange={(event) => handleTableChange(event.target.value)}
|
||
>
|
||
{tableOptions.map(({name, optionLabel, description}) => (
|
||
<option key={name} title={description} value={name}>
|
||
{optionLabel}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<label className="admin-field">
|
||
<span>关键词</span>
|
||
<input
|
||
placeholder="全部"
|
||
value={search}
|
||
onChange={(event) => setSearch(event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="admin-field">
|
||
<span>筛选 JSON</span>
|
||
<input
|
||
placeholder='{"user_id":"u1"}'
|
||
value={filters}
|
||
onChange={(event) => setFilters(event.target.value)}
|
||
/>
|
||
</label>
|
||
<label className="admin-field admin-field-compact">
|
||
<span>条数</span>
|
||
<input
|
||
inputMode="numeric"
|
||
value={limit}
|
||
onChange={(event) => setLimit(event.target.value)}
|
||
/>
|
||
</label>
|
||
<button className="admin-secondary-button" disabled={isLoadingRows} type="submit">
|
||
<Search size={17} aria-hidden="true" />
|
||
<span>{isLoadingRows ? '查询中' : '查询'}</span>
|
||
</button>
|
||
</div>
|
||
<div className="admin-action-row admin-query-action-row">
|
||
<button
|
||
className="admin-ghost-button admin-query-reset-button"
|
||
disabled={isLoadingRows}
|
||
type="button"
|
||
onClick={handleResetQuery}
|
||
>
|
||
重置条件
|
||
</button>
|
||
<div className="admin-query-summary">
|
||
<span title={selectedTableHeader.description}>
|
||
已选表:{selectedTableHeader.optionLabel}
|
||
</span>
|
||
<span>显示列:{visibleColumns.length}</span>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
{errorMessage ? (
|
||
<div className="admin-alert" role="status">
|
||
{errorMessage}
|
||
</div>
|
||
) : null}
|
||
|
||
<section className="admin-panel">
|
||
<div className="admin-panel-heading">
|
||
<h3 title={resultTableHeader.description}>{resultTableHeader.label}</h3>
|
||
<span>{result?.totalReturned ?? 0} 条</span>
|
||
</div>
|
||
<div className="admin-table-wrap">
|
||
<table className="admin-table admin-table-wide admin-database-table">
|
||
<thead>
|
||
<tr>
|
||
{columnHeaders.map(({column, label, description}) => {
|
||
const isSorted = sortColumn === column;
|
||
return (
|
||
<th
|
||
key={column}
|
||
aria-sort={
|
||
isSorted
|
||
? sortDirection === 'asc'
|
||
? 'ascending'
|
||
: 'descending'
|
||
: undefined
|
||
}
|
||
>
|
||
<button
|
||
className="admin-table-sort-button"
|
||
data-active={isSorted ? 'true' : 'false'}
|
||
title={description}
|
||
type="button"
|
||
onClick={() => handleSortColumn(column)}
|
||
>
|
||
<span>{label}</span>
|
||
{isSorted ? (
|
||
sortDirection === 'asc' ? (
|
||
<ArrowUp size={14} aria-hidden="true" />
|
||
) : (
|
||
<ArrowDown size={14} aria-hidden="true" />
|
||
)
|
||
) : (
|
||
<ArrowUpDown size={14} aria-hidden="true" />
|
||
)}
|
||
</button>
|
||
</th>
|
||
);
|
||
})}
|
||
<th>详情</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{sortedRows.length ? (
|
||
sortedRows.map((row, rowIndex) => (
|
||
<tr
|
||
key={buildRowKey(row, rowIndex)}
|
||
data-clickable="true"
|
||
onClick={() => setDetailRow(row)}
|
||
>
|
||
{visibleColumns.map((column) => {
|
||
const cellValue = formatCellValue(row.cells[column]);
|
||
return (
|
||
<td key={column}>
|
||
<span
|
||
className="admin-table-cell-ellipsis"
|
||
title={cellValue.fullText}
|
||
>
|
||
{cellValue.content}
|
||
</span>
|
||
</td>
|
||
);
|
||
})}
|
||
<td>
|
||
<button
|
||
className="admin-secondary-button"
|
||
type="button"
|
||
onClick={(event) => {
|
||
event.stopPropagation();
|
||
setDetailRow(row);
|
||
}}
|
||
>
|
||
<Eye size={16} aria-hidden="true" />
|
||
<span>详情</span>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))
|
||
) : (
|
||
<tr>
|
||
<td colSpan={Math.max(visibleColumns.length + 1, 1)}>暂无数据</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{detailRow ? (
|
||
<div className="admin-confirm-backdrop" role="presentation">
|
||
<section className="admin-detail-panel" role="dialog" aria-modal="true">
|
||
<div className="admin-panel-heading">
|
||
<h3>行详情</h3>
|
||
<div className="admin-detail-actions">
|
||
<button
|
||
className="admin-secondary-button"
|
||
type="button"
|
||
onClick={() => void handleCopyDetailJson()}
|
||
>
|
||
<span>复制 JSON</span>
|
||
</button>
|
||
<button
|
||
className="admin-ghost-button"
|
||
title="关闭"
|
||
type="button"
|
||
onClick={() => setDetailRow(null)}
|
||
>
|
||
<X size={17} aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{copyMessage ? <div className="admin-status admin-status-ok">{copyMessage}</div> : null}
|
||
<pre className="admin-code-block">
|
||
{JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2)}
|
||
</pre>
|
||
</section>
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
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<string, string> = {
|
||
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<string, string> = {
|
||
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<string, string> = {
|
||
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<string, string> = {
|
||
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<string, string> = {
|
||
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<SortableTableCellValue, {kind: 'number'}> {
|
||
return value.kind === 'number';
|
||
}
|
||
|
||
function isSortableBooleanValue(
|
||
value: SortableTableCellValue,
|
||
): value is Extract<SortableTableCellValue, {kind: 'boolean'}> {
|
||
return value.kind === 'boolean';
|
||
}
|
||
|
||
function isSortableTextValue(
|
||
value: SortableTableCellValue,
|
||
): value is Extract<SortableTableCellValue, {kind: 'text'}> {
|
||
return value.kind === 'text';
|
||
}
|