feat(workflow): 工作流提供方 - 集成 RunningHub 与 ComfyUI Bridge
This commit is contained in:
31
web/src/services/api/comfy-bridge.ts
Normal file
31
web/src/services/api/comfy-bridge.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
|
||||
export type ComfyBridgeSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
online: boolean;
|
||||
lastSeenAt?: string;
|
||||
lastTaskAt?: string;
|
||||
capabilities?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ComfyBridgeRegistration = {
|
||||
bridge: ComfyBridgeSummary;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export function listComfyBridges() {
|
||||
return request<ComfyBridgeSummary[]>(apiClient.get("/comfy-bridges"));
|
||||
}
|
||||
|
||||
export function createComfyBridge(name: string, capabilities?: Record<string, unknown>) {
|
||||
return request<ComfyBridgeRegistration>(apiClient.post("/comfy-bridges", { name, capabilities }));
|
||||
}
|
||||
|
||||
export function revokeComfyBridge(id: string) {
|
||||
// 部分生产代理默认拦截 DELETE;后端保留 DELETE 兼容,但管理端统一走显式撤销动作。
|
||||
return request<{ revoked: boolean }>(apiClient.post(`/comfy-bridges/${encodeURIComponent(id)}/revoke`));
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { LOCAL_DREAMINA_WAIT_STOPPED_CODE, LocalDreaminaGenerationClientError, r
|
||||
import { isLocalDreaminaBackgroundTask, localDreaminaTaskId, projectLocalDreaminaTask, stripLocalDreaminaTaskPrefix } from "@/services/local-dreamina-task-projection";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { grokImagePromptLimitError } from "@/lib/grok-image-prompt-limit";
|
||||
import { resolveGenerationWorkflowExecution, type GenerationWorkflowExecution } from "@/lib/generation-workflow-execution";
|
||||
import { resolveVideoOperation } from "@/lib/model-selection";
|
||||
import { logicalModelIDForConfig, modelOptionName, resolveModelChannel, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { useLocalDreaminaModelStore } from "@/stores/use-local-dreamina-model-store";
|
||||
@@ -20,6 +21,7 @@ export { logicalModelIDForConfig };
|
||||
// 自建渠道则由所选协议插件负责第三方字段映射和结果解析。没有显式协议
|
||||
// 的旧配置继续保留前端直连兼容路径。
|
||||
export function backendModelRuntimeRequired(config: AiConfig) {
|
||||
if ((config.taskWorkflowProvider || "model") !== "model") return true;
|
||||
if (logicalModelIDForConfig(config)) return true;
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model);
|
||||
return Boolean(requestConfig.channelId || requestConfig.interfaceType);
|
||||
@@ -108,7 +110,7 @@ export async function runBackendGenerationTask(
|
||||
) {
|
||||
throwIfAborted(signal);
|
||||
assertClientPromptLimit(mode, prompt, config, metadata);
|
||||
if (isLocalDreaminaModel(config.model)) {
|
||||
if (usesLocalDreamina(config)) {
|
||||
await dependencies.ensureLocalDreaminaReady?.(signal);
|
||||
throwIfAborted(signal);
|
||||
return await runLocalDreaminaGeneration(
|
||||
@@ -161,7 +163,7 @@ export async function runBackendGenerationTaskBatch(options: BackendGenerationTa
|
||||
throwIfAborted(options.signal);
|
||||
assertClientPromptLimit(options.mode, options.prompt, options.config, options.metadata);
|
||||
if (options.retryContextsByBatchIndex && options.retryContextsByBatchIndex.length !== count) throw new Error("生成重试批次任务数量不匹配");
|
||||
if (isLocalDreaminaModel(options.config.model)) {
|
||||
if (usesLocalDreamina(options.config)) {
|
||||
await dependencies.ensureLocalDreaminaReady?.(options.signal);
|
||||
throwIfAborted(options.signal);
|
||||
return Promise.allSettled(
|
||||
@@ -368,12 +370,16 @@ function isLocalDreaminaModel(model: string) {
|
||||
return /^local:dreamina-cli:[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(model.trim());
|
||||
}
|
||||
|
||||
function usesLocalDreamina(config: AiConfig) {
|
||||
return (config.taskWorkflowProvider || "model") === "model" && isLocalDreaminaModel(config.model);
|
||||
}
|
||||
|
||||
function throwIfAborted(signal?: AbortSignal) {
|
||||
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
|
||||
function assertClientPromptLimit(mode: BackendGenerationMode, prompt: string, config: AiConfig, metadata?: Record<string, unknown>) {
|
||||
if (mode !== "image" || metadata?.promptTemplateOperation) return;
|
||||
if (mode !== "image" || metadata?.promptTemplateOperation || (config.taskWorkflowProvider || "model") !== "model") return;
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model);
|
||||
const promptLimitError = grokImagePromptLimitError(prompt, requestConfig.interfaceType, requestConfig.model);
|
||||
if (promptLimitError) throw new Error(promptLimitError);
|
||||
@@ -395,18 +401,21 @@ async function prepareGenerationReferences({
|
||||
async function createAndWaitGenerationTask(options: BackendGenerationTaskOptions, prepared: PreparedGenerationReferences, dependencies: GenerationTaskDependencies) {
|
||||
const { projectId, mode, prompt, config, signal, metadata, onTaskUpdate } = options;
|
||||
const videoOperation = generationOperation(options);
|
||||
const logicalModelId = logicalModelIDForConfig(config);
|
||||
const workflow = resolveGenerationWorkflowExecution(config, mode);
|
||||
const logicalModelId = workflow ? "" : logicalModelIDForConfig(config);
|
||||
const task = await dependencies.createTask({
|
||||
...(projectId ? { projectId } : {}),
|
||||
type: `canvas_${mode}`,
|
||||
operation: mode === "video" ? videoOperation : mode,
|
||||
prompt,
|
||||
model: config.model,
|
||||
...(workflow ? { provider: workflow.provider } : {}),
|
||||
model: workflow?.taskModel || config.model,
|
||||
...(logicalModelId ? { logicalModelId } : {}),
|
||||
input: {
|
||||
mode,
|
||||
prompt,
|
||||
config: backendProviderConfig(config),
|
||||
...(workflow ? { execution: workflowPublicExecution(workflow) } : {}),
|
||||
config: backendProviderConfig(config, mode),
|
||||
capabilityOptions: logicalModelId ? logicalCapabilityOptions(config, mode) : undefined,
|
||||
textHistory: options.textHistory,
|
||||
referenceImages: prepared.referenceImages,
|
||||
@@ -481,8 +490,10 @@ function backendMediaReference<T extends ReferenceVideo | ReferenceAudio>(media:
|
||||
} as T;
|
||||
}
|
||||
|
||||
export function backendProviderConfig(config: AiConfig) {
|
||||
export function backendProviderConfig(config: AiConfig, mode: BackendGenerationMode = "image") {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model);
|
||||
const workflow = resolveGenerationWorkflowExecution(config, mode);
|
||||
if (workflow) return workflowProviderConfig(config, requestConfig, workflow);
|
||||
const generationOptions = {
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
@@ -514,6 +525,57 @@ export function backendProviderConfig(config: AiConfig) {
|
||||
};
|
||||
}
|
||||
|
||||
function workflowProviderConfig(config: AiConfig, requestConfig: ReturnType<typeof resolveModelRequestConfig>, workflow: GenerationWorkflowExecution) {
|
||||
const runningHubActive = workflow.provider === "runninghub";
|
||||
const comfyBridgeActive = workflow.provider === "comfyui-bridge";
|
||||
return {
|
||||
channelId: "",
|
||||
apiFormat: requestConfig.apiFormat,
|
||||
interfaceType: workflow.interfaceType,
|
||||
baseUrl: comfyBridgeActive ? "bridge://local" : config.runningHub.baseUrl,
|
||||
allowLocalChannel: false,
|
||||
apiKey: runningHubActive ? config.runningHub.apiKey : "",
|
||||
// 工作流是独立 Provider,不能继承普通模型渠道的密钥和自定义头。
|
||||
secretKey: "",
|
||||
headers: [],
|
||||
model: workflow.providerModel,
|
||||
size: config.size,
|
||||
quality: config.quality,
|
||||
transparentBackground: config.transparentBackground,
|
||||
count: config.count,
|
||||
videoSeconds: config.videoSeconds,
|
||||
vquality: config.vquality,
|
||||
videoGenerateAudio: config.videoGenerateAudio,
|
||||
videoWatermark: config.videoWatermark,
|
||||
videoArkPrivateAssetUpload: config.videoArkPrivateAssetUpload,
|
||||
audioVoice: config.audioVoice,
|
||||
audioFormat: config.audioFormat,
|
||||
audioSpeed: config.audioSpeed,
|
||||
audioInstructions: config.audioInstructions,
|
||||
workflowId: workflow.workflowId,
|
||||
webappId: workflow.webappId,
|
||||
workflowJson: workflow.workflowJson,
|
||||
workflowFields: workflow.workflowFields,
|
||||
bridgeId: workflow.bridgeId,
|
||||
runningHubUseWallet: false,
|
||||
runningHubWalletApiKey: "",
|
||||
runningHubUploadApiKey: runningHubActive ? config.runningHub.uploadApiKey || "" : "",
|
||||
capabilityConfig: modelCapabilityConfigFor(config, requestConfig.model),
|
||||
systemPrompt: "",
|
||||
};
|
||||
}
|
||||
|
||||
function workflowPublicExecution(workflow: GenerationWorkflowExecution) {
|
||||
return {
|
||||
provider: workflow.provider,
|
||||
kind: workflow.kind,
|
||||
interfaceType: workflow.interfaceType,
|
||||
name: workflow.name,
|
||||
...(workflow.workflowId ? { workflowId: workflow.workflowId } : {}),
|
||||
...(workflow.webappId ? { webappId: workflow.webappId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function logicalCapabilityOptions(config: AiConfig, mode: BackendGenerationMode) {
|
||||
const channel = resolveModelChannel(config, config.model);
|
||||
const spec = channel.modelCosts?.find((item) => item.model === modelOptionName(config.model))?.logicalCapabilitySpec;
|
||||
|
||||
27
web/src/services/api/runninghub.ts
Normal file
27
web/src/services/api/runninghub.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { apiClient, request } from "@/services/api/request";
|
||||
import type { RunningHubConfig, RunningHubWorkflow } from "@/stores/use-config-store";
|
||||
|
||||
export type RunningHubWorkflowFetchResult = RunningHubWorkflow & { kind: "workflow" | "app"; webappId?: string; raw?: Record<string, unknown> };
|
||||
|
||||
type FetchRequest = Pick<RunningHubConfig, "baseUrl" | "apiKey" | "walletApiKey" | "useWallet"> & { workflowId?: string; webappId?: string; title?: string; capability: RunningHubConfig["capability"] };
|
||||
|
||||
export function fetchRunningHubWorkflow(config: FetchRequest) {
|
||||
return request<RunningHubWorkflowFetchResult>(apiClient.post("/runninghub/workflow-info", runningHubFetchPayload(config)));
|
||||
}
|
||||
|
||||
export function fetchRunningHubApp(config: FetchRequest) {
|
||||
return request<RunningHubWorkflowFetchResult>(apiClient.post("/runninghub/app-info", runningHubFetchPayload(config)));
|
||||
}
|
||||
|
||||
function runningHubFetchPayload(config: FetchRequest) {
|
||||
return {
|
||||
baseUrl: config.baseUrl,
|
||||
apiKey: config.apiKey,
|
||||
walletApiKey: config.walletApiKey,
|
||||
useWallet: config.useWallet,
|
||||
workflowId: config.workflowId,
|
||||
webappId: config.webappId,
|
||||
title: config.title,
|
||||
capability: config.capability,
|
||||
};
|
||||
}
|
||||
@@ -404,6 +404,17 @@ export function retryGenerationTask(id: string) {
|
||||
return request<GenerationTask>(api.post(`/tasks/${encodeURIComponent(id)}/retry`));
|
||||
}
|
||||
|
||||
export function cancelGenerationTask(id: string) {
|
||||
if (isLocalDreaminaTaskId(id)) {
|
||||
return Promise.reject(new Error("官方即梦 CLI 当前不支持可靠取消"));
|
||||
}
|
||||
return request<GenerationTask>(api.post(`/tasks/${encodeURIComponent(id)}/cancel`)).then((task) => {
|
||||
window.dispatchEvent(new CustomEvent("canvas:task-cancelled", { detail: { task } }));
|
||||
window.dispatchEvent(new CustomEvent("wallet:updated"));
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
export function queryFailedVideoProviderTask(id: string) {
|
||||
return request<ProviderTaskQueryResult>(api.post(`/tasks/${encodeURIComponent(id)}/query-provider`));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user