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:
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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user