fix(*): 持续集成 - 修复锁文件、格式门禁与生成链路构建失败
This commit is contained in:
@@ -49,7 +49,10 @@ export type StyleProfileSnapshot = {
|
||||
revision: number;
|
||||
};
|
||||
|
||||
export type StyleProfileSource = Pick<StyleProfileSnapshot, "presetId" | "title" | "description" | "tags" | "prompt"> & {
|
||||
export type StyleProfileSource = Pick<StyleProfileSnapshot, "title" | "description" | "tags" | "prompt"> & {
|
||||
// 内置画风使用 id,持久化配置使用 presetId,两种来源在此统一归一化。
|
||||
presetId?: string;
|
||||
id?: string;
|
||||
negativePrompt?: string;
|
||||
coverUrl?: string;
|
||||
sourceProfileId?: string;
|
||||
@@ -93,7 +96,7 @@ export type StyleExecutionPlan = {
|
||||
export function createStyleProfileSnapshot(source: StyleProfileSource): StyleProfileSnapshot {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
presetId: source.presetId.trim(),
|
||||
presetId: (source.presetId || source.id || "").trim(),
|
||||
title: source.title.trim(),
|
||||
description: source.description.trim(),
|
||||
tags: source.tags.map((tag) => tag.trim()).filter(Boolean),
|
||||
@@ -102,15 +105,17 @@ export function createStyleProfileSnapshot(source: StyleProfileSource): StylePro
|
||||
coverUrl: source.coverUrl?.trim() || undefined,
|
||||
sourceProfileId: source.sourceProfileId?.trim() || undefined,
|
||||
selection: source.selection ? { ...source.selection } : undefined,
|
||||
assets: source.assets ? source.assets.map((asset) => ({
|
||||
...asset,
|
||||
parameters: asset.parameters ? { ...asset.parameters } : undefined,
|
||||
license: asset.license ? { ...asset.license } : undefined,
|
||||
baseModels: asset.baseModels ? [...asset.baseModels] : undefined,
|
||||
triggerWords: asset.triggerWords ? [...asset.triggerWords] : undefined,
|
||||
referenceUrls: asset.referenceUrls ? [...asset.referenceUrls] : undefined,
|
||||
referenceResourceIds: asset.referenceResourceIds ? [...asset.referenceResourceIds] : undefined,
|
||||
})) : [],
|
||||
assets: source.assets
|
||||
? source.assets.map((asset) => ({
|
||||
...asset,
|
||||
parameters: asset.parameters ? { ...asset.parameters } : undefined,
|
||||
license: asset.license ? { ...asset.license } : undefined,
|
||||
baseModels: asset.baseModels ? [...asset.baseModels] : undefined,
|
||||
triggerWords: asset.triggerWords ? [...asset.triggerWords] : undefined,
|
||||
referenceUrls: asset.referenceUrls ? [...asset.referenceUrls] : undefined,
|
||||
referenceResourceIds: asset.referenceResourceIds ? [...asset.referenceResourceIds] : undefined,
|
||||
}))
|
||||
: [],
|
||||
executionPolicy: source.executionPolicy || "compatible-fallback",
|
||||
source: source.source || "builtin",
|
||||
revision: source.revision || 1,
|
||||
@@ -131,7 +136,16 @@ export function parseStyleProfile(value?: string | null): StyleProfileSnapshot |
|
||||
if (!raw || new TextEncoder().encode(raw).length > MAX_PROFILE_BYTES) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<StyleProfileSnapshot>;
|
||||
if (parsed.schemaVersion !== 1 || typeof parsed.presetId !== "string" || typeof parsed.title !== "string" || typeof parsed.description !== "string" || typeof parsed.prompt !== "string" || !Array.isArray(parsed.tags) || !Array.isArray(parsed.assets)) return null;
|
||||
if (
|
||||
parsed.schemaVersion !== 1 ||
|
||||
typeof parsed.presetId !== "string" ||
|
||||
typeof parsed.title !== "string" ||
|
||||
typeof parsed.description !== "string" ||
|
||||
typeof parsed.prompt !== "string" ||
|
||||
!Array.isArray(parsed.tags) ||
|
||||
!Array.isArray(parsed.assets)
|
||||
)
|
||||
return null;
|
||||
if (!parsed.tags.every((tag) => typeof tag === "string")) return null;
|
||||
if ([parsed.negativePrompt, parsed.coverUrl, parsed.sourceProfileId].some((value) => value !== undefined && typeof value !== "string")) return null;
|
||||
if (parsed.selection !== undefined && (!parsed.selection || typeof parsed.selection !== "object" || Array.isArray(parsed.selection) || !Object.values(parsed.selection).every((value) => typeof value === "string"))) return null;
|
||||
@@ -243,18 +257,34 @@ function resolveAssetExecution(asset: StyleAssetBinding, context: StyleExecution
|
||||
function isStyleAssetBinding(value: unknown): value is StyleAssetBinding {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const asset = value as Partial<StyleAssetBinding>;
|
||||
if (typeof asset.id !== "string" || typeof asset.kind !== "string" || !["lora", "template", "reference", "prompt"].includes(asset.kind) || typeof asset.provider !== "string" || typeof asset.title !== "string" || !["draft", "validated", "unavailable"].includes(String(asset.status))) return false;
|
||||
if (
|
||||
typeof asset.id !== "string" ||
|
||||
typeof asset.kind !== "string" ||
|
||||
!["lora", "template", "reference", "prompt"].includes(asset.kind) ||
|
||||
typeof asset.provider !== "string" ||
|
||||
typeof asset.title !== "string" ||
|
||||
!["draft", "validated", "unavailable"].includes(String(asset.status))
|
||||
)
|
||||
return false;
|
||||
if (asset.enabled !== undefined && typeof asset.enabled !== "boolean") return false;
|
||||
if (asset.weight !== undefined && (!Number.isFinite(asset.weight) || asset.weight < 0 || asset.weight > 2)) return false;
|
||||
if (![asset.sourceId, asset.sourceUrl, asset.model, asset.version, asset.promptFragment].every(isOptionalString)) return false;
|
||||
if (![asset.baseModels, asset.triggerWords, asset.referenceUrls, asset.referenceResourceIds].every((values) => values === undefined || (Array.isArray(values) && values.every((item) => typeof item === "string")))) return false;
|
||||
if (asset.parameters !== undefined && (!asset.parameters || typeof asset.parameters !== "object"
|
||||
|| !isOptionalString(asset.parameters.sampler) || !isOptionalString(asset.parameters.size)
|
||||
|| (asset.parameters.steps !== undefined && !Number.isInteger(asset.parameters.steps))
|
||||
|| (asset.parameters.cfg !== undefined && !Number.isFinite(asset.parameters.cfg)))) return false;
|
||||
if (asset.license !== undefined && (!asset.license || typeof asset.license !== "object"
|
||||
|| (asset.license.commercial !== undefined && typeof asset.license.commercial !== "boolean")
|
||||
|| ![asset.license.note, asset.license.source, asset.license.capturedAt].every(isOptionalString))) return false;
|
||||
if (
|
||||
asset.parameters !== undefined &&
|
||||
(!asset.parameters ||
|
||||
typeof asset.parameters !== "object" ||
|
||||
!isOptionalString(asset.parameters.sampler) ||
|
||||
!isOptionalString(asset.parameters.size) ||
|
||||
(asset.parameters.steps !== undefined && !Number.isInteger(asset.parameters.steps)) ||
|
||||
(asset.parameters.cfg !== undefined && !Number.isFinite(asset.parameters.cfg)))
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
asset.license !== undefined &&
|
||||
(!asset.license || typeof asset.license !== "object" || (asset.license.commercial !== undefined && typeof asset.license.commercial !== "boolean") || ![asset.license.note, asset.license.source, asset.license.capturedAt].every(isOptionalString))
|
||||
)
|
||||
return false;
|
||||
return !styleAssetValidationMessage(asset as StyleAssetBinding);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,58 +20,51 @@ type UseCanvasStyleWorkflowOptions = {
|
||||
setStylePickerOpen: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export function useCanvasStyleWorkflow({
|
||||
domainProjectId,
|
||||
nodesRef,
|
||||
selectedNodeIdsRef,
|
||||
getCanvasCenter,
|
||||
setNodes,
|
||||
setSelectedNodeIds,
|
||||
setSelectedConnectionId,
|
||||
setDialogNodeId,
|
||||
setStylePickerOpen,
|
||||
}: UseCanvasStyleWorkflowOptions) {
|
||||
export function useCanvasStyleWorkflow({ domainProjectId, nodesRef, selectedNodeIdsRef, getCanvasCenter, setNodes, setSelectedNodeIds, setSelectedConnectionId, setDialogNodeId, setStylePickerOpen }: UseCanvasStyleWorkflowOptions) {
|
||||
const { message } = App.useApp();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const applyCanvasStyle = useCallback((preset: CanvasStylePreset, profileJson: string) => {
|
||||
const current = nodesRef.current.find((node) => node.type === CanvasNodeType.Text && node.metadata?.workflowKind === "styleboard");
|
||||
const metadata: CanvasNodeMetadata = {
|
||||
content: preset.prompt,
|
||||
prompt: preset.prompt,
|
||||
status: "success",
|
||||
workflowKind: "styleboard",
|
||||
workflowTitle: "项目画风",
|
||||
workflowDescription: preset.description,
|
||||
stylePresetId: preset.id,
|
||||
styleProfileJson: profileJson,
|
||||
fontSize: 14,
|
||||
};
|
||||
let styleNode: CanvasNodeData;
|
||||
if (current) {
|
||||
styleNode = { ...current, title: `画风 · ${preset.title}`, metadata: { ...current.metadata, ...metadata } };
|
||||
nodesRef.current = nodesRef.current.map((node) => node.id === current.id ? styleNode : node);
|
||||
} else {
|
||||
styleNode = createCanvasNode(CanvasNodeType.Text, getCanvasCenter(), metadata);
|
||||
styleNode.title = `画风 · ${preset.title}`;
|
||||
styleNode.width = 420;
|
||||
styleNode.height = 240;
|
||||
nodesRef.current = [...nodesRef.current, styleNode];
|
||||
}
|
||||
setNodes(nodesRef.current);
|
||||
const selection = new Set([styleNode.id]);
|
||||
selectedNodeIdsRef.current = selection;
|
||||
setSelectedNodeIds(selection);
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
setStylePickerOpen(false);
|
||||
message.success(`已应用“${preset.title}”画风`);
|
||||
}, [getCanvasCenter, message, nodesRef, selectedNodeIdsRef, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, setStylePickerOpen]);
|
||||
const applyCanvasStyle = useCallback(
|
||||
(preset: CanvasStylePreset, profileJson: string) => {
|
||||
const current = nodesRef.current.find((node) => node.type === CanvasNodeType.Text && node.metadata?.workflowKind === "styleboard");
|
||||
const metadata: CanvasNodeMetadata = {
|
||||
content: preset.prompt,
|
||||
prompt: preset.prompt,
|
||||
status: "success",
|
||||
workflowKind: "styleboard",
|
||||
workflowTitle: "项目画风",
|
||||
workflowDescription: preset.description,
|
||||
stylePresetId: preset.id,
|
||||
styleProfileJson: profileJson,
|
||||
fontSize: 14,
|
||||
};
|
||||
let styleNode: CanvasNodeData;
|
||||
if (current) {
|
||||
styleNode = { ...current, title: `画风 · ${preset.title}`, metadata: { ...current.metadata, ...metadata } };
|
||||
nodesRef.current = nodesRef.current.map((node) => (node.id === current.id ? styleNode : node));
|
||||
} else {
|
||||
styleNode = createCanvasNode(CanvasNodeType.Text, getCanvasCenter(), metadata);
|
||||
styleNode.title = `画风 · ${preset.title}`;
|
||||
styleNode.width = 420;
|
||||
styleNode.height = 240;
|
||||
nodesRef.current = [...nodesRef.current, styleNode];
|
||||
}
|
||||
setNodes(nodesRef.current);
|
||||
const selection = new Set([styleNode.id]);
|
||||
selectedNodeIdsRef.current = selection;
|
||||
setSelectedNodeIds(selection);
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(null);
|
||||
setStylePickerOpen(false);
|
||||
message.success(`已应用“${preset.title}”画风`);
|
||||
},
|
||||
[getCanvasCenter, message, nodesRef, selectedNodeIdsRef, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, setStylePickerOpen],
|
||||
);
|
||||
|
||||
const persistStyleMutation = useMutation({
|
||||
mutationFn: ({ preset, profileJson }: { preset: CanvasStylePreset; profileJson: string }) => {
|
||||
if (!domainProjectId) throw new Error("画布尚未关联项目");
|
||||
return updateDomainProject(domainProjectId, { stylePresetId: preset.id, styleProfileJson });
|
||||
return updateDomainProject(domainProjectId, { stylePresetId: preset.id, styleProfileJson: profileJson });
|
||||
},
|
||||
onSuccess: (_project, { preset, profileJson }) => {
|
||||
applyCanvasStyle(preset, profileJson);
|
||||
@@ -80,15 +73,18 @@ export function useCanvasStyleWorkflow({
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "项目画风保存失败"),
|
||||
});
|
||||
|
||||
const selectCanvasStyle = useCallback((preset: CanvasStylePreset) => {
|
||||
if (persistStyleMutation.isPending) return;
|
||||
const profileJson = serializeStyleProfile(preset.profile || createStyleProfileSnapshot(preset));
|
||||
if (!domainProjectId) {
|
||||
applyCanvasStyle(preset, profileJson);
|
||||
return;
|
||||
}
|
||||
persistStyleMutation.mutate({ preset, profileJson });
|
||||
}, [applyCanvasStyle, domainProjectId, persistStyleMutation]);
|
||||
const selectCanvasStyle = useCallback(
|
||||
(preset: CanvasStylePreset) => {
|
||||
if (persistStyleMutation.isPending) return;
|
||||
const profileJson = serializeStyleProfile(preset.profile || createStyleProfileSnapshot(preset));
|
||||
if (!domainProjectId) {
|
||||
applyCanvasStyle(preset, profileJson);
|
||||
return;
|
||||
}
|
||||
persistStyleMutation.mutate({ preset, profileJson });
|
||||
},
|
||||
[applyCanvasStyle, domainProjectId, persistStyleMutation],
|
||||
);
|
||||
|
||||
return { selectCanvasStyle, styleApplying: persistStyleMutation.isPending };
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ export type ResponseToolCall = {
|
||||
thoughtSignature?: string;
|
||||
};
|
||||
|
||||
export type ResponseInputMessage =
|
||||
| AiTextMessage
|
||||
| { type: "function_call"; call_id: string; name: string; arguments: string; thoughtSignature?: string }
|
||||
| { role: "tool"; tool_call_id: string; content: string };
|
||||
export type ResponseInputMessage = AiTextMessage | { type: "function_call"; call_id: string; name: string; arguments: string; thoughtSignature?: string } | { role: "tool"; tool_call_id: string; content: string };
|
||||
|
||||
export type ResponseFunctionTool = {
|
||||
type: "function";
|
||||
@@ -46,10 +43,7 @@ export type ToolResponseResult = {
|
||||
type ToolChoice = "auto" | "required" | { type: "function"; name: string };
|
||||
type ResponseMessageContent = AiTextMessage["content"] | string;
|
||||
type ResponseInputContent = { type: "input_text"; text: string } | { type: "input_image"; image_url: string };
|
||||
type ResponseInputItem =
|
||||
| { role: "system" | "user" | "assistant"; content: string | ResponseInputContent[] }
|
||||
| { type: "function_call"; call_id: string; name: string; arguments: string }
|
||||
| { type: "function_call_output"; call_id: string; output: string };
|
||||
type ResponseInputItem = { role: "system" | "user" | "assistant"; content: string | ResponseInputContent[] } | { type: "function_call"; call_id: string; name: string; arguments: string } | { type: "function_call_output"; call_id: string; output: string };
|
||||
type ResponseApiToolDefinition = {
|
||||
type: "function";
|
||||
name: string;
|
||||
@@ -57,9 +51,7 @@ type ResponseApiToolDefinition = {
|
||||
parameters: Record<string, unknown>;
|
||||
strict?: boolean;
|
||||
};
|
||||
type ResponseApiOutputItem =
|
||||
| { type?: "message"; content?: Array<{ type?: string; text?: string }> }
|
||||
| { type?: "function_call"; id?: string; call_id?: string; name?: string; arguments?: string };
|
||||
type ResponseApiOutputItem = { type?: "message"; content?: Array<{ type?: string; text?: string }> } | { type?: "function_call"; id?: string; call_id?: string; name?: string; arguments?: string };
|
||||
type ResponseApiPayload = {
|
||||
id?: string;
|
||||
output?: ResponseApiOutputItem[];
|
||||
@@ -211,7 +203,8 @@ function dimensionGCD(left: number, right: number) {
|
||||
function resolveImageRequestSize(profile: ImageCapabilityConfig, quality: string | undefined, size: string) {
|
||||
const request = imageSizeRequest(profile, size);
|
||||
if (!request) return undefined;
|
||||
return { parameter: request.parameter, value: request.parameter === "size" ? resolveRequestSize(quality, request.value) : resolveAspectRatio(request.value) };
|
||||
const value = request.parameter === "size" ? resolveRequestSize(quality, request.value) : resolveAspectRatio(request.value);
|
||||
return value ? { parameter: request.parameter, value } : undefined;
|
||||
}
|
||||
|
||||
function validateImageCapability(profile: ImageCapabilityConfig, prompt: string, references: ReferenceImage[], mask?: ReferenceImage) {
|
||||
@@ -620,12 +613,7 @@ async function requestStreamingChatCompletion(config: AiConfig, body: Record<str
|
||||
}
|
||||
|
||||
function toGeminiBody(config: AiConfig, messages: ResponseInputMessage[], extra?: Record<string, unknown>) {
|
||||
const systemText = [
|
||||
config.systemPrompt.trim(),
|
||||
...messages.flatMap((message) => (!("type" in message) && message.role === "system" ? [geminiTextContent(message.content)] : [])),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
const systemText = [config.systemPrompt.trim(), ...messages.flatMap((message) => (!("type" in message) && message.role === "system" ? [geminiTextContent(message.content)] : []))].filter(Boolean).join("\n\n");
|
||||
const contents = toGeminiContents(messages.filter((message) => ("type" in message ? true : message.role !== "system")));
|
||||
return {
|
||||
contents,
|
||||
@@ -685,10 +673,7 @@ function toGeminiToolOptions(tools: ResponseFunctionTool[], toolChoice: ToolChoi
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters,
|
||||
}));
|
||||
const functionCallingConfig =
|
||||
typeof toolChoice === "object"
|
||||
? { mode: "ANY", allowedFunctionNames: [toolChoice.name] }
|
||||
: { mode: toolChoice === "required" ? "ANY" : "AUTO" };
|
||||
const functionCallingConfig = typeof toolChoice === "object" ? { mode: "ANY", allowedFunctionNames: [toolChoice.name] } : { mode: toolChoice === "required" ? "ANY" : "AUTO" };
|
||||
return {
|
||||
tools: [{ functionDeclarations }],
|
||||
toolConfig: { functionCallingConfig },
|
||||
@@ -849,9 +834,7 @@ export async function requestGeneration(config: AiConfig, prompt: string, option
|
||||
...(imageProfile.outputFormat.supported ? { output_format: IMAGE_OUTPUT_FORMAT } : {}),
|
||||
...(imageProfile.transparentBackground.supported && normalizedImage.transparentBackground === "true" ? { background: "transparent" } : {}),
|
||||
};
|
||||
const responseData = isVolcengineArk
|
||||
? await postVolcengineArkImage(requestConfig, payload, options)
|
||||
: await postChannelJSON<ImageApiResponse>(requestConfig, aiApiUrl(requestConfig, "/images/generations"), payload, options);
|
||||
const responseData = isVolcengineArk ? await postVolcengineArkImage(requestConfig, payload, options) : await postChannelJSON<ImageApiResponse>(requestConfig, aiApiUrl(requestConfig, "/images/generations"), payload, options);
|
||||
const images = parseImagePayload(responseData);
|
||||
return images;
|
||||
} catch (error) {
|
||||
@@ -949,17 +932,33 @@ export async function requestImageQuestion(config: AiConfig, messages: AiTextMes
|
||||
return answer;
|
||||
}
|
||||
if (requestConfig.interfaceType === "chat-completion") {
|
||||
const answer = (await requestStreamingChatCompletion(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
messages: toChatCompletionMessages(withSystemMessage(requestConfig, messages)),
|
||||
}, onDelta, options)).content || "没有返回内容";
|
||||
const answer =
|
||||
(
|
||||
await requestStreamingChatCompletion(
|
||||
requestConfig,
|
||||
{
|
||||
model: requestConfig.model,
|
||||
messages: toChatCompletionMessages(withSystemMessage(requestConfig, messages)),
|
||||
},
|
||||
onDelta,
|
||||
options,
|
||||
)
|
||||
).content || "没有返回内容";
|
||||
if (answer === "没有返回内容") onDelta(answer);
|
||||
return answer;
|
||||
}
|
||||
const answer = (await requestStreamingResponse(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
}, onDelta, options)).content || "没有返回内容";
|
||||
const answer =
|
||||
(
|
||||
await requestStreamingResponse(
|
||||
requestConfig,
|
||||
{
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
},
|
||||
onDelta,
|
||||
options,
|
||||
)
|
||||
).content || "没有返回内容";
|
||||
if (answer === "没有返回内容") onDelta(answer);
|
||||
return answer;
|
||||
} catch (error) {
|
||||
@@ -974,13 +973,18 @@ export async function requestToolResponse(config: AiConfig, messages: ResponseIn
|
||||
return await requestGeminiStreamingResponse(requestConfig, toGeminiBody(requestConfig, messages, toGeminiToolOptions(tools, toolChoice)), onDelta, options);
|
||||
}
|
||||
if (requestConfig.interfaceType === "chat-completion") {
|
||||
return await requestStreamingChatCompletion(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
messages: toChatCompletionMessages(withSystemMessage(requestConfig, messages)),
|
||||
tools,
|
||||
tool_choice: toChatCompletionToolChoice(toolChoice),
|
||||
parallel_tool_calls: false,
|
||||
}, onDelta, options);
|
||||
return await requestStreamingChatCompletion(
|
||||
requestConfig,
|
||||
{
|
||||
model: requestConfig.model,
|
||||
messages: toChatCompletionMessages(withSystemMessage(requestConfig, messages)),
|
||||
tools,
|
||||
tool_choice: toChatCompletionToolChoice(toolChoice),
|
||||
parallel_tool_calls: false,
|
||||
},
|
||||
onDelta,
|
||||
options,
|
||||
);
|
||||
}
|
||||
return await requestStreamingResponse(
|
||||
requestConfig,
|
||||
|
||||
Reference in New Issue
Block a user