Changeset 0.27.3 (#170)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/webhook"
|
||||
)
|
||||
|
||||
// ── Late Registration ────────────────────────
|
||||
@@ -120,6 +121,9 @@ func (t *workflowAdvanceTool) Execute(ctx context.Context, execCtx ExecutionCont
|
||||
// Create note with collected data
|
||||
CreateWorkflowStageNote(ctx, t.stores, channelID, currentStage, args.Data, args.Summary)
|
||||
|
||||
// v0.27.3: Fire on_complete chaining + webhook (was missing from tool path)
|
||||
go TriggerWorkflowOnComplete(ctx, t.stores, workflowID, channelID, mergedData)
|
||||
|
||||
result, _ := json.Marshal(map[string]interface{}{
|
||||
"status": "completed",
|
||||
"current_stage": nextStage,
|
||||
@@ -245,3 +249,128 @@ func CreateWorkflowAssignment(ctx context.Context, channelID string, stage int,
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// ── Workflow Completion Triggers (v0.27.3) ──
|
||||
|
||||
// TriggerWorkflowOnComplete checks if the workflow has an on_complete chain
|
||||
// config and fires the target workflow. Also delivers webhook if configured.
|
||||
// Shared by both the HTTP handler (Advance endpoint) and the workflow_advance tool.
|
||||
func TriggerWorkflowOnComplete(ctx context.Context, stores store.Stores, workflowID, channelID, mergedData string) {
|
||||
if stores.Workflows == nil {
|
||||
return
|
||||
}
|
||||
wf, err := stores.Workflows.GetByID(ctx, workflowID)
|
||||
if err != nil || wf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// v0.27.3: Webhook delivery on workflow completion
|
||||
if wf.WebhookURL != "" {
|
||||
var stageData any
|
||||
_ = json.Unmarshal([]byte(mergedData), &stageData)
|
||||
|
||||
go deliverWorkflowWebhook(wf.WebhookURL, wf.WebhookSecret, workflowID, channelID, stageData)
|
||||
}
|
||||
|
||||
// on_complete chaining
|
||||
if len(wf.OnComplete) == 0 || string(wf.OnComplete) == "null" {
|
||||
return
|
||||
}
|
||||
|
||||
var chain struct {
|
||||
Action string `json:"action"`
|
||||
TargetSlug string `json:"target_slug"`
|
||||
DataMap map[string]string `json:"data_map"`
|
||||
}
|
||||
if err := json.Unmarshal(wf.OnComplete, &chain); err != nil || chain.Action != "start_workflow" || chain.TargetSlug == "" {
|
||||
return
|
||||
}
|
||||
|
||||
target, err := stores.Workflows.GetBySlug(ctx, wf.TeamID, chain.TargetSlug)
|
||||
if err != nil || target == nil || !target.IsActive {
|
||||
log.Printf("[workflow] on_complete: target workflow '%s' not found or inactive", chain.TargetSlug)
|
||||
return
|
||||
}
|
||||
|
||||
var sourceData map[string]any
|
||||
if err := json.Unmarshal([]byte(mergedData), &sourceData); err != nil {
|
||||
sourceData = map[string]any{}
|
||||
}
|
||||
targetData := map[string]any{}
|
||||
if len(chain.DataMap) > 0 {
|
||||
for srcKey, tgtKey := range chain.DataMap {
|
||||
if v, ok := sourceData[srcKey]; ok {
|
||||
targetData[tgtKey] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targetData = sourceData
|
||||
}
|
||||
|
||||
var ownerID string
|
||||
_ = database.DB.QueryRowContext(ctx, database.Q(
|
||||
`SELECT user_id FROM channels WHERE id = $1`,
|
||||
), channelID).Scan(&ownerID)
|
||||
if ownerID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ver, err := stores.Workflows.GetLatestVersion(ctx, target.ID)
|
||||
if err != nil {
|
||||
log.Printf("[workflow] on_complete: target '%s' has no published version", chain.TargetSlug)
|
||||
return
|
||||
}
|
||||
stages, err := stores.Workflows.ListStages(ctx, target.ID)
|
||||
if err != nil || len(stages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ch := &models.Channel{
|
||||
UserID: ownerID,
|
||||
Title: target.Name + " (chained)",
|
||||
Description: target.Description,
|
||||
Type: "workflow",
|
||||
TeamID: target.TeamID,
|
||||
}
|
||||
if err := stores.Channels.Create(ctx, ch); err != nil {
|
||||
log.Printf("[workflow] on_complete: failed to create chained channel: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
initialData, _ := json.Marshal(targetData)
|
||||
_, err = database.DB.ExecContext(ctx, database.Q(`
|
||||
UPDATE channels
|
||||
SET workflow_id = $1, workflow_version = $2, current_stage = 0,
|
||||
stage_data = $3, workflow_status = 'active',
|
||||
last_activity_at = $4, ai_mode = 'auto'
|
||||
WHERE id = $5
|
||||
`), target.ID, ver.VersionNumber, string(initialData), time.Now().UTC(), ch.ID)
|
||||
if err != nil {
|
||||
log.Printf("[workflow] on_complete: failed to set workflow columns: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Bind first stage persona
|
||||
if len(stages) > 0 && stages[0].PersonaID != nil {
|
||||
_, _ = database.DB.ExecContext(ctx, database.Q(`
|
||||
INSERT INTO channel_participants (channel_id, participant_type, participant_id)
|
||||
VALUES ($1, 'persona', $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`), ch.ID, *stages[0].PersonaID)
|
||||
}
|
||||
|
||||
log.Printf("[workflow] on_complete: started chained workflow %s → %s (channel %s)",
|
||||
workflowID, target.ID, ch.ID)
|
||||
}
|
||||
|
||||
// deliverWorkflowWebhook fires a webhook for workflow completion.
|
||||
func deliverWorkflowWebhook(url, secret, workflowID, channelID string, stageData any) {
|
||||
webhook.Deliver(url, secret, webhook.Payload{
|
||||
WorkflowID: workflowID,
|
||||
ChannelID: channelID,
|
||||
Status: "completed",
|
||||
CompletedAt: time.Now().UTC(),
|
||||
StageData: stageData,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user