feat: 发布热更 1.5.94,修复中转历史会话并支持一键清空
This commit is contained in:
+137
-38
@@ -561,8 +561,8 @@ async function voiceLoad() {
|
||||
let skillCat = null; // 完整 catalog {total, categories, skills}
|
||||
let storeCat = "__all__"; // 当前分类(__all__ / __hot__ / 中文分类名)
|
||||
let storeSort = "hot"; // hot / downloads / stars
|
||||
let storeShown = 0; // 已渲染数量(分页)
|
||||
const STORE_PAGE = 60;
|
||||
let storePage = 0;
|
||||
const STORE_PAGE = 24;
|
||||
const installed = new Set(JSON.parse(localStorage.getItem("installedSkills") || "[]"));
|
||||
let installedSkills = []; // 磁盘实况(~/.codex/skills 扫描结果)
|
||||
let installedDirs = new Set(); // 已安装技能的目录名,用于精确标注“已安装”
|
||||
@@ -668,10 +668,11 @@ function skillCardHTML(s) {
|
||||
<div class="sk-subtitle">${esc(s.subtitle || s.name)}</div>
|
||||
<div class="sk-brief">${esc(s.brief || "")}</div>
|
||||
${agentBadges(s)}
|
||||
<div class="sk-meta">${meta}</div>
|
||||
<div class="sk-foot">
|
||||
<button class="btn primary small" data-install="${esc(String(s.id))}">${isIn ? "重新安装" : "安装"}</button>
|
||||
<button class="btn ghost small" data-detail="${esc(String(s.id))}">详情</button>
|
||||
<div class="sk-meta">${meta}</div>
|
||||
<div class="sk-card-actions">
|
||||
<button class="btn ghost small install" data-install="${esc(String(s.id))}" title="${isIn ? "重新安装技能" : "安装技能"}">${isIn ? "重新安装" : "安装"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -682,7 +683,7 @@ function renderStore(reset = true) {
|
||||
const more = $("#store-more");
|
||||
if (storeCat === "__installed__") { renderInstalledSkills(); return; }
|
||||
const list = storeFiltered();
|
||||
if (reset) { storeShown = 0; grid.innerHTML = ""; }
|
||||
if (reset) storePage = 0;
|
||||
if (!list.length) {
|
||||
grid.innerHTML = "";
|
||||
empty.classList.remove("hidden");
|
||||
@@ -691,35 +692,75 @@ function renderStore(reset = true) {
|
||||
return;
|
||||
}
|
||||
empty.classList.add("hidden");
|
||||
const next = list.slice(storeShown, storeShown + STORE_PAGE);
|
||||
grid.insertAdjacentHTML("beforeend", next.map(skillCardHTML).join(""));
|
||||
storeShown += next.length;
|
||||
// 副标题
|
||||
const pageCount = Math.ceil(list.length / STORE_PAGE);
|
||||
storePage = Math.min(storePage, pageCount - 1);
|
||||
const first = storePage * STORE_PAGE;
|
||||
const next = list.slice(first, first + STORE_PAGE);
|
||||
grid.innerHTML = next.map(skillCardHTML).join("");
|
||||
|
||||
$("#store-cat-title").textContent =
|
||||
storeCat === "__all__" ? "全部技能" : storeCat === "__hot__" ? "热门技能" : storeCat;
|
||||
$("#store-count").textContent = `${list.length.toLocaleString()} 个`;
|
||||
$("#store-cat-sub").textContent =
|
||||
`${list.length} 个技能 · 通用 SKILL.md 标准,适配 Claude · Codex · OpenClaw`;
|
||||
// 加载更多
|
||||
if (storeShown < list.length) {
|
||||
more.classList.remove("hidden");
|
||||
more.innerHTML = `已显示 ${storeShown} / ${list.length} · <button class="btn ghost small" id="store-more-btn">加载更多</button>`;
|
||||
$("#store-more-btn").addEventListener("click", () => renderStore(false));
|
||||
} else {
|
||||
more.classList.remove("hidden");
|
||||
more.innerHTML = `<span class="muted small">已全部显示 · 共 ${list.length} 个</span>`;
|
||||
}
|
||||
`通用 SKILL.md 标准,适配 Claude · Codex · OpenClaw`;
|
||||
renderStorePager(more, pageCount, list.length);
|
||||
bindStoreCards();
|
||||
}
|
||||
|
||||
function renderStorePager(more, pageCount, total) {
|
||||
more.classList.remove("hidden");
|
||||
if (pageCount <= 1) {
|
||||
more.innerHTML = `<span class="muted small">共 ${total.toLocaleString()} 个技能</span>`;
|
||||
return;
|
||||
}
|
||||
const pages = new Set([0, pageCount - 1, storePage - 1, storePage, storePage + 1]);
|
||||
const orderedPages = [...pages].filter((p) => p >= 0 && p < pageCount).sort((a, b) => a - b);
|
||||
let last = -1;
|
||||
const pageButtons = orderedPages.map((p) => {
|
||||
const gap = p - last > 1 ? `<span class="store-page-gap">…</span>` : "";
|
||||
last = p;
|
||||
return `${gap}<button class="store-page-btn${p === storePage ? " active" : ""}" data-store-page="${p}" aria-label="第 ${p + 1} 页" aria-current="${p === storePage ? "page" : "false"}">${p + 1}</button>`;
|
||||
}).join("");
|
||||
more.innerHTML = `
|
||||
<span class="store-page-summary">${storePage * STORE_PAGE + 1}-${Math.min((storePage + 1) * STORE_PAGE, total)} / ${total.toLocaleString()}</span>
|
||||
<div class="store-pagination" aria-label="技能分页">
|
||||
<button class="store-page-btn" data-store-page="${storePage - 1}" title="上一页" aria-label="上一页" ${storePage === 0 ? "disabled" : ""}>‹</button>
|
||||
${pageButtons}
|
||||
<button class="store-page-btn" data-store-page="${storePage + 1}" title="下一页" aria-label="下一页" ${storePage === pageCount - 1 ? "disabled" : ""}>›</button>
|
||||
</div>`;
|
||||
more.querySelectorAll("[data-store-page]").forEach((btn) => btn.addEventListener("click", () => {
|
||||
const nextPage = Number(btn.dataset.storePage);
|
||||
if (Number.isNaN(nextPage) || nextPage < 0 || nextPage >= pageCount || nextPage === storePage) return;
|
||||
storePage = nextPage;
|
||||
renderStore(false);
|
||||
$("#market-grid").scrollIntoView({ block: "start", behavior: "smooth" });
|
||||
}));
|
||||
}
|
||||
|
||||
function bindStoreCards() {
|
||||
const grid = $("#market-grid");
|
||||
grid.querySelectorAll(".skill-card[data-id]").forEach((card) => {
|
||||
if (card._bound) return; card._bound = true;
|
||||
card.tabIndex = 0;
|
||||
card.setAttribute("role", "button");
|
||||
card.setAttribute("aria-label", `查看 ${card.querySelector(".sk-name")?.textContent || "技能"} 详情`);
|
||||
const openDetail = () => openSkillDetail(card.dataset.id);
|
||||
card.addEventListener("click", (ev) => {
|
||||
if (ev.target.closest("[data-install]")) return;
|
||||
openDetail();
|
||||
});
|
||||
card.addEventListener("keydown", (ev) => {
|
||||
if (ev.key !== "Enter" && ev.key !== " ") return;
|
||||
ev.preventDefault();
|
||||
openDetail();
|
||||
});
|
||||
});
|
||||
grid.querySelectorAll("[data-install]").forEach((btn) => {
|
||||
if (btn._bound) return; btn._bound = true;
|
||||
btn.addEventListener("click", () => installSkill(btn.dataset.install, btn));
|
||||
});
|
||||
grid.querySelectorAll("[data-detail]").forEach((btn) => {
|
||||
if (btn._bound) return; btn._bound = true;
|
||||
btn.addEventListener("click", () => openSkillDetail(btn.dataset.detail));
|
||||
btn.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
installSkill(btn.dataset.install, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -731,8 +772,9 @@ function renderInstalledSkills() {
|
||||
more.classList.add("hidden");
|
||||
more.innerHTML = "";
|
||||
$("#store-cat-title").textContent = "我已安装";
|
||||
$("#store-count").textContent = `${installedSkills.length.toLocaleString()} 个`;
|
||||
$("#store-cat-sub").textContent =
|
||||
`${installedSkills.length} 个已安装技能 · Codex 侧位于 ~/.codex/skills · 卸载会同时删除工具箱仓库实体`;
|
||||
`Codex 侧位于 ~/.codex/skills · 卸载会同时删除工具箱仓库实体`;
|
||||
if (!installedSkills.length) {
|
||||
grid.innerHTML = "";
|
||||
empty.classList.remove("hidden");
|
||||
@@ -784,9 +826,9 @@ function renderStoreNav() {
|
||||
const item = (key, name, count) => {
|
||||
const active = storeCat === key ? " active" : "";
|
||||
const c = count != null ? `<span class="nav-count">${count.toLocaleString()}</span>` : "";
|
||||
return `<div class="nav-item${active}" data-cat="${esc(key)}">
|
||||
return `<button type="button" class="nav-item${active}" data-cat="${esc(key)}" aria-pressed="${storeCat === key}">
|
||||
<span class="nav-ico">${CAT_ICON[name] || "•"}</span>
|
||||
<span class="nav-name">${esc(name)}</span>${c}</div>`;
|
||||
<span class="nav-name">${esc(name)}</span>${c}</button>`;
|
||||
};
|
||||
let html = "";
|
||||
html += `<div class="store-nav-group">精选</div>`;
|
||||
@@ -797,15 +839,32 @@ function renderStoreNav() {
|
||||
for (const c of skillCat.categories) html += item(c.name, c.name, c.count);
|
||||
nav.innerHTML = html;
|
||||
nav.querySelectorAll(".nav-item").forEach((el) =>
|
||||
el.addEventListener("click", async () => {
|
||||
storeCat = el.dataset.cat;
|
||||
nav.querySelectorAll(".nav-item").forEach((x) => x.classList.remove("active"));
|
||||
el.classList.add("active");
|
||||
$("#market-grid").scrollTop = 0;
|
||||
if (storeCat === "__installed__") await loadInstalledSkills({ includeSize: true, includeMeta: true });
|
||||
renderStore(true);
|
||||
})
|
||||
el.addEventListener("click", () => selectStoreCategory(el.dataset.cat))
|
||||
);
|
||||
renderStoreCategoryChips();
|
||||
}
|
||||
|
||||
function renderStoreCategoryChips() {
|
||||
const chips = $("#store-category-chips");
|
||||
if (!skillCat || !chips) return;
|
||||
const quickCategories = skillCat.categories.slice(0, 9);
|
||||
const item = (key, name) => `<button type="button" class="store-filter-chip${storeCat === key ? " active" : ""}" data-store-filter="${esc(key)}" aria-pressed="${storeCat === key}">${esc(name)}</button>`;
|
||||
chips.innerHTML = [
|
||||
item("__all__", "全部"),
|
||||
item("__hot__", "热门"),
|
||||
...quickCategories.map((c) => item(c.name, c.name)),
|
||||
].join("");
|
||||
chips.querySelectorAll("[data-store-filter]").forEach((el) =>
|
||||
el.addEventListener("click", () => selectStoreCategory(el.dataset.storeFilter))
|
||||
);
|
||||
}
|
||||
|
||||
async function selectStoreCategory(category) {
|
||||
storeCat = category;
|
||||
$("#market-grid").scrollTop = 0;
|
||||
if (storeCat === "__installed__") await loadInstalledSkills({ includeSize: true, includeMeta: true });
|
||||
renderStoreNav();
|
||||
renderStore(true);
|
||||
}
|
||||
|
||||
async function installSkill(id, btn) {
|
||||
@@ -1172,6 +1231,8 @@ let sessData = [];
|
||||
let sessCurrent = null;
|
||||
let sessLoaded = false;
|
||||
let sessAllMode = false;
|
||||
const SESS_DEFAULT_LIMIT = 500;
|
||||
const SESS_ALL_LIMIT = 5000;
|
||||
|
||||
function renderSessionDataDir(info) {
|
||||
if (!info || !info.path) return;
|
||||
@@ -1290,8 +1351,8 @@ async function sessLoad(opts = {}) {
|
||||
try {
|
||||
sessData = await apic.sessionsList(
|
||||
sessAllMode
|
||||
? { includeSubagent: true, includeArchived: true }
|
||||
: { includeSubagent: false, includeArchived: false }
|
||||
? { includeSubagent: true, includeArchived: true, limit: SESS_ALL_LIMIT }
|
||||
: { includeSubagent: false, includeArchived: false, limit: SESS_DEFAULT_LIMIT }
|
||||
);
|
||||
sessLoaded = true;
|
||||
sessCurrent = null;
|
||||
@@ -1329,6 +1390,33 @@ $("#sess-delete").addEventListener("click", async (ev) => {
|
||||
});
|
||||
|
||||
$("#sess-search").addEventListener("input", sessRender);
|
||||
$("#sess-clear-all").addEventListener("click", async (ev) => {
|
||||
const status = await apic.providerSyncStatus();
|
||||
if (status.running) {
|
||||
toast("请先完全退出 Codex(Cmd+Q)再清除会话记录", "err");
|
||||
return;
|
||||
}
|
||||
const confirmed = confirm(
|
||||
"确定清除全部会话记录?\n\n将删除 sessions、archived_sessions、Codex 会话索引、状态库线程、侧栏目录和工具箱会话记忆中的全部对话和项目摘要(已固定的长期偏好会保留)。\n不会删除中转配置、登录凭证、项目源码、插件和自动化任务。\n\n执行前会自动备份,操作不可直接撤销。"
|
||||
);
|
||||
if (!confirmed) return;
|
||||
const btn = ev.currentTarget;
|
||||
busy(btn, true, "清除中…");
|
||||
try {
|
||||
const r = await apic.sessionsClearAll();
|
||||
sessData = [];
|
||||
sessCurrent = null;
|
||||
$("#sess-empty").classList.remove("hidden");
|
||||
$("#sess-conv").classList.add("hidden");
|
||||
sessRender();
|
||||
if (r.cleanupWarning) toast(`会话已清除,但会话记忆清理有警告:${r.cleanupWarning}`, "err");
|
||||
else toast(`已清除 ${r.filesRemoved || 0} 个会话文件和 ${r.memoryRemoved || 0} 条会话记忆;执行前备份在 ${r.backup}`, "ok");
|
||||
} catch (e) {
|
||||
toast(`清除会话失败:${e.message || e}`, "err");
|
||||
} finally {
|
||||
busy(btn, false);
|
||||
}
|
||||
});
|
||||
$("#sess-refresh").addEventListener("click", sessLoad);
|
||||
$("#session-data-dir-select").addEventListener("click", (ev) => changeSessionDataDir(false, ev.currentTarget));
|
||||
$("#session-data-dir-reset").addEventListener("click", (ev) => changeSessionDataDir(true, ev.currentTarget));
|
||||
@@ -2268,6 +2356,17 @@ $("#btn-check-update").addEventListener("click", async (ev) => {
|
||||
showUpdateModal({ version: h.contentVersion, notes: h.notes, mandatory: h.mandatory, kind: "hot" });
|
||||
return;
|
||||
}
|
||||
if (h && (h.needFull || h.needFullAfterHot)) {
|
||||
info.textContent = `需要安装完整包 v${h.fullAppVersion || h.contentVersion}`;
|
||||
showUpdateModal({
|
||||
version: h.fullAppVersion || h.contentVersion,
|
||||
notes: `${h.notes || ""}${h.needFullAfterHot ? "\n\n本次更新还需要安装完整包,以注册 TD 工具箱深链协议。" : ""}`.trim(),
|
||||
mandatory: h.mandatory,
|
||||
url: RELEASE_FALLBACK,
|
||||
kind: "full",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 否则查完整安装包
|
||||
const r = await apic.checkUpdate();
|
||||
if (r.dev) info.textContent = "(开发版不检查更新)";
|
||||
|
||||
Reference in New Issue
Block a user