From 39def1c20b316f59769c54cf535b3bc5cef59dde Mon Sep 17 00:00:00 2001 From: xuhd Date: Sat, 16 May 2026 15:50:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(skills):=20curator=20=E5=81=A5=E5=A3=AE?= =?UTF-8?q?=E6=80=A7=E5=8A=A0=E5=9B=BA=20+=20=E8=AF=84=E5=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=8F=AF=E9=85=8D=E7=BD=AE=20+=20=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调度健壮性: - 技能进化总开关 skill_evolution.enabled 真正生效;CuratorConfig 与 CuratorSubConfig 默认 enabled 改为 false(显式 opt-in) - 新增 per-user 运行锁(.curator_state 的 running/started_at), 防定时器与手动触发并发重复运行;6h stale 检测防进程残留死锁 - maybe_run_curator 改用 ThreadPoolExecutor 限并发(上限 5), 防首次部署所有用户同时到期的惊群 - 首次运行持续失败时按 6h 退避重试,不再每个 tick 烧 LLM 配额 - should_run_now 时间解析容忍 naive 时间串;损坏的 last_run_at 触发重跑而非永久死锁 - SkillStore 新增 list_published_names 批量接口,curator 一轮 只查一次 published,取代逐技能查库 LLM 评审超时: - _run_llm_review 给 agent.invoke 套整体超时 (curator.review_timeout_seconds,默认 600s),超时中止本次评审 并在 curator 日志输出,curator 正常收尾、释放运行锁 - copy_context 把 user 上下文传入工作线程 模型可配置: - 新增 curator.model_name;curator 评审与技能安全审核共用该模型, 未配置则回退主模型 - 移除独立的 moderation_model_name 配置 - 前端进化面板新增模型下拉选择器 手动触发体验: - /api/curator/run 派发后台线程前先预检,返回 {triggered, reason}, 不再无论开关状态都提示"已开始" - 前端:总开关/curator 开关未开时禁用"立即执行"按钮, 被预检拦下时按原因(开关未开 / 已有任务在跑)给出准确提示 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/skill-evolution-panel.tsx | 70 ++++- frontend-web/src/core/curator/api.ts | 10 +- frontend-web/src/core/curator/types.ts | 4 +- frontend-web/src/core/i18n/locales/en-US.ts | 13 +- frontend-web/src/core/i18n/locales/types.ts | 9 +- frontend-web/src/core/i18n/locales/zh-CN.ts | 9 +- offline-backend-20260512/backend/CLAUDE.md | 49 +++ .../backend/app/gateway/routers/skills.py | 25 +- .../deerflow/config/skill_evolution_config.py | 13 +- .../deerflow/persistence/skills/base.py | 10 + .../deerflow/persistence/skills/memory.py | 3 + .../deerflow/persistence/skills/sql.py | 6 + .../harness/deerflow/skills/curator.py | 281 ++++++++++++++++-- .../deerflow/skills/security_scanner.py | 6 +- .../tests/test_skill_evolution_runtime.py | 2 +- .../backend/tests/test_skills_multitenancy.py | 3 +- 16 files changed, 448 insertions(+), 65 deletions(-) diff --git a/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx b/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx index 1cd87920..b9549ed6 100644 --- a/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx +++ b/frontend-web/src/components/workspace/skills/skill-evolution-panel.tsx @@ -21,6 +21,13 @@ import { CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { useCuratorConfig, @@ -31,6 +38,7 @@ import { } from "@/core/curator/hooks"; import type { CuratorConfig } from "@/core/curator/types"; import { useI18n } from "@/core/i18n/hooks"; +import { useModels } from "@/core/models/hooks"; import { cn } from "@/lib/utils"; export function SkillEvolutionPanel() { @@ -68,6 +76,7 @@ function StatusCard() { const { t } = useI18n(); const tt = t.settings.skillEvolution.status; const { status, isLoading } = useCuratorStatus(); + const { config } = useCuratorConfig(); const runCurator = useRunCurator(); const lastRunDisplay = useMemo(() => { @@ -79,10 +88,25 @@ function StatusCard() { } }, [status?.last_run_at, tt.lastRunNever]); + // 总开关或 curator 子开关任一关闭时,手动触发跑不起来 —— 直接禁用按钮。 + const runDisabled = !config || !config.enabled || !config.curator.enabled; + const handleRun = async () => { try { - await runCurator.mutateAsync(); - toast.success(tt.runStarted); + const res = await runCurator.mutateAsync(); + if (res.triggered) { + toast.success(tt.runStarted); + } else { + // 后端预检拦下了(开关未开 / 已有任务在跑)—— 给出对应原因,而不是误报"已开始"。 + const reasonMsg: Record = { + evolution_disabled: tt.runBlockedEvolutionDisabled, + curator_disabled: tt.runBlockedCuratorDisabled, + already_running: tt.runBlockedAlreadyRunning, + }; + toast.warning( + reasonMsg[res.reason ?? ""] ?? tt.runBlockedEvolutionDisabled, + ); + } } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } @@ -107,7 +131,8 @@ function StatusCard() {