fix(canvas): 在线 Agent - 系统模型接入工具任务路由

This commit is contained in:
dlh committed 2026-08-20 09:59:27 +08:00
1 parent 298b5ebf1f
commit 31026a112d
6 files changed
+239 -25

No files matched your search

+81
View File
@@ -35,11 +35,17 @@ type canvasGenerationInput struct {
TextHistory []providerTextMessage `json:"textHistory"`
Mask *providerMedia `json:"mask"`
Metadata map[string]interface{} `json:"metadata"`
AgentRequests *agentToolRequests `json:"agentRequests"`
ImageCapability *ImageCapabilityConfig `json:"-"`
StreamText bool `json:"-"` // 分镜请求使用上游 SSE 保活;最终结构仍在流结束后统一校验。
VideoCapability *VideoCapabilityConfig `json:"-"`
}
type agentToolRequests struct {
Responses map[string]interface{} `json:"responses"`
ChatCompletion map[string]interface{} `json:"chatCompletion"`
}
type providerTextMessage struct {
Role string `json:"role"`
Content string `json:"content"`
@@ -247,6 +253,9 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string
case "image":
return runImageTask(ctx, input)
case "text":
if input.AgentRequests != nil {
return runAgentToolTask(ctx, input)
}
result, taskErr := runTextTask(ctx, input)
if taskErr == nil && promptTemplateOperation != "" {
taskErr = validatePromptTemplateResult(promptTemplateOperation, result)
@@ -261,6 +270,78 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string
}
}
func runAgentToolTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
request := input.AgentRequests.ChatCompletion
path := "/chat/completions"
protocol := "chat-completion"
if input.Config.InterfaceType == string(model.ChannelInterfaceOpenAIResponse) {
request = input.AgentRequests.Responses
path = "/responses"
protocol = "responses"
}
if request == nil {
return nil, errors.New("画布 Agent 工具请求缺少协议参数")
}
body := cloneStringAnyMap(request)
body["model"] = input.Config.Model
var payload map[string]interface{}
if err := postJSON(ctx, input.Config, path, body, &payload); err != nil {
return nil, err
}
return parseAgentToolPayload(payload, protocol)
}
func parseAgentToolPayload(payload map[string]interface{}, protocol string) (map[string]interface{}, error) {
if err := validateTextPayload(payload); err != nil {
return nil, err
}
result := map[string]interface{}{"mode": "text", "text": "", "toolCalls": []interface{}{}}
if protocol == "responses" {
result["text"] = firstNonEmptyString(stringField(payload, "output_text"), extractResponseText(payload))
calls := make([]interface{}, 0)
for _, value := range interfaceSlice(payload["output"]) {
item, _ := value.(map[string]interface{})
if stringField(item, "type") != "function_call" {
continue
}
calls = append(calls, map[string]interface{}{"id": firstNonEmptyString(stringField(item, "call_id"), stringField(item, "id")), "type": "function", "function": map[string]interface{}{"name": stringField(item, "name"), "arguments": stringField(item, "arguments")}})
}
result["toolCalls"] = calls
return result, nil
}
choices := interfaceSlice(payload["choices"])
if len(choices) == 0 {
return nil, errors.New("画布 Agent 接口没有返回 choices")
}
choice, _ := choices[0].(map[string]interface{})
message, _ := choice["message"].(map[string]interface{})
result["text"] = stringField(message, "content")
if reasoning := firstNonEmptyString(stringField(message, "reasoning_content"), stringField(message, "reasoning")); reasoning != "" {
result["reasoning"] = reasoning
}
calls := make([]interface{}, 0)
for _, value := range interfaceSlice(message["tool_calls"]) {
item, _ := value.(map[string]interface{})
function, _ := item["function"].(map[string]interface{})
calls = append(calls, map[string]interface{}{"id": stringField(item, "id"), "type": "function", "function": map[string]interface{}{"name": stringField(function, "name"), "arguments": stringField(function, "arguments")}})
}
result["toolCalls"] = calls
return result, nil
}
func interfaceSlice(value interface{}) []interface{} {
items, _ := value.([]interface{})
return items
}
func cloneStringAnyMap(value map[string]interface{}) map[string]interface{} {
cloned := make(map[string]interface{}, len(value)+1)
for key, item := range value {
cloned[key] = item
}
return cloned
}
type styleExecutionPlanDocument struct {
SchemaVersion int `json:"schemaVersion"`
ProfilePresetID string `json:"profilePresetId"`
+53
View File
@@ -107,6 +107,59 @@ data: [DONE]
}
}
func TestParseAgentToolPayloadSupportsChatCompletions(t *testing.T) {
result, err := parseAgentToolPayload(map[string]interface{}{
"choices": []interface{}{map[string]interface{}{
"message": map[string]interface{}{
"content": "准备读取画布",
"tool_calls": []interface{}{map[string]interface{}{
"id": "call-1",
"function": map[string]interface{}{"name": "canvas_get_state", "arguments": `{}`},
}},
},
}},
}, "chat-completion")
if err != nil {
t.Fatalf("parseAgentToolPayload() error = %v", err)
}
if result["text"] != "准备读取画布" {
t.Fatalf("text = %v", result["text"])
}
calls, _ := result["toolCalls"].([]interface{})
if len(calls) != 1 {
t.Fatalf("toolCalls = %#v", result["toolCalls"])
}
call, _ := calls[0].(map[string]interface{})
function, _ := call["function"].(map[string]interface{})
if call["id"] != "call-1" || function["name"] != "canvas_get_state" || function["arguments"] != `{}` {
t.Fatalf("tool call = %#v", call)
}
}
func TestParseAgentToolPayloadSupportsResponses(t *testing.T) {
result, err := parseAgentToolPayload(map[string]interface{}{
"output": []interface{}{
map[string]interface{}{"type": "message", "content": []interface{}{map[string]interface{}{"type": "output_text", "text": "开始操作"}}},
map[string]interface{}{"type": "function_call", "call_id": "call-2", "name": "canvas_apply_ops", "arguments": `{"ops":[]}`},
},
}, "responses")
if err != nil {
t.Fatalf("parseAgentToolPayload() error = %v", err)
}
if result["text"] != "开始操作" {
t.Fatalf("text = %v", result["text"])
}
calls, _ := result["toolCalls"].([]interface{})
if len(calls) != 1 {
t.Fatalf("toolCalls = %#v", result["toolCalls"])
}
call, _ := calls[0].(map[string]interface{})
function, _ := call["function"].(map[string]interface{})
if call["id"] != "call-2" || function["name"] != "canvas_apply_ops" || function["arguments"] != `{"ops":[]}` {
t.Fatalf("tool call = %#v", call)
}
}
func TestPostStreamingTextSetsStreamHeaders(t *testing.T) {
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {