Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
147 lines
4.5 KiB
Go
147 lines
4.5 KiB
Go
package jsplugin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"slices"
|
|
"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 slices.Contains(a.plugin.Meta.RequiredCapabilities, pluginruntime.CapabilitySubmitSSEDelta) {
|
|
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 can be
|
|
// isolated without parsing large strings again; codec-specific values
|
|
// (for example exported typed arrays) retain the old normalization.
|
|
var plainJSON bool
|
|
state, plainJSON = cloneJSONValue(nextState, 0)
|
|
if !plainJSON {
|
|
if err = common.Unmarshal(encoded, &state); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
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")
|
|
}
|