// app.js —— 前端逻辑,只经 window.apic.* 与主进程通信。 // 绝不出现市场地址 / 密钥(那些只在主进程)。 "use strict"; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); // 注意:preload 已在 window 上定义了不可配置的全局属性 `api`, // 顶层再用 const/let 声明同名会抛 "already been declared",故用 window.api 起个别名。 const apic = window.api; // —— 工具 —— function humanSize(n) { const u = ["B", "K", "M", "G"]; let x = n; for (const unit of u) { if (x < 1024) return unit === "B" ? `${Math.round(x)}${unit}` : `${x.toFixed(1)}${unit}`; x /= 1024; } return `${x.toFixed(1)}T`; } function esc(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } let toastTimer = null; function toast(msg, kind = "") { const t = $("#toast"); t.textContent = msg; t.className = `toast ${kind}`; clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.add("hidden"), 4200); } function busy(btn, on, textWhenBusy) { if (!btn) return; if (on) { btn.dataset.label = btn.textContent; btn.textContent = textWhenBusy || "处理中…"; btn.disabled = true; } else { btn.textContent = btn.dataset.label || btn.textContent; btn.disabled = false; } } // —— 顶部广告:点击用系统浏览器打开官方推荐中转 —— const promoEl = document.querySelector(".promo"); if (promoEl) { promoEl.addEventListener("click", (e) => { e.preventDefault(); apic.openExternal(promoEl.dataset.open); }); } // —— 标签切换 —— $$(".tab").forEach((tab) => { tab.addEventListener("click", () => { $$(".tab").forEach((t) => t.classList.remove("active")); $$(".panel").forEach((p) => p.classList.remove("active")); tab.classList.add("active"); $(`#tab-${tab.dataset.tab}`).classList.add("active"); }); }); $$(".subtab").forEach((st) => { st.addEventListener("click", () => { $$(".subtab").forEach((s) => s.classList.remove("active")); $$(".subpanel").forEach((s) => s.classList.remove("active")); st.classList.add("active"); $(`#sub-${st.dataset.sub}`).classList.add("active"); }); }); // —— 日志抽屉 —— $("#log-toggle").addEventListener("click", () => { const p = $("#log-panel"); p.classList.toggle("hidden"); $("#log-caret").textContent = p.classList.contains("hidden") ? "▲" : "▼"; }); apic.onLog((msg) => { const out = $("#log-out"); out.textContent += msg + "\n"; $("#log-panel").scrollTop = $("#log-panel").scrollHeight; }); // =========================================================================== // ① 插件市场 // =========================================================================== 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; const installed = new Set(JSON.parse(localStorage.getItem("installedSkills") || "[]")); // 分类图标(与网页语义对应) const CAT_ICON = { "全部": "🗂️", "热门": "🔥", "API": "🔌", "办公": "📊", "设计": "🎨", "效率": "⚡", "内容媒体": "📝", "文档处理": "📄", "开发": "🛠️", "教育科研": "🎓", "图表": "📈", "客服": "🎧", "后端": "🗄️", "数据分析": "📊", "数据库": "🗃️", "产品管理": "📋", "运维": "⚙️", "财务": "💰", "健康": "🩺", "前端": "🖥️", "Git": "🔀", "人力资源": "👥", "图片生成": "🖼️", "法律": "⚖️", "市场": "📣", "移动端": "📱", "运营": "🚀", "支付": "💳", "项目管理": "🗓️", "React": "⚛️", "房产": "🏠", "销售": "🤝", "测试": "🧪", "AI增强": "🤖", "其它": "🧩", }; const AGENT_CLASS = { Claude: "claude", Codex: "codex", OpenClaw: "openclaw" }; function numify(s) { if (!s) return 0; const n = parseFloat(String(s).replace(/,/g, "")); if (isNaN(n)) return 0; if (/k/i.test(s)) return n * 1e3; if (/m/i.test(s)) return n * 1e6; return n; } // 某个技能在「我的 gitea 仓库」里的文件页地址 function skillGitUrl(s) { // 浏览走 update 域名(管理登录会话在此),clone 用 up 域名(git 直连) const base = (skillCat.repo || "") .replace(/\.git$/, "") .replace("up.tokendun.cc", "update.tokendun.cc"); // https://update.tokendun.cc/mhgmh/skills if (!base) return "https://hub.cocoloop.cn/popular"; if (!s.path) return base; // 无本地路径时退回仓库首页 // 中文分类目录需 URL 编码,路径分隔符保留 const enc = s.path.split("/").map(encodeURIComponent).join("/"); return `${base}/src/branch/main/${enc}`; } // 当前筛选后的技能列表 function storeFiltered() { const q = $("#market-search").value.trim().toLowerCase(); let list = skillCat.skills; if (storeCat === "__hot__") { list = list.filter((s) => numify(s.dl) >= 50000 || numify(s.fav) >= 200000); } else if (storeCat !== "__all__") { list = list.filter((s) => s.category === storeCat || (s.cnTags || []).includes(storeCat)); } if (q) { list = list.filter((s) => (s.name || "").toLowerCase().includes(q) || (s.author || "").toLowerCase().includes(q) || (s.subtitle || "").toLowerCase().includes(q) || (s.brief || "").toLowerCase().includes(q) ); } if (storeSort === "downloads") list = [...list].sort((a, b) => numify(b.dl) - numify(a.dl)); else if (storeSort === "stars") list = [...list].sort((a, b) => numify(b.stars) - numify(a.stars)); // hot:保持 catalog 原序(已按下载量排) return list; } function agentBadges(s) { const a = (s.agents || []).map( (x) => `${esc(x)}` ).join(""); const mm = s.mm ? '🎨 多模态' : ""; return `
适配${a}${mm}
`; } function skillCardHTML(s) { const sec = (s.sec || "").toUpperCase(); const secBadge = /^[SAB]$/.test(sec) ? `
${sec}
` : ""; const isIn = installed.has(String(s.id)); const meta = [ `⬇ ${esc(s.dl || "0")}`, s.stars ? `★ ${esc(s.stars)}` : "", s.fav ? `♥ ${esc(s.fav)}` : "", s.cred ? `${esc(s.cred)}` : "", ].join(""); return `
${esc(s.icon || "🧩")}
${esc(s.name)}
by ${esc(s.author || "未知")}
${secBadge}
${esc(s.subtitle || s.name)}
${esc(s.brief || "")}
${agentBadges(s)}
${meta}
`; } function renderStore(reset = true) { const grid = $("#market-grid"); const empty = $("#market-empty"); const more = $("#store-more"); const list = storeFiltered(); if (reset) { storeShown = 0; grid.innerHTML = ""; } if (!list.length) { grid.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = "没有匹配的技能,换个关键词或分类试试。"; more.classList.add("hidden"); return; } empty.classList.add("hidden"); const next = list.slice(storeShown, storeShown + STORE_PAGE); grid.insertAdjacentHTML("beforeend", next.map(skillCardHTML).join("")); storeShown += next.length; // 副标题 $("#store-cat-title").textContent = storeCat === "__all__" ? "全部技能" : storeCat === "__hot__" ? "热门技能" : storeCat; $("#store-cat-sub").textContent = `${list.length} 个技能 · 通用 SKILL.md 标准,适配 Claude · Codex · OpenClaw`; // 加载更多 if (storeShown < list.length) { more.classList.remove("hidden"); more.innerHTML = `已显示 ${storeShown} / ${list.length} · `; $("#store-more-btn").addEventListener("click", () => renderStore(false)); } else { more.classList.remove("hidden"); more.innerHTML = `已全部显示 · 共 ${list.length} 个`; } bindStoreCards(); } function bindStoreCards() { const grid = $("#market-grid"); 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)); }); } function renderStoreNav() { const nav = $("#store-nav"); $("#store-total").textContent = skillCat.total.toLocaleString(); const item = (key, name, count) => { const active = storeCat === key ? " active" : ""; const c = count != null ? `${count.toLocaleString()}` : ""; return ``; }; let html = ""; html += `
精选
`; html += item("__all__", "全部", skillCat.total); html += item("__hot__", "热门", null); html += `
全部分类
`; 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", () => { storeCat = el.dataset.cat; nav.querySelectorAll(".nav-item").forEach((x) => x.classList.remove("active")); el.classList.add("active"); $("#market-grid").scrollTop = 0; renderStore(true); }) ); } async function installSkill(id, btn) { const s = skillCat.skills.find((x) => String(x.id) === String(id)); if (!s) return; busy(btn, true, "安装中…"); btn.classList.add("installing"); try { const r = await apic.skillInstall(s); installed.add(String(s.id)); localStorage.setItem("installedSkills", JSON.stringify([...installed])); toast(`已安装 ${s.name} ✅(${r.files} 个文件)重启 Codex 生效`, "ok"); btn.textContent = "已安装 ✓"; btn.dataset.label = "已安装 ✓"; } catch (e) { toast(`安装失败:${e.message || e}`, "err"); busy(btn, false); } finally { btn.classList.remove("installing"); } } function openSkillDetail(id) { const s = skillCat.skills.find((x) => String(x.id) === String(id)); if (!s) return; $("#modal-title").textContent = "技能详情"; const tags = (s.cnTags || []).map((t) => `${esc(t)}`).join(""); $("#modal-body").innerHTML = `
${esc(s.icon || "🧩")}

${esc(s.name)}

by ${esc(s.author || "未知")} · 分类 ${esc(s.category)}

${esc(s.subtitle || "")}

${esc(s.brief || "(暂无简介)")}

${tags ? `
${tags}
` : ""}
⬇ 下载 ${esc(s.dl || "0")} ${s.stars ? `★ 星标 ${esc(s.stars)}` : ""} ${s.fav ? `♥ 收藏 ${esc(s.fav)}` : ""} ${s.sec ? `🛡 安全级 ${esc(s.sec)}` : ""} ${s.cred ? `可信度 ${esc(s.cred)}` : ""}

适配 AI 模型 / Agent

${agentBadges(s)}

${esc(skillCat.model_note || "SKILL.md 标准技能")}${s.mm ? ";本技能涉及图片/视觉,建议搭配多模态强模型。" : "。"}

`; modalOverlay.classList.remove("hidden"); $("#sd-install").addEventListener("click", (e) => installSkill(s.id, e.currentTarget)); $("#sd-source").addEventListener("click", () => apic.openExternal(skillGitUrl(s))); } async function loadMarket() { const grid = $("#market-grid"); const empty = $("#market-empty"); empty.classList.add("hidden"); grid.innerHTML = '
正在加载技能商店…
'; try { skillCat = await apic.skillCatalog(); renderStoreNav(); renderStore(true); } catch (e) { grid.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = `加载失败:${e.message || e}`; } } let storeSearchTimer = null; $("#market-search").addEventListener("input", () => { clearTimeout(storeSearchTimer); storeSearchTimer = setTimeout(() => renderStore(true), 180); }); $("#store-sort").querySelectorAll(".seg").forEach((s) => { s.addEventListener("click", () => { $("#store-sort").querySelectorAll(".seg").forEach((x) => x.classList.remove("active")); s.classList.add("active"); storeSort = s.dataset.sort; renderStore(true); }); }); // =========================================================================== // 通用勾选列表渲染(本机导出 / 拉取还原共用) // =========================================================================== function renderChecklist(container, items, emptyEl, emptyMsg) { if (!items.length) { container.innerHTML = ""; emptyEl.classList.remove("hidden"); emptyEl.textContent = emptyMsg; return; } emptyEl.classList.add("hidden"); const groups = {}; for (const it of items) (groups[it.category] ||= []).push(it); let html = ""; for (const cat of Object.keys(groups)) { html += `
${esc(cat)}
`; for (const it of groups[cat]) { html += ` `; } } container.innerHTML = html; } function checkedRels(container) { return $$(`#${container.id} input[type=checkbox]:checked`).map((c) => c.value); } // =========================================================================== // ② 备份 —— 从本机导出 / 上传 // =========================================================================== let localItems = []; async function scanLocal() { const list = $("#local-list"); const empty = $("#local-empty"); empty.classList.add("hidden"); list.innerHTML = '
扫描本机 ~/.codex…
'; try { localItems = await apic.scanLocal(); renderChecklist(list, localItems, empty, "本机 ~/.codex 下暂时没有插件或技能。"); } catch (e) { list.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = `扫描失败:${e.message || e}`; } } $("#scan-refresh").addEventListener("click", scanLocal); $("#btn-export").addEventListener("click", async (ev) => { const rels = checkedRels($("#local-list")); if (!rels.length) return toast("请先勾选要导出的项", "err"); const dir = await apic.pickDir(); if (!dir) return; const btn = ev.currentTarget; busy(btn, true, "导出中…"); try { const r = await apic.packLocal(rels, dir); toast(`已导出到:${r.path}`, "ok"); } catch (e) { toast(`导出失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); $("#btn-upload").addEventListener("click", async (ev) => { const rels = checkedRels($("#local-list")); if (!rels.length) return toast("请先勾选要上传的项", "err"); const repo = $("#repo-name").value.trim(); const token = $("#gh-token").value.trim(); const isPrivate = $("#repo-private").checked; if (!repo) return toast("请填写仓库名", "err"); if (!token) return toast("请填写 GitHub token", "err"); const btn = ev.currentTarget; busy(btn, true, "上传中…"); $("#log-panel").classList.remove("hidden"); $("#log-caret").textContent = "▼"; try { const r = await apic.upload(rels, repo, token, isPrivate); if (r.marketSkipped) { toast(`已备份到 ${r.owner} 的仓库 ✅(未设管理员密钥,未上架市场)`, "ok"); } else { toast(`上传成功:${r.owner},已登记 ${r.published} 个插件到市场 ✅`, "ok"); } loadMarket(); } catch (e) { toast(`上传失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); // =========================================================================== // ② 还原 —— 从 GitHub 拉取 // =========================================================================== let pullItems = []; $("#btn-pull").addEventListener("click", async (ev) => { const repo = $("#pull-repo").value.trim(); const token = $("#pull-token").value.trim(); if (!repo) return toast("请填写仓库地址", "err"); const btn = ev.currentTarget; const list = $("#pull-list"); const empty = $("#pull-empty"); busy(btn, true, "拉取中…"); empty.classList.add("hidden"); list.innerHTML = '
克隆并扫描仓库…
'; $("#pull-actions").classList.add("hidden"); try { pullItems = await apic.ghPull(repo, token); renderChecklist(list, pullItems, empty, "该仓库里没找到可还原的插件或技能。"); if (pullItems.length) $("#pull-actions").classList.remove("hidden"); } catch (e) { list.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = `拉取失败:${e.message || e}`; } finally { busy(btn, false); } }); $("#btn-restore").addEventListener("click", async (ev) => { const rels = checkedRels($("#pull-list")); if (!rels.length) return toast("请先勾选要还原的项", "err"); const enable = $("#pull-enable").checked; const btn = ev.currentTarget; busy(btn, true, "还原中…"); try { await apic.ghRestore(rels, enable); toast("还原完成 ✅ 重启 codex 生效", "ok"); } catch (e) { toast(`还原失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); // =========================================================================== // 设置 —— 密钥 // =========================================================================== async function loadSecrets() { try { const gh = await apic.getSecret("github"); if (gh) { $("#set-github").value = gh; $("#gh-token").value = gh; $("#pull-token").value = gh; } const ak = await apic.getSecret("adminKey"); if (ak) $("#set-admin").value = ak; } catch { /* 忽略 */ } } $("#btn-save-admin").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "保存中…"); try { await apic.setSecret("adminKey", $("#set-admin").value.trim()); toast("管理员密钥已保存 ✅", "ok"); } catch (e) { toast(`保存失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); $("#btn-save-secret").addEventListener("click", async (ev) => { const val = $("#set-github").value.trim(); const btn = ev.currentTarget; busy(btn, true, "保存中…"); try { await apic.setSecret("github", val); if (val) { $("#gh-token").value = val; $("#pull-token").value = val; } toast("已保存 ✅", "ok"); } catch (e) { toast(`保存失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); // =========================================================================== // 强制清除本机 Codex —— 检测安装位置 + 一键清干净 // =========================================================================== const KIND_LABEL = { data: "数据目录", bin: "可执行文件", "npm-package": "npm 全局包", }; function renderPurgeList(items) { const box = $("#purge-list"); const actions = $("#purge-actions"); if (!items || !items.length) { box.innerHTML = '

未检测到本机安装的 Codex(可能未安装,或不在常见位置)。

'; actions.style.display = "none"; return; } box.innerHTML = items .map((it, i) => { const tag = KIND_LABEL[it.kind] || it.kind; return ``; }) .join(""); box._items = items; actions.style.display = "flex"; } async function purgeScan(btn) { busy(btn, true, "检测中…"); try { const items = await apic.codexDetect(); renderPurgeList(items); toast(items.length ? `检测到 ${items.length} 个位置` : "未检测到 Codex", items.length ? "ok" : ""); } catch (e) { toast("检测失败:" + (e.message || e), "err"); } finally { busy(btn, false); } } $("#btn-purge-scan").addEventListener("click", (ev) => purgeScan(ev.currentTarget)); $("#btn-purge-do").addEventListener("click", async (ev) => { const box = $("#purge-list"); const items = box._items || []; const paths = $$('#purge-list input[data-purge]') .filter((el) => el.checked) .map((el) => items[+el.dataset.purge]?.path) .filter(Boolean); if (!paths.length) { toast("请先勾选要清除的位置", "err"); return; } const btn = ev.currentTarget; busy(btn, true, "清除中…"); try { const r = await apic.codexPurge(paths); if (r.canceled) { toast("已取消", ""); return; } const ok = r.results.filter((x) => x.ok).length; const fail = r.results.length - ok; toast(fail ? `清除完成:成功 ${ok},失败 ${fail}(详见运行日志)` : `已彻底清除 ${ok} 个位置 ✅ 可重新安装 Codex 了`, fail ? "err" : "ok"); await purgeScan(btn); // 刷新列表,确认已清空 } catch (e) { toast("清除失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); // =========================================================================== // 会话记录 —— 浏览 / 搜索 / 阅读 / 导出 / 备份 / 导入 // =========================================================================== let sessData = []; let sessCurrent = null; let sessLoaded = false; function sessRender() { const q = $("#sess-search").value.trim().toLowerCase(); const box = $("#sess-list"); const list = sessData.filter( (s) => !q || (s.title || "").toLowerCase().includes(q) || (s.project || "").toLowerCase().includes(q) ); if (!list.length) { box.innerHTML = `
${sessData.length ? "没有匹配的对话" : "没有找到会话记录"}
`; return; } box.innerHTML = list .map( (s) => `
${esc(s.title)}
${esc(s.project || "—")}${(s.ts || "").slice(0, 10)}
` ) .join(""); box.querySelectorAll(".sess-item").forEach((el) => el.addEventListener("click", () => sessOpen(+el.dataset.idx, el)) ); } async function sessOpen(idx, el) { const item = sessData[idx]; sessCurrent = item; $$(".sess-item").forEach((x) => x.classList.remove("active")); if (el) el.classList.add("active"); $("#sess-empty").classList.add("hidden"); $("#sess-conv").classList.remove("hidden"); $("#sess-conv-title").textContent = item.title; $("#sess-messages").innerHTML = `
读取中…
`; try { const conv = await apic.sessionRead(item.path); const html = conv.messages .map((m) => { const who = m.role === "user" ? "👤 用户" : m.role === "assistant" ? "🤖 Codex" : "🔧 工具"; return `
${who}
${esc(m.text)}
`; }) .join(""); $("#sess-messages").innerHTML = html || `
这条对话没有可显示的消息
`; } catch (e) { $("#sess-messages").innerHTML = `
读取失败:${esc(e.message || e)}
`; } } async function sessLoad() { const box = $("#sess-list"); box.innerHTML = `
加载中…
`; try { sessData = await apic.sessionsList(false); sessLoaded = true; sessRender(); } catch (e) { box.innerHTML = `
加载失败:${esc(e.message || e)}
`; } } $("#sess-search").addEventListener("input", sessRender); $("#sess-refresh").addEventListener("click", sessLoad); document.querySelector('.tab[data-tab="sessions"]').addEventListener("click", () => { if (!sessLoaded) sessLoad(); }); $("#sess-export").addEventListener("click", async (ev) => { if (!sessCurrent) return; const btn = ev.currentTarget; busy(btn, true, "导出中…"); try { const r = await apic.sessionExport(sessCurrent); if (!r.canceled) toast("已导出 ✅", "ok"); } catch (e) { toast(`导出失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); $("#sess-backup").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "备份中…"); try { const r = await apic.sessionsBackup(); if (!r.canceled) toast(`已备份 ${r.count} 条会话 ✅`, "ok"); } catch (e) { toast(`备份失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); $("#sess-import").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "导入中…"); try { const r = await apic.sessionsImport(); if (!r.canceled) { toast(`导入完成:新增 ${r.added},跳过 ${r.skipped}`, "ok"); sessLoad(); } } catch (e) { toast(`导入失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); apic.onProviderSyncProgress((p) => { const box = $("#sync-progress"); const fill = $("#sync-progress-fill"); const label = $("#sync-progress-label"); box.classList.remove("hidden"); if (p.phase === "完成") { fill.style.width = "100%"; label.textContent = "完成 ✅"; } else if (p.total > 0) { const pct = Math.round((p.done / p.total) * 100); fill.style.width = pct + "%"; label.textContent = `${p.phase} ${p.done}/${p.total}(${pct}%)`; } else { fill.style.width = "8%"; label.textContent = p.phase + "…"; } }); $("#sync-btn").addEventListener("click", async (ev) => { const st = await apic.providerSyncStatus(); if (st.running) { toast("请先完全退出 Codex(Cmd+Q)再同步", "err"); return; } if (!confirm(`将把全部历史对话同步到当前账号(provider:${st.target}),使其在 Codex 侧栏重新显示。\n\n开始前会自动备份,可还原。确认继续?`)) return; const btn = ev.currentTarget; busy(btn, true, "同步中…"); $("#sync-progress").classList.remove("hidden"); $("#sync-progress-fill").style.width = "0%"; try { const r = await apic.providerSync(); $("#sync-progress-fill").style.width = "100%"; const changedMsg = r.changedFiles > 0 ? `本次同步 ${r.changedFiles} 条` : `全部已是最新,无需改动`; $("#sync-progress-label").textContent = `完成 ✅ 共 ${r.totalConversations} 条会话,${changedMsg}`; const tip = r.changedFiles > 0 ? `同步完成 ✅ 本次改写 ${r.changedFiles} 条会话。重新打开 Codex 即可看到` : `全部 ${r.totalConversations} 条会话之前已同步过,本次无需改动 ✅`; toast(tip, "ok"); setTimeout(() => $("#sync-progress").classList.add("hidden"), 4000); } catch (e) { $("#sync-progress").classList.add("hidden"); toast(`同步失败:${e.message || e}`, "err"); } finally { busy(btn, false); } }); // =========================================================================== // 会话记忆 —— 状态 / 项目开关 / 历史摘要检索 // =========================================================================== let memStatus = null; let memLoaded = false; function memRenderStatus(st) { memStatus = st; const badge = $("#mem-state"); badge.textContent = st.enabled ? "已开启" : "已关闭"; badge.className = "pc-badge " + (st.enabled ? "skill" : ""); $("#mem-engine").textContent = st.engine + (st.pending ? ` · 待处理 ${st.pending}` : ""); $("#mem-count").textContent = `记忆库共 ${st.total} 条摘要`; const tg = $("#mem-toggle"); tg.textContent = st.enabled ? "关闭记忆" : "开启记忆"; tg.className = "btn " + (st.enabled ? "ghost" : "primary"); // 项目列表(开关 + 打开 HANDOFF) const box = $("#mem-projects"); if (!st.projects.length) { box.innerHTML = `
还没有任何项目的会话被摘要。去 Codex 里聊一会儿,静默 ${st.idleMinutes} 分钟后这里就有了。
`; } else { box.innerHTML = st.projects .map( (p) => `
${esc(p.project || "(未知)")}
${esc(p.cwd)}
${p.count} 条 · ${(p.last || "").slice(0, 10)} ${p.hasHandoff ? `` : ""}
` ) .join(""); box.querySelectorAll("[data-mem-proj]").forEach((el) => el.addEventListener("change", async () => { try { memRenderStatus(await apic.memoryToggleProject(el.dataset.memProj, el.checked)); toast(el.checked ? "已开启该项目的记忆写入 ✅" : "已关闭该项目的记忆写入", "ok"); } catch (e) { toast("操作失败:" + (e.message || e), "err"); } }) ); box.querySelectorAll("[data-mem-open]").forEach((el) => el.addEventListener("click", async () => { try { await apic.memoryOpenHandoff(el.dataset.memOpen); } catch (e) { toast(e.message || String(e), "err"); } }) ); } // 项目筛选下拉(保留当前选中) const sel = $("#mem-project-filter"); const cur = sel.value; sel.innerHTML = '' + st.projects.map((p) => ``).join(""); sel.value = cur; } async function memRenderList() { const box = $("#mem-list"); const empty = $("#mem-empty"); empty.classList.add("hidden"); try { const rows = await apic.memoryList({ project: $("#mem-project-filter").value, q: $("#mem-search").value.trim(), limit: 100, }); if (!rows.length) { box.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = "没有匹配的摘要。"; return; } box.innerHTML = rows .map((r) => { const when = (r.ts || "").slice(0, 16).replace("T", " "); const dec = r.decisions.length ? `
决策${r.decisions.map((d) => esc(d)).join(" · ")}
` : ""; const next = r.next_steps.length ? `
下一步${r.next_steps.map((d) => esc(d)).join(" · ")}
` : ""; const files = r.files.length ? `
文件${esc(r.files.join("、"))}
` : ""; return `
${esc(r.topic)} ${esc(r.project || "—")} ${when} ${r.engine === "local" ? "本地提取" : "LLM"}
${esc(r.summary)}
${dec}${next}${files}
`; }) .join(""); box.querySelectorAll("[data-mem-redo]").forEach((el) => el.addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "摘要中…"); try { await apic.memoryResummarize(el.dataset.memRedo); toast("已重新摘要 ✅", "ok"); } catch (e) { toast("摘要失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }) ); box.querySelectorAll("[data-mem-del]").forEach((el) => el.addEventListener("click", async () => { if (!confirm("删除这条摘要?(不影响原始会话记录)")) return; try { await apic.memoryDelete(el.dataset.memDel); } catch (e) { toast("删除失败:" + (e.message || e), "err"); } }) ); } catch (e) { box.innerHTML = ""; empty.classList.remove("hidden"); empty.textContent = "加载失败:" + (e.message || e); } } async function memLoad() { memLoaded = true; try { memRenderStatus(await apic.memoryStatus()); await memRenderList(); } catch (e) { toast("会话记忆读取失败:" + (e.message || e), "err"); } } document.querySelector('.tab[data-tab="memory"]').addEventListener("click", () => { if (!memLoaded) memLoad(); }); $("#mem-refresh").addEventListener("click", memLoad); $("#mem-search").addEventListener("input", memRenderList); $("#mem-project-filter").addEventListener("change", memRenderList); $("#mem-toggle").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "处理中…"); try { memRenderStatus(await apic.memorySetEnabled(!(memStatus && memStatus.enabled))); } catch (e) { toast("操作失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); apic.onMemoryChanged(() => { if (memLoaded) memLoad(); }); // =========================================================================== // 说明弹窗 —— 使用说明 / 版权声明 // =========================================================================== const MODAL_CONTENT = { usage: { title: "📖 使用说明", html: `

插件是「命令包」,不是单个命令

一个插件(如 creative-production)里通常包含多个技能命令。安装后不是输入插件名,而是输入里面的具体命令,比如 /explore、/ads-explorer。

三步开始使用

1在「插件市场」搜索并点 安装,或在「备份与还原 → 从 GitHub 拉取还原」里拉取仓库后还原到本机。
2重启 Codex,让新装的插件生效。
3在 Codex 输入框打 /,再输入命令关键词(如 ads、scan)即可定位并运行对应功能。

备份 / 分享给他人

每个插件具体能干嘛?

点插件卡片可查看它的功能说明;各插件仓库首页的 README 里也有完整的命令清单与示例。

`, }, notice: { title: "📄 版权声明 / NOTICE", html: `

内容归属

本市场收录并可分发的插件为 各自作者(含 OpenAI Codex 官方插件) 的作品,其源代码及相关知识产权归 原始著作权人 所有。本工具仅提供备份、分享与在已获授权环境中的还原,不修改插件源代码。

使用范围

凭证安全

导出 / 上传时,凭证文件(auth.json、config.toml、history 等)已自动排除;GitHub Token 仅经系统安全存储(safeStorage)加密后保存在本机。

免责声明

本工具及其分发的备份按「现状(AS IS)」提供,不附带任何明示或默示担保。作者不对因使用而产生的任何后果负责。本声明不构成对第三方代码的任何再授权。

`, }, }; const modalOverlay = $("#modal-overlay"); function openModal(key) { const c = MODAL_CONTENT[key]; if (!c) return; $("#modal-title").textContent = c.title; $("#modal-body").innerHTML = c.html; modalOverlay.classList.remove("hidden"); } function closeModal() { modalOverlay.classList.add("hidden"); } $("#open-usage").addEventListener("click", () => openModal("usage")); $("#open-notice").addEventListener("click", () => openModal("notice")); $("#modal-close").addEventListener("click", closeModal); modalOverlay.addEventListener("click", (e) => { if (e.target === modalOverlay) closeModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !modalOverlay.classList.contains("hidden")) closeModal(); }); // =========================================================================== // 路由中转 // =========================================================================== let routerState = null; let grpEditing = null; function renderRouter(st) { routerState = st; $("#router-addr").textContent = st.address; const badge = $("#router-state"); badge.textContent = st.running ? "运行中" : "未启动"; badge.className = "pc-badge " + (st.running ? "skill" : ""); $("#router-toggle").textContent = st.running ? "停止" : "启动"; $("#router-key").value = st.routerKey; if (document.activeElement !== $("#router-port")) $("#router-port").value = st.port; const claudeBtn = $("#router-apply-claude"); if (claudeBtn) { claudeBtn.textContent = st.claudeApplied ? "取消接入 Claude Code" : "接入 Claude Code"; claudeBtn.classList.toggle("ghost", !!st.claudeApplied); } const box = $("#router-groups"); if (!st.groups.length) { box.innerHTML = `
还没有分组,点右上「+ 新增分组」添加一个中转 API
`; return; } box.innerHTML = st.groups .map((g) => { const badges = (g.id === st.activeId ? '当前' : "") + (g.isDefault ? ' 默认·不可删' : ""); const actions = g.isDefault ? '切回这里 = 直连你的账号' : ``; return `
${esc(g.name)} ${badges}
${esc(g.baseUrl)}${g.model ? " · 模型 " + esc(g.model) : ""}${!g.isDefault && g.mode === "chat" ? " · 🔄转换" : ""}${g.hasKey ? "" : " · ⚠️无密钥"}
${actions}
`; }) .join(""); box.querySelectorAll("[data-active]").forEach((el) => el.addEventListener("change", async () => { const r = await apic.routerSetActive(el.dataset.active); renderRouter(r); if (r._needsRestart) toast("已切换,重启 Codex 后生效(配置有变)", "ok"); else if (r._mode === "direct") toast("已切回默认(直连你的账号)✅", "ok"); else toast("已秒切分组 ✅ 无需重启 Codex", "ok"); }) ); box.querySelectorAll("[data-edit]").forEach((el) => el.addEventListener("click", () => openGrpModal(st.groups.find((g) => g.id === el.dataset.edit))) ); box.querySelectorAll("[data-del]").forEach((el) => el.addEventListener("click", async () => { if (confirm("删除这个分组?")) renderRouter(await apic.routerDeleteGroup(el.dataset.del)); }) ); } async function loadRouter() { try { renderRouter(await apic.routerStatus()); } catch (e) { toast("路由状态读取失败:" + (e.message || e), "err"); } } function openGrpModal(g) { grpEditing = g || null; $("#grp-modal-title").textContent = g ? "编辑分组" : "新增分组"; $("#grp-name").value = g ? g.name : ""; $("#grp-mode").value = g && g.mode ? g.mode : "responses"; $("#grp-url").value = g ? g.baseUrl : ""; $("#grp-key").value = ""; // 密钥不回显,留空则不改 $("#grp-key").placeholder = g && g.hasKey ? "(已保存,留空则不修改)" : "sk-…"; $("#grp-model").value = g ? g.model || "" : ""; $("#grp-ctx").value = g && g.contextWindow ? g.contextWindow : ""; const sel = $("#grp-model-list"); sel.classList.add("hidden"); sel.innerHTML = ''; $("#grp-model-hint").textContent = ""; $("#grp-modal").classList.remove("hidden"); } function closeGrpModal() { $("#grp-modal").classList.add("hidden"); } document.querySelectorAll("[data-ctx]").forEach((b) => b.addEventListener("click", () => { $("#grp-ctx").value = b.dataset.ctx; }) ); $("#grp-add").addEventListener("click", () => openGrpModal(null)); $("#grp-modal-close").addEventListener("click", closeGrpModal); $("#grp-modal").addEventListener("click", (e) => { if (e.target === $("#grp-modal")) closeGrpModal(); }); $("#grp-save").addEventListener("click", async () => { const name = $("#grp-name").value.trim(); const baseUrl = $("#grp-url").value.trim(); if (!name || !baseUrl) { toast("名称和 API 地址必填", "err"); return; } const group = { name, baseUrl, model: $("#grp-model").value.trim(), mode: $("#grp-mode").value, contextWindow: $("#grp-ctx").value.trim(), }; if (grpEditing) group.id = grpEditing.id; const key = $("#grp-key").value; if (key) group.key = key; else if (grpEditing) group.key = undefined; // 留空且是编辑 → 保留原密钥(主进程 merge 时不覆盖) // 编辑留空时不传 key 字段,避免覆盖 if (grpEditing && !key) delete group.key; try { renderRouter(await apic.routerUpsertGroup(group)); closeGrpModal(); toast("已保存分组 ✅", "ok"); } catch (e) { toast("保存失败:" + (e.message || e), "err"); } }); $("#grp-fetch-models").addEventListener("click", async (ev) => { const btn = ev.currentTarget; const baseUrl = $("#grp-url").value.trim(); const key = $("#grp-key").value; // 留空且是编辑 → 用已保存的密钥(传 groupId) const hint = $("#grp-model-hint"); if (!baseUrl) { hint.textContent = "请先填 API 地址"; return; } busy(btn, true, "获取中…"); hint.textContent = ""; try { const models = await apic.routerFetchModels(baseUrl, key, grpEditing ? grpEditing.id : null); const sel = $("#grp-model-list"); sel.innerHTML = '' + models.map((m) => ``).join(""); // 若当前已有模型值,选中它 if ($("#grp-model").value) sel.value = $("#grp-model").value; sel.classList.remove("hidden"); hint.textContent = `共 ${models.length} 个模型,选一个或保持手填`; } catch (e) { hint.textContent = "获取失败:" + (e.message || e) + "(可手动填模型)"; } finally { busy(btn, false); } }); $("#grp-model-list").addEventListener("change", (e) => { if (e.target.value) $("#grp-model").value = e.target.value; }); $("#router-toggle").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "处理中…"); try { renderRouter(routerState.running ? await apic.routerStop() : await apic.routerStart()); } catch (e) { toast("操作失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); $("#router-save-port").addEventListener("click", async () => { try { renderRouter(await apic.routerSetPort($("#router-port").value)); toast("端口已更新", "ok"); } catch (e) { toast("改端口失败:" + (e.message || e), "err"); } }); $("#router-apply").addEventListener("click", async (ev) => { if (!confirm("将修改 Codex 的 config.toml,指向本地路由(会自动备份)。\n注意:启用后账号切换请统一在本软件切,不要再用 cc-switch,以免冲突。\n\n确认?")) return; const btn = ev.currentTarget; busy(btn, true, "配置中…"); try { const r = await apic.routerApplyCodex(); renderRouter(r.status); toast("已配置到 Codex ✅ 重启 Codex 后即可通过本地路由,之后切分组无需重启", "ok"); } catch (e) { toast("配置失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); $("#router-apply-claude").addEventListener("click", async (ev) => { const applied = routerState && routerState.claudeApplied; if (applied) { if (!confirm("取消接入:将从 ~/.claude/settings.json 移除 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN(还原成你原来的值)。\n\n确认?")) return; const btn = ev.currentTarget; busy(btn, true, "还原中…"); try { const r = await apic.routerRestoreClaude(); renderRouter(r.status); toast("已取消接入 Claude Code ✅", "ok"); } catch (e) { toast("还原失败:" + (e.message || e), "err"); } finally { busy(btn, false); } return; } const hasAnthropicGroup = routerState && routerState.groups.some((g) => !g.isDefault && g.hasKey); if (!hasAnthropicGroup && !confirm("还没有可用的中转分组。先去下面新增一个 Anthropic 格式的中转(base_url 填站点根、模式选 Responses 直连、填密钥),再回来接入。\n\n仍要继续?")) return; if (!confirm("将修改 ~/.claude/settings.json,把 ANTHROPIC_BASE_URL 指向本地路由(会自动备份)。\n须搭配 Anthropic 格式的中转分组使用。\n\n确认?")) return; const btn = ev.currentTarget; busy(btn, true, "接入中…"); try { const r = await apic.routerApplyClaude(); renderRouter(r.status); toast("已接入 Claude Code ✅ 新开 claude 会话即走本地路由,之后切分组无需重启", "ok"); } catch (e) { toast("接入失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); document.querySelector('.tab[data-tab="router"]').addEventListener("click", loadRouter); apic.onRouterChanged((st) => renderRouter(st)); // =========================================================================== // 在线更新 // =========================================================================== async function loadVersion() { try { $("#app-version").textContent = "v" + (await apic.appVersion()); } catch {} } // 显示"发现新版本 + 前往下载"链接(Mac 半自动更新) const RELEASE_FALLBACK = "https://update.tokendun.cc/mhgmh/codex-market-app/releases"; const updateModal = $("#update-modal"); let updateMandatory = false; // 更新弹窗:显示版本 + 更新内容,区分强制 / 一般 function showUpdateModal({ version, notes, mandatory, url }) { const link = url || RELEASE_FALLBACK; updateMandatory = !!mandatory; $("#update-title").textContent = mandatory ? "重要更新(必须升级)" : "发现新版本"; $("#update-ver").textContent = `新版本 v${version} · 当前 v${$("#app-version").textContent.replace(/^v/, "") || "?"}`; const badge = $("#update-badge"); badge.textContent = mandatory ? "强制更新" : "推荐更新"; badge.className = "update-badge " + (mandatory ? "force" : "normal"); $("#update-notes").textContent = notes || ""; // 按钮 const foot = $("#update-foot"); foot.innerHTML = mandatory ? ` ` : ` `; $("#upd-go").addEventListener("click", () => apic.openExternal(link)); if (mandatory) { $("#upd-quit").addEventListener("click", () => apic.quitApp()); } else { $("#upd-later").addEventListener("click", closeUpdateModal); } updateModal.classList.remove("hidden"); } function closeUpdateModal() { if (updateMandatory) return; // 强制更新不允许关闭 updateModal.classList.add("hidden"); } // 一般更新:点背景关闭;强制更新:点背景无效 updateModal.addEventListener("click", (e) => { if (e.target === updateModal) closeUpdateModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !updateModal.classList.contains("hidden")) closeUpdateModal(); }); $("#btn-check-update").addEventListener("click", async (ev) => { const btn = ev.currentTarget; const info = $("#update-info"); busy(btn, true, "检查中…"); info.textContent = ""; try { const r = await apic.checkUpdate(); if (r.dev) info.textContent = "(开发版不检查更新)"; else if (r.network) { info.innerHTML = `⚠️ ${esc(r.error)} 前往下载页 ↗`; const a = $("#dl-manual"); if (a) a.addEventListener("click", () => apic.openExternal(r.url || RELEASE_FALLBACK)); } else if (r.error) info.textContent = "检查失败:" + r.error; else if (r.hasNew) { info.textContent = `发现新版本 v${r.version}`; showUpdateModal({ version: r.version, notes: r.notes, mandatory: r.mandatory, url: r.url }); } else info.textContent = "已是最新版本 ✅"; } catch (e) { info.textContent = "检查失败:" + (e.message || e); } finally { busy(btn, false); } }); apic.onUpdateStatus((s) => { const info = $("#update-info"); if (s.state === "available-manual") { info.textContent = `发现新版本 v${s.version}`; showUpdateModal({ version: s.version, notes: s.notes, mandatory: s.mandatory, url: s.url }); } else if (s.state === "available") info.textContent = `发现新版本 ${s.version},下载中…`; else if (s.state === "downloading") info.textContent = `下载新版本 ${s.percent}%…`; else if (s.state === "downloaded") info.textContent = `新版本 ${s.version} 已就绪,退出软件即自动更新`; else if (s.state === "none") info.textContent = "已是最新版本 ✅"; else if (s.state === "error") info.textContent = "更新检查失败:" + (s.message || ""); }); // =========================================================================== // 全局 / 系统代理 // =========================================================================== function renderProxy(st) { const badge = $("#proxy-state"); if (!badge) return; const detail = $("#proxy-state-detail"); const stopBtn = $("#proxy-stop"); const gBtn = $("#proxy-start-global"); const sBtn = $("#proxy-start-system"); if (st.mode === "global") { badge.textContent = "全局代理运行中"; badge.className = "pc-badge on"; detail.textContent = "整机 TUN · 所有程序流量走代理"; } else if (st.mode === "system") { badge.textContent = "系统代理运行中"; badge.className = "pc-badge on"; detail.textContent = `本地中转 127.0.0.1:${st.localPort}`; } else { badge.textContent = "未开启"; badge.className = "pc-badge"; detail.textContent = st.proxy ? `已配置 ${st.proxy.host}:${st.proxy.port}${st.proxy.hasAuth ? " · 需密码" : ""}` : "未配置代理"; } const running = st.mode !== "off"; stopBtn.classList.toggle("hidden", !running); const disabled = !st.hasConfig || !st.binOk; gBtn.disabled = disabled; sBtn.disabled = disabled; gBtn.classList.toggle("active", st.mode === "global"); sBtn.classList.toggle("active", st.mode === "system"); if (!st.binOk) detail.textContent = "⚠️ 缺少 sing-box 引擎文件,请重装软件"; } async function loadProxy() { try { const cfg = await apic.proxyGetConfig(); if (cfg && cfg.proxyStr && !$("#proxy-str").value) $("#proxy-str").value = cfg.proxyStr; renderProxy(await apic.proxyStatus()); } catch (e) { toast("代理状态读取失败:" + (e.message || e), "err"); } } $("#proxy-save").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "保存中…"); try { await apic.proxySetConfig({ proxyStr: $("#proxy-str").value.trim() }); renderProxy(await apic.proxyStatus()); toast("已保存 ✅", "ok"); } catch (e) { toast("保存失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); $("#proxy-test").addEventListener("click", async (ev) => { const btn = ev.currentTarget; const out = $("#proxy-test-out"); busy(btn, true, "测试中…"); out.textContent = "正在通过代理请求出口 IP…"; try { await apic.proxySetConfig({ proxyStr: $("#proxy-str").value.trim() }); const r = await apic.proxyTest(); out.innerHTML = `✅ 代理可用 · 出口 IP:${esc(r.ip)}`; } catch (e) { out.textContent = "❌ 测试失败:" + (e.message || e); } finally { busy(btn, false); } }); async function startProxy(mode, btn) { busy(btn, true, mode === "global" ? "启动全局中…" : "启动系统代理中…"); try { await apic.proxySetConfig({ proxyStr: $("#proxy-str").value.trim() }); renderProxy(await apic.proxyStart(mode)); toast(mode === "global" ? "全局代理已开启(整机)✅" : "系统代理已开启 ✅", "ok"); } catch (e) { toast("启动失败:" + (e.message || e), "err"); try { renderProxy(await apic.proxyStatus()); } catch {} } finally { busy(btn, false); } } $("#proxy-start-global").addEventListener("click", (ev) => startProxy("global", ev.currentTarget)); $("#proxy-start-system").addEventListener("click", (ev) => startProxy("system", ev.currentTarget)); $("#proxy-stop").addEventListener("click", async (ev) => { const btn = ev.currentTarget; busy(btn, true, "关闭中…"); try { renderProxy(await apic.proxyStop()); toast("代理已关闭,网络已还原", "ok"); } catch (e) { toast("关闭失败:" + (e.message || e), "err"); } finally { busy(btn, false); } }); document.querySelector('.tab[data-tab="proxy"]').addEventListener("click", loadProxy); apic.onProxyChanged((st) => renderProxy(st)); // —— 启动 —— loadMarket(); scanLocal(); loadSecrets(); loadVersion(); loadProxy();