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
@@ -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",