feat(agent): 用对话起草巡检(schedule.draft_check + 起草巡检 Skill + 计划页入口与对话卡片)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 00:04:31 +08:00
1 parent 8eb8a68295
commit 7c4ad901d9
14 files changed
+408 -7

No files matched your search

@@ -0,0 +1,94 @@
import { LoaderCircle, Sparkles } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { useCreateAgentRun } from "~/api/agent.ts";
import { FormField } from "~/components/console/form-field.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { Button } from "~/components/ui/button.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog.tsx";
import { Textarea } from "~/components/ui/textarea.tsx";
import { errorMessage } from "~/lib/api.ts";
// 用对话起草巡检:用户一句话描述需求 → 发起智能体对话(起草巡检 Skill)→ 智能体建成未启用的巡检,
// 对话里给「检查并启用」卡片,用户在计划页检查、修改后启用。
const EXAMPLES = [
"每天早上 9 点前看看有没有失败或卡住的下载,有就写成待处理",
"每隔 6 小时检查一下站点登录有没有失效",
"每周汇报一次订阅追更情况和缺集",
];
export function DraftCheckDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {
const [text, setText] = useState("");
const create = useCreateAgentRun();
const navigate = useNavigate();
const submit = async () => {
const need = text.trim();
if (need.length < 4) return;
try {
const run = await create.mutateAsync({ goal: `帮我起草一个巡检:${need}` });
onOpenChange(false);
navigate(consolePath("agent", run.id));
} catch (err) {
toast.error(errorMessage(err, "发起对话失败"));
}
};
return (
<Dialog open={open} onOpenChange={(o) => !create.isPending && onOpenChange(o)}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>用对话起草巡检</DialogTitle>
<DialogDescription>
说说想定期检查什么、多久一次。智能体会建一个未启用的巡检,你检查后再启用。
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
void submit();
}}
>
<FormField label="需求" htmlFor="draft-need" required>
<Textarea
id="draft-need"
rows={4}
autoFocus
value={text}
placeholder={EXAMPLES[0]}
onChange={(e) => setText(e.target.value)}
/>
</FormField>
<div className="mb-3 flex flex-wrap gap-1.5">
{EXAMPLES.map((x) => (
<button
key={x}
type="button"
onClick={() => setText(x)}
className="rounded-full border border-border/70 px-2.5 py-1 text-left text-meta text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground"
>
{x}
</button>
))}
</div>
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)} disabled={create.isPending}>
取消
</Button>
<Button type="submit" disabled={create.isPending || text.trim().length < 4}>
{create.isPending ? <LoaderCircle className="animate-spin" /> : <Sparkles />}
开始起草
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { SCHEDULE_CHANGED_EVENT, type ScheduledJob } from "@umt/contracts";
import { AlertTriangle, Bot, CalendarClock, Plus } from "lucide-react";
import { AlertTriangle, Bot, CalendarClock, Plus, Sparkles } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import { usePiUpstreamModels, usePiUpstreams } from "~/api/pi.ts";
@@ -14,6 +14,7 @@ import { useSse } from "~/lib/sse.ts";
import { AgentCheckCard, type DefaultUpstream } from "./agent-check-card.tsx";
import { AgentCheckDialog } from "./agent-check-dialog.tsx";
import { AutoTaskList } from "./auto-task-list.tsx";
import { DraftCheckDialog } from "./draft-check-dialog.tsx";
import { formatUsd } from "./labels.ts";
import { RunsSheet } from "./runs-sheet.tsx";
@@ -101,6 +102,7 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
const now = useNow();
const { hash } = useLocation();
const [editing, setEditing] = useState<{ job: ScheduledJob | null; key: number } | null>(null);
const [drafting, setDrafting] = useState(false);
const [history, setHistory] = useState<ScheduledJob | null>(null);
const data = board.data;
@@ -169,10 +171,16 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
: "到点让智能体按目标检查并汇报。"
}
actions={
<Button size="sm" onClick={() => setEditing({ job: null, key: Date.now() })}>
<Plus />
新建巡检
</Button>
<>
<Button size="sm" variant="outline" onClick={() => setDrafting(true)}>
<Sparkles />
用对话起草
</Button>
<Button size="sm" onClick={() => setEditing({ job: null, key: Date.now() })}>
<Plus />
新建巡检
</Button>
</>
}
/>
{checks.length === 0 ? (
@@ -218,6 +226,7 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
{editing ? (
<AgentCheckDialog key={editing.key} open job={editing.job} onOpenChange={(o) => !o && setEditing(null)} />
) : null}
{drafting ? <DraftCheckDialog open onOpenChange={setDrafting} /> : null}
<RunsSheet job={history} onClose={() => setHistory(null)} />
</>
);