Polish skill sync controls
This commit is contained in:
+28
-1
@@ -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:
|
||||||
|
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)
|
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)
|
||||||
|
|||||||
@@ -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,19 +1258,20 @@ 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">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 gap-2"
|
className="h-8 gap-2 px-2.5"
|
||||||
disabled={syncingSkills}
|
disabled={syncingSkills}
|
||||||
onClick={syncSkills}
|
onClick={syncSkills}
|
||||||
>
|
>
|
||||||
@@ -1235,14 +1280,24 @@ function SkillInsertDialog() {
|
|||||||
/>
|
/>
|
||||||
更新 Skill
|
更新 Skill
|
||||||
</Button>
|
</Button>
|
||||||
<p className="text-muted-foreground text-xs">
|
<Button
|
||||||
从当前 git 分支拉取最新 skill 文件
|
type="button"
|
||||||
</p>
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2.5"
|
||||||
|
disabled={!configurableSkills.length || bulkUpdatingSkills}
|
||||||
|
onClick={() => setAllSkillsEnabled(!allSkillsEnabled)}
|
||||||
|
>
|
||||||
|
{allSkillsEnabled ? "全取消" : "全选"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{status ? (
|
{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}
|
) : 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) => (
|
{skills.map((skill) => (
|
||||||
<div
|
<div
|
||||||
key={skill.name}
|
key={skill.name}
|
||||||
|
|||||||
Reference in New Issue
Block a user