internal/pluginhost/adapters_executors_deframe.go
Rama main · réplica de lectura del repositorio original.
package pluginhost
import (
"bytes"
"context"
"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)
// This file normalizes plugin executor SSE streams before stream translation.
//
// Same-format stream translators assume each incoming payload is exactly one
// complete event. A passthrough plugin relaying raw host HTTP stream bytes
// does not guarantee that: chunks may hold several coalesced events or split
// one event at an arbitrary byte. deframeExecutorSSEChunks rebuilds event
// boundaries so the translators always receive exactly one event at a time.
//
// Payloads that are not SSE at all (bare JSON objects, the stock example's
// unterminated "plugin-example" text) are never parsed: they are forwarded
// with their original chunk boundary intact, so consecutive bare objects
// without line breaks are not concatenated, and multi-line bare JSON is not
// split per line.
// executorSSEDeframer accumulates incoming plugin stream chunks only while
// they can still belong to an incomplete SSE event. Once an event boundary
// (a blank line) is reached the event is emitted whole. Chunk bytes that can
// no longer belong to an SSE event are released verbatim with their chunk
// boundary intact instead of being buffered or split. A partial prefix such
// as "da" is buffered because later bytes may complete a "data:" line.
//
// The deframer also remembers whether the stream has ever produced an SSE
// event (sawSSE). Established SSE streams treat blank lines between events
// as framing even when a blank line arrives in a later chunk, and bare
// lines are emitted one line at a time so a following SSE event can still
// be recognized. Streams that have never produced an SSE event stay in
// bare mode, where payloads are forwarded with their chunk boundaries
// intact and never split or merged.
type executorSSEDeframer struct {
buffer []byte
sawSSE bool
}
func (d *executorSSEDeframer) feed(payload []byte) [][]byte {
if len(payload) == 0 {
return nil
}
// Fast path: nothing pending, no SSE event seen yet, and the chunk
// cannot begin an SSE event. Emit the chunk untouched so bare payloads
// keep their boundaries, including bare content that opens with a
// newline.
if len(d.buffer) == 0 && !d.sawSSE && !canBeginSSEEvent(payload) {
return [][]byte{payload}
}
d.buffer = append(d.buffer, payload...)
var chunks [][]byte
for {
if len(d.buffer) == 0 {
break
}
// Leading blank lines are the unmatched second half of an earlier
// event boundary and are dropped. In bare mode this only runs after
// an SSE event started in an earlier chunk (the fast path above
// keeps newline-led bare payloads untouched).
dropBlankSSELine := false
if idx := bytes.IndexByte(d.buffer, '\n'); idx >= 0 && isBlankSSELine(d.buffer[:idx]) {
d.advance(idx + 1)
dropBlankSSELine = true
}
if dropBlankSSELine {
continue
}
if len(d.buffer) == 0 {
break
}
if !sseDataLinePrefix(d.buffer) && !bytes.HasPrefix(d.buffer, []byte("event:")) && !bytes.HasPrefix(d.buffer, []byte(":")) {
// Leftover bytes do not begin a new SSE event. A partial
// "data:"/"event:" prefix stays buffered. In SSE mode only the
// first complete line is released so an SSE event that follows
// in a later chunk is still deframed; in bare mode the whole
// leftover is released so bare content keeps its chunk
// boundary (no line-based splitting or merging).
if isSSEFieldPrefixPartial(d.buffer) {
break
}
if d.sawSSE {
idx := bytes.IndexByte(d.buffer, '\n')
if idx < 0 {
break
}
chunks = append(chunks, bytes.Clone(d.buffer[:idx+1]))
d.advance(idx + 1)
continue
}
chunks = append(chunks, bytes.Clone(d.buffer))
d.buffer = nil
break
}
event, complete := d.takeEvent()
if !complete {
break
}
d.sawSSE = true
if len(event) > 0 {
chunks = append(chunks, event)
}
}
return chunks
}
// flush returns any buffered partial event at end of stream.
func (d *executorSSEDeframer) flush() []byte {
pending := d.buffer
d.buffer = nil
// Trailing CR/LF bytes are line framing only: the partial content before
// them is what remains to be delivered, so a final "data: [DONE]\r" still
// reads as the terminal marker. Interior whitespace (including a
// leading newline) belongs to the payload and is preserved.
pending = bytes.TrimRight(pending, "\r\n")
if len(pending) == 0 {
return nil
}
return pending
}
// canBeginSSEEvent reports whether payload could start an SSE event: a data
// line, an event line, a comment line, or a strict prefix of the "data:" or
// "event:" field names that later bytes may complete. A leading newline
// alone is not enough: bare JSON may open with a newline and must stay one
// untouched payload.
func canBeginSSEEvent(b []byte) bool {
if len(b) == 0 {
return false
}
switch b[0] {
case ':':
return true
case 'd':
return sseFieldPrefixCandidate(b, "data:")
case 'e':
return sseFieldPrefixCandidate(b, "event:")
}
return false
}
func sseFieldPrefixCandidate(b []byte, field string) bool {
if len(b) <= len(field) {
return bytes.HasPrefix([]byte(field), b)
}
return bytes.HasPrefix(b, []byte(field))
}
// isSSEFieldPrefixPartial reports whether b is a strict prefix of "data:" or
// "event:": the field name may be completed by a later chunk, so these bytes
// must stay buffered instead of being released.
func isSSEFieldPrefixPartial(b []byte) bool {
if len(b) == 0 || len(b) >= len("event:") {
return false
}
return bytes.HasPrefix([]byte("data:"), b) || bytes.HasPrefix([]byte("event:"), b)
}
// sseDataLinePrefix reports whether b begins with an SSE data line. The
// space after the colon is optional in SSE, so any payload bytes after
// "data:" count as a data line.
func sseDataLinePrefix(b []byte) bool {
return bytes.HasPrefix(b, []byte("data:"))
}
// takeEvent extracts the next complete SSE event when one is available.
// Comment-only blocks are consumed but not returned: they are SSE
// keep-alives with no data semantics, matching how built-in executors ignore
// ":" lines instead of forwarding them as payloads.
func (d *executorSSEDeframer) takeEvent() ([]byte, bool) {
if len(d.buffer) == 0 {
return nil, false
}
idx := indexSSEFrameBoundary(d.buffer)
if idx < 0 {
return nil, false
}
end := idx + 1
event := bytes.Clone(d.buffer[:end])
d.advance(end)
if isSSECommentOnlyEvent(event) {
return nil, true
}
return event, true
}
func (d *executorSSEDeframer) advance(n int) {
rest := d.buffer[n:]
if len(rest) == 0 {
d.buffer = nil
return
}
d.buffer = append(d.buffer[:0], rest...)
}
// deframeExecutorSSEChunks reassembles full SSE events from arbitrary chunk
// boundaries and passes bare payloads through with their chunk boundaries
// intact. It only runs for same-format streams with a registered native
// stream translator.
//
// It never synthesizes a terminal marker: emitTranslatedExecutorStreamTail
// in the translation goroutine owns that responsibility (it feeds one
// [DONE] through TranslateStream at channel close for [DONE]-terminated
// formats). Emitting a second synthetic marker here would run plugin
// response hooks twice and could surface two terminal frames.
func deframeExecutorSSEChunks(ctx context.Context, in <-chan pluginapi.ExecutorStreamChunk) <-chan pluginapi.ExecutorStreamChunk {
if in == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
out := make(chan pluginapi.ExecutorStreamChunk)
go func() {
defer close(out)
deframer := &executorSSEDeframer{}
emit := func(payloads [][]byte) bool {
for _, payload := range payloads {
if len(payload) == 0 {
continue
}
if !sendExecutorPluginStreamChunk(ctx, out, pluginapi.ExecutorStreamChunk{Payload: payload}) {
return false
}
}
return true
}
for {
select {
case <-ctx.Done():
return
case chunk, ok := <-in:
if !ok {
if tail := deframer.flush(); len(tail) > 0 {
_ = emit([][]byte{tail})
}
return
}
if chunk.Err != nil {
// Buffered partial events belong to the failed prefix;
// emit them before the error is forwarded.
if tail := deframer.flush(); len(tail) > 0 {
if !emit([][]byte{tail}) {
return
}
}
_ = sendExecutorPluginStreamChunk(ctx, out, chunk)
continue
}
if !emit(deframer.feed(chunk.Payload)) {
return
}
}
}
}()
return out
}
// indexSSEFrameBoundary scans complete lines and returns the index of the LF
// ending the blank line that terminates an SSE event, or -1 while the event
// is incomplete. Handling line by line keeps LF, CRLF, and mixed endings
// correct, including a CRLF blank line split across chunk boundaries.
func indexSSEFrameBoundary(b []byte) int {
pos := 0
for {
idx := bytes.IndexByte(b[pos:], '\n')
if idx < 0 {
return -1
}
lineEnd := pos + idx
if isBlankSSELine(b[pos:lineEnd]) {
return lineEnd
}
pos = lineEnd + 1
}
}
func isBlankSSELine(line []byte) bool {
line = bytes.TrimSuffix(line, []byte{'\r'})
return len(line) == 0
}
func isSSECommentOnlyEvent(event []byte) bool {
sawComment := false
for _, line := range bytes.Split(event, []byte("\n")) {
trimmed := bytes.TrimSpace(line)
if len(trimmed) == 0 {
continue
}
if !bytes.HasPrefix(trimmed, []byte(":")) {
return false
}
sawComment = true
}
return sawComment
}