2026-03-30 14:09:15 +00:00
|
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
|
|
import "encoding/json"
|
|
|
|
|
|
|
|
|
|
|
|
type StreamParser func(line string)
|
|
|
|
|
|
|
2026-04-01 00:53:34 +00:00
|
|
|
|
type Parser struct {
|
|
|
|
|
|
Parse StreamParser
|
|
|
|
|
|
Flush func()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateStreamParser 建立串流解析器(向後相容,不傳遞 thinking)
|
|
|
|
|
|
func CreateStreamParser(onText func(string), onDone func()) Parser {
|
|
|
|
|
|
return CreateStreamParserWithThinking(onText, nil, onDone)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateStreamParserWithThinking 建立串流解析器,支援思考過程輸出。
|
|
|
|
|
|
// onThinking 可為 nil,表示忽略思考過程。
|
|
|
|
|
|
func CreateStreamParserWithThinking(onText func(string), onThinking func(string), onDone func()) Parser {
|
|
|
|
|
|
accumulatedText := ""
|
|
|
|
|
|
accumulatedThinking := ""
|
2026-03-30 14:09:15 +00:00
|
|
|
|
done := false
|
|
|
|
|
|
|
2026-04-01 00:53:34 +00:00
|
|
|
|
parse := func(line string) {
|
2026-03-30 14:09:15 +00:00
|
|
|
|
if done {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var obj struct {
|
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Subtype string `json:"subtype"`
|
|
|
|
|
|
Message *struct {
|
|
|
|
|
|
Content []struct {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
|
Thinking string `json:"thinking"`
|
2026-03-30 14:09:15 +00:00
|
|
|
|
} `json:"content"`
|
|
|
|
|
|
} `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := json.Unmarshal([]byte(line), &obj); err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if obj.Type == "assistant" && obj.Message != nil {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
fullText := ""
|
|
|
|
|
|
fullThinking := ""
|
2026-03-30 14:09:15 +00:00
|
|
|
|
for _, p := range obj.Message.Content {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
switch p.Type {
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
if p.Text != "" {
|
|
|
|
|
|
fullText += p.Text
|
|
|
|
|
|
}
|
|
|
|
|
|
case "thinking":
|
|
|
|
|
|
if p.Thinking != "" {
|
|
|
|
|
|
fullThinking += p.Thinking
|
|
|
|
|
|
}
|
2026-03-30 14:09:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
|
|
|
|
|
|
// 處理思考過程 delta
|
|
|
|
|
|
if onThinking != nil && fullThinking != "" {
|
|
|
|
|
|
if fullThinking == accumulatedThinking {
|
|
|
|
|
|
// 重複的完整思考文字,跳過
|
|
|
|
|
|
} else if len(fullThinking) > len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking {
|
|
|
|
|
|
delta := fullThinking[len(accumulatedThinking):]
|
|
|
|
|
|
onThinking(delta)
|
|
|
|
|
|
accumulatedThinking = fullThinking
|
|
|
|
|
|
} else {
|
|
|
|
|
|
onThinking(fullThinking)
|
|
|
|
|
|
accumulatedThinking += fullThinking
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 處理一般文字 delta
|
|
|
|
|
|
if fullText == "" {
|
2026-03-30 14:09:15 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
// 若此訊息文字等於已累積內容(重複的完整文字),跳過
|
|
|
|
|
|
if fullText == accumulatedText {
|
2026-03-30 14:09:15 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
// 若此訊息是已累積內容的延伸,只輸出新的 delta
|
|
|
|
|
|
if len(fullText) > len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText {
|
|
|
|
|
|
delta := fullText[len(accumulatedText):]
|
|
|
|
|
|
onText(delta)
|
|
|
|
|
|
accumulatedText = fullText
|
2026-03-30 14:09:15 +00:00
|
|
|
|
} else {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
// 獨立的 token fragment(一般情況),直接輸出
|
|
|
|
|
|
onText(fullText)
|
|
|
|
|
|
accumulatedText += fullText
|
2026-03-30 14:09:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if obj.Type == "result" && obj.Subtype == "success" {
|
|
|
|
|
|
done = true
|
|
|
|
|
|
onDone()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
|
|
|
|
|
|
flush := func() {
|
|
|
|
|
|
if !done {
|
|
|
|
|
|
done = true
|
|
|
|
|
|
onDone()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Parser{Parse: parse, Flush: flush}
|
2026-03-30 14:09:15 +00:00
|
|
|
|
}
|