Files

148 lines
4.5 KiB
Go

package jsplugin
import (
"context"
"fmt"
"mime"
"net/http"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
pluginruntime "github.com/QuantumNous/new-api/pkg/jsplugin"
"github.com/QuantumNous/new-api/relay/helper"
)
// readSubmitEvents owns only SSE framing and bounded JSON state. The plugin
// interprets data, accumulates provider output and decides when it is complete.
// No response bytes are written to the client at this boundary.
func (a *TaskAdaptor) readSubmitEvents(parent context.Context, resp *http.Response, driverContext map[string]any) (any, error) {
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil || mediaType != "text/event-stream" {
return nil, fmt.Errorf("expected a text/event-stream submit response")
}
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil)
stopClose := context.AfterFunc(ctx, func() { _ = resp.Body.Close() })
defer stopClose()
idleTimeout := time.Duration(constant.StreamingTimeout) * time.Second
if idleTimeout <= 0 {
idleTimeout = 30 * time.Second
}
timer := time.AfterFunc(idleTimeout, func() { cancel(fmt.Errorf("task submit stream idle timeout")) })
defer timer.Stop()
scanner := helper.NewStreamScanner(resp.Body, maxTaskPluginPersistedJSONBytes+1)
hook := "parseSubmitEvent"
var accumulated *pluginruntime.JSONState
if pluginruntime.UsesSubmitEventDelta(a.plugin.Meta, a.plugin.Engine) {
hook = "parseSubmitEventDelta"
accumulated = pluginruntime.NewJSONState(maxTaskPluginPersistedJSONBytes)
}
var state any
var data []string
var eventName, lastID string
frameBytes := 0
firstLine := true
for scanner.Scan() {
if err := context.Cause(ctx); err != nil {
return nil, err
}
timer.Reset(idleTimeout)
line := scanner.Text()
if firstLine {
line = strings.TrimPrefix(line, "\ufeff")
firstLine = false
}
frameBytes += len(line) + 1
if frameBytes > maxTaskPluginPersistedJSONBytes {
return nil, fmt.Errorf("task submit SSE event exceeds size limit")
}
if line != "" {
field, value, _ := strings.Cut(line, ":")
value, _ = strings.CutPrefix(value, " ")
switch field {
case "data":
data = append(data, value)
case "event":
eventName = value
case "id":
if !strings.ContainsRune(value, '\x00') {
lastID = value
}
}
continue
}
frameBytes = 0
if len(data) == 0 {
eventName = ""
continue
}
if eventName == "" {
eventName = "message"
}
event := map[string]any{"event": eventName, "id": lastID, "data": strings.Join(data, "\n")}
value, err := a.plugin.Engine.Call(ctx, hook, driverContext, event, state)
if err != nil {
return nil, err
}
result, ok := value.(map[string]any)
if !ok {
return nil, fmt.Errorf("%s must return an object", hook)
}
nextState, hasState := result["state"]
done, hasDone := result["done"].(bool)
if !hasState || !hasDone {
return nil, fmt.Errorf("%s must return state and a boolean done", hook)
}
encoded, err := common.Marshal(nextState)
if err != nil {
return nil, fmt.Errorf("invalid submit stream state: %w", err)
}
if len(encoded) > maxTaskPluginPersistedJSONBytes {
return nil, fmt.Errorf("task submit stream state exceeds size limit")
}
if accumulated != nil {
if len(result) != 3 {
return nil, fmt.Errorf("parseSubmitEventDelta must return only changes, state and done")
}
// Control state is separate from the full result and is the only
// snapshot passed back to JS on the next event.
if len(encoded) > 64<<10 {
return nil, fmt.Errorf("task submit stream control state exceeds size limit")
}
if err = accumulated.Apply(ctx, result["changes"]); err != nil {
return nil, fmt.Errorf("invalid submit stream changes: %w", err)
}
}
// Keep the exact encoded-byte limit above. Plain JSON state goes back
// to the next event as is; codec-specific values (for example exported
// typed arrays) retain the old normalization.
if isPlainJSONValue(nextState, 0) {
state = nextState
} else {
var decoded any
if err = common.Unmarshal(encoded, &decoded); err != nil {
return nil, err
}
state = decoded
}
if done {
if accumulated != nil {
return accumulated.Value()
}
return state, nil
}
data = nil
eventName = ""
}
if err := context.Cause(ctx); err != nil {
return nil, err
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("read task submit stream: %w", err)
}
return nil, fmt.Errorf("task submit stream ended before the plugin reported completion")
}