mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
Cut time-to-interactive on those admin pages and align button/table copy with the existing zh/en i18n switch. Co-authored-by: Cursor <cursoragent@cursor.com>
1584 lines
64 KiB
JavaScript
1584 lines
64 KiB
JavaScript
import { t, tf, el, apiGet, apiPost, assertSkillMutationOk, shortText, renderPageShell, rowActions } from "../core.js";
|
|
|
|
function skillsFold(summaryText, innerNodes) {
|
|
const det = el("details", { class: "details plugins-fold" });
|
|
det.appendChild(el("summary", { text: summaryText }));
|
|
const inner = el("div", { class: "plugins-fold__inner" });
|
|
inner.addEventListener("click", (e) => e.stopPropagation());
|
|
(innerNodes || []).forEach((n) => inner.appendChild(n));
|
|
det.appendChild(inner);
|
|
return det;
|
|
}
|
|
|
|
async function renderSkills() {
|
|
document.querySelectorAll("[data-skill-modal]").forEach((n) => n.remove());
|
|
document.querySelectorAll("body > .row-actions__menu, body > .chat-sess-menu-pop.row-actions__menu").forEach((n) => n.remove());
|
|
const SKILL_AUDIT_RETRYABLE_ONLY_KEY = "ops_admin_skill_audit_retryable_only";
|
|
const status = el("div", { class: "muted", text: "" });
|
|
const rowsState = { items: [] };
|
|
const auditState = { items: [] };
|
|
const nameInp = el("input", { class: "input", placeholder: t("skills.ph.name") });
|
|
const descInp = el("input", { class: "input", placeholder: t("skills.ph.description") });
|
|
const bodyInp = el("textarea", { class: "input", rows: "4", placeholder: t("skills.ph.body") });
|
|
const regInp = el("input", { class: "input", placeholder: t("skills.ph.registry") });
|
|
const localDirInp = el("input", { class: "input", placeholder: t("skills.ph.localDir") });
|
|
const marketStatus = el("div", { class: "muted", text: "" });
|
|
const skillModeStatus = el("div", { class: "muted", text: "" });
|
|
const skillPromptModeCb = el("input", { type: "checkbox" });
|
|
const marketQ = el("input", { class: "input", placeholder: t("skills.ph.marketSearch") });
|
|
const marketLimitInp = el("input", {
|
|
class: "input",
|
|
placeholder: t("skills.ph.limit"),
|
|
value: "40",
|
|
style: "max-width:120px;",
|
|
});
|
|
const marketTbody = el("tbody");
|
|
const marketDetailPre = el("pre", { class: "muted pre", text: "" });
|
|
let marketItems = [];
|
|
|
|
const loadSkillMode = async () => {
|
|
try {
|
|
const r = await apiGet("/admin/api/skills/mode");
|
|
skillPromptModeCb.checked = !!r.prompt_in_system;
|
|
skillModeStatus.textContent = "";
|
|
} catch (e) {
|
|
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
const saveSkillMode = async () => {
|
|
skillModeStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/mode", {
|
|
prompt_in_system: !!skillPromptModeCb.checked,
|
|
market_provider: "clawhub",
|
|
});
|
|
skillPromptModeCb.checked = !!r.prompt_in_system;
|
|
skillModeStatus.textContent = `saved: prompt=${String(!!r.prompt_in_system)} market=clawhub`;
|
|
} catch (e) {
|
|
skillModeStatus.textContent = `mode: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
|
|
const loadMarket = async (q) => {
|
|
const qq = String(q || "").trim();
|
|
const lim = Math.max(1, Math.min(200, parseInt(String(marketLimitInp.value || "40"), 10) || 40));
|
|
marketStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet(
|
|
`/admin/api/skills/market/search?q=${encodeURIComponent(qq)}&limit=${encodeURIComponent(String(lim))}`,
|
|
);
|
|
marketItems = Array.isArray(resp.items) ? resp.items : [];
|
|
marketStatus.textContent = `results: ${marketItems.length}`;
|
|
repaintMarket();
|
|
} catch (e) {
|
|
marketItems = [];
|
|
marketStatus.textContent = `${t("common.error")}: ${String(e && e.message ? e.message : e)}`;
|
|
repaintMarket();
|
|
}
|
|
};
|
|
|
|
const loadMarketDetail = async (slug) => {
|
|
const s = String(slug || "").trim();
|
|
if (!s) return;
|
|
marketDetailPre.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet(`/admin/api/skills/market/detail?slug=${encodeURIComponent(s)}`);
|
|
marketDetailPre.textContent = JSON.stringify(resp.detail || {}, null, 2);
|
|
} catch (e) {
|
|
marketDetailPre.textContent = `${t("common.error")}: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
|
|
const installFromMarket = async (slug, version) => {
|
|
const s = String(slug || "").trim();
|
|
if (!s) return;
|
|
marketStatus.textContent = `installing: ${s}...`;
|
|
openSkillInstallModal(`Installing ${s}...`);
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/market/install", {
|
|
slug: s,
|
|
version: version ? String(version) : undefined,
|
|
overwrite: false,
|
|
});
|
|
assertSkillMutationOk(r, "Market install failed");
|
|
status.textContent = `install-market success: ${JSON.stringify(r.result || {})}`;
|
|
marketStatus.textContent = `installed: ${s}`;
|
|
await refreshSkillsState();
|
|
finishSkillInstallModal(true, `${s} installed successfully.`);
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
marketStatus.textContent = `install failed: ${s}`;
|
|
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
|
|
}
|
|
};
|
|
|
|
const repaintMarket = () => {
|
|
marketTbody.innerHTML = "";
|
|
(marketItems || []).forEach((x) => {
|
|
const slug = String(x.slug || "");
|
|
const ver = String(x.version || "");
|
|
const btnDetail = el("button", {
|
|
class: "btn btn--small",
|
|
text: t("skills.action.detail"),
|
|
onclick: async () => await loadMarketDetail(slug),
|
|
});
|
|
const btnInstall = el("button", {
|
|
class: "btn btn--small btn--primary",
|
|
text: t("skills.action.install"),
|
|
onclick: async () => await installFromMarket(slug, ver),
|
|
});
|
|
marketTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: slug }),
|
|
el("td", { text: String(x.name || "") }),
|
|
el("td", { text: ver }),
|
|
el("td", { text: shortText(String(x.description || ""), 80) }),
|
|
el("td", { class: "table__cell--actions", "data-copy-disabled": "1" }, [
|
|
el("div", { class: "table__cell-actions" }, [btnInstall, btnDetail]),
|
|
]),
|
|
]),
|
|
);
|
|
});
|
|
};
|
|
|
|
const btnMarketSearch = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.search"),
|
|
onclick: async () => await loadMarket(marketQ.value),
|
|
});
|
|
const btnMarketLatest = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.latest"),
|
|
onclick: async () => await loadMarket(""),
|
|
});
|
|
|
|
// --- Binding ---
|
|
const skillBindingState = { roles: [], names: [], mapping: {}, enabled: false };
|
|
const skillBindingStatus = el("div", { class: "muted", text: "" });
|
|
const skillBindingPersistHint = el("div", {
|
|
class: "muted",
|
|
style: "margin:6px 0;line-height:1.5;",
|
|
text: t("skills.hint.bindingPersist"),
|
|
});
|
|
const skillBindingEnvHint = el("div", {
|
|
class: "muted",
|
|
style: "margin:6px 0;line-height:1.5;display:none;",
|
|
});
|
|
const skillEffectiveState = { items: [] };
|
|
const skillBindingEnabledCb = el("input", { type: "checkbox" });
|
|
const skillRoleSelect = el("select", { class: "input" }, []);
|
|
const skillBindingListWrap = el("div");
|
|
const skillBindingDashTbody = el("tbody");
|
|
const skillEffectiveTbody = el("tbody");
|
|
|
|
const renderSkillBindingDashboard = () => {
|
|
skillBindingDashTbody.innerHTML = "";
|
|
const roles = Array.isArray(skillBindingState.roles) ? skillBindingState.roles : [];
|
|
const mapping = skillBindingState.mapping && typeof skillBindingState.mapping === "object" ? skillBindingState.mapping : {};
|
|
roles.forEach((role) => {
|
|
const direct = new Set(Array.isArray(mapping[role]) ? mapping[role].map((x) => String(x)) : []);
|
|
const effective = new Set(Array.from(direct));
|
|
skillBindingDashTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: role }),
|
|
el("td", { text: String(direct.size) }),
|
|
el("td", { text: String(effective.size) }),
|
|
]),
|
|
);
|
|
});
|
|
if (!roles.length) {
|
|
skillBindingDashTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "3" })]));
|
|
}
|
|
};
|
|
|
|
const renderSkillEffectiveDashboard = () => {
|
|
skillEffectiveTbody.innerHTML = "";
|
|
const rows = Array.isArray(skillEffectiveState.items) ? skillEffectiveState.items : [];
|
|
rows.forEach((x) => {
|
|
const names = Array.isArray(x.names_preview)
|
|
? x.names_preview.map((v) => String(v || "").trim()).filter(Boolean)
|
|
: [];
|
|
const docsOnlyNames = Array.isArray(x.docs_only_names_preview)
|
|
? x.docs_only_names_preview.map((v) => String(v || "").trim()).filter(Boolean)
|
|
: [];
|
|
const previewText = names.slice(0, 6).join(", ");
|
|
const namesCell = names.length
|
|
? el("details", {}, [
|
|
el("summary", { text: previewText || "(empty)" }),
|
|
el("div", { class: "muted u-mt-4 u-wrap-lh", text: names.join(", ") }),
|
|
])
|
|
: el("span", { class: "muted", text: "-" });
|
|
const docsOnlyCell = docsOnlyNames.length
|
|
? el("details", {}, [
|
|
el("summary", { text: docsOnlyNames.slice(0, 4).join(", ") }),
|
|
el("div", { class: "muted u-mt-4 u-wrap-lh", text: docsOnlyNames.join(", ") }),
|
|
])
|
|
: el("span", { class: "muted", text: "-" });
|
|
skillEffectiveTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: String(x.role || "") }),
|
|
el("td", { text: String(x.total || 0) }),
|
|
el("td", { text: String(x.workspace_total || 0) }),
|
|
el("td", { text: String(x.workspace_direct || 0) }),
|
|
el("td", { text: String(x.workspace_resolved_tool_match || 0) }),
|
|
el("td", { text: String(x.workspace_docs_only || 0) }),
|
|
el("td", { text: String(x.mcp_total || 0) }),
|
|
el("td", { text: String(x.tool_total || 0) }),
|
|
el("td", {}, [docsOnlyCell]),
|
|
el("td", {}, [namesCell]),
|
|
]),
|
|
);
|
|
});
|
|
if (!rows.length) {
|
|
skillEffectiveTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "10" })]));
|
|
}
|
|
};
|
|
|
|
const loadSkillEffective = async () => {
|
|
try {
|
|
const r = await apiGet("/admin/api/skills/effective");
|
|
skillEffectiveState.items = Array.isArray(r.items) ? r.items : [];
|
|
renderSkillEffectiveDashboard();
|
|
} catch (e) {
|
|
skillEffectiveState.items = [];
|
|
renderSkillEffectiveDashboard();
|
|
skillBindingStatus.textContent = `effective: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
|
|
const renderSkillBindingList = () => {
|
|
const current = String(skillRoleSelect.value || "");
|
|
const draft = skillBindingState.mapping;
|
|
const existing = Array.isArray(draft[current]) ? draft[current].map((x) => String(x)) : [];
|
|
const selected = new Set(existing);
|
|
skillBindingListWrap.innerHTML = "";
|
|
const skills =
|
|
skillBindingState.names.length > 0
|
|
? skillBindingState.names
|
|
: Array.isArray(rowsState.items)
|
|
? rowsState.items.map((x) => String(x.name || "").trim()).filter(Boolean)
|
|
: [];
|
|
if (!skills.length) {
|
|
skillBindingListWrap.appendChild(el("div", { class: "muted", text: t("skills.hint.noSkills") }));
|
|
return;
|
|
}
|
|
skills.forEach((nm) => {
|
|
const cb = el("input", { type: "checkbox" });
|
|
cb.checked = selected.has(nm);
|
|
cb.addEventListener("change", () => {
|
|
const prev = new Set(Array.isArray(draft[current]) ? draft[current].map((x) => String(x)) : []);
|
|
if (cb.checked) prev.add(nm);
|
|
else prev.delete(nm);
|
|
draft[current] = Array.from(prev);
|
|
});
|
|
skillBindingListWrap.appendChild(el("label", { class: "row u-row-center" }, [cb, el("span", { text: nm })]));
|
|
});
|
|
};
|
|
|
|
const applyBindingEnvHint = (r) => {
|
|
if (r && r.enabled_env_present) {
|
|
skillBindingEnvHint.style.display = "block";
|
|
const stored = !!r.enabled_stored;
|
|
const eff = !!r.enabled;
|
|
skillBindingEnvHint.textContent = tf("skills.hint.bindingEnv", {
|
|
stored: String(stored),
|
|
effective: String(eff),
|
|
});
|
|
} else {
|
|
skillBindingEnvHint.style.display = "none";
|
|
skillBindingEnvHint.textContent = "";
|
|
}
|
|
};
|
|
|
|
const loadSkillBinding = async () => {
|
|
try {
|
|
const r = await apiGet("/admin/api/skills/binding");
|
|
skillBindingState.roles = Array.isArray(r.available_roles) ? r.available_roles : [];
|
|
skillBindingState.names = (Array.isArray(r.installed_skills) ? r.installed_skills : [])
|
|
.map((x) => String(x.name || "").trim())
|
|
.filter(Boolean);
|
|
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
|
|
skillBindingState.enabled = !!r.enabled;
|
|
skillBindingEnabledCb.checked = skillBindingState.enabled;
|
|
applyBindingEnvHint(r);
|
|
skillRoleSelect.innerHTML = "";
|
|
skillBindingState.roles.forEach((role) => {
|
|
skillRoleSelect.appendChild(el("option", { value: role, text: role }));
|
|
});
|
|
if (skillBindingState.roles.length && !skillRoleSelect.value) {
|
|
skillRoleSelect.value = skillBindingState.roles[0];
|
|
}
|
|
renderSkillBindingList();
|
|
renderSkillBindingDashboard();
|
|
await loadSkillEffective();
|
|
skillBindingStatus.textContent = "";
|
|
} catch (e) {
|
|
skillBindingStatus.textContent = `binding: ${String(e && e.message ? e.message : e)}`;
|
|
skillBindingListWrap.innerHTML = "";
|
|
}
|
|
};
|
|
skillRoleSelect.addEventListener("change", () => renderSkillBindingList());
|
|
|
|
const btnSaveSkillBinding = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.saveBinding"),
|
|
onclick: async () => {
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/binding", {
|
|
enabled: !!skillBindingEnabledCb.checked,
|
|
mapping: skillBindingState.mapping,
|
|
});
|
|
skillBindingState.mapping = r.mapping && typeof r.mapping === "object" ? { ...r.mapping } : {};
|
|
skillBindingState.enabled = !!r.enabled;
|
|
skillBindingEnabledCb.checked = skillBindingState.enabled;
|
|
applyBindingEnvHint(r);
|
|
skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)}`;
|
|
renderSkillBindingList();
|
|
renderSkillBindingDashboard();
|
|
await loadSkillEffective();
|
|
} catch (e) {
|
|
skillBindingStatus.textContent = String(e && e.message ? e.message : e);
|
|
}
|
|
},
|
|
});
|
|
|
|
// --- Diagnostics: internal tools ---
|
|
const internalToolsState = { role: "", available_roles: [], tools: [], skipped_public: [], skipped_expert: [] };
|
|
const lazyLoadState = { internalLoaded: false, llmLoaded: false, selfCheckLoaded: false, skillHealthLoaded: false };
|
|
const internalToolsStatus = el("div", { class: "muted", text: "" });
|
|
const internalRoleSelect = el("select", { class: "input" }, []);
|
|
const internalToolsTbody = el("tbody");
|
|
const internalSkippedPre = el("pre", { class: "muted pre", text: "" });
|
|
|
|
const renderInternalTools = () => {
|
|
internalToolsTbody.innerHTML = "";
|
|
const rows = Array.isArray(internalToolsState.tools) ? internalToolsState.tools : [];
|
|
rows.forEach((x) => {
|
|
internalToolsTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: String(x.source || "") }),
|
|
el("td", { text: String(x.name || "") }),
|
|
el("td", { text: shortText(String(x.description || ""), 120) }),
|
|
el("td", { text: String((Array.isArray(x.tags) ? x.tags.join(", ") : x.tags) || "") }),
|
|
el("td", { text: String(x.read_only ? "1" : "0") }),
|
|
el("td", { text: String(x.risk_level || "") }),
|
|
]),
|
|
);
|
|
});
|
|
if (!rows.length) {
|
|
internalToolsTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "6" })]));
|
|
}
|
|
internalSkippedPre.textContent = JSON.stringify(
|
|
{
|
|
skipped_public: Array.isArray(internalToolsState.skipped_public) ? internalToolsState.skipped_public : [],
|
|
skipped_expert: Array.isArray(internalToolsState.skipped_expert) ? internalToolsState.skipped_expert : [],
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
};
|
|
|
|
const loadInternalToolsPreview = async (role) => {
|
|
const r = String(role || internalRoleSelect.value || "").trim();
|
|
if (!r) return;
|
|
internalToolsStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet(`/admin/api/tools/internal/preview?role=${encodeURIComponent(r)}`);
|
|
internalToolsState.role = String(resp.role || "");
|
|
internalToolsState.available_roles = Array.isArray(resp.available_roles) ? resp.available_roles : [];
|
|
internalToolsState.tools = Array.isArray(resp.tools) ? resp.tools : [];
|
|
internalToolsState.skipped_public = Array.isArray(resp.skipped_public) ? resp.skipped_public : [];
|
|
internalToolsState.skipped_expert = Array.isArray(resp.skipped_expert) ? resp.skipped_expert : [];
|
|
internalToolsStatus.textContent = `role=${internalToolsState.role} tools=${internalToolsState.tools.length}`;
|
|
renderInternalTools();
|
|
} catch (e) {
|
|
internalToolsState.tools = [];
|
|
internalToolsStatus.textContent = `preview: ${String(e && e.message ? e.message : e)}`;
|
|
renderInternalTools();
|
|
}
|
|
};
|
|
|
|
const btnInternalPreview = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.preview"),
|
|
onclick: async () => await loadInternalToolsPreview(internalRoleSelect.value),
|
|
});
|
|
|
|
const internalToolsInner = [
|
|
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.internalTools") }),
|
|
internalToolsStatus,
|
|
el("div", { class: "row u-row-wrap-mt" }, [
|
|
el("label", { text: t("skills.label.role") }),
|
|
internalRoleSelect,
|
|
btnInternalPreview,
|
|
el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.clearCache"),
|
|
onclick: async () => {
|
|
try {
|
|
await apiPost("/admin/api/tools/internal/reload", {});
|
|
internalToolsStatus.textContent = "cache cleared";
|
|
await loadInternalToolsPreview(internalRoleSelect.value);
|
|
} catch (e) {
|
|
internalToolsStatus.textContent = `reload: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
},
|
|
}),
|
|
]),
|
|
el("div", { class: "table-wrap u-mt-8" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.source") }),
|
|
el("th", { text: t("skills.col.name") }),
|
|
el("th", { text: t("skills.col.description") }),
|
|
el("th", { text: t("skills.col.tags") }),
|
|
el("th", { text: t("skills.col.readOnly") }),
|
|
el("th", { text: t("skills.col.risk") }),
|
|
]),
|
|
]),
|
|
internalToolsTbody,
|
|
]),
|
|
]),
|
|
el("div", { class: "muted u-mt-8", text: t("skills.hint.skippedModules") }),
|
|
internalSkippedPre,
|
|
];
|
|
const internalToolsBox = skillsFold(t("skills.fold.internalTools"), internalToolsInner);
|
|
|
|
// --- Diagnostics: LLM tools ---
|
|
const llmToolsState = { role: "", tools_raw: [], tools_wired: [], removed_mcp_names: [], meta: {} };
|
|
const llmToolsStatus = el("div", { class: "muted", text: "" });
|
|
const llmRoleSelect = el("select", { class: "input" }, []);
|
|
const llmTracePlanCb = el("input", { type: "checkbox" });
|
|
const llmToolsTbody = el("tbody");
|
|
const llmRemovedPre = el("pre", { class: "muted pre", text: "" });
|
|
const llmModeBadge = el("span", { class: "badge badge--mode-restricted", text: "restricted" });
|
|
const llmDiffPre = el("pre", { class: "muted pre", text: "" });
|
|
const llmDiffTbody = el("tbody");
|
|
const llmDiffTableWrap = el("div", { class: "table-wrap u-mt-8" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.type") }),
|
|
el("th", { text: t("skills.col.name") }),
|
|
el("th", { text: t("skills.col.kind") }),
|
|
el("th", { text: t("skills.col.details") }),
|
|
]),
|
|
]),
|
|
llmDiffTbody,
|
|
]),
|
|
]);
|
|
|
|
// --- Diagnostics: tools self-check ---
|
|
const selfCheckStatus = el("div", { class: "muted", text: "" });
|
|
const selfCheckSummary = el("div", { class: "muted", text: "" });
|
|
const selfCheckTbody = el("tbody");
|
|
const selfCheckState = { data: null };
|
|
|
|
const renderSelfCheck = (data) => {
|
|
const body = data && typeof data === "object" ? data : {};
|
|
selfCheckState.data = body;
|
|
const summary = body.summary && typeof body.summary === "object" ? body.summary : {};
|
|
const rolesTotal = Number(body.roles_total || 0);
|
|
const totalInternal = Number(summary.total_internal_tools || 0);
|
|
const totalWired = Number(summary.total_wired_tools || 0);
|
|
const items = Array.isArray(body.items) ? body.items : [];
|
|
selfCheckSummary.textContent = `roles=${rolesTotal} internal=${totalInternal} wired=${totalWired}`;
|
|
selfCheckTbody.innerHTML = "";
|
|
items.forEach((x) => {
|
|
const removedMcp = Number(x.removed_mcp_total || 0);
|
|
const wired = Number(x.wired_count || 0);
|
|
const mode = String(x.role_mode || "restricted");
|
|
let health = "ok";
|
|
if (mode === "forbidden" && wired === 0) health = "forbidden";
|
|
else if (mode === "unrestricted") health = "unrestricted";
|
|
else if (removedMcp > 0) health = "warning";
|
|
const badgeCls =
|
|
health === "warning"
|
|
? "badge badge--mode-restricted"
|
|
: health === "forbidden"
|
|
? "badge badge--bad"
|
|
: health === "unrestricted"
|
|
? "badge badge--mode-unrestricted"
|
|
: "badge badge--ok";
|
|
selfCheckTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: String(x.role || "") }),
|
|
el("td", {}, [el("span", { class: badgeCls, text: health })]),
|
|
el("td", { text: String(x.role_mode || "") }),
|
|
el("td", { text: String(x.internal_count || 0) }),
|
|
el("td", { text: String(x.raw_count || 0) }),
|
|
el("td", { text: String(x.wired_count || 0) }),
|
|
el("td", { text: String(x.removed_mcp_total || 0) }),
|
|
]),
|
|
);
|
|
});
|
|
if (!items.length) selfCheckTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "8" })]));
|
|
};
|
|
|
|
const runSelfCheck = async () => {
|
|
selfCheckStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet("/admin/api/tools/self-check");
|
|
renderSelfCheck(resp);
|
|
selfCheckStatus.textContent = "self-check complete";
|
|
} catch (e) {
|
|
selfCheckStatus.textContent = `self-check: ${String(e && e.message ? e.message : e)}`;
|
|
renderSelfCheck({});
|
|
}
|
|
};
|
|
|
|
const exportSelfCheckJson = () => {
|
|
const data = selfCheckState.data;
|
|
if (!data || typeof data !== "object") {
|
|
selfCheckStatus.textContent = "no self-check data to export";
|
|
return;
|
|
}
|
|
try {
|
|
const blob = new Blob([`${JSON.stringify(data, null, 2)}\n`], { type: "application/json;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `tools-self-check-${ts}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
selfCheckStatus.textContent = "self-check exported";
|
|
} catch (e) {
|
|
selfCheckStatus.textContent = `export failed: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
|
|
const selfCheckInner = [
|
|
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.toolsSelfCheck") }),
|
|
selfCheckStatus,
|
|
el("div", { class: "row u-row-center-mt" }, [
|
|
el("button", { class: "btn btn--primary", text: t("skills.action.runSelfCheck"), onclick: runSelfCheck }),
|
|
el("button", { class: "btn", text: t("skills.action.exportSelfCheck"), onclick: exportSelfCheckJson }),
|
|
selfCheckSummary,
|
|
]),
|
|
el("div", { class: "table-wrap u-mt-8" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.role") }),
|
|
el("th", { text: t("skills.col.health") }),
|
|
el("th", { text: t("skills.col.mode") }),
|
|
el("th", { text: t("skills.col.internal") }),
|
|
el("th", { text: t("skills.col.raw") }),
|
|
el("th", { text: t("skills.col.wired") }),
|
|
el("th", { text: t("skills.col.removedMcp") }),
|
|
]),
|
|
]),
|
|
selfCheckTbody,
|
|
]),
|
|
]),
|
|
];
|
|
const selfCheckBox = skillsFold(t("skills.fold.toolsSelfCheck"), selfCheckInner);
|
|
|
|
// --- Diagnostics: skills health ---
|
|
const skillHealthStatus = el("div", { class: "muted", text: "" });
|
|
const skillHealthSummary = el("div", { class: "muted", text: "" });
|
|
const skillHealthClassifyTbody = el("tbody");
|
|
const skillHealthExecTbody = el("tbody");
|
|
const skillHealthFailedOnlyCb = el("input", { type: "checkbox" });
|
|
const skillHealthState = { data: null };
|
|
|
|
const renderSkillHealthExecRows = () => {
|
|
const body = skillHealthState.data && typeof skillHealthState.data === "object" ? skillHealthState.data : {};
|
|
const checks = Array.isArray(body.execution_checks) ? body.execution_checks : [];
|
|
const failedOnly = !!skillHealthFailedOnlyCb.checked;
|
|
skillHealthExecTbody.innerHTML = "";
|
|
checks
|
|
.filter((x) => (failedOnly ? !x.ok : true))
|
|
.slice(0, 30)
|
|
.forEach((x) => {
|
|
const ok = !!x.ok;
|
|
const code = String(x.error_code || "");
|
|
const truncated = !!x.output_truncated;
|
|
skillHealthExecTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: String(x.name || "") }),
|
|
el("td", {}, [
|
|
ok
|
|
? el("span", { class: "badge badge--ok", text: "ok" })
|
|
: el("span", { class: "badge badge--bad", text: "fail" }),
|
|
]),
|
|
el("td", { text: code || "-" }),
|
|
el("td", { text: truncated ? "1" : "0" }),
|
|
el("td", {}, [
|
|
el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.testRunArgs"),
|
|
disabled: !String(x.name || "").trim(),
|
|
onclick: () => {
|
|
const nm = String(x.name || "").trim();
|
|
if (!nm) return;
|
|
openSkillTestRunModal(nm);
|
|
},
|
|
}),
|
|
]),
|
|
]),
|
|
);
|
|
});
|
|
if (!skillHealthExecTbody.children.length) {
|
|
skillHealthExecTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "5" })]));
|
|
}
|
|
};
|
|
|
|
const renderSkillHealthCheck = (data) => {
|
|
const body = data && typeof data === "object" ? data : {};
|
|
skillHealthState.data = body;
|
|
const counts = body.classification_counts && typeof body.classification_counts === "object" ? body.classification_counts : {};
|
|
const skillsTotal = Number(body.skills_total || 0);
|
|
const execTotal = Number(body.executable_total || 0);
|
|
const checked = Number(body.execution_checked_total || 0);
|
|
skillHealthSummary.textContent = `skills=${skillsTotal} executable=${execTotal} checked=${checked}`;
|
|
skillHealthClassifyTbody.innerHTML = "";
|
|
const countRows = Object.entries(counts).sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0));
|
|
countRows.forEach(([k, v]) => {
|
|
skillHealthClassifyTbody.appendChild(
|
|
el("tr", {}, [el("td", { text: String(k || "") }), el("td", { text: String(Number(v || 0)) })]),
|
|
);
|
|
});
|
|
if (!countRows.length) {
|
|
skillHealthClassifyTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "2" })]));
|
|
}
|
|
renderSkillHealthExecRows();
|
|
};
|
|
skillHealthFailedOnlyCb.addEventListener("change", () => renderSkillHealthExecRows());
|
|
|
|
const runSkillHealthCheck = async () => {
|
|
skillHealthStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet("/admin/api/skills/self-check?include_execution=true");
|
|
renderSkillHealthCheck(resp);
|
|
skillHealthStatus.textContent = "skill self-check complete";
|
|
} catch (e) {
|
|
skillHealthStatus.textContent = `skill self-check: ${String(e && e.message ? e.message : e)}`;
|
|
renderSkillHealthCheck({});
|
|
}
|
|
};
|
|
|
|
const exportSkillHealthJson = () => {
|
|
const data = skillHealthState.data;
|
|
if (!data || typeof data !== "object") {
|
|
skillHealthStatus.textContent = "no skills self-check data to export";
|
|
return;
|
|
}
|
|
try {
|
|
const blob = new Blob([`${JSON.stringify(data, null, 2)}\n`], { type: "application/json;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `skills-self-check-${ts}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
skillHealthStatus.textContent = "skills self-check exported";
|
|
} catch (e) {
|
|
skillHealthStatus.textContent = `export failed: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
|
|
const skillHealthInner = [
|
|
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.skillHealth") }),
|
|
skillHealthStatus,
|
|
el("div", { class: "row u-row-center-mt" }, [
|
|
el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.runSkillHealth"),
|
|
onclick: runSkillHealthCheck,
|
|
}),
|
|
el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.exportSkillHealth"),
|
|
onclick: exportSkillHealthJson,
|
|
}),
|
|
el("label", { class: "row u-row-center" }, [
|
|
skillHealthFailedOnlyCb,
|
|
el("span", { class: "muted", text: t("skills.action.failedOnly") }),
|
|
]),
|
|
skillHealthSummary,
|
|
]),
|
|
el("div", { class: "row", style: "gap:12px;align-items:flex-start;flex-wrap:wrap;margin-top:8px;" }, [
|
|
el("div", { class: "table-wrap", style: "min-width:280px;flex:1;" }, [
|
|
el("div", { class: "muted u-mb-6", text: "classification_counts" }),
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [el("th", { text: t("skills.col.code") }), el("th", { text: t("skills.col.count") })]),
|
|
]),
|
|
skillHealthClassifyTbody,
|
|
]),
|
|
]),
|
|
el("div", { class: "table-wrap", style: "min-width:360px;flex:2;" }, [
|
|
el("div", { class: "muted u-mb-6", text: "execution_checks (top 30)" }),
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.skill") }),
|
|
el("th", { text: t("skills.col.status") }),
|
|
el("th", { text: t("skills.col.errorCode") }),
|
|
el("th", { text: t("skills.col.outputTruncated") }),
|
|
el("th", { text: t("skills.col.action") }),
|
|
]),
|
|
]),
|
|
skillHealthExecTbody,
|
|
]),
|
|
]),
|
|
]),
|
|
];
|
|
const skillHealthBox = skillsFold(t("skills.fold.health"), skillHealthInner);
|
|
|
|
const _renderLLMToolRows = (tools) => {
|
|
llmToolsTbody.innerHTML = "";
|
|
const rows = Array.isArray(tools) ? tools : [];
|
|
rows.forEach((ent) => {
|
|
const fn = ent && typeof ent === "object" ? ent.function : null;
|
|
const name = fn && typeof fn === "object" ? String(fn.name || "") : "";
|
|
const desc = fn && typeof fn === "object" ? String(fn.description || "") : "";
|
|
const params = fn && typeof fn === "object" ? fn.parameters : null;
|
|
llmToolsTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: name }),
|
|
el("td", { text: shortText(desc, 140) }),
|
|
el("td", { text: name.startsWith("mcp__") ? "mcp" : "internal" }),
|
|
el("td", { text: params ? shortText(JSON.stringify(params), 120) : "" }),
|
|
]),
|
|
);
|
|
});
|
|
if (!rows.length) {
|
|
llmToolsTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "4" })]));
|
|
}
|
|
};
|
|
|
|
const loadLLMToolsPreview = async (role) => {
|
|
const r = String(role || llmRoleSelect.value || "").trim();
|
|
if (!r) return;
|
|
llmToolsStatus.textContent = `${t("chat.loading")}...`;
|
|
try {
|
|
const resp = await apiGet(`/admin/api/tools/llm/preview?role=${encodeURIComponent(r)}`);
|
|
llmToolsState.role = String(resp.role || "");
|
|
llmToolsState.tools_raw = Array.isArray(resp.tools_raw) ? resp.tools_raw : [];
|
|
llmToolsState.tools_wired = Array.isArray(resp.tools_wired) ? resp.tools_wired : [];
|
|
llmToolsState.removed_mcp_names = Array.isArray(resp.removed_mcp_names) ? resp.removed_mcp_names : [];
|
|
llmToolsState.meta = resp && typeof resp === "object" ? resp : {};
|
|
const mode = String(resp.role_mode || "restricted");
|
|
llmModeBadge.textContent = mode;
|
|
llmModeBadge.className = `badge badge--mode-${mode}`;
|
|
llmToolsStatus.textContent = `role=${llmToolsState.role} raw=${llmToolsState.tools_raw.length} wired=${llmToolsState.tools_wired.length} mcp_enabled=${String(!!resp.mcp_enabled)}`;
|
|
llmRemovedPre.textContent = JSON.stringify(
|
|
{
|
|
removed_mcp_names: llmToolsState.removed_mcp_names,
|
|
role_mode: resp.role_mode,
|
|
skipped_public: resp.skipped_public || [],
|
|
skipped_expert: resp.skipped_expert || [],
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
const _toMap = (arr) => {
|
|
const m = new Map();
|
|
(Array.isArray(arr) ? arr : []).forEach((ent) => {
|
|
const fn = ent && typeof ent === "object" ? ent.function : null;
|
|
const nm = fn && typeof fn === "object" ? String(fn.name || "") : "";
|
|
if (!nm) return;
|
|
const desc = fn && typeof fn === "object" ? String(fn.description || "") : "";
|
|
const params = fn && typeof fn === "object" ? fn.parameters : null;
|
|
m.set(nm, { description: desc, parameters: params });
|
|
});
|
|
return m;
|
|
};
|
|
const rawMap = _toMap(llmToolsState.tools_raw);
|
|
const wiredMap = _toMap(llmToolsState.tools_wired);
|
|
const rawNames = Array.from(rawMap.keys());
|
|
const wiredNames = Array.from(wiredMap.keys());
|
|
const rawSet = new Set(rawNames);
|
|
const wiredSet = new Set(wiredNames);
|
|
const removed = rawNames.filter((n) => !wiredSet.has(n)).sort();
|
|
const added = wiredNames.filter((n) => !rawSet.has(n)).sort();
|
|
const changed = [];
|
|
wiredNames.forEach((n) => {
|
|
if (!rawSet.has(n)) return;
|
|
const a = rawMap.get(n) || {};
|
|
const b = wiredMap.get(n) || {};
|
|
const d0 = String(a.description || "");
|
|
const d1 = String(b.description || "");
|
|
const p0 = a.parameters ? JSON.stringify(a.parameters) : "";
|
|
const p1 = b.parameters ? JSON.stringify(b.parameters) : "";
|
|
if (d0 !== d1 || p0 !== p1) changed.push(n);
|
|
});
|
|
llmDiffPre.textContent = JSON.stringify(
|
|
{
|
|
added,
|
|
removed,
|
|
changed,
|
|
counts: {
|
|
raw: rawNames.length,
|
|
wired: wiredNames.length,
|
|
added: added.length,
|
|
removed: removed.length,
|
|
changed: changed.length,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
|
|
llmDiffTbody.innerHTML = "";
|
|
const _kind = (nm) => (String(nm || "").startsWith("mcp__") ? "mcp" : "internal");
|
|
const _pushRow = (tp, nm, detailEl) => {
|
|
llmDiffTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: tp }),
|
|
el("td", { text: nm }),
|
|
el("td", { text: _kind(nm) }),
|
|
el("td", {}, [detailEl]),
|
|
]),
|
|
);
|
|
};
|
|
const _detailsCompare = (nm) => {
|
|
const a = rawMap.get(nm) || {};
|
|
const b = wiredMap.get(nm) || {};
|
|
const d0 = String(a.description || "");
|
|
const d1 = String(b.description || "");
|
|
const p0 = a.parameters ? JSON.stringify(a.parameters, null, 2) : "";
|
|
const p1 = b.parameters ? JSON.stringify(b.parameters, null, 2) : "";
|
|
return el("details", {}, [
|
|
el("summary", { class: "u-drag", text: "compare" }),
|
|
el("div", { class: "muted u-mt-6" }, [el("div", { text: "raw.description" }), el("pre", { class: "muted pre", text: d0 })]),
|
|
el("div", { class: "muted u-mt-6" }, [el("div", { text: "wired.description" }), el("pre", { class: "muted pre", text: d1 })]),
|
|
el("div", { class: "muted u-mt-6" }, [el("div", { text: "raw.parameters" }), el("pre", { class: "muted pre", text: p0 })]),
|
|
el("div", { class: "muted u-mt-6" }, [el("div", { text: "wired.parameters" }), el("pre", { class: "muted pre", text: p1 })]),
|
|
]);
|
|
};
|
|
added.forEach((nm) => _pushRow("added", nm, el("span", { class: "muted", text: "present in wired only" })));
|
|
removed.forEach((nm) => _pushRow("removed", nm, el("span", { class: "muted", text: "present in raw only" })));
|
|
changed.sort().forEach((nm) => _pushRow("changed", nm, _detailsCompare(nm)));
|
|
if (!added.length && !removed.length && !changed.length) {
|
|
llmDiffTbody.appendChild(el("tr", {}, [el("td", { text: "-", colspan: "4" })]));
|
|
}
|
|
|
|
_renderLLMToolRows(llmToolsState.tools_wired);
|
|
} catch (e) {
|
|
llmToolsState.tools_wired = [];
|
|
llmToolsStatus.textContent = `preview: ${String(e && e.message ? e.message : e)}`;
|
|
llmRemovedPre.textContent = "";
|
|
llmDiffPre.textContent = "";
|
|
llmDiffTbody.innerHTML = "";
|
|
_renderLLMToolRows([]);
|
|
}
|
|
};
|
|
|
|
const btnLLMPreview = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.preview"),
|
|
onclick: async () => await loadLLMToolsPreview(llmRoleSelect.value),
|
|
});
|
|
const loadExposureTraceSetting = async () => {
|
|
try {
|
|
const resp = await apiGet("/admin/api/tools/exposure-trace-setting");
|
|
llmTracePlanCb.checked = !!(resp && resp.enabled);
|
|
} catch (_) {
|
|
llmTracePlanCb.checked = false;
|
|
}
|
|
};
|
|
const saveExposureTraceSetting = async () => {
|
|
try {
|
|
const resp = await apiPost("/admin/api/tools/exposure-trace-setting", { enabled: !!llmTracePlanCb.checked });
|
|
llmTracePlanCb.checked = !!(resp && resp.enabled);
|
|
llmToolsStatus.textContent = `trace_exposure_plan=${llmTracePlanCb.checked ? "on" : "off"}`;
|
|
} catch (e) {
|
|
llmToolsStatus.textContent = `trace setting: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
};
|
|
const btnLLMToggle = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.toggleRawWired"),
|
|
onclick: () => {
|
|
const showing = String(btnLLMToggle.dataset.showing || "wired");
|
|
const next = showing === "wired" ? "raw" : "wired";
|
|
btnLLMToggle.dataset.showing = next;
|
|
_renderLLMToolRows(next === "wired" ? llmToolsState.tools_wired : llmToolsState.tools_raw);
|
|
},
|
|
});
|
|
|
|
const llmToolsInner = [
|
|
el("div", { class: "muted u-muted-block-lg", text: t("skills.hint.llmTools") }),
|
|
llmToolsStatus,
|
|
el("div", { class: "row u-row-wrap-mt" }, [
|
|
el("label", { text: t("skills.label.role") }),
|
|
llmRoleSelect,
|
|
btnLLMPreview,
|
|
btnLLMToggle,
|
|
el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.clearCache"),
|
|
onclick: async () => {
|
|
try {
|
|
await apiPost("/admin/api/tools/internal/reload", {});
|
|
llmToolsStatus.textContent = "cache cleared";
|
|
await loadLLMToolsPreview(llmRoleSelect.value);
|
|
} catch (e) {
|
|
llmToolsStatus.textContent = `reload: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
},
|
|
}),
|
|
el("span", { class: "muted", text: "role_mode:" }),
|
|
llmModeBadge,
|
|
el("label", { class: "row u-row-center" }, [
|
|
llmTracePlanCb,
|
|
el("span", { class: "muted", text: t("skills.action.traceExposure") }),
|
|
]),
|
|
el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.saveTrace"),
|
|
onclick: saveExposureTraceSetting,
|
|
}),
|
|
]),
|
|
el("div", { class: "table-wrap u-mt-8" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.name") }),
|
|
el("th", { text: t("skills.col.description") }),
|
|
el("th", { text: t("skills.col.kind") }),
|
|
el("th", { text: t("skills.col.parameters") }),
|
|
]),
|
|
]),
|
|
llmToolsTbody,
|
|
]),
|
|
]),
|
|
el("div", { class: "muted u-mt-8", text: t("skills.hint.diffDiag") }),
|
|
llmRemovedPre,
|
|
el("div", { class: "muted u-mt-8", text: t("skills.hint.rawWiredDiff") }),
|
|
llmDiffPre,
|
|
llmDiffTableWrap,
|
|
];
|
|
const llmToolsBox = skillsFold(t("skills.fold.llmTools"), llmToolsInner);
|
|
|
|
// --- Installed table + audit + modals ---
|
|
const tbody = el("tbody");
|
|
const auditTbody = el("tbody");
|
|
let skillTestRunName = "";
|
|
|
|
const skillTestRunModal = el("div", { class: "session-monitor-modal u-hidden", "data-skill-modal": "testrun" });
|
|
const skillTestRunTitle = el("div", { class: "card__title", text: t("skills.modal.testRun") });
|
|
const skillTestRunArgs = el("textarea", {
|
|
class: "input",
|
|
rows: "8",
|
|
style: "width:100%;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;",
|
|
placeholder: "{}",
|
|
});
|
|
const skillInstallModal = el("div", { class: "session-monitor-modal u-hidden", "data-skill-modal": "install" });
|
|
const skillInstallTitle = el("div", { class: "card__title", text: t("skills.modal.install") });
|
|
const skillInstallMsg = el("div", { class: "muted", style: "line-height:1.5;" });
|
|
|
|
const closeSkillInstallModal = () => {
|
|
skillInstallModal.classList.add("u-hidden");
|
|
skillInstallModal.style.display = "";
|
|
};
|
|
const openSkillInstallModal = (msg) => {
|
|
skillInstallTitle.textContent = t("skills.modal.install");
|
|
skillInstallMsg.textContent = String(msg || "");
|
|
if (skillInstallModal.parentNode !== document.body) document.body.appendChild(skillInstallModal);
|
|
skillInstallModal.classList.remove("u-hidden");
|
|
skillInstallModal.style.display = "";
|
|
};
|
|
const finishSkillInstallModal = (ok, msg) => {
|
|
skillInstallTitle.textContent = ok ? t("skills.modal.installOk") : t("skills.modal.installFail");
|
|
skillInstallMsg.textContent = String(msg || "");
|
|
setTimeout(() => {
|
|
closeSkillInstallModal();
|
|
}, 1200);
|
|
};
|
|
skillInstallModal.addEventListener("click", (e) => {
|
|
if (e.target === skillInstallModal) closeSkillInstallModal();
|
|
});
|
|
skillInstallModal.appendChild(
|
|
el("div", { class: "card session-monitor-modal__card", style: "width:min(560px,96vw);" }, [
|
|
skillInstallTitle,
|
|
skillInstallMsg,
|
|
el("div", { class: "row u-row-end" }, [
|
|
el("button", { class: "btn", text: t("skills.action.close"), onclick: closeSkillInstallModal }),
|
|
]),
|
|
]),
|
|
);
|
|
|
|
const closeSkillTestRunModal = () => {
|
|
skillTestRunModal.classList.add("u-hidden");
|
|
skillTestRunModal.style.display = "";
|
|
skillTestRunName = "";
|
|
};
|
|
const openSkillTestRunModal = (name) => {
|
|
skillTestRunName = String(name || "");
|
|
skillTestRunTitle.textContent = tf("skills.modal.testRunNamed", { name: skillTestRunName });
|
|
skillTestRunArgs.value = "{}";
|
|
if (skillTestRunModal.parentNode !== document.body) document.body.appendChild(skillTestRunModal);
|
|
skillTestRunModal.classList.remove("u-hidden");
|
|
skillTestRunModal.style.display = "";
|
|
setTimeout(() => skillTestRunArgs.focus(), 0);
|
|
};
|
|
skillTestRunModal.addEventListener("click", (e) => {
|
|
if (e.target === skillTestRunModal) closeSkillTestRunModal();
|
|
});
|
|
const skillTestRunRunBtn = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.run"),
|
|
onclick: async () => {
|
|
if (!skillTestRunName) return;
|
|
let args = {};
|
|
try {
|
|
const raw = String(skillTestRunArgs.value || "").trim();
|
|
args = raw ? JSON.parse(raw) : {};
|
|
} catch (e) {
|
|
status.textContent = `invalid json: ${String(e && e.message ? e.message : e)}`;
|
|
return;
|
|
}
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/test-run", { name: skillTestRunName, args });
|
|
status.textContent = `test-run: ${skillTestRunName} => ${JSON.stringify((r && r.result) || {}, null, 0)}`;
|
|
closeSkillTestRunModal();
|
|
} catch (e) {
|
|
status.textContent = `test-run: ${String(e && e.message ? e.message : e)}`;
|
|
}
|
|
},
|
|
});
|
|
const skillTestRunCancelBtn = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.cancel"),
|
|
onclick: closeSkillTestRunModal,
|
|
});
|
|
skillTestRunModal.appendChild(
|
|
el("div", { class: "card session-monitor-modal__card", style: "width:min(760px,96vw);" }, [
|
|
skillTestRunTitle,
|
|
el("div", { class: "muted", style: "margin-bottom:8px;", text: t("skills.hint.testRunArgs") }),
|
|
skillTestRunArgs,
|
|
el("div", { class: "row u-row-end" }, [skillTestRunCancelBtn, skillTestRunRunBtn]),
|
|
]),
|
|
);
|
|
|
|
const retryableOnlyCb = el("input", { type: "checkbox" });
|
|
const retryableOnlyStored = String(localStorage.getItem(SKILL_AUDIT_RETRYABLE_ONLY_KEY) || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
retryableOnlyCb.checked = retryableOnlyStored
|
|
? retryableOnlyStored !== "0" && retryableOnlyStored !== "false"
|
|
: true;
|
|
|
|
const _parseSourceFromAudit = (it) => {
|
|
const d = it && typeof it.detail === "object" ? it.detail : {};
|
|
const s = String(d.source || "").trim().toLowerCase();
|
|
if (s === "local" || s === "registry" || s === "auto") return s;
|
|
return "";
|
|
};
|
|
const _parseRetryTargetFromAudit = (it) => {
|
|
const d = it && typeof it.detail === "object" ? it.detail : {};
|
|
const inputTarget = String(d.input_target || "").trim();
|
|
if (inputTarget) return inputTarget;
|
|
return String((it && it.target_id) || "").trim();
|
|
};
|
|
const _isRetryableAudit = (it) => {
|
|
const action = String((it && it.action) || "").trim().toLowerCase();
|
|
if (action !== "skill_install_failed") return false;
|
|
const src = _parseSourceFromAudit(it);
|
|
const target = _parseRetryTargetFromAudit(it);
|
|
return Boolean(src && target);
|
|
};
|
|
|
|
const loadRows = async () => {
|
|
const data = await apiGet("/admin/api/skills");
|
|
rowsState.items = Array.isArray(data.items) ? data.items : [];
|
|
};
|
|
const loadAudits = async () => {
|
|
const data = await apiGet("/admin/api/admin-audit?limit=80");
|
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
auditState.items = items.filter((x) => String(x.action || "").startsWith("skill_")).slice(0, 20);
|
|
};
|
|
|
|
const updateInstalledFoldTitle = () => {
|
|
if (foldInstalledSummary) {
|
|
foldInstalledSummary.textContent = tf("skills.fold.installed", { n: (rowsState.items || []).length });
|
|
}
|
|
};
|
|
let foldInstalledSummary = null;
|
|
|
|
const repaint = () => {
|
|
const rows = rowsState.items || [];
|
|
tbody.innerHTML = "";
|
|
rows.forEach((x) => {
|
|
const name = String(x.name || "");
|
|
const enabled = !!x.enabled;
|
|
const executable = !!x.executable;
|
|
const runtime = x && typeof x.runtime === "object" ? x.runtime : null;
|
|
const runtimeType = runtime && typeof runtime.type === "string" ? String(runtime.type) : "";
|
|
const runtimeEntry = runtime && typeof runtime.entry === "string" ? String(runtime.entry) : "";
|
|
const actions = rowActions("⋯", [
|
|
{
|
|
label: enabled ? t("skills.action.disable") : t("skills.action.enable"),
|
|
onClick: async () => {
|
|
await apiPost(enabled ? "/admin/api/skills/disable" : "/admin/api/skills/enable", { name });
|
|
status.textContent = `${enabled ? "Disabled" : "Enabled"}: ${name}`;
|
|
await loadRows();
|
|
repaint();
|
|
},
|
|
},
|
|
{
|
|
label: t("skills.action.testRun"),
|
|
disabled: !executable,
|
|
onClick: () => openSkillTestRunModal(name),
|
|
},
|
|
{
|
|
label: t("skills.action.repairDeps"),
|
|
onClick: async () => {
|
|
status.textContent = `repair deps: ${name}...`;
|
|
const r = await apiPost("/admin/api/skills/repair-deps", { name });
|
|
assertSkillMutationOk(r, "Repair deps failed");
|
|
status.textContent = `repair deps: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
|
|
await loadRows();
|
|
await loadAudits();
|
|
repaint();
|
|
},
|
|
},
|
|
{
|
|
label: t("skills.action.uninstall"),
|
|
danger: true,
|
|
onClick: async () => {
|
|
if (!confirm(tf("skills.confirm.uninstall", { name }))) return;
|
|
status.textContent = `uninstalling: ${name}...`;
|
|
const r = await apiPost("/admin/api/skills/uninstall", { name });
|
|
assertSkillMutationOk(r, "Uninstall failed");
|
|
status.textContent = `uninstall success: ${JSON.stringify((r && r.result) || {}, null, 0)}`;
|
|
await loadRows();
|
|
await loadAudits();
|
|
repaint();
|
|
},
|
|
},
|
|
]);
|
|
tbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: name }),
|
|
el("td", { text: String(x.description || "") }),
|
|
el("td", { text: String(x.enabled ? "1" : "0") }),
|
|
el("td", {}, [
|
|
executable
|
|
? el("span", { class: "badge badge--ok", text: `${runtimeType || "runtime"}` })
|
|
: el("span", { class: "muted", text: "docs-only" }),
|
|
]),
|
|
el("td", { text: executable ? `${runtimeEntry}` : "-" }),
|
|
el("td", { text: String(x.skill_dir || "") }),
|
|
el("td", { class: "table__cell--actions", "data-copy-disabled": "1" }, [
|
|
el("div", { class: "table__cell-actions" }, [actions]),
|
|
]),
|
|
]),
|
|
);
|
|
});
|
|
updateInstalledFoldTitle();
|
|
|
|
auditTbody.innerHTML = "";
|
|
const auditRows = (auditState.items || []).filter((x) => (!retryableOnlyCb.checked ? true : _isRetryableAudit(x)));
|
|
for (const x of auditRows) {
|
|
const src = _parseSourceFromAudit(x);
|
|
const isRetryable = _isRetryableAudit(x);
|
|
const retryBtn = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.retry"),
|
|
disabled: !isRetryable,
|
|
onclick: async () => {
|
|
try {
|
|
const target = _parseRetryTargetFromAudit(x);
|
|
if (!src || !target || !isRetryable) {
|
|
status.textContent = "retry skipped: missing source/target";
|
|
return;
|
|
}
|
|
const r = await apiPost("/admin/api/skills/retry-install", { source: src, target });
|
|
assertSkillMutationOk(r, "Retry install failed");
|
|
status.textContent = `retry: ${JSON.stringify(r.result || {})}`;
|
|
await loadRows();
|
|
await loadAudits();
|
|
repaint();
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
}
|
|
},
|
|
});
|
|
auditTbody.appendChild(
|
|
el("tr", {}, [
|
|
el("td", { text: String(x.timestamp || "") }),
|
|
el("td", { text: String(x.action || "") }),
|
|
el("td", { text: String(x.target_id || "") }),
|
|
el("td", { text: String(x.status || "") }),
|
|
el("td", { text: shortText(JSON.stringify(x.detail || {}), 180) }),
|
|
el("td", {}, [retryBtn]),
|
|
]),
|
|
);
|
|
}
|
|
};
|
|
|
|
const refreshSkillsState = async () => {
|
|
await Promise.all([loadRows(), loadAudits(), loadSkillBinding()]);
|
|
repaint();
|
|
renderSkillBindingList();
|
|
};
|
|
|
|
try {
|
|
await Promise.all([loadRows(), loadAudits(), loadSkillBinding(), loadSkillMode()]);
|
|
const rolesForPreview = (
|
|
Array.isArray(skillBindingState.roles) && skillBindingState.roles.length
|
|
? skillBindingState.roles
|
|
: ["generalist", "ops", "memory"]
|
|
).map((x) => String(x));
|
|
internalRoleSelect.innerHTML = "";
|
|
rolesForPreview.forEach((role) => internalRoleSelect.appendChild(el("option", { value: role, text: role })));
|
|
internalRoleSelect.value = rolesForPreview.includes("generalist") ? "generalist" : rolesForPreview[0];
|
|
internalToolsStatus.textContent = t("skills.hint.expandToLoad");
|
|
llmRoleSelect.innerHTML = "";
|
|
rolesForPreview.forEach((role) => llmRoleSelect.appendChild(el("option", { value: role, text: role })));
|
|
llmRoleSelect.value = internalRoleSelect.value;
|
|
llmToolsStatus.textContent = t("skills.hint.expandToLoad");
|
|
selfCheckStatus.textContent = t("skills.hint.expandToSelfCheck");
|
|
skillHealthStatus.textContent = t("skills.hint.expandToSelfCheck");
|
|
} catch (e) {
|
|
return el("div", { class: "card" }, [
|
|
el("div", { class: "card__title", text: t("skills.title") }),
|
|
el("div", { class: "muted", text: String(e && e.message ? e.message : e) }),
|
|
]);
|
|
}
|
|
repaint();
|
|
|
|
const btnCreate = el("button", {
|
|
class: "btn btn--primary",
|
|
text: t("skills.action.create"),
|
|
onclick: async () => {
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/create", {
|
|
name: String(nameInp.value || "").trim(),
|
|
description: String(descInp.value || "").trim(),
|
|
body_markdown: String(bodyInp.value || ""),
|
|
});
|
|
assertSkillMutationOk(r, "Create skill failed");
|
|
status.textContent = `create: ${JSON.stringify(r.result || {})}`;
|
|
await loadRows();
|
|
await loadAudits();
|
|
repaint();
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
}
|
|
},
|
|
});
|
|
const btnInstallRegistry = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.installRegistry"),
|
|
onclick: async () => {
|
|
const prev = btnInstallRegistry.textContent;
|
|
btnInstallRegistry.disabled = true;
|
|
btnInstallRegistry.textContent = t("skills.action.installing");
|
|
status.textContent = "installing from registry...";
|
|
openSkillInstallModal("Installing skill from registry...");
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/install-registry", {
|
|
archive_url: String(regInp.value || "").trim(),
|
|
});
|
|
assertSkillMutationOk(r, "Registry install failed");
|
|
status.textContent = `install-registry success: ${JSON.stringify(r.result || {})}`;
|
|
await refreshSkillsState();
|
|
finishSkillInstallModal(true, "Registry skill installed successfully.");
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
|
|
} finally {
|
|
btnInstallRegistry.disabled = false;
|
|
btnInstallRegistry.textContent = prev;
|
|
}
|
|
},
|
|
});
|
|
const btnInstallLocal = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.installLocal"),
|
|
onclick: async () => {
|
|
const prev = btnInstallLocal.textContent;
|
|
btnInstallLocal.disabled = true;
|
|
btnInstallLocal.textContent = t("skills.action.installing");
|
|
status.textContent = "installing from local dir...";
|
|
openSkillInstallModal("Installing skill from local directory...");
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/install", {
|
|
source_dir: String(localDirInp.value || "").trim(),
|
|
});
|
|
assertSkillMutationOk(r, "Local install failed");
|
|
status.textContent = `install-local success: ${JSON.stringify(r.result || {})}`;
|
|
await refreshSkillsState();
|
|
finishSkillInstallModal(true, "Local skill installed successfully.");
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
finishSkillInstallModal(false, `Install failed: ${String(e && e.message ? e.message : e)}`);
|
|
} finally {
|
|
btnInstallLocal.disabled = false;
|
|
btnInstallLocal.textContent = prev;
|
|
}
|
|
},
|
|
});
|
|
const btnRefresh = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.refresh"),
|
|
onclick: async () => {
|
|
try {
|
|
await refreshSkillsState();
|
|
status.textContent = "refreshed";
|
|
} catch (e) {
|
|
status.textContent = String(e && e.message ? e.message : e);
|
|
}
|
|
},
|
|
});
|
|
const btnRepairDepsAll = el("button", {
|
|
class: "btn",
|
|
text: t("skills.action.repairAllDeps"),
|
|
onclick: async () => {
|
|
const prev = btnRepairDepsAll.textContent;
|
|
btnRepairDepsAll.disabled = true;
|
|
btnRepairDepsAll.textContent = t("skills.action.repairing");
|
|
try {
|
|
const r = await apiPost("/admin/api/skills/repair-deps-all", {});
|
|
const s = r && typeof r.summary === "object" ? r.summary : {};
|
|
status.textContent = `repair all deps: total=${Number(s.total || 0)} ok=${Number(s.ok_count || 0)} warn=${Number(s.warn_count || 0)} fail=${Number(s.fail_count || 0)}`;
|
|
await refreshSkillsState();
|
|
} catch (e) {
|
|
status.textContent = `repair all deps failed: ${String(e && e.message ? e.message : e)}`;
|
|
} finally {
|
|
btnRepairDepsAll.disabled = false;
|
|
btnRepairDepsAll.textContent = prev;
|
|
}
|
|
},
|
|
});
|
|
retryableOnlyCb.addEventListener("change", () => {
|
|
localStorage.setItem(SKILL_AUDIT_RETRYABLE_ONLY_KEY, retryableOnlyCb.checked ? "1" : "0");
|
|
repaint();
|
|
});
|
|
|
|
const docsHint = el("div", { class: "muted", style: "margin-bottom:10px;line-height:1.5;" }, [
|
|
el("div", { text: t("skills.docsTitle") }),
|
|
el("div", { text: t("skills.docsTroubleshooting") }),
|
|
el("div", { text: t("skills.docsTraceTaxonomy") }),
|
|
]);
|
|
|
|
internalToolsBox.addEventListener("toggle", async () => {
|
|
if (!internalToolsBox.open || lazyLoadState.internalLoaded) return;
|
|
lazyLoadState.internalLoaded = true;
|
|
await loadInternalToolsPreview(internalRoleSelect.value);
|
|
});
|
|
llmToolsBox.addEventListener("toggle", async () => {
|
|
if (!llmToolsBox.open || lazyLoadState.llmLoaded) return;
|
|
lazyLoadState.llmLoaded = true;
|
|
await Promise.all([loadExposureTraceSetting(), loadLLMToolsPreview(llmRoleSelect.value)]);
|
|
});
|
|
selfCheckBox.addEventListener("toggle", async () => {
|
|
if (!selfCheckBox.open || lazyLoadState.selfCheckLoaded) return;
|
|
lazyLoadState.selfCheckLoaded = true;
|
|
await runSelfCheck();
|
|
});
|
|
skillHealthBox.addEventListener("toggle", async () => {
|
|
if (!skillHealthBox.open || lazyLoadState.skillHealthLoaded) return;
|
|
lazyLoadState.skillHealthLoaded = true;
|
|
await runSkillHealthCheck();
|
|
});
|
|
|
|
// --- Page folds ---
|
|
const foldMode = skillsFold(t("skills.fold.mode"), [
|
|
el("div", { class: "muted", text: t("skills.fold.modeHint") }),
|
|
el("div", { class: "row", style: "gap:8px;align-items:center;flex-wrap:wrap;" }, [
|
|
el("label", { class: "row u-row-center" }, [
|
|
skillPromptModeCb,
|
|
el("span", { text: t("skills.label.promptMode") }),
|
|
]),
|
|
el("button", { class: "btn btn--primary", text: t("skills.action.save"), onclick: saveSkillMode }),
|
|
skillModeStatus,
|
|
]),
|
|
docsHint,
|
|
]);
|
|
|
|
const foldMarket = skillsFold(t("skills.fold.market"), [
|
|
el("div", { class: "row u-row-wrap-mb" }, [marketQ, marketLimitInp, btnMarketSearch, btnMarketLatest]),
|
|
marketStatus,
|
|
el("div", { class: "u-h-8" }),
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.slug") }),
|
|
el("th", { text: t("skills.col.name") }),
|
|
el("th", { text: t("skills.col.version") }),
|
|
el("th", { text: t("skills.col.description") }),
|
|
el("th", { text: t("skills.col.action") }),
|
|
]),
|
|
]),
|
|
marketTbody,
|
|
]),
|
|
]),
|
|
el("div", { class: "u-h-8" }),
|
|
el("div", { class: "muted", text: t("skills.action.detail") }),
|
|
marketDetailPre,
|
|
]);
|
|
|
|
const foldLocalRegistry = skillsFold(t("skills.fold.localRegistry"), [
|
|
el("div", { class: "muted", style: "margin-bottom:8px;line-height:1.5;" }, [
|
|
el("div", { text: t("skills.hintLocalDir") }),
|
|
el("div", { text: t("skills.hintRegistry") }),
|
|
]),
|
|
el("div", { class: "row u-row-wrap-mb" }, [regInp, btnInstallRegistry]),
|
|
el("div", { class: "row u-row-wrap-mb" }, [localDirInp, btnInstallLocal]),
|
|
]);
|
|
|
|
const foldCreate = skillsFold(t("skills.fold.create"), [
|
|
el("div", { class: "row u-row-wrap-mb" }, [nameInp, descInp]),
|
|
el("div", { style: "margin-bottom:8px;" }, [bodyInp]),
|
|
el("div", { class: "row u-row-wrap-mb" }, [btnCreate]),
|
|
]);
|
|
|
|
const foldInstall = skillsFold(t("skills.fold.install"), [
|
|
el("div", { class: "muted", text: t("skills.fold.installHint") }),
|
|
foldMarket,
|
|
foldLocalRegistry,
|
|
foldCreate,
|
|
]);
|
|
foldMarket.open = true;
|
|
|
|
const foldAudit = skillsFold(t("skills.fold.audit"), [
|
|
el("div", { class: "row", style: "gap:8px;align-items:center;" }, [
|
|
el("label", { class: "row u-row-center" }, [
|
|
retryableOnlyCb,
|
|
el("span", { class: "muted", text: t("skills.action.retryableOnly") }),
|
|
]),
|
|
]),
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.timestamp") }),
|
|
el("th", { text: t("skills.col.action") }),
|
|
el("th", { text: t("skills.col.target") }),
|
|
el("th", { text: t("skills.col.status") }),
|
|
el("th", { text: t("skills.col.detail") }),
|
|
el("th", { text: t("skills.col.retry") }),
|
|
]),
|
|
]),
|
|
auditTbody,
|
|
]),
|
|
]),
|
|
]);
|
|
|
|
const foldInstalled = skillsFold(tf("skills.fold.installed", { n: (rowsState.items || []).length }), [
|
|
el("div", { class: "row u-row-wrap-mb" }, [btnRefresh, btnRepairDepsAll]),
|
|
status,
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.name") }),
|
|
el("th", { text: t("skills.col.description") }),
|
|
el("th", { text: t("skills.col.enabled") }),
|
|
el("th", { text: t("skills.col.kind") }),
|
|
el("th", { text: t("skills.col.runtimeEntry") }),
|
|
el("th", { text: t("skills.col.path") }),
|
|
el("th", { class: "table__cell--actions", text: t("skills.col.action") }),
|
|
]),
|
|
]),
|
|
tbody,
|
|
]),
|
|
]),
|
|
foldAudit,
|
|
]);
|
|
foldInstalled.open = true;
|
|
foldInstalledSummary = foldInstalled.querySelector("summary");
|
|
|
|
const foldEffective = skillsFold(t("skills.fold.effective"), [
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.role") }),
|
|
el("th", { text: t("skills.col.totalEffective") }),
|
|
el("th", { text: t("skills.col.workspace") }),
|
|
el("th", { text: t("skills.col.directBind") }),
|
|
el("th", { text: t("skills.col.workspaceResolved") }),
|
|
el("th", { text: t("skills.col.workspaceDocsOnly") }),
|
|
el("th", { text: t("skills.col.mcpConverted") }),
|
|
el("th", { text: t("skills.col.otherTools") }),
|
|
el("th", { text: t("skills.col.docsOnlyNames") }),
|
|
el("th", { text: t("skills.col.namesPreview") }),
|
|
]),
|
|
]),
|
|
skillEffectiveTbody,
|
|
]),
|
|
]),
|
|
]);
|
|
|
|
const foldBindingDash = skillsFold(t("skills.fold.bindingDash"), [
|
|
el("div", { class: "table-wrap" }, [
|
|
el("table", { class: "table table--compact" }, [
|
|
el("thead", {}, [
|
|
el("tr", {}, [
|
|
el("th", { text: t("skills.col.role") }),
|
|
el("th", { text: t("skills.col.directSkills") }),
|
|
el("th", { text: t("skills.col.effective") }),
|
|
]),
|
|
]),
|
|
skillBindingDashTbody,
|
|
]),
|
|
]),
|
|
foldEffective,
|
|
]);
|
|
|
|
const foldBinding = skillsFold(t("skills.fold.binding"), [
|
|
el("div", { class: "muted", text: t("skills.fold.bindingHint") }),
|
|
skillBindingStatus,
|
|
skillBindingPersistHint,
|
|
skillBindingEnvHint,
|
|
el("label", { class: "row", style: "gap:8px;align-items:center;margin-top:6px;" }, [
|
|
skillBindingEnabledCb,
|
|
el("span", { text: t("skills.label.enableBinding") }),
|
|
]),
|
|
el("div", { class: "row u-row-wrap-mt" }, [
|
|
el("label", { text: t("skills.label.role") }),
|
|
skillRoleSelect,
|
|
btnSaveSkillBinding,
|
|
]),
|
|
skillBindingListWrap,
|
|
foldBindingDash,
|
|
]);
|
|
|
|
const foldDiagnostics = skillsFold(t("skills.fold.diagnostics"), [
|
|
skillHealthBox,
|
|
selfCheckBox,
|
|
internalToolsBox,
|
|
llmToolsBox,
|
|
]);
|
|
|
|
return renderPageShell(
|
|
{
|
|
title: t("skills.title"),
|
|
subtitle: t("skills.subtitle"),
|
|
sections: [
|
|
{ id: "skills-mode", label: t("skills.toc.mode") },
|
|
{ id: "skills-install", label: t("skills.toc.install") },
|
|
{ id: "skills-installed", label: t("skills.toc.installed") },
|
|
{ id: "skills-binding", label: t("skills.toc.binding") },
|
|
{ id: "skills-diagnostics", label: t("skills.toc.diagnostics") },
|
|
],
|
|
},
|
|
[
|
|
el("div", { class: "page-grid page-grid--single" }, [
|
|
el("div", { id: "skills-mode" }, [foldMode]),
|
|
el("div", { id: "skills-install" }, [foldInstall]),
|
|
el("div", { id: "skills-installed" }, [foldInstalled]),
|
|
el("div", { id: "skills-binding" }, [foldBinding]),
|
|
el("div", { id: "skills-diagnostics" }, [foldDiagnostics]),
|
|
]),
|
|
],
|
|
);
|
|
}
|
|
|
|
export { skillsFold, renderSkills };
|