feat: harden deployment and public api handoff

This commit is contained in:
inman
2026-05-29 14:00:39 +08:00
parent 63e62d444c
commit 4b21d2999c
16 changed files with 961 additions and 19 deletions

View File

@@ -215,7 +215,8 @@ export async function syncImageJob(jobId: string, origin: string): Promise<Gener
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
index
index,
tags: assetTagsForJob(job)
}));
}
if (assets.length) {
@@ -300,7 +301,8 @@ async function syncEvolinkImageJob(job: GenerationJob, origin: string): Promise<
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
index
index,
tags: assetTagsForJob(job)
}));
}
await recordUsageEvent({
@@ -346,6 +348,7 @@ async function completeMockJob(job: GenerationJob, origin: string): Promise<Gene
source: sourceForCapability(job.capability),
capability: job.capability,
jobId: job.id,
tags: assetTagsForJob(job),
metadata: {
mock: true
}
@@ -376,6 +379,10 @@ function sourceForCapability(capability: string) {
return "generated";
}
function assetTagsForJob(job: GenerationJob): string[] {
return job.externalClientId ? [job.capability, `api-client:${job.externalClientId}`] : [job.capability];
}
function asRecord(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>

View File

@@ -0,0 +1,45 @@
import { getAsset, listAssets, listGenerationJobsFiltered } from "@/lib/server/data-store";
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
import type { Asset, GenerationJob } from "@/lib/types";
const JOB_LOOKUP_LIMIT = 200;
export async function listPublicApiAssets(clientId: string): Promise<Asset[]> {
const [assets, jobs] = await Promise.all([
listAssets(DEFAULT_OWNER_ID),
listGenerationJobsFiltered({
ownerId: DEFAULT_OWNER_ID,
externalClientId: clientId,
limit: JOB_LOOKUP_LIMIT
})
]);
const accessibleIds = assetIdsFromJobs(jobs);
return assets.filter((asset) => canAccessAsset(clientId, asset, accessibleIds));
}
export async function getPublicApiAsset(clientId: string, assetId: string): Promise<Asset | null> {
const asset = await getAsset(assetId);
if (!asset || asset.ownerId !== DEFAULT_OWNER_ID) return null;
if (asset.tags.includes(apiClientTag(clientId))) return asset;
const jobs = await listGenerationJobsFiltered({
ownerId: DEFAULT_OWNER_ID,
externalClientId: clientId,
limit: JOB_LOOKUP_LIMIT
});
return assetIdsFromJobs(jobs).has(asset.id) ? asset : null;
}
function canAccessAsset(clientId: string, asset: Asset, accessibleIds: Set<string>): boolean {
return asset.ownerId === DEFAULT_OWNER_ID && (
asset.tags.includes(apiClientTag(clientId)) ||
accessibleIds.has(asset.id)
);
}
function assetIdsFromJobs(jobs: GenerationJob[]): Set<string> {
return new Set(jobs.flatMap((job) => [...job.inputAssetIds, ...job.outputAssetIds]));
}
function apiClientTag(clientId: string): string {
return `api-client:${clientId}`;
}

View File

@@ -87,6 +87,7 @@ export async function importRemoteImageAsAsset(input: {
capability: string;
jobId: string;
index: number;
tags?: string[];
}): Promise<Asset> {
const response = await fetch(input.url);
if (!response.ok) {
@@ -104,6 +105,7 @@ export async function importRemoteImageAsAsset(input: {
source: input.source,
capability: input.capability,
jobId: input.jobId,
tags: input.tags,
metadata: {
importedFrom: input.url
}
@@ -119,6 +121,7 @@ export async function importRemoteAssetAsAsset(input: {
jobId: string;
index: number;
fallbackContentType?: string;
tags?: string[];
}): Promise<Asset> {
const response = await fetch(input.url);
if (!response.ok) {
@@ -136,6 +139,7 @@ export async function importRemoteAssetAsAsset(input: {
source: input.source,
capability: input.capability,
jobId: input.jobId,
tags: input.tags,
metadata: {
importedFrom: input.url
}

View File

@@ -131,7 +131,8 @@ export async function syncVideoJob(jobId: string, origin: string): Promise<Gener
capability: "video.generate",
jobId: job.id,
index: 0,
fallbackContentType: "video/mp4"
fallbackContentType: "video/mp4",
tags: assetTagsForJob(job)
});
await recordUsageEvent({
ownerId: job.ownerId,
@@ -171,7 +172,7 @@ async function completeMockVideoJob(job: GenerationJob): Promise<GenerationJob>
name: `mock-video-${job.id}.mp4`,
url: "/mock/seedance-mock.mp4",
source: "generated",
tags: ["video.generate", "mock"],
tags: [...assetTagsForJob(job), "mock"],
metadata: {
mock: true,
capability: "video.generate",
@@ -203,3 +204,7 @@ function asRecord(value: unknown): Record<string, unknown> {
? value as Record<string, unknown>
: {};
}
function assetTagsForJob(job: GenerationJob): string[] {
return job.externalClientId ? ["video.generate", `api-client:${job.externalClientId}`] : ["video.generate"];
}