feat: implement skill debug resume functionality and related API endpoints

This commit is contained in:
mlogclub
2026-04-10 17:27:44 +08:00
parent 1087ddca44
commit 97f0df17b6
6 changed files with 292 additions and 0 deletions
+94
View File
@@ -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
}
@@ -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)
}
@@ -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"`
}
@@ -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)
}
@@ -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<AIAgent[]>([])
const [result, setResult] = useState<SkillDebugRunResult | null>(null)
const [resumeResult, setResumeResult] = useState<SkillDebugRunResult | null>(null)
const [resumeMessage, setResumeMessage] = useState("")
const form = useForm<
z.input<typeof debugFormSchema>,
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 (
<ProjectDialog
open={open}
@@ -408,6 +454,104 @@ function DebugDialogBody({
<ResultBlock title="Graph Tool Trace" value={result?.graphToolTrace} />
<ResultBlock title="Trace Data" value={result?.traceData} />
</div>
{result?.interrupted && result.checkPointId ? (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg bg-muted/50 p-3 text-sm">
<div className="text-xs text-muted-foreground"> Checkpoint</div>
<div className="mt-1 break-all">{result.checkPointId}</div>
</div>
<Field>
<FieldLabel htmlFor="skill-debug-resume-message"></FieldLabel>
<FieldContent>
<Textarea
id="skill-debug-resume-message"
rows={3}
placeholder="输入确认、取消或其他恢复消息"
value={resumeMessage}
onChange={(event) => setResumeMessage(event.target.value)}
/>
</FieldContent>
</Field>
<div className="flex flex-wrap gap-2">
{quickResumeActions.map((item) => (
<Button
key={item.value}
type="button"
variant="outline"
disabled={resuming}
onClick={() => void handleResumeDebug(item.value)}
>
{item.label}
</Button>
))}
<Button
type="button"
disabled={resuming}
onClick={() => void handleResumeDebug()}
>
{resuming ? (
<LoaderCircleIcon className="animate-spin" />
) : (
<PlayIcon />
)}
{resuming ? "恢复中..." : "恢复调试"}
</Button>
</div>
</CardContent>
</Card>
) : null}
{resumeResult ? (
<div className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm"></CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{resumeResult.skillCode || skillCode}</Badge>
{resumeResult.graphToolCode ? (
<Badge variant="secondary">{resumeResult.graphToolCode}</Badge>
) : null}
{resumeResult.interruptType ? (
<Badge variant="secondary">{resumeResult.interruptType}</Badge>
) : null}
{resumeResult.interrupted ? (
<Badge></Badge>
) : (
<Badge variant="outline"></Badge>
)}
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 whitespace-pre-wrap break-words">
{resumeMessage || "暂无"}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 whitespace-pre-wrap break-words">
{resumeResult.replyText || "暂无"}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground"> Plan Reason</div>
<div className="mt-1 whitespace-pre-wrap break-words">
{resumeResult.planReason || "暂无"}
</div>
</div>
</CardContent>
</Card>
<ResultBlock title="恢复 Tool Search Trace" value={resumeResult.toolSearchTrace} />
<ResultBlock title="恢复 Graph Tool Trace" value={resumeResult.graphToolTrace} />
<ResultBlock title="恢复 Trace Data" value={resumeResult.traceData} />
</div>
) : null}
</div>
</ProjectDialog>
)
+14
View File
@@ -308,6 +308,13 @@ export type SkillDebugRunPayload = {
userMessage: string
}
export type SkillDebugResumePayload = {
aiAgentId: number
conversationId?: number
checkPointId: string
userMessage: string
}
export type SkillDebugRunResult = {
skillCode: string
skillName: string
@@ -916,6 +923,13 @@ export function debugRunSkillDefinition(payload: SkillDebugRunPayload) {
})
}
export function debugResumeSkillDefinition(payload: SkillDebugResumePayload) {
return request<SkillDebugRunResult>("/api/console/skill-definition/debug_resume", {
method: "POST",
body: JSON.stringify(payload),
})
}
export function testMCPConnection(serverCode: string) {
return request<MCPConnectionResult>("/api/console/mcp/test_connection", {
method: "POST",