Polish skill sync controls
This commit is contained in:
+28
-1
@@ -633,6 +633,27 @@ class AgentState:
|
||||
if key.startswith(account_prefix):
|
||||
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:
|
||||
account_prefix = f'{self._account_key(account_id)}:'
|
||||
for key in list(self._agents):
|
||||
@@ -856,8 +877,9 @@ class ModelListRequest(BaseModel):
|
||||
|
||||
|
||||
class SkillPreferenceUpdate(BaseModel):
|
||||
skill: str = Field(min_length=1)
|
||||
skill: str | None = None
|
||||
enabled: bool
|
||||
apply_all: bool = False
|
||||
account_id: str | None = None
|
||||
|
||||
|
||||
@@ -942,7 +964,12 @@ def create_app(state: AgentState) -> FastAPI:
|
||||
@app.patch('/api/skills')
|
||||
async def update_skill_preference(payload: SkillPreferenceUpdate) -> list[dict[str, Any]]:
|
||||
try:
|
||||
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:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return await list_skills(payload.account_id)
|
||||
|
||||
@@ -1111,6 +1111,13 @@ function SkillInsertDialog() {
|
||||
const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。");
|
||||
const [updatingSkill, setUpdatingSkill] = useState<string | null>(null);
|
||||
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() {
|
||||
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 (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -1214,19 +1258,20 @@ function SkillInsertDialog() {
|
||||
<BookOpenIcon className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogContent className="gap-3 p-5">
|
||||
<DialogHeader className="gap-1 pr-6">
|
||||
<DialogTitle>Skills</DialogTitle>
|
||||
<DialogDescription>
|
||||
勾选的 Skill 会进入模型可见列表;点击名称可插入显式调用。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-2"
|
||||
className="h-8 gap-2 px-2.5"
|
||||
disabled={syncingSkills}
|
||||
onClick={syncSkills}
|
||||
>
|
||||
@@ -1235,14 +1280,24 @@ function SkillInsertDialog() {
|
||||
/>
|
||||
更新 Skill
|
||||
</Button>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
从当前 git 分支拉取最新 skill 文件
|
||||
</p>
|
||||
<Button
|
||||
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="text-muted-foreground text-sm">{status}</p>
|
||||
<p className="min-w-0 flex-1 truncate text-muted-foreground text-xs">
|
||||
{status}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid max-h-96 gap-2 overflow-y-auto">
|
||||
</div>
|
||||
<div className="grid max-h-96 gap-2 overflow-y-auto pt-1">
|
||||
{skills.map((skill) => (
|
||||
<div
|
||||
key={skill.name}
|
||||
|
||||
Reference in New Issue
Block a user