259 lines
7.6 KiB
Go
259 lines
7.6 KiB
Go
package handlers
|
|
|
|
// workflow_packages.go — v0.30.2 CS1
|
|
//
|
|
// Handles workflow-specific package operations:
|
|
// - ExportWorkflowPackage: bundles a workflow definition + stages into a .pkg
|
|
// - InstallWorkflowFromManifest: creates/updates a workflow from a .pkg manifest
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"chat-switchboard/models"
|
|
"chat-switchboard/store"
|
|
)
|
|
|
|
// WorkflowPackageHandler handles workflow package export and install.
|
|
type WorkflowPackageHandler struct {
|
|
stores store.Stores
|
|
}
|
|
|
|
// NewWorkflowPackageHandler creates a new workflow package handler.
|
|
func NewWorkflowPackageHandler(s store.Stores) *WorkflowPackageHandler {
|
|
return &WorkflowPackageHandler{stores: s}
|
|
}
|
|
|
|
// ExportWorkflowPackage exports a workflow definition as a downloadable .pkg.
|
|
// GET /api/v1/admin/workflows/:id/export
|
|
func (h *WorkflowPackageHandler) ExportWorkflowPackage(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
wfID := c.Param("id")
|
|
|
|
wf, err := h.stores.Workflows.GetByID(ctx, wfID)
|
|
if err != nil || wf == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"})
|
|
return
|
|
}
|
|
|
|
stages, err := h.stores.Workflows.ListStages(ctx, wfID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load stages"})
|
|
return
|
|
}
|
|
|
|
// Build stage definitions for the manifest
|
|
stageDefs := make([]map[string]any, 0, len(stages))
|
|
for _, s := range stages {
|
|
sd := map[string]any{
|
|
"name": s.Name,
|
|
"ordinal": s.Ordinal,
|
|
"stage_mode": s.StageMode,
|
|
"history_mode": s.HistoryMode,
|
|
"auto_transition": s.AutoTransition,
|
|
}
|
|
if s.PersonaID != nil {
|
|
sd["persona_id"] = *s.PersonaID
|
|
}
|
|
if s.AssignmentTeamID != nil {
|
|
sd["assignment_team_id"] = *s.AssignmentTeamID
|
|
}
|
|
if s.SurfacePkgID != nil {
|
|
sd["surface_pkg_id"] = *s.SurfacePkgID
|
|
}
|
|
if len(s.FormTemplate) > 0 && string(s.FormTemplate) != "{}" {
|
|
var ft any
|
|
if json.Unmarshal(s.FormTemplate, &ft) == nil {
|
|
sd["form_template"] = ft
|
|
}
|
|
}
|
|
if len(s.TransitionRules) > 0 && string(s.TransitionRules) != "{}" {
|
|
var tr any
|
|
if json.Unmarshal(s.TransitionRules, &tr) == nil {
|
|
sd["transition_rules"] = tr
|
|
}
|
|
}
|
|
stageDefs = append(stageDefs, sd)
|
|
}
|
|
|
|
// Build the manifest
|
|
manifest := map[string]any{
|
|
"id": wf.Slug,
|
|
"title": wf.Name,
|
|
"type": "workflow",
|
|
"workflow_definition": map[string]any{
|
|
"name": wf.Name,
|
|
"slug": wf.Slug,
|
|
"entry_mode": wf.EntryMode,
|
|
"stages": stageDefs,
|
|
},
|
|
}
|
|
if wf.Description != "" {
|
|
manifest["description"] = wf.Description
|
|
}
|
|
|
|
// Include branding if set
|
|
if len(wf.Branding) > 0 && string(wf.Branding) != "{}" {
|
|
var branding any
|
|
if json.Unmarshal(wf.Branding, &branding) == nil {
|
|
wfDef := manifest["workflow_definition"].(map[string]any)
|
|
wfDef["branding"] = branding
|
|
}
|
|
}
|
|
|
|
// Set response headers
|
|
filename := fmt.Sprintf("%s.pkg", wf.Slug)
|
|
c.Header("Content-Type", "application/zip")
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
|
|
// Create zip writer directly to response
|
|
zw := zip.NewWriter(c.Writer)
|
|
defer zw.Close()
|
|
|
|
// Write manifest.json
|
|
manifestJSON, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to serialize manifest"})
|
|
return
|
|
}
|
|
mf, err := zw.Create("manifest.json")
|
|
if err != nil {
|
|
return
|
|
}
|
|
mf.Write(manifestJSON)
|
|
}
|
|
|
|
// InstallWorkflowFromManifest creates or updates a workflow from a package manifest.
|
|
// Called from InstallPackage when type="workflow".
|
|
func InstallWorkflowFromManifest(ctx *gin.Context, stores store.Stores, pkgID string, manifest map[string]any) error {
|
|
wfDefRaw, ok := manifest["workflow_definition"]
|
|
if !ok {
|
|
return fmt.Errorf("missing workflow_definition in manifest")
|
|
}
|
|
|
|
// Re-marshal and parse for type safety
|
|
wfDefJSON, err := json.Marshal(wfDefRaw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid workflow_definition: %w", err)
|
|
}
|
|
|
|
var wfDef struct {
|
|
Name string `json:"name"`
|
|
Slug string `json:"slug"`
|
|
EntryMode string `json:"entry_mode"`
|
|
Branding json.RawMessage `json:"branding"`
|
|
Stages []workflowPkgStage `json:"stages"`
|
|
}
|
|
if err := json.Unmarshal(wfDefJSON, &wfDef); err != nil {
|
|
return fmt.Errorf("invalid workflow_definition: %w", err)
|
|
}
|
|
|
|
if wfDef.Name == "" || wfDef.Slug == "" {
|
|
return fmt.Errorf("workflow_definition requires name and slug")
|
|
}
|
|
if wfDef.EntryMode == "" {
|
|
wfDef.EntryMode = "public_link"
|
|
}
|
|
|
|
userID := ctx.GetString("user_id")
|
|
reqCtx := ctx.Request.Context()
|
|
|
|
// Check if workflow already exists (by slug, global scope)
|
|
existing, _ := stores.Workflows.GetBySlug(reqCtx, nil, wfDef.Slug)
|
|
|
|
var workflowID string
|
|
|
|
if existing != nil {
|
|
// Update existing workflow
|
|
workflowID = existing.ID
|
|
patch := models.WorkflowPatch{
|
|
Name: &wfDef.Name,
|
|
EntryMode: &wfDef.EntryMode,
|
|
}
|
|
if len(wfDef.Branding) > 0 {
|
|
b := json.RawMessage(wfDef.Branding)
|
|
patch.Branding = &b
|
|
}
|
|
if err := stores.Workflows.Update(reqCtx, workflowID, patch); err != nil {
|
|
return fmt.Errorf("failed to update workflow: %w", err)
|
|
}
|
|
|
|
// Delete existing stages (will be replaced)
|
|
existingStages, _ := stores.Workflows.ListStages(reqCtx, workflowID)
|
|
for _, s := range existingStages {
|
|
stores.Workflows.DeleteStage(reqCtx, s.ID)
|
|
}
|
|
} else {
|
|
// Create new workflow
|
|
wf := &models.Workflow{
|
|
Name: wfDef.Name,
|
|
Slug: wfDef.Slug,
|
|
EntryMode: wfDef.EntryMode,
|
|
CreatedBy: userID,
|
|
}
|
|
if len(wfDef.Branding) > 0 {
|
|
wf.Branding = json.RawMessage(wfDef.Branding)
|
|
}
|
|
if err := stores.Workflows.Create(reqCtx, wf); err != nil {
|
|
return fmt.Errorf("failed to create workflow: %w", err)
|
|
}
|
|
workflowID = wf.ID
|
|
}
|
|
|
|
// Create stages from definition
|
|
for _, s := range wfDef.Stages {
|
|
st := &models.WorkflowStage{
|
|
WorkflowID: workflowID,
|
|
Ordinal: s.Ordinal,
|
|
Name: s.Name,
|
|
StageMode: s.StageMode,
|
|
HistoryMode: s.HistoryMode,
|
|
AutoTransition: s.AutoTransition,
|
|
PersonaID: s.PersonaID,
|
|
AssignmentTeamID: s.AssignmentTeamID,
|
|
SurfacePkgID: s.SurfacePkgID,
|
|
}
|
|
if st.StageMode == "" {
|
|
st.StageMode = models.StageModeChatOnly
|
|
}
|
|
if st.HistoryMode == "" {
|
|
st.HistoryMode = "full"
|
|
}
|
|
if s.FormTemplate != nil {
|
|
st.FormTemplate, _ = json.Marshal(s.FormTemplate)
|
|
}
|
|
if s.TransitionRules != nil {
|
|
st.TransitionRules, _ = json.Marshal(s.TransitionRules)
|
|
}
|
|
if err := stores.Workflows.CreateStage(reqCtx, st); err != nil {
|
|
log.Printf("[workflow-pkg] failed to create stage %q: %v", s.Name, err)
|
|
}
|
|
}
|
|
|
|
// Store workflow_id in package_settings for reference
|
|
settingsJSON, _ := json.Marshal(map[string]string{"workflow_id": workflowID})
|
|
stores.Packages.SetPackageSettings(reqCtx, pkgID, json.RawMessage(settingsJSON))
|
|
|
|
log.Printf("[workflow-pkg] installed workflow %q (id=%s) from package %s", wfDef.Name, workflowID, pkgID)
|
|
return nil
|
|
}
|
|
|
|
// workflowPkgStage is the stage definition within a workflow package manifest.
|
|
type workflowPkgStage struct {
|
|
Name string `json:"name"`
|
|
Ordinal int `json:"ordinal"`
|
|
StageMode string `json:"stage_mode"`
|
|
HistoryMode string `json:"history_mode"`
|
|
AutoTransition bool `json:"auto_transition"`
|
|
PersonaID *string `json:"persona_id,omitempty"`
|
|
AssignmentTeamID *string `json:"assignment_team_id,omitempty"`
|
|
SurfacePkgID *string `json:"surface_pkg_id,omitempty"`
|
|
FormTemplate any `json:"form_template,omitempty"`
|
|
TransitionRules any `json:"transition_rules,omitempty"`
|
|
}
|