补齐陶泥儿精选审核链路

新增 editor_showcase_asset、点赞和活动配置表,生成素材默认不公开。

补齐素材提交精选审核、后台审核通过返还泥点、展示开关和公开精选分页接口。

更新素材库右键菜单和创作主页精选瀑布流,展示作者昵称、陶泥号、成本和点赞状态。

新增后台素材查询页,支持时间、用户、分类、关键词查询和缩略图详情查看。

同步 SpacetimeDB 绑定、前后端契约、项目文档和决策记录。
This commit is contained in:
2026-07-04 15:47:14 +08:00
parent 571a46ac29
commit 636934de88
79 changed files with 6258 additions and 329 deletions
+153
View File
@@ -11,6 +11,14 @@ import type {
AdminDatabaseTableListResponse,
AdminDatabaseTableRowsQuery,
AdminDatabaseTableRowsResponse,
AdminEditorAssetListQuery,
AdminEditorAssetListResponse,
AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest,
AdminEditorShowcaseListQuery,
AdminEditorShowcaseListResponse,
AdminEditorShowcaseReviewRequest,
AdminLoginResponse,
AdminMeResponse,
AdminOverviewResponse,
@@ -19,6 +27,7 @@ import type {
AdminTrackingEventListResponse,
AdminUpdateWorkVisibilityRequest,
AdminUpdateWorkVisibilityResponse,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileRechargeProductRequest,
AdminUpsertProfileRedeemCodeRequest,
@@ -54,6 +63,23 @@ interface AdminRequestOptions {
signal?: AbortSignal;
}
interface AdminAssetReadUrlQuery {
objectKey?: string | null;
legacyPublicPath?: string | null;
expireSeconds?: number | null;
}
export interface AdminAssetReadUrlResponse {
read?: {
objectKey?: string;
signedUrl?: string;
expiresAt?: string;
};
signedUrl?: string;
objectKey?: string;
expiresAt?: string;
}
export class AdminApiError extends Error {
status: number;
code: string;
@@ -293,6 +319,81 @@ export function updateAdminWorkVisibility(
);
}
export function getAdminAssetReadUrl(query: AdminAssetReadUrlQuery) {
return request<AdminAssetReadUrlResponse>(
`/api/assets/read-url${buildAssetReadUrlQuery(query)}`,
);
}
export function listAdminEditorAssets(
token: string,
query: AdminEditorAssetListQuery = {},
) {
return request<AdminEditorAssetListResponse>(
`/admin/api/editor-assets${buildEditorAssetListQuery(query)}`,
{ token },
);
}
export function listAdminEditorShowcaseAssets(
token: string,
query: AdminEditorShowcaseListQuery = {},
) {
return request<AdminEditorShowcaseListResponse>(
`/admin/api/editor-showcase/assets${buildEditorShowcaseListQuery(query)}`,
{ token },
);
}
export function reviewAdminEditorShowcaseAsset(
token: string,
payload: AdminEditorShowcaseReviewRequest,
) {
return request<AdminEditorShowcaseAssetResponse>(
'/admin/api/editor-showcase/assets/review',
{
method: 'POST',
token,
body: payload,
},
);
}
export function updateAdminEditorShowcaseDisplay(
token: string,
payload: AdminEditorShowcaseDisplayRequest,
) {
return request<AdminEditorShowcaseAssetResponse>(
'/admin/api/editor-showcase/assets/display',
{
method: 'POST',
token,
body: payload,
},
);
}
export function getAdminEditorShowcaseCampaign(token: string) {
return request<AdminEditorShowcaseCampaignResponse>(
'/admin/api/editor-showcase/campaign',
{ token },
);
}
export function upsertAdminEditorShowcaseCampaign(
token: string,
payload: AdminUpsertEditorShowcaseCampaignRequest,
) {
return request<AdminEditorShowcaseCampaignResponse>(
'/admin/api/editor-showcase/campaign',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listProfileRedeemCodes(token: string) {
return request<ProfileRedeemCodeAdminListResponse>(
'/admin/api/profile/redeem-codes',
@@ -432,6 +533,29 @@ function buildRequestUrl(path: string) {
return `${ADMIN_API_BASE_URL}${normalizedPath}`;
}
function buildAssetReadUrlQuery(query: AdminAssetReadUrlQuery) {
const params = new URLSearchParams();
const objectKey = query.objectKey?.trim().replace(/^\/+/u, '') ?? '';
const legacyPublicPath = query.legacyPublicPath?.trim() ?? '';
if (objectKey) {
params.set('objectKey', objectKey);
} else if (legacyPublicPath) {
params.set(
'legacyPublicPath',
`/${legacyPublicPath.replace(/^\/+/u, '')}`,
);
}
if (
typeof query.expireSeconds === 'number' &&
Number.isFinite(query.expireSeconds) &&
query.expireSeconds > 0
) {
params.set('expireSeconds', String(Math.floor(query.expireSeconds)));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildQueryString(query: AdminTrackingEventListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'eventKey', query.eventKey);
@@ -471,6 +595,35 @@ function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
return queryString ? `?${queryString}` : '';
}
function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'assetKind', query.assetKind);
appendQueryParam(params, 'createdAfter', query.createdAfter);
appendQueryParam(params, 'createdBefore', query.createdBefore);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildEditorShowcaseListQuery(query: AdminEditorShowcaseListQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'cursor', query.cursor);
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
appendQueryParam(params, 'assetKind', query.assetKind);
appendQueryParam(params, 'reviewStatus', query.reviewStatus);
appendQueryParam(params, 'submittedAfter', query.submittedAfter);
appendQueryParam(params, 'submittedBefore', query.submittedBefore);
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
params.set('limit', String(query.limit));
}
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function appendQueryParam(
params: URLSearchParams,
key: string,
+129
View File
@@ -346,6 +346,135 @@ export interface AdminUpdateWorkVisibilityResponse {
entry: AdminWorkVisibilityEntryPayload;
}
export interface AdminEditorAssetListQuery {
cursor?: string | null;
ownerUserId?: string | null;
assetKind?: string | null;
createdAfter?: string | null;
createdBefore?: string | null;
limit?: number | null;
}
export interface AdminEditorAssetPayload {
assetId: string;
ownerUserId: string;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
folderId: string;
label: string;
assetObjectId?: string | null;
imageSrc: string;
objectKey?: string | null;
width: number;
height: number;
sourceType: string;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
sourceResourceId?: string | null;
thumbnailSrc?: string | null;
generationCostMudPoints: number;
createdAt: string;
updatedAt: string;
}
export interface AdminEditorAssetListResponse {
entries: AdminEditorAssetPayload[];
nextCursor?: string | null;
}
export interface AdminEditorShowcaseListQuery {
cursor?: string | null;
ownerUserId?: string | null;
assetKind?: string | null;
reviewStatus?: string | null;
submittedAfter?: string | null;
submittedBefore?: string | null;
limit?: number | null;
}
export interface AdminEditorShowcaseAssetPayload {
showcaseId: string;
assetId: string;
ownerUserId: string;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
label: string;
imageSrc: string;
objectKey?: string | null;
width: number;
height: number;
prompt?: string | null;
actualPrompt?: string | null;
model?: string | null;
provider?: string | null;
taskId?: string | null;
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
generationCostMudPoints: number;
refundMudPoints: number;
reviewStatus: 'pending' | 'approved' | 'rejected' | string;
displayEnabled: boolean;
likeCount: number;
assetDeletedWhilePending: boolean;
reviewedByAdminUserId?: string | null;
reviewNote?: string | null;
refundLedgerId?: string | null;
refundCompletedAt?: string | null;
submittedAt: string;
reviewedAt?: string | null;
approvedAt?: string | null;
rejectedAt?: string | null;
updatedAt: string;
}
export interface AdminEditorShowcaseListResponse {
entries: AdminEditorShowcaseAssetPayload[];
nextCursor?: string | null;
}
export interface AdminEditorShowcaseReviewRequest {
showcaseId: string;
reviewStatus: 'approved' | 'rejected';
reviewNote?: string | null;
}
export interface AdminEditorShowcaseDisplayRequest {
showcaseId: string;
displayEnabled: boolean;
}
export interface AdminEditorShowcaseAssetResponse {
entry: AdminEditorShowcaseAssetPayload;
}
export interface AdminEditorShowcaseCampaignPayload {
enabled: boolean;
title: string;
imageSrc: string;
prompt: string;
author: string;
costText: string;
updatedAt?: string;
}
export interface AdminEditorShowcaseCampaignResponse {
campaign?: AdminEditorShowcaseCampaignPayload | null;
}
export interface AdminUpsertEditorShowcaseCampaignRequest {
enabled: boolean;
title: string;
imageSrc: string;
prompt: string;
author: string;
costText: string;
}
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;
+7
View File
@@ -24,6 +24,7 @@ import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
@@ -245,6 +246,12 @@ export function AdminApp() {
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'editor-showcase' ? (
<AdminEditorAssetQueryPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
</AdminShell>
);
}
+2
View File
@@ -7,6 +7,7 @@ import {
LogOut,
Megaphone,
Eye,
Images,
WalletCards,
ShieldCheck,
ListChecks,
@@ -42,6 +43,7 @@ const routeIcons = {
tasks: ListChecks,
'recharge-products': BadgeDollarSign,
'editor-generation-pricing': Coins,
'editor-showcase': Images,
'creation-announcement': Megaphone,
'creation-entry': SlidersHorizontal,
'work-visibility': Eye,
@@ -39,3 +39,13 @@ test('后台模型定价路由可通过导航和 hash 访问', () => {
'#editor-generation-pricing',
);
});
test('后台素材查询路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'editor-showcase',
label: '素材查询',
hash: '#editor-showcase',
});
expect(resolveAdminRoute('#editor-showcase')).toBe('editor-showcase');
expect(routeHash('editor-showcase')).toBe('#editor-showcase');
});
+2
View File
@@ -11,6 +11,7 @@ export type AdminRouteId =
| 'tasks'
| 'recharge-products'
| 'editor-generation-pricing'
| 'editor-showcase'
| 'creation-announcement'
| 'creation-entry'
| 'work-visibility';
@@ -34,6 +35,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
{id: 'tasks', label: '任务配置', hash: '#tasks'},
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
{id: 'editor-generation-pricing', label: '模型定价', hash: '#editor-generation-pricing'},
{id: 'editor-showcase', label: '素材查询', hash: '#editor-showcase'},
{id: 'creation-announcement', label: '入口公告', hash: '#creation-announcement'},
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
{id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'},
@@ -0,0 +1,164 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
import {beforeEach, expect, test, vi} from 'vitest';
import {
getAdminAssetReadUrl,
listAdminEditorAssets,
} from '../api/adminApiClient';
import type {AdminEditorAssetPayload} from '../api/adminApiTypes';
import {AdminEditorAssetQueryPage} from './AdminEditorAssetQueryPage';
vi.mock('../api/adminApiClient', () => ({
getAdminAssetReadUrl: vi.fn(),
listAdminEditorAssets: vi.fn(),
}));
const generatedAsset: AdminEditorAssetPayload = {
assetId: 'asset-1',
ownerUserId: 'user-1',
authorDisplayName: '作者昵称',
authorPublicUserCode: 'SY-00000042',
folderId: 'folder-1',
label: '角色形象 1',
assetObjectId: 'asset-object-1',
imageSrc: '/generated-character-drafts/editor/spec.png',
objectKey: 'generated-character-drafts/editor/spec.png',
width: 1024,
height: 1024,
sourceType: 'generated',
prompt: '完整提示词内容',
actualPrompt: null,
model: 'gpt-image-2',
provider: 'character',
taskId: 'task-1',
assetKind: 'character',
generationInputs: {style: 'clay'},
sourceResourceId: 'resource-1',
thumbnailSrc: null,
generationCostMudPoints: 12,
createdAt: '2026-07-04T10:00:00Z',
updatedAt: '2026-07-04T10:00:00Z',
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listAdminEditorAssets).mockResolvedValue({
entries: [generatedAsset],
nextCursor: null,
});
vi.mocked(getAdminAssetReadUrl).mockResolvedValue({
read: {
objectKey: 'generated-character-drafts/editor/spec.png',
signedUrl:
'https://signed.example.com/generated-character-drafts/editor/spec.png',
expiresAt: '2026-07-04T11:00:00Z',
},
});
});
test('后台素材查询展示作者昵称和陶泥号', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
expect(await screen.findByText('作者昵称')).toBeTruthy();
expect(screen.getByText('SY-00000042')).toBeTruthy();
expect(screen.queryByText('user-1')).toBeNull();
});
test('后台素材查询按用户、分类和时间调用查询接口', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByRole('img', {name: '素材:角色形象 1'});
fireEvent.change(screen.getByLabelText('用户 ID'), {
target: {value: 'user-1'},
});
fireEvent.change(screen.getByLabelText('分类'), {
target: {value: 'character'},
});
fireEvent.change(screen.getByLabelText('开始时间'), {
target: {value: '2026-07-01'},
});
fireEvent.change(screen.getByLabelText('结束时间'), {
target: {value: '2026-07-04'},
});
await waitFor(() => {
expect(listAdminEditorAssets).toHaveBeenLastCalledWith('admin-token', {
ownerUserId: 'user-1',
assetKind: 'character',
createdAfter: '2026-07-01T00:00:00+08:00',
createdBefore: '2026-07-04T23:59:59.999+08:00',
limit: 80,
});
});
});
test('后台素材查询缩略图使用 objectKey 换签后展示', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
const image = await screen.findByRole('img', {name: '素材:角色形象 1'});
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
'https://signed.example.com/generated-character-drafts/editor/spec.png',
);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledWith({
objectKey: 'generated-character-drafts/editor/spec.png',
expireSeconds: 300,
});
});
test('后台素材查询可查看素材详情', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
fireEvent.click(await screen.findByRole('button', {name: '详情'}));
const dialog = screen.getByRole('dialog', {name: '素材详情'});
expect(within(dialog).getByText('asset-1')).toBeTruthy();
expect(within(dialog).getByText('1024 x 1024')).toBeTruthy();
expect(within(dialog).getByText('12 泥点')).toBeTruthy();
expect(within(dialog).getByText(/"style": "clay"/u)).toBeTruthy();
});
test('后台素材查询可打开弹窗查看完整提示词', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
fireEvent.click(await screen.findByRole('button', {name: '完整提示词内容'}));
const dialog = screen.getByRole('dialog', {name: '完整提示词'});
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
fireEvent.click(within(dialog).getByRole('button', {name: '关闭完整提示词'}));
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: '完整提示词'})).toBeNull();
});
});
File diff suppressed because it is too large Load Diff
+123
View File
@@ -553,6 +553,46 @@ button:disabled {
gap: 10px;
}
.admin-asset-query-filter-row {
align-items: end;
}
.admin-asset-query-filter-row .admin-field {
min-width: 140px;
}
.admin-asset-query-filter-row .admin-field:last-child {
min-width: 240px;
}
.admin-asset-query-thumb-button {
display: inline-flex;
border: 0;
background: transparent;
padding: 0;
}
.admin-asset-query-thumb {
border: 1px solid #eaded2;
border-radius: 8px;
width: 72px;
height: 72px;
background: #fffdf9;
object-fit: cover;
}
.admin-asset-query-thumb-placeholder {
display: block;
}
.admin-asset-query-prompt-text {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-query-action-row {
justify-content: space-between;
}
@@ -905,6 +945,17 @@ button:disabled {
font-size: 12px;
}
.admin-table textarea {
width: min(100%, 220px);
min-height: 64px;
border: 1px solid #dfc8b7;
border-radius: 8px;
color: #3d1f10;
background: #fffdf9;
padding: 8px 10px;
resize: vertical;
}
.admin-muted-text {
color: #a38f80;
}
@@ -939,6 +990,78 @@ button:disabled {
min-width: 1180px;
}
.admin-asset-query-table {
table-layout: fixed;
}
.admin-asset-query-table th:nth-child(1),
.admin-asset-query-table td:nth-child(1) {
width: 10%;
}
.admin-asset-query-table th:nth-child(2),
.admin-asset-query-table td:nth-child(2),
.admin-asset-query-table th:nth-child(3),
.admin-asset-query-table td:nth-child(3) {
width: 14%;
}
.admin-asset-query-table th:nth-child(4),
.admin-asset-query-table td:nth-child(4) {
width: 10%;
}
.admin-asset-query-table th:nth-child(5),
.admin-asset-query-table td:nth-child(5) {
width: 30%;
}
.admin-asset-query-table th:nth-child(6),
.admin-asset-query-table td:nth-child(6) {
width: 8%;
}
.admin-asset-query-table th:nth-child(7),
.admin-asset-query-table td:nth-child(7) {
width: 10%;
}
.admin-asset-query-detail-dialog,
.admin-asset-query-prompt-dialog {
width: min(100%, 860px);
}
.admin-asset-query-prompt-dialog .admin-panel-heading > div,
.admin-asset-query-detail-dialog .admin-panel-heading > div {
min-width: 0;
}
.admin-asset-query-prompt-dialog .admin-panel-heading span,
.admin-asset-query-detail-dialog .admin-panel-heading span {
display: block;
max-width: 100%;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-asset-query-prompt-full {
max-height: min(68dvh, 520px);
}
.admin-asset-query-detail-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.admin-asset-query-detail-layout > .admin-asset-query-thumb {
width: 220px;
height: 220px;
}
.admin-database-table {
width: max-content;
min-width: 100%;
+5
View File
@@ -40,6 +40,11 @@ export default defineConfig(({mode}) => {
changeOrigin: true,
secure: false,
},
'/api/assets': {
target: apiTarget,
changeOrigin: true,
secure: false,
},
'/healthz': {
target: apiTarget,
changeOrigin: true,
@@ -24,6 +24,14 @@
- 验证方式:运行 `cargo fmt --manifest-path server-rs/Cargo.toml --all``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema``npm run check:encoding``git diff --check`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`
## 2026-07-04 陶泥儿精选改为素材提交审核后公开
- 背景:`/creation``陶泥儿精选` 过去依赖 `editor_project_resource.public_showcase_enabled`,生成画布资源默认可公开,和“作品公开默认关闭、由用户主动投稿精选”的运营要求冲突,也无法在后台审核、返还泥点和配置固定活动卡。
- 决策:`陶泥儿精选` 的公开事实改为独立 `editor_showcase_asset` 审核表。生成素材默认不公开;用户在账号级素材库对 `sourceType="generated"` 且有媒体内容的素材提交审核,后端快照素材信息并写入 `pending`。后台审核通过后写入 `approved`、默认 `display_enabled=true` 并按 `generation_cost_mud_points` 返还 50% 泥点;拒绝后写入 `rejected`。公开接口 `GET /api/editor/showcase/resources` 只返回已通过且展示开启的快照,按通过时间和 `showcaseId` 倒序分页,并可携带后台配置的固定活动卡。旧 `editor_project_resource.public_showcase_enabled` 和旧 PATCH 接口只保留兼容,不再驱动精选公开。
- 影响范围:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``spacetime-client` 绑定与 mapper、`api-server` 编辑器和后台路由、admin-web 精选审核页、素材库右键菜单、`/creation` 精选瀑布流、图片画布文档和后端表目录。
- 验证方式:运行 `npm run spacetime:generate``npm run check:spacetime-schema``cargo check --manifest-path server-rs/Cargo.toml -p spacetime-module -p spacetime-client -p api-server`、前端 / 后台 typecheck 与精选相关组件测试,确认默认不公开、提交后 pending、审核通过后展示和返还、展示开关与点赞生效。
- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
## 2026-07-03 外部编辑器 API 生成默认写入画布与素材库
- 背景:外部 API 面向美术 Agent 使用时,需要从自然语言自动选路,并保证生成结果不会只停留在接口回包里;同时后续素材生成需要复用已抽象出的美术规范,避免每次重新追问风格要求。
@@ -92,7 +100,7 @@
## 2026-06-22 创作主页精选展示全站公开画布生成资源
- 背景:`/creation``陶泥儿精选` 曾从账号级素材库读取,并在素材为空时用公开作品图片补充,导致新创作页出现不属于任何当前图片画布项目的素材。
- 决策:`陶泥儿精选` 从全站 `editor_project_resource` 构建素材包和素材,读取路径固定为 `GET /api/editor/showcase/resources`;只保留 `sourceType="generated"`、图片源非空且 `public_showcase_enabled = true` 的画布生成资源,按创建时间和资源 ID 倒序 cursor 分页展示,每页最多 36 条,有 `nextCursor` 时前端滚动到底部继续追加。账号级 `editor_asset`、上传素材、公开作品图片和 `mock_generated` 都不作为精选来源。任意画布左侧素材列表中,单素材右键菜单承接文字 `删除``公开展示该素材` 勾选项;勾选项默认打开,修改时写回对应 `editor_project_resource.public_showcase_enabled`
- 决策:该历史决策已被 2026-07-04 的“素材提交审核后公开”取代。历史背景仍有效:公开作品图片和假数据不应回填精选;但精选事实源不再是 `editor_project_resource.public_showcase_enabled`,而是 `editor_showcase_asset` 审核快照
- 影响范围:`/creation` 创作主页、`creationShowcaseModel`、公开精选 BFF、图片画布素材列表右键菜单、账号素材库快照、创作主页改版计划和精选素材相关测试。
- 验证方式:运行 `src/components/creation-home/creationShowcaseModel.test.ts``CreationLandingView.test.tsx``ImageCanvasAssetRowView.test.tsx``useImageCanvasAssetLibrary.test.tsx``src/services/image-editor/editorProjectClient.test.ts`,确认公开生成资源展示、上传素材过滤、公开开关隐藏资源、删除入口在右键菜单中、公开作品不再 fallback。
- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
@@ -46,7 +46,8 @@
- 新增 `editor_project` 表保存图片画布工程:`projectId``ownerUserId`、标题、创建时间和更新时间;历史 layout 字段暂保留为兼容列,不再作为权威画布数据。
- 新增 `editor_canvas` 表保存工程下的画布:`canvasId``projectId``ownerUserId`、标题、viewport、图层布局 JSON、创建时间和更新时间。当前编辑器使用项目默认画布,后续可扩展为一个 project 下多个 canvas。
- 新增 `editor_asset_folder` 表保存账号级素材文件夹:`folderId``ownerUserId`、名称、排序、折叠状态、系统默认标记、创建时间和更新时间。素材文件夹不归属于 project,同一个账号进入任一项目都能看到。
- 新增 `editor_asset` 表保存账号级素材:`assetId``ownerUserId``folderId`、名称、图片读取地址、可选封面 `thumbnailSrc`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、`assetKind``generationInputs`、创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。
- 新增 `editor_asset` 表保存账号级素材:`assetId``ownerUserId``folderId`、名称、图片读取地址、可选封面 `thumbnailSrc`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、`assetKind``generationInputs``generationCostMudPoints`创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。
- 新增 `editor_showcase_asset``editor_showcase_asset_like``editor_showcase_campaign_config` 表承接 `陶泥儿精选`:生成素材默认不公开,用户在素材菜单中提交精选审核后生成独立快照;后台审核通过才进入公开精选,并按生成成本返还 50% 泥点。公开列表不再读取 `editor_project_resource.public_showcase_enabled`,而是读取已通过且展示开启的精选快照,支持点赞数和首位活动卡。
- `editor_project_resource` 表保存工程画布引用过的资源快照:`resourceId``projectId``ownerUserId`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、sourceResourceId、`assetKind``generationInputs`、创建时间和更新时间。上传素材被拖入画布时会复制为 project resource,图层只引用 resourceId;图片、图标和 UI 素材生成 BFF 在请求携带 `projectId` 时由后端直接创建新 resource,并把 `resourceId` 随生成响应返回给前端。图片生成请求如果同时携带 `canvasCompletion`(生成器 `dialogId`、标题和占位框,或无 dialog 的右侧完成占位),BFF / worker 在生成成功后必须直接读取当前项目布局,优先使用最新 `generation-dialog` 占位框位置;只有当前布局仍存在对应 `generation-dialog` 时才插入轻量结果图层、把生成器标记为 `idle` 并写入 `generatedLayerId`,沿用后端当前 viewport 保存布局,再返回或刷新最新项目快照;前端只应用该快照刷新显示,不把生成完成态作为本地业务真相,也不在项目加载时根据资源行推断完成态。有项目上下文但后端没有返回项目快照时,前端不得本地补结果图层,只保留当前生成器交互状态等待下一次项目刷新。
- 项目封面图是画布当前视口栅格化后的静态快照资源,不在项目列表页临时重放 `layers + viewport`。前端在项目加载后和防抖保存 layout 时生成 320x240 PNG,走私有 OSS / asset object 上传,再创建 `editor_project_resource`,其中 `assetKind="project-cover-snapshot"``sourceType="uploaded"``/project``/creation` 最近项目卡只读取最新封面快照资源渲染;没有封面快照时显示普通项目占位,不回退为实时画布组合。
- 图片、音频、视频和角色动画帧文件本体继续走 OSS / asset object;浏览器读取私有 generated 对象统一经 `/api/assets/read-url` 换签,签名 URL 可在 session 内复用,但不得作为持久化真相。`/api/assets/read-url` 属于页面展示层高频后台请求,前端统一在 `assetReadUrlService` 内做同 key pending 去重、session 缓存和跨组件节流;UI 设计切片、角色动画帧或大量素材恢复时不得绕过该服务并发换签,否则单页可在同一秒内打满发布入口 `genarrative_api_rps` burst。
@@ -54,7 +54,7 @@ npm run check:server-rs-ddd
路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组:
- 健康检查:`GET /healthz`
- 后台管理:`/admin/api/*`,包括登录、Dashboard 运营看板、概览、HTTP debug、埋点、表查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。Dashboard 指标口径见 [`docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md`](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。
- 后台管理:`/admin/api/*`,包括登录、Dashboard 运营看板、概览、HTTP debug、埋点、表查询、素材查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。Dashboard 指标口径见 [`docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md`](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。
- 认证与账号:`/api/auth/*``/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请和兑换等账号侧能力。
- 平台基础能力:`/api/llm/*``/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
@@ -62,6 +62,7 @@ npm run check:server-rs-ddd
- 外部 OpenAPI`/api/external/v1/openapi.json``/api/external/v1/assets/direct-upload-tickets``/api/external/v1/assets/objects/confirm``/api/external/v1/assets/read-url``/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
- 创作 / 游玩支撑能力:`/api/creation-entry/config``/api/ai/tasks*``/api/runtime/chat/*``/api/runtime/settings``/api/runtime/save/snapshot``/api/profile/browse-history``/api/profile/save-archives*``/api/profile/play-stats``/api/assets/history``/api/assets/character-visual/*``/api/assets/character-animation/*``/api/assets/character-workflow-cache*``/api/assets/hyper3d/*``/api/runtime/custom-world/asset-studio/*``/api/editor/projects*``/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
- 后台入口配置:`/admin/api/creation-entry/config``/admin/api/creation-entry/config/banners``/admin/api/creation-entry/config/interactions`
- 后台素材查询:`GET /admin/api/editor-assets` 读取账号级 `editor_asset``source_type = 'generated'` 的素材,支持 `ownerUserId``assetKind``createdAfter``createdBefore``cursor``limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不执行精选审核、返还或展示状态修改。
- 自定义世界 / RPG`/api/runtime/custom-world*``/api/story/*``/api/runtime/chat/*`
- 拼图:`/api/runtime/puzzle/*`
- 抓大鹅 Match3D`/api/creation/match3d/*``/api/runtime/match3d/*`
@@ -463,7 +464,7 @@ npm run check:server-rs-ddd
- Rust 结构体:`EditorProjectResource`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind``generation_inputs_json``public_showcase_enabled``asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复`public_showcase_enabled` 控制该生成资源是否进入 `/creation``陶泥儿精选`,新增和旧行默认 `true`,但通过 `source_resource_id` 指向同一 owner 源资源且媒体引用相同的画布副本默认不公开。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot``source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。公开精选读取走 `GET /api/editor/showcase/resources`,只返回 `source_type = generated`、图片源非空且 `public_showcase_enabled = true` 的全站资源,并跳过同源同媒体副本,按 `created_at` / `resource_id` 倒序 cursor 分页;每页最多 36 条,响应返回 `nextCursor`,下一页通过 `?cursor=...` 继续读取。公开响应的作者展示字段优先提供 `authorDisplayName` / `display_name`,没有展示名时提供 `authorPublicUserCode` / 陶泥号作为展示兜底,前端公开展示绝不能兜底到内部 `ownerUserId` / `user_id`。公开开关修改走登录鉴权 `PATCH /api/editor/project-resources/{resource_id}/showcase`,只允许资源 owner 更新。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind``generation_inputs_json`历史 `public_showcase_enabled``asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation``陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot``source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
- 索引:`by_editor_project_resource_project_id``by_editor_project_resource_owner_user_id`
### `editor_asset_folder`
@@ -477,9 +478,30 @@ npm run check:server-rs-ddd
- Rust 结构体:`EditorAsset`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind``generation_inputs_json`可选 `source_resource_id`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `source_resource_id` 回查对应 project resource 的 `public_showcase_enabled`,供左侧素材菜单展示和切换公开状态;公开开关本身仍只落在 `editor_project_resource`,不在账号素材表复制真相。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind``generation_inputs_json`可选 `source_resource_id``generation_cost_mud_points`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `asset_id` 回查对应 `editor_showcase_asset`,供左侧素材菜单展示 `pending` / `approved` / `rejected` 审核状态;公开事实不落在账号素材表,素材库只发起提交审核。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。
- 索引:`by_editor_asset_owner_user_id``by_editor_asset_folder_id`
### `editor_showcase_asset`
- Rust 结构体:`EditorShowcaseAsset`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:`陶泥儿精选` 的独立审核与公开快照表。用户从账号级生成素材提交后,后端把素材媒体、提示词、生成输入、素材类型和 `generation_cost_mud_points` 快照到该表,初始 `review_status = pending``display_enabled = false`。后台审核通过后写入 `approved`、默认开启展示并生成确定性返还流水 `editor-showcase-refund:{showcase_id}`,BFF 按 50% 生成成本返还泥点后回写 `refund_completed_at`;拒绝后写入 `rejected`。公开精选 `GET /api/editor/showcase/resources` 只读取 `review_status = approved``display_enabled = true` 且媒体非空的记录,按通过时间 / `showcase_id` 倒序 cursor 分页。素材删除时,待审核记录标记 `asset_deleted_while_pending`,已拒绝记录删除,已通过记录保留快照继续展示。
- 索引:`by_editor_showcase_asset_owner_user_id``by_editor_showcase_asset_review_status`
### `editor_showcase_asset_like`
- Rust 结构体:`EditorShowcaseAssetLike`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:`陶泥儿精选` 点赞去重表,主键由 `showcase_id:user_id` 组成。点赞 / 取消点赞通过登录接口更新该表,并同步回写 `editor_showcase_asset.like_count`;未通过审核或未展示的精选素材不能点赞。
- 索引:`by_editor_showcase_asset_like_showcase_id``by_editor_showcase_asset_like_user_id`
### `editor_showcase_campaign_config`
- Rust 结构体:`EditorShowcaseCampaignConfig`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:`陶泥儿精选` 首位固定活动卡配置表,当前使用固定 `config_id = default`。后台可配置启用状态、标题、图片 URL、提示词、作者和成本文案;固定活动卡不包含副标题或 Object Key,公开精选接口只在启用时返回该配置。
- 索引:主键 `config_id`
### `inventory_slot`
- Rust 结构体:`InventorySlot`
@@ -73,9 +73,9 @@
`陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/<play>` 工作台使用,但不作为本页精选区内容。
精选内容只使用 `editor_project_resource``sourceType="generated"``public_showcase_enabled = true` 的项目资源,代表用户通过图片画布生成、已经落入某个项目并允许公开展示的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。从项目素材把同一个生成素材再次拖入画布时,只是新增图层实例或复用同源同媒体项目资源,不应额外生成新的精选候选;历史上已经产生的同源同媒体副本也需要在精选读取时跳过。公开 BFF 必须返回资源作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
瀑布流通过 `GET /api/editor/showcase/resources` `created_at` / `resource_id` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有瀑布流,而不是固定只展示首屏数量。
瀑布流通过 `GET /api/editor/showcase/resources`通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有瀑布流,而不是固定只展示首屏数量。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选。
Tab
@@ -112,11 +112,12 @@ Tab
数据来源:
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_project_resource` 快照,快照包含 `authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号用于作者展示;前端只保留 `sourceType="generated"``publicShowcaseEnabled !== false` 的画布生成资源;若某条资源通过 `sourceResourceId` 指回同一 owner 的源资源且媒体引用相同,则视为拖拽画布副本,不进入精选。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`
- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自已存在项目资源;素材库里尚未放入任何项目的账号级素材不展示。
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId``assetId`、审核状态、展示状态、点赞数、生成成本、返还泥点、`authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号用于展示;前端只展示 `reviewStatus="approved"``displayEnabled=true` 的后端结果。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`
- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自用户素材库中已提交并通过审核的生成素材;不要求素材当前仍保留在某个项目资源中,已通过审核的快照在原素材删除后仍可保留公开展示。
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。菜单内 `公开展示该素材` 默认勾选,取消勾选后调用 `PATCH /api/editor/project-resources/{resourceId}/showcase` 写回 `public_showcase_enabled = false`,该资源从 `陶泥儿精选` 移除
- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。生成素材菜单内提供 `提交精选审核`调用 `POST /api/editor/assets/{assetId}/showcase-submissions` 后进入 `pending` 状态;已提交、已通过或已拒绝的素材显示对应状态,不再显示默认打开的公开勾选项
- 后台新增纯素材查询页,读取账号级 `editor_asset``sourceType="generated"` 的素材,用于查看所有用户生成素材。该页只提供时间、用户 ID、分类和关键词查询,以及缩略图详情、提示词全文、生成成本、作者展示名和陶泥号查看;不承载审核、返还、展示开关或活动卡配置操作。
- 不为了填满展示区创建假素材、假作者、假泥点成本或假组合关系。
- 暂无真实数据的 Tab 保留 Tab 入口,但内容区显示简洁空态。
@@ -127,7 +128,7 @@ Tab
- 最近项目继续使用编辑器项目接口:`listEditorProjects``createEditorProject``/editor/canvas?projectid=xxx`
- 最近项目和项目页打开画布时,浏览器 history 只保留带 `projectid` 的最终画布路由;新建画布引导使用 `guide=toolbar` 一次性 query,主站九宫格直达生成器使用 `tool=<intent>` 一次性 query,画布消费后通过 `history.replaceState` 清理这两个参数。
- 项目封面逻辑复用 `/project` 项目卡已有的画布中心缩略图算法。
- `陶泥儿精选` 读取后端公开精选接口返回的全站公开画布生成资源;公开开关以后端 `editor_project_resource.public_showcase_enabled` 为准,前端只做展示过滤、同源同媒体副本兜底过滤和乐观交互回滚。
- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status``display_enabled` 为准,前端只做展示组合、点赞交互和提交审核的乐观状态回滚。
- `/creation/<play>` 的玩法工作台、草稿、生成页、结果页、发布、运行态和作品架链路保持原状。
## 路由与导航
File diff suppressed because it is too large Load Diff
@@ -904,6 +904,7 @@ pub(crate) async fn generate_editor_video_for_owner(
asset_kind: Some(asset_kind.clone()),
generation_inputs: generation_inputs.clone(),
thumbnail_src: generated.thumbnail_src.clone(),
generation_cost_mud_points: u64::from(normalized.price_mud_points),
},
)
.await
File diff suppressed because it is too large Load Diff
@@ -491,6 +491,7 @@ pub async fn create_external_editor_asset(
asset_kind: normalize_optional_string(payload.asset_kind),
generation_inputs_json,
source_resource_id: None,
generation_cost_mud_points: 0,
now_micros: current_utc_micros(),
thumbnail_src: None,
})
@@ -6,11 +6,14 @@ use axum::{
use crate::{
admin::{
admin_dashboard, admin_debug_http, admin_get_creation_entry_config,
admin_get_editor_generation_pricing, admin_list_database_table_rows,
admin_list_database_tables, admin_list_tracking_event_keys, admin_list_tracking_events,
admin_list_work_visibility, admin_login, admin_me, admin_overview,
admin_update_work_visibility, admin_upsert_creation_entry_config,
admin_upsert_creation_entry_event_banners_config, admin_upsert_editor_generation_pricing,
admin_get_editor_generation_pricing, admin_get_editor_showcase_campaign,
admin_list_database_table_rows, admin_list_database_tables,
admin_list_editor_assets, admin_list_editor_showcase_assets, admin_list_tracking_event_keys,
admin_list_tracking_events, admin_list_work_visibility, admin_login, admin_me,
admin_overview, admin_review_editor_showcase_asset,
admin_update_editor_showcase_asset_display, admin_update_work_visibility,
admin_upsert_creation_entry_config, admin_upsert_creation_entry_event_banners_config,
admin_upsert_editor_generation_pricing, admin_upsert_editor_showcase_campaign,
admin_upsert_public_work_interaction_config, require_admin_auth,
},
runtime_profile::{
@@ -113,6 +116,42 @@ pub fn router(state: AppState) -> Router<AppState> {
require_admin_auth,
)),
)
.route(
"/admin/api/editor-assets",
get(admin_list_editor_assets).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/editor-showcase/assets",
get(admin_list_editor_showcase_assets).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/editor-showcase/assets/review",
post(admin_review_editor_showcase_asset).route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/editor-showcase/assets/display",
post(admin_update_editor_showcase_asset_display).route_layer(
middleware::from_fn_with_state(state.clone(), require_admin_auth),
),
)
.route(
"/admin/api/editor-showcase/campaign",
get(admin_get_editor_showcase_campaign)
.post(admin_upsert_editor_showcase_campaign)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/works/visibility",
get(admin_list_work_visibility)
@@ -15,7 +15,8 @@ use crate::{
get_editor_generation_pricing, get_editor_project, list_editor_projects,
list_public_editor_project_resources, load_recent_editor_project,
remove_editor_image_background, rename_editor_project, save_editor_project_layout,
update_editor_asset, update_editor_asset_folder, update_editor_project_resource_showcase,
submit_editor_asset_showcase, toggle_editor_showcase_asset_like, update_editor_asset,
update_editor_asset_folder, update_editor_project_resource_showcase,
},
state::AppState,
};
@@ -117,6 +118,20 @@ pub fn router(state: AppState) -> Router<AppState> {
require_bearer_auth,
)),
)
.route(
"/api/editor/assets/{asset_id}/showcase-submissions",
post(submit_editor_asset_showcase).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/showcase/assets/{showcase_id}/likes",
post(toggle_editor_showcase_asset_like).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/editor/images/generations",
post(generate_editor_image)
@@ -268,6 +268,7 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
asset_kind: Some("sound-effect".to_string()),
generation_inputs: generation_inputs.clone(),
thumbnail_src: None,
generation_cost_mud_points: u64::from(normalized.price_mud_points),
},
)
.await
@@ -486,6 +487,7 @@ pub(crate) async fn generate_editor_background_music_for_owner(
asset_kind: Some("background-music".to_string()),
generation_inputs: generation_inputs.clone(),
thumbnail_src: None,
generation_cost_mud_points: u64::from(normalized.price_mud_points),
},
)
.await
@@ -123,6 +123,159 @@ pub struct AdminUpdateWorkVisibilityResponse {
pub entry: AdminWorkVisibilityEntryPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetListQuery {
pub cursor: Option<String>,
pub owner_user_id: Option<String>,
pub asset_kind: Option<String>,
pub created_after: Option<String>,
pub created_before: Option<String>,
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetPayload {
pub asset_id: String,
pub owner_user_id: String,
pub author_display_name: Option<String>,
pub author_public_user_code: Option<String>,
pub folder_id: String,
pub label: String,
pub asset_object_id: Option<String>,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub source_type: String,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub source_resource_id: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorAssetListResponse {
pub entries: Vec<AdminEditorAssetPayload>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseListQuery {
pub cursor: Option<String>,
pub owner_user_id: Option<String>,
pub asset_kind: Option<String>,
pub review_status: Option<String>,
pub submitted_after: Option<String>,
pub submitted_before: Option<String>,
pub limit: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseAssetPayload {
pub showcase_id: String,
pub asset_id: String,
pub owner_user_id: String,
pub author_display_name: Option<String>,
pub author_public_user_code: Option<String>,
pub label: String,
pub image_src: String,
pub object_key: Option<String>,
pub width: u32,
pub height: u32,
pub prompt: Option<String>,
pub actual_prompt: Option<String>,
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub generation_cost_mud_points: u64,
pub refund_mud_points: u64,
pub review_status: String,
pub display_enabled: bool,
pub like_count: u64,
pub asset_deleted_while_pending: bool,
pub reviewed_by_admin_user_id: Option<String>,
pub review_note: Option<String>,
pub refund_ledger_id: Option<String>,
pub refund_completed_at: Option<String>,
pub submitted_at: String,
pub reviewed_at: Option<String>,
pub approved_at: Option<String>,
pub rejected_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseListResponse {
pub entries: Vec<AdminEditorShowcaseAssetPayload>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseReviewRequest {
pub showcase_id: String,
pub review_status: String,
pub review_note: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseDisplayRequest {
pub showcase_id: String,
pub display_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseAssetResponse {
pub entry: AdminEditorShowcaseAssetPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseCampaignPayload {
pub enabled: bool,
pub title: String,
pub image_src: String,
pub prompt: String,
pub author: String,
pub cost_text: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminEditorShowcaseCampaignResponse {
pub campaign: Option<AdminEditorShowcaseCampaignPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertEditorShowcaseCampaignRequest {
pub enabled: bool,
pub title: String,
pub image_src: String,
pub prompt: String,
pub author: String,
pub cost_text: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminLoginResponse {
@@ -411,3 +564,55 @@ pub struct AdminTrackingEventKeyPayload {
pub struct AdminTrackingEventKeyListResponse {
pub event_keys: Vec<AdminTrackingEventKeyPayload>,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::AdminEditorShowcaseAssetPayload;
#[test]
fn editor_showcase_asset_payload_serializes_author_fields_as_camel_case() {
let payload = AdminEditorShowcaseAssetPayload {
showcase_id: "showcase-1".to_string(),
asset_id: "asset-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: Some("作者昵称".to_string()),
author_public_user_code: Some("SY-00000042".to_string()),
label: "角色形象 1".to_string(),
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: Some("generated-character-drafts/editor/spec.png".to_string()),
width: 1024,
height: 1024,
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: None,
asset_kind: Some("character".to_string()),
generation_inputs: None,
generation_cost_mud_points: 12,
refund_mud_points: 6,
review_status: "pending".to_string(),
display_enabled: false,
like_count: 0,
asset_deleted_while_pending: false,
reviewed_by_admin_user_id: None,
review_note: None,
refund_ledger_id: None,
refund_completed_at: None,
submitted_at: "2026-07-04T10:00:00.000Z".to_string(),
reviewed_at: None,
approved_at: None,
rejected_at: None,
updated_at: "2026-07-04T10:00:00.000Z".to_string(),
};
let value = serde_json::to_value(payload).expect("payload should serialize");
assert_eq!(value["authorDisplayName"], json!("作者昵称"));
assert_eq!(value["authorPublicUserCode"], json!("SY-00000042"));
assert!(value.get("author_display_name").is_none());
assert!(value.get("author_public_user_code").is_none());
}
}
@@ -479,4 +479,229 @@ impl SpacetimeClient {
)
.await
}
pub async fn submit_editor_showcase_asset(
&self,
input: EditorShowcaseAssetSubmitRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"submit_editor_showcase_asset_and_return",
move |connection, sender| {
connection
.procedures()
.submit_editor_showcase_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn list_public_editor_showcase_assets(
&self,
input: EditorShowcaseAssetPublicListRecordInput,
) -> Result<Vec<EditorShowcaseAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"list_public_editor_showcase_assets_and_return",
move |connection, sender| {
connection
.procedures()
.list_public_editor_showcase_assets_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn admin_list_editor_showcase_assets(
&self,
input: EditorShowcaseAssetAdminListRecordInput,
) -> Result<Vec<EditorShowcaseAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"admin_list_editor_showcase_assets_and_return",
move |connection, sender| {
connection
.procedures()
.admin_list_editor_showcase_assets_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_list_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn admin_review_editor_showcase_asset(
&self,
input: EditorShowcaseAssetAdminReviewRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"admin_review_editor_showcase_asset_and_return",
move |connection, sender| {
connection
.procedures()
.admin_review_editor_showcase_asset_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_showcase_asset_display(
&self,
input: EditorShowcaseAssetDisplayUpdateRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"update_editor_showcase_asset_display_and_return",
move |connection, sender| {
connection
.procedures()
.update_editor_showcase_asset_display_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn mark_editor_showcase_asset_refunded(
&self,
input: EditorShowcaseAssetRefundMarkRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"mark_editor_showcase_asset_refunded_and_return",
move |connection, sender| {
connection
.procedures()
.mark_editor_showcase_asset_refunded_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn toggle_editor_showcase_asset_like(
&self,
input: EditorShowcaseAssetLikeToggleRecordInput,
) -> Result<EditorShowcaseAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"toggle_editor_showcase_asset_like_and_return",
move |connection, sender| {
connection
.procedures()
.toggle_editor_showcase_asset_like_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn get_editor_showcase_campaign_config(
&self,
input: EditorShowcaseCampaignConfigGetRecordInput,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"get_editor_showcase_campaign_config_and_return",
move |connection, sender| {
connection
.procedures()
.get_editor_showcase_campaign_config_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_campaign_config_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn upsert_editor_showcase_campaign_config(
&self,
input: EditorShowcaseCampaignConfigUpsertRecordInput,
) -> Result<Option<EditorShowcaseCampaignConfigRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"upsert_editor_showcase_campaign_config_and_return",
move |connection, sender| {
connection
.procedures()
.upsert_editor_showcase_campaign_config_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_showcase_campaign_config_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
}
+22 -18
View File
@@ -39,19 +39,25 @@ pub use mapper::{
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
EditorProjectResourceShowcaseUpdateRecordInput, ExternalApiKeyAuthenticateRecordInput,
ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, ExternalApiKeyRevokeRecordInput,
ExternalGenerationJobAcknowledgeRecordInput, ExternalGenerationJobClaimRecordInput,
ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput,
ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput,
ExternalGenerationJobListRecord, ExternalGenerationJobListRecordInput,
ExternalGenerationJobRecord, ExternalGenerationJobRenewLeaseRecordInput,
ExternalGenerationQueueStatsRecord, JumpHopActionRequest, JumpHopActionResponse,
JumpHopActionType, JumpHopCharacterAsset, JumpHopDifficulty, JumpHopDraftResponse,
JumpHopGalleryCardResponse, JumpHopGalleryDetailResponse, JumpHopGalleryResponse,
JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult,
JumpHopLastJump, JumpHopPath, JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse,
JumpHopRunStatus, JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput,
EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput,
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput,
ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord,
ExternalApiKeyRevokeRecordInput, ExternalGenerationJobAcknowledgeRecordInput,
ExternalGenerationJobClaimRecordInput, ExternalGenerationJobCompleteRecordInput,
ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput,
ExternalGenerationJobGetRecordInput, ExternalGenerationJobListRecord,
ExternalGenerationJobListRecordInput, ExternalGenerationJobRecord,
ExternalGenerationJobRenewLeaseRecordInput, ExternalGenerationQueueStatsRecord,
JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset,
JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse,
JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus,
JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath,
JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse,
@@ -202,8 +208,7 @@ use module_runtime::{
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeAdminListRecord,
RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileRechargeProductConfigRecord,
RuntimeProfileRedeemCodeAdminListRecord,
RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRedeemCodeAdminListRecord,
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord,
@@ -214,9 +219,8 @@ use module_runtime::{
RuntimeTrackingScopeKind as DomainRuntimeTrackingScopeKind, build_analytics_metric_query_input,
build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input,
build_runtime_browse_history_record, build_runtime_browse_history_sync_input,
build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record,
build_runtime_profile_code_operation_record,
build_runtime_profile_feedback_submission_input,
build_runtime_profile_code_operation_record, build_runtime_profile_dashboard_get_input,
build_runtime_profile_dashboard_record, build_runtime_profile_feedback_submission_input,
build_runtime_profile_feedback_submission_record,
build_runtime_profile_invite_code_admin_list_input,
build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record,
@@ -82,7 +82,12 @@ pub use self::editor_project::{
EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput,
EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord,
EditorProjectResourceShowcaseUpdateRecordInput,
EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput,
EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput,
EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput,
EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput,
EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput,
EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput,
};
pub use self::external_api_key::{
ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord,
@@ -214,7 +219,9 @@ pub(crate) use self::editor_project::{
map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result,
map_editor_project_required_procedure_result,
map_editor_project_resource_list_procedure_result,
map_editor_project_resource_procedure_result,
map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result,
map_editor_showcase_asset_procedure_result,
map_editor_showcase_campaign_config_procedure_result,
};
pub(crate) use self::external_api_key::{
map_external_api_key_list_procedure_result, map_external_api_key_single_procedure_result,

Some files were not shown because too many files have changed in this diff Show More