新增的批次接口把一个 UUID 正则写成了 8-4-4-12(少一组 -{4}),标准 UUID 是
8-4-4-4-12。结果是任何真实项目编号和请求编号都过不了校验:查询接口恒返 400
「项目编号无效」,提交接口恒返 400「任务编号或项目编号无效」,详情替换完全无法使用。
- 校验抽成 isDetailReplacementUuid 并修正为规范的 8-4-4-4-12,路由不再自带正则
- 回归测试覆盖真实线上项目编号、浏览器生成的请求编号,以及导致故障的 8-4-4-12 形状
- 增加源码守卫,防止该路由再次出现自带的错误 UUID 正则
- 前端在项目尚未保存时给出明确提示,而不是把空编号丢给接口
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
const DETAIL_REPLACEMENT_UUID_PATTERN =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
/**
|
|
* Every id this feature exchanges - project, chat, task and request ids - is a
|
|
* canonical 8-4-4-4-12 UUID. Keep the check in one place so the API route and
|
|
* its tests can never drift apart again.
|
|
*/
|
|
export function isDetailReplacementUuid(value: unknown): value is string {
|
|
return typeof value === "string" && DETAIL_REPLACEMENT_UUID_PATTERN.test(value.trim());
|
|
}
|
|
|
|
export type DetailReplacementBatchReference = {
|
|
id: string;
|
|
label: string;
|
|
imageUrl: string;
|
|
candidateId?: string | null;
|
|
passthrough?: boolean;
|
|
rerunReferenceImages?: Array<{ url: string }>;
|
|
};
|
|
|
|
export type DetailReplacementBatchRequest = {
|
|
taskKind: "agent-detail-replacement-batch";
|
|
projectId: string;
|
|
chatId: string;
|
|
submissionKey?: string;
|
|
model: string;
|
|
prompt: string;
|
|
userPrompt: string;
|
|
imageSize: string;
|
|
replacementStrategy: "product_only" | "scene_replace";
|
|
productImageUrls: string[];
|
|
modelImageUrl: string | null;
|
|
references: DetailReplacementBatchReference[];
|
|
};
|
|
|
|
export type DetailReplacementBatchCard = {
|
|
cardId: string;
|
|
status: "completed" | "fallback" | "passthrough";
|
|
imageUrl: string;
|
|
thumbnailUrl: string;
|
|
text: string;
|
|
error?: string;
|
|
};
|
|
|
|
export type DetailReplacementBatchResult = {
|
|
taskKind: "agent-detail-replacement-batch";
|
|
cards: DetailReplacementBatchCard[];
|
|
currentCardId: string | null;
|
|
};
|
|
|
|
export function isDetailReplacementBatchRequest(
|
|
value: unknown,
|
|
): value is DetailReplacementBatchRequest {
|
|
return Boolean(
|
|
value &&
|
|
typeof value === "object" &&
|
|
!Array.isArray(value) &&
|
|
(value as { taskKind?: unknown }).taskKind === "agent-detail-replacement-batch",
|
|
);
|
|
}
|
|
|
|
export function completedDetailReplacementCardIds(
|
|
result: DetailReplacementBatchResult | null | undefined,
|
|
) {
|
|
return new Set(
|
|
(result?.cards || [])
|
|
.filter((card) => card.status === "completed" || card.status === "passthrough")
|
|
.map((card) => card.cardId),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Identity of one user submission. Two clicks that ask for exactly the same
|
|
* work produce the same key, so the second one is answered with the running
|
|
* batch instead of submitting (and billing) the same images twice. A different
|
|
* card selection - for example re-running a single finished card while a batch
|
|
* is still going - produces a different key and is allowed to start.
|
|
*/
|
|
export function detailReplacementSubmissionKey(input: {
|
|
projectId: string;
|
|
model: string;
|
|
prompt: string;
|
|
userPrompt: string;
|
|
imageSize: string;
|
|
replacementStrategy: string;
|
|
productImageUrls: string[];
|
|
modelImageUrl: string | null;
|
|
references: DetailReplacementBatchReference[];
|
|
}) {
|
|
const payload = JSON.stringify({
|
|
projectId: input.projectId,
|
|
model: input.model,
|
|
prompt: input.prompt,
|
|
userPrompt: input.userPrompt,
|
|
imageSize: input.imageSize,
|
|
strategy: input.replacementStrategy,
|
|
products: [...input.productImageUrls].sort(),
|
|
modelImage: input.modelImageUrl || "",
|
|
cards: [...input.references]
|
|
.map((item) => [
|
|
item.id,
|
|
item.imageUrl,
|
|
item.passthrough ? 1 : 0,
|
|
(item.rerunReferenceImages || []).map((entry) => entry.url).join(","),
|
|
])
|
|
.sort((left, right) => String(left[0]).localeCompare(String(right[0]))),
|
|
});
|
|
return createHash("sha256").update(payload).digest("hex");
|
|
}
|
|
|
|
export function detailReplacementCardMessageId(taskId: string, cardId: string) {
|
|
const hex = createHash("sha256").update(`${taskId}:${cardId}`).digest("hex").slice(0, 32);
|
|
return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20, 32)].join("-");
|
|
}
|