592 lines
19 KiB
Go
592 lines
19 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"oci-portal/internal/aiwire"
|
|
)
|
|
|
|
// respRejectStateful 拒绝有状态特性(网关无状态)。
|
|
func respRejectStateful(req aiwire.RespRequest) error {
|
|
if req.PreviousResponseID != "" {
|
|
return fmt.Errorf("previous_response_id 不支持:网关不保存历史响应,请在 input 中自带完整上下文(store:false 模式)")
|
|
}
|
|
if len(req.Conversation) > 0 && string(req.Conversation) != "null" {
|
|
return fmt.Errorf("conversation 不支持:网关不保存对话状态")
|
|
}
|
|
if req.Background != nil && *req.Background {
|
|
return fmt.Errorf("background 模式不支持")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RespPassthroughValidate 校验直通请求:模型必填,有状态特性不支持,工具类型
|
|
// 放行 function、Oracle 文档化的服务端工具(web_search / x_search /
|
|
// code_interpreter / mcp)与 codex 特有形态(namespace 工具分组、custom 自由
|
|
// 格式,上游不识别,转发前拍平/转换)。
|
|
func RespPassthroughValidate(req aiwire.RespRequest) error {
|
|
if strings.TrimSpace(req.Model) == "" {
|
|
return fmt.Errorf("model 不能为空")
|
|
}
|
|
if err := respRejectStateful(req); err != nil {
|
|
return err
|
|
}
|
|
for _, t := range req.Tools {
|
|
switch t.Type {
|
|
case "function", "web_search", "x_search", "code_interpreter", "mcp", "namespace", "custom", "tool_search":
|
|
default:
|
|
return fmt.Errorf("不支持的工具类型 %q:服务端工具仅支持 web_search / x_search / code_interpreter / mcp", t.Type)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RespNsRef 一条拍平映射:限定名对应的原 namespace 与短名。codex 的回程路由
|
|
// 依赖 function_call 项上的 name+namespace 双字段,响应侧按此映射还原。
|
|
type RespNsRef struct {
|
|
Namespace string
|
|
Name string
|
|
}
|
|
|
|
// RespCompat 是直通请求 codex 兼容改写的结果:Flattened/Converted/Dropped 供
|
|
// 调用方记日志;NsRefs / CustomNames 非空表示响应侧需做工具调用项还原。
|
|
type RespCompat struct {
|
|
Flattened []string
|
|
Converted []string
|
|
Dropped []string
|
|
NsRefs map[string]RespNsRef
|
|
CustomNames map[string]struct{}
|
|
}
|
|
|
|
// NeedRestore 报告响应是否需要做工具调用项还原。
|
|
func (c RespCompat) NeedRestore() bool {
|
|
return len(c.NsRefs) > 0 || len(c.CustomNames) > 0
|
|
}
|
|
|
|
// RespPassthroughBody 以原始请求体为基构造上游 body:强制 store:false(禁上游
|
|
// 存态),stream 原样保留(流式直通);用 json.Number 保真未知字段与数值。
|
|
// 同时做 codex 兼容改写(namespace 拍平、多轮历史与 tool_choice 重限定)。
|
|
func RespPassthroughBody(raw []byte) ([]byte, RespCompat, error) {
|
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
|
dec.UseNumber()
|
|
var body map[string]any
|
|
if err := dec.Decode(&body); err != nil {
|
|
return nil, RespCompat{}, fmt.Errorf("解析请求体: %w", err)
|
|
}
|
|
body["store"] = false
|
|
compat := respCompatTools(body)
|
|
respCompatInputCalls(body)
|
|
respCompatToolChoice(body)
|
|
out, err := json.Marshal(body)
|
|
return out, compat, err
|
|
}
|
|
|
|
// respCompatTools 对 tools 做 codex 兼容改写(上游实测行为见任务档案):
|
|
// namespace 工具组上游 422,拆平为限定名 function 工具;custom(自由格式)上游
|
|
// 同样 422,apply_patch 丢弃(grok 系未训练该补丁格式,失败编辑不如 shell 回退),
|
|
// 其余转 function 包装;web_search 的 external_web_access 参数上游 400,false 是
|
|
// "仅缓存检索"降权模式,按不越权原则连工具剥离,true 等价默认行为仅删键;
|
|
// 剔空后删 tools,并连删 tool_choice 与 parallel_tool_calls(上游拒绝无 tools
|
|
// 带 tool_choice)。
|
|
func respCompatTools(body map[string]any) RespCompat {
|
|
tools, ok := body["tools"].([]any)
|
|
if !ok {
|
|
return RespCompat{}
|
|
}
|
|
compat := RespCompat{NsRefs: map[string]RespNsRef{}, CustomNames: map[string]struct{}{}}
|
|
kept := make([]any, 0, len(tools))
|
|
for _, item := range tools {
|
|
tool, _ := item.(map[string]any)
|
|
switch {
|
|
case tool != nil && tool["type"] == "namespace":
|
|
ns, _ := tool["name"].(string)
|
|
compat.Flattened = append(compat.Flattened, "namespace:"+ns)
|
|
kept = append(kept, respFlattenNsTool(ns, tool, &compat)...)
|
|
continue
|
|
case tool != nil && tool["type"] == "custom":
|
|
name, _ := tool["name"].(string)
|
|
if name == "apply_patch" {
|
|
compat.Dropped = append(compat.Dropped, "custom:apply_patch")
|
|
continue
|
|
}
|
|
respConvertCustomTool(tool)
|
|
if name != "" {
|
|
compat.CustomNames[name] = struct{}{}
|
|
compat.Converted = append(compat.Converted, "custom:"+name)
|
|
}
|
|
kept = append(kept, tool)
|
|
continue
|
|
}
|
|
if desc, drop := respToolDrop(tool); drop {
|
|
compat.Dropped = append(compat.Dropped, desc)
|
|
continue
|
|
}
|
|
kept = append(kept, item)
|
|
}
|
|
if len(kept) == 0 {
|
|
delete(body, "tools")
|
|
delete(body, "tool_choice")
|
|
delete(body, "parallel_tool_calls")
|
|
} else {
|
|
body["tools"] = kept
|
|
}
|
|
return compat
|
|
}
|
|
|
|
// respConvertCustomTool 把 custom(自由格式)工具改写为 function:补 input 包装
|
|
// schema(custom 工具本无 parameters),模型以 {"input": 文本} 形态调用,响应侧
|
|
// 按 CustomNames 回转;custom 专有的 format(语法约束)字段一并移除。
|
|
func respConvertCustomTool(tool map[string]any) {
|
|
tool["type"] = "function"
|
|
delete(tool, "format")
|
|
if _, ok := tool["parameters"]; !ok {
|
|
tool["parameters"] = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"input": map[string]any{"type": "string"}},
|
|
"required": []any{"input"},
|
|
}
|
|
}
|
|
}
|
|
|
|
// respFlattenNsTool 把 namespace 组内子工具上提为顶层工具:限定改名、缺
|
|
// parameters 补空 schema(上游必填),映射写入 NsRefs 供响应侧还原;非
|
|
// function 子工具(如 custom)上游同样不认,剥离并记录。
|
|
func respFlattenNsTool(ns string, tool map[string]any, compat *RespCompat) []any {
|
|
children, _ := tool["tools"].([]any)
|
|
out := make([]any, 0, len(children))
|
|
for _, c := range children {
|
|
child, _ := c.(map[string]any)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
short, _ := child["name"].(string)
|
|
if t, _ := child["type"].(string); t != "" && t != "function" {
|
|
compat.Dropped = append(compat.Dropped, ns+"."+short+"(type="+t+")")
|
|
continue
|
|
}
|
|
qualified := respQualifyNsName(ns, short)
|
|
if qualified == "" {
|
|
continue
|
|
}
|
|
child["name"] = qualified
|
|
if _, ok := child["parameters"]; !ok {
|
|
child["parameters"] = map[string]any{"type": "object", "properties": map[string]any{}}
|
|
}
|
|
compat.NsRefs[qualified] = RespNsRef{Namespace: ns, Name: short}
|
|
out = append(out, child)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// respQualifyNsName 生成拍平后的限定名,与响应侧还原互逆(对齐 CLIProxyAPI):
|
|
// mcp__ 开头的子工具名自带全局前缀不再限定;ns 以 __ 结尾直接拼接;已带前缀
|
|
// 不重复添加。
|
|
func respQualifyNsName(ns, name string) string {
|
|
ns, name = strings.TrimSpace(ns), strings.TrimSpace(name)
|
|
if ns == "" || name == "" || strings.HasPrefix(name, "mcp__") {
|
|
return name
|
|
}
|
|
prefix := ns
|
|
if !strings.HasSuffix(prefix, "__") {
|
|
prefix += "__"
|
|
}
|
|
if strings.HasPrefix(name, prefix) {
|
|
return name
|
|
}
|
|
return prefix + name
|
|
}
|
|
|
|
// respCompatInputCalls 改写多轮历史 input:function_call 的 namespace 字段重限定
|
|
// (上游不认识该字段);custom_tool_call / custom_tool_call_output 转换为上游
|
|
// 认识的 function_call(_output) 形态(上游 item 变体表不含 custom 系)。
|
|
func respCompatInputCalls(body map[string]any) {
|
|
input, _ := body["input"].([]any)
|
|
for _, it := range input {
|
|
item, _ := it.(map[string]any)
|
|
if item == nil {
|
|
continue
|
|
}
|
|
switch item["type"] {
|
|
case "function_call":
|
|
respQualifyCallField(item)
|
|
case "custom_tool_call":
|
|
item["type"] = "function_call"
|
|
item["arguments"] = respCustomCallArguments(item["input"])
|
|
delete(item, "input")
|
|
case "custom_tool_call_output":
|
|
item["type"] = "function_call_output"
|
|
item["output"] = respCustomCallOutput(item["output"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// respCustomCallArguments 把 custom_tool_call 的 input 包装为 function_call 的
|
|
// arguments JSON 串(对齐 CLIProxyAPI):input 串本身是 JSON 对象则直用,普通
|
|
// 文本包一层 {"input": 文本};对象取序列化;缺失为 {}。与响应侧
|
|
// respUnwrapCustomInput 互逆,保证多轮往返无损。
|
|
func respCustomCallArguments(input any) string {
|
|
switch v := input.(type) {
|
|
case string:
|
|
trimmed := strings.TrimSpace(v)
|
|
var probe map[string]any
|
|
if json.Unmarshal([]byte(trimmed), &probe) == nil && probe != nil {
|
|
return trimmed
|
|
}
|
|
enc, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "{}"
|
|
}
|
|
return `{"input":` + string(enc) + `}`
|
|
case nil:
|
|
return "{}"
|
|
default:
|
|
enc, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "{}"
|
|
}
|
|
return string(enc)
|
|
}
|
|
}
|
|
|
|
// respCustomCallOutput 归一 output 为字符串:非字符串时序列化保底。
|
|
func respCustomCallOutput(output any) any {
|
|
if _, ok := output.(string); ok {
|
|
return output
|
|
}
|
|
if output == nil {
|
|
return ""
|
|
}
|
|
enc, err := json.Marshal(output)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(enc)
|
|
}
|
|
|
|
// respCompatToolChoice 限定化对象形态的 tool_choice 及其 allowed_tools 列表
|
|
// (上游不认 tool_choice 上的 namespace 字段)。
|
|
func respCompatToolChoice(body map[string]any) {
|
|
tc, _ := body["tool_choice"].(map[string]any)
|
|
if tc == nil {
|
|
return
|
|
}
|
|
if tc["type"] == "function" {
|
|
respQualifyCallField(tc)
|
|
}
|
|
list, _ := tc["tools"].([]any)
|
|
for _, it := range list {
|
|
if sub, _ := it.(map[string]any); sub != nil && sub["type"] == "function" {
|
|
respQualifyCallField(sub)
|
|
}
|
|
}
|
|
}
|
|
|
|
// respQualifyCallField 把一个带 name/namespace 的对象改写为限定名形态。
|
|
func respQualifyCallField(m map[string]any) {
|
|
ns, _ := m["namespace"].(string)
|
|
if strings.TrimSpace(ns) == "" {
|
|
return
|
|
}
|
|
name, _ := m["name"].(string)
|
|
if q := respQualifyNsName(ns, name); q != "" {
|
|
m["name"] = q
|
|
}
|
|
delete(m, "namespace")
|
|
}
|
|
|
|
// respToolDrop 判定单个工具是否剥离。web_search 的 external_web_access 参数
|
|
// 上游 400:false 表示 OpenAI 的"仅缓存检索"降权模式,上游无对应能力,按不
|
|
// 越权原则整个工具剥离;true 等价上游默认行为,仅删键放行。tool_search(codex
|
|
// 的工具目录搜索)上游不识别,且 namespace 已全量拍平上送、搜索语义冗余,剥离。
|
|
func respToolDrop(tool map[string]any) (string, bool) {
|
|
if tool == nil {
|
|
return "", false
|
|
}
|
|
switch tool["type"] {
|
|
case "tool_search":
|
|
return "tool_search", true
|
|
case "web_search":
|
|
access, has := tool["external_web_access"]
|
|
if !has {
|
|
return "", false
|
|
}
|
|
delete(tool, "external_web_access")
|
|
if access == false {
|
|
return "web_search(external_web_access=false)", true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// RespGuardBytes 返回请求体中 instructions 与 tools 两字段的原始字节数之和,
|
|
// 流式保险丝据此判定。上游对二者合计 >≈64.5KB 的流式请求会在推理阶段静默断流
|
|
// (纯 EOF,input 正文不计入;2026-07-16 实测仍存在),阈值由设置页 AI Tab 配置。
|
|
// 解析失败返回 0(放行,交由上游正常报错)。
|
|
func RespGuardBytes(body []byte) int {
|
|
var probe struct {
|
|
Instructions json.RawMessage `json:"instructions"`
|
|
Tools json.RawMessage `json:"tools"`
|
|
}
|
|
if err := json.Unmarshal(body, &probe); err != nil {
|
|
return 0
|
|
}
|
|
return len(probe.Instructions) + len(probe.Tools)
|
|
}
|
|
|
|
// RespInjectGrokTools 为 xai. 前缀模型默认注入服务端搜索工具:开关开启且请求
|
|
// tools 中不存在同名工具时追加 {"type":"web_search"} / {"type":"x_search"};
|
|
// 已存在(含任意参数形态)不覆盖。返回改写后 body 与注入清单(观测日志用);
|
|
// 模型不匹配、两开关全关或解析失败时原样返回。
|
|
func RespInjectGrokTools(body []byte, model string, web, x bool) ([]byte, []string) {
|
|
if !strings.HasPrefix(model, "xai.") || (!web && !x) {
|
|
return body, nil
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(body)))
|
|
dec.UseNumber()
|
|
var m map[string]any
|
|
if err := dec.Decode(&m); err != nil {
|
|
return body, nil
|
|
}
|
|
tools, _ := m["tools"].([]any)
|
|
missing := map[string]bool{"web_search": web, "x_search": x}
|
|
for _, t := range tools {
|
|
tool, ok := t.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if typ, _ := tool["type"].(string); missing[typ] {
|
|
missing[typ] = false
|
|
}
|
|
}
|
|
var injected []string
|
|
for _, typ := range []string{"web_search", "x_search"} {
|
|
if missing[typ] {
|
|
tools = append(tools, map[string]any{"type": typ})
|
|
injected = append(injected, typ)
|
|
}
|
|
}
|
|
if len(injected) == 0 {
|
|
return body, nil
|
|
}
|
|
m["tools"] = tools
|
|
out, err := json.Marshal(m)
|
|
if err != nil {
|
|
return body, nil
|
|
}
|
|
return out, injected
|
|
}
|
|
|
|
// RespDisableStream 把请求体的 stream 改为 false(流式升级回退用),其余字段
|
|
// 原样保留。
|
|
func RespDisableStream(body []byte) ([]byte, error) {
|
|
dec := json.NewDecoder(strings.NewReader(string(body)))
|
|
dec.UseNumber()
|
|
var m map[string]any
|
|
if err := dec.Decode(&m); err != nil {
|
|
return nil, fmt.Errorf("解析请求体: %w", err)
|
|
}
|
|
m["stream"] = false
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
// RespSynthSSEEvents 把一份非流式响应合成为最小 SSE 事件序列:created →
|
|
// 每个输出项一条 output_item.done → completed。流式升级回退用,客户端拿到
|
|
// 完整事件语义但无增量;输出项与 usage 原样承载。
|
|
func RespSynthSSEEvents(payload []byte) ([][]byte, error) {
|
|
dec := json.NewDecoder(strings.NewReader(string(payload)))
|
|
dec.UseNumber()
|
|
var resp map[string]any
|
|
if err := dec.Decode(&resp); err != nil {
|
|
return nil, fmt.Errorf("解析上游响应: %w", err)
|
|
}
|
|
output, _ := resp["output"].([]any)
|
|
status, hadStatus := resp["status"]
|
|
resp["status"], resp["output"] = "in_progress", []any{}
|
|
created, err := json.Marshal(map[string]any{"type": "response.created", "response": resp})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hadStatus {
|
|
resp["status"] = status
|
|
} else {
|
|
delete(resp, "status")
|
|
}
|
|
resp["output"] = output
|
|
events := [][]byte{created}
|
|
for i, item := range output {
|
|
ev, err := json.Marshal(map[string]any{"type": "response.output_item.done", "output_index": i, "item": item})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, ev)
|
|
}
|
|
completed, err := json.Marshal(map[string]any{"type": "response.completed", "response": resp})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(events, completed), nil
|
|
}
|
|
|
|
// RespRestoreToolCalls 非流式响应还原:output 数组中命中 NsRefs 的 function_call
|
|
// 把限定名还原为短名并补回 namespace 字段(codex 以该双字段路由);命中
|
|
// CustomNames 的回转 custom_tool_call(codex 期望 input 字段形态)。无需还原或
|
|
// 解析失败时返回原字节,不破坏直通。
|
|
func RespRestoreToolCalls(payload []byte, compat RespCompat) []byte {
|
|
if !compat.NeedRestore() {
|
|
return payload
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(payload)))
|
|
dec.UseNumber()
|
|
var body map[string]any
|
|
if dec.Decode(&body) != nil {
|
|
return payload
|
|
}
|
|
if !respRestoreOutput(body["output"], compat) {
|
|
return payload
|
|
}
|
|
out, err := json.Marshal(body)
|
|
if err != nil {
|
|
return payload
|
|
}
|
|
return out
|
|
}
|
|
|
|
// RespRestoreToolCallsEvent 流式单事件还原:处理事件顶层 item(output_item.*)
|
|
// 与 response.output(created/completed 等快照)。返回还原后数据与是否改动;
|
|
// 未改动时调用方转发原始行,保持字节级直通。
|
|
func RespRestoreToolCallsEvent(data []byte, compat RespCompat) ([]byte, bool) {
|
|
if !compat.NeedRestore() {
|
|
return data, false
|
|
}
|
|
dec := json.NewDecoder(strings.NewReader(string(data)))
|
|
dec.UseNumber()
|
|
var ev map[string]any
|
|
if dec.Decode(&ev) != nil {
|
|
return data, false
|
|
}
|
|
changed := respRestoreCallItem(ev["item"], compat)
|
|
if resp, _ := ev["response"].(map[string]any); resp != nil {
|
|
changed = respRestoreOutput(resp["output"], compat) || changed
|
|
}
|
|
if !changed {
|
|
return data, false
|
|
}
|
|
out, err := json.Marshal(ev)
|
|
if err != nil {
|
|
return data, false
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// respRestoreOutput 还原一个 output 数组,返回是否有改动。
|
|
func respRestoreOutput(v any, compat RespCompat) bool {
|
|
items, _ := v.([]any)
|
|
changed := false
|
|
for _, it := range items {
|
|
if respRestoreCallItem(it, compat) {
|
|
changed = true
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// respRestoreCallItem 还原单个 function_call 项,返回是否改动。namespace 还原与
|
|
// custom 回转互斥:custom 工具不参与 namespace 拍平。
|
|
func respRestoreCallItem(v any, compat RespCompat) bool {
|
|
item, _ := v.(map[string]any)
|
|
if item == nil || item["type"] != "function_call" {
|
|
return false
|
|
}
|
|
name, _ := item["name"].(string)
|
|
if ref, ok := compat.NsRefs[name]; ok {
|
|
item["name"] = ref.Name
|
|
item["namespace"] = ref.Namespace
|
|
return true
|
|
}
|
|
if _, ok := compat.CustomNames[name]; ok {
|
|
item["type"] = "custom_tool_call"
|
|
item["input"] = respUnwrapCustomInput(item["arguments"])
|
|
delete(item, "arguments")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// respUnwrapCustomInput 解包 arguments:形如 {"input": 串} 取其 input(非串取该
|
|
// 值原文),其余情况整串返回。与请求侧 respCustomCallArguments 互逆。
|
|
func respUnwrapCustomInput(arguments any) string {
|
|
s, _ := arguments.(string)
|
|
var probe map[string]json.RawMessage
|
|
if json.Unmarshal([]byte(s), &probe) == nil {
|
|
if raw, ok := probe["input"]; ok {
|
|
var str string
|
|
if json.Unmarshal(raw, &str) == nil {
|
|
return str
|
|
}
|
|
return string(raw)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// RespPassthroughUsage 从直通响应提取用量;缺失时返回 nil(日志记零)。
|
|
func RespPassthroughUsage(payload []byte) *aiwire.Usage {
|
|
var root struct {
|
|
Usage *aiwire.RespUsage `json:"usage"`
|
|
}
|
|
if json.Unmarshal(payload, &root) != nil {
|
|
return nil
|
|
}
|
|
return usageFromResp(root.Usage)
|
|
}
|
|
|
|
// usageFromResp 换算 Responses usage 为 OpenAI 口径(缓存命中透传);nil 原样返回。
|
|
func usageFromResp(u *aiwire.RespUsage) *aiwire.Usage {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
usage := &aiwire.Usage{PromptTokens: u.InputTokens,
|
|
CompletionTokens: u.OutputTokens, TotalTokens: u.TotalTokens}
|
|
if cached := u.InputTokensDetails.CachedTokens; cached > 0 {
|
|
usage.PromptTokensDetails = &aiwire.PromptTokensDetails{CachedTokens: cached}
|
|
}
|
|
return usage
|
|
}
|
|
|
|
// RespStreamCompletedUsage 从一行 SSE data JSON 中提取 response.completed 事件的
|
|
// usage;非 completed 事件或解析失败返回 nil。流式直通逐行喂入,最后一次非 nil 生效。
|
|
func RespStreamCompletedUsage(data []byte) *aiwire.Usage {
|
|
var ev struct {
|
|
Type string `json:"type"`
|
|
Response json.RawMessage `json:"response"`
|
|
}
|
|
if json.Unmarshal(data, &ev) != nil || ev.Type != "response.completed" || len(ev.Response) == 0 {
|
|
return nil
|
|
}
|
|
return RespPassthroughUsage(ev.Response)
|
|
}
|
|
|
|
// RespStreamErrorMsg 从一行 SSE data JSON 中提取 error / response.failed 事件的
|
|
// 错误消息;非错误事件返回空串。流式直通据此把上游错误写入调用日志。
|
|
func RespStreamErrorMsg(data []byte) string {
|
|
var ev respStreamEvent
|
|
if json.Unmarshal(data, &ev) != nil {
|
|
return ""
|
|
}
|
|
switch ev.Type {
|
|
case "error":
|
|
if ev.Message != "" {
|
|
return ev.Message
|
|
}
|
|
return "上游返回错误事件 error"
|
|
case "response.failed":
|
|
if m := respErrorMsg(ev.Response); m != "" {
|
|
return m
|
|
}
|
|
return "上游返回错误事件 response.failed"
|
|
}
|
|
return ""
|
|
}
|