Title: URL Source: https://clonewars.rushos.com/app.js Markdown Content: /* AI Clone Wars 前端 v2: 战场图谱 / 排行榜 / 机会板 + 筛选 + 被克隆进度 + 提交 */ const API = location.hostname === "localhost" || location.hostname === "127.0.0.1" ? "https://clonewars.rushos.com" : ""; // 本地预览时提交打到线上 const CATMETA = { ai: { title: "AI 战场", sub: "a16z Gen AI 100 · 第6期 (2026-03)" }, traffic: { title: "流量战场", sub: "Cloudflare Radar 全球 Top 1000 域名" }, other: { title: "其他战场", sub: "两榜之外的经典克隆战争(遗留保留区)" }, }; let DATA = null, LB = null, OPS = null; let state = { view: "map", cat: "all", q: "", onlyCurated: true, status: null, family: null, ncat: null }; const NCAT_ZH = {}; let ALL_ITEMS = []; const EXPANDED = new Set(); const NAME_BY_ID = {}; const $ = (s, el) => (el || document).querySelector(s); const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const fmtStars = (n) => (n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k" : n); const tierWord = (p) => p >= 100 ? "已被超越/开源" : p >= 80 ? "主流可迁移" : p >= 60 ? "小规模生产" : p >= 40 ? "自用可行" : p >= 20 ? "Demo 可跑" : p > 0 ? "萌芽" : "空白"; function starGlyphs(progress) { const half = Math.round(progress / 10); const full = Math.floor(half / 2), hasHalf = half % 2 === 1; const off = 5 - full - (hasHalf ? 1 : 0); return "★".repeat(full) + (hasHalf ? "½" : "") + (off > 0 ? `${"★".repeat(off)}` : ""); } /* ---------- 战场卡 ---------- */ function clonedBar(b) { if (b.no_battlefield) return ""; const p = b.cloned || 0; const cls = p >= 60 ? "" : p >= 30 ? "mid" : "low"; return `
被克隆 ${p}%
${tierWord(p)} · ${b.clone_count} 个项目
`; } function entryHtml(e) { const gh = `https://github.com/${e.repo}`; const meter = e.relation === "self" ? `
本体开源
` : `
${starGlyphs(e.progress)}
${e.progress}%${e.relation === "part" ? " · 部件" : ""}${e.unverified ? " · 未校验" : ""}
`; const badges = [ `${{ clone: "克隆", adjacent: "同生态位", self: "本体开源", part: "部件" }[e.relation]}`, e.archived ? `archived` : "", e.rechecked ? `✓ ${esc(e.rechecked)} 核` : "", ].join(" "); return `
${meter}
${esc(e.repo)} ★ ${fmtStars(e.stars ?? 0)}${e.low_star ? " ·未过千" : ""}${e.language ? `${esc(e.language)}` : ""}${badges}
${esc(e.evidence)}${e.scope_note ? ` 〔${esc(e.scope_note)}〕` : ""}
`; } function bfHtml(b) { const chips = [ b.family ? `${esc(b.family)}` : "", b.parent ? `↑ ${esc(NAME_BY_ID[b.parent] || b.parent)}` : "", b.is_layer ? `底层模型` : "", b.n_category && NCAT_ZH[b.n_category] ? `${esc(NCAT_ZH[b.n_category])}` : "", b.a16z ? `a16z` : "", b.cf_rank ? `CF #${b.cf_rank}` : (b.category === "traffic" ? `CF Top1000` : ""), ...(b.domains || []).slice(0, 1).map((d) => `${esc(d)}`), ].join(" "); const act = `
`; if (b.no_battlefield) { return `

${esc(b.name)}

${chips}${act}
未设战场:${esc(b.no_battlefield)}
`; } if (b.alias_of) { return `

${esc(b.name)}

${chips}${act}
${esc(b.note || "")} → 见 ${esc(NAME_BY_ID[b.alias_of] || b.alias_of)}
`; } const entries = b.entries.map(entryHtml).join(""); const related = b.related?.length ? `` : ""; const nonoss = b.non_oss?.length ? `
非开源回应(未来克隆参考)${b.non_oss.map((n) => `
${esc(n.name)}${n.by ? ` ${esc(n.by)}` : ""}${n.note ? ` — ${esc(n.note)}` : ""}
`).join("")}
` : ""; const auto = b.auto_candidates?.length ? `
未评审候选:${b.auto_candidates.map((c) => `${esc(c.repo)} ★${fmtStars(c.stars)}`).join(" · ")}
` : ""; const nothing = (!b.entries.length && !b.auto_candidates?.length && !nonoss) ? `
暂无 ≥1000 星克隆 —— 空白战场就是机会,欢迎 提交。
` : ""; const kw = b.kw?.length ? `
检索关键词${b.kw.map((k) => `${esc(k)}`).join("")}
` : ""; return `

${esc(b.name)}

${chips}${act}
${clonedBar(b)}${b.note ? `
${esc(b.note)}
` : ""}${entries}${auto}${nonoss}${related}${nothing}${kw}
`; } /* ---------- 筛选 ---------- */ const LEDGER_STATUS = new Set(["infra", "adtech", "regional", "gambling", "unknown"]); function statusPass(b) { if (!state.status) return true; return b.status === state.status; } // 浏览模式 = 有筛选/搜索时把台账服务也纳入; 否则只看战场 function browseMode() { return !!(state.status || state.ncat || state.family || state.q); } function itemUniverse() { return browseMode() ? ALL_ITEMS : DATA.battlefields; } function matches(b) { if (state.cat !== "all" && b.category !== state.cat) return false; if (!statusPass(b)) return false; if (state.family && b.family !== state.family) return false; if (state.ncat && b.n_category !== state.ncat && b.n_category2 !== state.ncat) return false; if (state.q) { const hay = [b.name, b.id, b.category_hint, b.why, ...(b.domains || []), ...(b.kw || []), ...((b.entries || []).map((e) => e.repo + " " + (e.evidence || "")))].join(" ").toLowerCase(); if (!hay.includes(state.q)) return false; } // 默认(非浏览模式)且勾了"只看有战事": 隐藏无克隆战场 if (!browseMode() && state.onlyCurated && !(b.entries && b.entries.length)) return false; return true; } function renderFilter() { const f = DATA.facets; const seg = (label, items, key) => `
${label}
${items.map((it) => `${esc(it.label)}${it.count != null ? ` ${it.count}` : ""}`).join("")}
`; $("#fpanel").innerHTML = `
筛选 ×
` + seg("状态 / 类型", f.status, "status") + (f.ncat?.length ? seg("需求大类", f.ncat.map((c) => ({ id: c.id, label: c.zh, count: c.count })), "ncat") : "") + (f.family.length ? seg("家族", f.family.map((x) => ({ id: x, label: x })), "family") : "") + (browseMode() ? `
清除全部筛选
` : ""); const n = (state.status ? 1 : 0) + (state.family ? 1 : 0) + (state.ncat ? 1 : 0); const fc = $("#fcount"); fc.textContent = n; fc.hidden = !n; } /* ---------- 台账服务(未成战场)紧凑卡 ---------- */ const STATUS_ZH = { infra: "基础设施", adtech: "广告技术", regional: "地区门户", gambling: "博彩/成人", unknown: "暂未识别", surpassed: "已超越", strong: "主流可迁移", early: "早期", empty: "空白" }; function ledgerCardHtml(b) { const nc = b.n_category && NCAT_ZH[b.n_category] ? `${esc(NCAT_ZH[b.n_category])}` : ""; const doms = (b.domains || []).slice(0, 4).map((d) => `${esc(d)}`).join("") + ((b.domains || []).length > 4 ? `+${b.domains.length - 4}` : ""); return `

${esc(b.name)}

${STATUS_ZH[b.status] || b.status}${nc}${doms}
${b.why ? `
${esc(b.why)}
` : ""}
未单独设战场。若你知道有对应开源克隆/部件,欢迎 提交修正。
`; } function cardHtml(b) { return b.ledger ? ledgerCardHtml(b) : bfHtml(b); } /* ---------- 视图: 战场图谱 / 浏览 ---------- */ const NCAT_ORDER = (id) => (DATA.facets.ncat.findIndex((c) => c.id === id)); function renderMap() { const uni = itemUniverse(); let list; if (state.cat === "all") list = uni.filter(matches); else list = uni.filter((b) => b.category === state.cat).filter(matches); // 非浏览模式: 保持原三分类战场视图 if (!browseMode()) { const cats = state.cat === "all" ? ["ai", "traffic", "other"] : [state.cat]; $("#leftIndex").innerHTML = ""; $("#main").classList.remove("withindex"); $("#main").innerHTML = cats.map((cat) => { const l = list.filter((b) => b.category === cat); if (!l.length) return ""; const m = CATMETA[cat]; const opened = l.filter((b) => b.entries && b.entries.length).length; const avg = DATA.stats.cloned?.[cat]; return `

${m.title}

${m.sub} · ${opened} 开战${avg != null ? ` · 平均被克隆 ${avg}%` : ""}
${l.map(cardHtml).join("")}
`; }).join("") || `
没有匹配的战场。
`; return; } // 浏览模式: 按 N大类分组, 组内按状态细分; <100 出左侧索引, ≥100 折叠强制细化 const small = list.length < 100; $("#main").classList.toggle("withindex", small && list.length > 0); // 分组: 优先 N大类; 无 N大类的按状态归组(不显示"未归类") const glabel = (gk) => gk.startsWith("S:") ? (STATUS_ZH[gk.slice(2)] || gk.slice(2)) : (NCAT_ZH[gk] || "未归类"); const groups = {}; for (const b of list) { const g = b.n_category || ("S:" + b.status); (groups[g] ||= []).push(b); } const gkeys = Object.keys(groups).sort((a, c) => (a.startsWith("S:") - c.startsWith("S:")) || ((NCAT_ORDER(a) < 0 ? 99 : NCAT_ORDER(a)) - (NCAT_ORDER(c) < 0 ? 99 : NCAT_ORDER(c)))); const STATUS_SORT = ["surpassed", "strong", "early", "empty", "regional", "gambling", "adtech", "infra", "unknown"]; let html = `
${list.length} 个匹配目标${small ? "" : "(超过 100,已折叠分组,请在右侧筛选里再选一个维度细化)"}
`; for (const gk of gkeys) { const items = groups[gk]; const zh = glabel(gk); // 组内按状态细分 const sub = {}; for (const b of items) (sub[b.status] ||= []).push(b); const subKeys = Object.keys(sub).sort((a, c) => STATUS_SORT.indexOf(a) - STATUS_SORT.indexOf(c)); const openGroup = small || items.length <= 24; html += `

${esc(zh)} ${items.length}

${subKeys.map((s) => `${STATUS_ZH[s] || s} ${sub[s].length}`).join(" · ")}${openGroup ? "▾" : "▸"}
`; for (const sk of subKeys) { const arr = sub[sk]; const cap = small || EXPANDED.has(gk + ":" + sk) ? arr.length : 40; html += arr.length && subKeys.length > 1 ? `
${STATUS_ZH[sk] || sk} · ${arr.length}
` : ""; html += arr.slice(0, cap).map(cardHtml).join(""); if (arr.length > cap) html += `
还有 ${arr.length - cap} 个,点此展开
`; } html += `
`; } $("#main").innerHTML = html; // 左侧跳转索引(<100) if (small && list.length > 0) { $("#leftIndex").innerHTML = `
${list.length} 项 · 点击跳转
` + gkeys.map((gk) => `
${esc(glabel(gk))}
${groups[gk].map((b) => `
${esc(b.name)}
`).join("")}
`).join(""); } else { $("#leftIndex").innerHTML = ""; } } /* ---------- 视图: 排行榜 ---------- */ function lbCard(title, hint, rows, valFn, cls) { return `

${title}${hint}

${rows.map((r, i) => `
${i + 1}${esc(r.name || r.repo)}${valFn(r)}
`).join("")}
`; } function renderBoard() { if (!LB) { $("#main").innerHTML = `
加载排行榜…
`; return; } $("#main").innerHTML = `
${lbCard("🏆 最被彻底克隆", "被克隆% 最高的战场", LB.most_cloned.slice(0, 12), (r) => r.cloned + "%", "pct")} ${lbCard("🛡️ 最抵抗克隆", "被克隆% 最低 = 最难复刻", LB.most_resistant.slice(0, 12), (r) => r.cloned + "%", "low")} ${lbCard("⚔️ 最激烈战场", "克隆项目数最多", LB.most_contested.slice(0, 12), (r) => r.clone_count + " 项")} ${lbCard("⭐ 开源星王", "参战项目按 star", LB.star_kings.slice(0, 12), (r) => "★" + fmtStars(r.stars))} ${lbCard("👑 已被超越", "被克隆 100% / 本体开源", LB.surpassed.slice(0, 12), (r) => r.clone_count + " 项", "pct")} ${lbCard("🕳️ 空白机会", "有资格但暂无 ≥1k 星克隆", LB.empty_targets.slice(0, 12), () => "0%", "low")}

平均被克隆度:AI ${LB.avg_cloned.ai}% · 流量 ${LB.avg_cloned.traffic}% · 其他 ${LB.avg_cloned.other}%。点任意一行跳到该战场。

`; } /* ---------- 视图: 机会板 ---------- */ function renderOps() { if (!OPS) { $("#main").innerHTML = `
加载机会板…
`; return; } const catBadge = (c) => `${c}`; const opCard = (o) => `
${esc(o.name)}${catBadge(o.category)}
${esc(o.why)}
`; $("#main").innerHTML = `

🕳️ 待克隆机会

这些 AI / 流量目标有开源空间,但社区还没有 ≥1000 星项目。是机会——已有对应项目?点提交修正。
${OPS.open_targets.map(opCard).join("")}

🚫 明确不设战场

这些目标结构上没有「克隆」形态。
${OPS.no_battlefield.map(opCard).join("")}

❓ 为何 1000 域名里只有部分成了战场

流量 Top1000 全部逐个分析(共 ${OPS.ledger_total} 条判定${OPS.analyzed ? `,其中 ${OPS.analyzed.model} 条经模型/联网逐个核实` : ""})。点任一类展开看具体域名:
${OPS.ledger_summary.map((l) => `
${l.count}
${esc(l.label)} ▸ 查看
${esc(l.why)}
`).join("")}

+ 提交你的输入

发现漏掉的克隆项目、想修正评分、或想发起一个克隆需求?
`; loadRecent("_recent"); } function showKindDetail(kind) { const list = OPS.domains_by_kind?.[kind] || []; const meta = OPS.ledger_summary.find((l) => l.kind === kind); const el = $("#kindDetail"); el.innerHTML = `

${esc(meta?.label || kind)} · ${list.length} 个域名${esc(meta?.why || "")}

${list.map((d) => `
${esc(d.domain)}${esc(d.product || d.reason || "")}
`).join("")}
`; el.scrollIntoView({ behavior: "smooth", block: "nearest" }); } /* ---------- 提交弹窗 ---------- */ let modalKind = "correction", modalBf = "", modalTarget = ""; function openModal(bfId, targetName) { modalBf = bfId || ""; modalTarget = targetName || (bfId ? NAME_BY_ID[bfId] : "") || ""; modalKind = "correction"; document.querySelectorAll("#mseg .s").forEach((s) => s.classList.toggle("on", s.dataset.kind === "correction")); $("#mmsg").textContent = ""; renderModalFields(); $("#modal").classList.add("open"); } function renderModalFields() { const isReq = modalKind === "request"; $("#mtitle").textContent = isReq ? "发起克隆需求" : "补充 / 修正克隆"; $("#mhint").textContent = isReq ? "希望社区开源复刻哪个产品?投一票、说说你的理由。" : (modalTarget ? `战场:${modalTarget}` : "补充一个漏掉的开源克隆,或修正现有评分。"); $("#mfields").innerHTML = isReq ? ` ` : ` `; } async function doSubmit() { const kind = modalKind; const body = { kind, battlefield: modalBf || null, target_name: $("#f_target")?.value || modalTarget || null, repo: ($("#f_repo")?.value || "").trim() || null, note: ($("#f_note")?.value || "").trim() || null, contact: ($("#f_contact")?.value || "").trim() || null }; const msg = $("#mmsg"); msg.className = "msg"; msg.textContent = "提交中…"; try { const r = await fetch(API + "/api/submit", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); const j = await r.json(); if (r.ok && j.ok) { msg.className = "msg ok"; msg.textContent = "✓ 已收到,感谢!审核后会体现在战场里。"; setTimeout(() => $("#modal").classList.remove("open"), 1400); } else { msg.className = "msg err"; msg.textContent = "提交失败:" + (j.error || r.status); } } catch (e) { msg.className = "msg err"; msg.textContent = "网络错误,稍后再试。"; } } async function loadRecent(slot) { try { const r = await fetch(API + "/api/submissions?limit=8"); const j = await r.json(); const el = document.querySelector(`[data-usub="${slot}"]`); if (el && j.submissions?.length) { el.innerHTML = `最近提交 · ${j.submissions.map((s) => `${s.kind === "request" ? "需求" : "修正"}「${esc(s.target_name || s.battlefield || s.repo || "")}」`).join(" · ")}`; } } catch {} } /* ---------- 主渲染 ---------- */ function render() { $("#subnav").style.display = state.view === "map" ? "flex" : "none"; if (state.view !== "map") { $("#fpanel").classList.remove("open"); $("#leftIndex").innerHTML = ""; } if (state.view === "map") renderMap(); else if (state.view === "board") renderBoard(); else renderOps(); } function renderStats() { const s = DATA.stats; $("#statbar").innerHTML = [ [s.battlefields.total, "战场"], [s.entries.clone, "克隆项目"], [s.entries.adjacent, "同生态位"], [s.entries.self, "本体开源"], [(s.dataset.repos / 1000).toFixed(1) + "k", "≥1k星数据集"], [(s.opportunities?.open_targets ?? 0), "待克隆机会"], ].map(([b, t]) => `
${b}${t}
`).join(""); const ds = $("#dscount"); if (ds) ds.textContent = s.dataset.repos.toLocaleString(); } function jump(id) { if (!id) return; state.view = "map"; state.q = ""; $("#q").value = ""; document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map")); state.status = null; state.family = null; state.ncat = null; if ($("#onlyCurated").checked) { $("#onlyCurated").checked = false; state.onlyCurated = false; } const t = DATA.battlefields.find((b) => b.id === id); if (t) { state.cat = "all"; document.querySelectorAll(".tab").forEach((x) => x.classList.toggle("on", x.dataset.cat === "all")); } renderFilter(); render(); const el = document.querySelector(`.bf[data-id="${id}"]`); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("flash"); setTimeout(() => el.classList.remove("flash"), 1500); } } Promise.all([ fetch("data/battlefields.json").then((r) => r.json()), fetch("data/leaderboards.json").then((r) => r.json()).catch(() => null), fetch("data/opportunities.json").then((r) => r.json()).catch(() => null), ]).then(([d, lb, ops]) => { DATA = d; LB = lb; OPS = ops; (DATA.taxonomy || []).forEach((t) => { NCAT_ZH[t.id] = t.zh; }); DATA.battlefields.forEach((b) => { NAME_BY_ID[b.id] = b.name; }); const idx = new Map(DATA.battlefields.map((b, i) => [b.id, i])); DATA.battlefields = DATA.battlefields.slice().sort((a, b) => { if (a.category !== b.category) return 0; if (a.category === "traffic") return (a.cf_rank ?? 9999) - (b.cf_rank ?? 9999); const ak = a.parent ? (idx.get(a.parent) ?? idx.get(a.id)) * 100 + 1 : idx.get(a.id) * 100; const bk = b.parent ? (idx.get(b.parent) ?? idx.get(b.id)) * 100 + 1 : idx.get(b.id) * 100; return ak - bk; }); // 可浏览全集 = 真正战场(非别名/非no_bf) + 台账去重服务 const activeBf = DATA.battlefields.filter((b) => !b.no_battlefield && !b.alias_of); ALL_ITEMS = [...activeBf, ...(DATA.ledger_items || [])]; renderStats(); renderFilter(); render(); }); /* ---------- 事件 ---------- */ document.querySelectorAll(".view").forEach((v) => v.addEventListener("click", () => { document.querySelectorAll(".view").forEach((x) => x.classList.remove("on")); v.classList.add("on"); state.view = v.dataset.view; render(); })); document.querySelectorAll(".tab[data-cat]").forEach((t) => t.addEventListener("click", (e) => { if (e.target.tagName === "INPUT") return; document.querySelectorAll(".tab[data-cat]").forEach((x) => x.classList.remove("on")); t.classList.add("on"); state.cat = t.dataset.cat; render(); })); $("#filterToggle").addEventListener("click", () => { $("#fpanel").classList.toggle("open"); }); $("#onlyCurated").addEventListener("change", (e) => { state.onlyCurated = e.target.checked; render(); }); $("#q").addEventListener("input", (e) => { state.q = e.target.value.trim().toLowerCase(); if (state.q && state.view !== "map") { state.view = "map"; document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map")); } render(); }); document.addEventListener("click", (e) => { const kd = e.target.closest("[data-kind]"); if (kd && state.view === "ops") { showKindDetail(kd.dataset.kind); return; } const nc = e.target.closest("[data-fset-ncat]"); if (nc) { e.preventDefault(); e.stopPropagation(); state.ncat = nc.dataset.fsetNcat; if (!$("#fpanel").classList.contains("open")) $("#fpanel").classList.add("open"); renderFilter(); render(); return; } const kw = e.target.closest("[data-kw]"); if (kw) { e.preventDefault(); const w = kw.dataset.kw; $("#q").value = w; state.q = w.toLowerCase(); if (state.view !== "map") { state.view = "map"; document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map")); } render(); window.scrollTo({ top: 0, behavior: "smooth" }); return; } const fc = e.target.closest("[data-fkey]"); if (fc) { const k = fc.dataset.fkey, v = fc.dataset.fval; state[k] = state[k] === v ? null : v; renderFilter(); render(); return; } if (e.target.id === "fpClose") { $("#fpanel").classList.remove("open"); return; } if (e.target.id === "fClear") { state.status = state.family = state.ncat = null; state.q = ""; $("#q").value = ""; renderFilter(); render(); return; } const gt = e.target.closest("[data-gtoggle]"); if (gt) { const s = gt.closest(".gcollapse"); s.classList.toggle("open"); gt.querySelector(".gcaret").textContent = s.classList.contains("open") ? "▾" : "▸"; return; } const mo = e.target.closest("[data-more]"); if (mo) { EXPANDED.add(mo.dataset.more); renderMap(); return; } const ix = e.target.closest("[data-idx]"); if (ix) { const el = document.querySelector(`.bf[data-id="${CSS.escape(ix.dataset.idx)}"]`); if (el) { const s = el.closest(".gcollapse"); if (s && !s.classList.contains("open")) { s.classList.add("open"); s.querySelector(".gcaret").textContent = "▾"; } el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("flash"); setTimeout(() => el.classList.remove("flash"), 1500); } return; } const sub = e.target.closest("[data-submit]"); if (sub) { e.preventDefault(); openModal(sub.dataset.submit, sub.dataset.target); return; } const j = e.target.closest("[data-jump]"); if (j) { e.preventDefault(); jump(j.dataset.jump); return; } }); document.querySelectorAll("#mseg .s").forEach((s) => s.addEventListener("click", () => { document.querySelectorAll("#mseg .s").forEach((x) => x.classList.remove("on")); s.classList.add("on"); modalKind = s.dataset.kind; renderModalFields(); })); $("#mcancel").addEventListener("click", () => $("#modal").classList.remove("open")); $("#msubmit").addEventListener("click", doSubmit); $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") $("#modal").classList.remove("open"); });