From 201f3aac656aeb682dfdfd7a625a361bf686e96b Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Tue, 15 Sep 2026 05:22:07 +0800
Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E4=BF=AE=E5=A4=8D=E6=B5=81?=
=?UTF-8?q?=E5=BC=8F=E6=8E=A8=E7=90=86=E4=B8=8E=E5=B7=A5=E5=85=B7=E5=8F=82?=
=?UTF-8?q?=E6=95=B0=E9=87=8D=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/cloud_agent_approval_preview.go | 2 +-
backend/internal/app/cloud_agent_json.go | 10 +++
backend/internal/app/cloud_agent_runtime.go | 9 ++
backend/internal/app/cloud_agent_stream.go | 66 ++++++++++++++
backend/internal/app/cloud_agent_tools.go | 5 +-
backend/internal/app/provider.go | 39 ++++----
.../internal/app/provider_agent_protocol.go | 21 +++--
backend/internal/app/provider_protocol.go | 8 ++
backend/internal/app/provider_text.go | 89 ++++++++++++++-----
backend/internal/app/task_text_stream.go | 9 +-
backend/internal/repository/cloud_agent.go | 6 ++
.../canvas/canvas-cloud-agent-chat-ui.tsx | 7 ++
.../canvas/canvas-cloud-agent-panel.tsx | 6 ++
13 files changed, 231 insertions(+), 46 deletions(-)
create mode 100644 backend/internal/app/cloud_agent_stream.go
diff --git a/backend/internal/app/cloud_agent_approval_preview.go b/backend/internal/app/cloud_agent_approval_preview.go
index 580437cfd..bf6e57ce1 100644
--- a/backend/internal/app/cloud_agent_approval_preview.go
+++ b/backend/internal/app/cloud_agent_approval_preview.go
@@ -54,7 +54,7 @@ type cloudAgentCanvasMutationPlan struct {
func prepareCloudAgentCanvasMutation(repo *repository.Repository, userID, canvasID string, call cloudAgentCall) (*cloudAgentCanvasMutationPlan, error) {
var args agentCanvasArgs
if err := decodeCloudAgentJSONObject(call.Function.Arguments, &args); err != nil {
- return nil, BadAuthRequest("画布工具参数必须是只含支持字段的单个JSON对象")
+ return nil, canvasArgumentError()
}
if len(args.Ops) < 1 || len(args.Ops) > 20 || args.SnapshotHash == "" {
return nil, BadAuthRequest("画布操作数量或快照无效")
diff --git a/backend/internal/app/cloud_agent_json.go b/backend/internal/app/cloud_agent_json.go
index 00fc62182..d8cfa7dea 100644
--- a/backend/internal/app/cloud_agent_json.go
+++ b/backend/internal/app/cloud_agent_json.go
@@ -9,6 +9,16 @@ import (
var errCloudAgentJSONSingleObject = errors.New("参数必须是单个 JSON 对象")
+// Only syntax/schema errors may be repaired by the model; authorization and
+// unsupported mutations still fail admission before any write or approval.
+type cloudAgentArgumentError struct{ error }
+
+func (e *cloudAgentArgumentError) Unwrap() error { return e.error }
+
+func canvasArgumentError() error {
+ return &cloudAgentArgumentError{BadAuthRequest("画布工具参数无效:仅允许一个 JSON 对象;顶层只含 snapshotHash 和 ops,snapshotHash 不得放入 ops。请按工具 schema 修正后重试")}
+}
+
// decodeCloudAgentJSONObject is used for model tool arguments. Tool arguments
// are an untrusted protocol boundary: reject non-objects, unknown fields and
// trailing JSON instead of silently accepting an ambiguous payload.
diff --git a/backend/internal/app/cloud_agent_runtime.go b/backend/internal/app/cloud_agent_runtime.go
index a6c1c578a..31c88cd28 100644
--- a/backend/internal/app/cloud_agent_runtime.go
+++ b/backend/internal/app/cloud_agent_runtime.go
@@ -507,6 +507,7 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
}
var result struct {
Text string `json:"text"`
+ Reasoning string `json:"reasoning"`
ToolCalls []cloudAgentCall `json:"toolCalls"`
Legacy []cloudAgentCall `json:"tool_calls"`
}
@@ -534,6 +535,9 @@ func (s *Service) advanceCloudAgent(run *model.CloudAgentExecution) (err error)
return cloudAgentSave(current, &state)
}
calls := result.ToolCalls
+ if result.Reasoning != "" {
+ state.event(run.ID, "reasoning_message", map[string]any{"messageId": task.ID + ":reasoning", "text": truncateRunes(result.Reasoning, 8000)})
+ }
if result.Text != "" {
state.event(run.ID, "assistant_message", map[string]any{"messageId": task.ID, "text": result.Text})
if len(calls) == 0 {
@@ -784,6 +788,11 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
} else {
canvasPlan, err := prepareCloudAgentCanvasMutation(repo, run.UserID, state.Request.CanvasID, call)
if err != nil {
+ var argumentErr *cloudAgentArgumentError
+ if errors.As(err, &argumentErr) {
+ cloudAgentToolResult(run.ID, state, call, nil, err)
+ return cloudAgentSave(current, state)
+ }
var appErr *AppError
if errors.As(err, &appErr) && appErr != nil {
return failCloudAgentAdmission(current, state, run.ID, err)
diff --git a/backend/internal/app/cloud_agent_stream.go b/backend/internal/app/cloud_agent_stream.go
new file mode 100644
index 000000000..71432bc1b
--- /dev/null
+++ b/backend/internal/app/cloud_agent_stream.go
@@ -0,0 +1,66 @@
+package app
+
+import (
+ "errors"
+ "unicode/utf8"
+
+ "gorm.io/gorm"
+ "infinite-canvas/backend/internal/model"
+ "infinite-canvas/backend/internal/repository"
+)
+
+// Publish upstream deltas to the same durable, revisioned transcript used by
+// Agent SSE/replay. Each flush re-reads state; never overwrite a scheduler's
+// checkpoint with the snapshot from when the model request started.
+func newCloudAgentStreamPublisher(s *Service, userID, taskID, kind string) *taskTextStreamPublisher {
+ p := newTaskTextStreamPublisher(s, userID, taskID)
+ written := 0
+ p.sink = func(delta string) error {
+ remaining := 32000 - written
+ if remaining <= 0 {
+ return nil
+ }
+ if len(delta) > remaining {
+ delta = delta[:remaining]
+ for !utf8.ValidString(delta) {
+ delta = delta[:len(delta)-1]
+ }
+ }
+ if delta == "" {
+ return nil
+ }
+ for attempt := 0; attempt < 3; attempt++ {
+ run, err := s.repo.CloudAgentForActiveTask(userID, taskID)
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ err = s.repo.MutateCloudAgent(userID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
+ state, err := cloudAgentDecode(current)
+ if err != nil {
+ return err
+ }
+ if state.ActiveTaskID != taskID || (current.Status != "running" && current.Status != "queued") {
+ return nil
+ }
+ messageID := taskID
+ if kind == "reasoning_delta" {
+ messageID += ":reasoning"
+ }
+ state.event(run.ID, kind, map[string]any{"messageId": messageID, "text": delta})
+ return cloudAgentSave(current, &state)
+ })
+ if errors.Is(err, repository.ErrCreationConflict) {
+ continue
+ }
+ if err == nil {
+ written += len(delta)
+ }
+ return err
+ }
+ return repository.ErrCreationConflict
+ }
+ return p
+}
diff --git a/backend/internal/app/cloud_agent_tools.go b/backend/internal/app/cloud_agent_tools.go
index bc154ead0..46c6ccff5 100644
--- a/backend/internal/app/cloud_agent_tools.go
+++ b/backend/internal/app/cloud_agent_tools.go
@@ -1,6 +1,7 @@
package app
import (
+ "crypto/sha256"
"encoding/json"
"fmt"
"sort"
@@ -103,7 +104,9 @@ func cloudAgentCanonical(system string, history []providerTextMessage, prompt st
messages = append(messages, map[string]any{"role": m.Role, "content": m.Content})
}
messages = append(messages, map[string]any{"role": "user", "content": prompt})
- return canonicalAgentRequest{SystemPrompt: system, Messages: messages, Tools: cloudAgentTools(req), ToolChoice: "auto"}
+ // Keep routing stable across tool turns, without exposing canvas identifiers.
+ cacheHash := sha256.Sum256([]byte(req.CanvasID + "\x00" + system))
+ return canonicalAgentRequest{SystemPrompt: system, Messages: messages, Tools: cloudAgentTools(req), ToolChoice: "auto", PromptCacheKey: fmt.Sprintf("cloud-agent:%x", cacheHash[:24])}
}
func cloudAgentTools(req CloudAgentRequest) []map[string]any {
tools := []map[string]any{}
diff --git a/backend/internal/app/provider.go b/backend/internal/app/provider.go
index 2f1f7b2d9..654f122d5 100644
--- a/backend/internal/app/provider.go
+++ b/backend/internal/app/provider.go
@@ -25,22 +25,23 @@ import (
var sseFrameBoundaryPattern = regexp.MustCompile(`\r?\n\r?\n`)
type canvasGenerationInput struct {
- Mode string `json:"mode"`
- Prompt string `json:"prompt"`
- Config providerConfig `json:"config"`
- ReferenceImages []providerMedia `json:"referenceImages"`
- ReferenceVideos []providerMedia `json:"referenceVideos"`
- ReferenceAudios []providerMedia `json:"referenceAudios"`
- TextHistory []providerTextMessage `json:"textHistory"`
- Mask *providerMedia `json:"mask"`
- Metadata map[string]interface{} `json:"metadata"`
- AgentRequests *agentToolRequests `json:"agentRequests"`
- TextOptions canvasTextOptions `json:"textOptions"`
- ImageCapability *ImageCapabilityConfig `json:"-"`
- StreamText bool `json:"-"` // 分镜请求使用上游 SSE 保活;最终结构仍在流结束后统一校验。
- MaxOutputTokens int `json:"-"`
- OnTextDelta func(string) `json:"-"`
- VideoCapability *VideoCapabilityConfig `json:"-"`
+ Mode string `json:"mode"`
+ Prompt string `json:"prompt"`
+ Config providerConfig `json:"config"`
+ ReferenceImages []providerMedia `json:"referenceImages"`
+ ReferenceVideos []providerMedia `json:"referenceVideos"`
+ ReferenceAudios []providerMedia `json:"referenceAudios"`
+ TextHistory []providerTextMessage `json:"textHistory"`
+ Mask *providerMedia `json:"mask"`
+ Metadata map[string]interface{} `json:"metadata"`
+ AgentRequests *agentToolRequests `json:"agentRequests"`
+ TextOptions canvasTextOptions `json:"textOptions"`
+ ImageCapability *ImageCapabilityConfig `json:"-"`
+ StreamText bool `json:"-"` // 分镜请求使用上游 SSE 保活;最终结构仍在流结束后统一校验。
+ MaxOutputTokens int `json:"-"`
+ OnTextDelta func(string) `json:"-"`
+ OnReasoningDelta func(string) `json:"-"`
+ VideoCapability *VideoCapabilityConfig `json:"-"`
}
type canvasTextOptions struct {
@@ -359,6 +360,12 @@ func (s *Service) processCanvasGenerationTask(ctx context.Context, userID string
}
if input.Mode == "text" && strings.HasPrefix(taskType, "canvas_text") && input.StreamText {
textPublisher = newTaskTextStreamPublisher(s, userID, taskExecutionID(ctx))
+ if input.AgentRequests != nil {
+ textPublisher = newCloudAgentStreamPublisher(s, userID, taskExecutionID(ctx), "assistant_delta")
+ reasoningPublisher := newCloudAgentStreamPublisher(s, userID, taskExecutionID(ctx), "reasoning_delta")
+ input.OnReasoningDelta = reasoningPublisher.Publish
+ defer reasoningPublisher.Close()
+ }
input.OnTextDelta = textPublisher.Publish
defer textPublisher.Close()
}
diff --git a/backend/internal/app/provider_agent_protocol.go b/backend/internal/app/provider_agent_protocol.go
index ec97a4a37..96dd9d6ca 100644
--- a/backend/internal/app/provider_agent_protocol.go
+++ b/backend/internal/app/provider_agent_protocol.go
@@ -11,10 +11,11 @@ import (
// The browser sends one protocol-neutral conversation. Only the selected
// provider body is materialized; declarative plugins retain their request paths.
type canonicalAgentRequest struct {
- Messages []map[string]interface{} `json:"messages"`
- Tools []map[string]interface{} `json:"tools"`
- ToolChoice interface{} `json:"toolChoice"`
- SystemPrompt string `json:"systemPrompt"`
+ Messages []map[string]interface{} `json:"messages"`
+ Tools []map[string]interface{} `json:"tools"`
+ ToolChoice interface{} `json:"toolChoice"`
+ SystemPrompt string `json:"systemPrompt"`
+ PromptCacheKey string `json:"promptCacheKey,omitempty"`
}
func expandCanonicalAgentRequest(source *canonicalAgentRequest, config providerConfig, declarative bool) (*agentToolRequests, error) {
@@ -155,7 +156,11 @@ func canonicalAgentChatBody(source *canonicalAgentRequest, claude bool) map[stri
if named, ok := choice.(map[string]interface{}); ok {
choice = map[string]interface{}{"type": "function", "function": map[string]interface{}{"name": named["name"]}}
}
- return map[string]interface{}{"messages": messages, "tools": tools, "tool_choice": choice, "parallel_tool_calls": false}
+ body := map[string]interface{}{"messages": messages, "tools": tools, "tool_choice": choice, "parallel_tool_calls": false}
+ if !claude && source.PromptCacheKey != "" {
+ body["prompt_cache_key"] = source.PromptCacheKey
+ }
+ return body
}
func canonicalAgentResponsesBody(source *canonicalAgentRequest) map[string]interface{} {
@@ -183,7 +188,11 @@ func canonicalAgentResponsesBody(source *canonicalAgentRequest) map[string]inter
converted["type"] = "function"
tools = append(tools, converted)
}
- return map[string]interface{}{"input": messages, "tools": tools, "tool_choice": source.ToolChoice, "parallel_tool_calls": false}
+ body := map[string]interface{}{"input": messages, "tools": tools, "tool_choice": source.ToolChoice, "parallel_tool_calls": false}
+ if source.PromptCacheKey != "" {
+ body["prompt_cache_key"] = source.PromptCacheKey
+ }
+ return body
}
func canonicalAgentGeminiBody(source *canonicalAgentRequest) map[string]interface{} {
diff --git a/backend/internal/app/provider_protocol.go b/backend/internal/app/provider_protocol.go
index c4b01106b..2e98b1098 100644
--- a/backend/internal/app/provider_protocol.go
+++ b/backend/internal/app/provider_protocol.go
@@ -319,6 +319,10 @@ func executeProtocolRequest(ctx context.Context, config providerConfig, spec pro
// method/path/body/auth;最终 URL 校验、凭证注入、SSRF、超时、大小限制和审计仍由宿主统一执行,
// 插件不能通过自定义请求规格绕过这些安全约束。
func executeProtocolBinaryRequest(ctx context.Context, config providerConfig, spec protocol.RequestSpec) ([]byte, string, error) {
+ return executeProtocolBinaryRequestWithConsumer(ctx, config, spec, nil)
+}
+
+func executeProtocolBinaryRequestWithConsumer(ctx context.Context, config providerConfig, spec protocol.RequestSpec, consume func(string, []byte)) ([]byte, string, error) {
if err := spec.Validate(); err != nil {
return nil, "", err
}
@@ -345,6 +349,10 @@ func executeProtocolBinaryRequest(ctx context.Context, config providerConfig, sp
if err := applyProtocolAuth(req, config, spec.Auth); err != nil {
return nil, "", err
}
+ if consume != nil {
+ req.Header.Set("Accept", "text/event-stream")
+ return doBinaryWithConsumer(req, consume)
+ }
return doBinary(req)
}
diff --git a/backend/internal/app/provider_text.go b/backend/internal/app/provider_text.go
index 30ca6da32..a31fac246 100644
--- a/backend/internal/app/provider_text.go
+++ b/backend/internal/app/provider_text.go
@@ -79,7 +79,7 @@ func runAgentToolTask(ctx context.Context, input canvasGenerationInput) (map[str
func postAgentRequest(ctx context.Context, input canvasGenerationInput, path string, body map[string]interface{}, protocol string) (map[string]interface{}, error) {
if input.StreamText {
- return postStreamingAgent(ctx, input.Config, path, body, protocol, input.OnTextDelta)
+ return postStreamingAgent(ctx, input.Config, path, body, protocol, input.OnTextDelta, input.OnReasoningDelta)
}
delete(body, "stream")
var payload map[string]interface{}
@@ -90,7 +90,12 @@ func postAgentRequest(ctx context.Context, input canvasGenerationInput, path str
}
func runDeclarativeAgentTask(ctx context.Context, input canvasGenerationInput, adapter protocol.AgentAdapter) (map[string]interface{}, error) {
- if input.TextOptions.Thinking {
+ wire := input.Config.InterfaceType
+ if wire == string(model.ChannelInterfaceOpenAIResponse) {
+ wire = "responses"
+ }
+ knownWire := wire == "chat-completion" || wire == "responses" || wire == "claude-api"
+ if input.TextOptions.Thinking && !knownWire {
return nil, errors.New("当前声明式 Agent 渠道尚不支持思考模式,请关闭思考模式或切换内置协议渠道")
}
if input.AgentRequests == nil {
@@ -106,6 +111,37 @@ func runDeclarativeAgentTask(ctx context.Context, input canvasGenerationInput, a
if err != nil {
return nil, err
}
+ if knownWire {
+ body := protocolBodyObject(spec.Body)
+ if body == nil {
+ return nil, errors.New("声明式 Agent 请求体必须是 JSON 对象")
+ }
+ applyTextThinking(body, input, wire)
+ spec.Body = body
+ if input.StreamText {
+ body["stream"] = true
+ if wire == "chat-completion" {
+ if err := ensureChatCompletionStreamUsage(body); err != nil {
+ return nil, err
+ }
+ }
+ parser := newStreamingAgentParser(wire, input.OnTextDelta)
+ parser.emitReasoning = input.OnReasoningDelta
+ data, mime, err := executeProtocolBinaryRequestWithConsumer(ctx, input.Config, spec, parser.consume)
+ if err != nil {
+ return nil, err
+ }
+ if strings.Contains(strings.ToLower(mime), "event-stream") {
+ parser.flush()
+ return parser.result()
+ }
+ var payload map[string]interface{}
+ if err := json.Unmarshal(data, &payload); err != nil {
+ return nil, fmt.Errorf("Agent 接口返回格式无效:%w", err)
+ }
+ return parseAgentToolPayload(payload, wire)
+ }
+ }
body, err := executeProtocolRequest(ctx, input.Config, spec)
if err != nil {
return nil, err
@@ -330,17 +366,17 @@ func parseAgentToolPayload(payload map[string]interface{}, protocol string) (map
return result, nil
}
-func postStreamingAgent(ctx context.Context, config providerConfig, path string, body map[string]interface{}, protocol string, onDelta func(string)) (map[string]interface{}, error) {
+func postStreamingAgent(ctx context.Context, config providerConfig, path string, body map[string]interface{}, protocol string, onDelta func(string), onReasoning ...func(string)) (map[string]interface{}, error) {
body["stream"] = true
if protocol == "chat-completion" {
- metadata, _ := ctx.Value(providerAnalyticsKey{}).(providerAnalyticsContext)
- if metadata.BillingMode == "token" {
- if err := ensureChatCompletionStreamUsage(body); err != nil {
- return nil, err
- }
+ if err := ensureChatCompletionStreamUsage(body); err != nil {
+ return nil, err
}
}
parser := newStreamingAgentParser(protocol, onDelta)
+ if len(onReasoning) > 0 {
+ parser.emitReasoning = onReasoning[0]
+ }
data, mimeType, err := postStreamingBinary(ctx, config, path, body, parser.consume)
if err != nil {
return nil, err
@@ -363,15 +399,16 @@ type streamingAgentToolCall struct {
}
type streamingAgentParser struct {
- protocol string
- buffer string
- text strings.Builder
- reasoning strings.Builder
- toolCalls map[int]*streamingAgentToolCall
- toolCallByID map[string]int
- completed map[string]interface{}
- err error
- emit func(string)
+ protocol string
+ buffer string
+ text strings.Builder
+ reasoning strings.Builder
+ toolCalls map[int]*streamingAgentToolCall
+ toolCallByID map[string]int
+ completed map[string]interface{}
+ err error
+ emit func(string)
+ emitReasoning func(string)
}
func newStreamingAgentParser(protocol string, emit func(string)) *streamingAgentParser {
@@ -448,7 +485,7 @@ func (p *streamingAgentParser) consumeResponsesEvent(eventName string, payload m
case "response.output_text.delta", "output_text.delta":
p.appendText(stringField(payload, "delta"))
case "response.reasoning.delta", "response.reasoning_text.delta", "response.reasoning_summary_text.delta":
- p.reasoning.WriteString(stringField(payload, "delta"))
+ p.appendReasoning(stringField(payload, "delta"))
case "response.completed":
p.completed, _ = payload["response"].(map[string]interface{})
case "response.output_item.added":
@@ -482,7 +519,7 @@ func (p *streamingAgentParser) consumeChatCompletionEvent(payload map[string]int
choice, _ := value.(map[string]interface{})
delta, _ := choice["delta"].(map[string]interface{})
p.appendText(streamContentText(delta["content"]))
- p.reasoning.WriteString(firstNonEmptyString(stringField(delta, "reasoning_content"), stringField(delta, "reasoning"), stringField(delta, "reasoning_text")))
+ p.appendReasoning(firstNonEmptyString(stringField(delta, "reasoning_content"), stringField(delta, "reasoning"), stringField(delta, "reasoning_text")))
for fallbackIndex, toolValue := range interfaceSlice(delta["tool_calls"]) {
tool, _ := toolValue.(map[string]interface{})
index := intField(tool, "index", fallbackIndex)
@@ -509,7 +546,7 @@ func (p *streamingAgentParser) consumeClaudeEvent(payload map[string]interface{}
case "text":
p.appendText(stringField(block, "text"))
case "thinking":
- p.reasoning.WriteString(firstNonEmptyString(stringField(block, "thinking"), stringField(block, "text")))
+ p.appendReasoning(firstNonEmptyString(stringField(block, "thinking"), stringField(block, "text")))
case "tool_use":
arguments := ""
if input := block["input"]; input != nil {
@@ -526,7 +563,7 @@ func (p *streamingAgentParser) consumeClaudeEvent(payload map[string]interface{}
p.appendText(stringField(delta, "text"))
}
if stringField(delta, "type") == "thinking_delta" {
- p.reasoning.WriteString(firstNonEmptyString(stringField(delta, "thinking"), stringField(delta, "text")))
+ p.appendReasoning(firstNonEmptyString(stringField(delta, "thinking"), stringField(delta, "text")))
}
if stringField(delta, "type") == "input_json_delta" {
p.toolCall(index).arguments += stringField(delta, "partial_json")
@@ -551,6 +588,16 @@ func (p *streamingAgentParser) appendText(delta string) {
}
}
+func (p *streamingAgentParser) appendReasoning(delta string) {
+ if delta == "" {
+ return
+ }
+ p.reasoning.WriteString(delta)
+ if p.emitReasoning != nil {
+ p.emitReasoning(delta)
+ }
+}
+
func (p *streamingAgentParser) toolCall(index int) *streamingAgentToolCall {
if index < 0 {
index = 0
diff --git a/backend/internal/app/task_text_stream.go b/backend/internal/app/task_text_stream.go
index e59e08abc..2c558f32a 100644
--- a/backend/internal/app/task_text_stream.go
+++ b/backend/internal/app/task_text_stream.go
@@ -23,6 +23,7 @@ type taskTextStreamPublisher struct {
timer *time.Timer
disabled bool
closed bool
+ sink func(string) error
}
func newTaskTextStreamPublisher(service *Service, userID string, taskID string) *taskTextStreamPublisher {
@@ -77,7 +78,13 @@ func (p *taskTextStreamPublisher) flushLocked() {
}
content := p.buffer.String()
p.buffer.Reset()
- if _, err := p.service.AppendTaskTextDelta(p.userID, p.taskID, content); err != nil {
+ var err error
+ if p.sink != nil {
+ err = p.sink(content)
+ } else {
+ _, err = p.service.AppendTaskTextDelta(p.userID, p.taskID, content)
+ }
+ if err != nil {
p.disabled = true
_ = p.service.log(p.userID, p.taskID, "warn", "文本流持久化已降级", err.Error())
}
diff --git a/backend/internal/repository/cloud_agent.go b/backend/internal/repository/cloud_agent.go
index 3b3a09c13..bf5bda5fb 100644
--- a/backend/internal/repository/cloud_agent.go
+++ b/backend/internal/repository/cloud_agent.go
@@ -16,6 +16,12 @@ func (r *Repository) CloudAgent(userID, id string) (*model.CloudAgentExecution,
err := r.db.First(&run, "id = ? AND user_id = ?", id, userID).Error
return &run, err
}
+
+func (r *Repository) CloudAgentForActiveTask(userID, taskID string) (*model.CloudAgentExecution, error) {
+ var run model.CloudAgentExecution
+ err := r.db.Where("user_id = ? AND active_task_id = ? AND status IN ?", userID, taskID, []string{"running", "queued"}).First(&run).Error
+ return &run, err
+}
func (r *Repository) CloudAgentRoots() ([]model.Task, error) {
var tasks []model.Task
err := r.db.Where("operation = ? AND id NOT IN (SELECT id FROM cloud_agent_executions)", "cloud_agent").Order("created_at").Limit(50).Find(&tasks).Error
diff --git a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx
index 508ec3905..49e9fe269 100644
--- a/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-chat-ui.tsx
@@ -30,6 +30,7 @@ export type CloudAgentChatMessage = {
title?: string;
text: string;
streaming?: boolean;
+ reasoning?: boolean;
meta?: string;
detail?: unknown;
attachments?: CloudAgentChatAttachment[];
@@ -91,6 +92,12 @@ export function AgentChatMessage({
const isUser = item.role === "user";
const isSystem = item.role === "system";
const color = item.role === "error" ? "#ef4444" : theme.node.text;
+ if (item.reasoning) {
+ return {item.streaming ? "模型推理中" : "模型推理摘要"}
+