package dify import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "strconv" "strings" "time" ) // Only the backend has the bridge credential. Dify receives a short-lived step // token, and records counts and IDs rather than elder speech or image bytes. type Bridge struct { BaseURL, Secret string HTTP *http.Client } type Ticket struct { ID string `json:"context_id"` Token string `json:"step_token"` } func NewBridge(base, secret string) *Bridge { return &Bridge{strings.TrimRight(base, "/"), secret, &http.Client{Timeout: 30 * time.Second}} } func (b *Bridge) request(ctx context.Context, method, path string, payload any, out any) error { var body io.Reader if payload != nil { raw, _ := json.Marshal(payload) body = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, b.BaseURL+path, body) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+b.Secret) req.Header.Set("Content-Type", "application/json") resp, err := b.HTTP.Do(req) if err != nil { return ErrUnavailable } defer resp.Body.Close() if resp.StatusCode != 200 { return ErrUnavailable } if out != nil { return json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(out) } return nil } func (b *Bridge) Register(ctx context.Context, kind string, input any) (Ticket, error) { var t Ticket err := b.request(ctx, "POST", "/contexts", map[string]any{"kind": kind, "input": input}, &t) return t, err } func (b *Bridge) Result(ctx context.Context, t Ticket, out any) error { return b.request(ctx, "GET", "/contexts/"+t.ID, nil, out) } func (b *Bridge) Forget(t Ticket) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = b.request(ctx, "DELETE", "/contexts/"+t.ID, nil, nil) } func (b *Bridge) Image(ctx context.Context, t Ticket, index int) ([]byte, error) { path := b.BaseURL + "/contexts/" + t.ID + "/images/" + strconv.Itoa(index) req, _ := http.NewRequestWithContext(ctx, "GET", path, nil) req.Header.Set("Authorization", "Bearer "+b.Secret) resp, err := b.HTTP.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == 404 { return nil, nil } if resp.StatusCode != 200 { return nil, ErrUnavailable } raw, err := io.ReadAll(io.LimitReader(resp.Body, (10<<20)+1)) if err != nil || len(raw) > 10<<20 || http.DetectContentType(raw) != "image/jpeg" { return nil, errors.New("invalid workflow illustration") } return raw, nil }