Files
story/internal/dify/bridge.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
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.
2026-10-08 10:06:44 +08:00

88 lines
2.4 KiB
Go

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
}