修复后台素材查询分页与任务归组

支持陶泥号解析、稳定游标和后台素材多页查询

补齐查询防抖、请求取消、刷新保留与加载更多状态

新增可信来源索引和拆分批次完成事实,修复跨项目与删除切片归组

同步生成绑定、数据契约、项目记忆和回归测试
This commit is contained in:
2026-07-15 20:02:34 +08:00
parent 8d1c640eb5
commit 793ec1b6b0
29 changed files with 2885 additions and 169 deletions
+2 -1
View File
@@ -398,10 +398,11 @@ export function getAdminAssetReadUrl(
export function listAdminEditorAssets(
token: string,
query: AdminEditorAssetListQuery = {},
signal?: AbortSignal,
) {
return request<AdminEditorAssetListResponse>(
`/admin/api/editor-assets${buildEditorAssetListQuery(query)}`,
{ token },
{ token, signal },
);
}
+1
View File
@@ -460,6 +460,7 @@ export interface AdminEditorAssetPayload {
model?: string | null;
provider?: string | null;
taskId?: string | null;
groupTaskId?: string | null;
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
sourceResourceId?: string | null;
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X } from 'lucide-react';
import { Fragment, type ReactNode } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
getAdminAssetReadUrl,
@@ -24,6 +24,7 @@ const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300;
const ADMIN_ASSET_READ_DISPATCH_SPACING_MS = 40;
const ADMIN_ASSET_READ_RETRY_DELAYS_MS = [400, 1_200, 3_000] as const;
const ADMIN_ASSET_THUMBNAIL_ROOT_MARGIN = '240px 0px';
const ADMIN_EDITOR_ASSET_FILTER_DEBOUNCE_MS = 300;
const AUDIO_ASSET_COVER_SRC = `${import.meta.env.DEV ? import.meta.env.BASE_URL : '/'}creation-home/audio-asset-cover.png`;
let adminAssetReadDispatchTail = Promise.resolve();
@@ -37,6 +38,9 @@ export function AdminEditorAssetQueryPage({
const [createdAfter, setCreatedAfter] = useState('');
const [createdBefore, setCreatedBefore] = useState('');
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [committedQueryKey, setCommittedQueryKey] = useState<string | null>(
null,
);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
@@ -51,49 +55,147 @@ export function AdminEditorAssetQueryPage({
title: string;
prompt: string;
} | null>(null);
const listRequestGenerationRef = useRef(0);
const listRequestAbortControllerRef = useRef<AbortController | null>(null);
const automaticQueryInitializedRef = useRef(false);
const automaticQueryTokenRef = useRef(token);
const activeQuery = buildListQuery();
const activeQueryKey = editorAssetListQueryKey(token, activeQuery);
const hasCurrentQuerySnapshot = committedQueryKey === activeQueryKey;
const visibleEntries = hasCurrentQuerySnapshot ? entries : [];
const visibleNextCursor = hasCurrentQuerySnapshot ? nextCursor : null;
useEffect(() => {
void refreshPage();
const runImmediately =
!automaticQueryInitializedRef.current ||
automaticQueryTokenRef.current !== token;
automaticQueryInitializedRef.current = true;
automaticQueryTokenRef.current = token;
if (runImmediately) {
void refreshPage();
return;
}
invalidateListRequest();
setIsLoading(true);
setIsLoadingMore(false);
setErrorMessage('');
const timer = setTimeout(
() => void refreshPage(),
ADMIN_EDITOR_ASSET_FILTER_DEBOUNCE_MS,
);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, ownerUserId, keyword, createdAfter, createdBefore]);
useEffect(
() => () => {
automaticQueryInitializedRef.current = false;
invalidateListRequest();
},
[],
);
async function refreshPage() {
const query = buildListQuery();
const queryKey = editorAssetListQueryKey(token, query);
const { requestGeneration, abortController } = beginListRequest();
setIsLoading(true);
setIsLoadingMore(false);
setErrorMessage('');
try {
const response = await listAdminEditorAssets(token, buildListQuery());
const response = await listAdminEditorAssets(
token,
query,
abortController.signal,
);
if (listRequestGenerationRef.current !== requestGeneration) {
return;
}
setEntries(response.entries);
setNextCursor(response.nextCursor ?? null);
setCommittedQueryKey(queryKey);
} catch (error: unknown) {
if (
listRequestGenerationRef.current !== requestGeneration ||
isAbortError(error)
) {
return;
}
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
if (listRequestGenerationRef.current === requestGeneration) {
if (listRequestAbortControllerRef.current === abortController) {
listRequestAbortControllerRef.current = null;
}
setIsLoading(false);
}
}
}
async function loadMore() {
if (!nextCursor || isLoadingMore) {
if (!visibleNextCursor || isLoading || isLoadingMore) {
return;
}
const query = buildListQuery();
const queryKey = editorAssetListQueryKey(token, query);
if (committedQueryKey !== queryKey) {
return;
}
const cursor = visibleNextCursor;
const { requestGeneration, abortController } = beginListRequest();
setIsLoadingMore(true);
setErrorMessage('');
try {
const response = await listAdminEditorAssets(token, {
...buildListQuery(),
cursor: nextCursor,
});
const response = await listAdminEditorAssets(
token,
{
...query,
cursor,
},
abortController.signal,
);
if (listRequestGenerationRef.current !== requestGeneration) {
return;
}
setEntries((current) => mergeAssetEntries(current, response.entries));
setNextCursor(response.nextCursor ?? null);
} catch (error: unknown) {
if (
listRequestGenerationRef.current !== requestGeneration ||
isAbortError(error)
) {
return;
}
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoadingMore(false);
if (listRequestGenerationRef.current === requestGeneration) {
if (listRequestAbortControllerRef.current === abortController) {
listRequestAbortControllerRef.current = null;
}
setIsLoadingMore(false);
}
}
}
function beginListRequest() {
invalidateListRequest();
const abortController = new AbortController();
listRequestAbortControllerRef.current = abortController;
const requestGeneration = listRequestGenerationRef.current;
return { requestGeneration, abortController };
}
function invalidateListRequest() {
listRequestGenerationRef.current += 1;
listRequestAbortControllerRef.current?.abort();
listRequestAbortControllerRef.current = null;
}
function buildListQuery(): AdminEditorAssetListQuery {
return {
ownerUserId: ownerUserId || null,
ownerUserId: ownerUserId.trim() || null,
keyword: keyword.trim() || null,
createdAfter: dateInputToStartRfc3339(createdAfter),
createdBefore: dateInputToEndRfc3339(createdBefore),
@@ -143,7 +245,7 @@ export function AdminEditorAssetQueryPage({
/>
</label>
<label className="admin-field">
<span> ID</span>
<span> ID / </span>
<input
value={ownerUserId}
onChange={(event) => setOwnerUserId(event.target.value)}
@@ -173,7 +275,7 @@ export function AdminEditorAssetQueryPage({
</tr>
</thead>
<tbody>
{entries.map((entry) => {
{visibleEntries.map((entry) => {
const promptText = entry.prompt || entry.actualPrompt || '-';
const expanded = expandedTaskIds.has(entry.assetId);
const children = entry.children ?? [];
@@ -318,13 +420,13 @@ export function AdminEditorAssetQueryPage({
</table>
</div>
{!isLoading && entries.length === 0 ? (
{!isLoading && !errorMessage && visibleEntries.length === 0 ? (
<div className="admin-empty-state"></div>
) : null}
{nextCursor ? (
{visibleNextCursor ? (
<button
className="admin-secondary-button"
disabled={isLoadingMore}
disabled={isLoading || isLoadingMore}
type="button"
onClick={loadMore}
>
@@ -645,6 +747,12 @@ function AdminAssetDetailDialog({
{entry.provider || '-'}
</AdminInfoItem>
<AdminInfoItem label="Task ID">{entry.taskId || '-'}</AdminInfoItem>
{entry.groupTaskId?.trim() &&
entry.groupTaskId.trim() !== entry.taskId?.trim() ? (
<AdminInfoItem label="归组 Task ID">
{entry.groupTaskId.trim()}
</AdminInfoItem>
) : null}
<AdminInfoItem label="Object Key">
{entry.objectKey || '-'}
</AdminInfoItem>
@@ -988,6 +1096,29 @@ function mergeAssetEntries(
);
}
function editorAssetListQueryKey(
token: string,
query: AdminEditorAssetListQuery,
) {
return JSON.stringify([
token.trim(),
query.ownerUserId?.trim() || null,
query.keyword?.trim() || null,
query.createdAfter?.trim() || null,
query.createdBefore?.trim() || null,
query.limit ?? null,
]);
}
function isAbortError(error: unknown) {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
error.name === 'AbortError'
);
}
function dateInputToStartRfc3339(value: string) {
return value ? `${value}T00:00:00+08:00` : null;
}
@@ -47,6 +47,14 @@
- 验证:`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml``cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``server-rs/crates/spacetime-client/src/editor_project.rs``server-rs/crates/api-server/src/admin.rs`
## 后台素材查询要在分页前归一用户、游标和派生任务
- 现象:输入 `SY-*` 陶泥号查不到已有素材;无筛选时只显示 80 个任务且没有“读取更多”;手动重拆图集后,`素材 N` 被单独当成零成本父任务,原图集又显示成另一组。
- 原因:`editor_asset.owner_user_id` 保存内部 `user_id`,不保存公开陶泥号;作者陶泥号在 procedure 返回后才映射,不能直接参与素材表过滤。`spacetime-client` 的统一时间文本是 `seconds.microsZ`,若 cursor 只按 RFC3339 解析,第 80 个任务无法生成 `nextCursor`。手动图集拆分使用独立 `editor-atlas-split-*` 任务号,但切片项目资源通过 `source_resource_id` 指回原图集资源,只按当前 `task_id` 分组会割裂同一条素材生产链。
- 处理:`api-server` 在调用素材 procedure 前把“用户 ID / 陶泥号”字段或精确 `SY-*` keyword 解析成内部 `user_id`;游标解析同时接受整数微秒、统一 `seconds.microsZ` 和 RFC3339,编码失败必须返回服务错误,不能伪装成末页,客户端传入的非法 cursor 必须返回 `400`,不能静默回到第一页。手动拆分素材保留真实 `task_id`,通过 `editor_asset_group_source_provenance` 按原 resource、asset object 或 Object Key 查找服务端生成账号素材的可信来源任务,再把来源任务写入 `editor_asset.group_task_id`;跨项目复用时同时携带当前和原始 source resource,首次历史回溯命中后补写 provenance,后续不再扫描账号全量素材。没有可信来源的新批次显式以自己的拆分任务作为 `group_task_id`,不得落回可读取用户资源元数据的 legacy 分支。每片保存 `group_task_expected_asset_count`,全部切片落库后再写不可逆的 `editor_asset_group_cohort` 完成事实;read model 依据完成事实判断批次资格,不用当前剩余行数猜测初始是否完整,因此用户后来删除切片不会让批次脱组。部分失败批次没有完成事实,始终保留为独立拆分任务。历史行兼容沿资源链回溯,删除项目时只固化直接引用待删资源且尚未固化的历史切片;固化始终保存真实来源任务,有界展示分组不得反写覆盖来源。read model 只让同根任务的一个完整拆分批次并入原任务,重复批次按真实拆分任务独立分页。带 owner 条件时先走 `by_editor_asset_owner_user_id`,不要为单用户查询扫描全站素材。后台筛选输入使用防抖并取消旧 transport;手动刷新同一查询失败时保留已有结果和游标,筛选已变化时不展示旧查询结果。
- 验证:API 测试覆盖陶泥号组合条件、未知陶泥号、可信来源归组、`seconds.microsZ` / 极值游标和真实 / 归组 Task IDSpacetimeDB 测试覆盖资源删除后稳定归组、部分失败批次不抢占根任务、重复拆分有界无丢失和 owner 索引分支;后台页面测试覆盖逐字输入防抖、请求取消、刷新失败保留结果、筛选请求乱序、旧分页响应失效和“用户 ID / 陶泥号”请求。再运行 `cargo test -p api-server admin_editor_asset --manifest-path server-rs/Cargo.toml``cargo test -p spacetime-module admin_editor_asset --manifest-path server-rs/Cargo.toml``npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx`
- 关联:`apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx``server-rs/crates/api-server/src/admin.rs``server-rs/crates/spacetime-module/src/editor_project_storage.rs`
## 后台素材缩略图不要在首次挂载时全量换签
- 现象:后台“素材查询”首批缩略图正常,继续向下滚动或读取更多后长期显示占位图;api-server journald 中已到达的 `/admin/api/assets/read-url` 可能全部是 `200`
@@ -48,14 +48,14 @@
- 新增 `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``generationCostMudPoints`、创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。
- 新增 `editor_asset` 表保存账号级素材:`assetId``ownerUserId``folderId`、名称、图片读取地址、可选封面 `thumbnailSrc`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、可选 `groupTaskId`、可选 `groupTaskExpectedAssetCount``assetKind``generationInputs``generationCostMudPoints`、创建时间和更新时间。素材只跟账号走,不跟 project 走;`taskId` 保留真实生成 / 拆分操作身份,`groupTaskId` 保存服务端验证后的来源任务,`groupTaskExpectedAssetCount` 保存该拆分批次完整时应有的素材数;角色、图标、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。
- 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/<objectKey>"``objectKey``assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*``data:video/*``data:audio/*``blob:`。旧数据读取时如果已有 `objectKey``imageSrc` 归一成 `/<objectKey>`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。裁扩在项目上下文中虽然由前端 canvas 本地渲染 PNG,也必须先上传 OSS / asset object 并创建 `editor_project_resource`,再把带正式 `resourceId/objectKey/assetObjectId` 的裁扩图层加入画布;不能先把 `local-resource-*` + Data URL 图层交给项目保存或后续去背景。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId``assetId` 写入参考图临时状态,生成请求仍使用临时状态中的图片源或 `objectKey`
- 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;角色、图标等纯色抠图生成器的前端用户路径不保存或恢复 `screenColor` / `segModel`,同源重绘也不再从 `generationInputs.fields` 恢复 `抠图背景色``抠图模型`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId``publicationGameInfo``publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口所需的图片 Data URL、signed URL 或 `objectKey` 只存在于提交前的临时参考图状态和请求体字段,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution``originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。
- 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。角色、图标图集、UI 提取和角色动作等多产物任务把 provider 原始输出及后处理结果分别入库:所有条目沿用 `character``icon-spritesheet``character-animation` 等真实类型,provider 原始输出承载任务模型成本,后处理派生产物阶段成本为 0。后台素材查询以最终产物为父行、每个中间产物为可展开的独立子行,分页只计算父任务。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。
- 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。角色、图标图集、UI 提取和角色动作等多产物任务把 provider 原始输出及后处理结果分别入库:所有条目沿用 `character``icon-spritesheet``character-animation` 等真实类型,provider 原始输出承载任务模型成本,后处理派生产物阶段成本为 0。后台素材查询以最终产物为父行、每个中间产物为可展开的独立子行,分页只计算父任务;手动重拆图集保留独立 `taskId` 用于存储隔离和日志排障,通过私有 provenance 从服务端生成账号素材的 source resource、asset object 或 Object Key 取得可信来源任务,并把它写入 `groupTaskId`,不信任客户端可提交的 resource `taskId/assetKind`;跨项目复用后仍可通过稳定媒体引用找回来源。没有可信来源的新拆分显式归到自身任务,不走历史资源链回溯。每个手动切片同时写入 `groupTaskExpectedAssetCount`,全部切片落库后写独立 cohort 完成事实;后台 read model 只让同一根任务的一个已完成拆分批次并入原图集父项,用户后来删除单片不会让批次脱组,部分失败批次和后续重复拆分批次按各自真实任务分页,避免残缺批次抢占根任务、单组无限增长或素材丢失。历史行在项目资源仍存在时兼容回溯,删除项目资源前只固化直接受影响行的真实来源字段,有界展示 ID 不反写数据库`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。
- 生成面板不展示资源名称输入,默认使用原有自动编号;提示词输入保持统一可见边框。内部命名契约仍使用可选 `assetLabel`,最大 80 字符并在提交时 trim;历史状态或内部调用携带非空名称时,同一个名称必须贯穿 `assetLabel``canvasCompletion.title`、项目资源、账号素材和本地兜底图层,刷新后不得退回模板名。图标图集与角色动作请求同样兼容该字段,中间原图使用主名称加固定后缀,拆分素材继续按素材描述命名。
- 画布 Agent 会话按“SpacetimeDB 元数据 + OSS 消息正文”存储:`editor_agent_conversation` 只保存 `conversationId/projectId/ownerUserId/title/messagesObjectKey/deleted/createdAt/updatedAt` 等会话元数据;消息正文整体保存为私有 OSS JSON 文档 `editor-agent/{conversationId}.json`。消息文档单对象上限为 2 MiB,同一会话的消息追加和 SSE 最终写回由 api-server 按 `conversationId` 串行化,避免“读 OSS → 改消息 → 写 OSS”并发覆盖。前端只通过 api-server BFF 读取和发送会话,不直接读写 SpacetimeDB,也不直接读写 OSS。
- Agent 消息附件只允许引用当前工程画布资源或账号素材库图片,来源类型为 `canvas_resource` / `library_asset`,最多 9 张。附件请求可携带展示用 `imageSrc/thumbnailSrc/objectKey/width/height/label`,但持久化真相仍以后端校验后的 resource / asset 行和 OSS 对象为准;不得把 Data URL、signed URL 或 blob URL 当作会话长期事实。
File diff suppressed because one or more lines are too long
+311 -102
View File
@@ -626,29 +626,40 @@ pub async fn admin_list_editor_assets(
.clamp(1, ADMIN_EDITOR_ASSET_MAX_LIMIT);
let date_range = resolve_admin_editor_asset_date_range(&query)
.map_err(|error| AppError::from_status(StatusCode::BAD_REQUEST).with_message(error))?;
let cursor = normalize_admin_optional(query.cursor);
validate_admin_editor_asset_cursor(cursor.as_deref())
.map_err(|error| AppError::from_status(StatusCode::BAD_REQUEST).with_message(error))?;
let Some((owner_user_id, keyword)) = resolve_admin_editor_asset_filters(
&state,
normalize_admin_optional(query.owner_user_id),
normalize_admin_optional(query.keyword),
)?
else {
return Ok(json_success_body(
Some(&request_context),
AdminEditorAssetListResponse {
entries: Vec::new(),
next_cursor: None,
},
));
};
let records = state
.spacetime_client()
.admin_list_editor_assets(AdminEditorAssetListRecordInput {
limit: page_size + 1,
cursor: normalize_admin_optional(query.cursor),
owner_user_id: normalize_admin_optional(query.owner_user_id),
keyword: normalize_admin_optional(query.keyword),
cursor,
owner_user_id,
keyword,
created_after_micros: date_range.created_after_micros,
created_before_micros: date_range.created_before_micros,
})
.await
.map_err(map_admin_spacetime_error)?;
let mut groups = group_admin_editor_asset_records(records);
let next_cursor = if groups.len() > page_size as usize {
groups
.get(page_size.saturating_sub(1) as usize)
.and_then(|group| group.first())
.and_then(admin_editor_asset_cursor)
} else {
None
};
groups.truncate(page_size as usize);
let (groups, next_cursor) = paginate_admin_editor_asset_records(records, page_size as usize)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error)
})?;
Ok(json_success_body(
Some(&request_context),
AdminEditorAssetListResponse {
@@ -1101,6 +1112,7 @@ fn admin_editor_asset_payload_from_record(
model: record.model,
provider: record.provider,
task_id: record.task_id,
group_task_id: record.group_task_id,
asset_kind: record.asset_kind,
generation_inputs: record.generation_inputs,
source_resource_id: record.source_resource_id,
@@ -1122,9 +1134,10 @@ fn group_admin_editor_asset_records(
let mut indexes = BTreeMap::<String, usize>::new();
for record in records {
let key = record
.task_id
.as_ref()
.map(|value| value.trim())
.group_task_id
.as_deref()
.or(record.task_id.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| format!("owner:{}:task:{value}", record.owner_user_id))
.unwrap_or_else(|| format!("asset:{}", record.asset_id));
@@ -1141,6 +1154,24 @@ fn group_admin_editor_asset_records(
groups
}
fn paginate_admin_editor_asset_records(
records: Vec<AdminEditorAssetRecord>,
page_size: usize,
) -> Result<(Vec<Vec<AdminEditorAssetRecord>>, Option<String>), String> {
let mut groups = group_admin_editor_asset_records(records);
let next_cursor = if groups.len() > page_size {
let cursor_record = groups
.get(page_size.saturating_sub(1))
.and_then(|group| group.first())
.ok_or_else(|| "生成素材分页游标失败:分页末项不存在".to_string())?;
Some(admin_editor_asset_cursor(cursor_record)?)
} else {
None
};
groups.truncate(page_size);
Ok((groups, next_cursor))
}
fn admin_editor_asset_group_payload(
state: &AppState,
mut records: Vec<AdminEditorAssetRecord>,
@@ -1262,49 +1293,88 @@ fn resolve_admin_editor_asset_generator(record: &AdminEditorAssetRecord) -> Stri
.to_string()
}
fn admin_editor_asset_cursor(record: &AdminEditorAssetRecord) -> Option<String> {
let created_at_micros = parse_timestamp_text_to_micros(record.created_at.as_str())?;
Some(format!("{created_at_micros}:{}", record.asset_id))
fn admin_editor_asset_cursor(record: &AdminEditorAssetRecord) -> Result<String, String> {
let created_at_micros = parse_timestamp_text_to_micros(record.created_at.as_str())
.ok_or_else(|| format!("素材 {} 的创建时间格式非法", record.asset_id))?;
Ok(format!("{created_at_micros}:{}", record.asset_id))
}
#[cfg(test)]
fn matches_admin_editor_asset_keyword(
payload: &AdminEditorAssetPayload,
keyword: Option<&str>,
) -> bool {
let Some(keyword) = keyword else {
return true;
fn validate_admin_editor_asset_cursor(cursor: Option<&str>) -> Result<(), String> {
let Some(cursor) = cursor else {
return Ok(());
};
let haystack = [
payload.asset_id.as_str(),
payload.owner_user_id.as_str(),
payload.author_display_name.as_deref().unwrap_or_default(),
payload
.author_public_user_code
.as_deref()
.unwrap_or_default(),
payload.folder_id.as_str(),
payload.label.as_str(),
payload.asset_object_id.as_deref().unwrap_or_default(),
payload.image_src.as_str(),
payload.object_key.as_deref().unwrap_or_default(),
payload.source_type.as_str(),
payload.prompt.as_deref().unwrap_or_default(),
payload.actual_prompt.as_deref().unwrap_or_default(),
payload.model.as_deref().unwrap_or_default(),
payload.provider.as_deref().unwrap_or_default(),
payload.task_id.as_deref().unwrap_or_default(),
payload.asset_kind.as_deref().unwrap_or_default(),
payload.source_resource_id.as_deref().unwrap_or_default(),
payload.thumbnail_src.as_deref().unwrap_or_default(),
]
.join(" ")
.to_lowercase();
haystack.contains(keyword)
|| payload
.generation_inputs
let Some((created_at_micros, asset_id)) = cursor.split_once(':') else {
return Err("cursor 格式非法".to_string());
};
if created_at_micros.parse::<i64>().is_err() || asset_id.trim().is_empty() {
return Err("cursor 格式非法".to_string());
}
Ok(())
}
fn resolve_admin_editor_asset_filters(
state: &AppState,
mut owner_user_id: Option<String>,
mut keyword: Option<String>,
) -> Result<Option<(Option<String>, Option<String>)>, AppError> {
if let Some(public_user_code) = owner_user_id
.as_deref()
.and_then(normalize_admin_public_user_code_candidate)
{
owner_user_id = resolve_admin_editor_asset_public_user_id(state, &public_user_code)?;
if owner_user_id.is_none() {
return Ok(None);
}
}
if let Some(public_user_code) = keyword
.as_deref()
.and_then(normalize_admin_public_user_code_candidate)
{
let Some(keyword_owner_user_id) =
resolve_admin_editor_asset_public_user_id(state, &public_user_code)?
else {
return Ok(None);
};
if owner_user_id
.as_ref()
.is_some_and(|value| value.to_string().to_lowercase().contains(keyword))
.is_some_and(|owner_user_id| owner_user_id != &keyword_owner_user_id)
{
return Ok(None);
}
owner_user_id = Some(keyword_owner_user_id);
keyword = None;
}
Ok(Some((owner_user_id, keyword)))
}
fn normalize_admin_public_user_code_candidate(value: &str) -> Option<String> {
let compact = value
.trim()
.chars()
.filter(|character| character.is_ascii_alphanumeric())
.collect::<String>()
.to_ascii_uppercase();
let digits = compact.strip_prefix("SY")?;
if digits.is_empty() || !digits.chars().all(|character| character.is_ascii_digit()) {
return None;
}
module_auth::normalize_public_user_code(value).ok()
}
fn resolve_admin_editor_asset_public_user_id(
state: &AppState,
public_user_code: &str,
) -> Result<Option<String>, AppError> {
state
.auth_user_service()
.get_user_by_public_user_code(public_user_code)
.map(|user| user.map(|user| user.id))
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("读取素材作者认证信息失败:{error}"))
})
}
fn normalize_admin_optional(value: Option<String>) -> Option<String> {
@@ -3501,6 +3571,22 @@ fn parse_timestamp_text_to_micros(text: &str) -> Option<i64> {
if let Ok(value) = trimmed.parse::<i64>() {
return Some(value);
}
if let Some((seconds, micros)) = trimmed
.strip_suffix('Z')
.and_then(|value| value.split_once('.'))
{
if let Ok(seconds) = seconds.parse::<i64>()
&& !micros.is_empty()
&& micros.len() <= 6
&& micros.chars().all(|character| character.is_ascii_digit())
{
let micros = format!("{micros:0<6}").parse::<i128>().ok()?;
let value = i128::from(seconds)
.checked_mul(1_000_000)?
.checked_add(micros)?;
return i64::try_from(value).ok();
}
}
OffsetDateTime::parse(trimmed, &Rfc3339)
.ok()
.and_then(|timestamp| i64::try_from(timestamp.unix_timestamp_nanos() / 1_000).ok())
@@ -3846,12 +3932,14 @@ mod tests {
clamp_admin_tracking_event_limit, enforce_admin_request_permission,
finalize_admin_database_table_rows_response, group_admin_editor_asset_records,
hash_admin_password, is_admin_account_not_found, is_safe_spacetime_table_name,
matches_admin_editor_asset_keyword, normalize_debug_path, normalize_table_count_error,
normalize_debug_path, normalize_table_count_error, paginate_admin_editor_asset_records,
parse_admin_database_table_rows_sql_response, parse_admin_tracking_event_keys_sql_response,
parse_admin_tracking_events_sql_response, parse_spacetime_sql_count_response,
resolve_admin_dashboard_range, resolve_admin_dashboard_range_at,
resolve_admin_database_table_sql_limit, timestamp_value_to_micros, trim_preview,
verify_admin_password, wallet_ledger_source_type_to_string,
parse_timestamp_text_to_micros, resolve_admin_dashboard_range,
resolve_admin_dashboard_range_at, resolve_admin_database_table_sql_limit,
resolve_admin_editor_asset_filters, timestamp_value_to_micros, trim_preview,
validate_admin_editor_asset_cursor, verify_admin_password,
wallet_ledger_source_type_to_string,
};
use axum::{
http::{Method, StatusCode},
@@ -3859,8 +3947,7 @@ mod tests {
};
use serde_json::json;
use shared_contracts::admin::{
AdminDashboardQuery, AdminDatabaseTableRowsQuery, AdminEditorAssetPayload,
AdminTrackingEventListQuery,
AdminDashboardQuery, AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery,
};
use shared_contracts::assets::GetReadUrlQuery;
use spacetime_client::{
@@ -3934,6 +4021,7 @@ mod tests {
model: Some("gpt-image-2".to_string()),
provider: Some("VectorEngine".to_string()),
task_id: Some(task_id.to_string()),
group_task_id: None,
asset_kind: Some("character".to_string()),
generation_inputs: None,
source_resource_id: None,
@@ -4034,48 +4122,169 @@ mod tests {
}
#[test]
fn admin_editor_asset_keyword_matches_public_author_prompt_and_generation_inputs() {
let payload = AdminEditorAssetPayload {
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()),
folder_id: "folder-1".to_string(),
label: "角色形象".to_string(),
asset_object_id: Some("asset-object-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,
source_type: "generated".to_string(),
prompt: Some("完整提示词内容".to_string()),
actual_prompt: None,
model: Some("gpt-image-2".to_string()),
provider: Some("character".to_string()),
task_id: Some("task-1".to_string()),
asset_kind: Some("character".to_string()),
generation_inputs: Some(json!({"style": "clay"})),
source_resource_id: Some("resource-1".to_string()),
thumbnail_src: None,
generation_cost_mud_points: 12,
created_at: "2026-07-04T10:00:00Z".to_string(),
updated_at: "2026-07-04T10:00:00Z".to_string(),
generator: "角色生图".to_string(),
task_generator: "角色生成".to_string(),
task_cost_mud_points: 12,
children: Vec::new(),
};
fn admin_editor_asset_group_uses_group_task_id_without_hiding_operation_task_id() {
let final_output = test_admin_editor_asset_record(
"asset-final",
"vector-engine-source-task-1",
"图标素材图集",
"generated-icon-spritesheets/editor/spritesheet-task-1.png",
9,
);
let mut manual_slice = test_admin_editor_asset_record(
"asset-slice",
"editor-atlas-split-operation-1",
"素材 1",
"generated-icon-spritesheet-assets/editor/slice-1.png",
0,
);
manual_slice.group_task_id = Some("vector-engine-source-task-1".to_string());
assert!(matches_admin_editor_asset_keyword(
&payload,
Some("sy-00000042")
));
assert!(matches_admin_editor_asset_keyword(&payload, Some("提示词")));
assert!(matches_admin_editor_asset_keyword(&payload, Some("clay")));
assert!(!matches_admin_editor_asset_keyword(
&payload,
Some("不存在的素材")
));
let mut groups = group_admin_editor_asset_records(vec![final_output, manual_slice]);
assert_eq!(groups.len(), 1);
let state = AppState::new(AppConfig::default()).expect("state should build");
let payload = admin_editor_asset_group_payload(&state, groups.remove(0))
.expect("group payload should exist");
assert_eq!(
payload.task_id.as_deref(),
Some("vector-engine-source-task-1")
);
assert_eq!(payload.children.len(), 1);
assert_eq!(
payload.children[0].task_id.as_deref(),
Some("editor-atlas-split-operation-1")
);
assert_eq!(
payload.children[0].group_task_id.as_deref(),
Some("vector-engine-source-task-1")
);
}
#[test]
fn admin_editor_asset_filters_resolve_public_user_code_before_querying_assets() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.auth_user_service()
.ensure_orphan_work_owner_user("user-author-42", "author-42", "作者昵称", "SY-00000042")
.expect("author should be inserted");
state
.auth_user_service()
.ensure_orphan_work_owner_user("user-author-43", "author-43", "另一作者", "SY-00000043")
.expect("second author should be inserted");
let owner_filter = resolve_admin_editor_asset_filters(
&state,
Some("SY-00000042".to_string()),
Some("角色".to_string()),
)
.expect("owner filter should resolve")
.expect("owner should exist");
assert_eq!(owner_filter.0.as_deref(), Some("user-author-42"));
assert_eq!(owner_filter.1.as_deref(), Some("角色"));
let keyword_filter =
resolve_admin_editor_asset_filters(&state, None, Some("sy00000042".to_string()))
.expect("keyword filter should resolve")
.expect("owner should exist");
assert_eq!(keyword_filter.0.as_deref(), Some("user-author-42"));
assert_eq!(keyword_filter.1, None);
let matching_filters = resolve_admin_editor_asset_filters(
&state,
Some("user-author-42".to_string()),
Some("SY-00000042".to_string()),
)
.expect("matching filters should resolve")
.expect("matching author should exist");
assert_eq!(matching_filters.0.as_deref(), Some("user-author-42"));
assert_eq!(matching_filters.1, None);
assert_eq!(
resolve_admin_editor_asset_filters(
&state,
Some("user-author-42".to_string()),
Some("SY-00000043".to_string()),
)
.expect("conflicting filters should resolve to an empty query"),
None
);
assert_eq!(
resolve_admin_editor_asset_filters(&state, None, Some("SY-00009999".to_string()),)
.expect("missing author should produce an empty query"),
None
);
assert_eq!(
resolve_admin_editor_asset_filters(&state, Some("SY-00009999".to_string()), None,)
.expect("missing owner should produce an empty query"),
None
);
}
#[test]
fn admin_editor_asset_cursor_accepts_shared_seconds_micros_timestamp() {
let mut record = test_admin_editor_asset_record(
"asset-page-end",
"task-page-end",
"分页末项",
"generated-images/editor/page-end.png",
3,
);
record.created_at = "1784087532.408003Z".to_string();
assert_eq!(
super::admin_editor_asset_cursor(&record).expect("cursor should build"),
"1784087532408003:asset-page-end"
);
assert_eq!(
parse_timestamp_text_to_micros("2026-07-15T11:52:12+08:00"),
Some(1_784_087_532_000_000)
);
assert_eq!(
parse_timestamp_text_to_micros("-9223372036855.224192Z"),
Some(i64::MIN)
);
record.created_at = "invalid-timestamp".to_string();
assert!(super::admin_editor_asset_cursor(&record).is_err());
}
#[test]
fn admin_editor_asset_cursor_rejects_malformed_input_instead_of_repeating_first_page() {
assert!(validate_admin_editor_asset_cursor(None).is_ok());
assert!(validate_admin_editor_asset_cursor(Some("123:asset-1")).is_ok());
assert!(validate_admin_editor_asset_cursor(Some("invalid")).is_err());
assert!(validate_admin_editor_asset_cursor(Some("abc:asset-1")).is_err());
assert!(validate_admin_editor_asset_cursor(Some("123: ")).is_err());
}
#[test]
fn admin_editor_asset_page_returns_cursor_for_eighty_first_task() {
let mut records = (0..81)
.map(|index| {
test_admin_editor_asset_record(
format!("asset-{index}").as_str(),
format!("task-{index}").as_str(),
format!("素材 {index}").as_str(),
format!("generated-images/editor/{index}.png").as_str(),
3,
)
})
.collect::<Vec<_>>();
records[79].created_at = "1784087532.408003Z".to_string();
let (groups, next_cursor) =
paginate_admin_editor_asset_records(records, 80).expect("page should build");
assert_eq!(groups.len(), 80);
assert_eq!(next_cursor.as_deref(), Some("1784087532408003:asset-79"));
assert!(
groups
.iter()
.flatten()
.all(|record| record.asset_id != "asset-80")
);
}
#[test]
@@ -38,7 +38,8 @@ use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetGroupCohortCompleteRecordInput,
EditorAssetGroupSourceLookupRecordInput, EditorAssetLibraryRecord,
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorProjectCreateRecordInput,
EditorProjectDeleteRecordInput, EditorProjectGetRecordInput,
@@ -461,6 +462,8 @@ pub(crate) struct PersistEditorGeneratedAssetInput {
pub(crate) model: String,
pub(crate) provider: String,
pub(crate) task_id: String,
pub(crate) group_task_id: Option<String>,
pub(crate) group_task_expected_asset_count: Option<u32>,
pub(crate) source_resource_id: Option<String>,
pub(crate) asset_kind: Option<String>,
pub(crate) generation_inputs: Option<Value>,
@@ -1203,6 +1206,8 @@ pub async fn create_editor_asset(
now_micros: current_utc_micros(),
thumbnail_src: None,
generation_cost_mud_points: 0,
group_task_id: None,
group_task_expected_asset_count: None,
})
.await
.map_err(map_editor_project_error)?;
@@ -1740,6 +1745,8 @@ pub(crate) async fn generate_editor_image_for_owner(
model: output_model,
provider: output_provider,
task_id: generated.task_id.clone(),
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: provider_source_resource_id.or(payload.source_resource_id),
asset_kind: payload.asset_kind,
generation_inputs: output_generation_inputs,
@@ -2561,6 +2568,8 @@ pub(crate) async fn edit_editor_image_for_owner(
model: generation_options.model.to_string(),
provider: "VectorEngine".to_string(),
task_id: generated.task_id.clone(),
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: payload.source_resource_id,
asset_kind: payload.asset_kind,
generation_inputs: payload.generation_inputs,
@@ -2692,6 +2701,8 @@ pub(crate) async fn remove_editor_image_background_for_owner(
model: "BiRefNet".to_string(),
provider: "BiRefNet".to_string(),
task_id: task_id.clone(),
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: payload.source_resource_id,
asset_kind: payload.asset_kind,
generation_inputs: payload.generation_inputs,
@@ -3404,6 +3415,7 @@ struct PersistEditorSpritesheetSlicesInput {
asset_folder_id: Option<String>,
source_resource_id: Option<String>,
task_id: String,
group_task_id: Option<String>,
prompt: String,
actual_prompt: Option<String>,
model: String,
@@ -3428,6 +3440,12 @@ async fn persist_editor_spritesheet_slices(
);
}
let expected_asset_count = u32::try_from(slices.len()).map_err(|_| {
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
"provider": "editor-icon-spritesheet-slicing",
"message": "图集拆分产物数量超过支持范围。",
}))
})?;
let mut results = Vec::with_capacity(slices.len());
for (index, slice) in slices.into_iter().enumerate() {
let image = DownloadedOpenAiImage {
@@ -3468,6 +3486,11 @@ async fn persist_editor_spritesheet_slices(
model: input.model.clone(),
provider: input.provider.clone(),
task_id: input.task_id.clone(),
group_task_id: input.group_task_id.clone(),
group_task_expected_asset_count: input
.group_task_id
.as_ref()
.map(|_| expected_asset_count),
source_resource_id: input.source_resource_id.clone(),
asset_kind: Some("icon".to_string()),
generation_inputs: input.generation_inputs.clone(),
@@ -3485,6 +3508,24 @@ async fn persist_editor_spritesheet_slices(
asset: record.asset,
});
}
if let Some(group_task_id) = input.group_task_id.as_deref() {
let asset_ids = results
.iter()
.filter_map(|result| result.asset.as_ref().map(|asset| asset.asset_id.clone()))
.collect::<Vec<_>>();
state
.spacetime_client()
.complete_editor_asset_group_cohort(EditorAssetGroupCohortCompleteRecordInput {
owner_user_id: input.owner_user_id,
task_id: input.task_id,
group_task_id: group_task_id.to_string(),
expected_asset_count,
asset_ids,
completed_at_micros: current_utc_micros(),
})
.await
.map_err(map_editor_project_error)?;
}
Ok(results)
}
@@ -3778,6 +3819,8 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
model: removal.model.clone(),
provider: removal.provider.to_string(),
task_id: generated.task_id.clone(),
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: source_record
.resource
.as_ref()
@@ -3812,6 +3855,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
.as_ref()
.map(|resource| resource.resource_id.clone()),
task_id: generated.task_id.clone(),
group_task_id: None,
prompt: "自动拆分图集".to_string(),
actual_prompt: None,
model: "connected-components".to_string(),
@@ -3938,6 +3982,7 @@ pub async fn split_editor_icon_spritesheet(
.resources
.iter()
.find(|resource| resource.resource_id == source_resource_id)
.cloned()
.ok_or_else(|| {
AppError::from_status(StatusCode::NOT_FOUND).with_details(json!({
"provider": "editor-icon-spritesheet-slicing",
@@ -3952,6 +3997,14 @@ pub async fn split_editor_icon_spritesheet(
})),
);
}
let task_id = build_prefixed_uuid_id("editor-atlas-split-");
let group_task_id = resolve_editor_manual_atlas_split_group_task_id(
&state,
owner_user_id.as_str(),
&source_resource,
)
.await?
.unwrap_or_else(|| task_id.clone());
let reference =
parse_editor_reference_image(&state, owner_user_id.as_str(), source_resource_id.as_str())
@@ -3970,7 +4023,6 @@ pub async fn split_editor_icon_spritesheet(
}))
})?;
validate_editor_icon_spritesheet_manual_slice_count(slices.len())?;
let task_id = build_prefixed_uuid_id("editor-atlas-split-");
let prompt = source_resource
.prompt
.clone()
@@ -3996,6 +4048,7 @@ pub async fn split_editor_icon_spritesheet(
.or_else(|| Some(EDITOR_ASSET_DEFAULT_FOLDER_ID.to_string())),
source_resource_id: Some(source_resource_id),
task_id,
group_task_id: Some(group_task_id),
prompt,
actual_prompt,
model,
@@ -4059,6 +4112,47 @@ fn validate_editor_icon_spritesheet_manual_source(
validate_editor_icon_spritesheet_manual_dimensions(width, height)
}
async fn resolve_editor_manual_atlas_split_group_task_id(
state: &AppState,
owner_user_id: &str,
source_resource: &EditorProjectResourceRecord,
) -> Result<Option<String>, AppError> {
let lookup = editor_manual_atlas_split_group_source_lookup_input(
owner_user_id,
source_resource.resource_id.as_str(),
source_resource.source_resource_id.as_deref(),
source_resource.asset_object_id.as_deref(),
source_resource.object_key.as_deref(),
);
let asset = state
.spacetime_client()
.find_editor_asset_group_source(lookup)
.await
.map_err(map_editor_project_error)?;
Ok(asset.and_then(|asset| normalize_optional_string(asset.task_id)))
}
fn editor_manual_atlas_split_group_source_lookup_input(
owner_user_id: &str,
resource_id: &str,
source_resource_id: Option<&str>,
asset_object_id: Option<&str>,
object_key: Option<&str>,
) -> EditorAssetGroupSourceLookupRecordInput {
let mut source_resource_ids = vec![resource_id.to_string()];
if let Some(source_resource_id) =
normalize_optional_string(source_resource_id.map(str::to_string))
{
source_resource_ids.push(source_resource_id);
}
EditorAssetGroupSourceLookupRecordInput {
owner_user_id: owner_user_id.to_string(),
source_resource_ids,
asset_object_id: asset_object_id.map(str::to_string),
object_key: object_key.map(str::to_string),
}
}
fn validate_editor_icon_spritesheet_manual_dimensions(
width: u32,
height: u32,
@@ -4384,6 +4478,8 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
model: removal.model.clone(),
provider: removal.provider.to_string(),
task_id: generated.task_id.clone(),
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: source_record
.resource
.as_ref()
@@ -4415,6 +4511,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
.as_ref()
.map(|resource| resource.resource_id.clone()),
task_id: generated.task_id.clone(),
group_task_id: None,
prompt: "自动拆分图集".to_string(),
actual_prompt: None,
model: "connected-components".to_string(),
@@ -5166,6 +5263,8 @@ pub(crate) async fn persist_editor_generated_asset(
generation_cost_mud_points: input.generation_cost_mud_points,
now_micros,
thumbnail_src: normalize_optional_string(input.thumbnail_src),
group_task_id: normalize_optional_string(input.group_task_id),
group_task_expected_asset_count: input.group_task_expected_asset_count,
})
.await
.map_err(map_editor_project_error)?;
@@ -5204,6 +5303,8 @@ pub(crate) async fn persist_editor_generated_media_asset(
model: input.model,
provider: input.provider,
task_id: input.task_id,
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: input.source_resource_id,
asset_kind: input.asset_kind,
generation_inputs: input.generation_inputs,
@@ -6471,6 +6572,8 @@ async fn persist_editor_provider_source_resource(
model: input.model,
provider: "VectorEngine".to_string(),
task_id: input.task_id,
group_task_id: None,
group_task_expected_asset_count: None,
source_resource_id: input.source_resource_id,
asset_kind: input.asset_kind,
generation_inputs: input.generation_inputs,
@@ -9233,6 +9336,41 @@ mod tests {
assert_eq!(request.canvas_completion.placeholder.width, 360.0);
}
#[test]
fn editor_manual_atlas_split_lookup_keeps_original_provenance_after_project_copy() {
let mut copied_resource = editor_project_resource_for_canvas_test(
"copied-resource-atlas-1",
"icon-spritesheet",
1024,
1024,
);
copied_resource.source_resource_id = Some("resource-atlas-1".to_string());
copied_resource.asset_object_id = Some("asset-object-atlas-1".to_string());
copied_resource.object_key = Some("generated/icon-atlas-1.png".to_string());
let lookup = editor_manual_atlas_split_group_source_lookup_input(
"user-1",
copied_resource.resource_id.as_str(),
copied_resource.source_resource_id.as_deref(),
copied_resource.asset_object_id.as_deref(),
copied_resource.object_key.as_deref(),
);
assert_eq!(lookup.owner_user_id, "user-1");
assert_eq!(
lookup.source_resource_ids,
vec!["copied-resource-atlas-1", "resource-atlas-1"]
);
assert_eq!(
lookup.asset_object_id.as_deref(),
Some("asset-object-atlas-1")
);
assert_eq!(
lookup.object_key.as_deref(),
Some("generated/icon-atlas-1.png")
);
}
#[test]
fn editor_bgfilter_seg_model_parser_defaults_and_rejects_unknown() {
assert_eq!(
@@ -219,6 +219,7 @@ pub struct AdminEditorAssetPayload {
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub group_task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub source_resource_id: Option<String>,
@@ -333,6 +333,31 @@ impl SpacetimeClient {
.await
}
pub async fn find_editor_asset_group_source(
&self,
input: EditorAssetGroupSourceLookupRecordInput,
) -> Result<Option<EditorAssetRecord>, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"find_editor_asset_group_source_and_return",
move |connection, sender| {
connection
.procedures()
.find_editor_asset_group_source_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_optional_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn create_editor_asset_folder(
&self,
input: EditorAssetFolderCreateRecordInput,
@@ -431,6 +456,31 @@ impl SpacetimeClient {
.await
}
pub async fn complete_editor_asset_group_cohort(
&self,
input: EditorAssetGroupCohortCompleteRecordInput,
) -> Result<EditorAssetRecord, SpacetimeClientError> {
let procedure_input = input.into();
self.call_after_connect(
"complete_editor_asset_group_cohort_and_return",
move |connection, sender| {
connection
.procedures()
.complete_editor_asset_group_cohort_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_editor_asset_procedure_result);
send_once(&sender, mapped);
},
);
},
)
.await
}
pub async fn update_editor_asset(
&self,
input: EditorAssetUpdateRecordInput,
+2 -1
View File
@@ -39,7 +39,8 @@ pub use mapper::{
EditorAgentConversationTouchRecordInput, EditorAssetCreateRecordInput,
EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetGroupCohortCompleteRecordInput,
EditorAssetGroupSourceLookupRecordInput, EditorAssetLibraryRecord,
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorGenerationModelPricingRecord,
EditorGenerationPricingConfigRecord, EditorGenerationPricingConfigUpsertRecordInput,
@@ -91,7 +91,8 @@ pub use self::editor_project::{
AdminEditorAssetListRecordInput, AdminEditorAssetRecord, EditorAssetCreateRecordInput,
EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetLibraryRecord,
EditorAssetFolderUpdateRecordInput, EditorAssetGroupCohortCompleteRecordInput,
EditorAssetGroupSourceLookupRecordInput, EditorAssetLibraryRecord,
EditorAssetMediaRepairRecordInput, EditorAssetRecord, EditorAssetUpdateRecordInput,
EditorCanvasRecord, EditorCanvasViewportRecord, EditorGenerationModelPricingRecord,
EditorGenerationPricingConfigRecord, EditorGenerationPricingConfigUpsertRecordInput,
@@ -255,6 +256,7 @@ pub(crate) use self::editor_project::{
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,
map_optional_editor_asset_procedure_result,
};
pub(crate) use self::external_api_key::{
map_external_api_key_list_procedure_result, map_external_api_key_single_procedure_result,
@@ -121,6 +121,7 @@ pub struct AdminEditorAssetRecord {
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub group_task_id: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<serde_json::Value>,
pub source_resource_id: Option<String>,
@@ -344,6 +345,26 @@ pub struct EditorAssetCreateRecordInput {
pub now_micros: i64,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub group_task_id: Option<String>,
pub group_task_expected_asset_count: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetGroupSourceLookupRecordInput {
pub owner_user_id: String,
pub source_resource_ids: Vec<String>,
pub asset_object_id: Option<String>,
pub object_key: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorAssetGroupCohortCompleteRecordInput {
pub owner_user_id: String,
pub task_id: String,
pub group_task_id: String,
pub expected_asset_count: u32,
pub asset_ids: Vec<String>,
pub completed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -653,6 +674,36 @@ impl From<EditorAssetCreateRecordInput> for crate::module_bindings::EditorAssetC
now_micros: input.now_micros,
thumbnail_src: input.thumbnail_src,
generation_cost_mud_points: input.generation_cost_mud_points,
group_task_id: input.group_task_id,
group_task_expected_asset_count: input.group_task_expected_asset_count,
}
}
}
impl From<EditorAssetGroupSourceLookupRecordInput>
for crate::module_bindings::EditorAssetGroupSourceLookupInput
{
fn from(input: EditorAssetGroupSourceLookupRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
source_resource_ids: input.source_resource_ids,
asset_object_id: input.asset_object_id,
object_key: input.object_key,
}
}
}
impl From<EditorAssetGroupCohortCompleteRecordInput>
for crate::module_bindings::EditorAssetGroupCohortCompleteInput
{
fn from(input: EditorAssetGroupCohortCompleteRecordInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
task_id: input.task_id,
group_task_id: input.group_task_id,
expected_asset_count: input.expected_asset_count,
asset_ids: input.asset_ids,
completed_at_micros: input.completed_at_micros,
}
}
}
@@ -1003,6 +1054,16 @@ pub(crate) fn map_editor_asset_procedure_result(
.ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材快照"))
}
pub(crate) fn map_optional_editor_asset_procedure_result(
result: EditorAssetProcedureResult,
) -> Result<Option<EditorAssetRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result.asset.map(map_editor_asset_snapshot).transpose()
}
pub(crate) fn map_admin_editor_asset_list_procedure_result(
result: AdminEditorAssetListProcedureResult,
) -> Result<Vec<AdminEditorAssetRecord>, SpacetimeClientError> {
@@ -1239,6 +1300,7 @@ fn map_admin_editor_asset_snapshot(
model: snapshot.model,
provider: snapshot.provider,
task_id: snapshot.task_id,
group_task_id: snapshot.group_task_id,
asset_kind: snapshot.asset_kind,
generation_inputs,
source_resource_id: snapshot.source_resource_id,
@@ -261,6 +261,7 @@ pub mod compile_visual_novel_work_profile_procedure;
pub mod compile_wooden_fish_draft_procedure;
pub mod complete_ai_stage_and_return_procedure;
pub mod complete_ai_task_and_return_procedure;
pub mod complete_editor_asset_group_cohort_and_return_procedure;
pub mod complete_external_generation_job_and_return_procedure;
pub mod complete_profile_recharge_order_expiration_schedule_and_return_procedure;
pub mod confirm_asset_object_and_return_procedure;
@@ -415,6 +416,12 @@ pub mod editor_asset_folder_snapshot_type;
pub mod editor_asset_folder_table;
pub mod editor_asset_folder_type;
pub mod editor_asset_folder_update_input_type;
pub mod editor_asset_group_cohort_complete_input_type;
pub mod editor_asset_group_cohort_table;
pub mod editor_asset_group_cohort_type;
pub mod editor_asset_group_source_lookup_input_type;
pub mod editor_asset_group_source_provenance_table;
pub mod editor_asset_group_source_provenance_type;
pub mod editor_asset_library_get_input_type;
pub mod editor_asset_library_procedure_result_type;
pub mod editor_asset_library_snapshot_type;
@@ -534,6 +541,7 @@ pub mod finalize_match_3_d_agent_message_turn_procedure;
pub mod finalize_puzzle_agent_message_turn_procedure;
pub mod finalize_square_hole_agent_message_turn_procedure;
pub mod finalize_visual_novel_agent_message_turn_procedure;
pub mod find_editor_asset_group_source_and_return_procedure;
pub mod finish_bark_battle_run_procedure;
pub mod finish_match_3_d_time_up_procedure;
pub mod finish_square_hole_time_up_procedure;
@@ -1684,6 +1692,7 @@ pub use compile_visual_novel_work_profile_procedure::compile_visual_novel_work_p
pub use compile_wooden_fish_draft_procedure::compile_wooden_fish_draft;
pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return;
pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return;
pub use complete_editor_asset_group_cohort_and_return_procedure::complete_editor_asset_group_cohort_and_return;
pub use complete_external_generation_job_and_return_procedure::complete_external_generation_job_and_return;
pub use complete_profile_recharge_order_expiration_schedule_and_return_procedure::complete_profile_recharge_order_expiration_schedule_and_return;
pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return;
@@ -1838,6 +1847,12 @@ pub use editor_asset_folder_snapshot_type::EditorAssetFolderSnapshot;
pub use editor_asset_folder_table::*;
pub use editor_asset_folder_type::EditorAssetFolder;
pub use editor_asset_folder_update_input_type::EditorAssetFolderUpdateInput;
pub use editor_asset_group_cohort_complete_input_type::EditorAssetGroupCohortCompleteInput;
pub use editor_asset_group_cohort_table::*;
pub use editor_asset_group_cohort_type::EditorAssetGroupCohort;
pub use editor_asset_group_source_lookup_input_type::EditorAssetGroupSourceLookupInput;
pub use editor_asset_group_source_provenance_table::*;
pub use editor_asset_group_source_provenance_type::EditorAssetGroupSourceProvenance;
pub use editor_asset_library_get_input_type::EditorAssetLibraryGetInput;
pub use editor_asset_library_procedure_result_type::EditorAssetLibraryProcedureResult;
pub use editor_asset_library_snapshot_type::EditorAssetLibrarySnapshot;
@@ -1957,6 +1972,7 @@ pub use finalize_match_3_d_agent_message_turn_procedure::finalize_match_3_d_agen
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
pub use finalize_square_hole_agent_message_turn_procedure::finalize_square_hole_agent_message_turn;
pub use finalize_visual_novel_agent_message_turn_procedure::finalize_visual_novel_agent_message_turn;
pub use find_editor_asset_group_source_and_return_procedure::find_editor_asset_group_source_and_return;
pub use finish_bark_battle_run_procedure::finish_bark_battle_run;
pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up;
pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up;
@@ -3178,6 +3194,8 @@ pub struct DbUpdate {
editor_agent_conversation: __sdk::TableUpdate<EditorAgentConversation>,
editor_asset: __sdk::TableUpdate<EditorAsset>,
editor_asset_folder: __sdk::TableUpdate<EditorAssetFolder>,
editor_asset_group_cohort: __sdk::TableUpdate<EditorAssetGroupCohort>,
editor_asset_group_source_provenance: __sdk::TableUpdate<EditorAssetGroupSourceProvenance>,
editor_canvas: __sdk::TableUpdate<EditorCanvas>,
editor_generation_pricing_config: __sdk::TableUpdate<EditorGenerationPricingConfig>,
editor_generation_runtime_identity_rotation:
@@ -3434,6 +3452,16 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"editor_asset_folder" => db_update
.editor_asset_folder
.append(editor_asset_folder_table::parse_table_update(table_update)?),
"editor_asset_group_cohort" => db_update.editor_asset_group_cohort.append(
editor_asset_group_cohort_table::parse_table_update(table_update)?,
),
"editor_asset_group_source_provenance" => {
db_update.editor_asset_group_source_provenance.append(
editor_asset_group_source_provenance_table::parse_table_update(
table_update,
)?,
)
}
"editor_canvas" => db_update
.editor_canvas
.append(editor_canvas_table::parse_table_update(table_update)?),
@@ -4019,6 +4047,18 @@ impl __sdk::DbUpdate for DbUpdate {
&self.editor_asset_folder,
)
.with_updates_by_pk(|row| &row.folder_id);
diff.editor_asset_group_cohort = cache
.apply_diff_to_table::<EditorAssetGroupCohort>(
"editor_asset_group_cohort",
&self.editor_asset_group_cohort,
)
.with_updates_by_pk(|row| &row.cohort_id);
diff.editor_asset_group_source_provenance = cache
.apply_diff_to_table::<EditorAssetGroupSourceProvenance>(
"editor_asset_group_source_provenance",
&self.editor_asset_group_source_provenance,
)
.with_updates_by_pk(|row| &row.lookup_key);
diff.editor_canvas = cache
.apply_diff_to_table::<EditorCanvas>("editor_canvas", &self.editor_canvas)
.with_updates_by_pk(|row| &row.canvas_id);
@@ -4699,6 +4739,12 @@ impl __sdk::DbUpdate for DbUpdate {
"editor_asset_folder" => db_update
.editor_asset_folder
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"editor_asset_group_cohort" => db_update
.editor_asset_group_cohort
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"editor_asset_group_source_provenance" => db_update
.editor_asset_group_source_provenance
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"editor_canvas" => db_update
.editor_canvas
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@@ -5153,6 +5199,12 @@ impl __sdk::DbUpdate for DbUpdate {
"editor_asset_folder" => db_update
.editor_asset_folder
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"editor_asset_group_cohort" => db_update
.editor_asset_group_cohort
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"editor_asset_group_source_provenance" => db_update
.editor_asset_group_source_provenance
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"editor_canvas" => db_update
.editor_canvas
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@@ -5525,6 +5577,9 @@ pub struct AppliedDiff<'r> {
editor_agent_conversation: __sdk::TableAppliedDiff<'r, EditorAgentConversation>,
editor_asset: __sdk::TableAppliedDiff<'r, EditorAsset>,
editor_asset_folder: __sdk::TableAppliedDiff<'r, EditorAssetFolder>,
editor_asset_group_cohort: __sdk::TableAppliedDiff<'r, EditorAssetGroupCohort>,
editor_asset_group_source_provenance:
__sdk::TableAppliedDiff<'r, EditorAssetGroupSourceProvenance>,
editor_canvas: __sdk::TableAppliedDiff<'r, EditorCanvas>,
editor_generation_pricing_config: __sdk::TableAppliedDiff<'r, EditorGenerationPricingConfig>,
editor_generation_runtime_identity_rotation:
@@ -5856,6 +5911,16 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.editor_asset_folder,
event,
);
callbacks.invoke_table_row_callbacks::<EditorAssetGroupCohort>(
"editor_asset_group_cohort",
&self.editor_asset_group_cohort,
event,
);
callbacks.invoke_table_row_callbacks::<EditorAssetGroupSourceProvenance>(
"editor_asset_group_source_provenance",
&self.editor_asset_group_source_provenance,
event,
);
callbacks.invoke_table_row_callbacks::<EditorCanvas>(
"editor_canvas",
&self.editor_canvas,
@@ -7067,6 +7132,8 @@ impl __sdk::SpacetimeModule for RemoteModule {
editor_agent_conversation_table::register_table(client_cache);
editor_asset_table::register_table(client_cache);
editor_asset_folder_table::register_table(client_cache);
editor_asset_group_cohort_table::register_table(client_cache);
editor_asset_group_source_provenance_table::register_table(client_cache);
editor_canvas_table::register_table(client_cache);
editor_generation_pricing_config_table::register_table(client_cache);
editor_generation_runtime_identity_rotation_table::register_table(client_cache);
@@ -7216,6 +7283,8 @@ impl __sdk::SpacetimeModule for RemoteModule {
"editor_agent_conversation",
"editor_asset",
"editor_asset_folder",
"editor_asset_group_cohort",
"editor_asset_group_source_provenance",
"editor_canvas",
"editor_generation_pricing_config",
"editor_generation_runtime_identity_rotation",
@@ -29,6 +29,7 @@ pub struct AdminEditorAssetSnapshot {
pub generation_cost_mud_points: u64,
pub created_at_micros: i64,
pub updated_at_micros: i64,
pub group_task_id: Option<String>,
}
impl __sdk::InModule for AdminEditorAssetSnapshot {
@@ -0,0 +1,62 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::editor_asset_group_cohort_complete_input_type::EditorAssetGroupCohortCompleteInput;
use super::editor_asset_procedure_result_type::EditorAssetProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct CompleteEditorAssetGroupCohortAndReturnArgs {
pub input: EditorAssetGroupCohortCompleteInput,
}
impl __sdk::InModule for CompleteEditorAssetGroupCohortAndReturnArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `complete_editor_asset_group_cohort_and_return`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait complete_editor_asset_group_cohort_and_return {
fn complete_editor_asset_group_cohort_and_return(
&self,
input: EditorAssetGroupCohortCompleteInput,
) {
self.complete_editor_asset_group_cohort_and_return_then(input, |_, _| {});
}
fn complete_editor_asset_group_cohort_and_return_then(
&self,
input: EditorAssetGroupCohortCompleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorAssetProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl complete_editor_asset_group_cohort_and_return for super::RemoteProcedures {
fn complete_editor_asset_group_cohort_and_return_then(
&self,
input: EditorAssetGroupCohortCompleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<EditorAssetProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, EditorAssetProcedureResult>(
"complete_editor_asset_group_cohort_and_return",
CompleteEditorAssetGroupCohortAndReturnArgs { input },
__callback,
);
}
}
@@ -28,6 +28,8 @@ pub struct EditorAssetCreateInput {
pub now_micros: i64,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub group_task_id: Option<String>,
pub group_task_expected_asset_count: Option<u32>,
}
impl __sdk::InModule for EditorAssetCreateInput {
@@ -0,0 +1,20 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorAssetGroupCohortCompleteInput {
pub owner_user_id: String,
pub task_id: String,
pub group_task_id: String,
pub expected_asset_count: u32,
pub asset_ids: Vec<String>,
pub completed_at_micros: i64,
}
impl __sdk::InModule for EditorAssetGroupCohortCompleteInput {
type Module = super::RemoteModule;
}
@@ -0,0 +1,162 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::editor_asset_group_cohort_type::EditorAssetGroupCohort;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `editor_asset_group_cohort`.
///
/// Obtain a handle from the [`EditorAssetGroupCohortTableAccess::editor_asset_group_cohort`] method on [`super::RemoteTables`],
/// like `ctx.db.editor_asset_group_cohort()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_asset_group_cohort().on_insert(...)`.
pub struct EditorAssetGroupCohortTableHandle<'ctx> {
imp: __sdk::TableHandle<EditorAssetGroupCohort>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `editor_asset_group_cohort`.
///
/// Implemented for [`super::RemoteTables`].
pub trait EditorAssetGroupCohortTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`EditorAssetGroupCohortTableHandle`], which mediates access to the table `editor_asset_group_cohort`.
fn editor_asset_group_cohort(&self) -> EditorAssetGroupCohortTableHandle<'_>;
}
impl EditorAssetGroupCohortTableAccess for super::RemoteTables {
fn editor_asset_group_cohort(&self) -> EditorAssetGroupCohortTableHandle<'_> {
EditorAssetGroupCohortTableHandle {
imp: self
.imp
.get_table::<EditorAssetGroupCohort>("editor_asset_group_cohort"),
ctx: std::marker::PhantomData,
}
}
}
pub struct EditorAssetGroupCohortInsertCallbackId(__sdk::CallbackId);
pub struct EditorAssetGroupCohortDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for EditorAssetGroupCohortTableHandle<'ctx> {
type Row = EditorAssetGroupCohort;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = EditorAssetGroupCohort> + '_ {
self.imp.iter()
}
type InsertCallbackId = EditorAssetGroupCohortInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorAssetGroupCohortInsertCallbackId {
EditorAssetGroupCohortInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: EditorAssetGroupCohortInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = EditorAssetGroupCohortDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorAssetGroupCohortDeleteCallbackId {
EditorAssetGroupCohortDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: EditorAssetGroupCohortDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct EditorAssetGroupCohortUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for EditorAssetGroupCohortTableHandle<'ctx> {
type UpdateCallbackId = EditorAssetGroupCohortUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> EditorAssetGroupCohortUpdateCallbackId {
EditorAssetGroupCohortUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: EditorAssetGroupCohortUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `cohort_id` unique index on the table `editor_asset_group_cohort`,
/// which allows point queries on the field of the same name
/// via the [`EditorAssetGroupCohortCohortIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_asset_group_cohort().cohort_id().find(...)`.
pub struct EditorAssetGroupCohortCohortIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<EditorAssetGroupCohort, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> EditorAssetGroupCohortTableHandle<'ctx> {
/// Get a handle on the `cohort_id` unique index on the table `editor_asset_group_cohort`.
pub fn cohort_id(&self) -> EditorAssetGroupCohortCohortIdUnique<'ctx> {
EditorAssetGroupCohortCohortIdUnique {
imp: self.imp.get_unique_constraint::<String>("cohort_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> EditorAssetGroupCohortCohortIdUnique<'ctx> {
/// Find the subscribed row whose `cohort_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<EditorAssetGroupCohort> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<EditorAssetGroupCohort>("editor_asset_group_cohort");
_table.add_unique_constraint::<String>("cohort_id", |row| &row.cohort_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<EditorAssetGroupCohort>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<EditorAssetGroupCohort>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `EditorAssetGroupCohort`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait editor_asset_group_cohortQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `EditorAssetGroupCohort`.
fn editor_asset_group_cohort(&self) -> __sdk::__query_builder::Table<EditorAssetGroupCohort>;
}
impl editor_asset_group_cohortQueryTableAccess for __sdk::QueryTableAccessor {
fn editor_asset_group_cohort(&self) -> __sdk::__query_builder::Table<EditorAssetGroupCohort> {
__sdk::__query_builder::Table::new("editor_asset_group_cohort")
}
}
@@ -0,0 +1,67 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorAssetGroupCohort {
pub cohort_id: String,
pub owner_user_id: String,
pub task_id: String,
pub group_task_id: String,
pub expected_asset_count: u32,
pub completed_at: __sdk::Timestamp,
}
impl __sdk::InModule for EditorAssetGroupCohort {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `EditorAssetGroupCohort`.
///
/// Provides typed access to columns for query building.
pub struct EditorAssetGroupCohortCols {
pub cohort_id: __sdk::__query_builder::Col<EditorAssetGroupCohort, String>,
pub owner_user_id: __sdk::__query_builder::Col<EditorAssetGroupCohort, String>,
pub task_id: __sdk::__query_builder::Col<EditorAssetGroupCohort, String>,
pub group_task_id: __sdk::__query_builder::Col<EditorAssetGroupCohort, String>,
pub expected_asset_count: __sdk::__query_builder::Col<EditorAssetGroupCohort, u32>,
pub completed_at: __sdk::__query_builder::Col<EditorAssetGroupCohort, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for EditorAssetGroupCohort {
type Cols = EditorAssetGroupCohortCols;
fn cols(table_name: &'static str) -> Self::Cols {
EditorAssetGroupCohortCols {
cohort_id: __sdk::__query_builder::Col::new(table_name, "cohort_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
task_id: __sdk::__query_builder::Col::new(table_name, "task_id"),
group_task_id: __sdk::__query_builder::Col::new(table_name, "group_task_id"),
expected_asset_count: __sdk::__query_builder::Col::new(
table_name,
"expected_asset_count",
),
completed_at: __sdk::__query_builder::Col::new(table_name, "completed_at"),
}
}
}
/// Indexed column accessor struct for the table `EditorAssetGroupCohort`.
///
/// Provides typed access to indexed columns for query building.
pub struct EditorAssetGroupCohortIxCols {
pub cohort_id: __sdk::__query_builder::IxCol<EditorAssetGroupCohort, String>,
}
impl __sdk::__query_builder::HasIxCols for EditorAssetGroupCohort {
type IxCols = EditorAssetGroupCohortIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
EditorAssetGroupCohortIxCols {
cohort_id: __sdk::__query_builder::IxCol::new(table_name, "cohort_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for EditorAssetGroupCohort {}
@@ -0,0 +1,18 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorAssetGroupSourceLookupInput {
pub owner_user_id: String,
pub source_resource_ids: Vec<String>,
pub asset_object_id: Option<String>,
pub object_key: Option<String>,
}
impl __sdk::InModule for EditorAssetGroupSourceLookupInput {
type Module = super::RemoteModule;
}
@@ -0,0 +1,174 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::editor_asset_group_source_provenance_type::EditorAssetGroupSourceProvenance;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `editor_asset_group_source_provenance`.
///
/// Obtain a handle from the [`EditorAssetGroupSourceProvenanceTableAccess::editor_asset_group_source_provenance`] method on [`super::RemoteTables`],
/// like `ctx.db.editor_asset_group_source_provenance()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_asset_group_source_provenance().on_insert(...)`.
pub struct EditorAssetGroupSourceProvenanceTableHandle<'ctx> {
imp: __sdk::TableHandle<EditorAssetGroupSourceProvenance>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `editor_asset_group_source_provenance`.
///
/// Implemented for [`super::RemoteTables`].
pub trait EditorAssetGroupSourceProvenanceTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`EditorAssetGroupSourceProvenanceTableHandle`], which mediates access to the table `editor_asset_group_source_provenance`.
fn editor_asset_group_source_provenance(
&self,
) -> EditorAssetGroupSourceProvenanceTableHandle<'_>;
}
impl EditorAssetGroupSourceProvenanceTableAccess for super::RemoteTables {
fn editor_asset_group_source_provenance(
&self,
) -> EditorAssetGroupSourceProvenanceTableHandle<'_> {
EditorAssetGroupSourceProvenanceTableHandle {
imp: self.imp.get_table::<EditorAssetGroupSourceProvenance>(
"editor_asset_group_source_provenance",
),
ctx: std::marker::PhantomData,
}
}
}
pub struct EditorAssetGroupSourceProvenanceInsertCallbackId(__sdk::CallbackId);
pub struct EditorAssetGroupSourceProvenanceDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for EditorAssetGroupSourceProvenanceTableHandle<'ctx> {
type Row = EditorAssetGroupSourceProvenance;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = EditorAssetGroupSourceProvenance> + '_ {
self.imp.iter()
}
type InsertCallbackId = EditorAssetGroupSourceProvenanceInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorAssetGroupSourceProvenanceInsertCallbackId {
EditorAssetGroupSourceProvenanceInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: EditorAssetGroupSourceProvenanceInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = EditorAssetGroupSourceProvenanceDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> EditorAssetGroupSourceProvenanceDeleteCallbackId {
EditorAssetGroupSourceProvenanceDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: EditorAssetGroupSourceProvenanceDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct EditorAssetGroupSourceProvenanceUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for EditorAssetGroupSourceProvenanceTableHandle<'ctx> {
type UpdateCallbackId = EditorAssetGroupSourceProvenanceUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> EditorAssetGroupSourceProvenanceUpdateCallbackId {
EditorAssetGroupSourceProvenanceUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: EditorAssetGroupSourceProvenanceUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `lookup_key` unique index on the table `editor_asset_group_source_provenance`,
/// which allows point queries on the field of the same name
/// via the [`EditorAssetGroupSourceProvenanceLookupKeyUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.editor_asset_group_source_provenance().lookup_key().find(...)`.
pub struct EditorAssetGroupSourceProvenanceLookupKeyUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<EditorAssetGroupSourceProvenance, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> EditorAssetGroupSourceProvenanceTableHandle<'ctx> {
/// Get a handle on the `lookup_key` unique index on the table `editor_asset_group_source_provenance`.
pub fn lookup_key(&self) -> EditorAssetGroupSourceProvenanceLookupKeyUnique<'ctx> {
EditorAssetGroupSourceProvenanceLookupKeyUnique {
imp: self.imp.get_unique_constraint::<String>("lookup_key"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> EditorAssetGroupSourceProvenanceLookupKeyUnique<'ctx> {
/// Find the subscribed row whose `lookup_key` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<EditorAssetGroupSourceProvenance> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<EditorAssetGroupSourceProvenance>(
"editor_asset_group_source_provenance",
);
_table.add_unique_constraint::<String>("lookup_key", |row| &row.lookup_key);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<EditorAssetGroupSourceProvenance>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<EditorAssetGroupSourceProvenance>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `EditorAssetGroupSourceProvenance`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait editor_asset_group_source_provenanceQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `EditorAssetGroupSourceProvenance`.
fn editor_asset_group_source_provenance(
&self,
) -> __sdk::__query_builder::Table<EditorAssetGroupSourceProvenance>;
}
impl editor_asset_group_source_provenanceQueryTableAccess for __sdk::QueryTableAccessor {
fn editor_asset_group_source_provenance(
&self,
) -> __sdk::__query_builder::Table<EditorAssetGroupSourceProvenance> {
__sdk::__query_builder::Table::new("editor_asset_group_source_provenance")
}
}
@@ -0,0 +1,64 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct EditorAssetGroupSourceProvenance {
pub lookup_key: String,
pub owner_user_id: String,
pub asset_id: String,
pub task_id: String,
pub source_resource_id: String,
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for EditorAssetGroupSourceProvenance {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `EditorAssetGroupSourceProvenance`.
///
/// Provides typed access to columns for query building.
pub struct EditorAssetGroupSourceProvenanceCols {
pub lookup_key: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, String>,
pub owner_user_id: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, String>,
pub asset_id: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, String>,
pub task_id: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, String>,
pub source_resource_id: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, String>,
pub created_at: __sdk::__query_builder::Col<EditorAssetGroupSourceProvenance, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for EditorAssetGroupSourceProvenance {
type Cols = EditorAssetGroupSourceProvenanceCols;
fn cols(table_name: &'static str) -> Self::Cols {
EditorAssetGroupSourceProvenanceCols {
lookup_key: __sdk::__query_builder::Col::new(table_name, "lookup_key"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
asset_id: __sdk::__query_builder::Col::new(table_name, "asset_id"),
task_id: __sdk::__query_builder::Col::new(table_name, "task_id"),
source_resource_id: __sdk::__query_builder::Col::new(table_name, "source_resource_id"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
/// Indexed column accessor struct for the table `EditorAssetGroupSourceProvenance`.
///
/// Provides typed access to indexed columns for query building.
pub struct EditorAssetGroupSourceProvenanceIxCols {
pub lookup_key: __sdk::__query_builder::IxCol<EditorAssetGroupSourceProvenance, String>,
}
impl __sdk::__query_builder::HasIxCols for EditorAssetGroupSourceProvenance {
type IxCols = EditorAssetGroupSourceProvenanceIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
EditorAssetGroupSourceProvenanceIxCols {
lookup_key: __sdk::__query_builder::IxCol::new(table_name, "lookup_key"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for EditorAssetGroupSourceProvenance {}
@@ -33,6 +33,7 @@ pub struct EditorAssetSnapshot {
pub showcase_review_status: Option<String>,
pub showcase_display_enabled: Option<bool>,
pub showcase_like_count: Option<u64>,
pub group_task_id: Option<String>,
}
impl __sdk::InModule for EditorAssetSnapshot {

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