hub_pro

Native-style filters for Linux.do Hub Marketplace Channel Hub, with badges, free-only, model keyword, popularity sort, and full-page listing.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         hub_pro
// @name:zh-CN   hub_pro
// @namespace    https://hub.linux.do/
// @version      3.0.0
// @description  Native-style filters for Linux.do Hub Marketplace Channel Hub, with badges, free-only, model keyword, popularity sort, and full-page listing.
// @description:zh-CN  为 Linux.do Hub Marketplace 的 Channel Hub 增加原生风格筛选:Badges、只看 Free、模型关键字、热门度排序和一页显示全部。
// @author       lhish
// @license      MIT
// @homepageURL  https://github.com/lhish/hub_pro
// @supportURL   https://github.com/lhish/hub_pro/issues
// @match        https://hub.linux.do/marketplace*
// @match        https://hub.linux.do/marketplace/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

const __TEST__ = {
  isChannelsGraphqlBody(body) {
    try {
      const payload = typeof body === "string" ? JSON.parse(body) : body;
      const query = String(payload?.query || "");
      return /\bchannels\s*\(/.test(query) && !/\bmarketplaceModels\b|\bmarketplaceModel\b/.test(query);
    } catch {
      return false;
    }
  },
  isMarketplaceChannelsUrl(url) {
    const value = String(url || "");
    const path = value.replace(/^https?:\/\/[^/]+/i, "").split(/[?#]/)[0];
    return path === "/admin/marketplace/channels";
  },
  buildAuthHeaders(token) {
    const headers = { "Content-Type": "application/json" };
    if (token) headers.Authorization = `Bearer ${token}`;
    return headers;
  },
  normalizeState(saved, defaults) {
    return {
      tag: typeof saved?.tag === "string" ? saved.tag : defaults.tag,
      badges: Array.isArray(saved?.badges) ? saved.badges : defaults.badges,
      free: saved?.free !== false,
      sort: typeof saved?.sort === "string" ? saved.sort : defaults.sort,
      modelKeyword: typeof saved?.modelKeyword === "string" ? saved.modelKeyword : defaults.modelKeyword,
    };
  },
  panelModelKeywordValue(value) {
    return value ?? "";
  },
  cleanMarketplaceSearch(value) {
    return String(value || "").replace(/[\u200b-\u200d\ufeff]/g, "").trim();
  },
  marketplaceCacheKey(url) {
    const parsedUrl = new URL(String(url || ""), "https://hub.linux.do");
    return JSON.stringify({
      search: this.cleanMarketplaceSearch(parsedUrl.searchParams.get("search")),
    });
  },
  channelBadges(channel) {
    const badges = [];
    if (channel?.usesOfficialBaseURL) badges.push("Official");
    if (channel?.settings?.codingAgentMode === "broad") badges.push("Broad");
    if (channel?.settings?.codingAgentMode === "strict") badges.push("Strict");
    if (channel?.type) badges.push(channel.type);
    return [...new Set(badges)];
  },
  collectBadgeOptions(channels) {
    const priority = ["Official", "Broad", "Strict"];
    const values = new Set();
    for (const channel of channels || []) {
      for (const badge of this.channelBadges(channel)) values.add(badge);
    }
    return Array.from(values).sort((a, b) => {
      const ai = priority.indexOf(a);
      const bi = priority.indexOf(b);
      if (ai !== -1 || bi !== -1) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
      return a.localeCompare(b);
    });
  },
  badgesMatch(actualBadges, selectedBadges) {
    return !selectedBadges.length || selectedBadges.some((badge) => actualBadges.includes(badge));
  },
  tagMatches(channel, tag) {
    switch (tag || "all") {
      case "official":
        return Boolean(channel?.usesOfficialBaseURL);
      case "third_party":
        return !channel?.usesOfficialBaseURL;
      case "client_restricted":
        return channel?.settings?.codingAgentMode === "broad" || channel?.settings?.codingAgentMode === "strict";
      case "strict_client_restricted":
        return channel?.settings?.codingAgentMode === "strict";
      default:
        return true;
    }
  },
  isFreeChannel(channel) {
    if (typeof channel?.priceSummary?.allFree === "boolean") return channel.priceSummary.allFree;
    const prices = channel?.channelModelPrices || [];
    if (prices.length === 0) return true;
    return prices.every((modelPrice) =>
      (modelPrice?.price?.items || []).every((item) => {
        const value = Number.parseFloat(item?.pricing?.usagePerUnit);
        return !Number.isFinite(value) || value <= 0;
      }),
    );
  },
  channelMatches(channel, state) {
    return this.tagMatches(channel, state.tag)
      && this.badgesMatch(this.channelBadges(channel), state.badges || [])
      && (!state.free || this.isFreeChannel(channel))
      && this.modelKeywordMatch(channel, state.modelKeyword || "");
  },
  scriptOnlyChannelMatches(channel, state) {
    return this.channelMatches(channel, state);
  },
  modelKeywordMatch(channel, keyword) {
    const text = String(keyword || "").trim().toLowerCase();
    if (!text) return true;
    return (channel?.supportedModels || []).some((model) => String(model).toLowerCase().includes(text));
  },
  popularityScore(probe) {
    return (probe?.points || []).reduce((sum, point) => {
      const value = Number(point?.successRequestCount);
      return sum + (Number.isFinite(value) ? value : 0);
    }, 0);
  },
  channelMultiplierValue(channel) {
    const multiplier = channel?.priceSummary?.multiplier;
    if (multiplier == null || multiplier === "") return 0;
    if (typeof multiplier === "number") return Number.isFinite(multiplier) ? multiplier : 0;
    if (typeof multiplier === "string") {
      const value = Number.parseFloat(multiplier);
      return Number.isFinite(value) ? value : 0;
    }
    const min = Number(multiplier.min);
    const max = Number(multiplier.max);
    if (Number.isFinite(min) && Number.isFinite(max)) return (min + max) / 2;
    if (Number.isFinite(max)) return max;
    if (Number.isFinite(min)) return min;
    return 0;
  },
  sortEdgesByPopularity(edges, scoreMap) {
    return [...edges].sort((a, b) => {
      const diff = (scoreMap.get(b?.node?.id) || 0) - (scoreMap.get(a?.node?.id) || 0);
      if (diff !== 0) return diff;
      return String(a?.node?.name || "").localeCompare(String(b?.node?.name || ""));
    });
  },
  sortEdgesByScriptSort(edges, state, scoreMap) {
    const sorted = [...edges];
    return sorted.sort((a, b) => {
      switch (state.sort || "created_desc") {
        case "popular_desc": {
          const diff = (scoreMap.get(b?.node?.id) || 0) - (scoreMap.get(a?.node?.id) || 0);
          if (diff !== 0) return diff;
          return String(a?.node?.name || "").localeCompare(String(b?.node?.name || ""));
        }
        case "consumed_desc":
          return (b.node?.budgetStats?.consumedAmount || 0) - (a.node?.budgetStats?.consumedAmount || 0);
        case "consumed_asc":
          return (a.node?.budgetStats?.consumedAmount || 0) - (b.node?.budgetStats?.consumedAmount || 0);
        case "models_desc":
          return (b.node?.supportedModels?.length || 0) - (a.node?.supportedModels?.length || 0);
        case "multiplier_desc":
          return this.channelMultiplierValue(b.node) - this.channelMultiplierValue(a.node);
        case "multiplier_asc":
          return this.channelMultiplierValue(a.node) - this.channelMultiplierValue(b.node);
        case "name_asc":
          return String(a.node?.name || "").localeCompare(String(b.node?.name || ""));
        case "created_desc":
        default:
          return new Date(b.node?.createdAt || 0).getTime() - new Date(a.node?.createdAt || 0).getTime();
      }
    });
  },
  isRenderedChannelEntry(entry) {
    return entry?.providerLabel != null
      && Array.isArray(entry?.node?.supportedModels)
      && "usesOfficialBaseURL" in entry.node;
  },
  shouldKeepFullRenderedSlice(source, result) {
    return Array.isArray(source)
      && Array.isArray(result)
      && source.length > result.length
      && (this.isRenderedChannelEntry(source[0]) || this.isRenderedChannelEntry(source[source.length - 1]));
  },
  nextDefaultChannelSelectionState({ alreadySelected, channelTabExists, channelActive }) {
    if (!channelTabExists) return { shouldClick: false, selected: Boolean(alreadySelected && channelActive) };
    if (channelActive) return { shouldClick: false, selected: true };
    return { shouldClick: true, selected: false };
  },
  isLikelyPaginationText(text) {
    const normalized = String(text || "").replace(/\s+/g, " ").trim();
    if (!normalized) return false;
    if (/Search|Channel Tags|Sort|筛选|搜索|排序|标签/i.test(normalized)) return false;
    return /Previous|Next|Prev\b|Page\s*\d|上一页|下一页|分页|加载更多/i.test(normalized);
  },
  marketplacePayloadItems(payload) {
    return Array.isArray(payload?.items) ? payload.items : [];
  },
  marketplaceRemainingPages(totalPages, maxScanPages) {
    const limit = Math.min(
      Math.max(Number(totalPages) || 1, 1),
      Math.max(Number(maxScanPages) || 1, 1),
    );
    return Array.from({ length: Math.max(0, limit - 1) }, (_, index) => index + 2);
  },
  marketplaceProgressLabel(loadedPages, totalPages, loadedItems) {
    const current = Math.max(Number(loadedPages) || 0, 0);
    const total = Math.max(Number(totalPages) || current || 1, 1);
    const items = Math.max(Number(loadedItems) || 0, 0);
    return `加载 Channel ${Math.min(current, total)}/${total} 页,已获取 ${items} 个`;
  },
  stateSummary(state) {
    return {
      tag: state?.tag || "all",
      badges: Array.isArray(state?.badges) ? state.badges : [],
      free: state?.free !== false,
      sort: state?.sort || "created_desc",
      modelKeyword: state?.modelKeyword || "",
    };
  },
  marketplaceFilterStats(payload, state) {
    const items = this.marketplacePayloadItems(payload);
    const summary = this.stateSummary(state);
    const tagMatched = items.filter((channel) => this.tagMatches(channel, summary.tag));
    const badgesMatched = tagMatched.filter((channel) => this.badgesMatch(this.channelBadges(channel), summary.badges));
    const freeMatched = badgesMatched.filter((channel) => !summary.free || this.isFreeChannel(channel));
    const modelMatched = freeMatched.filter((channel) => this.modelKeywordMatch(channel, summary.modelKeyword));
    return {
      total: items.length,
      afterTag: tagMatched.length,
      afterBadges: badgesMatched.length,
      afterFree: freeMatched.length,
      afterModel: modelMatched.length,
      state: summary,
      first: modelMatched.slice(0, 5).map((item) => item.name || item.id || ""),
    };
  },
  marketplaceNextRenderLimit(current, total, step) {
    const safeCurrent = Math.max(Number(current) || 0, 0);
    const safeTotal = Math.max(Number(total) || 0, 0);
    const safeStep = Math.max(Number(step) || 1, 1);
    return Math.min(safeTotal, safeCurrent + safeStep);
  },
  filterChannelsPayload(payload, state, scoreMap = new Map()) {
    const channels = payload?.data?.channels;
    if (!channels?.edges) return payload;
    const matchedEdges = channels.edges.filter((edge) => this.channelMatches(edge.node, state));
    const nextEdges = this.sortEdgesByScriptSort(matchedEdges, state, scoreMap);
    return {
      ...payload,
      data: {
        ...payload.data,
        channels: {
          ...channels,
          edges: nextEdges,
          totalCount: nextEdges.length,
        },
      },
    };
  },
  filterMarketplaceChannelsPayload(payload, state, scoreMap = new Map()) {
    const items = this.marketplacePayloadItems(payload);
    if (!items.length && !Array.isArray(payload?.items)) return payload;
    const matchedItems = items.filter((channel) => this.channelMatches(channel, state));
    const sortedItems = this.sortEdgesByScriptSort(
      matchedItems.map((item) => ({ node: item })),
      state,
      scoreMap,
    ).map((edge) => edge.node);
    return {
      ...payload,
      items: sortedItems,
      totalCount: sortedItems.length,
      totalPages: 1,
      page: 1,
      first: sortedItems.length,
    };
  },
  limitMarketplaceChannelsPayload(payload, limit) {
    const items = this.marketplacePayloadItems(payload);
    const visibleItems = items.slice(0, Math.max(Number(limit) || 0, 0));
    return {
      ...payload,
      items: visibleItems,
      totalCount: items.length,
      totalPages: 1,
      page: 1,
      first: visibleItems.length,
    };
  },
};

(function () {
  "use strict";

  const STATE_KEY = "ld_marketplace_native_filter_state_v2";
  const BADGES_KEY = "ld_marketplace_native_filter_badges";
  const CHANNELS_KEY = "ld_marketplace_native_filter_channels";
  const PANEL_ID = "ld-native-marketplace-filter";
  const MODEL_TOOLTIP_ID = "ld-model-tooltip";
  const PROGRESS_ID = "ld-marketplace-loading-progress";
  const APPLY_PROGRESS_ID = "ld-marketplace-apply-progress";
  const LOAD_MORE_ID = "ld-marketplace-load-more";
  const STYLE_ID = "ld-native-marketplace-filter-style";
  const DEBUG_PREFIX = "[hub_pro debug]";
  const GRAPHQL_PATH = "/admin/graphql";
  const POPULAR_SORT = "popular_desc";
  const NATIVE_SORT = "native";
  const PAGE_SIZE = 100;
  const MARKETPLACE_PAGE_SIZE = 100;
  const MARKETPLACE_FETCH_CONCURRENCY = 4;
  const MARKETPLACE_RENDER_BATCH_SIZE = 100;
  const MAX_SCAN_PAGES = 80;
  const PROBE_CHUNK_SIZE = 200;
  const PRICE_FIELDS = `
            channelModelPrices {
              modelID
              price {
                items {
                  itemCode
                  pricing {
                    usagePerUnit
                  }
                }
              }
            }
  `;
  const PROBE_QUERY = `
    query GetChannelProbeData($input: GetChannelProbeDataInput!) {
      channelProbeData(input: $input) {
        channelID
        points {
          successRequestCount
        }
      }
    }
  `;

  const defaultState = { tag: "all", badges: [], free: true, sort: "created_desc", modelKeyword: "" };
  const popularityScores = new Map();
  const marketplaceChannelsCache = new Map();
  const marketplaceChannelsPending = new Map();
  const knownChannels = new Map();
  const knownChannelsByName = new Map();
  const nativeFetch = window.fetch.bind(window);
  const nativeArrayFilter = Array.prototype.filter;
  const nativeArraySort = Array.prototype.sort;
  const nativeArraySlice = Array.prototype.slice;
  let uiFrame = 0;
  let tooltipBound = false;
  let nativeInputListenerAttached = false;
  let triggeringNativeRerender = false;
  let defaultChannelSelected = false;
  let defaultChannelClickTimer = 0;
  let progressHideTimer = 0;
  let applyProgressHideTimer = 0;
  let applyProgressFallbackTimer = 0;
  let lastScrollDebugAt = 0;
  let rerenderMarkerCounter = 0;
  let marketplaceRenderLimit = MARKETPLACE_RENDER_BATCH_SIZE;
  let lastMarketplaceResultCount = 0;
  let scrollLoadScheduled = false;
  const hiddenNativeControls = new Set();
  const hiddenNativePagination = new Set();
  patchDomMutationSafety();
  patchArrayFiltering();
  initDebug();
  window.fetch = patchedFetch;

  function initDebug() {
    window.__hubProDebug = window.__hubProDebug || {};
    window.__hubProDebug.enabled = window.__hubProDebug.enabled !== false;
    window.__hubProDebug.events = Array.isArray(window.__hubProDebug.events) ? window.__hubProDebug.events : [];
    window.__hubProDebug.state = () => ({
      filterState: loadState(),
      cacheKeys: Array.from(marketplaceChannelsCache.keys()),
      pendingKeys: Array.from(marketplaceChannelsPending.keys()),
      renderLimit: marketplaceRenderLimit,
      lastMarketplaceResultCount,
      knownChannels: knownChannels.size,
      badgeOptions: loadBadgeOptions(),
    });
    window.__hubProDebug.clear = () => {
      window.__hubProDebug.events.length = 0;
      window.__hubProDebug.last = null;
      console.info(DEBUG_PREFIX, "cleared");
    };
    debugLog("init", { state: loadState() });
  }

  function debugLog(event, details = {}) {
    const debug = window.__hubProDebug;
    if (!debug?.enabled) return;
    const entry = {
      time: new Date().toISOString(),
      event,
      ...details,
    };
    debug.events.push(entry);
    if (debug.events.length > 120) debug.events.splice(0, debug.events.length - 120);
    debug.last = entry;
    if (event === "scroll-check" || event === "load-more-button-update") return;
    console.info(DEBUG_PREFIX, event, safeDebugText(details), details);
  }

  function safeDebugText(value) {
    try {
      return JSON.stringify(value);
    } catch {
      return String(value);
    }
  }

  function loadState() {
    try {
      const saved = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
      return __TEST__.normalizeState(saved, defaultState);
    } catch {
      return { ...defaultState };
    }
  }

  function saveState(state) {
    localStorage.setItem(STATE_KEY, JSON.stringify(state));
  }

  function loadBadgeOptions() {
    try {
      const values = JSON.parse(sessionStorage.getItem(BADGES_KEY) || "[]");
      return Array.isArray(values) ? values : [];
    } catch {
      return [];
    }
  }

  function loadKnownChannels() {
    try {
      const values = JSON.parse(sessionStorage.getItem(CHANNELS_KEY) || "[]");
      if (!Array.isArray(values)) return;
      for (const channel of values) {
        rememberChannel(channel);
      }
    } catch {
      // ignore invalid cache
    }
  }

  function saveKnownChannels() {
    sessionStorage.setItem(CHANNELS_KEY, JSON.stringify(Array.from(knownChannels.values()).map((channel) => ({
      id: channel.id,
      name: channel.name,
      supportedModels: channel.supportedModels || [],
    }))));
  }

  function saveBadgeOptions(options) {
    sessionStorage.setItem(BADGES_KEY, JSON.stringify(options));
  }

  function stateSignature() {
    return JSON.stringify({ state: loadState(), badges: loadBadgeOptions(), layout: controlsSignature() });
  }

  async function patchedFetch(input, init) {
    const requestInfo = getRequestInfo(input, init);
    if (!requestInfo.shouldHandle) return nativeFetch(input, init);

    try {
      if (requestInfo.kind === "marketplaceChannels") {
        return await buildFullMarketplaceChannelsResponse(requestInfo, init);
      }
      return await buildFullChannelsResponse(requestInfo, init);
    } catch (error) {
      console.warn("[Linux.do Marketplace Filter] full channel scan failed, falling back to current request", error);
      if (requestInfo.kind === "marketplaceChannels") {
        const response = await nativeFetch(requestInfo.url, buildFetchInit(requestInfo, init));
        return wrapMarketplaceChannelsResponse(response, requestInfo);
      }
      const response = await nativeFetch(requestInfo.url, buildGraphqlInit(requestInfo, init, {
        ...requestInfo.body,
        query: ensurePriceFields(requestInfo.body.query),
      }));
      return wrapGraphqlResponse(response, requestInfo);
    }
  }

  function getRequestInfo(input, init) {
    const url = typeof input === "string" ? input : input?.url || "";
    const bodyText = typeof init?.body === "string" ? init.body : "";
    const headers = new Headers(init?.headers || input?.headers || {});
    const isGraphql = url.includes(GRAPHQL_PATH) && __TEST__.isChannelsGraphqlBody(bodyText);
    const isMarketplaceChannels = __TEST__.isMarketplaceChannelsUrl(url);
    if (isGraphql || bodyText) headers.set("Content-Type", "application/json");
    headers.delete("content-length");
    return {
      shouldHandle: isGraphql || isMarketplaceChannels,
      kind: isMarketplaceChannels ? "marketplaceChannels" : "graphqlChannels",
      body: bodyText ? JSON.parse(bodyText) : {},
      headers,
      method: init?.method || input?.method || "POST",
      credentials: init?.credentials || input?.credentials || "same-origin",
      url,
    };
  }

  function ensurePriceFields(query) {
    if (!query || /channelModelPrices\s*\{/.test(query)) return query;
    const withUserInsert = query.replace(/(user\s*\{[\s\S]*?linuxdoUsername\s*\})/, `$1\n${PRICE_FIELDS}`);
    if (withUserInsert !== query) return withUserInsert;
    return query.replace(/(settings\s*\{[\s\S]*?codingAgentMode\s*\})/, `$1\n${PRICE_FIELDS}`);
  }

  function buildGraphqlInit(requestInfo, init, body) {
    const headers = new Headers(requestInfo.headers);
    headers.set("Content-Type", "application/json");
    return {
      ...init,
      method: "POST",
      credentials: requestInfo.credentials,
      headers,
      body: JSON.stringify(body),
    };
  }

  function buildFetchInit(requestInfo, init) {
    return {
      ...init,
      method: requestInfo.method,
      credentials: requestInfo.credentials,
      headers: requestInfo.headers,
    };
  }

  async function buildFullMarketplaceChannelsResponse(requestInfo, init) {
    const baseUrl = marketplaceChannelsBaseUrl(requestInfo.url);
    const cacheKey = __TEST__.marketplaceCacheKey(baseUrl.toString());
    const cached = marketplaceChannelsCache.get(cacheKey);
    if (cached) {
      debugLog("marketplace-cache-hit", {
        cacheKey,
        items: __TEST__.marketplacePayloadItems(cached.payload).length,
        state: loadState(),
      });
      return buildMarketplaceResponseFromCache(cached, requestInfo);
    }

    const pending = marketplaceChannelsPending.get(cacheKey);
    if (pending) {
      debugLog("marketplace-cache-pending", { cacheKey, state: loadState() });
      return buildMarketplaceResponseFromCache(await pending, requestInfo);
    }

    debugLog("marketplace-cache-miss", { cacheKey, url: baseUrl.toString(), state: loadState() });
    const pendingLoad = loadMarketplaceChannelsCacheEntry(baseUrl, requestInfo, init);
    marketplaceChannelsPending.set(cacheKey, pendingLoad);
    try {
      const cacheEntry = await pendingLoad;
      marketplaceChannelsCache.set(cacheKey, cacheEntry);
      debugLog("marketplace-cache-store", {
        cacheKey,
        items: __TEST__.marketplacePayloadItems(cacheEntry.payload).length,
      });
      return buildMarketplaceResponseFromCache(cacheEntry, requestInfo);
    } finally {
      marketplaceChannelsPending.delete(cacheKey);
    }
  }

  async function loadMarketplaceChannelsCacheEntry(baseUrl, requestInfo, init) {
    showMarketplaceProgress(0, 1, 0);
    try {
      const firstResult = await fetchMarketplaceChannelsPage(baseUrl, 1, requestInfo, init);
      const allItems = [...firstResult.items];
      debugLog("marketplace-fetch-first-page", {
        url: baseUrl.toString(),
        items: firstResult.items.length,
        totalPages: firstResult.payload?.totalPages,
        totalCount: firstResult.payload?.totalCount,
      });

      const remainingPages = firstResult.items.length
        ? __TEST__.marketplaceRemainingPages(firstResult.payload?.totalPages, MAX_SCAN_PAGES)
        : [];
      const totalPages = 1 + remainingPages.length;
      let loadedPages = 1;
      let loadedItems = allItems.length;
      showMarketplaceProgress(loadedPages, totalPages, loadedItems);

      for (let index = 0; index < remainingPages.length; index += MARKETPLACE_FETCH_CONCURRENCY) {
        const batch = remainingPages.slice(index, index + MARKETPLACE_FETCH_CONCURRENCY);
        const results = await Promise.all(batch.map((page) =>
          fetchMarketplaceChannelsPage(baseUrl, page, requestInfo, init).then((result) => {
            loadedPages += 1;
            loadedItems += result.items.length;
            showMarketplaceProgress(loadedPages, totalPages, loadedItems);
            return result;
          }),
        ));
        for (const result of results) {
          allItems.push(...result.items);
        }
      }

      const combinedPayload = combineMarketplaceChannelsPayload(firstResult.payload, allItems);
      debugLog("marketplace-fetch-complete", {
        items: allItems.length,
        pages: totalPages,
      });
      showMarketplaceProgress(totalPages, totalPages, allItems.length);
      scheduleHideMarketplaceProgress();
      return marketplaceCacheEntry(firstResult.response, combinedPayload);
    } catch (error) {
      hideMarketplaceProgress();
      throw error;
    }
  }

  async function buildMarketplaceResponseFromCache(cacheEntry, requestInfo) {
    const items = __TEST__.marketplacePayloadItems(cacheEntry.payload);
    await ensurePopularityScoresForState(items, requestInfo);
    const state = loadState();
    const filteredPayload = __TEST__.filterMarketplaceChannelsPayload(cacheEntry.payload, state, popularityScores);
    const visiblePayload = applyMarketplaceRenderLimit(filteredPayload);
    const stats = __TEST__.marketplaceFilterStats(cacheEntry.payload, state);
    debugLog("marketplace-filter-result", {
      before: items.length,
      after: __TEST__.marketplacePayloadItems(filteredPayload).length,
      visible: __TEST__.marketplacePayloadItems(visiblePayload).length,
      renderLimit: marketplaceRenderLimit,
      state,
      stateText: safeDebugText(__TEST__.stateSummary(state)),
      stats,
      statsText: safeDebugText(stats),
      sort: state.sort,
    });
    rememberBadges(filteredPayload);
    rememberChannels(filteredPayload);
    scheduleHideApplyProgress();
    scheduleRenderedDebugSnapshot(visiblePayload, state);
    return responseFromCachedPayload(cacheEntry, visiblePayload);
  }

  function applyMarketplaceRenderLimit(payload) {
    const items = __TEST__.marketplacePayloadItems(payload);
    lastMarketplaceResultCount = items.length;
    scrollLoadScheduled = false;
    const visiblePayload = items.length <= marketplaceRenderLimit
      ? payload
      : __TEST__.limitMarketplaceChannelsPayload(payload, marketplaceRenderLimit);
    scheduleUpdateLoadMoreButton();
    return visiblePayload;
  }

  function marketplaceChannelsBaseUrl(url) {
    const baseUrl = new URL(url, window.location.origin);
    baseUrl.searchParams.delete("tag");
    baseUrl.searchParams.delete("sort");
    const search = __TEST__.cleanMarketplaceSearch(baseUrl.searchParams.get("search"));
    if (search) {
      baseUrl.searchParams.set("search", search);
    } else {
      baseUrl.searchParams.delete("search");
    }
    return baseUrl;
  }

  async function fetchMarketplaceChannelsPage(baseUrl, page, requestInfo, init) {
    const pageUrl = new URL(baseUrl.toString());
    pageUrl.searchParams.set("page", String(page));
    pageUrl.searchParams.set("first", String(Math.max(Number(baseUrl.searchParams.get("first")) || MARKETPLACE_PAGE_SIZE, MARKETPLACE_PAGE_SIZE)));
    const response = await nativeFetch(pageUrl.toString(), buildFetchInit(requestInfo, init));
    const payload = await response.clone().json();
    rememberBadges(payload);
    rememberChannels(payload);
    return { response, payload, items: __TEST__.marketplacePayloadItems(payload) };
  }

  function combineMarketplaceChannelsPayload(payload, items) {
    if (!payload || !Array.isArray(payload.items)) return payload;
    return {
      ...payload,
      items,
      totalCount: items.length,
      totalPages: 1,
      page: 1,
      first: items.length,
    };
  }

  function marketplaceCacheEntry(response, payload) {
    return {
      payload,
      status: response.status,
      statusText: response.statusText,
      headers: Array.from(new Headers(response.headers).entries()),
    };
  }

  function responseFromCachedPayload(cacheEntry, payload) {
    const headers = new Headers(cacheEntry.headers);
    headers.set("Content-Type", "application/json");
    return new Response(JSON.stringify(payload), {
      status: cacheEntry.status,
      statusText: cacheEntry.statusText,
      headers,
    });
  }

  async function ensurePopularityScoresForState(items, requestInfo) {
    if (loadState().sort !== POPULAR_SORT) return;
    const missingIds = items
      .map((channel) => channel.id)
      .filter((id) => id && !popularityScores.has(id));
    debugLog("popularity-score-check", {
      total: items.length,
      known: items.length - missingIds.length,
      missing: missingIds.length,
    });
    if (missingIds.length) await fetchPopularityScores(missingIds, requestInfo);
  }

  async function buildFullChannelsResponse(requestInfo, init) {
    const query = ensurePriceFields(requestInfo.body.query);
    const baseVariables = requestInfo.body.variables || {};
    const allEdges = [];
    let after = baseVariables.after || null;
    let lastPayload = null;
    let lastResponse = null;

    for (let page = 0; page < MAX_SCAN_PAGES; page += 1) {
      const body = {
        ...requestInfo.body,
        query,
        variables: {
          ...baseVariables,
          first: Math.max(Number(baseVariables.first) || PAGE_SIZE, PAGE_SIZE),
          after,
        },
      };
      lastResponse = await nativeFetch(requestInfo.url, buildGraphqlInit(requestInfo, init, body));
      lastPayload = await lastResponse.clone().json();
      rememberBadges(lastPayload);
      rememberChannels(lastPayload);

      const channels = lastPayload?.data?.channels;
      if (!channels?.edges) return lastResponse;
      allEdges.push(...channels.edges);
      if (!channels.pageInfo?.hasNextPage || !channels.pageInfo.endCursor) break;
      after = channels.pageInfo.endCursor;
    }

    const combinedPayload = combineChannelsPayload(lastPayload, allEdges);
    if (loadState().sort === POPULAR_SORT) {
      await fetchPopularityScores(allEdges.map((edge) => edge.node.id), requestInfo);
    }
    rememberChannels(combinedPayload);
    return responseFromPayload(lastResponse, combinedPayload);
  }

  function combineChannelsPayload(payload, edges) {
    const channels = payload?.data?.channels;
    if (!channels) return payload;
    return {
      ...payload,
      data: {
        ...payload.data,
        channels: {
          ...channels,
          edges,
          totalCount: edges.length,
          pageInfo: {
            hasNextPage: false,
            hasPreviousPage: false,
            startCursor: edges[0]?.cursor || null,
            endCursor: edges[edges.length - 1]?.cursor || null,
          },
        },
      },
    };
  }

  async function fetchPopularityScores(channelIDs, requestInfo) {
    const ids = Array.from(new Set(channelIDs.filter(Boolean)));
    const graphqlUrl = new URL(GRAPHQL_PATH, window.location.origin).toString();
    const url = requestInfo.kind === "marketplaceChannels" ? graphqlUrl : requestInfo.url;
    for (let index = 0; index < ids.length; index += PROBE_CHUNK_SIZE) {
      const chunk = ids.slice(index, index + PROBE_CHUNK_SIZE);
      const response = await nativeFetch(url, buildGraphqlInit(requestInfo, undefined, {
        query: PROBE_QUERY,
        variables: { input: { channelIDs: chunk } },
      }));
      const payload = await response.json();
      for (const item of payload?.data?.channelProbeData || []) {
        popularityScores.set(item.channelID, __TEST__.popularityScore(item));
      }
    }
  }

  function responseFromPayload(response, payload) {
    const headers = new Headers(response.headers);
    headers.set("Content-Type", "application/json");
    return new Response(JSON.stringify(payload), {
      status: response.status,
      statusText: response.statusText,
      headers,
    });
  }

  function wrapMarketplaceChannelsResponse(response, requestInfo) {
    return new Proxy(response, {
      get(target, prop) {
        if (prop === "json") {
          return async () => {
            const payload = await target.clone().json();
            rememberBadges(payload);
            rememberChannels(payload);
            const items = __TEST__.marketplacePayloadItems(payload);
            if (loadState().sort === POPULAR_SORT) {
              await fetchPopularityScores(items.map((channel) => channel.id), requestInfo);
            }
            scheduleHideApplyProgress();
            const state = loadState();
            const filteredPayload = __TEST__.filterMarketplaceChannelsPayload(payload, state, popularityScores);
            const visiblePayload = applyMarketplaceRenderLimit(filteredPayload);
            const stats = __TEST__.marketplaceFilterStats(payload, state);
            debugLog("marketplace-wrap-filter-result", {
              before: items.length,
              after: __TEST__.marketplacePayloadItems(filteredPayload).length,
              visible: __TEST__.marketplacePayloadItems(visiblePayload).length,
              renderLimit: marketplaceRenderLimit,
              stateText: safeDebugText(__TEST__.stateSummary(state)),
              statsText: safeDebugText(stats),
            });
            scheduleRenderedDebugSnapshot(visiblePayload, state);
            return visiblePayload;
          };
        }
        const value = target[prop];
        return typeof value === "function" ? value.bind(target) : value;
      },
    });
  }

  function wrapGraphqlResponse(response, requestInfo) {
    return new Proxy(response, {
      get(target, prop) {
        if (prop === "json") {
          return async () => {
            const payload = await target.clone().json();
            rememberBadges(payload);
            rememberChannels(payload);
            const edges = payload?.data?.channels?.edges || [];
            if (loadState().sort === POPULAR_SORT) {
              await fetchPopularityScores(edges.map((edge) => edge.node.id), requestInfo);
            }
            return payload;
          };
        }
        const value = target[prop];
        return typeof value === "function" ? value.bind(target) : value;
      },
    });
  }

  function patchArrayFiltering() {
    if (!Array.prototype.filter.__ldMarketplacePatched) {
      const patchedFilter = function (callback, thisArg) {
        const result = nativeArrayFilter.call(this, callback, thisArg);
        if (!isRenderedChannelEntryArray(this) && !isRenderedChannelEntryArray(result)) return result;
        const state = loadState();
        return nativeArrayFilter.call(result, (entry) => __TEST__.scriptOnlyChannelMatches(entry.node, state));
      };
      patchedFilter.__ldMarketplacePatched = true;
      Array.prototype.filter = patchedFilter;
    }

    if (!Array.prototype.sort.__ldMarketplacePatched) {
      const patchedSort = function (compareFn) {
        if (isRenderedChannelEntryArray(this)) {
          return nativeArraySort.call(this, (a, b) => {
            const sorted = __TEST__.sortEdgesByScriptSort([{ node: a.node }, { node: b.node }], loadState(), popularityScores);
            return sorted[0]?.node === a.node ? -1 : 1;
          });
        }
        return nativeArraySort.call(this, compareFn);
      };
      patchedSort.__ldMarketplacePatched = true;
      Array.prototype.sort = patchedSort;
    }

    if (!Array.prototype.slice.__ldMarketplacePatched) {
      const patchedSlice = function (start, end) {
        const result = nativeArraySlice.call(this, start, end);
        if (__TEST__.shouldKeepFullRenderedSlice(this, result)) return nativeArraySlice.call(this, 0);
        return result;
      };
      patchedSlice.__ldMarketplacePatched = true;
      Array.prototype.slice = patchedSlice;
    }
  }

  function patchDomMutationSafety() {
    const nativeRemoveChild = Node.prototype.removeChild;
    if (nativeRemoveChild.__ldMarketplacePatched) return;
    const patchedRemoveChild = function (child) {
      if (child?.parentNode !== this) return child;
      return nativeRemoveChild.call(this, child);
    };
    patchedRemoveChild.__ldMarketplacePatched = true;
    Node.prototype.removeChild = patchedRemoveChild;
  }

  function isRenderedChannelEntryArray(value) {
    if (!Array.isArray(value) || value.length === 0 || value.length > 10000) return false;
    const first = value[0];
    const last = value[value.length - 1];
    return isRenderedChannelEntry(first) || isRenderedChannelEntry(last);
  }

  function isRenderedChannelEntry(entry) {
    return __TEST__.isRenderedChannelEntry(entry);
  }

  function rememberBadges(payload) {
    const edges = payload?.data?.channels?.edges || [];
    const items = __TEST__.marketplacePayloadItems(payload);
    const channels = edges.length ? edges.map((edge) => edge.node) : items;
    const current = new Set(loadBadgeOptions());
    for (const option of __TEST__.collectBadgeOptions(channels)) current.add(option);
    saveBadgeOptions(Array.from(current).sort((a, b) => a.localeCompare(b)));
    queueMicrotask(renderPanel);
  }

  function rememberChannels(payload) {
    const edges = payload?.data?.channels?.edges || [];
    const items = __TEST__.marketplacePayloadItems(payload);
    const channels = edges.length ? edges.map((edge) => edge.node) : items;
    for (const channel of channels) {
      rememberChannel({
        id: channel.id,
        name: channel.name || "",
        supportedModels: channel.supportedModels || [],
      });
    }
    saveKnownChannels();
    queueMicrotask(bindModelTooltipEvents);
  }

  function rememberChannel(channel) {
    if (!channel?.id) return;
    knownChannels.set(channel.id, channel);
    if (channel.name) knownChannelsByName.set(channel.name, channel);
  }

  function isChannelHubActive() {
    const selected = document.querySelector('[role="tab"][aria-selected="true"], [role="tab"][data-state="active"]');
    return /channel hub|渠道/i.test(String(selected?.textContent || ""));
  }

  function ensureStyle() {
    if (document.getElementById(STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      #${PANEL_ID} {
        display: contents;
      }
      #${PANEL_ID}.ld-panel-fallback {
        display: grid;
        grid-template-columns: repeat(5, minmax(0, 1fr));
        gap: 12px;
        width: min(1180px, calc(100% - 24px));
        margin: 12px auto;
        padding: 12px;
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background: #fff;
        box-shadow: 0 8px 20px rgba(15, 23, 42, .08);
      }
      @media (max-width: 860px) {
        #${PANEL_ID}.ld-panel-fallback {
          grid-template-columns: 1fr;
        }
      }
      #${PANEL_ID} .ld-label {
        display: block;
        margin-bottom: 4px;
        color: hsl(var(--muted-foreground, 215 16% 47%));
        font-size: 12px;
        font-weight: 600;
        line-height: 1;
        text-transform: uppercase;
        letter-spacing: .025em;
      }
      #${PANEL_ID} .ld-control-wrap {
        position: relative;
      }
      #${PANEL_ID} .ld-field,
      #${PANEL_ID} .ld-extra-field {
        position: relative;
        min-width: 0;
      }
      #${PANEL_ID} input[type="search"],
      #${PANEL_ID} button,
      #${PANEL_ID} summary {
        height: 36px;
        border: 1px solid hsl(var(--input, 214 32% 91%));
        border-radius: 6px;
        background: #fff;
        color: #0f172a;
        font-size: 14px;
        box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
      }
      #${PANEL_ID} summary {
        display: inline-flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        width: 100%;
        padding: 0 12px;
      }
      #${PANEL_ID} summary::after {
        content: "⌄";
        color: #64748b;
        font-size: 14px;
        line-height: 1;
        transform: translateY(-1px);
      }
      #${PANEL_ID} input[type="search"] {
        width: 100%;
        padding: 0 10px;
        outline: none;
      }
      #${PANEL_ID} .ld-control {
        display: inline-flex;
        align-items: center;
        width: 100%;
        height: 36px;
        border: 1px solid hsl(var(--input, 214 32% 91%));
        border-radius: 6px;
        background: #fff;
        color: #0f172a;
        padding: 0 10px;
        font-size: 14px;
      }
      #${PANEL_ID} select {
        width: 100%;
        padding: 0 32px 0 10px;
      }
      #${PANEL_ID} summary {
        cursor: pointer;
        list-style: none;
      }
      #${PANEL_ID} summary::-webkit-details-marker {
        display: none;
      }
      #${PANEL_ID} .ld-summary-text {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
      #${PANEL_ID} .ld-menu {
        position: absolute;
        z-index: 999998;
        top: calc(100% + 4px);
        left: 0;
        width: min(320px, 90vw);
        max-height: 280px;
        overflow: auto;
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background-color: #fff !important;
        color: #0f172a !important;
        box-shadow: 0 12px 30px rgba(15, 23, 42, .16);
        padding: 6px;
      }
      #${PANEL_ID} .ld-option {
        display: flex;
        align-items: center;
        gap: 8px;
        border-radius: 6px;
        padding: 7px 8px;
        font-size: 13px;
        line-height: 1.2;
        background-color: #fff !important;
      }
      #${PANEL_ID} .ld-option input {
        margin: 0;
      }
      #${PANEL_ID} .ld-separator {
        height: 1px;
        margin: 6px 4px;
        background: #e2e8f0;
      }
      #${PANEL_ID} button {
        width: 100%;
        padding: 0 10px;
        cursor: pointer;
      }
      #${PANEL_ID} button:hover,
      #${PANEL_ID} summary:hover,
      #${PANEL_ID} .ld-option:hover {
        background: #f1f5f9;
      }
      #${MODEL_TOOLTIP_ID} {
        position: fixed;
        z-index: 999999;
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background-color: #fff !important;
        color: #0f172a !important;
        box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
        padding: 8px;
        font-size: 12px;
        line-height: 1.45;
        white-space: normal;
        pointer-events: none;
      }
      #${MODEL_TOOLTIP_ID} .ld-model-tooltip-title {
        margin-bottom: 6px;
        padding: 2px 4px 6px;
        border-bottom: 1px solid hsl(var(--border, 214 32% 91%));
        background-color: #fff !important;
        font-weight: 600;
      }
      #${MODEL_TOOLTIP_ID} .ld-model-item {
        margin: 2px 0;
        padding: 3px 4px;
        border-radius: 4px;
        background-color: #f1f5f9 !important;
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      }
      #${PROGRESS_ID} {
        position: fixed;
        z-index: 999999;
        right: 16px;
        bottom: 16px;
        width: min(320px, calc(100vw - 32px));
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background: #fff;
        color: #0f172a;
        box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
        padding: 10px 12px;
        font-size: 13px;
        line-height: 1.4;
      }
      #${PROGRESS_ID} .ld-progress-label {
        margin-bottom: 8px;
        font-weight: 600;
      }
      #${PROGRESS_ID} .ld-progress-track {
        height: 6px;
        overflow: hidden;
        border-radius: 999px;
        background: #e2e8f0;
      }
      #${PROGRESS_ID} .ld-progress-bar {
        height: 100%;
        width: var(--ld-progress, 0%);
        border-radius: inherit;
        background: #2563eb;
        transition: width .18s ease;
      }
      #${APPLY_PROGRESS_ID} {
        position: fixed;
        inset: 0;
        z-index: 999998;
        display: grid;
        place-items: center;
        background: rgba(248, 250, 252, .42);
        pointer-events: none;
      }
      #${APPLY_PROGRESS_ID} .ld-apply-box {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        min-width: 176px;
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background: #fff;
        color: #0f172a;
        box-shadow: 0 12px 30px rgba(15, 23, 42, .18);
        padding: 12px 14px;
        font-size: 13px;
        font-weight: 600;
      }
      #${APPLY_PROGRESS_ID} .ld-apply-spinner {
        width: 16px;
        height: 16px;
        flex: 0 0 auto;
        border: 2px solid #cbd5e1;
        border-top-color: #2563eb;
        border-radius: 999px;
        animation: ld-apply-spin .75s linear infinite;
      }
      @keyframes ld-apply-spin {
        to { transform: rotate(360deg); }
      }
      #${LOAD_MORE_ID} {
        position: fixed;
        z-index: 999997;
        left: 50%;
        bottom: 18px;
        transform: translateX(-50%);
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 184px;
        height: 38px;
        border: 1px solid hsl(var(--border, 214 32% 91%));
        border-radius: 8px;
        background: #fff;
        color: #0f172a;
        box-shadow: 0 10px 24px rgba(15, 23, 42, .18);
        padding: 0 14px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
      }
      #${LOAD_MORE_ID}:hover {
        background: #f8fafc;
      }
    `;
    document.head.appendChild(style);
  }

  function showMarketplaceProgress(loadedPages, totalPages, loadedItems) {
    if (!document.body) return;
    ensureStyle();
    clearTimeout(progressHideTimer);
    const total = Math.max(Number(totalPages) || 1, 1);
    const loaded = Math.max(Number(loadedPages) || 0, 0);
    const percent = Math.max(4, Math.min(100, Math.round((loaded / total) * 100)));
    let element = document.getElementById(PROGRESS_ID);
    if (!element) {
      element = document.createElement("div");
      element.id = PROGRESS_ID;
      element.innerHTML = `<div class="ld-progress-label"></div><div class="ld-progress-track"><div class="ld-progress-bar"></div></div>`;
      document.body.appendChild(element);
    }
    element.querySelector(".ld-progress-label").textContent = __TEST__.marketplaceProgressLabel(loaded, total, loadedItems);
    element.style.setProperty("--ld-progress", `${percent}%`);
  }

  function scheduleHideMarketplaceProgress() {
    clearTimeout(progressHideTimer);
    progressHideTimer = setTimeout(hideMarketplaceProgress, 700);
  }

  function hideMarketplaceProgress() {
    clearTimeout(progressHideTimer);
    document.getElementById(PROGRESS_ID)?.remove();
  }

  function showApplyProgress() {
    if (!document.body) return;
    ensureStyle();
    clearTimeout(applyProgressHideTimer);
    clearTimeout(applyProgressFallbackTimer);
    let element = document.getElementById(APPLY_PROGRESS_ID);
    if (!element) {
      element = document.createElement("div");
      element.id = APPLY_PROGRESS_ID;
      element.innerHTML = `<div class="ld-apply-box"><span class="ld-apply-spinner"></span><span>应用筛选/排序中</span></div>`;
      document.body.appendChild(element);
    }
    applyProgressFallbackTimer = setTimeout(() => {
      scrollLoadScheduled = false;
      debugLog("apply-progress-timeout", { state: loadState() });
      hideApplyProgress();
      scheduleUpdateLoadMoreButton();
    }, 2500);
  }

  function scheduleHideApplyProgress() {
    clearTimeout(applyProgressHideTimer);
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        applyProgressHideTimer = setTimeout(hideApplyProgress, 120);
      });
    });
  }

  function hideApplyProgress() {
    clearTimeout(applyProgressHideTimer);
    clearTimeout(applyProgressFallbackTimer);
    document.getElementById(APPLY_PROGRESS_ID)?.remove();
  }

  function scheduleUpdateLoadMoreButton() {
    requestAnimationFrame(updateLoadMoreButton);
  }

  function updateLoadMoreButton() {
    const active = shouldShowMarketplaceLoadMoreUi();
    const remaining = Math.max(lastMarketplaceResultCount - marketplaceRenderLimit, 0);
    debugLog("load-more-button-update", {
      active,
      renderLimit: marketplaceRenderLimit,
      total: lastMarketplaceResultCount,
      remaining,
    });
    if (!document.body || !active || remaining <= 0) {
      hideLoadMoreButton();
      return;
    }
    ensureStyle();
    let button = document.getElementById(LOAD_MORE_ID);
    if (!button) {
      button = document.createElement("button");
      button.id = LOAD_MORE_ID;
      button.type = "button";
      button.addEventListener("click", () => loadMoreMarketplaceChannels("button"));
      document.body.appendChild(button);
    }
    button.textContent = `加载更多(剩余 ${remaining})`;
  }

  function hideLoadMoreButton() {
    document.getElementById(LOAD_MORE_ID)?.remove();
  }

  function loadMoreMarketplaceChannels(source) {
    if (scrollLoadScheduled) return;
    if (marketplaceRenderLimit >= lastMarketplaceResultCount) {
      hideLoadMoreButton();
      return;
    }
    const nextLimit = __TEST__.marketplaceNextRenderLimit(
      marketplaceRenderLimit,
      lastMarketplaceResultCount,
      MARKETPLACE_RENDER_BATCH_SIZE,
    );
    if (nextLimit === marketplaceRenderLimit) return;
    scrollLoadScheduled = true;
    marketplaceRenderLimit = nextLimit;
    debugLog("scroll-load-more", {
      source,
      renderLimit: marketplaceRenderLimit,
      total: lastMarketplaceResultCount,
    });
    applyFiltersWithoutReload({ reset: false });
  }

  function shouldShowMarketplaceLoadMoreUi() {
    return isChannelHubActive() || (location.pathname.includes("/admin/marketplace") && lastMarketplaceResultCount > 0);
  }

  function scheduleRenderedDebugSnapshot(payload, state) {
    const expectedItems = __TEST__.marketplacePayloadItems(payload);
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        debugLog("rendered-snapshot", {
          expected: expectedItems.length,
          expectedFirst: expectedItems.slice(0, 5).map((item) => item.name || item.id),
          rendered: renderedChannelSnapshot(),
          state,
        });
      });
    });
  }

  function renderedChannelSnapshot() {
    loadKnownChannels();
    const names = new Set(Array.from(knownChannels.values()).map((channel) => channel.name).filter(Boolean));
    const renderedNames = [];
    const candidates = Array.from(document.querySelectorAll("main h1, main h2, main h3, main h4, main p, main span, main div"));
    for (const element of candidates) {
      if (element.closest(`#${PANEL_ID}, #${MODEL_TOOLTIP_ID}, #${PROGRESS_ID}, #${APPLY_PROGRESS_ID}, #${LOAD_MORE_ID}`)) continue;
      if (element.children.length > 2) continue;
      const text = String(element.textContent || "").trim();
      if (names.has(text) && !renderedNames.includes(text)) renderedNames.push(text);
      if (renderedNames.length >= 30) break;
    }
    return {
      count: renderedNames.length,
      first: renderedNames.slice(0, 10),
    };
  }

  function renderPanel() {
    if (!document.body) return;
    preferChannelHub();
    cancelAnimationFrame(uiFrame);
    uiFrame = requestAnimationFrame(renderPanelNow);
  }

  function preferChannelHub() {
    const { channelTab, modelTab } = marketplaceTabs();
    if (channelTab && modelTab) {
      channelTab.style.order = "0";
      modelTab.style.order = "1";
    }
    const nextSelection = __TEST__.nextDefaultChannelSelectionState({
      alreadySelected: defaultChannelSelected,
      channelTabExists: Boolean(channelTab),
      channelActive: isTabActive(channelTab),
    });
    defaultChannelSelected = nextSelection.selected;
    if (nextSelection.shouldClick && channelTab && !defaultChannelClickTimer) {
      defaultChannelClickTimer = setTimeout(() => {
        defaultChannelClickTimer = 0;
        channelTab.click();
        setTimeout(renderPanel, 60);
      }, 0);
    }
  }

  function marketplaceTabs() {
    const tablist = document.querySelector('[role="tablist"]');
    const tabs = Array.from(tablist?.querySelectorAll('[role="tab"]') || []);
    return {
      channelTab: tabs.find((tab) => /channel hub|渠道/i.test(String(tab.textContent || ""))),
      modelTab: tabs.find((tab) => /model hub|模型/i.test(String(tab.textContent || ""))),
    };
  }

  function isTabActive(tab) {
    return Boolean(tab)
      && (/true/.test(String(tab.getAttribute("aria-selected"))) || tab.getAttribute("data-state") === "active");
  }

  function renderPanelNow() {
    const existing = document.getElementById(PANEL_ID);
    if (!isChannelHubActive()) {
      existing?.remove();
      restoreNativeFields();
      restoreNativePagination();
      return;
    }
    ensureStyle();
    const controls = findNativeControls();
    const useNativeAnchor = Boolean(controls.grid && controls.tags && controls.sort);
    const fallbackAnchor = useNativeAnchor ? null : findPanelFallbackAnchor();
    if (!useNativeAnchor && !fallbackAnchor) return;
    const panel = existing || document.createElement("div");
    const signature = stateSignature();
    panel.id = PANEL_ID;
    panel.classList.toggle("ld-panel-fallback", !useNativeAnchor);
    if (panel.dataset.signature !== signature) {
      panel.innerHTML = panelHtml();
      panel.dataset.signature = signature;
    }
    if (useNativeAnchor && (!existing || panel.parentElement !== controls.grid || panel.previousElementSibling !== controls.tags)) {
      controls.tags.insertAdjacentElement("afterend", panel);
    }
    if (!useNativeAnchor && panel.parentElement !== fallbackAnchor) {
      fallbackAnchor.insertAdjacentElement("afterbegin", panel);
    }
    if (useNativeAnchor) {
      placePanelFields(panel, controls);
    } else {
      restoreNativeFields();
    }
    panel.onchange = handlePanelChange;
    panel.onclick = handlePanelClick;
    panel.oninput = handlePanelInput;
    hideNativePagination();
    attachNativeSearchListener();
    bindModelTooltipEvents();
  }

  function findPanelFallbackAnchor() {
    return document.querySelector("main") || document.body;
  }

  function findPanelAnchor() {
    const bars = Array.from(document.querySelectorAll("div[class*='rounded-lg'][class*='border'][class*='p-3']"));
    return bars.find((element) => {
      const text = String(element.textContent || "");
      return /Search|搜索/i.test(text) && /Sort|排序/i.test(text);
    });
  }

  function findNativeControls() {
    const anchor = findPanelAnchor();
    const fields = Array.from(anchor?.querySelectorAll("div") || []);
    const grid = Array.from(anchor?.children || []).find((child) => String(child.className || "").includes("grid"));
    return {
      anchor,
      grid,
      tags: fields.find((field) => /Channel Tags|Tags|渠道标签|标签/i.test(String(field.querySelector("p")?.textContent || ""))),
      sort: fields.find((field) => /Sort|排序/i.test(String(field.querySelector("p")?.textContent || ""))),
    };
  }

  function controlsSignature() {
    const controls = findNativeControls();
    return [controls.tags, controls.sort].map((element) => {
      if (!element) return "";
      const rect = element.getBoundingClientRect();
      return `${Math.round(rect.left)},${Math.round(rect.top)},${Math.round(rect.width)},${Math.round(rect.height)}`;
    }).join("|");
  }

  function placePanelFields(panel, controls) {
    hideNativeField(controls.tags);
    hideNativeField(controls.sort);
  }

  function hideNativeField(field) {
    if (!field || hiddenNativeControls.has(field)) return;
    field.style.display = "none";
    hiddenNativeControls.add(field);
  }

  function restoreNativeFields() {
    for (const element of hiddenNativeControls) element.style.display = "";
    hiddenNativeControls.clear();
  }

  function hideNativePagination() {
    restoreDetachedHiddenPagination();
    const candidates = Array.from(document.querySelectorAll("nav, ul, div"));
    for (const element of candidates) {
      if (!isLikelyPaginationElement(element) || hiddenNativePagination.has(element)) continue;
      element.style.display = "none";
      hiddenNativePagination.add(element);
    }
  }

  function isLikelyPaginationElement(element) {
    if (!element || element.closest(`#${PANEL_ID}, #${MODEL_TOOLTIP_ID}`)) return false;
    const controls = Array.from(element.querySelectorAll("button, a, [role='button']"));
    if (controls.length < 2) return false;
    const labels = controls.map((control) => control.getAttribute("aria-label") || control.textContent || "");
    const text = [element.textContent || "", ...labels].join(" ");
    return __TEST__.isLikelyPaginationText(text);
  }

  function restoreNativePagination() {
    for (const element of hiddenNativePagination) element.style.display = "";
    hiddenNativePagination.clear();
  }

  function restoreDetachedHiddenPagination() {
    for (const element of Array.from(hiddenNativePagination)) {
      if (!document.documentElement.contains(element)) hiddenNativePagination.delete(element);
    }
  }

  function panelHtml() {
    const state = loadState();
    const selected = new Set(state.badges);
    const badges = loadBadgeOptions();
    const tagHtml = tagOptions().map((option) => `<label class="ld-option"><input type="radio" name="ld-tag" data-role="tag" value="${option.value}" ${state.tag === option.value ? "checked" : ""}> <span>${option.label}</span></label>`).join("");
    const badgeHtml = badges.length
      ? badges.map((badge) => `<label class="ld-option"><input type="checkbox" data-role="badge" value="${escapeHtml(badge)}" ${selected.has(badge) ? "checked" : ""}> <span>${escapeHtml(badge)}</span></label>`).join("")
      : `<div class="ld-option">等待列表加载 Badges</div>`;
    const tagLabel = tagOptions().find((option) => option.value === state.tag)?.label || "All";
    const selectedText = state.badges.length ? `${tagLabel} · ${state.badges.join(", ")}` : tagLabel;
    const sortLabel = sortOptions().find((option) => option.value === state.sort)?.label || "Newest";
    const sortHtml = sortOptions().map((option) => `<label class="ld-option"><input type="radio" name="ld-sort" data-role="sort" value="${option.value}" ${state.sort === option.value ? "checked" : ""}> <span>${option.label}</span></label>`).join("");
    return `
      <div class="ld-field" data-field="tags">
        <details>
          <summary><span class="ld-summary-text">${escapeHtml(selectedText)}</span></summary>
          <div class="ld-menu">${tagHtml}<div class="ld-separator"></div>${badgeHtml}</div>
        </details>
      </div>
      <div class="ld-field" data-field="sort">
        <details>
          <summary><span class="ld-summary-text">${escapeHtml(sortLabel)}</span></summary>
          <div class="ld-menu">${sortHtml}</div>
        </details>
      </div>
      <div class="ld-extra-field" data-field="free">
        <span class="ld-label">Free</span>
        <label class="ld-control"><input type="checkbox" data-role="free" ${state.free ? "checked" : ""}> <span style="margin-left:8px">只看 Free</span></label>
      </div>
      <div class="ld-extra-field" data-field="model">
        <span class="ld-label">模型</span>
        <input type="search" data-role="modelKeyword" value="${escapeHtml(state.modelKeyword || "")}" placeholder="筛选模型 ID">
      </div>
      <div class="ld-extra-field" data-field="clear">
        <span class="ld-label">操作</span>
        <button type="button" data-role="clear">清空筛选</button>
      </div>
    `;
  }

  function tagOptions() {
    return [
      { value: "all", label: "All" },
      { value: "official", label: "Official" },
      { value: "third_party", label: "Third party" },
      { value: "client_restricted", label: "Client restricted" },
      { value: "strict_client_restricted", label: "Strict client restricted" },
    ];
  }

  function sortOptions() {
    return [
      { value: "created_desc", label: "Newest" },
      { value: "popular_desc", label: "热门度" },
      { value: "consumed_desc", label: "Most consumed" },
      { value: "consumed_asc", label: "Least consumed" },
      { value: "models_desc", label: "Most models" },
      { value: "multiplier_desc", label: "Highest multiplier" },
      { value: "multiplier_asc", label: "Lowest multiplier" },
      { value: "name_asc", label: "Name A-Z" },
    ];
  }

  function handlePanelChange(event) {
    if (event.target?.dataset?.role === "modelKeyword") return;
    const panel = event.currentTarget;
    const state = {
      tag: panel.querySelector('[data-role="tag"]:checked')?.value || "all",
      free: Boolean(panel.querySelector('[data-role="free"]')?.checked),
      badges: Array.from(panel.querySelectorAll('[data-role="badge"]:checked')).map((input) => input.value),
      sort: panel.querySelector('[data-role="sort"]:checked')?.value || "created_desc",
      modelKeyword: __TEST__.panelModelKeywordValue(panel.querySelector('[data-role="modelKeyword"]')?.value),
    };
    saveState(state);
    debugLog("panel-change", { state });
    resetMarketplaceRenderLimit({ scrollTop: true });
    panel.dataset.signature = "";
    renderPanel();
    applyFiltersWithoutReload({ reset: false });
  }

  function handlePanelClick(event) {
    if (event.target?.dataset?.role !== "clear") return;
    const state = { tag: "all", badges: [], free: false, sort: "created_desc", modelKeyword: "" };
    saveState(state);
    debugLog("panel-clear", { state });
    resetMarketplaceRenderLimit({ scrollTop: true });
    const panel = event.currentTarget;
    panel.dataset.signature = "";
    renderPanel();
    applyFiltersWithoutReload({ reset: false });
  }

  function handlePanelInput(event) {
    if (event.target?.dataset?.role !== "modelKeyword") return;
    const panel = event.currentTarget;
    const state = loadState();
    saveState({
      ...state,
      modelKeyword: event.target.value,
    });
    debugLog("panel-model-input", { modelKeyword: event.target.value, state: loadState() });
    panel.dataset.signature = stateSignature();
    debounceApplyFilters({ reset: true });
  }

  let applyTimer = 0;
  function debounceApplyFilters(options = {}) {
    clearTimeout(applyTimer);
    applyTimer = setTimeout(() => applyFiltersWithoutReload(options), 180);
  }

  function applyFiltersWithoutReload({ reset = false } = {}) {
    if (reset) resetMarketplaceRenderLimit({ scrollTop: true });
    debugLog("apply-start", { state: loadState() });
    showApplyProgress();
    if (!triggerChannelHubRerender()) {
      scrollLoadScheduled = false;
      scheduleUpdateLoadMoreButton();
      scheduleHideApplyProgress();
    }
    queueMicrotask(bindModelTooltipEvents);
  }

  function triggerChannelHubRerender() {
    const input = findChannelSearchInput();
    if (!input) {
      debugLog("rerender-no-native-input", { state: loadState() });
      return triggerChannelHubTabReload();
    }
    triggeringNativeRerender = true;
    const search = __TEST__.cleanMarketplaceSearch(input.value);
    rerenderMarkerCounter = (rerenderMarkerCounter + 1) % 7;
    const marker = "\u200b".repeat(rerenderMarkerCounter + 1);
    debugLog("rerender-trigger", {
      nativeSearch: search,
      markerLength: marker.length,
      state: loadState(),
    });
    setNativeInputValue(input, `${search}${marker}`);
    input.dispatchEvent(new Event("input", { bubbles: true }));
    setTimeout(() => {
      triggeringNativeRerender = false;
    }, 20);
    return true;
  }

  function triggerChannelHubTabReload() {
    const { channelTab, modelTab } = marketplaceTabs();
    if (!channelTab) return false;
    debugLog("rerender-tab-reload", {
      hasModelTab: Boolean(modelTab),
      state: loadState(),
    });
    const retryNativeInputRerender = (attempt = 0) => {
      const input = findChannelSearchInput();
      if (input) {
        triggerChannelHubRerender();
        return;
      }
      if (attempt < 5) {
        setTimeout(() => retryNativeInputRerender(attempt + 1), 120);
        return;
      }
      debugLog("rerender-give-up", { state: loadState() });
      scrollLoadScheduled = false;
      scheduleHideApplyProgress();
      scheduleUpdateLoadMoreButton();
      renderPanel();
    };
    if (modelTab && modelTab !== channelTab) {
      modelTab.click();
      setTimeout(() => {
        marketplaceTabs().channelTab?.click();
        renderPanel();
        setTimeout(retryNativeInputRerender, 120);
      }, 40);
    } else {
      channelTab.click();
      setTimeout(() => {
        renderPanel();
        setTimeout(retryNativeInputRerender, 120);
      }, 60);
    }
    return true;
  }

  function attachNativeSearchListener() {
    if (nativeInputListenerAttached) return;
    nativeInputListenerAttached = true;
    document.addEventListener("input", (event) => {
      if (triggeringNativeRerender) return;
      if (event.target?.matches?.("input") && event.target.dataset.role !== "modelKeyword") {
        const input = findChannelSearchInput();
        if (event.target === input) {
          debugLog("native-search-input", {
            nativeSearch: __TEST__.cleanMarketplaceSearch(input.value),
            state: loadState(),
          });
          debounceApplyFilters({ reset: true });
        }
      }
    }, true);
  }

  function resetMarketplaceRenderLimit({ scrollTop = false } = {}) {
    marketplaceRenderLimit = MARKETPLACE_RENDER_BATCH_SIZE;
    scrollLoadScheduled = false;
    debugLog("render-limit-reset", { renderLimit: marketplaceRenderLimit, total: lastMarketplaceResultCount, scrollTop });
    scheduleUpdateLoadMoreButton();
    if (scrollTop) requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "auto" }));
  }

  function handleMarketplaceScrollLoad(event) {
    if (!shouldShowMarketplaceLoadMoreUi() || scrollLoadScheduled) return;
    if (marketplaceRenderLimit >= lastMarketplaceResultCount) return;
    const scrollingElement = scrollElementFromEvent(event);
    const remaining = scrollRemaining(scrollingElement);
    const now = Date.now();
    if (remaining <= 1200 || now - lastScrollDebugAt > 1000) {
      lastScrollDebugAt = now;
      debugLog("scroll-check", {
        remaining,
        renderLimit: marketplaceRenderLimit,
        total: lastMarketplaceResultCount,
        target: scrollingElement === window ? "window" : scrollingElement?.tagName || "element",
      });
    }
    if (remaining > 700) return;
    loadMoreMarketplaceChannels("scroll");
  }

  function scrollElementFromEvent(event) {
    const target = event?.target;
    if (target && target !== document && target !== window) {
      const element = target.nodeType === 1 ? target : target.parentElement;
      const scrollable = element?.closest?.("*");
      let current = scrollable;
      while (current && current !== document.documentElement) {
        if (current.scrollHeight > current.clientHeight + 4) return current;
        current = current.parentElement;
      }
    }
    return document.scrollingElement || document.documentElement;
  }

  function scrollRemaining(scrollingElement) {
    if (!scrollingElement || scrollingElement === window) {
      const root = document.scrollingElement || document.documentElement;
      return root.scrollHeight - window.innerHeight - root.scrollTop;
    }
    return scrollingElement.scrollHeight - scrollingElement.clientHeight - scrollingElement.scrollTop;
  }

  function findChannelSearchInput() {
    const bars = Array.from(document.querySelectorAll("div[class*='rounded-lg'][class*='border'][class*='p-3']"));
    const bar = bars.find((element) => /Search|搜索/i.test(String(element.textContent || ""))
      && /Sort|排序/i.test(String(element.textContent || "")));
    if (!bar) return null;
    return Array.from(bar.querySelectorAll("input")).find((input) => input.dataset.role !== "modelKeyword") || null;
  }

  function setNativeInputValue(input, value) {
    const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
    setter?.call(input, value);
  }

  function bindModelTooltipEvents() {
    if (tooltipBound || !document.body) return;
    tooltipBound = true;
    loadKnownChannels();
    document.addEventListener("mousemove", handleModelTooltipMove, true);
    document.addEventListener("mouseleave", hideModelTooltip, true);
    window.addEventListener("scroll", hideModelTooltip, { passive: true });
  }

  function handleModelTooltipMove(event) {
    if (!isChannelHubActive()) return hideModelTooltip();
    const channel = channelFromHoverTarget(event.target);
    if (!channel) return hideModelTooltip();
    showModelTooltip(channel, event.clientX, event.clientY);
  }

  function channelFromHoverTarget(target) {
    loadKnownChannels();
    const element = target?.closest?.("p, h1, h2, h3, h4, span, div");
    if (!element || element.closest(`#${PANEL_ID}, #${MODEL_TOOLTIP_ID}`)) return null;
    if (element.children.length > 2) return null;
    const text = String(element.textContent || "").trim();
    if (!text) return null;
    const channel = knownChannelsByName.get(text);
    return channel?.supportedModels?.length ? channel : null;
  }

  function showModelTooltip(channel, clientX, clientY) {
    const tooltip = ensureModelTooltip();
    tooltip.innerHTML = modelTooltipHtml(channel);
    tooltip.hidden = false;
    const left = Math.min(clientX + 14, window.innerWidth - tooltip.offsetWidth - 12);
    const top = Math.min(clientY + 14, window.innerHeight - tooltip.offsetHeight - 12);
    tooltip.style.left = `${Math.max(12, left)}px`;
    tooltip.style.top = `${Math.max(12, top)}px`;
  }

  function hideModelTooltip() {
    const tooltip = document.getElementById(MODEL_TOOLTIP_ID);
    if (tooltip) tooltip.hidden = true;
  }

  function ensureModelTooltip() {
    let tooltip = document.getElementById(MODEL_TOOLTIP_ID);
    if (!tooltip) {
      tooltip = document.createElement("div");
      tooltip.id = MODEL_TOOLTIP_ID;
      tooltip.hidden = true;
      document.body.appendChild(tooltip);
    }
    return tooltip;
  }

  function modelTooltipHtml(channel) {
    const models = channel.supportedModels || [];
    const modelItems = models.map((model) => `<div class="ld-model-item">${escapeHtml(model)}</div>`).join("");
    return `
      <div class="ld-model-tooltip-title">${escapeHtml(channel.name)} · ${models.length} 模型</div>
      ${modelItems}
    `;
  }

  function escapeHtml(value) {
    return String(value ?? "").replace(/[&<>"']/g, (char) => ({
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#39;",
    })[char]);
  }

  const observer = new MutationObserver(renderPanel);
  function startUi() {
    renderPanel();
    bindModelTooltipEvents();
    window.addEventListener("resize", renderPanel, { passive: true });
    window.addEventListener("scroll", handleMarketplaceScrollLoad, { passive: true });
    document.addEventListener("scroll", handleMarketplaceScrollLoad, { passive: true, capture: true });
    document.addEventListener("wheel", handleMarketplaceScrollLoad, { passive: true, capture: true });
    document.addEventListener("touchmove", handleMarketplaceScrollLoad, { passive: true, capture: true });
    observer.observe(document.documentElement, { childList: true, subtree: true });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", startUi, { once: true });
  } else {
    startUi();
  }
})();