feat: 发布热更 1.5.94,修复中转历史会话并支持一键清空

This commit is contained in:
mac
2026-09-11 09:06:41 +08:00
parent 0446d09b3b
commit a74576c3e1
28 changed files with 3217 additions and 155 deletions
+137 -38
View File
@@ -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 = "(开发版不检查更新)";