ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package dify
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrUnavailable = errors.New("DIFY_WORKFLOW_UNAVAILABLE")
|
|
var ErrFailed = errors.New("DIFY_WORKFLOW_FAILED")
|
|
|
|
type Client struct {
|
|
BaseURL, APIKey string
|
|
HTTP *http.Client
|
|
}
|
|
type Event struct {
|
|
Event string `json:"event"`
|
|
WorkflowRunID string `json:"workflow_run_id"`
|
|
TaskID string `json:"task_id"`
|
|
Data struct {
|
|
ID string `json:"id"`
|
|
NodeID string `json:"node_id"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error"`
|
|
Outputs map[string]json.RawMessage `json:"outputs"`
|
|
} `json:"data"`
|
|
}
|
|
type Run struct {
|
|
ID string
|
|
Outputs map[string]json.RawMessage
|
|
}
|
|
|
|
func NewClient(base, key string) (*Client, error) {
|
|
u, err := url.Parse(base)
|
|
if err != nil || u.Host == "" || (u.Scheme != "https" && u.Scheme != "http") || strings.TrimSpace(key) == "" {
|
|
return nil, errors.New("invalid Dify configuration")
|
|
}
|
|
return &Client{BaseURL: strings.TrimRight(base, "/"), APIKey: key, HTTP: &http.Client{Timeout: 13 * time.Minute}}, nil
|
|
}
|
|
|
|
// Streaming avoids the reverse proxy timeout for a book with many pictures,
|
|
// and gives the caller a chance to publish text before illustration completes.
|
|
func (c *Client) Run(ctx context.Context, inputs map[string]any, onNode func(Event) error) (Run, error) {
|
|
raw, _ := json.Marshal(map[string]any{"inputs": inputs, "response_mode": "streaming", "user": "story-platform"})
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/workflows/run", bytes.NewReader(raw))
|
|
if err != nil {
|
|
return Run{}, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return Run{}, ErrUnavailable
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return Run{}, ErrUnavailable
|
|
}
|
|
scanner := bufio.NewScanner(io.LimitReader(resp.Body, 16<<20))
|
|
scanner.Buffer(make([]byte, 4096), 2<<20)
|
|
var run Run
|
|
finished := false
|
|
var taskID string
|
|
defer func() {
|
|
if !finished && taskID != "" {
|
|
c.stop(taskID)
|
|
}
|
|
}()
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !strings.HasPrefix(line, "data:") {
|
|
continue
|
|
}
|
|
payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
if payload == "[DONE]" {
|
|
break
|
|
}
|
|
var event Event
|
|
if json.Unmarshal([]byte(payload), &event) != nil {
|
|
return run, ErrFailed
|
|
}
|
|
if event.WorkflowRunID != "" {
|
|
run.ID = event.WorkflowRunID
|
|
}
|
|
if event.TaskID != "" {
|
|
taskID = event.TaskID
|
|
}
|
|
if event.Event == "node_finished" && event.Data.Status == "succeeded" && onNode != nil {
|
|
if err = onNode(event); err != nil {
|
|
return run, err
|
|
}
|
|
}
|
|
if event.Event == "workflow_finished" {
|
|
finished = true
|
|
if event.Data.Status != "succeeded" {
|
|
return run, ErrFailed
|
|
}
|
|
run.Outputs = event.Data.Outputs
|
|
if run.ID == "" {
|
|
run.ID = event.Data.ID
|
|
}
|
|
}
|
|
if event.Event == "error" {
|
|
return run, ErrFailed
|
|
}
|
|
}
|
|
if scanner.Err() != nil || !finished {
|
|
return run, ErrUnavailable
|
|
}
|
|
return run, nil
|
|
}
|
|
|
|
func (c *Client) stop(taskID string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
body := strings.NewReader(`{"user":"story-platform"}`)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/workflows/tasks/"+taskID+"/stop", body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := c.HTTP.Do(req)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|