// 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 `
${CAT_ICON[name] || "•"}
${esc(name)}${c}
`;
};
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 = '';
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 ``;
})
.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;
$("#sess-scope").textContent = "当前会话";
sessRender();
} catch (e) {
box.innerHTML = `加载失败:${esc(e.message || e)}
`;
}
}
async function sessLoadAll() {
const box = $("#sess-list");
box.innerHTML = ``;
try {
sessData = await apic.sessionsList({ includeArchived: true, includeSubagent: true });
sessLoaded = true;
$("#sess-scope").textContent = "全部记录";
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-fetch-all").addEventListener("click", sessLoadAll);
$("#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},侧栏重建 ${r.catalogRebuilt || 0} 条 ✅`, "ok");
sessLoaded = false;
await 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)即可定位并运行对应功能。
备份 / 分享给他人
- 「从本机导出 / 上传」可把你本机的插件打包推送到 GitHub 仓库(凭证文件已自动排除)。
- 把生成的仓库链接发给对方,对方在本 app 用「从 GitHub 拉取还原」即可一键装好。
每个插件具体能干嘛?
点插件卡片可查看它的功能说明;各插件仓库首页的 README 里也有完整的命令清单与示例。
`,
},
notice: {
title: "📄 版权声明 / NOTICE",
html: `
内容归属
本市场收录并可分发的插件为 各自作者(含 OpenAI Codex 官方插件) 的作品,其源代码及相关知识产权归 原始著作权人 所有。本工具仅提供备份、分享与在已获授权环境中的还原,不修改插件源代码。
使用范围
- 请在已获得相应使用授权的前提下备份与还原插件。
- 使用时请遵守 OpenAI 服务条款及各插件的原始许可条款。
- 如不确定是否有权使用某插件,请以其官方渠道说明为准。
凭证安全
导出 / 上传时,凭证文件(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 codexBtn = $("#router-apply");
if (codexBtn) {
const applied = !!st.codexApplied;
codexBtn.textContent = applied ? "取消本地路由(恢复直连)" : "启用本地路由(高级)";
codexBtn.classList.toggle("ghost", applied);
}
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) => {
const applied = routerState && routerState.codexApplied;
const message = applied
? "将取消本地路由,并立即从 Codex config.toml / auth.json 清除本工具写入的配置,恢复 Codex 原生登录直连。\n\n确认?"
: "将修改 Codex 的 config.toml,指向本地路由(会自动备份)。\n注意:启用后账号切换请统一在本软件切,不要再用 cc-switch,以免冲突。\n\n确认?";
if (!confirm(message)) return;
const btn = ev.currentTarget;
busy(btn, true, applied ? "取消中…" : "配置中…");
try {
const r = applied ? await apic.routerDisableCodex() : await apic.routerApplyCodex();
renderRouter(r.status);
toast(
applied
? "已取消本地路由 ✅ Codex 配置已恢复直连,重启 Codex 后生效"
: "已配置到 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();