diff --git a/internal/ai/runtime/debug_run.go b/internal/ai/runtime/debug_run.go index 2699814..b73be5d 100644 --- a/internal/ai/runtime/debug_run.go +++ b/internal/ai/runtime/debug_run.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "cs-agent/internal/ai/runtime/graphs" "cs-agent/internal/models" "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/dto/response" @@ -14,6 +15,7 @@ import ( func init() { svc.SkillDebugRunHook = DebugRunSkill + svc.SkillDebugResumeHook = DebugResumeSkill } func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) { @@ -55,6 +57,71 @@ func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*resp return buildSkillDebugRunResponse(req, summary, selectedSkill), nil } +func DebugResumeSkill(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) { + aiAgent := svc.AIAgentService.Get(req.AIAgentID) + if aiAgent == nil || aiAgent.Status != enums.StatusOk { + return nil, errorsx.InvalidParam("AI Agent不存在或未启用") + } + aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) + if aiConfig == nil { + return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") + } + pendingInterrupt := svc.ConversationInterruptService.GetByCheckPointID(strings.TrimSpace(req.CheckPointID)) + if pendingInterrupt == nil { + return nil, errorsx.InvalidParam("CheckPoint 不存在") + } + if pendingInterrupt.AIAgentID > 0 && pendingInterrupt.AIAgentID != req.AIAgentID { + return nil, errorsx.InvalidParam("CheckPoint 与 AI Agent 不匹配") + } + conversationID := req.ConversationID + if conversationID <= 0 { + conversationID = pendingInterrupt.ConversationID + } + if conversationID <= 0 { + return nil, errorsx.InvalidParam("会话不存在") + } + conversation := svc.ConversationService.Get(conversationID) + if conversation == nil { + return nil, errorsx.InvalidParam("会话不存在") + } + if conversation.AIAgentID > 0 && conversation.AIAgentID != req.AIAgentID { + return nil, errorsx.InvalidParam("会话与 AI Agent 不匹配") + } + resumeText := strings.TrimSpace(req.UserMessage) + summary, err := Service.Resume(ctx, ResumeRequest{ + Conversation: conversation, + AIAgent: aiAgent, + AIConfig: aiConfig, + CheckPointID: strings.TrimSpace(req.CheckPointID), + ResumeData: map[string]any{ + strings.TrimSpace(pendingInterrupt.InterruptID): resumeText, + }, + }) + if err != nil { + if isCheckpointMissingError(err) { + summary = &Summary{ + Status: "expired", + ReplyText: graphs.ConfirmationExpiredReply, + } + if pendingInterrupt.ID > 0 { + _ = svc.ConversationInterruptService.MarkExpired(pendingInterrupt.ID, 0) + } + return buildSkillDebugResumeResponse(req, summary, conversationID), nil + } + return buildSkillDebugResumeResponse(req, summary, conversationID), err + } + if pendingInterrupt.ID > 0 { + if summary != nil && summary.Interrupted { + _ = svc.ConversationInterruptService.MarkPendingAgain(pendingInterrupt.ID, firstInterruptID(summary), resolveInterruptPrompt(summary), 0) + } else if summary != nil && graphs.IsCancellationReply(summary.ReplyText) { + _ = svc.ConversationInterruptService.MarkCancelled(pendingInterrupt.ID, 0) + } else { + _ = svc.ConversationInterruptService.MarkResolved(pendingInterrupt.ID, 0) + } + } + return buildSkillDebugResumeResponse(req, summary, conversationID), nil +} + func buildSkillDebugRunResponse(req request.SkillDebugRunRequest, summary *Summary, skill *models.SkillDefinition) *response.SkillDebugRunResponse { resp := &response.SkillDebugRunResponse{ ConversationID: req.ConversationID, @@ -86,3 +153,30 @@ func buildSkillDebugRunResponse(req request.SkillDebugRunRequest, summary *Summa resp.ErrorMessage = summary.ErrorMessage return resp } + +func buildSkillDebugResumeResponse(req request.SkillDebugResumeRequest, summary *Summary, conversationID int64) *response.SkillDebugRunResponse { + resp := &response.SkillDebugRunResponse{ + ConversationID: conversationID, + AIAgentID: req.AIAgentID, + } + if summary == nil { + return resp + } + resp.SkillCode = strings.TrimSpace(summary.PlannedSkillCode) + resp.SkillName = strings.TrimSpace(summary.PlannedSkillName) + resp.ReplyText = summary.ReplyText + resp.PlanReason = summary.PlanReason + resp.SkillRouteTrace = summary.SkillRouteTrace + resp.SkillAllowedToolCodes = append([]string(nil), summary.SkillAllowedToolCodes...) + resp.ToolCodes = append([]string(nil), summary.ToolCodes...) + resp.InvokedToolCodes = append([]string(nil), summary.InvokedToolCodes...) + resp.ToolSearchTrace = extractToolSearchTrace(summary) + resp.GraphToolTrace = extractGraphToolTrace(summary) + resp.GraphToolCode = firstGraphToolCode(summary) + resp.InterruptType = firstInterruptType(summary) + resp.CheckPointID = summary.CheckPointID + resp.Interrupted = summary.Interrupted + resp.TraceData = summary.TraceData + resp.ErrorMessage = summary.ErrorMessage + return resp +} diff --git a/internal/controllers/console/skill_definition_controller.go b/internal/controllers/console/skill_definition_controller.go index d2dfede..2a73139 100644 --- a/internal/controllers/console/skill_definition_controller.go +++ b/internal/controllers/console/skill_definition_controller.go @@ -186,3 +186,19 @@ func (c *SkillDefinitionController) PostDebug_run() *web.JsonResult { } return web.JsonData(resp) } + +func (c *SkillDefinitionController) PostDebug_resume() *web.JsonResult { + if _, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionSkillDefinitionView); err != nil { + return web.JsonError(err) + } + + req := request.SkillDebugResumeRequest{} + if err := params.ReadJSON(c.Ctx, &req); err != nil { + return web.JsonError(err) + } + resp, err := services.SkillRuntimeService.DebugResume(context.Background(), req) + if err != nil { + return web.JsonError(err) + } + return web.JsonData(resp) +} diff --git a/internal/pkg/dto/request/skill_request.go b/internal/pkg/dto/request/skill_request.go index 30336db..2a7ca59 100644 --- a/internal/pkg/dto/request/skill_request.go +++ b/internal/pkg/dto/request/skill_request.go @@ -37,3 +37,10 @@ type SkillDebugRunRequest struct { SkillCode string `json:"skillCode"` UserMessage string `json:"userMessage"` } + +type SkillDebugResumeRequest struct { + AIAgentID int64 `json:"aiAgentId"` + ConversationID int64 `json:"conversationId"` + CheckPointID string `json:"checkPointId"` + UserMessage string `json:"userMessage"` +} diff --git a/internal/services/skill_runtime_service.go b/internal/services/skill_runtime_service.go index 6fdf5eb..da51800 100644 --- a/internal/services/skill_runtime_service.go +++ b/internal/services/skill_runtime_service.go @@ -12,6 +12,7 @@ import ( var SkillRuntimeService = newSkillRuntimeService() var SkillDebugRunHook func(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) +var SkillDebugResumeHook func(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) func newSkillRuntimeService() *skillRuntimeService { return &skillRuntimeService{} @@ -34,3 +35,19 @@ func (s *skillRuntimeService) DebugRun(ctx context.Context, req request.SkillDeb } return SkillDebugRunHook(ctx, req) } + +func (s *skillRuntimeService) DebugResume(ctx context.Context, req request.SkillDebugResumeRequest) (*response.SkillDebugRunResponse, error) { + if req.AIAgentID <= 0 { + return nil, errorsx.InvalidParam("aiAgentId不能为空") + } + if strings.TrimSpace(req.CheckPointID) == "" { + return nil, errorsx.InvalidParam("checkPointId不能为空") + } + if strings.TrimSpace(req.UserMessage) == "" { + return nil, errorsx.InvalidParam("userMessage不能为空") + } + if SkillDebugResumeHook == nil { + return nil, fmt.Errorf("skill debug resume runner is not initialized") + } + return SkillDebugResumeHook(ctx, req) +} diff --git a/web/app/(console)/skill-definition/_components/debug-dialog.tsx b/web/app/(console)/skill-definition/_components/debug-dialog.tsx index 0325cf0..5f577a9 100644 --- a/web/app/(console)/skill-definition/_components/debug-dialog.tsx +++ b/web/app/(console)/skill-definition/_components/debug-dialog.tsx @@ -21,9 +21,11 @@ import { import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { + debugResumeSkillDefinition, debugRunSkillDefinition, fetchAIAgentsAll, type AIAgent, + type SkillDebugResumePayload, type SkillDebugRunPayload, type SkillDebugRunResult, } from "@/lib/api/admin" @@ -55,6 +57,11 @@ const emptyForm: DebugForm = { userMessage: "", } +const quickResumeActions = [ + { label: "确认", value: "确认" }, + { label: "取消", value: "取消" }, +] + function ResultBlock({ title, value, @@ -111,8 +118,11 @@ function DebugDialogBody({ }: DebugDialogProps) { const formId = `skill-debug-form-${skillCode}` const [running, setRunning] = useState(false) + const [resuming, setResuming] = useState(false) const [aiAgents, setAiAgents] = useState([]) const [result, setResult] = useState(null) + const [resumeResult, setResumeResult] = useState(null) + const [resumeMessage, setResumeMessage] = useState("") const form = useForm< z.input, undefined, @@ -152,6 +162,8 @@ function DebugDialogBody({ } reset(emptyForm) setResult(null) + setResumeResult(null) + setResumeMessage("") }, [open, reset]) useEffect(() => { @@ -190,6 +202,8 @@ function DebugDialogBody({ try { const data = await debugRunSkillDefinition(payload) setResult(data) + setResumeResult(null) + setResumeMessage("") } catch (error) { toast.error(error instanceof Error ? error.message : "Skill 调试失败") setResult(null) @@ -198,6 +212,38 @@ function DebugDialogBody({ } } + async function handleResumeDebug(messageText?: string) { + const nextMessage = (messageText ?? resumeMessage).trim() + if (!result?.checkPointId || !result.interrupted) { + return + } + if (!nextMessage) { + toast.error("请输入恢复消息") + return + } + const payload: SkillDebugResumePayload = { + aiAgentId: Number(selectedAgentId || result.aiAgentId), + checkPointId: result.checkPointId, + userMessage: nextMessage, + } + const conversationId = result.conversationId || Number(watch("conversationId")) + if (conversationId > 0) { + payload.conversationId = conversationId + } + + setResuming(true) + try { + const data = await debugResumeSkillDefinition(payload) + setResumeResult(data) + setResumeMessage(nextMessage) + } catch (error) { + toast.error(error instanceof Error ? error.message : "恢复调试失败") + setResumeResult(null) + } finally { + setResuming(false) + } + } + return ( + + {result?.interrupted && result.checkPointId ? ( + + + 恢复调试 + + +
+
当前 Checkpoint
+
{result.checkPointId}
+
+ + 恢复消息 + +