Files
kandev/apps/web/app/settings/workspace/workflow-duplication.ts
T
2026-09-10 16:32:41 +01:00

117 lines
3.8 KiB
TypeScript

import { generateUUID } from "@/lib/utils";
import {
normalizeWorkflowProfileSessionStartPolicy,
normalizeWorkflowProfileSessionEndPolicy,
workflowId as toWorkflowId,
type Workflow,
type WorkflowStep,
} from "@/lib/types/http";
const COPY_SUFFIX_PATTERN = /^(.*) \(copy(?: (\d+))?\)$/;
export function getWorkflowCopyName(
sourceName: string,
existingWorkflows: readonly Pick<Workflow, "name">[],
): string {
const baseName = sourceName.match(COPY_SUFFIX_PATTERN)?.[1] ?? sourceName;
const existingNames = new Set(existingWorkflows.map((workflow) => workflow.name));
for (let suffix = 1; ; suffix += 1) {
const candidate = `${baseName} (copy${suffix === 1 ? "" : ` ${suffix}`})`;
if (!existingNames.has(candidate)) return candidate;
}
}
function remapStepReferences<T>(value: T, stepIds: ReadonlyMap<string, string>): T {
if (Array.isArray(value)) return value.map((item) => remapStepReferences(item, stepIds)) as T;
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
key === "step_id" && typeof item === "string"
? (stepIds.get(item) ?? item)
: remapStepReferences(item, stepIds),
]),
) as T;
}
function temporaryWorkflowId(): string {
return `temp-workflow-${generateUUID()}`;
}
function temporaryStepId(): string {
return `temp-step-${generateUUID()}`;
}
function cloneWorkflow(source: Workflow, workflows: readonly Workflow[], id: string): Workflow {
return {
id: toWorkflowId(id),
workspace_id: source.workspace_id,
name: getWorkflowCopyName(source.name, workflows),
description: source.description,
prompt: source.prompt,
agent_profile_id: source.agent_profile_id,
created_at: "",
updated_at: "",
};
}
function cloneWorkflowStep(
source: WorkflowStep,
workflowId: WorkflowStep["workflow_id"],
stepId: string,
stepIds: ReadonlyMap<string, string>,
): WorkflowStep {
const remapStepId = (id: string | null | undefined) =>
id == null ? id : (stepIds.get(id) ?? id);
return {
id: stepId,
workflow_id: workflowId,
name: source.name,
position: source.position,
color: source.color,
prompt: source.prompt,
events: remapStepReferences(source.events, stepIds),
allow_manual_move: source.allow_manual_move,
is_start_step: source.is_start_step,
show_in_command_panel: source.show_in_command_panel,
auto_archive_after_hours: source.auto_archive_after_hours,
agent_profile_id: source.agent_profile_id,
profile_session_start_policy: normalizeWorkflowProfileSessionStartPolicy(
source.profile_session_start_policy,
),
profile_session_end_policy: normalizeWorkflowProfileSessionEndPolicy(
source.profile_session_end_policy,
),
complete_task_on_enter: source.complete_task_on_enter,
auto_advance_requires_signal: source.auto_advance_requires_signal,
cancel_triggers_turn_complete: source.cancel_triggers_turn_complete,
wip_limit: source.wip_limit,
session_target:
source.session_target?.kind === "step"
? { kind: "step", step_id: remapStepId(source.session_target.step_id)! }
: (source.session_target ?? null),
pull_from_step_id: remapStepId(source.pull_from_step_id),
stage_type: source.stage_type,
created_at: "",
updated_at: "",
};
}
export function createWorkflowDuplication(
source: Workflow,
workflows: readonly Workflow[],
sourceSteps: readonly WorkflowStep[],
): { workflow: Workflow; steps: WorkflowStep[] } {
const clientWorkflowId = temporaryWorkflowId();
const stepIds = new Map(sourceSteps.map((step) => [step.id, temporaryStepId()]));
const workflow = cloneWorkflow(source, workflows, clientWorkflowId);
const steps = sourceSteps.map((step) =>
cloneWorkflowStep(step, workflow.id, stepIds.get(step.id)!, stepIds),
);
return { workflow, steps };
}