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 {
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// accumulated 是所有已輸出內容的串接
|
2026-04-01 00:53:34 +00:00
|
|
|
|
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
|
|
|
|
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// 處理思考過程(不因去重而 return,避免跳過同行的文字內容)
|
|
|
|
|
|
if onThinking != nil && fullThinking != "" && fullThinking != accumulatedThinking {
|
|
|
|
|
|
// 增量模式:新內容以 accumulated 為前綴
|
|
|
|
|
|
if len(fullThinking) >= len(accumulatedThinking) && fullThinking[:len(accumulatedThinking)] == accumulatedThinking {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
delta := fullThinking[len(accumulatedThinking):]
|
2026-04-02 13:54:28 +00:00
|
|
|
|
if delta != "" {
|
|
|
|
|
|
onThinking(delta)
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
accumulatedThinking = fullThinking
|
|
|
|
|
|
} else {
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// 獨立片段:直接輸出,但 accumulated 要串接
|
2026-04-01 00:53:34 +00:00
|
|
|
|
onThinking(fullThinking)
|
2026-04-02 13:54:28 +00:00
|
|
|
|
accumulatedThinking = accumulatedThinking + fullThinking
|
2026-04-01 00:53:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// 處理一般文字
|
|
|
|
|
|
if fullText == "" || fullText == accumulatedText {
|
2026-03-30 14:09:15 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// 增量模式:新內容以 accumulated 為前綴
|
|
|
|
|
|
if len(fullText) >= len(accumulatedText) && fullText[:len(accumulatedText)] == accumulatedText {
|
2026-04-01 00:53:34 +00:00
|
|
|
|
delta := fullText[len(accumulatedText):]
|
2026-04-02 13:54:28 +00:00
|
|
|
|
if delta != "" {
|
|
|
|
|
|
onText(delta)
|
|
|
|
|
|
}
|
2026-04-01 00:53:34 +00:00
|
|
|
|
accumulatedText = fullText
|
2026-03-30 14:09:15 +00:00
|
|
|
|
} else {
|
2026-04-02 13:54:28 +00:00
|
|
|
|
// 獨立片段:直接輸出,但 accumulated 要串接
|
2026-04-01 00:53:34 +00:00
|
|
|
|
onText(fullText)
|
2026-04-02 13:54:28 +00:00
|
|
|
|
accumulatedText = 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
|
|
|
|
}
|