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,
} 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,
@@ -38,7 +31,6 @@ 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() {
@@ -273,20 +265,30 @@ function StatRow({
);
}
/** Select 不允许空字符串 value,用此哨兵代表"用主模型"(对应 model_name=null)。 */
const DEFAULT_MODEL_VALUE = "__default__";
interface CuratorDraft {
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() {
const { t } = useI18n();
const tt = t.settings.skillEvolution.config;
const { config, isLoading } = useCuratorConfig();
const updateConfig = useUpdateCuratorConfig();
const { models } = useModels();
const [draft, setDraft] = useState<CuratorConfig | null>(null);
const [draft, setDraft] = useState<CuratorDraft | null>(null);
useEffect(() => {
if (config && !draft) {
setDraft(config);
setDraft(toDraft(config));
}
}, [config, draft]);
@@ -305,14 +307,12 @@ function ConfigCard() {
);
}
const isDirty =
!!config &&
JSON.stringify(draft) !== JSON.stringify(config);
const original = config ? toDraft(config) : null;
const isDirty = !!original && JSON.stringify(draft) !== JSON.stringify(original);
const handleSave = async () => {
try {
const updated = await updateConfig.mutateAsync(draft);
setDraft(updated);
await updateConfig.mutateAsync(draft);
toast.success(tt.saveSuccess);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
@@ -326,29 +326,6 @@ function ConfigCard() {
<CardDescription>{tt.description}</CardDescription>
</CardHeader>
<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
label={tt.intervalHoursLabel}
helper={tt.intervalHoursHelper}
@@ -357,15 +334,9 @@ function ConfigCard() {
type="number"
min={1}
className="max-w-[10rem]"
value={draft.curator.interval_hours}
value={draft.interval_hours}
onChange={(e) =>
setDraft({
...draft,
curator: {
...draft.curator,
interval_hours: Math.max(1, Number(e.target.value) || 1),
},
})
setDraft({ ...draft, interval_hours: Math.max(1, Number(e.target.value) || 1) })
}
/>
</FieldRow>
@@ -377,15 +348,9 @@ function ConfigCard() {
type="number"
min={1}
className="max-w-[10rem]"
value={draft.curator.stale_after_days}
value={draft.stale_after_days}
onChange={(e) =>
setDraft({
...draft,
curator: {
...draft.curator,
stale_after_days: Math.max(1, Number(e.target.value) || 1),
},
})
setDraft({ ...draft, stale_after_days: Math.max(1, Number(e.target.value) || 1) })
}
/>
</FieldRow>
@@ -397,61 +362,19 @@ function ConfigCard() {
type="number"
min={1}
className="max-w-[10rem]"
value={draft.curator.archive_after_days}
value={draft.archive_after_days}
onChange={(e) =>
setDraft({
...draft,
curator: {
...draft.curator,
archive_after_days: Math.max(1, Number(e.target.value) || 1),
},
})
setDraft({ ...draft, archive_after_days: Math.max(1, Number(e.target.value) || 1) })
}
/>
</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">
<Button
variant="outline"
size="sm"
disabled={!isDirty || updateConfig.isPending}
onClick={() => config && setDraft(config)}
onClick={() => original && setDraft(original)}
>
{tt.reset}
</Button>
+7 -1
View File
@@ -17,8 +17,14 @@ export async function getCuratorConfig(): 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(
payload: CuratorConfig,
payload: CuratorConfigUpdatePayload,
): Promise<CuratorConfig> {
const response = await apiFetch(`${getBackendBaseURL()}/api/curator/config`, {
method: "PUT",
+2 -1
View File
@@ -10,6 +10,7 @@ import {
runCurator,
updateCuratorConfig,
} from "./api";
import type { CuratorConfigUpdatePayload } from "./api";
import type { CuratorConfig } from "./types";
// All curator-related queries are per-user data (status / config / logs all
@@ -51,7 +52,7 @@ export function useUpdateCuratorConfig() {
const queryClient = useQueryClient();
const keys = useCuratorKeys();
return useMutation({
mutationFn: (payload: CuratorConfig) => updateCuratorConfig(payload),
mutationFn: (payload: CuratorConfigUpdatePayload) => updateCuratorConfig(payload),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: keys.config });
void queryClient.invalidateQueries({ queryKey: keys.status });
@@ -2329,12 +2329,12 @@ class CuratorConfigResponse(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,
interval, model, timeouts) are global and controlled via config.yaml.
All other settings (enabled, model, timeouts) are 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.")
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:
"""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).
"""
"""Pure helper so both the GET route and the post-PUT refresh share one shape."""
return CuratorConfigResponse(
enabled=evo.enabled,
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")
async def get_curator_config(request: Request) -> CuratorConfigResponse:
"""Return the current curator config for the authenticated user (yaml
defaults + that user's overrides)."""
"""Return the current curator config for the authenticated user (yaml defaults + per-user overrides)."""
try:
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")
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.
All other settings (enabled, interval, model, timeouts) are global and
read from config.yaml; users cannot override them here.
Other settings (enabled, model, timeouts) are global and controlled via config.yaml.
"""
try:
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)
save_user_overrides(user_id, {
"curator": {
"interval_hours": body.interval_hours,
"stale_after_days": body.stale_after_days,
"archive_after_days": body.archive_after_days,
}
},
})
return _build_curator_config_response(load_skill_evolution_config_for_user(user_id))
except HTTPException: