113 lines
4.4 KiB
JavaScript
113 lines
4.4 KiB
JavaScript
// 技能 modal:查看「平台 skill」/「我的 skill」两组列表,点任一项展开完整 SKILL.md,
|
||
// 「我的」每项可删除(平台 skill 只读)。左侧 rail 底部「技能」按钮触发。
|
||
// 后端:GET /v1/skills(列表)、GET /v1/skills/{name}(正文)、DELETE /v1/skills/{name}(只删 user 源)。
|
||
// 创建 / 改 / fork 仍走对话(save_skill / fork_skill / skill-creator)。
|
||
import { $ } from "./dom.js";
|
||
import { state } from "./state.js";
|
||
import { api } from "./api.js";
|
||
import { escapeHtml } from "./format.js";
|
||
import { renderMd, highlightIn } from "./markdown.js";
|
||
|
||
function openSkillsModal() {
|
||
$("skills-modal").classList.add("show");
|
||
renderList();
|
||
}
|
||
export function closeSkillsModal() {
|
||
$("skills-modal").classList.remove("show");
|
||
}
|
||
|
||
function itemHtml(s) {
|
||
const badge = s.overrides_builtin
|
||
? ' <span class="sk-badge">已覆盖平台同名</span>'
|
||
: "";
|
||
const del =
|
||
s.source === "user"
|
||
? `<button class="sk-del small danger" data-del="${escapeHtml(s.name)}" title="删除我的这个 skill">删除</button>`
|
||
: "";
|
||
return `<div class="sk-item" data-name="${escapeHtml(s.name)}">
|
||
<div class="sk-item-main">
|
||
<div class="sk-name">${escapeHtml(s.name)}${badge}</div>
|
||
<div class="sk-desc">${escapeHtml(s.description || "")}</div>
|
||
</div>${del}
|
||
</div>`;
|
||
}
|
||
|
||
async function renderList() {
|
||
const body = $("sk-body");
|
||
$("sk-title").textContent = "技能";
|
||
body.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
|
||
let data;
|
||
try {
|
||
data = await api("GET", "/v1/skills");
|
||
} catch (e) {
|
||
body.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
|
||
return;
|
||
}
|
||
state.skills = data.skills || []; // 顺手刷新新建任务下拉的缓存
|
||
const platform = state.skills.filter((s) => s.source === "builtin");
|
||
const mine = state.skills.filter((s) => s.source === "user");
|
||
|
||
let html = "";
|
||
html += `<div class="sk-group-title">平台 skill (${platform.length})</div>`;
|
||
html +=
|
||
platform.map(itemHtml).join("") ||
|
||
'<div class="muted" style="padding:4px 8px;">(无)</div>';
|
||
html += `<div class="sk-group-title" style="margin-top:14px;">我的 skill (${mine.length})</div>`;
|
||
html += mine.length
|
||
? mine.map(itemHtml).join("")
|
||
: '<div class="muted" style="padding:4px 8px;">还没有。让助手「帮我做个 skill」或「把某个平台 skill fork 成我的」即可创建。</div>';
|
||
|
||
if (data.load_errors && data.load_errors.length) {
|
||
const errs = data.load_errors
|
||
.map((e) => `${escapeHtml(e.name)}(${escapeHtml(e.reason)})`)
|
||
.join(";");
|
||
html += `<div class="sk-loaderr">⚠ ${data.load_errors.length} 个 skill 因格式问题未加载:${errs}</div>`;
|
||
}
|
||
body.innerHTML = html;
|
||
}
|
||
|
||
async function showDetail(name) {
|
||
const body = $("sk-body");
|
||
$("sk-title").innerHTML = `<button id="sk-back" class="small">‹ 返回</button><span class="sk-detail-name">${escapeHtml(name)}</span>`;
|
||
$("sk-back").onclick = renderList;
|
||
body.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
|
||
let data;
|
||
try {
|
||
data = await api("GET", "/v1/skills/" + encodeURIComponent(name));
|
||
} catch (e) {
|
||
body.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
|
||
return;
|
||
}
|
||
body.innerHTML = `<div class="sk-detail">${renderMd(data.content)}</div>`;
|
||
highlightIn(body);
|
||
}
|
||
|
||
// ───── 顶层绑定 ─────
|
||
$("hd-skills").onclick = openSkillsModal;
|
||
$("sk-close").onclick = closeSkillsModal;
|
||
$("skills-modal").addEventListener("click", (e) => {
|
||
if (e.target.id === "skills-modal") closeSkillsModal(); // 点遮罩关闭
|
||
});
|
||
|
||
// 列表区事件委托:删除(冒泡到 [data-del])优先于点开详情(.sk-item)
|
||
$("sk-body").addEventListener("click", async (e) => {
|
||
const del = e.target.closest("[data-del]");
|
||
if (del) {
|
||
e.stopPropagation();
|
||
const name = del.getAttribute("data-del");
|
||
if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return;
|
||
del.disabled = true;
|
||
try {
|
||
await api("DELETE", "/v1/skills/" + encodeURIComponent(name));
|
||
state.skills = null; // 失效缓存,新建任务下拉下次重拉
|
||
renderList();
|
||
} catch (err) {
|
||
alert("删除失败: " + err.message);
|
||
del.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
const item = e.target.closest(".sk-item");
|
||
if (item) showDetail(item.getAttribute("data-name"));
|
||
});
|