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