refactor(skill-evolution): simplify config UI to 3 user-editable fields

前端配置页只保留运行间隔、陈旧阈值、归档阈值三个字段,
其余配置(enabled、model_name 等)走 config.yaml 全局默认。
后端 PUT /api/curator/config 仅保存这三个字段到 per-user skill_evolution.json。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Sonnet 4.6 committed 2026-06-05 13:32:19 +08:00
1 parent 6f9afcdba2
commit d0c696e523
4 files changed
+43 -119

No files matched your search

@@ -21,13 +21,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { import {
useCuratorConfig, useCuratorConfig,
@@ -38,7 +31,6 @@ import {
} from "@/core/curator/hooks"; } from "@/core/curator/hooks";
import type { CuratorConfig } from "@/core/curator/types"; import type { CuratorConfig } from "@/core/curator/types";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function SkillEvolutionPanel() { export function SkillEvolutionPanel() {
@@ -273,20 +265,30 @@ function StatRow({
); );
} }
/** Select 不允许空字符串 value,用此哨兵代表"用主模型"(对应 model_name=null)。 */ interface CuratorDraft {
const DEFAULT_MODEL_VALUE = "__default__"; interval_hours: number;
stale_after_days: number;
archive_after_days: number;
}
function toDraft(config: CuratorConfig): CuratorDraft {
return {
interval_hours: config.curator.interval_hours,
stale_after_days: config.curator.stale_after_days,
archive_after_days: config.curator.archive_after_days,
};
}
function ConfigCard() { function ConfigCard() {
const { t } = useI18n(); const { t } = useI18n();
const tt = t.settings.skillEvolution.config; const tt = t.settings.skillEvolution.config;
const { config, isLoading } = useCuratorConfig(); const { config, isLoading } = useCuratorConfig();
const updateConfig = useUpdateCuratorConfig(); const updateConfig = useUpdateCuratorConfig();
const { models } = useModels();
const [draft, setDraft] = useState<CuratorConfig | null>(null); const [draft, setDraft] = useState<CuratorDraft | null>(null);
useEffect(() => { useEffect(() => {
if (config && !draft) { if (config && !draft) {
setDraft(config); setDraft(toDraft(config));
} }
}, [config, draft]); }, [config, draft]);
@@ -305,14 +307,12 @@ function ConfigCard() {
); );
} }
const isDirty = const original = config ? toDraft(config) : null;
!!config && const isDirty = !!original && JSON.stringify(draft) !== JSON.stringify(original);
JSON.stringify(draft) !== JSON.stringify(config);
const handleSave = async () => { const handleSave = async () => {
try { try {
const updated = await updateConfig.mutateAsync(draft); await updateConfig.mutateAsync(draft);
setDraft(updated);
toast.success(tt.saveSuccess); toast.success(tt.saveSuccess);
} catch (err) { } catch (err) {
toast.error(err instanceof Error ? err.message : String(err)); toast.error(err instanceof Error ? err.message : String(err));
@@ -326,29 +326,6 @@ function ConfigCard() {
<CardDescription>{tt.description}</CardDescription> <CardDescription>{tt.description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-5"> <CardContent className="space-y-5">
<FieldRow
label={tt.enabledLabel}
helper={tt.enabledHelper}
>
<Switch
checked={draft.enabled}
onCheckedChange={(v) => setDraft({ ...draft, enabled: v })}
/>
</FieldRow>
<FieldRow
label={tt.curatorEnabledLabel}
helper={tt.curatorEnabledHelper}
>
<Switch
checked={draft.curator.enabled}
onCheckedChange={(v) =>
setDraft({
...draft,
curator: { ...draft.curator, enabled: v },
})
}
/>
</FieldRow>
<FieldRow <FieldRow
label={tt.intervalHoursLabel} label={tt.intervalHoursLabel}
helper={tt.intervalHoursHelper} helper={tt.intervalHoursHelper}
@@ -357,15 +334,9 @@ function ConfigCard() {
type="number" type="number"
min={1} min={1}
className="max-w-[10rem]" className="max-w-[10rem]"
value={draft.curator.interval_hours} value={draft.interval_hours}
onChange={(e) => onChange={(e) =>
setDraft({ setDraft({ ...draft, interval_hours: Math.max(1, Number(e.target.value) || 1) })
...draft,
curator: {
...draft.curator,
interval_hours: Math.max(1, Number(e.target.value) || 1),
},
})
} }
/> />
</FieldRow> </FieldRow>
@@ -377,15 +348,9 @@ function ConfigCard() {
type="number" type="number"
min={1} min={1}
className="max-w-[10rem]" className="max-w-[10rem]"
value={draft.curator.stale_after_days} value={draft.stale_after_days}
onChange={(e) => onChange={(e) =>
setDraft({ setDraft({ ...draft, stale_after_days: Math.max(1, Number(e.target.value) || 1) })
...draft,
curator: {
...draft.curator,
stale_after_days: Math.max(1, Number(e.target.value) || 1),
},
})
} }
/> />
</FieldRow> </FieldRow>
@@ -397,61 +362,19 @@ function ConfigCard() {
type="number" type="number"
min={1} min={1}
className="max-w-[10rem]" className="max-w-[10rem]"
value={draft.curator.archive_after_days} value={draft.archive_after_days}
onChange={(e) => onChange={(e) =>
setDraft({ setDraft({ ...draft, archive_after_days: Math.max(1, Number(e.target.value) || 1) })
...draft,
curator: {
...draft.curator,
archive_after_days: Math.max(1, Number(e.target.value) || 1),
},
})
} }
/> />
</FieldRow> </FieldRow>
<FieldRow
label={tt.curatorModelLabel}
helper={tt.curatorModelHelper}
>
<Select
value={draft.curator.model_name ?? DEFAULT_MODEL_VALUE}
onValueChange={(v) =>
setDraft({
...draft,
curator: {
...draft.curator,
model_name: v === DEFAULT_MODEL_VALUE ? null : v,
},
})
}
>
<SelectTrigger
className={cn(
"max-w-[20rem]",
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#0D2D54]",
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value={DEFAULT_MODEL_VALUE}>
{tt.curatorModelDefault}
</SelectItem>
{models.map((m) => (
<SelectItem key={m.name} value={m.name}>
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FieldRow>
<div className="flex justify-end gap-2 pt-2"> <div className="flex justify-end gap-2 pt-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={!isDirty || updateConfig.isPending} disabled={!isDirty || updateConfig.isPending}
onClick={() => config && setDraft(config)} onClick={() => original && setDraft(original)}
> >
{tt.reset} {tt.reset}
</Button> </Button>
+7 -1
View File
@@ -17,8 +17,14 @@ export async function getCuratorConfig(): Promise<CuratorConfig> {
return response.json() as Promise<CuratorConfig>; return response.json() as Promise<CuratorConfig>;
} }
export interface CuratorConfigUpdatePayload {
interval_hours: number;
stale_after_days: number;
archive_after_days: number;
}
export async function updateCuratorConfig( export async function updateCuratorConfig(
payload: CuratorConfig, payload: CuratorConfigUpdatePayload,
): Promise<CuratorConfig> { ): Promise<CuratorConfig> {
const response = await apiFetch(`${getBackendBaseURL()}/api/curator/config`, { const response = await apiFetch(`${getBackendBaseURL()}/api/curator/config`, {
method: "PUT", method: "PUT",
+2 -1
View File
@@ -10,6 +10,7 @@ import {
runCurator, runCurator,
updateCuratorConfig, updateCuratorConfig,
} from "./api"; } from "./api";
import type { CuratorConfigUpdatePayload } from "./api";
import type { CuratorConfig } from "./types"; import type { CuratorConfig } from "./types";
// All curator-related queries are per-user data (status / config / logs all // All curator-related queries are per-user data (status / config / logs all
@@ -51,7 +52,7 @@ export function useUpdateCuratorConfig() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const keys = useCuratorKeys(); const keys = useCuratorKeys();
return useMutation({ return useMutation({
mutationFn: (payload: CuratorConfig) => updateCuratorConfig(payload), mutationFn: (payload: CuratorConfigUpdatePayload) => updateCuratorConfig(payload),
onSuccess: () => { onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: keys.config }); void queryClient.invalidateQueries({ queryKey: keys.config });
void queryClient.invalidateQueries({ queryKey: keys.status }); void queryClient.invalidateQueries({ queryKey: keys.status });
@@ -2329,12 +2329,12 @@ class CuratorConfigResponse(BaseModel):
class CuratorConfigRequest(BaseModel): class CuratorConfigRequest(BaseModel):
"""Payload for updating per-user skill evolution overrides. """Payload for updating the three user-facing curator thresholds in skill_evolution.json.
Users may only adjust archiving thresholds. All other settings (enabled, All other settings (enabled, model, timeouts) are controlled via config.yaml.
interval, model, timeouts) are global and controlled via config.yaml.
""" """
interval_hours: int = Field(..., ge=1, description="Hours between curator runs.")
stale_after_days: int = Field(..., ge=1, description="Days of inactivity before a skill is marked stale.") stale_after_days: int = Field(..., ge=1, description="Days of inactivity before a skill is marked stale.")
archive_after_days: int = Field(..., ge=1, description="Days of inactivity before a stale skill is archived.") archive_after_days: int = Field(..., ge=1, description="Days of inactivity before a stale skill is archived.")
@@ -2352,11 +2352,7 @@ class CuratorLogsResponse(BaseModel):
def _build_curator_config_response(evo) -> CuratorConfigResponse: def _build_curator_config_response(evo) -> CuratorConfigResponse:
"""Pure helper so both the GET route and the post-PUT refresh share one shape. """Pure helper so both the GET route and the post-PUT refresh share one shape."""
``evo`` is a ``SkillEvolutionConfig`` instance (already merged with per-user
overrides if any).
"""
return CuratorConfigResponse( return CuratorConfigResponse(
enabled=evo.enabled, enabled=evo.enabled,
curator=CuratorSubConfig( curator=CuratorSubConfig(
@@ -2372,8 +2368,7 @@ def _build_curator_config_response(evo) -> CuratorConfigResponse:
@router.get("/curator/config", response_model=CuratorConfigResponse, summary="Get Skill Evolution Config") @router.get("/curator/config", response_model=CuratorConfigResponse, summary="Get Skill Evolution Config")
async def get_curator_config(request: Request) -> CuratorConfigResponse: async def get_curator_config(request: Request) -> CuratorConfigResponse:
"""Return the current curator config for the authenticated user (yaml """Return the current curator config for the authenticated user (yaml defaults + per-user overrides)."""
defaults + that user's overrides)."""
try: try:
from deerflow.config.skill_evolution_runtime import load_skill_evolution_config_for_user from deerflow.config.skill_evolution_runtime import load_skill_evolution_config_for_user
@@ -2386,11 +2381,9 @@ async def get_curator_config(request: Request) -> CuratorConfigResponse:
@router.put("/curator/config", response_model=CuratorConfigResponse, summary="Update Skill Evolution Config") @router.put("/curator/config", response_model=CuratorConfigResponse, summary="Update Skill Evolution Config")
async def update_curator_config(body: CuratorConfigRequest, request: Request) -> CuratorConfigResponse: async def update_curator_config(body: CuratorConfigRequest, request: Request) -> CuratorConfigResponse:
"""Persist per-user archiving thresholds to ``users/{user_id}/skill_evolution.json``. """Persist the three user-facing curator thresholds to the per-user ``skill_evolution.json``.
Only ``stale_after_days`` and ``archive_after_days`` are user-editable. Other settings (enabled, model, timeouts) are global and controlled via config.yaml.
All other settings (enabled, interval, model, timeouts) are global and
read from config.yaml; users cannot override them here.
""" """
try: try:
from deerflow.config.skill_evolution_runtime import load_skill_evolution_config_for_user, save_user_overrides from deerflow.config.skill_evolution_runtime import load_skill_evolution_config_for_user, save_user_overrides
@@ -2398,9 +2391,10 @@ async def update_curator_config(body: CuratorConfigRequest, request: Request) ->
user_id = _current_user_id(request) user_id = _current_user_id(request)
save_user_overrides(user_id, { save_user_overrides(user_id, {
"curator": { "curator": {
"interval_hours": body.interval_hours,
"stale_after_days": body.stale_after_days, "stale_after_days": body.stale_after_days,
"archive_after_days": body.archive_after_days, "archive_after_days": body.archive_after_days,
} },
}) })
return _build_curator_config_response(load_skill_evolution_config_for_user(user_id)) return _build_curator_config_response(load_skill_evolution_config_for_user(user_id))
except HTTPException: except HTTPException: