Polish skill sync controls

This commit is contained in:
wuyang6
2026-05-11 10:04:38 +08:00
parent b4bd7698a4
commit 119ac41916
2 changed files with 107 additions and 25 deletions
+29 -2
View File
@@ -633,6 +633,27 @@ class AgentState:
if key.startswith(account_prefix): if key.startswith(account_prefix):
self._agents.pop(key, None) self._agents.pop(key, None)
def set_all_skills_enabled(
self,
account_id: str | None,
enabled: bool,
) -> None:
with self._lock:
config = self._config_for(account_id)
configurable_skill_names = {
skill.name.lower()
for skill in get_bundled_skills(config.cwd)
if skill.user_invocable
and skill.name.lower() not in ALWAYS_ENABLED_HIDDEN_SKILL_NAMES
}
disabled = self._load_disabled_skill_names(account_id)
if enabled:
disabled.difference_update(configurable_skill_names)
else:
disabled.update(configurable_skill_names)
self._save_disabled_skill_names(account_id, disabled)
self._clear_agents_for_account(account_id)
def _clear_agents_for_account(self, account_id: str | None) -> None: def _clear_agents_for_account(self, account_id: str | None) -> None:
account_prefix = f'{self._account_key(account_id)}:' account_prefix = f'{self._account_key(account_id)}:'
for key in list(self._agents): for key in list(self._agents):
@@ -856,8 +877,9 @@ class ModelListRequest(BaseModel):
class SkillPreferenceUpdate(BaseModel): class SkillPreferenceUpdate(BaseModel):
skill: str = Field(min_length=1) skill: str | None = None
enabled: bool enabled: bool
apply_all: bool = False
account_id: str | None = None account_id: str | None = None
@@ -942,7 +964,12 @@ def create_app(state: AgentState) -> FastAPI:
@app.patch('/api/skills') @app.patch('/api/skills')
async def update_skill_preference(payload: SkillPreferenceUpdate) -> list[dict[str, Any]]: async def update_skill_preference(payload: SkillPreferenceUpdate) -> list[dict[str, Any]]:
try: try:
state.set_skill_enabled(payload.account_id, payload.skill, payload.enabled) if payload.apply_all:
state.set_all_skills_enabled(payload.account_id, payload.enabled)
elif payload.skill:
state.set_skill_enabled(payload.account_id, payload.skill, payload.enabled)
else:
raise ValueError('skill must be provided unless apply_all is true')
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
return await list_skills(payload.account_id) return await list_skills(payload.account_id)
+78 -23
View File
@@ -1111,6 +1111,13 @@ function SkillInsertDialog() {
const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。"); const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。");
const [updatingSkill, setUpdatingSkill] = useState<string | null>(null); const [updatingSkill, setUpdatingSkill] = useState<string | null>(null);
const [syncingSkills, setSyncingSkills] = useState(false); const [syncingSkills, setSyncingSkills] = useState(false);
const [bulkUpdatingSkills, setBulkUpdatingSkills] = useState(false);
const configurableSkills = skills.filter(
(skill) => skill.configurable !== false,
);
const allSkillsEnabled =
configurableSkills.length > 0 &&
configurableSkills.every((skill) => skill.enabled !== false);
async function loadSkills() { async function loadSkills() {
setStatus("正在读取 Skill 列表..."); setStatus("正在读取 Skill 列表...");
@@ -1195,6 +1202,43 @@ function SkillInsertDialog() {
} }
} }
async function setAllSkillsEnabled(enabled: boolean) {
if (!configurableSkills.length) return;
const previous = skills;
setBulkUpdatingSkills(true);
setSkills((items) =>
items.map((item) =>
item.configurable === false ? item : { ...item, enabled },
),
);
try {
const response = await fetch("/api/claw/skills", {
method: "PATCH",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ apply_all: true, enabled }),
});
const payload = (await response.json()) as
| SkillSummary[]
| { error?: string; detail?: string };
if (!response.ok || !Array.isArray(payload)) {
throw new Error(
Array.isArray(payload)
? "更新失败"
: (payload.detail ?? payload.error ?? "更新失败"),
);
}
setSkills(payload);
setStatus(enabled ? "已启用全部 Skill。" : "已停用全部可选 Skill。");
} catch (err) {
setSkills(previous);
setStatus(err instanceof Error ? err.message : "批量更新 Skill 失败");
} finally {
setBulkUpdatingSkills(false);
}
}
return ( return (
<Dialog <Dialog
open={open} open={open}
@@ -1214,35 +1258,46 @@ function SkillInsertDialog() {
<BookOpenIcon className="size-4" /> <BookOpenIcon className="size-4" />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent className="gap-3 p-5">
<DialogHeader> <DialogHeader className="gap-1 pr-6">
<DialogTitle>Skills</DialogTitle> <DialogTitle>Skills</DialogTitle>
<DialogDescription> <DialogDescription>
Skill Skill
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex items-center justify-between gap-3"> <div className="flex flex-wrap items-center gap-2">
<Button <div className="flex items-center gap-2">
type="button" <Button
variant="outline" type="button"
size="sm" variant="outline"
className="h-8 gap-2" size="sm"
disabled={syncingSkills} className="h-8 gap-2 px-2.5"
onClick={syncSkills} disabled={syncingSkills}
> onClick={syncSkills}
<RefreshCwIcon >
className={cn("size-3.5", syncingSkills && "animate-spin")} <RefreshCwIcon
/> className={cn("size-3.5", syncingSkills && "animate-spin")}
Skill />
</Button> Skill
<p className="text-muted-foreground text-xs"> </Button>
git skill <Button
</p> type="button"
variant="ghost"
size="sm"
className="h-8 px-2.5"
disabled={!configurableSkills.length || bulkUpdatingSkills}
onClick={() => setAllSkillsEnabled(!allSkillsEnabled)}
>
{allSkillsEnabled ? "全取消" : "全选"}
</Button>
</div>
{status ? (
<p className="min-w-0 flex-1 truncate text-muted-foreground text-xs">
{status}
</p>
) : null}
</div> </div>
{status ? ( <div className="grid max-h-96 gap-2 overflow-y-auto pt-1">
<p className="text-muted-foreground text-sm">{status}</p>
) : null}
<div className="grid max-h-96 gap-2 overflow-y-auto">
{skills.map((skill) => ( {skills.map((skill) => (
<div <div
key={skill.name} key={skill.name}