Awesome LinuxDo Reader Lite Platform Library

Data, network, synchronization, and platform modules for Awesome LinuxDo Reader Lite.

Hindi dapat direktang i-install ang script na ito. Ito ay isang library para sa iba pang mga script na isasama sa meta directive. // @require https://update.greasyfork.org/scripts/591595/1904489/Awesome%20LinuxDo%20Reader%20Lite%20Platform%20Library.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Awesome LinuxDo Reader Lite Platform Library
// @name:zh-CN   Awesome LinuxDo Reader Lite 平台库
// @namespace    https://github.com/sunbigfly/awesome-linuxdo-reader
// @version      1.5.2
// @description  Data, network, synchronization, and platform modules for Awesome LinuxDo Reader Lite.
// @description:zh-CN 缓存、集合、Discourse、网络、队列、同步、通知与监控平台模块
// @author       sunbigfly
// @license      MIT
// @homepageURL  https://github.com/sunbigfly/awesome-linuxdo-reader
// @supportURL   https://github.com/sunbigfly/awesome-linuxdo-reader/issues
// @match        https://linux.do/*
// @grant        none
// ==/UserScript==

/* Awesome LinuxDo Reader Lite 1.5.2 - main-lite-platform
 * 缓存、集合、Discourse、网络、队列、同步、通知与监控平台模块
 * 项目 TypeScript 源码保持可读;固定版本第三方依赖压缩打包。
 * 不要直接编辑此文件;修改 lite/src 后重新构建。
 */
(function () {
	'use strict';

	const root = globalThis;
	const runtimeKey = "__AWESOME_LINUXDO_READER_LITE_MODULE_RUNTIME__";
	let runtime = root[runtimeKey];
	if (runtime === undefined) {
		const factories = new Map();
		const sourceHashes = new Map();
		const modules = new Map();
		const libraries = new Set();
		let started = false;
		const externalModuleIds = Object.freeze({"@xsai/generate-text":"vendor/xsai-generate-text.js"});

		const resolve = (parentId, request) => {
			const externalId = externalModuleIds[request];
			if (externalId) return externalId;
			if (!request.startsWith('.')) {
				throw new Error(`[main-lite] unsupported external module: ${request}`);
			}
			const parts = parentId.split('/');
			parts.pop();
			for (const part of request.split('/')) {
				if (!part || part === '.') continue;
				if (part === '..') {
					if (!parts.length) {
						throw new Error(`[main-lite] module escapes root: ${parentId} -> ${request}`);
					}
					parts.pop();
				} else {
					parts.push(part);
				}
			}
			const resolved = parts.join('/');
			return /\.(?:js|json)$/.test(resolved) ? resolved : `${resolved}.js`;
		};

		const requireModule = (id) => {
			const cached = modules.get(id);
			if (cached) return cached.exports;
			const factory = factories.get(id);
			if (!factory) throw new Error(`[main-lite] missing module: ${id}`);
			const module = { exports: {} };
			modules.set(id, module);
			try {
				factory(module, module.exports, (request) => (
					requireModule(resolve(id, request))
				));
			} catch (error) {
				modules.delete(id);
				throw error;
			}
			return module.exports;
		};

		runtime = Object.freeze({
			schemaVersion: 1,
			sourceVersion: "1.5.2",
			register(id, factory, sourceHash) {
				const currentHash = sourceHashes.get(id);
				if (currentHash !== undefined) {
					if (currentHash !== sourceHash) {
						throw new Error(`[main-lite] conflicting module: ${id}`);
					}
					return;
				}
				factories.set(id, factory);
				sourceHashes.set(id, sourceHash);
			},
			markLibrary(name) {
				libraries.add(name);
			},
			start(entryId, expectedLibraries) {
				for (const name of expectedLibraries) {
					if (!libraries.has(name)) {
						throw new Error(`[main-lite] missing library: ${name}`);
					}
				}
				if (started) return requireModule(entryId);
				started = true;
				try {
					return requireModule(entryId);
				} catch (error) {
					started = false;
					throw error;
				}
			},
		});
		Object.defineProperty(root, runtimeKey, {
			configurable: true,
			enumerable: false,
			writable: false,
			value: runtime,
		});
	}
	if (runtime.schemaVersion !== 1 || runtime.sourceVersion !== "1.5.2") {
		throw new Error('[main-lite] Library 版本不匹配');
	}

	var __defProp = Object.defineProperty;
	var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
	var __getOwnPropNames = Object.getOwnPropertyNames;
	var __hasOwnProp = Object.prototype.hasOwnProperty;
	var __export = (target, all) => {
	  for (var name in all)
	    __defProp(target, name, { get: all[name], enumerable: !0 });
	};
	var __copyProps = (to, from, except, desc) => {
	  if (from && typeof from == "object" || typeof from == "function")
	    for (let key of __getOwnPropNames(from))
	      !__hasOwnProp.call(to, key) && key !== except && __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
	  return to;
	};
	var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: !0 }), mod);
	var __create = Object.create;
	var __getProtoOf = Object.getPrototypeOf;
	var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
	  // If the importer is in node compatibility mode or this is not an ESM
	  // file that has been converted to a CommonJS file using a Babel-
	  // compatible transform (i.e. "__esModule" has not been set), then set
	  // "default" to the CommonJS "module.exports" for node compatibility.
	  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: !0 }) : target,
	  mod
	));

/* Source: lite/src/cache/browser-asset-cache.ts */
runtime.register("src/cache/browser-asset-cache.js", function(module, exports, require) {
	var browser_asset_cache_exports = {};
	__export(browser_asset_cache_exports, {
	  READER_ASSET_CACHE_GROUPS: () => READER_ASSET_CACHE_GROUPS,
	  ReaderBrowserAssetCacheRepository: () => ReaderBrowserAssetCacheRepository
	});
	module.exports = __toCommonJS(browser_asset_cache_exports);
	const READER_ASSET_CACHE_GROUPS = Object.freeze([
	  Object.freeze({
	    id: "avatar",
	    label: "头像",
	    cacheName: "linuxdo-enhanced-reader:avatars:v1"
	  }),
	  Object.freeze({
	    id: "emoji",
	    label: "表情",
	    cacheName: "linuxdo-enhanced-reader:emoji-images:v1"
	  }),
	  Object.freeze({
	    id: "original",
	    label: "原图",
	    cacheName: "linuxdo-enhanced-reader:lightbox-images:v1"
	  })
	]), CACHE_STAT_BATCH_SIZE = 32;
	function responseBytes(response) {
	  const header = response.headers.get("content-length"), declared = header === null ? Number.NaN : Number(header);
	  return Number.isFinite(declared) && declared >= 0 ? declared : response.blob().then((blob) => blob.size);
	}
	function emptyGroup(definition, state) {
	  return Object.freeze({
	    ...definition,
	    count: 0,
	    bytes: 0,
	    state
	  });
	}
	class ReaderBrowserAssetCacheRepository {
	  #storage;
	  constructor(storage) {
	    this.#storage = storage;
	  }
	  async stats() {
	    let existing;
	    try {
	      existing = new Set(await this.#storage.keys());
	    } catch {
	      const errors2 = READER_ASSET_CACHE_GROUPS.map(({ label }) => `${label}缓存目录不可用`);
	      return Object.freeze({
	        count: 0,
	        bytes: 0,
	        groups: Object.freeze(
	          READER_ASSET_CACHE_GROUPS.map((group) => emptyGroup(group, "error"))
	        ),
	        errors: Object.freeze(errors2)
	      });
	    }
	    const errors = [], groups = [];
	    for (const definition of READER_ASSET_CACHE_GROUPS) {
	      if (!existing.has(definition.cacheName)) {
	        groups.push(emptyGroup(definition, "missing"));
	        continue;
	      }
	      try {
	        const cache = await this.#storage.open(definition.cacheName), requests = await cache.keys();
	        let bytes = 0;
	        for (let offset = 0; offset < requests.length; offset += CACHE_STAT_BATCH_SIZE) {
	          const batch = requests.slice(
	            offset,
	            offset + CACHE_STAT_BATCH_SIZE
	          ), sizes = await Promise.all(batch.map(async (request) => {
	            const response = await cache.match(request);
	            return response ? responseBytes(response) : 0;
	          }));
	          bytes += sizes.reduce((total, size) => total + size, 0);
	        }
	        groups.push(Object.freeze({
	          ...definition,
	          count: requests.length,
	          bytes,
	          state: "available"
	        }));
	      } catch {
	        errors.push(`${definition.label}缓存统计失败`), groups.push(emptyGroup(definition, "error"));
	      }
	    }
	    return Object.freeze({
	      count: groups.reduce((total, group) => total + group.count, 0),
	      bytes: groups.reduce((total, group) => total + group.bytes, 0),
	      groups: Object.freeze(groups),
	      errors: Object.freeze(errors)
	    });
	  }
	  async clear() {
	    const deleted = [], missing = [], failed = [];
	    return await Promise.all(READER_ASSET_CACHE_GROUPS.map(async (definition) => {
	      try {
	        await this.#storage.delete(definition.cacheName) ? deleted.push(definition.id) : missing.push(definition.id);
	      } catch {
	        failed.push(definition.id);
	      }
	    })), Object.freeze({
	      deleted: Object.freeze(deleted),
	      missing: Object.freeze(missing),
	      failed: Object.freeze(failed)
	    });
	  }
	}
}, "2d25f3dbe9f24884170a8d18afbc4fa6b627c60f06d2f902087cdd418d9a21c7");

/* Source: lite/src/cache/cache-coordination.ts */
runtime.register("src/cache/cache-coordination.js", function(module, exports, require) {
	var cache_coordination_exports = {};
	__export(cache_coordination_exports, {
	  BroadcastCacheCoordinationChannel: () => BroadcastCacheCoordinationChannel,
	  BrowserCacheCoordinationStatePort: () => BrowserCacheCoordinationStatePort,
	  CrossTabCacheCoordinator: () => CrossTabCacheCoordinator,
	  normalizeCacheInvalidation: () => normalizeCacheInvalidation
	});
	module.exports = __toCommonJS(cache_coordination_exports);
	function positiveInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function uniqueStrings(values) {
	  if (!values) return;
	  const normalized = [...new Set(values.map(String).map((value) => value.trim()).filter(Boolean))].sort();
	  return normalized.length ? Object.freeze(normalized) : void 0;
	}
	function normalizeCacheInvalidation(query) {
	  if (query.all) return Object.freeze({ all: !0 });
	  const ids = uniqueStrings(query.ids), kinds = uniqueStrings(query.kinds), tags = uniqueStrings(query.tags);
	  return !ids && !kinds && !tags ? null : Object.freeze({
	    ...ids ? { ids } : {},
	    ...kinds ? { kinds } : {},
	    ...tags ? { tags } : {}
	  });
	}
	function normalizeState(raw, now, staleAfterMs) {
	  const source = raw && typeof raw == "object" ? raw : {}, flights = Array.isArray(source.flights) ? source.flights.filter((flight) => !flight || typeof flight != "object" ? !1 : String(flight.token || "").length > 0 && String(flight.flightId || "").length > 0 && String(flight.ownerId || "").length > 0 && Number.isFinite(flight.epoch) && Number(flight.expiresAt) > now && Number(flight.heartbeatAt) > now - staleAfterMs) : [], failures = Array.isArray(source.failures) ? source.failures.filter((failure) => !failure || typeof failure != "object" ? !1 : String(failure.token || "").length > 0 && String(failure.flightId || "").length > 0 && Number(failure.expiresAt) > now && (failure.kind === "cloudflare" || failure.kind === "rate-limit") && Number.isSafeInteger(Number(failure.status)) && Number(failure.status) >= 400) : [];
	  return {
	    schemaVersion: 1,
	    epoch: Math.max(0, Math.floor(Number(source.epoch) || 0)),
	    updatedAt: Math.max(0, Number(source.updatedAt) || 0),
	    flights: flights.map((flight) => Object.freeze({
	      token: String(flight.token),
	      flightId: String(flight.flightId),
	      ownerId: String(flight.ownerId),
	      epoch: Math.max(0, Math.floor(Number(flight.epoch) || 0)),
	      heartbeatAt: Number(flight.heartbeatAt),
	      expiresAt: Number(flight.expiresAt)
	    })).sort((left, right) => left.expiresAt - right.expiresAt).slice(-128),
	    failures: failures.map((failure) => Object.freeze({
	      token: String(failure.token),
	      flightId: String(failure.flightId),
	      expiresAt: Number(failure.expiresAt),
	      status: Number(failure.status),
	      cloudflareMitigated: failure.cloudflareMitigated === !0,
	      kind: failure.kind
	    })).sort((left, right) => left.expiresAt - right.expiresAt).slice(-128)
	  };
	}
	function immutableState(state) {
	  return Object.freeze({
	    schemaVersion: 1,
	    epoch: state.epoch,
	    updatedAt: state.updatedAt,
	    flights: Object.freeze([...state.flights]),
	    failures: Object.freeze([...state.failures])
	  });
	}
	class BrowserCacheCoordinationStatePort {
	  atomic;
	  #storage;
	  #storageKey;
	  #lockName;
	  #locks;
	  #onError;
	  constructor(options) {
	    if (this.#storage = options.storage, this.#storageKey = String(options.storageKey).trim(), this.#lockName = String(options.lockName).trim(), !this.#storageKey || !this.#lockName)
	      throw new Error("cache coordination storageKey/lockName 不能为空");
	    this.#locks = options.locks ?? null, this.atomic = !!this.#locks, this.#onError = options.onError ?? (() => {
	    });
	  }
	  async read(now, staleAfterMs) {
	    return immutableState(this.#readMutable(now, staleAfterMs));
	  }
	  async transact(now, staleAfterMs, operation) {
	    const execute = async () => {
	      const state = this.#readMutable(now, staleAfterMs), result = await operation(state);
	      state.updatedAt = now;
	      try {
	        this.#storage.setItem(this.#storageKey, JSON.stringify(state));
	      } catch (error) {
	        throw this.#onError(error), error;
	      }
	      return result;
	    };
	    return this.#locks ? this.#locks.request(this.#lockName, { mode: "exclusive" }, execute) : execute();
	  }
	  #readMutable(now, staleAfterMs) {
	    try {
	      return normalizeState(
	        JSON.parse(this.#storage.getItem(this.#storageKey) || "null"),
	        now,
	        staleAfterMs
	      );
	    } catch (error) {
	      return this.#onError(error), normalizeState(null, now, staleAfterMs);
	    }
	  }
	}
	class BroadcastCacheCoordinationChannel {
	  #channel;
	  #listeners = /* @__PURE__ */ new Set();
	  #onError;
	  constructor(options) {
	    const name = String(options.name).trim();
	    if (!name) throw new Error("BroadcastChannel name 不能为空");
	    this.#onError = options.onError ?? (() => {
	    });
	    const factory = options.factory === void 0 ? typeof BroadcastChannel > "u" ? null : (channelName) => new BroadcastChannel(channelName) : options.factory;
	    let channel = null;
	    try {
	      channel = factory?.(name) ?? null, channel?.addEventListener("message", this.#onMessage);
	    } catch (error) {
	      this.#onError(error);
	    }
	    this.#channel = channel;
	  }
	  publish(message) {
	    try {
	      this.#channel?.postMessage(message);
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  subscribe(listener) {
	    return this.#listeners.add(listener), () => this.#listeners.delete(listener);
	  }
	  close() {
	    this.#channel?.removeEventListener("message", this.#onMessage), this.#channel?.close(), this.#listeners.clear();
	  }
	  #onMessage = (event) => {
	    for (const listener of this.#listeners) listener(event.data);
	  };
	}
	class CrossTabCacheCoordinator {
	  coordinationMode;
	  #sourceId;
	  #channel;
	  #state;
	  #flightTtlMs;
	  #flightStaleMs;
	  #now;
	  #createId;
	  #onError;
	  #listeners = /* @__PURE__ */ new Set();
	  #waiters = /* @__PURE__ */ new Set();
	  #unsubscribe;
	  #closed = !1;
	  constructor(options) {
	    if (this.#sourceId = String(options.sourceId).trim(), !this.#sourceId) throw new Error("cache coordinator sourceId 不能为空");
	    this.#channel = options.channel, this.#state = options.state, this.#flightTtlMs = positiveInteger(options.flightTtlMs, "flightTtlMs"), this.#flightStaleMs = positiveInteger(options.flightStaleMs, "flightStaleMs"), this.#now = options.now ?? Date.now, this.#createId = options.createId ?? (() => `${this.#sourceId}:${this.#now().toString(36)}:${Math.random().toString(36).slice(2)}`), this.#onError = options.onError ?? (() => {
	    }), this.coordinationMode = options.state.atomic ? "atomic" : "mutation-only", this.#unsubscribe = this.#channel.subscribe((message) => this.#receive(message));
	  }
	  publish(query) {
	    const normalized = normalizeCacheInvalidation(query);
	    !normalized || this.#closed || this.#channel.publish(Object.freeze({
	      schemaVersion: 1,
	      sourceId: this.#sourceId,
	      type: "invalidate",
	      query: normalized
	    }));
	  }
	  subscribeInvalidation(listener) {
	    return this.#closed ? () => {
	    } : (this.#listeners.add(listener), () => this.#listeners.delete(listener));
	  }
	  async acquireFlight(rawToken) {
	    const token = String(rawToken).trim();
	    if (!token || !this.#state.atomic) return this.#uncoordinatedLease(token);
	    const now = this.#now();
	    try {
	      const lease = await this.#state.transact(now, this.#flightStaleMs, (state) => {
	        const existing = state.flights.find((flight2) => flight2.token === token);
	        if (existing)
	          return Object.freeze({
	            producer: !1,
	            token,
	            flightId: existing.flightId,
	            epoch: existing.epoch,
	            expiresAt: existing.expiresAt,
	            coordinated: !0
	          });
	        const flightId = this.#createId(), flight = Object.freeze({
	          token,
	          flightId,
	          ownerId: this.#sourceId,
	          epoch: state.epoch,
	          heartbeatAt: now,
	          expiresAt: now + this.#flightTtlMs
	        });
	        return state.flights.push(flight), Object.freeze({
	          producer: !0,
	          token,
	          flightId,
	          epoch: state.epoch,
	          expiresAt: flight.expiresAt,
	          coordinated: !0
	        });
	      });
	      return this.#publishState(), lease;
	    } catch (error) {
	      return this.#onError(error), this.#uncoordinatedLease(token);
	    }
	  }
	  async renewFlight(lease) {
	    if (!lease.coordinated || !lease.producer || !this.#state.atomic) return !1;
	    const now = this.#now();
	    try {
	      const renewed = await this.#state.transact(now, this.#flightStaleMs, (state) => {
	        const index = state.flights.findIndex((flight) => flight.token === lease.token && flight.flightId === lease.flightId && flight.ownerId === this.#sourceId);
	        if (index < 0) return !1;
	        const current = state.flights[index];
	        return current ? (state.flights[index] = Object.freeze({
	          ...current,
	          heartbeatAt: now,
	          expiresAt: now + this.#flightTtlMs
	        }), !0) : !1;
	      });
	      return renewed && this.#publishState(), renewed;
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  async releaseFlight(lease) {
	    if (!lease.coordinated || !lease.producer || !this.#state.atomic) return;
	    const now = this.#now();
	    try {
	      await this.#state.transact(now, this.#flightStaleMs, (state) => {
	        state.flights = state.flights.filter((flight) => flight.token !== lease.token || flight.flightId !== lease.flightId || flight.ownerId !== this.#sourceId);
	      }), this.#publishState();
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  async waitForFlight(rawToken, signal, deadline = this.#now() + this.#flightTtlMs * 2) {
	    const token = String(rawToken).trim();
	    if (!token || !this.#state.atomic) return !0;
	    for (; !this.#closed && this.#now() < deadline; ) {
	      if (signal?.aborted) throw this.#abortError(signal);
	      let state;
	      try {
	        state = await this.#state.read(this.#now(), this.#flightStaleMs);
	      } catch (error) {
	        return this.#onError(error), !0;
	      }
	      const flight = state.flights.find((candidate) => candidate.token === token);
	      if (!flight) return !0;
	      await this.#wait(
	        Math.min(1e3, Math.max(25, flight.expiresAt - this.#now()), deadline - this.#now()),
	        signal
	      );
	    }
	    if (signal?.aborted) throw this.#abortError(signal);
	    return !1;
	  }
	  async failFlight(lease, failure) {
	    if (!lease.coordinated || !lease.producer || !this.#state.atomic) return !1;
	    const now = this.#now();
	    try {
	      const committed = await this.#state.transact(
	        now,
	        this.#flightStaleMs,
	        (state) => {
	          const index = state.flights.findIndex((flight) => flight.token === lease.token && flight.flightId === lease.flightId && flight.ownerId === this.#sourceId);
	          return index < 0 ? !1 : (state.flights.splice(index, 1), state.failures = state.failures.filter((candidate) => candidate.flightId !== lease.flightId), state.failures.push(Object.freeze({
	            token: lease.token,
	            flightId: lease.flightId,
	            expiresAt: now + this.#flightTtlMs,
	            status: failure.status,
	            cloudflareMitigated: failure.cloudflareMitigated,
	            kind: failure.kind
	          })), !0);
	        }
	      );
	      return committed && this.#publishState(), committed;
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  async readFlightFailure(lease) {
	    if (!lease.coordinated || !this.#state.atomic) return null;
	    try {
	      const failure = (await this.#state.read(this.#now(), this.#flightStaleMs)).failures?.find((candidate) => candidate.token === lease.token && candidate.flightId === lease.flightId);
	      return failure ? Object.freeze({
	        status: failure.status,
	        cloudflareMitigated: failure.cloudflareMitigated,
	        kind: failure.kind
	      }) : null;
	    } catch (error) {
	      return this.#onError(error), null;
	    }
	  }
	  async invalidateWrites() {
	    if (!this.#state.atomic)
	      return this.#publishState(), 0;
	    const now = this.#now();
	    try {
	      const epoch = await this.#state.transact(now, this.#flightStaleMs, (state) => (state.epoch += 1, state.epoch));
	      return this.#publishState(), epoch;
	    } catch (error) {
	      return this.#onError(error), 0;
	    }
	  }
	  async commitFlight(lease, operation) {
	    if (!lease.coordinated || !this.#state.atomic)
	      return await operation(), !0;
	    try {
	      return await this.#state.transact(this.#now(), this.#flightStaleMs, async (state) => state.epoch === lease.epoch && state.flights.some((flight) => flight.token === lease.token && flight.flightId === lease.flightId && flight.ownerId === this.#sourceId) ? (await operation(), !0) : !1);
	    } catch (error) {
	      return this.#onError(error), !1;
	    }
	  }
	  close() {
	    this.#closed || (this.#closed = !0, this.#unsubscribe(), this.#channel.close(), this.#listeners.clear(), this.#notifyWaiters());
	  }
	  #receive(raw) {
	    if (!raw || typeof raw != "object") return;
	    const message = raw;
	    if (!(message.schemaVersion !== 1 || message.sourceId === this.#sourceId || message.type !== "invalidate" && message.type !== "state")) {
	      if (message.type === "invalidate") {
	        const query = normalizeCacheInvalidation(message.query ?? {});
	        if (query)
	          for (const listener of this.#listeners)
	            try {
	              listener(query);
	            } catch (error) {
	              this.#onError(error);
	            }
	      }
	      this.#notifyWaiters();
	    }
	  }
	  #publishState() {
	    this.#closed || (this.#channel.publish(Object.freeze({
	      schemaVersion: 1,
	      sourceId: this.#sourceId,
	      type: "state"
	    })), this.#notifyWaiters());
	  }
	  #notifyWaiters() {
	    for (const waiter of this.#waiters) waiter();
	    this.#waiters.clear();
	  }
	  #wait(milliseconds, signal) {
	    return new Promise((resolve, reject) => {
	      let settled = !1;
	      const finish = (error) => {
	        settled || (settled = !0, clearTimeout(timer), this.#waiters.delete(wake), signal?.removeEventListener("abort", abort), error !== void 0 ? reject(error) : resolve());
	      }, wake = () => finish(), abort = () => finish(this.#abortError(signal)), timer = setTimeout(wake, Math.max(0, milliseconds));
	      this.#waiters.add(wake), signal?.addEventListener("abort", abort, { once: !0 });
	    });
	  }
	  #abortError(signal) {
	    return signal?.reason ?? new DOMException("Aborted", "AbortError");
	  }
	  #uncoordinatedLease(token) {
	    return Object.freeze({
	      producer: !0,
	      token,
	      flightId: "",
	      epoch: 0,
	      expiresAt: 0,
	      coordinated: !1
	    });
	  }
	}
}, "426097a26de19d79f96aa759e8005745ffdcb42c7015d0cc7750b2e276348878");

/* Source: lite/src/cache/cache-identity.ts */
runtime.register("src/cache/cache-identity.js", function(module, exports, require) {
	var cache_identity_exports = {};
	__export(cache_identity_exports, {
	  sharedCacheIdToken: () => sharedCacheIdToken
	});
	module.exports = __toCommonJS(cache_identity_exports);
	function sharedCacheIdToken(value) {
	  const source = String(value);
	  let hash = 2166136261;
	  for (let index = 0; index < source.length; index += 1)
	    hash ^= source.charCodeAt(index), hash = Math.imul(hash, 16777619);
	  return (hash >>> 0).toString(36);
	}
}, "fca985f86f39ba26872e25e42a855fbaff32da679df43591f16ca5e46a5e1157");

/* Source: lite/src/cache/discourse-application-cache-invalidation.ts */
runtime.register("src/cache/discourse-application-cache-invalidation.js", function(module, exports, require) {
	var discourse_application_cache_invalidation_exports = {};
	__export(discourse_application_cache_invalidation_exports, {
	  DiscourseApplicationCacheInvalidationCoordinator: () => DiscourseApplicationCacheInvalidationCoordinator
	});
	module.exports = __toCommonJS(discourse_application_cache_invalidation_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_lifecycle = require("../kernel/lifecycle.js");
	function record(value) {
	  if (!value || typeof value != "object" || Array.isArray(value)) return null;
	  const source = value, toJSON = source.toJSON;
	  if (typeof toJSON != "function") return source;
	  try {
	    return record(toJSON.call(value)) ?? source;
	  } catch {
	    return source;
	  }
	}
	function positiveId(value) {
	  const id = Number(value);
	  return Number.isSafeInteger(id) && id > 0 ? id : null;
	}
	function postIdentity(value) {
	  const envelope = record(value), post = record(envelope?.post) ?? envelope;
	  return Object.freeze({
	    postId: positiveId(post?.id),
	    topicId: positiveId(post?.topic_id) ?? positiveId(record(post?.topic)?.id)
	  });
	}
	function eventPost(value) {
	  const envelope = record(value);
	  return record(envelope?.post) ?? envelope;
	}
	class DiscourseApplicationCacheInvalidationCoordinator {
	  scope;
	  #options;
	  #pending = /* @__PURE__ */ new Set();
	  #scheduled = !1;
	  #flushPromise = null;
	  constructor(options) {
	    this.#options = options, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#bindReactionEvents(), this.scope.add(options.composerEvents.subscribe((event) => {
	      this.#onComposerEvent(event);
	    })), this.scope.add(() => {
	      this.#pending.clear(), this.#scheduled = !1;
	    });
	  }
	  flush() {
	    if (this.scope.destroyed) return this.#flushPromise ?? Promise.resolve();
	    if (this.#scheduled = !1, !this.#pending.size) return this.#flushPromise ?? Promise.resolve();
	    const tags = Object.freeze([...this.#pending].sort());
	    this.#pending.clear();
	    const transaction = (this.#flushPromise ?? Promise.resolve()).catch(() => {
	    }).then(async () => {
	      try {
	        await this.#options.cache.invalidate({ tags });
	      } catch (cause) {
	        this.#report(cause);
	      }
	    });
	    return this.#flushPromise = transaction, transaction.finally(() => {
	      this.#flushPromise === transaction && (this.#flushPromise = null), this.#pending.size && this.#schedule();
	    }), transaction;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #bindReactionEvents() {
	    const listener = (payload) => {
	      const post = eventPost(payload), identity = postIdentity(post ?? payload);
	      if (post)
	        try {
	          this.#options.onPostChanged?.(post);
	        } catch (cause) {
	          this.#report(cause);
	        }
	      this.#queue(["reactions-given"], identity);
	    };
	    this.scope.add((0, import_native_host_api.discourseNativeAppEventSubscription)(
	      this.#options.host,
	      "discourse-reactions:reaction-toggled",
	      listener,
	      (cause) => this.#report(cause)
	    ));
	  }
	  #onComposerEvent(event) {
	    const identity = postIdentity(event.payload), fallbackTopicId = event.kind === "edited" ? positiveId(this.#options.currentTopicId()) : null;
	    this.#queue([], Object.freeze({
	      postId: identity.postId,
	      topicId: identity.topicId ?? fallbackTopicId
	    }));
	  }
	  #queue(baseTags, identity) {
	    if (!this.scope.destroyed) {
	      for (const tag of baseTags) this.#pending.add(tag);
	      identity.postId !== null && this.#pending.add(`post:${identity.postId}`), identity.topicId !== null && this.#pending.add(`topic:${identity.topicId}`), this.#schedule();
	    }
	  }
	  #schedule() {
	    this.#scheduled || this.scope.destroyed || !this.#pending.size || (this.#scheduled = !0, queueMicrotask(() => {
	      this.scope.destroyed || (this.#scheduled = !1, this.flush());
	    }));
	  }
	  #report(cause) {
	    try {
	      this.#options.onError?.(cause);
	    } catch {
	    }
	  }
	}
}, "fe45e48ea5b25ee591dcd5cd1a9e7b072dbf709b3b0d371f04232238fac05d59");

/* Source: lite/src/cache/indexeddb-response-cache-store.ts */
runtime.register("src/cache/indexeddb-response-cache-store.js", function(module, exports, require) {
	var indexeddb_response_cache_store_exports = {};
	__export(indexeddb_response_cache_store_exports, {
	  IndexedDbResponseCacheStore: () => IndexedDbResponseCacheStore,
	  selectResponseCachePruneIds: () => selectResponseCachePruneIds
	});
	module.exports = __toCommonJS(indexeddb_response_cache_store_exports);
	function positiveInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function matches(entry, query) {
	  return !!(query.all || query.ids?.includes(entry.id) || query.kinds?.includes(entry.kind) || query.tags?.some((tag) => entry.tags.includes(tag)));
	}
	function selectResponseCachePruneIds(entries, options) {
	  const sorted = [...entries].sort(
	    (left, right) => left.storedAt - right.storedAt || left.id.localeCompare(right.id)
	  ), remove = /* @__PURE__ */ new Set();
	  let retainedEntries = 0, retainedBytes = 0;
	  for (const entry of sorted) {
	    if (entry.schemaVersion !== 1 || !Number.isFinite(entry.expiresAt) || entry.permanent !== !0 && entry.expiresAt <= options.now) {
	      remove.add(entry.id);
	      continue;
	    }
	    entry.permanent !== !0 && (retainedEntries += 1, retainedBytes += Math.max(0, Number(entry.bytes) || 0));
	  }
	  for (const entry of sorted)
	    if (!remove.has(entry.id)) {
	      if (retainedEntries <= options.maxEntries && retainedBytes <= options.maxBytes) break;
	      entry.permanent !== !0 && (remove.add(entry.id), retainedEntries -= 1, retainedBytes -= Math.max(0, Number(entry.bytes) || 0));
	    }
	  return Object.freeze([...remove]);
	}
	class IndexedDbResponseCacheStore {
	  #databaseName;
	  #storeName;
	  #operationTimeoutMs;
	  #maxEntries;
	  #maxBytes;
	  #factory;
	  #now;
	  #onError;
	  #databasePromise = null;
	  #writesSincePrune = 0;
	  #prunePromise = null;
	  constructor(options) {
	    if (this.#databaseName = String(options.databaseName).trim(), this.#storeName = String(options.storeName).trim(), !this.#databaseName || !this.#storeName)
	      throw new Error("IndexedDB databaseName/storeName 不能为空");
	    this.#operationTimeoutMs = positiveInteger(
	      options.operationTimeoutMs,
	      "operationTimeoutMs"
	    ), this.#maxEntries = positiveInteger(options.maxEntries, "maxEntries"), this.#maxBytes = positiveInteger(options.maxBytes, "maxBytes"), this.#factory = options.factory === void 0 ? typeof indexedDB > "u" ? null : indexedDB : options.factory, this.#now = options.now ?? Date.now, this.#onError = options.onError ?? (() => {
	    });
	  }
	  async read(id) {
	    const result = await this.#transaction(
	      "readonly",
	      null,
	      (store, setValue) => {
	        const request = store.get(id);
	        request.onsuccess = () => {
	          const value = request.result;
	          setValue(value ?? null);
	        };
	      }
	    );
	    return result.ok || this.#report(result.error), result.ok ? result.value : null;
	  }
	  async write(entry) {
	    let result = await this.#put(entry);
	    !result.ok && this.#quotaError(result.error) && (await this.prune(!0), result = await this.#put(entry)), result.ok ? this.#recordSuccessfulWrite() : this.#report(result.error);
	  }
	  async invalidate(query) {
	    if (query.all) {
	      const result2 = await this.#transaction(
	        "readwrite",
	        !1,
	        (store) => {
	          store.clear();
	        }
	      );
	      return result2.ok || this.#report(result2.error), Object.freeze({ ok: result2.ok, error: result2.error });
	    }
	    const result = await this.#transaction(
	      "readwrite",
	      !1,
	      (store, setValue) => {
	        const request = store.openCursor();
	        request.onsuccess = () => {
	          const cursor = request.result;
	          if (!cursor) {
	            setValue(!0);
	            return;
	          }
	          const entry = cursor.value;
	          matches(entry, query) && cursor.delete(), cursor.continue();
	        };
	      }
	    );
	    return result.ok || this.#report(result.error), Object.freeze({ ok: result.ok, error: result.error });
	  }
	  async merge(id, update) {
	    let result = await this.#mergeEntry(id, update);
	    return !result.ok && this.#quotaError(result.error) && (await this.prune(!0), result = await this.#mergeEntry(id, update)), result.ok ? this.#recordSuccessfulWrite() : this.#report(result.error), result.ok ? result.value : null;
	  }
	  #mergeEntry(id, update) {
	    return this.#transaction(
	      "readwrite",
	      null,
	      (store, setValue) => {
	        const request = store.get(id);
	        request.onsuccess = () => {
	          try {
	            const next = update(
	              request.result ?? null
	            ), write = store.put(next);
	            write.onsuccess = () => setValue(next);
	          } catch {
	            try {
	              store.transaction.abort();
	            } catch {
	            }
	          }
	        };
	      }
	    );
	  }
	  async records() {
	    const entries = await this.snapshotEntries();
	    return Object.freeze(entries.map((entry) => Object.freeze({
	      id: entry.id,
	      kind: entry.kind,
	      tags: Object.freeze([...entry.tags]),
	      storedAt: entry.storedAt,
	      expiresAt: entry.expiresAt,
	      bytes: Math.max(0, Number(entry.bytes) || 0),
	      ...entry.permanent === !0 ? { permanent: !0 } : {}
	    })));
	  }
	  async snapshotEntries() {
	    const result = await this.#transaction(
	      "readonly",
	      [],
	      (store, setValue) => {
	        const request = store.getAll();
	        request.onsuccess = () => setValue(
	          Array.isArray(request.result) ? request.result : []
	        );
	      }
	    );
	    if (!result.ok) {
	      const error = result.error ?? new Error("IndexedDB cache directory unavailable");
	      throw this.#report(error), error;
	    }
	    return Object.freeze(result.value.map((entry) => Object.freeze({
	      ...entry,
	      tags: Object.freeze([...entry.tags])
	    })));
	  }
	  async prune(forceQuotaRecovery = !1) {
	    const entries = await this.#allEntries(), ids = selectResponseCachePruneIds(entries, {
	      now: this.#now(),
	      maxEntries: this.#maxEntries,
	      maxBytes: this.#maxBytes
	    }), pruneIds = ids.length ? ids : forceQuotaRecovery ? [...entries].filter((entry) => entry.permanent !== !0).sort((left, right) => left.storedAt - right.storedAt).slice(0, Math.max(1, Math.ceil(entries.length / 4))).map((entry) => entry.id) : [];
	    if (!pruneIds.length) return;
	    const result = await this.#deleteIds(pruneIds);
	    result.ok || this.#report(result.error);
	  }
	  async close() {
	    await this.#prunePromise, (await this.#databasePromise)?.close(), this.#databasePromise = null;
	  }
	  #open() {
	    if (this.#databasePromise) return this.#databasePromise;
	    const factory = this.#factory;
	    if (!factory) return Promise.resolve(null);
	    let promise;
	    return promise = new Promise((resolve) => {
	      let settled = !1, request;
	      const finish = (database, error) => {
	        if (settled) {
	          database?.close();
	          return;
	        }
	        settled = !0, clearTimeout(timeoutId), error !== void 0 && this.#report(error), resolve(database);
	      }, timeoutId = setTimeout(
	        () => finish(null, new Error("IndexedDB open timeout")),
	        this.#operationTimeoutMs
	      );
	      try {
	        request = factory.open(this.#databaseName, 1);
	      } catch (error) {
	        finish(null, error);
	        return;
	      }
	      request.onupgradeneeded = () => {
	        const database = request.result, store = database.objectStoreNames.contains(this.#storeName) ? request.transaction?.objectStore(this.#storeName) : database.createObjectStore(this.#storeName, { keyPath: "id" });
	        store && (store.indexNames.contains("kind") || store.createIndex("kind", "kind"), store.indexNames.contains("tags") || store.createIndex("tags", "tags", { multiEntry: !0 }), store.indexNames.contains("storedAt") || store.createIndex("storedAt", "storedAt"));
	      }, request.onsuccess = () => {
	        const database = request.result;
	        database.onversionchange = () => {
	          database.close(), this.#databasePromise === promise && (this.#databasePromise = null);
	        }, finish(database);
	      }, request.onerror = () => finish(null, request.error), request.onblocked = () => finish(null, new Error("IndexedDB open blocked"));
	    }), this.#databasePromise = promise, promise.then((database) => {
	      !database && this.#databasePromise === promise && (this.#databasePromise = null);
	    }), promise;
	  }
	  async #transaction(mode, initialValue, operation) {
	    const database = await this.#open();
	    return database ? new Promise((resolve) => {
	      let settled = !1, value = initialValue, transaction = null;
	      const finish = (ok, error = null) => {
	        settled || (settled = !0, clearTimeout(timeoutId), resolve({ ok, value, error }));
	      }, timeoutId = setTimeout(() => {
	        try {
	          transaction?.abort();
	        } catch {
	        }
	        finish(!1, new Error("IndexedDB transaction timeout"));
	      }, this.#operationTimeoutMs);
	      try {
	        const activeTransaction = database.transaction(this.#storeName, mode);
	        transaction = activeTransaction, activeTransaction.oncomplete = () => finish(!0), activeTransaction.onerror = () => finish(!1, activeTransaction.error), activeTransaction.onabort = () => finish(!1, activeTransaction.error), operation(activeTransaction.objectStore(this.#storeName), (nextValue) => {
	          value = nextValue;
	        });
	      } catch (error) {
	        finish(!1, error);
	      }
	    }) : { ok: !1, value: initialValue, error: new Error("IndexedDB unavailable") };
	  }
	  #put(entry) {
	    return this.#transaction("readwrite", !1, (store, setValue) => {
	      const request = store.put(entry);
	      request.onsuccess = () => setValue(!0);
	    });
	  }
	  #recordSuccessfulWrite() {
	    if (this.#writesSincePrune += 1, this.#writesSincePrune < 32 || (this.#writesSincePrune = 0, this.#prunePromise)) return;
	    const pruning = this.prune().catch((error) => this.#report(error)).finally(() => {
	      this.#prunePromise === pruning && (this.#prunePromise = null);
	    });
	    this.#prunePromise = pruning;
	  }
	  async #allEntries() {
	    const result = await this.#transaction(
	      "readonly",
	      [],
	      (store, setValue) => {
	        const request = store.getAll();
	        request.onsuccess = () => setValue(
	          Array.isArray(request.result) ? request.result : []
	        );
	      }
	    );
	    return result.ok || this.#report(result.error), result.ok ? result.value : [];
	  }
	  #deleteIds(ids) {
	    return this.#transaction("readwrite", !1, (store, setValue) => {
	      for (const id of ids) store.delete(id);
	      setValue(!0);
	    });
	  }
	  #quotaError(error) {
	    return error instanceof DOMException ? error.name === "QuotaExceededError" : String(error?.name ?? "") === "QuotaExceededError";
	  }
	  #report(error) {
	    error != null && this.#onError(error);
	  }
	}
}, "9e8743dbf8e5060e156abde4e4e0a5e5eb3815966802707f2a91a9c54243500b");

/* Source: lite/src/cache/reader-cache-management-surface.ts */
runtime.register("src/cache/reader-cache-management-surface.js", function(module, exports, require) {
	var reader_cache_management_surface_exports = {};
	__export(reader_cache_management_surface_exports, {
	  ReaderCacheManagementSurface: () => ReaderCacheManagementSurface
	});
	module.exports = __toCommonJS(reader_cache_management_surface_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_reader_settings_dom = require("../settings/reader-settings-dom.js");
	const CATEGORIES = Object.freeze([
	  {
	    id: "history",
	    title: "浏览历史与岁月史书",
	    help: "勾选“浏览历史与岁月史书”后再点下方清理,会删除阅读器保存的主题、最近阅读楼层、查看时间和岁月史书 404 记录;不会删除浏览器本身的访问历史,也不会由收到 404 自动触发清理。",
	    retention: "最多 365 天"
	  },
	  {
	    id: "topics",
	    title: "帖子与楼层内容",
	    help: "勾选“帖子与楼层内容”后清理,会删除本机保存的帖子信息、楼层正文、回复关系、主题快照和相关接口数据;若当前正打开主题,会先安全结束旧会话并立即联网重建,避免旧快照在清理后写回。",
	    retention: "接口 7 天 · 快照 30 天"
	  },
	  {
	    id: "users",
	    title: "用户资料卡",
	    help: "勾选“用户资料卡”后清理,会删除用户名、简介、徽章、用户组、关注列表、账户摘要,以及用户观察已缓存的公开历史和采集断点;观察名单仍保留。不会删除 Connect 近 400 天的本机信任观察历史,头像仍归“头像、表情与原图”单独管理。",
	    retention: "资料 1 天 · 观察历史直到主动清理"
	  },
	  {
	    id: "notifications",
	    title: "通知与消息",
	    help: "勾选“通知与消息”后清理,会删除通知分页、回复展开、已读状态和跳转位置等缓存;不会删除站点账号里的真实消息。",
	    retention: "最多 180 天"
	  },
	  {
	    id: "responses",
	    title: "收藏、回应与其他数据",
	    help: "勾选“收藏、回应与其他数据”后清理,会删除收藏列表、给出的回应与点赞、翻译和其他通用接口结果;不会撤销站点上的真实收藏或回应。",
	    retention: "按接口 8 分钟–180 天"
	  },
	  {
	    id: "assets",
	    title: "头像、表情与原图",
	    help: "勾选“头像、表情与原图”后清理,会删除统一图片响应和旧版头像、Boost 表情、实际查看过的原图资源;不会删除帖子中的线上图片。",
	    retention: "接口 30 天 · 兼容缓存 7–90 天"
	  }
	]);
	function categoryOf(record) {
	  return record.kind.startsWith("topic-offline-artifact") || record.tags.some((tag) => tag.startsWith("topic-offline-artifact")) ? null : record.kind === "images" || record.tags.includes("images") ? "assets" : record.kind.includes("notification") || record.tags.includes("notifications") ? "notifications" : record.kind === "topics" || record.kind.startsWith("discourse-topic") || record.tags.some((tag) => tag.startsWith("topic:") || tag.startsWith("post:")) ? "topics" : record.kind === "users" || record.kind === "external-user-summary" || record.kind === "user-observation-history" || record.tags.includes("users") ? "users" : record.kind.includes("bookmark") || record.tags.includes("bookmarks") || record.tags.some((tag) => tag.startsWith("bookmark-")) ? "responses" : record.permanent === !0 ? null : "responses";
	}
	function formatBytes(rawBytes) {
	  const bytes = Math.max(0, Number(rawBytes) || 0);
	  return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
	}
	function assetCacheDetail(snapshot) {
	  return [...snapshot.groups.map((group) => {
	    const state = group.state === "error" ? "统计失败" : `${group.count} 条 · ${formatBytes(group.bytes)}`;
	    return `${group.label}:${state}`;
	  }), ...snapshot.errors].join(";");
	}
	function identityValue(record, key) {
	  const queryOffset = record.id.indexOf("?");
	  if (queryOffset < 0) return "";
	  try {
	    return new URLSearchParams(record.id.slice(queryOffset + 1)).get(key) ?? "";
	  } catch {
	    return "";
	  }
	}
	function taggedValues(records, prefix) {
	  const values = /* @__PURE__ */ new Set();
	  for (const record of records)
	    for (const tag of record.tags)
	      tag.startsWith(prefix) && tag.length > prefix.length && values.add(tag.slice(prefix.length));
	  return values;
	}
	function categoryBytes(records) {
	  return records.reduce(
	    (total, record) => total + Math.max(0, Number(record.bytes) || 0),
	    0
	  );
	}
	function withApplicationCacheDetail(detail, application) {
	  return application?.detail ? `${detail} · ${application.detail}` : detail;
	}
	function categoryStatText(id, records, history, assetCaches, application) {
	  const bytes = categoryBytes(records);
	  if (id === "history") {
	    const topicIds = new Set(history.map(
	      (entry) => Number(entry.topicId)
	    ).filter((topicId) => Number.isSafeInteger(topicId) && topicId > 0)), chronicle = history.filter((entry) => Number(entry.status) === 404 && ["topic", "reply", "boost"].includes(String(
	      entry.kind ?? ""
	    )));
	    return withApplicationCacheDetail(
	      `${topicIds.size} 个主题 · ${history.length - chronicle.length} 条浏览记录 · ${chronicle.length} 条 404 记录 · ${formatBytes(new TextEncoder().encode(JSON.stringify(history)).byteLength)} · 本机保存`,
	      application
	    );
	  }
	  if (id === "topics") {
	    const snapshots = records.filter((record) => record.id.includes("|snapshot:topic:")).length;
	    return withApplicationCacheDetail(`${taggedValues(records, "topic:").size} 个主题 · 已缓存 ${taggedValues(records, "post:").size} 个楼层 · 快照 ${snapshots} / 接口 ${Math.max(0, records.length - snapshots)} · 共 ${records.length} 条记录 · ${formatBytes(bytes)} · 本机保存`, application);
	  }
	  if (id === "users") {
	    const users = new Set(taggedValues(records, "user:"));
	    for (const record of records) {
	      const username = identityValue(record, "username");
	      username && users.add(username.toLocaleLowerCase());
	    }
	    return withApplicationCacheDetail(
	      `${users.size} 个用户 · 共 ${records.length} 条记录 · ${formatBytes(bytes)} · 本机保存`,
	      application
	    );
	  }
	  if (id === "notifications") {
	    const pages = records.filter((record) => record.kind === "discourse-notification-page").length;
	    return withApplicationCacheDetail(
	      `分页 ${pages} / 回复展开 ${Math.max(0, records.length - pages)} · 共 ${records.length} 条记录 · ${formatBytes(bytes)} · 本机保存`,
	      application
	    );
	  }
	  if (id === "responses") {
	    let bookmarks = 0, reactions = 0;
	    for (const record of records) {
	      const collection = identityValue(record, "collection");
	      collection === "bookmarks" ? bookmarks += 1 : collection.includes("reaction") && (reactions += 1);
	    }
	    const other = Math.max(0, records.length - bookmarks - reactions);
	    return withApplicationCacheDetail(
	      `收藏 ${bookmarks} / 回应 ${reactions} / 其他 ${other} · 共 ${records.length} 条响应 · ${formatBytes(bytes)} · 本机保存`,
	      application
	    );
	  }
	  const groups = assetCaches?.groups.map((group) => `${group.label} ${group.count} 个(${formatBytes(group.bytes)})`) ?? [], legacyCount = assetCaches?.count ?? 0, legacyBytes = assetCaches?.bytes ?? 0;
	  return withApplicationCacheDetail([
	    ...groups,
	    `接口图片 ${records.length} 个(${formatBytes(bytes)})`,
	    `共 ${legacyCount + records.length} 个资源 · ${formatBytes(legacyBytes + bytes)} · 本机保存`
	  ].join(" · "), application);
	}
	class ReaderCacheManagementSurface {
	  scope;
	  #options;
	  #root;
	  #selects = /* @__PURE__ */ new Map();
	  #stats = /* @__PURE__ */ new Map();
	  #clear;
	  #refreshCurrent;
	  #configActions;
	  #configFile;
	  #configStatus;
	  #status;
	  #refreshToken = 0;
	  #busy = !1;
	  constructor(options) {
	    this.#options = options, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const document = options.document;
	    if (this.#root = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-category-groups"
	    ), options.configuration) {
	      const configSection = (0, import_reader_settings_dom.settingsSection)(
	        document,
	        "导入与导出设置",
	        "配置文件包含当前偏好(性能项仅保存目标值)、其他适用站点、翻译规则与 WebDAV 非敏感选项;导入后仍按当前设备、网络与 429 状态自适应。生效策略、请求与性能记录、阅读队列、浏览历史、帖子内容、缓存和账号数据不会写入文件;翻译 API Key、WebDAV 用户名和密码始终排除。",
	        !0
	      );
	      configSection.dataset.settingCategory = "config-management";
	      const actions = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-config-actions"
	      ), exportButton = (0, import_reader_settings_dom.settingsButton)(
	        document,
	        "ldp-config-action ldp-config-export",
	        "",
	        "download",
	        "导出设置"
	      ), importButton = (0, import_reader_settings_dom.settingsButton)(
	        document,
	        "ldp-config-action ldp-config-import",
	        "",
	        "upload",
	        "导入设置"
	      ), resetButton = (0, import_reader_settings_dom.settingsButton)(
	        document,
	        "ldp-config-action ldp-config-reset danger",
	        "",
	        "rotate-ccw",
	        "恢复全部默认"
	      );
	      actions.append(exportButton, importButton, resetButton);
	      const file = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "input",
	        "ldp-config-file"
	      );
	      file.type = "file", file.accept = "application/json,.json", file.hidden = !0;
	      const status = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "small",
	        "ldp-cache-note ldp-config-status"
	      );
	      status.role = "status", status.setAttribute("aria-live", "polite");
	      const body = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-config-body"
	      );
	      body.append(actions, file, status), configSection.append(body), this.#root.append(configSection), this.#configActions = Object.freeze([
	        exportButton,
	        importButton,
	        resetButton
	      ]), this.#configFile = file, this.#configStatus = status, this.scope.listen(
	        exportButton,
	        "click",
	        () => void this.#exportConfiguration()
	      ), this.scope.listen(importButton, "click", () => {
	        file.value = "", file.click();
	      }), this.scope.listen(
	        file,
	        "change",
	        () => void this.#importConfiguration()
	      ), this.scope.listen(
	        resetButton,
	        "click",
	        () => void this.#resetConfiguration()
	      );
	    } else
	      this.#configActions = Object.freeze([]), this.#configFile = null, this.#configStatus = null;
	    const section = (0, import_reader_settings_dom.settingsSection)(
	      document,
	      "本地缓存",
	      "阅读器会在本机保存浏览历史、帖子与楼层、用户资料、通知、收藏和图片等可复用数据。清理后不会影响站点账号内容,需要时会重新联网获取。",
	      !0
	    );
	    section.dataset.settingCategory = "local-cache";
	    const content = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-category-content"
	    ), list = (0, import_reader_settings_dom.settingsElement)(document, "div", "ldp-cache-list");
	    for (const definition of CATEGORIES) {
	      const row = (0, import_reader_settings_dom.settingsElement)(document, "label", "ldp-cache-row");
	      row.dataset.settingHelp = definition.help;
	      const input = (0, import_reader_settings_dom.settingsElement)(document, "input", "ldp-cache-select");
	      input.type = "checkbox", input.value = definition.id;
	      const copy = (0, import_reader_settings_dom.settingsCopy)(
	        document,
	        "",
	        definition.title
	      ), stat = copy.querySelector("small") ?? (0, import_reader_settings_dom.settingsElement)(document, "small");
	      stat.dataset.cacheSize = definition.id, stat.parentElement || copy.append(stat);
	      const retention = (0, import_reader_settings_dom.settingsElement)(document, "em");
	      retention.dataset.cacheRetention = definition.id, retention.textContent = definition.retention, row.append(input, copy, retention), list.append(row), this.#selects.set(definition.id, input), this.#stats.set(definition.id, stat), this.scope.listen(input, "change", () => this.#syncButtons());
	    }
	    this.#clear = (0, import_reader_settings_dom.settingsButton)(
	      document,
	      "ldp-cache-clear",
	      "",
	      "trash",
	      "清理已选缓存"
	    ), this.#clear.disabled = !0, this.#clear.dataset.settingHelp = "只清理上面已经勾选的缓存类型。不会退出登录,也不会删除站点上的帖子、消息或图片;清理后需要时会重新联网获取。", this.#status = (0, import_reader_settings_dom.settingsElement)(document, "small", "ldp-cache-note"), this.#status.role = "status", this.#status.setAttribute("aria-live", "polite"), content.append(list, this.#clear, this.#status), section.append(content), this.#root.append(section), options.host.append(this.#root), this.scope.add(() => this.#root.remove()), this.scope.listen(this.#clear, "click", () => void this.#clearSelected()), options.headerActions ? (this.#refreshCurrent = (0, import_reader_settings_dom.settingsButton)(
	      document,
	      "ldp-reader-refresh ldp-icon-btn",
	      "清除当前帖子缓存并刷新",
	      "rotate-ccw"
	    ), options.headerActions.append(this.#refreshCurrent), this.scope.add(() => this.#refreshCurrent?.remove()), this.scope.listen(
	      this.#refreshCurrent,
	      "click",
	      () => void this.#clearCurrent()
	    )) : this.#refreshCurrent = null, this.sync(), this.refresh();
	  }
	  sync() {
	    this.#refreshCurrent && (this.#refreshCurrent.disabled = this.#busy || !this.#options.currentTopicAvailable());
	  }
	  async refresh() {
	    const token = ++this.#refreshToken, [recordsResult, assetCachesResult, applicationCachesResult] = await Promise.allSettled([
	      this.#options.responses.records(),
	      Promise.resolve(this.#options.assetCaches?.stats() ?? null),
	      Promise.resolve(this.#options.applicationCaches?.stats() ?? null)
	    ]);
	    if (token !== this.#refreshToken || this.scope.destroyed) return !1;
	    const incomplete = /* @__PURE__ */ new Set(), responseCategories = CATEGORIES.filter(
	      ({ id }) => id !== "history"
	    ).map(({ id }) => id), records = recordsResult.status === "fulfilled" ? recordsResult.value : Object.freeze([]);
	    if (recordsResult.status === "rejected") {
	      this.#options.onError?.(recordsResult.reason);
	      for (const category of responseCategories) incomplete.add(category);
	    }
	    const assetCaches = assetCachesResult.status === "fulfilled" ? assetCachesResult.value : null;
	    assetCachesResult.status === "rejected" && (this.#options.onError?.(assetCachesResult.reason), incomplete.add("assets")), assetCaches?.errors.length && incomplete.add("assets");
	    const applicationCaches = applicationCachesResult.status === "fulfilled" ? applicationCachesResult.value : null;
	    if (applicationCachesResult.status === "rejected") {
	      this.#options.onError?.(applicationCachesResult.reason);
	      for (const category of responseCategories) incomplete.add(category);
	    }
	    const grouped = new Map(
	      CATEGORIES.map(({ id }) => [id, []])
	    );
	    let history = Object.freeze([]);
	    try {
	      history = Object.freeze([
	        ...this.#options.history.snapshot.entries,
	        ...this.#options.chronicle?.snapshot.records ?? []
	      ]);
	    } catch (cause) {
	      this.#options.onError?.(cause), incomplete.add("history");
	    }
	    for (const record of records) {
	      const category = categoryOf(record);
	      category && grouped.get(category).push(record);
	    }
	    const assetTarget = this.#stats.get("assets");
	    assetTarget && (assetCaches ? assetTarget.dataset.ldpTooltipLabel = assetCacheDetail(assetCaches) : delete assetTarget.dataset.ldpTooltipLabel);
	    for (const { id } of CATEGORIES) {
	      const target = this.#stats.get(id);
	      if (target) {
	        const detail = categoryStatText(
	          id,
	          grouped.get(id) ?? [],
	          history,
	          assetCaches,
	          applicationCaches?.categories[id]
	        );
	        target.textContent = incomplete.has(id) ? `${detail} · 统计不完整` : detail;
	      }
	    }
	    const managedResponseRecords = [...grouped.values()].reduce(
	      (total2, categoryRecords) => total2 + categoryRecords.length,
	      0
	    ), total = history.length + managedResponseRecords + (assetCaches?.count ?? 0) + Object.values(applicationCaches?.categories ?? {}).reduce(
	      (sum, category) => sum + Math.max(0, Number(category?.records) || 0),
	      0
	    );
	    if (incomplete.size) {
	      const labels = CATEGORIES.filter(({ id }) => incomplete.has(id)).map(({ title }) => title);
	      return this.#status.textContent = `共 ${total} 条已知本地记录;统计不完整:${labels.join("、")}`, !1;
	    }
	    return this.#status.textContent = `共 ${total} 条本地记录`, !0;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #clearSelected() {
	    if (this.#busy) return;
	    const selected = new Set(
	      [...this.#selects].filter(([, input]) => input.checked).map(([id]) => id)
	    );
	    if (!selected.size) return;
	    this.#setBusy(!0, "等待确认清理已选缓存…");
	    let releasePreparation = () => {
	    };
	    try {
	      if (this.#options.configuration) {
	        const labels = CATEGORIES.filter(({ id }) => selected.has(id)).map(({ title }) => title);
	        if (!await this.#options.configuration.confirm({
	          title: "清理所选本地缓存?",
	          message: `将清理:${labels.join("、")}。`,
	          note: "只删除本机缓存,不删除站点账号或 WebDAV 远端数据;已同步记录可能在后续同步时重新合并回来。需要的数据将按需联网获取。",
	          confirmLabel: "清理已选缓存",
	          tone: "danger",
	          icon: "trash"
	        })) {
	          this.#status.textContent = "已取消缓存清理。";
	          return;
	        }
	      }
	      this.#status.textContent = "正在清理已选缓存…";
	      const failures = /* @__PURE__ */ new Map(), fail = (categories, message, cause) => {
	        for (const category of categories) {
	          const messages = failures.get(category) ?? [];
	          messages.push(message), failures.set(category, messages);
	        }
	        cause !== void 0 && this.#options.onError?.(cause);
	      }, clearing = new Set(selected);
	      if (this.#options.prepareClear)
	        try {
	          const prepared = await this.#options.prepareClear([...selected]);
	          releasePreparation = prepared.release ?? releasePreparation;
	          for (const category of prepared.failed)
	            selected.has(category) && fail([category], "同步基线未能安全解除");
	        } catch (cause) {
	          fail([...selected], "缓存清理准备失败", cause);
	        }
	      for (const category of failures.keys()) clearing.delete(category);
	      if (clearing.has("assets") && this.#options.assetCaches)
	        try {
	          const result = await this.#options.assetCaches.clear();
	          result.failed.length && fail(
	            ["assets"],
	            `兼容图片缓存 ${result.failed.join(",")} 未清理`
	          );
	        } catch (cause) {
	          fail(["assets"], "兼容图片缓存未清理", cause);
	        }
	      const responseCategories = [...clearing].filter(
	        (id) => id !== "history"
	      );
	      if (responseCategories.length)
	        try {
	          const ids = (await this.#options.responses.records()).filter((record) => {
	            const category = categoryOf(record);
	            return category !== null && clearing.has(category);
	          }).map((record) => record.id);
	          if (ids.length) {
	            let report = null;
	            if (this.#options.responses.invalidateWithReport ? report = await this.#options.responses.invalidateWithReport({ ids }) : await this.#options.responses.invalidate({ ids }), report && !report.complete) {
	              for (const failure of report.failures)
	                this.#options.onError?.(failure.cause);
	              fail(responseCategories, "统一响应缓存未完整失效");
	            }
	          }
	        } catch (cause) {
	          fail(responseCategories, "统一响应缓存未清理", cause);
	        }
	      if (clearing.size && this.#options.applicationCaches)
	        try {
	          const result = await this.#options.applicationCaches.clear([...clearing]);
	          for (const category of result.failed)
	            clearing.has(category) && fail([category], "应用内存热缓存未清理");
	        } catch (cause) {
	          fail([...clearing], "应用内存热缓存未清理", cause);
	        }
	      if (clearing.has("history"))
	        try {
	          this.#options.history.clear(), this.#options.chronicle?.clear();
	        } catch (cause) {
	          fail(["history"], "浏览历史或岁月史书未清理", cause);
	        }
	      if (clearing.has("assets"))
	        try {
	          this.#options.clearImageObjectUrls?.();
	        } catch (cause) {
	          fail(["assets"], "当前图片对象未释放", cause);
	        }
	      for (const [id, input] of this.#selects)
	        selected.has(id) && !failures.has(id) && (input.checked = !1);
	      releasePreparation(), releasePreparation = () => {
	      };
	      const refreshed = await this.refresh();
	      if (failures.size) {
	        const labels = CATEGORIES.filter(({ id }) => failures.has(id)).map(({ title }) => title);
	        this.#options.notify?.("部分本地缓存未能清理"), this.#status.textContent = `部分缓存已清理;未完成:${labels.join("、")}。已保留勾选,可重试。`;
	      } else
	        this.#options.notify?.("已清理所选本地缓存"), this.#status.textContent = refreshed ? "清理完成;需要的数据会按需重新获取。" : "清理已完成,但最新缓存统计读取失败;可稍后重新打开数据管理确认。";
	    } catch (cause) {
	      this.#options.onError?.(cause), this.#status.textContent = "清理失败,请稍后重试。";
	    } finally {
	      try {
	        releasePreparation();
	      } catch (cause) {
	        this.#options.onError?.(cause);
	      }
	      this.#setBusy(!1);
	    }
	  }
	  async #clearCurrent() {
	    if (!(this.#busy || !this.#options.currentTopicAvailable())) {
	      this.#setBusy(!0, "正在重建当前主题缓存…"), this.#refreshCurrent?.classList.add("is-refreshing");
	      try {
	        const result = await this.#options.clearCurrentTopic(), refreshed = await this.refresh();
	        if (result?.restored) {
	          const message = result.message ?? "当前主题刷新失败,已恢复刷新前内容;可稍后重试。";
	          this.#options.notify?.(message), this.#status.textContent = message;
	          return;
	        }
	        if (result && !result.complete) {
	          this.#options.notify?.(
	            "当前主题已重新获取,但部分旧缓存未能清理"
	          ), this.#status.textContent = result.message ?? "当前主题已从原站重新获取,但部分旧缓存未能清理;可再次重试。";
	          return;
	        }
	        this.#options.notify?.("当前主题缓存已清理并重新获取"), this.#status.textContent = refreshed ? "当前主题已从原站重新获取。" : "当前主题已从原站重新获取,但最新缓存统计读取失败。";
	      } catch (cause) {
	        this.#options.onError?.(cause), this.#status.textContent = "当前主题重建失败,原缓存不会作为恢复前提。";
	      } finally {
	        this.#refreshCurrent?.classList.remove("is-refreshing"), this.#setBusy(!1);
	      }
	    }
	  }
	  async #exportConfiguration() {
	    const configuration = this.#options.configuration;
	    if (!(!configuration || this.#busy)) {
	      this.#setBusy(!0, "正在导出设置…"), this.#setConfigStatus("正在导出设置…");
	      try {
	        const payload = await configuration.export(), filename = configuration.filename?.(payload) ?? `awesome-linuxdo-reader-settings-${payload.exportedAt.slice(0, 10)}.json`;
	        await configuration.saveTextFile(
	          `${JSON.stringify(payload, null, 2)}
`,
	          filename
	        ), this.#setConfigStatus(
	          `已导出 ${payload.settingsCount} 项偏好及安全扩展配置;性能项为目标值,不含运行时策略与日志。`
	        ), this.#options.notify?.("设置配置已导出");
	      } catch (cause) {
	        this.#options.onError?.(cause), this.#setConfigStatus("导出失败,请稍后重试。");
	      } finally {
	        this.#setBusy(!1);
	      }
	    }
	  }
	  async #importConfiguration() {
	    const configuration = this.#options.configuration, file = this.#configFile?.files?.[0];
	    if (!(!configuration || !file || this.#busy)) {
	      this.#setBusy(!0);
	      try {
	        let imported;
	        try {
	          imported = configuration.prepare(
	            JSON.parse(await file.text())
	          );
	        } catch (cause) {
	          this.#options.onError?.(cause), this.#setConfigStatus(
	            "配置文件无效或版本不匹配,请选择本阅读器导出的 JSON 文件。"
	          );
	          return;
	        }
	        if (!await configuration.confirm({
	          title: "导入这份设置配置?",
	          message: `将使用“${file.name}”覆盖当前阅读器设置。`,
	          note: imported.includesPortableSections ? "性能目标会立即应用,并继续按当前设备、网络与 429 状态自适应;生效策略和请求/性能记录不会从文件导入。API Key、WebDAV 用户名和密码不会从文件导入;仅复用本机同地址已有凭据,新 WebDAV 地址会关闭定时同步。浏览历史、阅读队列和缓存不会改变。" : "这是旧版配置,只覆盖阅读器偏好(含性能目标);性能目标会立即应用并继续自适应,生效策略和请求/性能记录不会从文件导入。其他适用站点、翻译和 WebDAV 设置保持不变。浏览历史、阅读队列和缓存不会改变。",
	          confirmLabel: "导入设置",
	          tone: "primary",
	          icon: "upload"
	        }) || this.scope.destroyed) return;
	        this.#status.textContent = "正在导入设置…", this.#setConfigStatus("正在导入设置…");
	        try {
	          const result = await configuration.apply(imported), skipped = result.skippedSections.length ? `;当前环境跳过 ${result.skippedSections.join("、")}` : "", autoSync = result.webDavAutoSyncDisabled ? ";WebDAV 缺少本机同地址凭据,定时同步已关闭" : "";
	          this.#setConfigStatus(
	            `导入完成,已应用 ${result.settingsCount} 项偏好(性能项为目标值)${skipped}${autoSync}。`
	          ), this.#options.notify?.("设置配置已导入");
	        } catch (cause) {
	          this.#options.onError?.(cause), this.#setConfigStatus(
	            cause instanceof Error && cause.message.includes("回滚不完整") ? "导入失败,部分设置可能未恢复;请重新打开设置逐项核对。" : "导入失败,当前设置保持不变。"
	          );
	        }
	      } catch (cause) {
	        this.#options.onError?.(cause), this.#setConfigStatus("无法确认导入,当前设置保持不变。");
	      } finally {
	        this.#setBusy(!1);
	      }
	    }
	  }
	  async #resetConfiguration() {
	    const configuration = this.#options.configuration;
	    if (!(!configuration || this.#busy)) {
	      this.#setBusy(!0);
	      try {
	        if (!await configuration.confirm({
	          title: "恢复全部默认设置?",
	          message: "当前偏好(含性能目标)、其他适用站点、翻译和 WebDAV 设置都会恢复默认。",
	          note: "性能项恢复推荐目标,运行时仍会自适应;请求/性能记录不会被删除。翻译 API Key、WebDAV 用户名和密码会从本机设置中清除;阅读队列图标位置会恢复默认,队列条目、浏览历史、帖子缓存和账号数据不会被删除。",
	          confirmLabel: "恢复全部默认",
	          tone: "danger",
	          icon: "rotate-ccw"
	        }) || this.scope.destroyed) return;
	        this.#status.textContent = "正在恢复默认设置…", this.#setConfigStatus("正在恢复默认设置…"), await configuration.reset(), this.#setConfigStatus(
	          "全部设置已恢复默认;性能项将继续按运行环境自适应。"
	        ), this.#options.notify?.("全部设置已恢复默认");
	      } catch (cause) {
	        this.#options.onError?.(cause), this.#setConfigStatus(
	          cause instanceof Error && cause.message.includes("回滚不完整") ? "恢复失败,部分设置可能未恢复;请重新打开设置逐项核对。" : "恢复失败,当前设置保持不变。"
	        );
	      } finally {
	        this.#setBusy(!1);
	      }
	    }
	  }
	  #setBusy(busy, status = "") {
	    this.#busy = busy;
	    for (const action of this.#configActions) action.disabled = busy;
	    this.#configFile && (this.#configFile.disabled = busy), this.#clear.disabled = busy || ![...this.#selects.values()].some((input) => input.checked), status && (this.#status.textContent = status), this.sync();
	  }
	  #syncButtons() {
	    this.#clear.disabled = this.#busy || ![...this.#selects.values()].some((input) => input.checked);
	  }
	  #setConfigStatus(message) {
	    this.#configStatus && (this.#configStatus.textContent = message);
	  }
	}
}, "495aff91d326a0cfaa60ffdc9717b3443671fdf4a8c740b1dabcb75ba0c43caa");

/* Source: lite/src/cache/reader-collection-page-repository.ts */
runtime.register("src/cache/reader-collection-page-repository.js", function(module, exports, require) {
	var reader_collection_page_repository_exports = {};
	__export(reader_collection_page_repository_exports, {
	  ReaderCollectionPageRepository: () => ReaderCollectionPageRepository
	});
	module.exports = __toCommonJS(reader_collection_page_repository_exports);
	function requiredToken(value, name) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error(`${name} 不能为空`);
	  return encodeURIComponent(normalized);
	}
	function safeInteger(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric >= 0 ? numeric : null;
	}
	function positiveSafeInteger(value) {
	  const numeric = safeInteger(value);
	  return numeric !== null && numeric > 0 ? numeric : null;
	}
	function yieldMainThread() {
	  return new Promise((resolve) => setTimeout(resolve, 0));
	}
	class ReaderCollectionPageRepository {
	  #responses;
	  #scope;
	  #namespace;
	  #kind;
	  #tags;
	  #normalizeRecord;
	  #sortRecords;
	  #mergeRecord;
	  #pageSize;
	  #retainForMs;
	  #permanent;
	  #coordination;
	  #writes = /* @__PURE__ */ new Map();
	  #generationNonce = Math.random().toString(36).slice(2);
	  #generation = 0;
	  constructor(options) {
	    if (this.#responses = options.responses, this.#scope = requiredToken(options.authScope, "集合投影 authScope"), this.#namespace = requiredToken(options.namespace, "集合投影 namespace"), this.#kind = String(options.kind).trim(), !this.#kind) throw new Error("集合投影 kind 不能为空");
	    if (this.#tags = Object.freeze([...new Set(options.tags.map(String).map((tag) => tag.trim()).filter(Boolean))]), this.#normalizeRecord = options.normalizeRecord, this.#sortRecords = options.sortRecords, this.#mergeRecord = options.mergeRecord ?? ((_stored, incoming) => incoming), this.#pageSize = Math.floor(Number(options.pageSize ?? 60)), !Number.isSafeInteger(this.#pageSize) || this.#pageSize < 1)
	      throw new RangeError("集合投影 pageSize 必须是正安全整数");
	    if (this.#retainForMs = Number(
	      options.retainForMs ?? 15552e6
	    ), !Number.isFinite(this.#retainForMs) || this.#retainForMs <= 0)
	      throw new RangeError("集合投影 retainForMs 必须是正有限数值");
	    this.#permanent = options.permanent === !0, this.#coordination = options.coordination;
	  }
	  async read(partitionValue, options = {}) {
	    const partition = requiredToken(partitionValue, "集合投影 partition"), manifestPolicy = this.#policy(partition, "manifest");
	    options.fresh && this.#responses.forgetMemory({ ids: [manifestPolicy.id] });
	    const cached = await this.#responses.read(
	      manifestPolicy
	    ), manifest = this.#manifest(cached.value, partition);
	    if (!manifest) return null;
	    const records = [], identities = /* @__PURE__ */ new Set();
	    for (let start = 0; start < manifest.pages; start += 6) {
	      const pages = await Promise.all(Array.from(
	        { length: Math.min(6, manifest.pages - start) },
	        (_, offset) => this.#readPage(
	          manifest,
	          partition,
	          start + offset,
	          options.fresh === !0
	        )
	      ));
	      for (const page of pages) {
	        if (!page) return null;
	        for (const record of page.records) {
	          if (identities.has(record.identity)) return null;
	          identities.add(record.identity), records.push(record);
	        }
	      }
	      start + 6 < manifest.pages && await yieldMainThread();
	    }
	    return records.length !== manifest.total ? null : Object.freeze({
	      records: Object.freeze([...this.#sortRecords(records)]),
	      totalHint: Math.max(manifest.total, manifest.totalHint),
	      complete: manifest.complete,
	      updatedAt: manifest.updatedAt,
	      ...manifest.sourceNextPage === void 0 ? {} : { sourceNextPage: manifest.sourceNextPage },
	      ...manifest.sourcePageSize === void 0 ? {} : { sourcePageSize: manifest.sourcePageSize },
	      ...manifest.sourceOffset === void 0 ? {} : { sourceOffset: manifest.sourceOffset }
	    });
	  }
	  write(partitionValue, records, options = {}) {
	    const partition = requiredToken(partitionValue, "集合投影 partition"), queued = (this.#writes.get(partition) ?? Promise.resolve()).catch(() => {
	    }).then(() => this.#withWriteLease(
	      partition,
	      () => this.#commit(partition, records, options)
	    ));
	    return this.#writes.set(partition, queued), queued.finally(() => {
	      this.#writes.get(partition) === queued && this.#writes.delete(partition);
	    }).catch(() => {
	    }), queued;
	  }
	  async #commit(partition, incoming, options) {
	    const manifestPolicy = this.#policy(partition, "manifest");
	    this.#responses.forgetMemory({ ids: [manifestPolicy.id] });
	    const previousRead = await this.#responses.read(
	      manifestPolicy
	    ), previousManifest = this.#manifest(previousRead.value, partition), previous = options.mergeStored === !1 ? null : await this.read(decodeURIComponent(partition), { fresh: !0 }), merged = /* @__PURE__ */ new Map();
	    for (const record of previous?.records ?? [])
	      merged.set(record.identity, record);
	    for (const value of incoming) {
	      const record = this.#normalizeRecord(value);
	      if (!record?.identity) continue;
	      const stored = merged.get(record.identity);
	      merged.set(
	        record.identity,
	        stored ? this.#mergeRecord(stored, record) : record
	      );
	    }
	    const records = Object.freeze([...this.#sortRecords([...merged.values()])]), updatedAt = Math.max(
	      0,
	      Math.floor(Number(options.updatedAt ?? Date.now()) || 0)
	    ), replaceCheckpoint = options.checkpointMode === "replace", requestedSourceNextPageValue = options.sourceNextPage === void 0 ? previousManifest?.sourceNextPage : safeInteger(options.sourceNextPage);
	    if (requestedSourceNextPageValue === null)
	      throw new RangeError("集合投影 sourceNextPage 必须是非负安全整数");
	    const requestedSourceNextPage = replaceCheckpoint ? requestedSourceNextPageValue : previousManifest?.sourceNextPage === void 0 && requestedSourceNextPageValue === void 0 ? void 0 : Math.max(
	      previousManifest?.sourceNextPage ?? 0,
	      requestedSourceNextPageValue ?? 0
	    ), requestedSourcePageSize = options.sourcePageSize === void 0 ? previousManifest?.sourcePageSize : positiveSafeInteger(options.sourcePageSize);
	    if (requestedSourcePageSize === null)
	      throw new RangeError("集合投影 sourcePageSize 必须是正安全整数");
	    const requestedSourceOffsetValue = options.sourceOffset === void 0 ? previousManifest?.sourceOffset : safeInteger(options.sourceOffset);
	    if (requestedSourceOffsetValue === null)
	      throw new RangeError("集合投影 sourceOffset 必须是非负安全整数");
	    const previousSourceNextPage = previousManifest?.sourceNextPage, previousSourceOffset = previousManifest?.sourceOffset;
	    let requestedSourceOffset;
	    replaceCheckpoint ? requestedSourceOffset = requestedSourceOffsetValue : options.sourceOffset === void 0 ? requestedSourceOffset = previousSourceOffset : previousSourceOffset === void 0 ? requestedSourceOffset = requestedSourceOffsetValue : requestedSourceNextPageValue !== void 0 && previousSourceNextPage !== void 0 && requestedSourceNextPageValue !== previousSourceNextPage ? requestedSourceOffset = requestedSourceNextPageValue > previousSourceNextPage ? requestedSourceOffsetValue : previousSourceOffset : requestedSourceOffset = options.sourceOffsetOrder === "descending" ? Math.min(previousSourceOffset, requestedSourceOffsetValue ?? 0) : Math.max(previousSourceOffset, requestedSourceOffsetValue ?? 0);
	    const generation = `${updatedAt.toString(36)}-${this.#generationNonce}-${(++this.#generation).toString(36)}`, pages = Math.ceil(records.length / this.#pageSize);
	    for (let start = 0; start < pages; start += 6)
	      await Promise.all(Array.from(
	        { length: Math.min(6, pages - start) },
	        (_, offset) => {
	          const page = start + offset;
	          return this.#responses.write(
	            this.#policy(partition, "page", page, generation),
	            Object.freeze({
	              schemaVersion: 1,
	              generation,
	              page,
	              records: Object.freeze(records.slice(
	                page * this.#pageSize,
	                (page + 1) * this.#pageSize
	              ))
	            }),
	            { publish: !1 }
	          );
	        }
	      )), start + 6 < pages && await yieldMainThread();
	    await this.#responses.write(
	      this.#policy(partition, "manifest"),
	      Object.freeze({
	        schemaVersion: 1,
	        partition,
	        generation,
	        pageSize: this.#pageSize,
	        total: records.length,
	        totalHint: Math.max(
	          records.length,
	          Math.floor(Number(options.totalHint) || 0)
	        ),
	        pages,
	        complete: options.checkpointMode === "advance" && previousManifest?.complete === !0 || options.complete === !0,
	        updatedAt,
	        ...requestedSourceNextPage === void 0 ? {} : { sourceNextPage: requestedSourceNextPage },
	        ...requestedSourcePageSize === void 0 ? {} : { sourcePageSize: requestedSourcePageSize },
	        ...requestedSourceOffset === void 0 ? {} : { sourceOffset: requestedSourceOffset }
	      })
	    ), previousManifest && previousManifest.generation !== generation && previousManifest.pages > 0 && await this.#responses.prune({
	      ids: Object.freeze(Array.from(
	        { length: previousManifest.pages },
	        (_, page) => this.#policy(
	          partition,
	          "page",
	          page,
	          previousManifest.generation
	        ).id
	      ))
	    });
	  }
	  #manifest(value, partition) {
	    if (!value || typeof value != "object" || Array.isArray(value)) return null;
	    const source = value, total = safeInteger(source.total), totalHint = safeInteger(source.totalHint), pages = safeInteger(source.pages), updatedAt = safeInteger(source.updatedAt), sourceNextPage = source.sourceNextPage === void 0 ? void 0 : safeInteger(source.sourceNextPage), sourcePageSize = source.sourcePageSize === void 0 ? void 0 : positiveSafeInteger(source.sourcePageSize), sourceOffset = source.sourceOffset === void 0 ? void 0 : safeInteger(source.sourceOffset);
	    return source.schemaVersion !== 1 || source.partition !== partition || typeof source.generation != "string" || !source.generation || source.pageSize !== this.#pageSize || total === null || totalHint === null || pages === null || updatedAt === null || sourceNextPage === null || sourcePageSize === null || sourceOffset === null || typeof source.complete != "boolean" || pages !== Math.ceil(total / this.#pageSize) ? null : Object.freeze({
	      schemaVersion: 1,
	      partition,
	      generation: source.generation,
	      pageSize: this.#pageSize,
	      total,
	      totalHint: Math.max(total, totalHint),
	      pages,
	      complete: source.complete,
	      updatedAt,
	      ...sourceNextPage === void 0 ? {} : { sourceNextPage },
	      ...sourcePageSize === void 0 ? {} : { sourcePageSize },
	      ...sourceOffset === void 0 ? {} : { sourceOffset }
	    });
	  }
	  async #withWriteLease(partition, operation) {
	    const coordination = this.#coordination;
	    if (!coordination) {
	      await operation();
	      return;
	    }
	    const token = `reader-collection-projection-write:v1:${this.#policy(partition, "manifest").id}`;
	    for (; ; ) {
	      const lease = await coordination.acquireFlight(token);
	      if (!lease.producer) {
	        await coordination.waitForFlight(token);
	        continue;
	      }
	      const heartbeat = lease.coordinated ? setInterval(() => {
	        coordination.renewFlight(lease).catch(() => {
	        });
	      }, 1e4) : null;
	      try {
	        await operation();
	        return;
	      } finally {
	        heartbeat !== null && clearInterval(heartbeat), await coordination.releaseFlight(lease);
	      }
	    }
	  }
	  async #readPage(manifest, partition, page, fresh = !1) {
	    const policy = this.#policy(
	      partition,
	      "page",
	      page,
	      manifest.generation
	    );
	    fresh && this.#responses.forgetMemory({ ids: [policy.id] });
	    const value = (await this.#responses.read(
	      policy
	    )).value;
	    if (!value || value.schemaVersion !== 1 || value.generation !== manifest.generation || value.page !== page || !Array.isArray(value.records)) return null;
	    const records = [];
	    for (const candidate of value.records) {
	      const record = this.#normalizeRecord(candidate);
	      if (!record?.identity) return null;
	      records.push(record);
	    }
	    const expected = Math.min(
	      this.#pageSize,
	      Math.max(0, manifest.total - page * this.#pageSize)
	    );
	    return records.length !== expected ? null : Object.freeze({
	      schemaVersion: 1,
	      generation: manifest.generation,
	      page,
	      records: Object.freeze(records)
	    });
	  }
	  #policy(partition, part, page = 0, generation = "") {
	    const id = part === "manifest" ? `reader-collection-projection:${this.#namespace}:manifest:v1:${this.#scope}:${partition}` : `reader-collection-projection:${this.#namespace}:page:v1:${this.#scope}:${partition}:${encodeURIComponent(generation)}:${page}`;
	    return Object.freeze({
	      id,
	      kind: this.#kind,
	      tags: this.#tags,
	      freshForMs: this.#retainForMs,
	      retainForMs: this.#retainForMs,
	      persist: !0,
	      ...this.#permanent ? { permanent: !0 } : {}
	    });
	  }
	}
}, "2c54916ac80b81ab088a90cd4e2a7c65fb2e83b6fe617fd31092c7c64abe1452");

/* Source: lite/src/cache/response-repository.ts */
runtime.register("src/cache/response-repository.js", function(module, exports, require) {
	var response_repository_exports = {};
	__export(response_repository_exports, {
	  ResponseCacheFlightTimeoutError: () => ResponseCacheFlightTimeoutError,
	  ResponseCacheInvalidationError: () => ResponseCacheInvalidationError,
	  ResponseCacheSharedFlightFailureError: () => ResponseCacheSharedFlightFailureError,
	  ResponseRepository: () => ResponseRepository
	});
	module.exports = __toCommonJS(response_repository_exports);
	var import_cache_identity = require("./cache-identity.js");
	class ResponseCacheInvalidationError extends AggregateError {
	  report;
	  constructor(report) {
	    super(
	      report.failures.map((failure) => failure.cause),
	      "响应缓存失效未完整提交"
	    ), this.name = "ResponseCacheInvalidationError", this.report = report;
	  }
	}
	class ResponseCacheSharedFlightFailureError extends Error {
	  status;
	  cloudflareMitigated;
	  kind;
	  sharedFlight = !0;
	  constructor(failure) {
	    super(
	      failure.kind === "cloudflare" ? "同一在途请求已遇到 Cloudflare 验证" : "同一在途请求已收到 429"
	    ), this.name = "ResponseCacheSharedFlightFailureError", this.status = failure.status, this.cloudflareMitigated = failure.cloudflareMitigated, this.kind = failure.kind;
	  }
	}
	function sharedFlightFailure(error) {
	  if (!error || typeof error != "object") return null;
	  const candidate = error, status = Number(candidate.status ?? 0);
	  return candidate.cloudflareMitigated === !0 ? Object.freeze({
	    status: Number.isSafeInteger(status) && status >= 400 ? status : 403,
	    cloudflareMitigated: !0,
	    kind: "cloudflare"
	  }) : status === 429 ? Object.freeze({
	    status: 429,
	    cloudflareMitigated: !1,
	    kind: "rate-limit"
	  }) : null;
	}
	function nonNegativeFinite(value, name) {
	  if (!Number.isFinite(value) || value < 0)
	    throw new RangeError(`${name} 必须是非负有限数值`);
	  return value;
	}
	function positiveInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function normalizePolicy(policy) {
	  const id = String(policy.id).trim(), kind = String(policy.kind).trim();
	  if (!id) throw new Error("cache id 不能为空");
	  if (!kind) throw new Error("cache kind 不能为空");
	  const freshForMs = nonNegativeFinite(policy.freshForMs, "freshForMs"), retainForMs = nonNegativeFinite(policy.retainForMs, "retainForMs");
	  if (retainForMs < freshForMs) throw new RangeError("retainForMs 不能小于 freshForMs");
	  return Object.freeze({
	    id,
	    kind,
	    tags: Object.freeze(
	      [...new Set(policy.tags.map(String).map((tag) => tag.trim()).filter(Boolean))].sort()
	    ),
	    freshForMs,
	    retainForMs,
	    persist: policy.persist,
	    ...policy.permanent === !0 ? { permanent: !0 } : {}
	  });
	}
	function defaultEstimateBytes(value) {
	  if (value !== null && typeof value == "object" && (typeof Blob < "u" && value instanceof Blob || Object.prototype.toString.call(value) === "[object Blob]")) {
	    const size = Number(value.size);
	    if (Number.isFinite(size) && size >= 0) return size;
	  }
	  if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value.byteLength;
	  try {
	    return new TextEncoder().encode(JSON.stringify(value)).byteLength;
	  } catch {
	    return 0;
	  }
	}
	function matchesInvalidation(entry, query) {
	  return !!(query.all || query.ids?.includes(entry.id) || query.kinds?.includes(entry.kind) || query.tags?.some((tag) => entry.tags.includes(tag)));
	}
	class ResponseRepository {
	  #store;
	  #maxMemoryEntries;
	  #maxMemoryBytes;
	  #now;
	  #estimateBytes;
	  #mutationPort;
	  #flightPort;
	  #flightHeartbeatMs;
	  #flightWaitTimeoutMs;
	  #onPersistenceError;
	  #memory = /* @__PURE__ */ new Map();
	  #inflight = /* @__PURE__ */ new Map();
	  #writes = /* @__PURE__ */ new Map();
	  #epoch = 0;
	  constructor(options) {
	    this.#store = options.store, this.#maxMemoryEntries = positiveInteger(options.maxMemoryEntries, "maxMemoryEntries"), this.#maxMemoryBytes = positiveInteger(options.maxMemoryBytes, "maxMemoryBytes"), this.#now = options.now ?? Date.now, this.#estimateBytes = options.estimateBytes ?? defaultEstimateBytes, this.#mutationPort = options.mutationPort, this.#flightPort = options.flightPort, this.#flightHeartbeatMs = positiveInteger(
	      options.flightHeartbeatMs ?? 15e3,
	      "flightHeartbeatMs"
	    ), this.#flightWaitTimeoutMs = positiveInteger(
	      options.flightWaitTimeoutMs ?? 65e3,
	      "flightWaitTimeoutMs"
	    ), this.#onPersistenceError = options.onPersistenceError ?? (() => {
	    });
	  }
	  async read(rawPolicy) {
	    const policy = normalizePolicy(rawPolicy);
	    let entry = this.#memory.get(policy.id);
	    if (entry)
	      this.#memory.delete(policy.id), this.#memory.set(policy.id, entry);
	    else {
	      const epoch = this.#epoch;
	      try {
	        entry = await this.#store.read(policy.id) ?? void 0;
	      } catch (error) {
	        this.#onPersistenceError(error);
	      }
	      if (epoch !== this.#epoch)
	        entry = void 0;
	      else if (entry && this.#validEntry(entry, policy))
	        this.#remember(entry);
	      else {
	        if (entry)
	          try {
	            await this.#store.invalidate({ ids: [policy.id] });
	          } catch (error) {
	            this.#onPersistenceError(error);
	          }
	        entry = void 0;
	      }
	    }
	    if (!entry || !this.#validEntry(entry, policy))
	      return Object.freeze({ state: "miss" });
	    const age = Math.max(0, this.#now() - entry.storedAt);
	    return entry.permanent !== !0 && (age > policy.retainForMs || entry.expiresAt <= this.#now()) ? (await this.#invalidateWithReport({ ids: [policy.id] }, !0), Object.freeze({ state: "miss" })) : Object.freeze({
	      state: age <= policy.freshForMs ? "fresh" : "stale",
	      value: entry.value,
	      storedAt: entry.storedAt
	    });
	  }
	  /**
	   * 只从持久层读取,不接受当前标签页的 memory LRU 回退。
	   *
	   * 下载存档等用户明确保留的数据必须用它确认 IndexedDB 已真正提交;
	   * 普通响应仍使用 read() 的容错路径,持久层故障不会扩大成正文请求失败。
	   */
	  async readPersistent(rawPolicy) {
	    const policy = normalizePolicy(rawPolicy), pending = this.#writes.get(policy.id);
	    pending && await pending;
	    let entry;
	    try {
	      entry = await this.#store.read(policy.id);
	    } catch (error) {
	      throw this.#onPersistenceError(error), error;
	    }
	    if (!entry || !this.#validEntry(entry, policy))
	      return Object.freeze({ state: "miss" });
	    const age = Math.max(0, this.#now() - entry.storedAt);
	    return entry.permanent !== !0 && (age > policy.retainForMs || entry.expiresAt <= this.#now()) ? Object.freeze({ state: "miss" }) : Object.freeze({
	      state: age <= policy.freshForMs ? "fresh" : "stale",
	      value: entry.value,
	      storedAt: entry.storedAt
	    });
	  }
	  getOrLoad(rawPolicy, loader, options = {}) {
	    const policy = normalizePolicy(rawPolicy);
	    options.signal?.throwIfAborted();
	    const cacheMode = options.cacheMode ?? "default", requestKey = `${cacheMode}:${policy.id}`, existing = this.#inflight.get(requestKey);
	    if (existing && !existing.controller.signal.aborted)
	      return this.#join(existing, options.signal);
	    const controller = new AbortController(), inflight = {
	      promise: Promise.resolve(void 0),
	      policy,
	      controller,
	      consumers: /* @__PURE__ */ new Set(),
	      unabortableConsumer: !1,
	      settled: !1
	    }, promise = this.#load(
	      policy,
	      loader,
	      { ...options, signal: controller.signal },
	      cacheMode
	    ).finally(() => {
	      inflight.settled = !0;
	    });
	    inflight.promise = promise, this.#inflight.set(requestKey, inflight);
	    const clearInflight = () => {
	      this.#inflight.get(requestKey)?.promise === promise && this.#inflight.delete(requestKey);
	    };
	    return promise.then(clearInflight, clearInflight), this.#join(inflight, options.signal);
	  }
	  #join(inflight, signal) {
	    if (!signal)
	      return inflight.unabortableConsumer = !0, inflight.promise;
	    if (signal.aborted) return Promise.reject(signal.reason);
	    const consumer = Symbol("cache-consumer");
	    return inflight.consumers.add(consumer), new Promise((resolve, reject) => {
	      let settled = !1;
	      const finish = (value) => {
	        settled || (settled = !0, signal.removeEventListener("abort", onAbort), inflight.consumers.delete(consumer), resolve(value));
	      }, fail = (cause) => {
	        settled || (settled = !0, signal.removeEventListener("abort", onAbort), inflight.consumers.delete(consumer), reject(cause));
	      }, onAbort = () => {
	        fail(signal.reason), !inflight.settled && !inflight.unabortableConsumer && inflight.consumers.size === 0 && !inflight.controller.signal.aborted && inflight.controller.abort(signal.reason);
	      };
	      signal.addEventListener("abort", onAbort, { once: !0 }), inflight.promise.then(finish, fail);
	    });
	  }
	  async write(rawPolicy, value, options = {}) {
	    const policy = normalizePolicy(rawPolicy), storedAt = Math.max(
	      this.#now(),
	      (this.#memory.get(policy.id)?.storedAt ?? -1) + 1
	    ), entry = Object.freeze({
	      schemaVersion: 1,
	      id: policy.id,
	      kind: policy.kind,
	      tags: policy.tags,
	      storedAt,
	      expiresAt: policy.permanent === !0 ? Number.MAX_SAFE_INTEGER : storedAt + policy.retainForMs,
	      bytes: Math.max(0, this.#estimateBytes(value)),
	      value,
	      ...policy.permanent === !0 ? { permanent: !0 } : {}
	    });
	    if (this.#remember(entry), !policy.persist) return;
	    const previous = this.#writes.get(policy.id) ?? Promise.resolve();
	    let persisted = !1;
	    const write = previous.catch(() => {
	    }).then(async () => {
	      await this.#store.write(entry), persisted = !0;
	    }).catch((error) => {
	      this.#onPersistenceError(error);
	    });
	    if (this.#writes.set(policy.id, write), await write, persisted && options.publish !== !1)
	      try {
	        await this.#mutationPort?.publish({ ids: [policy.id] });
	      } catch (error) {
	        this.#onPersistenceError(error);
	      }
	    this.#writes.get(policy.id) === write && this.#writes.delete(policy.id);
	  }
	  /** WebDAV 等受控迁移入口:仅在远端版本更新时保留原 storedAt 写回。 */
	  async restore(rawPolicy, value, storedAtValue, options = {}) {
	    const policy = normalizePolicy(rawPolicy), storedAt = Math.max(0, Math.floor(storedAtValue));
	    if (!Number.isSafeInteger(storedAt) || policy.permanent !== !0 && storedAt + policy.retainForMs <= this.#now() || ((await this.read(policy)).storedAt ?? -1) >= storedAt) return;
	    const entry = Object.freeze({
	      schemaVersion: 1,
	      id: policy.id,
	      kind: policy.kind,
	      tags: policy.tags,
	      storedAt,
	      expiresAt: policy.permanent === !0 ? Number.MAX_SAFE_INTEGER : storedAt + policy.retainForMs,
	      bytes: Math.max(0, this.#estimateBytes(value)),
	      value,
	      ...policy.permanent === !0 ? { permanent: !0 } : {}
	    });
	    if (this.#remember(entry), !policy.persist) return;
	    const previous = this.#writes.get(policy.id) ?? Promise.resolve();
	    let persisted = !1;
	    const write = previous.catch(() => {
	    }).then(async () => {
	      await this.#store.write(entry), persisted = !0;
	    }).catch((error) => {
	      this.#onPersistenceError(error);
	    });
	    if (this.#writes.set(policy.id, write), await write, persisted && options.publish !== !1)
	      try {
	        await this.#mutationPort?.publish({ ids: [policy.id] });
	      } catch (error) {
	        this.#onPersistenceError(error);
	      }
	    this.#writes.get(policy.id) === write && this.#writes.delete(policy.id);
	  }
	  async merge(rawPolicy, incoming, mergeValues) {
	    const policy = normalizePolicy(rawPolicy), memory = this.#memory.get(policy.id), localValue = !this.#store.merge && memory && this.#validEntry(memory, policy) ? mergeValues(memory.value, incoming) : incoming;
	    let committed = Object.freeze({
	      schemaVersion: 1,
	      id: policy.id,
	      kind: policy.kind,
	      tags: policy.tags,
	      storedAt: Math.max(this.#now(), (memory?.storedAt ?? -1) + 1),
	      expiresAt: 0,
	      bytes: Math.max(0, this.#estimateBytes(localValue)),
	      value: localValue,
	      ...policy.permanent === !0 ? { permanent: !0 } : {}
	    });
	    if (committed = Object.freeze({
	      ...committed,
	      expiresAt: policy.permanent === !0 ? Number.MAX_SAFE_INTEGER : committed.storedAt + policy.retainForMs
	    }), this.#remember(committed), !policy.persist) return committed.value;
	    const previous = this.#writes.get(policy.id) ?? Promise.resolve();
	    let persisted = !1;
	    const write = previous.catch(() => {
	    }).then(async () => {
	      if (this.#store.merge) {
	        const merged = await this.#store.merge(
	          policy.id,
	          (current) => {
	            const value = current && this.#validEntry(current, policy) ? mergeValues(current.value, committed.value) : committed.value, storedAt = Math.max(
	              this.#now(),
	              (current?.storedAt ?? -1) + 1,
	              committed.storedAt
	            );
	            return Object.freeze({
	              schemaVersion: 1,
	              id: policy.id,
	              kind: policy.kind,
	              tags: policy.tags,
	              storedAt,
	              expiresAt: policy.permanent === !0 ? Number.MAX_SAFE_INTEGER : storedAt + policy.retainForMs,
	              bytes: Math.max(0, this.#estimateBytes(value)),
	              value,
	              ...policy.permanent === !0 ? { permanent: !0 } : {}
	            });
	          }
	        );
	        if (!merged) return;
	        committed = merged;
	      } else
	        await this.#store.write(committed);
	      persisted = !0;
	    }).catch((error) => {
	      this.#onPersistenceError(error);
	    });
	    if (this.#writes.set(policy.id, write), await write, persisted)
	      try {
	        await this.#mutationPort?.publish({ ids: [policy.id] });
	      } catch (error) {
	        this.#onPersistenceError(error);
	      }
	    return this.#writes.get(policy.id) === write && (this.#writes.delete(policy.id), persisted && this.#remember(committed)), committed.value;
	  }
	  async invalidate(query, publish = !0) {
	    const report = await this.#invalidateWithReport(query, publish);
	    if (!report.complete) throw new ResponseCacheInvalidationError(report);
	  }
	  /**
	   * 删除只由当前 owner 判定为不可达的内部 generation。不广播、不提升全局 epoch、
	   * 不撤销其他 cache flight;不得用于用户可见缓存清理或领域失效。
	   */
	  async prune(query) {
	    for (const [id, entry] of this.#memory)
	      matchesInvalidation(entry, query) && this.#memory.delete(id);
	    this.#writes.size && await Promise.all(this.#writes.values());
	    const result = await this.#store.invalidate(query);
	    if (result && !result.ok)
	      throw result.error ?? new Error("响应缓存内部 generation 修剪失败");
	  }
	  /**
	   * 只忘记当前标签页的内存副本,让 owner 重新读取共享持久缓存。
	   * 不删除 IndexedDB、不广播,也不提升全局 epoch。
	   */
	  forgetMemory(query) {
	    let removed = 0;
	    for (const [id, entry] of this.#memory)
	      matchesInvalidation(entry, query) && (this.#memory.delete(id), removed += 1);
	    return removed;
	  }
	  async invalidateWithReport(query, publish = !0) {
	    return this.#invalidateWithReport(query, publish);
	  }
	  async #invalidateWithReport(query, publish) {
	    this.#epoch += 1;
	    let memoryEntries = 0;
	    for (const [id, entry] of this.#memory)
	      matchesInvalidation(entry, query) && (this.#memory.delete(id), memoryEntries += 1);
	    this.#writes.size && await Promise.all(this.#writes.values());
	    const failures = [];
	    try {
	      await this.#flightPort?.invalidateWrites();
	    } catch (error) {
	      this.#onPersistenceError(error), failures.push(Object.freeze({ stage: "flight", cause: error }));
	    }
	    try {
	      const result = await this.#store.invalidate(query);
	      result && !result.ok && failures.push(Object.freeze({
	        stage: "store",
	        cause: result.error ?? new Error("持久响应缓存失效失败")
	      }));
	    } catch (error) {
	      this.#onPersistenceError(error), failures.push(Object.freeze({ stage: "store", cause: error }));
	    }
	    if (publish)
	      try {
	        await this.#mutationPort?.publish(query);
	      } catch (error) {
	        this.#onPersistenceError(error), failures.push(Object.freeze({ stage: "broadcast", cause: error }));
	      }
	    return Object.freeze({
	      memoryEntries,
	      failures: Object.freeze(failures),
	      complete: failures.length === 0
	    });
	  }
	  applyExternalInvalidation(query) {
	    this.#epoch += 1;
	    for (const [id, entry] of this.#memory)
	      matchesInvalidation(entry, query) && this.#memory.delete(id);
	  }
	  memoryStats() {
	    let bytes = 0;
	    for (const entry of this.#memory.values()) bytes += entry.bytes;
	    return Object.freeze({ entries: this.#memory.size, bytes });
	  }
	  async records() {
	    this.#writes.size && await Promise.all(this.#writes.values());
	    let persistent = [];
	    try {
	      persistent = await this.#store.records?.() ?? [];
	    } catch (error) {
	      throw this.#onPersistenceError(error), error;
	    }
	    const records = new Map(
	      persistent.map((entry) => [entry.id, entry])
	    );
	    for (const entry of this.#memory.values())
	      records.set(entry.id, Object.freeze({
	        id: entry.id,
	        kind: entry.kind,
	        tags: entry.tags,
	        storedAt: entry.storedAt,
	        expiresAt: entry.expiresAt,
	        bytes: entry.bytes,
	        ...entry.permanent === !0 ? { permanent: !0 } : {}
	      }));
	    return Object.freeze([...records.values()]);
	  }
	  /**
	   * 仅供受控的数据迁移 owner 导出仍在保留期内的完整缓存记录。
	   * UI 目录继续使用 records(),避免普通调用方接触响应正文。
	   */
	  async entries(query) {
	    this.#writes.size && await Promise.all(this.#writes.values());
	    let persistent = [];
	    try {
	      persistent = await this.#store.snapshotEntries?.() ?? [];
	    } catch (error) {
	      throw this.#onPersistenceError(error), error;
	    }
	    const now = this.#now(), entries = /* @__PURE__ */ new Map();
	    for (const entry of persistent)
	      entry.schemaVersion !== 1 || !matchesInvalidation(entry, query) || !Number.isFinite(entry.expiresAt) || entry.permanent !== !0 && entry.expiresAt <= now || entries.set(entry.id, entry);
	    for (const entry of this.#memory.values())
	      !matchesInvalidation(entry, query) || entry.permanent !== !0 && entry.expiresAt <= now || entries.set(entry.id, entry);
	    return Object.freeze([...entries.values()].sort((left, right) => right.storedAt - left.storedAt || left.id.localeCompare(right.id)).map((entry) => Object.freeze({
	      ...entry,
	      tags: Object.freeze([...entry.tags])
	    })));
	  }
	  #validEntry(entry, policy) {
	    const tags = Array.isArray(entry.tags) && entry.tags.every((tag) => typeof tag == "string") ? [...new Set(entry.tags)].sort() : null;
	    return entry.schemaVersion === 1 && entry.id === policy.id && entry.kind === policy.kind && Number.isFinite(entry.storedAt) && entry.storedAt >= 0 && Number.isFinite(entry.expiresAt) && entry.expiresAt >= entry.storedAt && Number.isFinite(entry.bytes) && entry.bytes >= 0 && entry.permanent === !0 == (policy.permanent === !0) && tags !== null && tags.length === policy.tags.length && tags.every((tag, index) => tag === policy.tags[index]);
	  }
	  async #load(policy, loader, options, cacheMode) {
	    const throwIfAborted = () => options.signal?.throwIfAborted(), cached = cacheMode === "no-store" ? Object.freeze({ state: "miss" }) : await this.read(policy);
	    if (throwIfAborted(), cacheMode === "default" && cached.state === "fresh") return cached.value;
	    const cachedBeforeRequest = cached.storedAt ?? 0, flightToken = this.#flightPort && policy.persist && cacheMode !== "no-store" ? `v1:${(0, import_cache_identity.sharedCacheIdToken)(policy.id)}:${policy.id.length}` : "";
	    let lease = null, heartbeat = null;
	    try {
	      if (flightToken && this.#flightPort) {
	        const deadline = this.#now() + this.#flightWaitTimeoutMs;
	        for (; lease = await this.#flightPort.acquireFlight(flightToken), throwIfAborted(), !lease.producer; ) {
	          const released = await this.#flightPort.waitForFlight(
	            flightToken,
	            options.signal,
	            deadline
	          );
	          if (throwIfAborted(), !released) throw new ResponseCacheFlightTimeoutError();
	          const shared2 = await this.#readAfterFlight(
	            policy,
	            cacheMode,
	            cachedBeforeRequest
	          );
	          if (throwIfAborted(), shared2.state === "hit") return shared2.value;
	          const failure = await this.#flightPort.readFlightFailure(lease);
	          if (throwIfAborted(), failure) throw new ResponseCacheSharedFlightFailureError(failure);
	        }
	        const shared = await this.#readAfterFlight(
	          policy,
	          cacheMode,
	          cachedBeforeRequest
	        );
	        if (throwIfAborted(), shared.state === "hit") return shared.value;
	        if (lease.coordinated) {
	          const producerLease = lease;
	          heartbeat = setInterval(() => {
	            this.#flightPort?.renewFlight(producerLease).then((renewed) => {
	              !renewed && heartbeat !== null && (clearInterval(heartbeat), heartbeat = null);
	            }).catch((error) => {
	              this.#onPersistenceError(error), heartbeat !== null && (clearInterval(heartbeat), heartbeat = null);
	            });
	          }, this.#flightHeartbeatMs);
	        }
	      }
	      const epoch = this.#epoch, value = await loader(options.signal);
	      return throwIfAborted(), cacheMode !== "no-store" && epoch === this.#epoch && (lease && this.#flightPort ? await this.#flightPort.commitFlight(lease, () => this.write(policy, value)) : await this.write(policy, value)), value;
	    } catch (error) {
	      throwIfAborted();
	      const failure = lease?.producer ? sharedFlightFailure(error) : null;
	      if (failure && this.#flightPort)
	        try {
	          await this.#flightPort.failFlight(lease, failure);
	        } catch (coordinationError) {
	          this.#onPersistenceError(coordinationError);
	        }
	      const canFallback = options.canFallback?.(error) ?? !0;
	      if (options.allowStaleOnError !== !1 && cached.state === "stale" && canFallback) {
	        const value = cached.value;
	        return options.mapStaleFallback ? options.mapStaleFallback(value, error) : value;
	      }
	      throw error;
	    } finally {
	      if (heartbeat !== null && clearInterval(heartbeat), lease?.producer && this.#flightPort)
	        try {
	          await this.#flightPort.releaseFlight(lease);
	        } catch (error) {
	          this.#onPersistenceError(error);
	        }
	    }
	  }
	  async #readAfterFlight(policy, cacheMode, cachedBeforeRequest) {
	    this.#memory.delete(policy.id);
	    const latest = await this.read(policy);
	    return latest.state === "miss" ? Object.freeze({ state: "miss" }) : cacheMode === "refresh" ? (latest.storedAt ?? 0) > cachedBeforeRequest ? Object.freeze({ state: "hit", value: latest.value }) : Object.freeze({ state: "miss" }) : latest.state === "fresh" ? Object.freeze({ state: "hit", value: latest.value }) : Object.freeze({ state: "miss" });
	  }
	  #remember(entry) {
	    if (this.#memory.delete(entry.id), entry.permanent === !0 && entry.bytes > this.#maxMemoryBytes)
	      return;
	    this.#memory.set(entry.id, entry);
	    let bytes = 0;
	    for (const value of this.#memory.values()) bytes += value.bytes;
	    for (; this.#memory.size > this.#maxMemoryEntries || bytes > this.#maxMemoryBytes && this.#memory.size > 1; ) {
	      let evictedId;
	      for (const [id, value] of this.#memory)
	        if (value.permanent === !0) {
	          evictedId = id;
	          break;
	        }
	      if (evictedId ??= this.#memory.keys().next().value, evictedId === void 0) break;
	      const oldest = this.#memory.get(evictedId);
	      this.#memory.delete(evictedId), bytes -= oldest?.bytes ?? 0;
	    }
	  }
	}
	class ResponseCacheFlightTimeoutError extends Error {
	  constructor() {
	    super("等待共享缓存请求超时"), this.name = "TimeoutError";
	  }
	}
}, "4071456c5d0195d1a502791abf8f9b0f4ee8e2fa3bcdc9edaaa347bb394eedc2");

/* Source: lite/src/cache/topic-snapshot-repository.ts */
runtime.register("src/cache/topic-snapshot-repository.js", function(module, exports, require) {
	var topic_snapshot_repository_exports = {};
	__export(topic_snapshot_repository_exports, {
	  TopicSnapshotRepository: () => TopicSnapshotRepository
	});
	module.exports = __toCommonJS(topic_snapshot_repository_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_ingest_version = require("../discourse/ingest-version.js"), import_reply_tree = require("../dom/reply-tree.js");
	function nonNegativeInteger(value, name) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 0)
	    throw new RangeError(`${name} 必须是非负安全整数`);
	  return numeric;
	}
	function finiteTimestamp(value, name) {
	  const numeric = Number(value);
	  if (!Number.isFinite(numeric) || numeric < 0)
	    throw new RangeError(`${name} 必须是非负有限时间戳`);
	  return numeric;
	}
	function localArchiveStatus(value) {
	  const status = Number(value);
	  if (status !== 403 && status !== 404 && status !== 410)
	    throw new RangeError("本地存档状态必须是 403、404 或 410");
	  return status;
	}
	function normalizeStreamPostIds(values) {
	  return (0, import_identifiers.discoursePostIdStream)(values ?? []);
	}
	function moderationHiddenPlaceholder(value) {
	  const cooked = String(value ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
	  return /社区举报.*临时隐藏/.test(cooked) || /flagged by the community.*temporarily hidden/i.test(cooked);
	}
	function mergePostEntity(current, incoming) {
	  if (!current || current === incoming) return incoming;
	  const currentRecord = current, incomingRecord = incoming, preserveVisibleCooked = incomingRecord.hidden === !0 && moderationHiddenPlaceholder(incomingRecord.cooked) && typeof currentRecord.cooked == "string" && currentRecord.cooked.trim().length > 0 && !moderationHiddenPlaceholder(currentRecord.cooked), merged = {
	    ...currentRecord
	  };
	  for (const [key, value] of Object.entries(incomingRecord))
	    key === "cooked" && preserveVisibleCooked || value !== void 0 && (merged[key] = value);
	  return Object.freeze(merged);
	}
	function validTreeSnapshot(value, topicId, posts, removedPostNumbers, onInvalid) {
	  if (value == null) return null;
	  const candidate = value;
	  let valid = candidate.schemaVersion === 2 && candidate.topicId === topicId && Number.isFinite(candidate.savedAt) && Number.isSafeInteger(candidate.expectedPostCount) && Number(candidate.expectedPostCount) >= 0 && !!candidate.tree && Number.isSafeInteger(candidate.tree.revision) && Number(candidate.tree.revision) >= 0 && Array.isArray(candidate.tree.relations) && Array.isArray(candidate.versions) && (candidate.removedVersions === void 0 || Array.isArray(candidate.removedVersions));
	  if (valid) {
	    const relations = candidate.tree.relations, relationNumbers = new Set(relations.map((relation) => relation.postNumber)), versionNumbers = /* @__PURE__ */ new Set();
	    for (const version of candidate.versions) {
	      const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(version?.postNumber);
	      if (postNumber === null || versionNumbers.has(postNumber) || !relationNumbers.has(postNumber) || !Number.isFinite(version.observedAt) || version.observedAt < 0 || (0, import_ingest_version.normalizeDiscourseIngestSource)(version.source) === null) {
	        valid = !1;
	        break;
	      }
	      versionNumbers.add(postNumber);
	    }
	    versionNumbers.size !== relationNumbers.size && (valid = !1);
	    const removedNumbers = /* @__PURE__ */ new Set();
	    for (const version of candidate.removedVersions ?? []) {
	      const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(version?.postNumber);
	      if (postNumber === null || relationNumbers.has(postNumber) || removedNumbers.has(postNumber) || !Number.isFinite(version.observedAt) || version.observedAt < 0 || (0, import_ingest_version.normalizeDiscourseIngestSource)(version.source) === null) {
	        valid = !1;
	        break;
	      }
	      removedNumbers.add(postNumber);
	    }
	  }
	  if (valid)
	    try {
	      const topology = new import_reply_tree.ReplyTreeTopology();
	      topology.replace(candidate.tree);
	      for (const entry of posts) {
	        const rawParentPostNumber = entry.value.reply_to_post_number, parentPostNumber = rawParentPostNumber == null || rawParentPostNumber === "" ? null : (0, import_identifiers.tryDiscoursePostNumber)(rawParentPostNumber);
	        if (parentPostNumber === null && !(rawParentPostNumber == null || rawParentPostNumber === ""))
	          throw new Error(
	            `Topic ${topicId} 楼层 #${entry.postNumber} 的缓存父楼层无效`
	          );
	        const storedParentPostNumber = topology.parentOf(entry.postNumber), parentWasRemoved = parentPostNumber !== null && removedPostNumbers.has(parentPostNumber);
	        if (storedParentPostNumber === void 0 || storedParentPostNumber !== parentPostNumber && !parentWasRemoved)
	          throw new Error(
	            `Topic ${topicId} 楼层 #${entry.postNumber} 的正文与回复树缓存关系不一致`
	          );
	      }
	      return value;
	    } catch (error) {
	      return onInvalid(error), null;
	    }
	  return onInvalid(new Error(`Topic ${topicId} 的内嵌回复树快照无效`)), null;
	}
	class TopicSnapshotRepository {
	  topicId;
	  authScope;
	  #responses;
	  #policy;
	  #archivePolicy;
	  #now;
	  #persistenceIdleMs;
	  #persistenceWait;
	  #onInvalidSnapshot;
	  #onInvalidTreeSnapshot;
	  #posts = /* @__PURE__ */ new Map();
	  #removedPosts = /* @__PURE__ */ new Map();
	  #unavailablePosts = /* @__PURE__ */ new Map();
	  #topic = null;
	  #unavailableTopic = null;
	  #topicObservedAt = 0;
	  #topicSource = null;
	  #streamPostIds = Object.freeze([]);
	  #streamObservedAt = 0;
	  #expectedPostCount = 0;
	  #tree = null;
	  #updatedAt = 0;
	  #restorePromise = null;
	  #pendingWrite = !1;
	  #persisting = null;
	  #persistenceReadyAt = 0;
	  #readPersistenceDelayMs = () => 0;
	  #archiveRecordKnown = !1;
	  constructor(options) {
	    const topicId = String((0, import_identifiers.discourseTopicId)(options.topicId)), authScope = (0, import_identifiers.discourseAuthScope)(options.authScope);
	    if (this.topicId = topicId, this.authScope = authScope, this.#responses = options.responseRepository, this.#now = options.now ?? Date.now, this.#persistenceIdleMs = finiteTimestamp(
	      options.persistenceIdleMs ?? 0,
	      "persistenceIdleMs"
	    ), this.#persistenceWait = options.persistenceWait ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))), this.#onInvalidSnapshot = options.onInvalidSnapshot ?? (() => {
	    }), this.#onInvalidTreeSnapshot = options.onInvalidTreeSnapshot ?? (() => {
	    }), this.#policy = Object.freeze({
	      id: `${authScope}|snapshot:topic:${topicId}`,
	      kind: "topics",
	      tags: Object.freeze([`topic:${topicId}`]),
	      freshForMs: finiteTimestamp(options.freshForMs, "freshForMs"),
	      retainForMs: finiteTimestamp(options.retainForMs, "retainForMs"),
	      persist: !0
	    }), this.#archivePolicy = Object.freeze({
	      id: `${authScope}|snapshot:topic-archive:${topicId}`,
	      kind: "topics",
	      tags: Object.freeze([`topic:${topicId}`, "topic-local-archive"]),
	      freshForMs: finiteTimestamp(options.freshForMs, "freshForMs"),
	      retainForMs: finiteTimestamp(options.retainForMs, "retainForMs"),
	      persist: !0,
	      permanent: !0
	    }), this.#policy.retainForMs < this.#policy.freshForMs)
	      throw new RangeError("retainForMs 不能小于 freshForMs");
	  }
	  ingest(input) {
	    const observedAt = finiteTimestamp(input.observedAt ?? this.#now(), "observedAt");
	    let topicChanged = !1, streamChanged = !1, streamVersionAccepted = !1, expectedPostCountChanged = !1, ignoredPosts = 0, acceptedPosts = 0;
	    const changedPostNumbers = [];
	    if (input.topic !== void 0 && (0, import_ingest_version.shouldReplaceDiscourseVersion)(
	      { observedAt: this.#topicObservedAt, source: this.#topicSource },
	      { observedAt, source: input.source }
	    ) && (this.#topic = input.topic, this.#topicObservedAt = observedAt, this.#topicSource = input.source, topicChanged = !0, this.#unavailableTopic && observedAt >= this.#unavailableTopic.confirmedAt && (this.#unavailableTopic = null, topicChanged = !0)), input.streamPostIds !== void 0 && (0, import_ingest_version.shouldReplaceDiscourseVersion)(
	      { observedAt: this.#streamObservedAt, source: null },
	      { observedAt, source: input.source }
	    )) {
	      streamVersionAccepted = !0;
	      const blockedPostIds = new Set(
	        [...this.#removedPosts.values()].filter((removed) => !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, {
	          observedAt,
	          source: input.source
	        })).map((removed) => removed.postId)
	      ), stream = Object.freeze(
	        normalizeStreamPostIds(input.streamPostIds).filter((postId) => !blockedPostIds.has(postId))
	      );
	      (stream.length !== this.#streamPostIds.length || stream.some((postId, index) => postId !== this.#streamPostIds[index])) && (this.#streamPostIds = stream, streamChanged = !0), this.#streamObservedAt = observedAt;
	    }
	    if (input.expectedPostCount !== void 0) {
	      const blockedRemovalCount = [...this.#removedPosts.values()].filter((removed) => !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, {
	        observedAt,
	        source: input.source
	      })).length, incomingExpectedPostCount = Math.max(
	        0,
	        nonNegativeInteger(input.expectedPostCount, "expectedPostCount") - blockedRemovalCount
	      ), nextExpectedPostCount = input.source === "topic-json" && input.topic !== void 0 && topicChanged && streamVersionAccepted ? incomingExpectedPostCount : Math.max(
	        this.#expectedPostCount,
	        incomingExpectedPostCount
	      );
	      nextExpectedPostCount !== this.#expectedPostCount && (this.#expectedPostCount = nextExpectedPostCount, expectedPostCountChanged = !0);
	    }
	    for (const post of input.posts ?? []) {
	      const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(post.post_number);
	      if (postNumber === null) {
	        ignoredPosts += 1;
	        continue;
	      }
	      const current = this.#posts.get(postNumber), removed = this.#removedPosts.get(postNumber);
	      if (removed && !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removed, {
	        observedAt,
	        source: input.source
	      }) || current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(
	        current,
	        { observedAt, source: input.source }
	      ) || current && current.value === post && current.observedAt === observedAt) continue;
	      const value = mergePostEntity(current?.value, post);
	      this.#removedPosts.delete(postNumber);
	      const unavailable = this.#unavailablePosts.get(postNumber);
	      unavailable && observedAt >= unavailable.confirmedAt && this.#unavailablePosts.delete(postNumber), this.#posts.set(postNumber, Object.freeze({
	        postNumber,
	        observedAt,
	        source: input.source,
	        value
	      })), acceptedPosts += 1, changedPostNumbers.push(postNumber);
	    }
	    return (topicChanged || streamChanged || expectedPostCountChanged || changedPostNumbers.length > 0) && (this.#updatedAt = Math.max(this.#updatedAt, observedAt), this.#queuePersistence()), Object.freeze({
	      acceptedPosts,
	      ignoredPosts,
	      changedPostNumbers: Object.freeze(changedPostNumbers.sort((left, right) => left - right)),
	      topicChanged,
	      streamChanged
	    });
	  }
	  markTopicUnavailable(rawStatus, confirmedAt = this.#now()) {
	    if (this.#topic === null && this.#posts.size === 0) return !1;
	    const next = Object.freeze({
	      status: localArchiveStatus(rawStatus),
	      confirmedAt: finiteTimestamp(confirmedAt, "confirmedAt")
	    });
	    return this.#unavailableTopic && this.#unavailableTopic.confirmedAt > next.confirmedAt ? !1 : (this.#unavailableTopic = next, this.#archiveRecordKnown = !0, this.#updatedAt = Math.max(this.#updatedAt, next.confirmedAt), this.#queuePersistence(), !0);
	  }
	  markPostUnavailable(rawPostNumber, rawStatus, confirmedAt = this.#now()) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawPostNumber);
	    if (postNumber === null || !this.#posts.has(postNumber)) return !1;
	    const next = Object.freeze({
	      postNumber,
	      status: localArchiveStatus(rawStatus),
	      confirmedAt: finiteTimestamp(confirmedAt, "confirmedAt")
	    }), current = this.#unavailablePosts.get(postNumber);
	    return current && current.confirmedAt > next.confirmedAt ? !1 : (this.#unavailablePosts.set(postNumber, next), this.#archiveRecordKnown = !0, this.#updatedAt = Math.max(this.#updatedAt, next.confirmedAt), this.#queuePersistence(), !0);
	  }
	  localArchiveState() {
	    return Object.freeze({
	      topic: this.#unavailableTopic,
	      posts: Object.freeze(
	        [...this.#unavailablePosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      )
	    });
	  }
	  hasLocalArchive() {
	    return this.#unavailableTopic !== null || this.#unavailablePosts.size > 0;
	  }
	  removePost(rawPostNumber, rawPostId, source, observedAt = this.#now()) {
	    const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawPostNumber);
	    if (postNumber === null) throw new RangeError("postNumber 必须是正安全整数");
	    const postId = (0, import_identifiers.discoursePostId)(rawPostId), normalizedObservedAt = finiteTimestamp(observedAt, "observedAt"), current = this.#posts.get(postNumber), currentId = current?.value?.id;
	    if (currentId !== void 0 && (0, import_identifiers.discoursePostId)(currentId) !== postId)
	      throw new Error(`楼层 #${postNumber} 与 post.id ${postId} 不一致`);
	    const removed = this.#removedPosts.get(postNumber), next = Object.freeze({
	      postNumber,
	      postId,
	      observedAt: normalizedObservedAt,
	      source
	    });
	    if (current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, next) || removed && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(removed, next))
	      return Object.freeze({ removed: !1, postNumber, postId, streamChanged: !1 });
	    this.#posts.delete(postNumber), this.#unavailablePosts.delete(postNumber), this.#removedPosts.set(postNumber, next);
	    const nextStream = this.#streamPostIds.filter((candidate) => candidate !== postId), streamChanged = nextStream.length !== this.#streamPostIds.length;
	    return streamChanged && (this.#streamPostIds = Object.freeze(nextStream), this.#streamObservedAt = normalizedObservedAt), (current || streamChanged) && (this.#expectedPostCount = Math.max(0, this.#expectedPostCount - 1)), this.#updatedAt = Math.max(this.#updatedAt, normalizedObservedAt), this.#queuePersistence(), Object.freeze({ removed: !0, postNumber, postId, streamChanged });
	  }
	  restore() {
	    if (this.#restorePromise) return this.#restorePromise;
	    const promise = this.#restoreFromCache();
	    this.#restorePromise = promise;
	    const clearRestore = () => {
	      this.#restorePromise === promise && (this.#restorePromise = null);
	    };
	    return promise.then(clearRestore, clearRestore), promise;
	  }
	  topic() {
	    return this.#topic;
	  }
	  post(postNumber) {
	    const normalized = (0, import_identifiers.tryDiscoursePostNumber)(postNumber);
	    return normalized === null ? void 0 : this.#posts.get(normalized)?.value;
	  }
	  posts() {
	    return Object.freeze(
	      [...this.#posts.values()].sort((left, right) => left.postNumber - right.postNumber).map((entry) => entry.value)
	    );
	  }
	  streamPostIds() {
	    return this.#streamPostIds;
	  }
	  isFresh(now = this.#now()) {
	    return this.#unavailableTopic !== null ? !0 : this.#updatedAt > 0 ? Math.max(0, finiteTimestamp(now, "now") - this.#updatedAt) <= this.#policy.freshForMs : !1;
	  }
	  snapshot(now = this.#now()) {
	    return Object.freeze({
	      schemaVersion: 2,
	      topicId: this.topicId,
	      authScope: this.authScope,
	      savedAt: now,
	      updatedAt: this.#updatedAt,
	      expectedPostCount: this.#expectedPostCount,
	      topicObservedAt: this.#topicObservedAt,
	      topicSource: this.#topicSource,
	      topic: this.#topic,
	      streamObservedAt: this.#streamObservedAt,
	      streamPostIds: this.#streamPostIds,
	      posts: Object.freeze(
	        [...this.#posts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      removedPosts: Object.freeze(
	        [...this.#removedPosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      unavailableTopic: this.#unavailableTopic,
	      unavailablePosts: Object.freeze(
	        [...this.#unavailablePosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      tree: this.#tree
	    });
	  }
	  replyTreeSnapshotStore() {
	    return Object.freeze({
	      load: async (topicId) => String(topicId) !== this.topicId ? null : (await this.restore())?.snapshot.tree ?? null,
	      save: async (topicId, snapshot) => {
	        if (String(topicId) !== this.topicId || snapshot.topicId !== this.topicId)
	          throw new Error(`回复树快照 Topic ${topicId} 与仓储 ${this.topicId} 不匹配`);
	        this.#tree = snapshot, this.#expectedPostCount = Math.max(
	          this.#expectedPostCount,
	          snapshot.expectedPostCount
	        ), this.#queuePersistence(), await this.flush();
	      }
	    });
	  }
	  async flush() {
	    for (; this.#pendingWrite || this.#persisting; )
	      this.#persisting || this.#startPersistence(), await this.#persisting;
	  }
	  /**
	   * 缓存重建失败时,把当前完整快照重新写回唯一 snapshot policy。
	   *
	   * 正常增量持久化仍走 merge/idle;这里仅用于缓存已失效、且必须立即恢复阅读器的
	   * rollback 边界,因此直接覆盖刚被清空的同 Topic 快照。
	   */
	  persistCurrentSnapshot() {
	    return this.#responses.write(
	      this.hasLocalArchive() ? this.#archivePolicy : this.#policy,
	      this.snapshot()
	    );
	  }
	  setPersistenceDelayReader(reader) {
	    return this.#readPersistenceDelayMs = reader, () => {
	      this.#readPersistenceDelayMs === reader && (this.#readPersistenceDelayMs = () => 0);
	    };
	  }
	  async #restoreFromCache() {
	    const read = async (policy) => {
	      const cached2 = await this.#responses.read(
	        policy
	      );
	      if (cached2.state === "miss" || cached2.value === void 0) return null;
	      try {
	        const stored2 = this.#normalizeStoredSnapshot(cached2.value);
	        if (policy === this.#archivePolicy && !(stored2.unavailableTopic || stored2.unavailablePosts?.length))
	          throw new Error(`Topic ${this.topicId} 的永久存档缺少失效标记`);
	        return Object.freeze({
	          policy,
	          cached: cached2,
	          stored: stored2
	        });
	      } catch (error) {
	        return this.#onInvalidSnapshot(error), await this.#responses.invalidate({ ids: [policy.id] }), null;
	      }
	    }, [archived, regular] = await Promise.all([
	      read(this.#archivePolicy),
	      read(this.#policy)
	    ]), restored = archived && regular ? archived.stored.updatedAt >= regular.stored.updatedAt ? archived : regular : archived ?? regular;
	    if (!restored) return null;
	    const { cached, policy: restoredPolicy, stored } = restored;
	    this.#archiveRecordKnown = archived !== null;
	    const staleArchive = archived !== null && restored === regular, discardedInvalidTree = cached.value.tree !== null && cached.value.tree !== void 0 && stored.tree === null, hadLocalState = this.#topic !== null || this.#posts.size > 0 || this.#removedPosts.size > 0 || this.#streamPostIds.length > 0 || this.#tree !== null || this.hasLocalArchive();
	    let topicFilled = !1, streamFilled = !1, removalFilled = !1, availabilityFilled = !1;
	    const addedPostNumbers = [];
	    this.#topic === null && stored.topic !== null && (this.#topic = stored.topic, this.#topicObservedAt = stored.topicObservedAt, this.#topicSource = stored.topicSource, topicFilled = !0), !this.#streamPostIds.length && stored.streamPostIds.length && (this.#streamPostIds = stored.streamPostIds, this.#streamObservedAt = stored.streamObservedAt, streamFilled = !0);
	    for (const removed of stored.removedPosts ?? []) {
	      const current = this.#posts.get(removed.postNumber);
	      if (current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, removed))
	        continue;
	      const existingRemoval = this.#removedPosts.get(removed.postNumber);
	      if (existingRemoval && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(existingRemoval, removed))
	        continue;
	      this.#posts.delete(removed.postNumber), this.#removedPosts.set(removed.postNumber, removed);
	      const nextStream = this.#streamPostIds.filter(
	        (postId) => postId !== removed.postId
	      );
	      nextStream.length !== this.#streamPostIds.length && (this.#streamPostIds = Object.freeze(nextStream), streamFilled = !0), removalFilled = !0;
	    }
	    for (const entry of stored.posts)
	      this.#removedPosts.has(entry.postNumber) || this.#posts.has(entry.postNumber) || (this.#posts.set(entry.postNumber, entry), addedPostNumbers.push(entry.postNumber));
	    stored.unavailableTopic && (!this.#unavailableTopic || stored.unavailableTopic.confirmedAt >= this.#unavailableTopic.confirmedAt) && (this.#unavailableTopic = stored.unavailableTopic, availabilityFilled = !0);
	    for (const unavailable of stored.unavailablePosts ?? []) {
	      if (!this.#posts.has(unavailable.postNumber)) continue;
	      const current = this.#unavailablePosts.get(unavailable.postNumber);
	      current && current.confirmedAt > unavailable.confirmedAt || (this.#unavailablePosts.set(unavailable.postNumber, unavailable), availabilityFilled = !0);
	    }
	    this.hasLocalArchive() && (this.#archiveRecordKnown = !0), this.#tree === null && stored.tree !== null && (this.#tree = stored.tree);
	    const blockedStoredPostCount = stored.posts.filter((entry) => this.#removedPosts.has(entry.postNumber)).length;
	    if (this.#expectedPostCount = Math.max(
	      this.#expectedPostCount,
	      Math.max(0, stored.expectedPostCount - blockedStoredPostCount)
	    ), this.#updatedAt = Math.max(this.#updatedAt, stored.updatedAt), discardedInvalidTree && await this.#responses.invalidate({ ids: [restoredPolicy.id] }), (discardedInvalidTree || hadLocalState && (topicFilled || streamFilled || removalFilled || availabilityFilled || addedPostNumbers.length)) && this.#queuePersistence(), staleArchive) {
	      const cleanup = await this.#responses.invalidateWithReport({
	        ids: [this.#archivePolicy.id]
	      });
	      this.#archiveRecordKnown = !cleanup.complete, cleanup.complete || this.#queuePersistence();
	    }
	    return Object.freeze({
	      snapshot: stored,
	      addedPostNumbers: Object.freeze(addedPostNumbers.sort((left, right) => left - right)),
	      topicFilled,
	      streamFilled
	    });
	  }
	  #normalizeStoredSnapshot(value) {
	    if (!value || value.schemaVersion !== 2 || value.topicId !== this.topicId || value.authScope !== this.authScope || !Array.isArray(value.posts) || value.removedPosts !== void 0 && !Array.isArray(value.removedPosts) || value.unavailablePosts !== void 0 && !Array.isArray(value.unavailablePosts) || !Array.isArray(value.streamPostIds))
	      throw new Error(`Topic ${this.topicId} 的正文快照身份或 schema 无效`);
	    const removedPosts = /* @__PURE__ */ new Map();
	    for (const rawRemoval of value.removedPosts ?? [])
	      try {
	        const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawRemoval?.postNumber), source = (0, import_ingest_version.normalizeDiscourseIngestSource)(rawRemoval?.source);
	        if (postNumber === null || source === null || !Number.isFinite(rawRemoval.observedAt) || rawRemoval.observedAt < 0)
	          throw new Error("删除墓碑字段无效");
	        const removal = Object.freeze({
	          postNumber,
	          postId: (0, import_identifiers.discoursePostId)(rawRemoval.postId),
	          observedAt: rawRemoval.observedAt,
	          source
	        }), current = removedPosts.get(postNumber);
	        (!current || (0, import_ingest_version.shouldReplaceDiscourseVersion)(current, removal)) && removedPosts.set(postNumber, removal);
	      } catch (error) {
	        this.#onInvalidSnapshot(new Error(
	          `Topic ${this.topicId} 的楼层删除墓碑无效`,
	          { cause: error }
	        ));
	      }
	    const posts = /* @__PURE__ */ new Map(), blockedStreamPostIds = new Set(
	      [...removedPosts.values()].map((entry) => entry.postId)
	    );
	    for (const rawEntry of value.posts) {
	      if (rawEntry?.value?.reader_local_archive_placeholder === !0) {
	        this.#onInvalidSnapshot(new Error(
	          `Topic ${this.topicId} 的旧版 404 占位楼层已丢弃`
	        ));
	        continue;
	      }
	      const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawEntry?.postNumber), source = (0, import_ingest_version.normalizeDiscourseIngestSource)(rawEntry?.source), valuePostNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawEntry?.value?.post_number);
	      if (postNumber === null || valuePostNumber !== postNumber || source === null || !Number.isFinite(rawEntry.observedAt))
	        continue;
	      const postVersion = { observedAt: rawEntry.observedAt, source }, removal = removedPosts.get(postNumber);
	      if (removal) {
	        const rawPostId = rawEntry.value.id;
	        if (rawPostId !== void 0)
	          try {
	            const postId = (0, import_identifiers.discoursePostId)(rawPostId);
	            postId !== removal.postId && (blockedStreamPostIds.add(postId), this.#onInvalidSnapshot(new Error(
	              `Topic ${this.topicId} 楼层 #${postNumber} 的删除 post.id 不一致`
	            )));
	          } catch (error) {
	            this.#onInvalidSnapshot(error);
	          }
	        if (!(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removal, postVersion)) continue;
	        removedPosts.delete(postNumber), blockedStreamPostIds.delete(removal.postId);
	      }
	      const current = posts.get(postNumber);
	      (!current || (0, import_ingest_version.shouldReplaceDiscourseVersion)(
	        current,
	        postVersion
	      )) && posts.set(postNumber, Object.freeze({
	        postNumber,
	        observedAt: rawEntry.observedAt,
	        source,
	        value: rawEntry.value
	      }));
	    }
	    const topicSource = value.topicSource === null ? null : (0, import_ingest_version.normalizeDiscourseIngestSource)(value.topicSource);
	    if (value.topic !== null && topicSource === null)
	      throw new Error(`Topic ${this.topicId} 的正文来源无效`);
	    const normalizedPosts = Object.freeze(
	      [...posts.values()].sort((left, right) => left.postNumber - right.postNumber)
	    ), unavailableTopic = value.unavailableTopic ? Object.freeze({
	      status: localArchiveStatus(value.unavailableTopic.status),
	      confirmedAt: finiteTimestamp(
	        value.unavailableTopic.confirmedAt,
	        "unavailableTopic.confirmedAt"
	      )
	    }) : null, unavailablePosts = /* @__PURE__ */ new Map();
	    for (const rawUnavailable of value.unavailablePosts ?? [])
	      try {
	        const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(rawUnavailable?.postNumber);
	        if (postNumber === null || !posts.has(postNumber)) continue;
	        const unavailable = Object.freeze({
	          postNumber,
	          status: localArchiveStatus(rawUnavailable.status),
	          confirmedAt: finiteTimestamp(
	            rawUnavailable.confirmedAt,
	            "unavailablePost.confirmedAt"
	          )
	        });
	        if (posts.get(postNumber).observedAt > unavailable.confirmedAt) continue;
	        const current = unavailablePosts.get(postNumber);
	        (!current || current.confirmedAt <= unavailable.confirmedAt) && unavailablePosts.set(postNumber, unavailable);
	      } catch (error) {
	        this.#onInvalidSnapshot(error);
	      }
	    return Object.freeze({
	      schemaVersion: 2,
	      topicId: this.topicId,
	      authScope: this.authScope,
	      savedAt: finiteTimestamp(value.savedAt, "savedAt"),
	      updatedAt: finiteTimestamp(value.updatedAt, "updatedAt"),
	      expectedPostCount: nonNegativeInteger(value.expectedPostCount, "expectedPostCount"),
	      topicObservedAt: finiteTimestamp(value.topicObservedAt, "topicObservedAt"),
	      topicSource,
	      topic: value.topic,
	      streamObservedAt: finiteTimestamp(value.streamObservedAt, "streamObservedAt"),
	      streamPostIds: Object.freeze(
	        normalizeStreamPostIds(value.streamPostIds).filter((postId) => !blockedStreamPostIds.has(postId))
	      ),
	      posts: normalizedPosts,
	      removedPosts: Object.freeze(
	        [...removedPosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      unavailableTopic: unavailableTopic && unavailableTopic.confirmedAt >= value.topicObservedAt ? unavailableTopic : null,
	      unavailablePosts: Object.freeze(
	        [...unavailablePosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      tree: validTreeSnapshot(
	        value.tree,
	        this.topicId,
	        normalizedPosts,
	        new Set(removedPosts.keys()),
	        this.#onInvalidTreeSnapshot
	      )
	    });
	  }
	  #mergeStoredSnapshots(storedValue, incomingValue) {
	    let stored;
	    try {
	      stored = this.#normalizeStoredSnapshot(storedValue);
	    } catch (error) {
	      return this.#onInvalidSnapshot(error), this.#normalizeStoredSnapshot(incomingValue);
	    }
	    const incoming = this.#normalizeStoredSnapshot(incomingValue), posts = /* @__PURE__ */ new Map(), removals = /* @__PURE__ */ new Map(), applyRemoval = (removal) => {
	      const post = posts.get(removal.postNumber);
	      if (post && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(post, removal)) return;
	      const current = removals.get(removal.postNumber);
	      current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, removal) || (posts.delete(removal.postNumber), removals.set(removal.postNumber, removal));
	    }, applyPost = (post) => {
	      const removal = removals.get(post.postNumber);
	      if (removal && !(0, import_ingest_version.shouldReplaceDiscourseRemoval)(removal, post)) return;
	      const current = posts.get(post.postNumber);
	      current && !(0, import_ingest_version.shouldReplaceDiscourseVersion)(current, post) || (removals.delete(post.postNumber), posts.set(post.postNumber, post));
	    };
	    for (const snapshot of [stored, incoming]) {
	      for (const removal of snapshot.removedPosts ?? []) applyRemoval(removal);
	      for (const post of snapshot.posts) applyPost(post);
	    }
	    let topic = stored.topic, topicObservedAt = stored.topicObservedAt, topicSource = stored.topicSource;
	    incoming.topic !== null && incoming.topicSource !== null && (0, import_ingest_version.shouldReplaceDiscourseVersion)(
	      { observedAt: topicObservedAt, source: topicSource },
	      {
	        observedAt: incoming.topicObservedAt,
	        source: incoming.topicSource
	      }
	    ) && (topic = incoming.topic, topicObservedAt = incoming.topicObservedAt, topicSource = incoming.topicSource);
	    const incomingStreamWins = incoming.streamObservedAt >= stored.streamObservedAt, streamPostIds = incomingStreamWins ? incoming.streamPostIds : stored.streamPostIds, blockedPostIds = new Set(
	      [...removals.values()].map((removal) => removal.postId)
	    ), incomingStateWins = incoming.updatedAt >= stored.updatedAt, unavailableTopic = [stored.unavailableTopic, incoming.unavailableTopic].filter((value) => value != null).sort((left, right) => right.confirmedAt - left.confirmedAt)[0] ?? null, unavailablePosts = /* @__PURE__ */ new Map();
	    for (const snapshot of [stored, incoming])
	      for (const unavailable of snapshot.unavailablePosts ?? []) {
	        const current = unavailablePosts.get(unavailable.postNumber);
	        (!current || current.confirmedAt <= unavailable.confirmedAt) && unavailablePosts.set(unavailable.postNumber, unavailable);
	      }
	    for (const [postNumber, unavailable] of unavailablePosts) {
	      const post = posts.get(postNumber);
	      (!post || post.observedAt > unavailable.confirmedAt) && unavailablePosts.delete(postNumber);
	    }
	    const tree = stored.tree ? incoming.tree && incoming.tree.savedAt >= stored.tree.savedAt ? incoming.tree : stored.tree : incoming.tree;
	    return Object.freeze({
	      schemaVersion: 2,
	      topicId: this.topicId,
	      authScope: this.authScope,
	      savedAt: Math.max(stored.savedAt, incoming.savedAt),
	      updatedAt: Math.max(stored.updatedAt, incoming.updatedAt),
	      expectedPostCount: incomingStateWins && incoming.expectedPostCount > 0 ? incoming.expectedPostCount : stored.expectedPostCount,
	      topicObservedAt,
	      topicSource,
	      topic,
	      streamObservedAt: incomingStreamWins ? incoming.streamObservedAt : stored.streamObservedAt,
	      streamPostIds: Object.freeze(
	        streamPostIds.filter((postId) => !blockedPostIds.has(postId))
	      ),
	      posts: Object.freeze(
	        [...posts.values()].sort(
	          (left, right) => left.postNumber - right.postNumber
	        )
	      ),
	      removedPosts: Object.freeze(
	        [...removals.values()].sort(
	          (left, right) => left.postNumber - right.postNumber
	        )
	      ),
	      unavailableTopic: unavailableTopic && unavailableTopic.confirmedAt >= topicObservedAt ? unavailableTopic : null,
	      unavailablePosts: Object.freeze(
	        [...unavailablePosts.values()].sort((left, right) => left.postNumber - right.postNumber)
	      ),
	      tree
	    });
	  }
	  #queuePersistence() {
	    this.#pendingWrite = !0, this.#persistenceReadyAt = Math.max(
	      this.#persistenceReadyAt,
	      this.#now() + this.#persistenceIdleMs
	    ), this.#persisting || this.#startPersistence();
	  }
	  async #waitForPersistenceWindow() {
	    for (; this.#pendingWrite; ) {
	      const repositoryDelay = Math.max(
	        0,
	        this.#persistenceReadyAt - this.#now()
	      ), rawActivityDelay = Number(
	        this.#readPersistenceDelayMs(this.#persistenceIdleMs)
	      ), activityDelay = Number.isFinite(rawActivityDelay) ? Math.max(0, rawActivityDelay) : 0, delayMs = Math.max(repositoryDelay, activityDelay);
	      if (delayMs <= 0) return;
	      await this.#persistenceWait(delayMs);
	    }
	  }
	  #startPersistence() {
	    this.#persisting = Promise.resolve().then(async () => {
	      for (; this.#pendingWrite; ) {
	        if (await this.#waitForPersistenceWindow(), !this.#pendingWrite) continue;
	        this.#pendingWrite = !1;
	        const snapshot = this.snapshot(), archived = !!(snapshot.unavailableTopic || snapshot.unavailablePosts?.length), policy = archived ? this.#archivePolicy : this.#policy;
	        await this.#responses.merge(
	          policy,
	          snapshot,
	          (stored, incoming) => this.#mergeStoredSnapshots(stored, incoming)
	        ), archived ? this.#archiveRecordKnown = !0 : this.#archiveRecordKnown && (await this.#responses.invalidate({ ids: [this.#archivePolicy.id] }), this.#archiveRecordKnown = !1);
	      }
	    }).finally(() => {
	      this.#persisting = null, this.#pendingWrite || (this.#persistenceReadyAt = 0), this.#pendingWrite && this.#startPersistence();
	    });
	  }
	}
}, "47e7c0f0e76eeb5c7b5fce66c0227e242262b7875ec453b9aed5d144881dbc78");

/* Source: lite/src/collection/reader-collection-filter-model.ts */
runtime.register("src/collection/reader-collection-filter-model.js", function(module, exports, require) {
	var reader_collection_filter_model_exports = {};
	__export(reader_collection_filter_model_exports, {
	  readerCollectionDateKey: () => readerCollectionDateKey
	});
	module.exports = __toCommonJS(reader_collection_filter_model_exports);
	function readerCollectionDateKey(value) {
	  const timestamp = typeof value == "number" ? value : Date.parse(value);
	  if (!Number.isFinite(timestamp)) return "";
	  const date = new Date(timestamp);
	  return [
	    date.getFullYear(),
	    String(date.getMonth() + 1).padStart(2, "0"),
	    String(date.getDate()).padStart(2, "0")
	  ].join("-");
	}
}, "b5ee6d24616f884aa55ed482a8e2dea6ba7551cc340a582a254d2251287af0ae");

/* Source: lite/src/collection/reader-collection-floating-window.ts */
runtime.register("src/collection/reader-collection-floating-window.js", function(module, exports, require) {
	var reader_collection_floating_window_exports = {};
	__export(reader_collection_floating_window_exports, {
	  READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY: () => READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY,
	  READER_COLLECTION_FLOATING_WINDOW_PLACEMENT: () => READER_COLLECTION_FLOATING_WINDOW_PLACEMENT,
	  READER_COLLECTION_FLOATING_WINDOW_POLICY: () => READER_COLLECTION_FLOATING_WINDOW_POLICY,
	  ReaderCollectionFloatingWindow: () => ReaderCollectionFloatingWindow,
	  ReaderCollectionNodeCache: () => ReaderCollectionNodeCache,
	  ReaderCollectionProgressView: () => ReaderCollectionProgressView,
	  ReaderCollectionScrollWindow: () => ReaderCollectionScrollWindow
	});
	module.exports = __toCommonJS(reader_collection_floating_window_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_floating_window_frame = require("../shell/reader-floating-window-frame.js");
	const READER_COLLECTION_FLOATING_WINDOW_POLICY = Object.freeze({
	  minWidth: 320,
	  minHeight: 460,
	  defaultWidth: 560,
	  defaultHeight: 680
	}), READER_COLLECTION_FLOATING_WINDOW_PLACEMENT = "center", READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY = "linuxdo-enhanced-reader:collection-window:v1";
	class ReaderCollectionNodeCache {
	  #entries = /* @__PURE__ */ new Map();
	  node(keyValue, record, variantValue, create) {
	    const key = String(keyValue), variant = String(variantValue), cached = this.#entries.get(key);
	    if (cached?.record === record && cached.variant === variant)
	      return cached.node;
	    const node2 = create();
	    return this.#entries.set(key, Object.freeze({ record, variant, node: node2 })), node2;
	  }
	  prune(keys) {
	    const retained = new Set(keys);
	    for (const key of this.#entries.keys())
	      retained.has(key) || this.#entries.delete(key);
	  }
	  clear() {
	    this.#entries.clear();
	  }
	}
	class ReaderCollectionScrollWindow {
	  scope;
	  #list;
	  #identity;
	  #loadMore;
	  #onError;
	  #pages = /* @__PURE__ */ new Map();
	  #streamKey = "";
	  #records = Object.freeze([]);
	  #loading = !1;
	  #hasMore = !1;
	  #pending = !1;
	  #retryBoundaryAfterPending = !1;
	  #boundaryMicrotaskPending = !1;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#list = options.list, this.#identity = options.identity, this.#loadMore = options.loadMore, this.#onError = options.onError ?? (() => {
	    }), options.pager.hidden = !0, options.pager.setAttribute("aria-hidden", "true"), this.#list.dataset.collectionScrollLoading = "false", this.scope.listen(this.#list, "scroll", () => {
	      this.#requestMoreIfNearEnd(!0);
	    }, { passive: !0 }), this.scope.add(() => {
	      this.#boundaryMicrotaskPending = !1, this.#retryBoundaryAfterPending = !1, this.#pages.clear(), this.#records = Object.freeze([]), delete this.#list.dataset.collectionScrollLoading;
	    });
	  }
	  get records() {
	    return this.#records;
	  }
	  project(input) {
	    const page = Math.max(0, Math.floor(input.page));
	    input.streamKey !== this.#streamKey && (this.#streamKey = input.streamKey, this.#pages.clear(), this.#list.scrollTop = 0);
	    for (const index of [...this.#pages.keys()])
	      index > page && this.#pages.delete(index);
	    input.loading || this.#pages.set(page, Object.freeze([...input.records]));
	    const records = [], identities = /* @__PURE__ */ new Set();
	    for (let index = 0; index <= page; index += 1)
	      for (const record of this.#pages.get(index) ?? []) {
	        const identity = this.#identity(record);
	        identities.has(identity) || (identities.add(identity), records.push(record));
	      }
	    return this.#records = Object.freeze(records), this.sync({ loading: input.loading, hasMore: input.hasMore }), this.#records;
	  }
	  sync(state) {
	    this.#loading = state.loading, this.#hasMore = state.hasMore, this.#list.dataset.collectionScrollLoading = String(
	      state.loading || this.#pending
	    ), this.#scheduleBoundaryCheck();
	  }
	  update(identity, replace) {
	    this.replaceWhere(
	      (record) => this.#identity(record) === identity,
	      replace
	    );
	  }
	  replaceWhere(predicate, replace) {
	    for (const [page, records] of this.#pages) {
	      let changed = !1;
	      const next = records.map((record) => predicate(record) ? (changed = !0, replace(record)) : record);
	      changed && this.#pages.set(page, Object.freeze(next));
	    }
	    this.#rebuildRecords();
	  }
	  forget(predicate) {
	    for (const [page, records] of this.#pages)
	      this.#pages.set(page, Object.freeze(records.filter((record) => !predicate(record))));
	    this.#rebuildRecords();
	  }
	  #rebuildRecords() {
	    const records = [], identities = /* @__PURE__ */ new Set();
	    for (const page of [...this.#pages.keys()].sort((left, right) => left - right))
	      for (const record of this.#pages.get(page) ?? []) {
	        const identity = this.#identity(record);
	        identities.has(identity) || (identities.add(identity), records.push(record));
	      }
	    this.#records = Object.freeze(records);
	  }
	  #scheduleBoundaryCheck() {
	    this.scope.destroyed || this.#boundaryMicrotaskPending || (this.#boundaryMicrotaskPending = !0, queueMicrotask(() => {
	      this.#boundaryMicrotaskPending = !1, this.scope.destroyed || this.#requestMoreIfNearEnd();
	    }));
	  }
	  #requestMoreIfNearEnd(explicitScroll = !1) {
	    const scrollTop = Math.max(0, Number(this.#list.scrollTop) || 0), clientHeight = Math.max(0, Number(this.#list.clientHeight) || 0), scrollHeight = Math.max(0, Number(this.#list.scrollHeight) || 0);
	    (explicitScroll || clientHeight > 0) && scrollTop + clientHeight >= scrollHeight - 96 && this.#requestMore();
	  }
	  #requestMore() {
	    if (this.#pending) {
	      this.#retryBoundaryAfterPending = !0;
	      return;
	    }
	    this.#loading || !this.#hasMore || (this.#pending = !0, this.#list.dataset.collectionScrollLoading = "true", Promise.resolve(this.#loadMore()).catch(this.#onError).finally(() => {
	      this.#pending = !1, this.#list.dataset.collectionScrollLoading = String(this.#loading), this.#retryBoundaryAfterPending && (this.#retryBoundaryAfterPending = !1, this.#scheduleBoundaryCheck());
	    }));
	  }
	}
	class ReaderCollectionFloatingWindow {
	  scope;
	  frame;
	  #toggle;
	  #content;
	  #isOpen;
	  #requestClose;
	  constructor(options) {
	    this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#toggle = options.toggle, this.#content = options.content, this.#isOpen = options.isOpen, this.#requestClose = options.requestClose, this.frame = new import_reader_floating_window_frame.ReaderFloatingWindowFrame({
	      document: options.document,
	      mount: options.mount,
	      title: options.title,
	      ariaLabel: options.ariaLabel,
	      icon: options.icon,
	      variant: options.variant,
	      tabId: options.variant,
	      tabOrder: options.tabOrder,
	      requestOpen: options.requestOpen,
	      zIndex: 2147483584,
	      ...options.geometryStorage ? { geometryStorage: options.geometryStorage } : {},
	      geometryStorageKey: READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY,
	      policy: READER_COLLECTION_FLOATING_WINDOW_POLICY,
	      placement: READER_COLLECTION_FLOATING_WINDOW_PLACEMENT,
	      ...options.notify ? { notify: options.notify } : {},
	      onClose: () => {
	        this.#content.hidden = !0, this.#requestClose();
	      },
	      parentScope: this.scope
	    }), this.frame.element.classList.add("is-user-observation-list"), this.#content.hidden = !0, this.frame.body.append(this.#content), this.#toggle.setAttribute("aria-haspopup", "dialog"), this.scope.listen(options.document, "pointerdown", (event) => {
	      this.#isOpen() && this.frame.dismissFromPointerEvent(event);
	    }, !0), this.scope.listen(options.document, "keydown", (eventValue) => {
	      const event = eventValue;
	      !this.#isOpen() || !this.frame.dismissFromEscapeEvent(event) || !this.frame.active && this.#toggle.isConnected && this.#toggle.focus({ preventScroll: !0 });
	    }, !0), this.scope.add(() => {
	      this.#toggle.setAttribute("aria-expanded", "false");
	    });
	  }
	  get isOpen() {
	    return this.frame.isOpen;
	  }
	  attachHeaderActions(options) {
	    options.root.classList.add("ldp-reader-floating-window-extra-actions"), options.root.setAttribute("role", "group"), options.root.setAttribute("aria-label", options.label);
	    for (const button of options.buttons)
	      button.classList.add("ldp-reader-floating-window-extra-action");
	    const divider = this.frame.element.ownerDocument.createElement("span");
	    divider.className = "ldp-reader-floating-window-action-divider", divider.setAttribute("aria-hidden", "true"), options.root.append(divider), this.frame.actions.prepend(options.root);
	  }
	  sync(open) {
	    if (this.scope.destroyed) return;
	    const opening = open && !this.frame.isOpen;
	    this.#content.hidden = !open, this.#toggle.setAttribute("aria-expanded", String(open)), opening ? this.frame.open() : open || this.frame.close();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
	class ReaderCollectionProgressView {
	  scope;
	  element;
	  #document;
	  #retry;
	  #onError;
	  #retrying = !1;
	  constructor(options) {
	    this.#document = options.document, this.#retry = options.retry, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.element = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-user-observation-progress ldp-collection-cache-progress"
	    ), this.element.hidden = !0, this.scope.listen(this.element, "click", (event) => {
	      const target = event.target, retry = typeof target?.closest == "function" ? target.closest(
	        ".ldp-user-observation-progress-retry"
	      ) : null;
	      !retry || !this.element.contains(retry) || this.#retrying || (event.preventDefault(), event.stopPropagation(), this.#retrying = !0, this.#syncRetryButton(retry), Promise.resolve().then(() => this.#retry()).catch(this.#onError).finally(() => {
	        this.#retrying = !1;
	        const current = this.element.querySelector(
	          ".ldp-user-observation-progress-retry"
	        );
	        current && this.#syncRetryButton(current);
	      }));
	    });
	  }
	  #syncRetryButton(button) {
	    button.disabled = this.#retrying, button.dataset.ldpRequestBusy = this.#retrying ? "1" : "0", button.setAttribute("aria-busy", String(this.#retrying)), button.replaceChildren(
	      (0, import_reader_icon.createReaderIcon)(
	        this.#document,
	        this.#retrying ? "loader" : "rotate-ccw"
	      ),
	      this.#document.createTextNode(this.#retrying ? "重试中" : "重试")
	    );
	  }
	  render(snapshot) {
	    if (this.element.hidden = !snapshot.visible, !snapshot.visible) {
	      this.element.replaceChildren();
	      return;
	    }
	    this.element.dataset.phase = snapshot.state;
	    const copy = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-user-observation-progress-copy"
	    );
	    if (copy.append(
	      (0, import_html_element.htmlElement)(this.#document, "strong", "", snapshot.label),
	      (0, import_html_element.htmlElement)(this.#document, "span", "", snapshot.detail)
	    ), snapshot.retryable) {
	      const retry = this.#document.createElement("button");
	      retry.type = "button", retry.className = "ldp-user-observation-progress-retry", this.#syncRetryButton(retry), copy.append(retry);
	    }
	    const total = Math.max(1, Math.floor(snapshot.total)), completed = Math.max(
	      0,
	      Math.min(total, Number(snapshot.completed) || 0)
	    ), completeSegments = Math.floor(completed), partialProgress = completed - completeSegments, segments = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-user-observation-progress-segments"
	    );
	    segments.setAttribute("role", "progressbar"), segments.setAttribute("aria-label", "后台缓存进度"), segments.setAttribute("aria-valuemin", "0"), segments.setAttribute("aria-valuemax", String(total)), segments.setAttribute(
	      "aria-valuenow",
	      String(Math.round(completed * 1e3) / 1e3)
	    ), segments.setAttribute("aria-valuetext", snapshot.valueText), segments.style.gridTemplateColumns = `repeat(${total}, minmax(0, 1fr))`, segments.append(...Array.from({ length: total }, (_, index) => {
	      const segment = (0, import_html_element.htmlElement)(this.#document, "span", "");
	      return index < completeSegments ? segment.classList.add("is-complete") : index === completeSegments && completed < total && (segment.classList.add(
	        snapshot.state === "error" ? "is-error" : snapshot.state === "waiting" ? "is-waiting" : "is-active"
	      ), partialProgress > 0 && snapshot.state !== "error" && (segment.classList.add("is-partial"), segment.style.setProperty(
	        "--ldp-collection-progress",
	        `${Math.round(partialProgress * 100)}%`
	      ))), segment;
	    })), this.element.replaceChildren(copy, segments);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
}, "2d28396e95b4a7c0c9dca6205a0446b7d172f0fb6ef10cb582d94e4e030c5957");

/* Source: lite/src/collection/reader-collection-hydration.ts */
runtime.register("src/collection/reader-collection-hydration.js", function(module, exports, require) {
	var reader_collection_hydration_exports = {};
	__export(reader_collection_hydration_exports, {
	  readerCollectionResumePosition: () => readerCollectionResumePosition,
	  runReaderCollectionHydrationLease: () => runReaderCollectionHydrationLease,
	  runReaderCollectionWorkers: () => runReaderCollectionWorkers
	});
	module.exports = __toCommonJS(reader_collection_hydration_exports);
	function positiveSafeInteger(value, fallback) {
	  const normalized = Math.floor(Number(value));
	  return Number.isSafeInteger(normalized) && normalized > 0 ? normalized : fallback;
	}
	function nonNegativeSafeInteger(value) {
	  const normalized = Math.floor(Number(value));
	  return Number.isSafeInteger(normalized) && normalized >= 0 ? normalized : null;
	}
	function readerCollectionResumePosition(checkpoint, targetPageSizeValue, legacyPageSizeValue = targetPageSizeValue) {
	  const targetPageSize = positiveSafeInteger(targetPageSizeValue, 1), legacyPageSize = positiveSafeInteger(
	    checkpoint.sourcePageSize,
	    positiveSafeInteger(legacyPageSizeValue, targetPageSize)
	  ), storedOffset = nonNegativeSafeInteger(checkpoint.sourceOffset), storedPage = nonNegativeSafeInteger(checkpoint.sourceNextPage) ?? 0, offset = storedOffset ?? storedPage * legacyPageSize;
	  return Object.freeze({
	    page: Math.floor(offset / targetPageSize),
	    offset
	  });
	}
	async function runReaderCollectionWorkers(options) {
	  const concurrency = positiveSafeInteger(options.concurrency, 1), maxTasks = options.maxTasks === void 0 ? Number.MAX_SAFE_INTEGER : Math.max(0, Math.floor(Number(options.maxTasks) || 0));
	  let started = 0, completed = 0;
	  const worker = async () => {
	    for (; started < maxTasks && (options.shouldContinue?.() ?? !0); ) {
	      const key = options.claim();
	      if (key === null) return;
	      started += 1;
	      try {
	        await options.run(key), completed += 1;
	      } finally {
	        options.release?.(key);
	      }
	    }
	  };
	  return await Promise.all(Array.from(
	    { length: Math.min(concurrency, Math.max(1, maxTasks)) },
	    () => worker()
	  )), Object.freeze({ started, completed });
	}
	async function runReaderCollectionHydrationLease(options) {
	  const coordination = options.coordination, token = String(options.token).trim();
	  if (!coordination || !token)
	    return await options.beforeRun?.(), options.signal?.throwIfAborted(), await options.run(), "producer";
	  options.signal?.throwIfAborted();
	  const lease = await coordination.acquireFlight(token);
	  if (options.signal?.throwIfAborted(), !lease.producer)
	    return await coordination.waitForFlight(
	      token,
	      options.signal
	    ) ? "consumer" : "consumer-timeout";
	  const heartbeatMs = positiveSafeInteger(options.heartbeatMs, 1e4), heartbeat = lease.coordinated ? setInterval(() => {
	    coordination.renewFlight(lease).catch(
	      options.onError ?? (() => {
	      })
	    );
	  }, heartbeatMs) : null;
	  try {
	    return await options.beforeRun?.(), options.signal?.throwIfAborted(), await options.run(), "producer";
	  } finally {
	    heartbeat !== null && clearInterval(heartbeat), await coordination.releaseFlight(lease);
	  }
	}
}, "166c8ac2e1883e299191ca0bc5e86c31f6bbbe06974a722778507882459f29ca");

/* Source: lite/src/collection/reader-header-popover-position.ts */
runtime.register("src/collection/reader-header-popover-position.js", function(module, exports, require) {
	var reader_header_popover_position_exports = {};
	__export(reader_header_popover_position_exports, {
	  ReaderHeaderPopoverPosition: () => ReaderHeaderPopoverPosition,
	  ReaderHeaderPopoverSurface: () => ReaderHeaderPopoverSurface
	});
	module.exports = __toCommonJS(reader_header_popover_position_exports);
	var import_floating_surface_wheel = require("../dom/floating-surface-wheel.js");
	class ReaderHeaderPopoverPosition {
	  scope;
	  #document;
	  #toggle;
	  #popover;
	  #preferredPlacement;
	  #frame = null;
	  constructor(options) {
	    this.#document = options.document, this.#toggle = options.toggle, this.#popover = options.popover, this.#preferredPlacement = options.preferredPlacement ?? "bottom", this.scope = options.parentScope.child();
	    const viewport = this.#document.defaultView;
	    viewport && this.scope.listen(viewport, "resize", () => this.schedule());
	    for (const type of [
	      "ldp-reader-window-change",
	      "ldp-reader-workspace-change"
	    ])
	      this.scope.listen(options.root, type, () => this.schedule());
	    this.scope.add(() => {
	      this.#frame !== null && viewport && viewport.cancelAnimationFrame(this.#frame), this.#frame = null, this.#popover.style.removeProperty("left"), this.#popover.style.removeProperty("top"), delete this.#popover.dataset.placement;
	    });
	  }
	  position() {
	    const viewport = this.#document.defaultView;
	    if (!viewport || this.scope.destroyed || this.#popover.hidden || !this.#popover.isConnected) return;
	    const buttonRect = this.#toggle.getBoundingClientRect(), popoverRect = this.#popover.getBoundingClientRect(), gap = 8, margin = 12, left = Math.max(
	      margin,
	      Math.min(
	        viewport.innerWidth - popoverRect.width - margin,
	        buttonRect.right - popoverRect.width
	      )
	    ), below = buttonRect.bottom + gap, above = buttonRect.top - popoverRect.height - gap, spaceBelow = viewport.innerHeight - margin - below, spaceAbove = buttonRect.top - gap - margin, belowFits = popoverRect.height <= spaceBelow, aboveFits = popoverRect.height <= spaceAbove, placeAbove = this.#preferredPlacement === "top" ? aboveFits || !belowFits && spaceAbove >= spaceBelow : !belowFits && (aboveFits || spaceAbove > spaceBelow), nextLeft = `${Math.round(left)}px`, nextTop = `${Math.round(placeAbove ? Math.max(margin, above) : below)}px`;
	    this.#popover.style.left !== nextLeft && (this.#popover.style.left = nextLeft), this.#popover.style.top !== nextTop && (this.#popover.style.top = nextTop), this.#popover.dataset.placement = placeAbove ? "top" : "bottom";
	  }
	  schedule() {
	    const viewport = this.#document.defaultView;
	    !viewport || this.scope.destroyed || this.#popover.hidden || this.#frame !== null || (this.#frame = viewport.requestAnimationFrame(() => {
	      this.#frame = null, this.position();
	    }));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
	class ReaderHeaderPopoverSurface {
	  scope;
	  #document;
	  #toggle;
	  #popover;
	  #isOpen;
	  #requestClose;
	  #position;
	  #open = !1;
	  constructor(options) {
	    this.#document = options.document, this.#toggle = options.toggle, this.#popover = options.popover, this.#isOpen = options.isOpen, this.#requestClose = options.requestClose, this.scope = options.parentScope.child(), this.#position = new ReaderHeaderPopoverPosition({
	      document: options.document,
	      root: options.root,
	      toggle: options.toggle,
	      popover: options.popover,
	      parentScope: this.scope,
	      ...options.preferredPlacement === void 0 ? {} : { preferredPlacement: options.preferredPlacement }
	    }), this.scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(this.#popover)), this.scope.listen(
	      this.#document,
	      options.outsideEvent ?? "pointerdown",
	      (event) => {
	        !this.#isOpen() || event.composedPath().includes(this.#toggle) || event.composedPath().includes(this.#popover) || this.#requestClose();
	      },
	      options.outsideCapture ?? !0
	    ), this.scope.listen(this.#document, "keydown", (eventValue) => {
	      const event = eventValue;
	      event.key !== "Escape" || !this.#isOpen() || (event.preventDefault(), event.stopImmediatePropagation(), this.#requestClose(), this.#toggle.isConnected && this.#toggle.focus({ preventScroll: !0 }));
	    }), this.scope.add(() => {
	      this.#open = !1, this.#popover.hidden = !0, this.#toggle.setAttribute("aria-expanded", "false");
	    });
	  }
	  sync(open) {
	    if (this.scope.destroyed) return;
	    const opening = open && !this.#open;
	    this.#open = open, this.#popover.hidden = !open, this.#toggle.setAttribute("aria-expanded", String(open)), opening && this.#position.position();
	  }
	  position() {
	    this.#position.position();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
}, "b013b73a3e23c577b69c4e0505a798ad10bb1227c0fedf9e81a8aef8e00cc847");

/* Source: lite/src/collection/reader-popover-filter-controls.ts */
runtime.register("src/collection/reader-popover-filter-controls.js", function(module, exports, require) {
	var reader_popover_filter_controls_exports = {};
	__export(reader_popover_filter_controls_exports, {
	  ReaderPopoverFilterDisclosure: () => ReaderPopoverFilterDisclosure,
	  createReaderPopoverSearch: () => createReaderPopoverSearch,
	  createReaderPopoverSearchTools: () => createReaderPopoverSearchTools,
	  createReaderTaxonomyFilter: () => createReaderTaxonomyFilter,
	  syncReaderFilterOptions: () => syncReaderFilterOptions
	});
	module.exports = __toCommonJS(reader_popover_filter_controls_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_event_target = require("../dom/event-target.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_collection_filter_model = require("./reader-collection-filter-model.js");
	function ownerLabel(owner) {
	  return owner === "notification" ? "消息" : owner === "history" ? "浏览历史" : "收藏与回应";
	}
	function monthStart(value = /* @__PURE__ */ new Date()) {
	  return new Date(value.getFullYear(), value.getMonth(), 1);
	}
	function closestTarget(event, selector) {
	  const target = event.target;
	  return typeof target?.closest == "function" ? target.closest(selector) : null;
	}
	function taxonomyLabel(name) {
	  return name === "category" ? "类别" : "标签";
	}
	function createReaderPopoverSearch(document, name, placeholder, label, clearLabel, renderIcon) {
	  const root = document.createElement("label");
	  root.className = "ldp-popover-search", root.append((0, import_reader_icon.renderReaderIcon)(document, "search", renderIcon));
	  const input = document.createElement("input");
	  input.className = `ldp-popover-search-input ldp-${name}-search`, input.type = "search", input.autocomplete = "off", input.spellcheck = !1, input.placeholder = placeholder, input.setAttribute("aria-label", label);
	  const clear = document.createElement("button");
	  return clear.className = `ldp-popover-search-clear ldp-${name}-search-clear`, clear.type = "button", clear.setAttribute("aria-label", clearLabel), clear.append((0, import_reader_icon.renderReaderIcon)(document, "x", renderIcon)), clear.hidden = !0, root.append(input, clear), Object.freeze({ root, input, clear });
	}
	function createReaderTaxonomyFilter(document, owner, name) {
	  const label = taxonomyLabel(name), select = document.createElement("select");
	  select.className = [
	    "ldp-reader-select",
	    "ldp-popover-taxonomy-filter",
	    `ldp-${owner}-taxonomy-filter`,
	    `ldp-${owner}-${name}-filter`
	  ].join(" "), select.setAttribute("aria-label", `按${label}筛选${ownerLabel(owner)}`);
	  const all = document.createElement("option");
	  return all.value = "", all.textContent = label, select.append(all), select;
	}
	function createReaderPopoverSearchTools(document, owner, placeholder, label, clearLabel, renderIcon) {
	  const search = createReaderPopoverSearch(
	    document,
	    owner,
	    placeholder,
	    label,
	    clearLabel,
	    renderIcon
	  ), category = createReaderTaxonomyFilter(
	    document,
	    owner,
	    "category"
	  ), tag = createReaderTaxonomyFilter(document, owner, "tag"), calendarToggle = document.createElement("button");
	  calendarToggle.type = "button", calendarToggle.className = "ldp-user-observation-calendar-toggle", calendarToggle.setAttribute("aria-label", `按日期筛选${ownerLabel(owner)}`), calendarToggle.setAttribute("aria-haspopup", "dialog"), calendarToggle.setAttribute("aria-expanded", "false"), calendarToggle.append(
	    (0, import_reader_icon.renderReaderIcon)(document, "clock", renderIcon),
	    document.createTextNode("活动日历")
	  );
	  const calendar = document.createElement("div");
	  calendar.className = "ldp-user-observation-calendar", calendar.hidden = !0, calendar.setAttribute("role", "dialog"), calendar.setAttribute("aria-label", `${ownerLabel(owner)}活动日历`);
	  const calendarHeader = document.createElement("div");
	  calendarHeader.className = "ldp-user-observation-calendar-head";
	  const previousMonth = document.createElement("button");
	  previousMonth.type = "button", previousMonth.dataset.userObservationCalendarMonth = "-1", previousMonth.setAttribute("aria-label", "上个月"), previousMonth.append((0, import_reader_icon.renderReaderIcon)(document, "chevron-left", renderIcon));
	  const calendarTitle = document.createElement("strong");
	  calendarTitle.className = "ldp-user-observation-calendar-title";
	  const nextMonth = document.createElement("button");
	  nextMonth.type = "button", nextMonth.dataset.userObservationCalendarMonth = "1", nextMonth.setAttribute("aria-label", "下个月"), nextMonth.append((0, import_reader_icon.renderReaderIcon)(document, "chevron-right", renderIcon));
	  const today = document.createElement("button");
	  today.type = "button", today.dataset.userObservationCalendarToday = "", today.textContent = "今天";
	  const clearDate = document.createElement("button");
	  clearDate.type = "button", clearDate.dataset.userObservationCalendarClear = "", clearDate.textContent = "清除", calendarHeader.append(previousMonth, calendarTitle, nextMonth, today, clearDate);
	  const weekdays = document.createElement("div");
	  weekdays.className = "ldp-user-observation-calendar-weekdays", weekdays.setAttribute("aria-hidden", "true");
	  for (const text of ["一", "二", "三", "四", "五", "六", "日"]) {
	    const weekday = document.createElement("span");
	    weekday.textContent = text, weekdays.append(weekday);
	  }
	  const calendarGrid = document.createElement("div");
	  calendarGrid.className = "ldp-user-observation-calendar-grid", calendar.append(calendarHeader, weekdays, calendarGrid);
	  const sort = document.createElement("select");
	  sort.className = "ldp-reader-select ldp-user-observation-sort-filter", sort.setAttribute("aria-label", `${ownerLabel(owner)}排序字段`);
	  const sortOptions = Object.freeze(owner === "history" ? [
	    ["recent-viewed", "最近查看时间"],
	    ["first-viewed", "首次查看时间"]
	  ] : [["time", "时间排序"]]);
	  for (const [value, text] of sortOptions) {
	    const option = document.createElement("option");
	    option.value = value, option.textContent = text, sort.append(option);
	  }
	  const sortDirection = document.createElement("button");
	  sortDirection.type = "button", sortDirection.className = "ldp-user-observation-sort-direction", sortDirection.append(
	    (0, import_reader_icon.renderReaderIcon)(document, "chevron-down", renderIcon),
	    document.createTextNode("降序")
	  );
	  const reset = document.createElement("button");
	  reset.type = "button", reset.className = "ldp-user-observation-filter-reset", reset.textContent = "重置";
	  const filters = document.createElement("div");
	  filters.className = `ldp-popover-taxonomy-filters ldp-user-observation-filter-panel ldp-user-observation-taxonomy-filters ldp-${owner}-taxonomy-filters`, filters.hidden = !0, filters.append(
	    category,
	    tag,
	    calendarToggle,
	    sort,
	    sortDirection,
	    reset,
	    calendar
	  );
	  const filterToggle = document.createElement("button");
	  filterToggle.type = "button", filterToggle.className = `ldp-user-observation-filter-toggle ldp-${owner}-filter-toggle`, filterToggle.setAttribute("aria-label", "综合筛选与排序"), filterToggle.setAttribute("aria-expanded", "false"), filterToggle.title = "综合筛选与排序", filterToggle.append((0, import_reader_icon.renderReaderIcon)(
	    document,
	    "header-settings",
	    renderIcon
	  ));
	  const root = document.createElement("div");
	  return root.className = `ldp-popover-search-tools ldp-user-observation-detail-tools ldp-${owner}-search-tools`, search.root.classList.add("ldp-user-observation-search", "is-detail"), root.append(search.root, filterToggle, filters), Object.freeze({
	    root,
	    search,
	    filters,
	    filterToggle,
	    category,
	    tag,
	    calendarToggle,
	    calendar,
	    sort,
	    sortDirection,
	    reset
	  });
	}
	class ReaderPopoverFilterDisclosure {
	  scope;
	  #document;
	  #toggle;
	  #panel;
	  #calendarToggle;
	  #calendar;
	  #calendarTitle;
	  #calendarGrid;
	  #sort;
	  #sortDirection;
	  #reset;
	  #onDateChange;
	  #onSortChange;
	  #onDirectionChange;
	  #onReset;
	  #date = "";
	  #direction = "desc";
	  #dayCounts = /* @__PURE__ */ new Map();
	  #calendarMonth = monthStart();
	  constructor(options) {
	    const tools = options.search.closest(
	      ".ldp-popover-search-tools"
	    ), toggle = tools?.querySelector(
	      ".ldp-user-observation-filter-toggle"
	    ), panel = tools?.querySelector(
	      ".ldp-user-observation-filter-panel"
	    ), calendarToggle = tools?.querySelector(
	      ".ldp-user-observation-calendar-toggle"
	    ), calendar = tools?.querySelector(
	      ".ldp-user-observation-calendar"
	    ), calendarTitle = calendar?.querySelector(
	      ".ldp-user-observation-calendar-title"
	    ), calendarGrid = calendar?.querySelector(
	      ".ldp-user-observation-calendar-grid"
	    ), sort = tools?.querySelector(
	      ".ldp-user-observation-sort-filter"
	    ), sortDirection = tools?.querySelector(
	      ".ldp-user-observation-sort-direction"
	    ), reset = tools?.querySelector(
	      ".ldp-user-observation-filter-reset"
	    );
	    if (!toggle || !panel || !calendarToggle || !calendar || !calendarTitle || !calendarGrid || !sort || !sortDirection || !reset) throw new Error("集合搜索缺少完整筛选控件");
	    this.#document = options.search.ownerDocument, this.#toggle = toggle, this.#panel = panel, this.#calendarToggle = calendarToggle, this.#calendar = calendar, this.#calendarTitle = calendarTitle, this.#calendarGrid = calendarGrid, this.#sort = sort, this.#sortDirection = sortDirection, this.#reset = reset, this.#onDateChange = options.onDateChange, this.#onSortChange = options.onSortChange, this.#onDirectionChange = options.onDirectionChange, this.#onReset = options.onReset, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.listen(toggle, "click", () => {
	      const expanded = this.#panel.hidden === !0;
	      this.#panel.hidden = !expanded, this.#toggle.setAttribute("aria-expanded", String(expanded)), this.#toggle.classList.toggle("is-open", expanded), expanded || this.#setCalendarExpanded(!1);
	    }), this.scope.listen(calendarToggle, "click", () => {
	      this.#setCalendarExpanded(this.#calendar.hidden === !0);
	    }), this.scope.listen(calendar, "click", (event) => {
	      this.#onCalendarClick(event);
	    }), this.scope.listen(sort, "change", () => {
	      this.#onSortChange(this.#sort.value);
	    }), this.scope.listen(sortDirection, "click", () => {
	      this.#onDirectionChange(this.#direction === "desc" ? "asc" : "desc");
	    }), this.scope.listen(this.#reset, "click", () => {
	      this.#setCalendarExpanded(!1), this.#onReset();
	    }), this.scope.listen(this.#document, "pointerdown", (event) => {
	      this.#calendar.hidden || (0, import_event_target.eventPathIncludes)(event, this.#calendar) || (0, import_event_target.eventPathIncludes)(event, this.#calendarToggle) || this.#setCalendarExpanded(!1);
	    }, !0);
	  }
	  sync(snapshot) {
	    this.#date = snapshot.date, this.#direction = snapshot.direction, this.#dayCounts = snapshot.dayCounts;
	    for (const option of this.#sort.options)
	      option.selected = option.value === snapshot.sort;
	    this.#toggle.classList.toggle("has-active-filter", snapshot.active), this.#syncCalendarToggle(), this.#syncSortDirectionButton(), this.#calendar.hidden || this.#renderCalendar();
	  }
	  #setCalendarExpanded(expanded) {
	    if (this.#calendar.hidden = !expanded, this.#calendarToggle.setAttribute("aria-expanded", String(expanded)), this.#calendarToggle.classList.toggle("is-open", expanded), expanded) {
	      if (this.#date) {
	        const selected = /* @__PURE__ */ new Date(`${this.#date}T00:00:00`);
	        Number.isFinite(selected.getTime()) && (this.#calendarMonth = monthStart(selected));
	      }
	      this.#renderCalendar(), this.#positionCalendar();
	      return;
	    }
	    for (const property of [
	      "top",
	      "left",
	      "transform",
	      "--ldp-user-observation-calendar-anchor-x"
	    ]) this.#calendar.style.removeProperty(property);
	    this.#calendar.removeAttribute("data-placement");
	  }
	  #positionCalendar() {
	    const boundary = this.#panel.closest(
	      ".ldp-reader-floating-window-body"
	    )?.getBoundingClientRect(), panel = this.#panel.getBoundingClientRect(), toggle = this.#calendarToggle.getBoundingClientRect(), calendar = this.#calendar.getBoundingClientRect();
	    if (!boundary || [
	      boundary.top,
	      boundary.right,
	      boundary.bottom,
	      boundary.left,
	      panel.top,
	      panel.bottom,
	      toggle.left,
	      toggle.right,
	      calendar.width,
	      calendar.height
	    ].some((value) => !Number.isFinite(value)) || boundary.width <= 0 || boundary.height <= 0 || calendar.width <= 0 || calendar.height <= 0) return;
	    const inset = 8, gap = 6, boundaryTop = boundary.top + inset, boundaryBottom = boundary.bottom - inset, belowTop = panel.bottom + gap, aboveTop = panel.top - gap - calendar.height, belowSpace = boundaryBottom - belowTop, aboveSpace = panel.top - gap - boundaryTop, placeAbove = belowSpace < calendar.height && aboveSpace > belowSpace, maximumTop = Math.max(boundaryTop, boundaryBottom - calendar.height), viewportTop = Math.min(
	      Math.max(placeAbove ? aboveTop : belowTop, boundaryTop),
	      maximumTop
	    ), minimumLeft = boundary.left + inset, maximumLeft = Math.max(
	      minimumLeft,
	      boundary.right - inset - calendar.width
	    ), toggleCenter = (toggle.left + toggle.right) / 2, viewportLeft = Math.min(
	      Math.max(toggleCenter - calendar.width / 2, minimumLeft),
	      maximumLeft
	    ), anchorX = Math.min(
	      Math.max(toggleCenter - viewportLeft, 14),
	      calendar.width - 14
	    );
	    this.#calendar.style.top = `${Math.round(viewportTop - panel.top)}px`, this.#calendar.style.left = `${Math.round(viewportLeft - panel.left)}px`, this.#calendar.style.transform = "none", this.#calendar.style.setProperty(
	      "--ldp-user-observation-calendar-anchor-x",
	      `${Math.round(anchorX)}px`
	    ), this.#calendar.dataset.placement = placeAbove ? "top" : "bottom";
	  }
	  #onCalendarClick(event) {
	    const target = closestTarget(
	      event,
	      "[data-user-observation-calendar-month],[data-user-observation-calendar-day],[data-user-observation-calendar-today],[data-user-observation-calendar-clear]"
	    );
	    if (!target) return;
	    const monthOffset = target.dataset.userObservationCalendarMonth;
	    if (monthOffset !== void 0) {
	      const offset = Number(monthOffset);
	      if (!Number.isInteger(offset) || offset === 0) return;
	      this.#calendarMonth = new Date(
	        this.#calendarMonth.getFullYear(),
	        this.#calendarMonth.getMonth() + offset,
	        1
	      ), this.#renderCalendar();
	      return;
	    }
	    if (target.dataset.userObservationCalendarToday !== void 0) {
	      const now = /* @__PURE__ */ new Date();
	      this.#calendarMonth = monthStart(now);
	      const day2 = (0, import_reader_collection_filter_model.readerCollectionDateKey)(now.getTime());
	      (this.#dayCounts.get(day2) ?? 0) > 0 ? this.#onDateChange(day2) : this.#renderCalendar();
	      return;
	    }
	    if (target.dataset.userObservationCalendarClear !== void 0) {
	      this.#onDateChange("");
	      return;
	    }
	    const day = target.dataset.userObservationCalendarDay;
	    day && (this.#dayCounts.get(day) ?? 0) > 0 && this.#onDateChange(day);
	  }
	  #syncCalendarToggle() {
	    const label = this.#date || "活动日历", span = this.#document.createElement("span");
	    span.textContent = label, this.#calendarToggle.replaceChildren(
	      (0, import_reader_icon.renderReaderIcon)(this.#document, "clock"),
	      span
	    ), this.#calendarToggle.title = this.#date ? `当前筛选 ${this.#date}` : "按当前分类查看每月活跃程度";
	  }
	  #syncSortDirectionButton() {
	    const ascending = this.#direction === "asc";
	    this.#sortDirection.replaceChildren(
	      (0, import_reader_icon.renderReaderIcon)(
	        this.#document,
	        ascending ? "chevron-up" : "chevron-down"
	      ),
	      this.#document.createTextNode(ascending ? "升序" : "降序")
	    ), this.#sortDirection.setAttribute(
	      "aria-label",
	      ascending ? "切换为降序" : "切换为升序"
	    ), this.#sortDirection.title = ascending ? "当前升序" : "当前降序";
	  }
	  #renderCalendar() {
	    const year = this.#calendarMonth.getFullYear(), month = this.#calendarMonth.getMonth(), today = (0, import_reader_collection_filter_model.readerCollectionDateKey)(Date.now());
	    this.#calendarTitle.textContent = `${year}年${String(month + 1).padStart(2, "0")}月`;
	    const firstWeekday = (new Date(year, month, 1).getDay() + 6) % 7, daysInMonth = new Date(year, month + 1, 0).getDate(), monthPrefix = `${year}-${String(month + 1).padStart(2, "0")}-`, maximum = Math.max(
	      0,
	      ...[...this.#dayCounts].filter(([day]) => day.startsWith(monthPrefix)).map(([, count]) => count)
	    ), cells = [];
	    for (let index = 0; index < 42; index += 1) {
	      const dayNumber = index - firstWeekday + 1;
	      if (dayNumber < 1 || dayNumber > daysInMonth) {
	        const empty = this.#document.createElement("span");
	        empty.className = "ldp-user-observation-calendar-empty", empty.setAttribute("aria-hidden", "true"), cells.push(empty);
	        continue;
	      }
	      const day = `${monthPrefix}${String(dayNumber).padStart(2, "0")}`, count = this.#dayCounts.get(day) ?? 0, level = count === 0 || maximum === 0 ? 0 : Math.max(1, Math.ceil(count / maximum * 4)), button = this.#document.createElement("button");
	      button.type = "button", button.className = "ldp-user-observation-calendar-day", button.dataset.userObservationCalendarDay = day, button.dataset.activityLevel = String(level), button.disabled = count <= 0, button.classList.toggle("is-selected", day === this.#date), button.setAttribute("aria-pressed", String(day === this.#date)), button.setAttribute(
	        "aria-label",
	        `${month + 1}月${dayNumber}日,${count} 条当前分类记录`
	      ), day === today && button.setAttribute("aria-current", "date");
	      const dayLabel = this.#document.createElement("span");
	      dayLabel.textContent = String(dayNumber);
	      const countLabel = this.#document.createElement("small");
	      countLabel.textContent = count ? String(count) : "", button.append(dayLabel, countLabel), cells.push(button);
	    }
	    this.#calendarGrid.replaceChildren(...cells);
	    const clear = this.#calendar.querySelector(
	      "[data-user-observation-calendar-clear]"
	    );
	    clear && (clear.disabled = !this.#date);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	}
	function syncReaderFilterOptions(select, allLabel, emptyLabel, options, selected) {
	  const signature = JSON.stringify(options);
	  if (select.dataset.optionSignature !== signature) {
	    const all = select.ownerDocument.createElement("option");
	    all.value = "", all.textContent = options.length ? allLabel : emptyLabel, select.replaceChildren(all, ...options.map((entry) => {
	      const option = select.ownerDocument.createElement("option");
	      return option.value = entry.value, option.textContent = `${entry.label} · ${entry.count}`, option;
	    })), select.dataset.optionSignature = signature;
	  }
	  select.disabled = options.length === 0;
	  let matched = !1;
	  for (const option of select.options) {
	    const active = !matched && option.value === selected;
	    option.selected = active, active && (matched = !0);
	  }
	  !matched && select.options[0] && (select.options[0].selected = !0);
	}
}, "d2b04a4f50afc67d91bf5c89cabfccf98c192b42a8f7bff81592e11b599dfedf");

/* Source: lite/src/collection/reader-unwanted-topic-filter-editor.ts */
runtime.register("src/collection/reader-unwanted-topic-filter-editor.js", function(module, exports, require) {
	var reader_unwanted_topic_filter_editor_exports = {};
	__export(reader_unwanted_topic_filter_editor_exports, {
	  ReaderUnwantedTopicFilterEditor: () => ReaderUnwantedTopicFilterEditor
	});
	module.exports = __toCommonJS(reader_unwanted_topic_filter_editor_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_settings_dom = require("../settings/reader-settings-dom.js"), import_reader_unwanted_topic_filter = require("./reader-unwanted-topic-filter.js");
	const RULE_TABS = Object.freeze([
	  Object.freeze({
	    name: "categories",
	    title: "主题类别",
	    description: "从宿主类别目录选择;保存稳定类别 ID。",
	    placeholder: "搜索类别名称、slug 或 ID"
	  }),
	  Object.freeze({
	    name: "labels",
	    title: "主题标签",
	    description: "从宿主 Label 搜索结果选择真实标签。",
	    placeholder: "搜索 Label"
	  }),
	  Object.freeze({
	    name: "topicAuthors",
	    title: "OP 用户",
	    description: "匹配主题作者;重名结果需选择具体 @username。",
	    placeholder: "搜索用户名称、@username 或 ID"
	  }),
	  Object.freeze({
	    name: "topicFields",
	    title: "字符匹配",
	    description: "匹配指定 Topic 字段,支持普通包含与正则表达式。",
	    placeholder: "输入字符或正则表达式"
	  }),
	  Object.freeze({
	    name: "postAuthors",
	    title: "楼层用户",
	    description: "只隐藏所选用户的楼层本体,保留其回复树。",
	    placeholder: "搜索用户名称、@username 或 ID"
	  })
	]), RULE_NAMES = Object.freeze(RULE_TABS.map((tab) => tab.name));
	function mutableDraft(value) {
	  const normalized = (0, import_reader_unwanted_topic_filter.normalizeReaderUnwantedTopicFilterPreferences)(value);
	  return {
	    enabled: normalized.enabled,
	    categories: [...normalized.categories],
	    labels: [...normalized.labels],
	    topicAuthors: [...normalized.topicAuthors],
	    topicFields: [...normalized.topicFields],
	    postAuthors: [...normalized.postAuthors]
	  };
	}
	function frozenDraft(value) {
	  return (0, import_reader_unwanted_topic_filter.normalizeReaderUnwantedTopicFilterPreferences)(value);
	}
	function valueKey(value) {
	  return String(value ?? "").trim().toLocaleLowerCase("zh-CN");
	}
	function valuesEqual(left, right) {
	  return left.enabled === right.enabled && RULE_NAMES.every((name) => left[name].length === right[name].length && left[name].every((entry, index) => entry === right[name][index]));
	}
	function closestTarget(event, selector) {
	  const target = event.target;
	  return typeof target?.closest == "function" ? target.closest(selector) : null;
	}
	class ReaderUnwantedTopicFilterEditor {
	  scope;
	  element;
	  #document;
	  #preferences;
	  #catalog;
	  #notify;
	  #onError;
	  #enabled;
	  #tabs = /* @__PURE__ */ new Map();
	  #activeTitle;
	  #activeDescription;
	  #cardSearch;
	  #cards;
	  #addInput;
	  #topicField;
	  #regexMode;
	  #topicOptions;
	  #addButton;
	  #lookupStatus;
	  #results;
	  #status;
	  #reset;
	  #save;
	  #active = "categories";
	  #baseline = import_reader_unwanted_topic_filter.DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES;
	  #draft = mutableDraft(import_reader_unwanted_topic_filter.DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES);
	  #candidates = Object.freeze([]);
	  #lookupTimer = null;
	  #lookupSequence = 0;
	  #saving = !1;
	  constructor(options) {
	    this.#document = options.document, this.#preferences = options.preferences, this.#catalog = options.catalog, this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.element = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-settings"
	    );
	    const workbench = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-workbench"
	    ), sidebar = (0, import_html_element.htmlElement)(
	      options.document,
	      "aside",
	      "ldp-unwanted-topic-filter-sidebar"
	    ), enabledSwitch = (0, import_reader_settings_dom.settingsSwitch)(
	      options.document,
	      "启用预设自动过滤",
	      "ldp-unwanted-filter-enabled"
	    );
	    this.#enabled = enabledSwitch.input;
	    const master = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-unwanted-topic-filter-master"
	    ), masterCopy = (0, import_html_element.htmlElement)(options.document, "span", "");
	    masterCopy.append(
	      (0, import_html_element.htmlElement)(options.document, "strong", "", "自动过滤"),
	      (0, import_html_element.htmlElement)(options.document, "small", "", "手动免打扰始终可用")
	    ), master.append(masterCopy, enabledSwitch.root);
	    const tabList = (0, import_html_element.htmlElement)(
	      options.document,
	      "nav",
	      "ldp-unwanted-topic-filter-tabs"
	    );
	    tabList.setAttribute("aria-label", "自动过滤规则类别");
	    for (const tab of RULE_TABS) {
	      const button = options.document.createElement("button");
	      button.type = "button", button.dataset.unwantedRuleTab = tab.name, button.textContent = tab.title, button.setAttribute("aria-pressed", String(tab.name === this.#active)), this.#tabs.set(tab.name, button), tabList.append(button);
	    }
	    sidebar.append(tabList);
	    const content = (0, import_html_element.htmlElement)(
	      options.document,
	      "section",
	      "ldp-unwanted-topic-filter-content"
	    ), activeHead = (0, import_html_element.htmlElement)(
	      options.document,
	      "header",
	      "ldp-unwanted-topic-filter-active-head"
	    ), activeCopy = (0, import_html_element.htmlElement)(options.document, "span", "");
	    this.#activeTitle = (0, import_html_element.htmlElement)(options.document, "strong"), this.#activeDescription = (0, import_html_element.htmlElement)(options.document, "small"), activeCopy.append(this.#activeTitle, this.#activeDescription), this.#cardSearch = options.document.createElement("input"), this.#cardSearch.type = "search", this.#cardSearch.autocomplete = "off", this.#cardSearch.placeholder = "搜索已添加规则", this.#cardSearch.setAttribute("aria-label", "搜索并定位已添加规则");
	    const cardSearchLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-unwanted-topic-filter-card-search"
	    );
	    cardSearchLabel.append(
	      (0, import_reader_icon.createReaderIcon)(options.document, "search"),
	      this.#cardSearch
	    ), activeHead.append(activeCopy, cardSearchLabel), this.#cards = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-cards"
	    ), this.#cards.setAttribute("aria-label", "已添加规则");
	    const add = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-add"
	    ), addControls = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-add-controls"
	    );
	    this.#topicOptions = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-topic-options"
	    ), this.#topicField = options.document.createElement("select"), this.#topicField.className = "ldp-reader-select", this.#topicField.setAttribute("aria-label", "字符匹配字段");
	    for (const [value, label] of [
	      ["title", "主题标题"],
	      ["category", "主题类别"],
	      ["label", "主题标签"],
	      ["user", "OP 用户"]
	    ]) {
	      const option = options.document.createElement("option");
	      option.value = value, option.textContent = label, this.#topicField.append(option);
	    }
	    const regexLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-unwanted-topic-filter-regex"
	    );
	    this.#regexMode = options.document.createElement("input"), this.#regexMode.type = "checkbox", regexLabel.append(this.#regexMode, (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "",
	      "正则表达式"
	    )), this.#topicOptions.append(this.#topicField);
	    const inputLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-unwanted-topic-filter-add-input"
	    );
	    inputLabel.append((0, import_reader_icon.createReaderIcon)(options.document, "search")), this.#addInput = options.document.createElement("input"), this.#addInput.type = "search", this.#addInput.autocomplete = "off", this.#addInput.setAttribute("aria-label", "查询并添加规则"), inputLabel.append(this.#addInput), this.#addButton = options.document.createElement("button"), this.#addButton.type = "button", this.#addButton.className = "ldp-unwanted-topic-filter-add-button", this.#addButton.append(
	      (0, import_reader_icon.createReaderIcon)(options.document, "plus"),
	      (0, import_html_element.htmlElement)(options.document, "span", "", "添加")
	    ), addControls.append(this.#topicOptions, inputLabel, this.#addButton), this.#lookupStatus = (0, import_html_element.htmlElement)(
	      options.document,
	      "p",
	      "ldp-unwanted-topic-filter-lookup-status"
	    ), this.#lookupStatus.setAttribute("role", "status"), this.#lookupStatus.setAttribute("aria-live", "polite");
	    const helpRow = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-help-row"
	    );
	    helpRow.append(this.#lookupStatus, regexLabel), this.#results = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-results"
	    ), this.#results.setAttribute("role", "listbox"), add.append(addControls, helpRow, this.#results), content.append(activeHead, this.#cards, add), workbench.append(sidebar, content);
	    const footer = (0, import_html_element.htmlElement)(
	      options.document,
	      "footer",
	      "ldp-unwanted-topic-filter-footer"
	    );
	    this.#status = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-unwanted-topic-filter-status"
	    ), this.#status.setAttribute("role", "status"), this.#status.setAttribute("aria-live", "polite");
	    const actions = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-actions"
	    );
	    this.#reset = this.#textButton("恢复默认", "rotate-ccw"), this.#save = this.#textButton("保存设置", "check"), this.#save.classList.add("ldp-unwanted-topic-filter-save"), actions.append(this.#reset, this.#save), footer.append(this.#status, master, actions), this.element.append(workbench, footer), this.#listen(), this.#preferences.subscribe((preferences) => {
	      this.#changeCount() || this.#accept(preferences);
	    }, this.scope), this.scope.add(() => this.#clearLookup()), this.#accept(this.#preferences.read());
	  }
	  open() {
	    this.#accept(this.#preferences.read()), this.#syncActive();
	  }
	  async saveIfChanged(showFeedback = !1) {
	    const update = this.#preferences.update;
	    if (typeof update != "function" || this.#saving) return !1;
	    const value = frozenDraft(this.#draft);
	    if (valuesEqual(value, this.#baseline)) return !0;
	    this.#saving = !0, this.#refreshStatus();
	    try {
	      return await update.call(this.#preferences, value), this.#accept(value), showFeedback && this.#notify("自动过滤设置已保存"), !0;
	    } catch (cause) {
	      return this.#onError(cause), this.#notify("自动过滤设置保存失败"), !1;
	    } finally {
	      this.#saving = !1, this.#refreshStatus();
	    }
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #textButton(label, icon) {
	    const button = this.#document.createElement("button");
	    return button.type = "button", button.append(
	      (0, import_reader_icon.createReaderIcon)(this.#document, icon),
	      (0, import_html_element.htmlElement)(this.#document, "span", "", label)
	    ), button;
	  }
	  #listen() {
	    this.scope.listen(this.element, "click", (event) => {
	      const tab = closestTarget(
	        event,
	        "[data-unwanted-rule-tab]"
	      );
	      if (tab) {
	        const name = tab.dataset.unwantedRuleTab;
	        RULE_NAMES.includes(name) && (this.#active = name, this.#syncActive());
	        return;
	      }
	      const remove = closestTarget(
	        event,
	        "[data-unwanted-rule-remove]"
	      );
	      if (remove) {
	        const index = Number(remove.dataset.unwantedRuleRemove);
	        Number.isSafeInteger(index) && index >= 0 && (this.#draft[this.#active].splice(index, 1), this.#renderCards(), this.#refreshStatus());
	        return;
	      }
	      const candidate = closestTarget(
	        event,
	        "[data-unwanted-rule-candidate]"
	      );
	      if (candidate) {
	        const index = Number(candidate.dataset.unwantedRuleCandidate), value = this.#candidates[index]?.value;
	        value && this.#addValue(value);
	      }
	    }), this.scope.listen(this.#enabled, "change", () => {
	      this.#draft.enabled = this.#enabled.checked, this.#refreshStatus();
	    }), this.scope.listen(this.#cardSearch, "input", () => this.#renderCards()), this.scope.listen(this.#addInput, "input", () => this.#scheduleLookup()), this.scope.listen(this.#addInput, "focus", () => this.#scheduleLookup()), this.scope.listen(this.#topicField, "change", () => this.#refreshAddState()), this.scope.listen(this.#regexMode, "change", () => this.#refreshAddState()), this.scope.listen(this.#addButton, "click", () => {
	      this.#active === "topicFields" && this.#addTopicField();
	    }), this.scope.listen(this.#addInput, "keydown", (event) => {
	      const keyboard = event;
	      keyboard.key !== "Enter" || this.#active !== "topicFields" || (keyboard.preventDefault(), this.#addTopicField());
	    }), this.scope.listen(this.#reset, "click", () => {
	      this.#draft = mutableDraft(
	        import_reader_unwanted_topic_filter.DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES
	      ), this.#syncAll();
	    }), this.scope.listen(this.#save, "click", () => {
	      this.saveIfChanged(!0);
	    });
	  }
	  #accept(preferences) {
	    this.#baseline = (0, import_reader_unwanted_topic_filter.normalizeReaderUnwantedTopicFilterPreferences)(preferences), this.#draft = mutableDraft(this.#baseline), this.#syncAll();
	  }
	  #syncAll() {
	    this.#enabled.checked = this.#draft.enabled, this.#syncActive(), this.#refreshStatus();
	  }
	  #syncActive() {
	    const tab = RULE_TABS.find((entry) => entry.name === this.#active);
	    for (const [name, button] of this.#tabs)
	      button.setAttribute("aria-pressed", String(name === this.#active));
	    this.#activeTitle.textContent = tab.title, this.#activeDescription.textContent = tab.description, this.#addInput.value = "", this.#addInput.placeholder = tab.placeholder, this.#cardSearch.value = "", this.#topicOptions.hidden = this.#active !== "topicFields", this.#addButton.hidden = this.#active !== "topicFields", this.#clearLookup(), this.#lookupStatus.textContent = this.#active === "topicFields" ? "输入字符后添加;正则使用 /表达式/标志 形式。" : "输入后自动查询宿主,必须选择一个真实候选。", this.#renderCards(), this.#refreshAddState();
	  }
	  #changeCount() {
	    const value = frozenDraft(this.#draft);
	    return +(value.enabled !== this.#baseline.enabled) + RULE_NAMES.reduce((count, name) => count + Number(
	      value[name].length !== this.#baseline[name].length || value[name].some((entry, index) => entry !== this.#baseline[name][index])
	    ), 0);
	  }
	  #refreshStatus() {
	    const count = this.#changeCount();
	    this.#status.textContent = this.#saving ? "正在保存…" : count ? `有 ${count} 项未保存` : "已与当前设置同步", this.#save.disabled = this.#saving || count === 0, this.#reset.disabled = valuesEqual(
	      frozenDraft(this.#draft),
	      import_reader_unwanted_topic_filter.DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES
	    );
	  }
	  #renderCards() {
	    const values = this.#draft[this.#active], query = valueKey(this.#cardSearch.value), cards = values.map((value, index) => {
	      const presentation = this.#rulePresentation(value), card = (0, import_html_element.htmlElement)(
	        this.#document,
	        "div",
	        "ldp-unwanted-topic-filter-card"
	      ), copy = (0, import_html_element.htmlElement)(
	        this.#document,
	        "span",
	        "ldp-unwanted-topic-filter-card-copy"
	      );
	      copy.append(
	        (0, import_html_element.htmlElement)(this.#document, "strong", "", presentation.label),
	        ...presentation.detail ? [(0, import_html_element.htmlElement)(this.#document, "small", "", presentation.detail)] : []
	      );
	      const remove = this.#document.createElement("button");
	      remove.type = "button", remove.dataset.unwantedRuleRemove = String(index), remove.setAttribute("aria-label", `删除规则 ${presentation.label}`), remove.title = "删除规则", remove.append((0, import_reader_icon.createReaderIcon)(this.#document, "x")), card.append(copy, remove);
	      const matches = !query || valueKey(
	        `${presentation.label} ${presentation.detail} ${value}`
	      ).includes(query);
	      return card.classList.toggle("is-search-match", !!(query && matches)), card.classList.toggle("is-search-miss", !!(query && !matches)), card;
	    });
	    if (!cards.length) {
	      this.#cards.replaceChildren((0, import_html_element.htmlElement)(
	        this.#document,
	        "p",
	        "ldp-unwanted-topic-filter-card-empty",
	        "当前类别还没有规则。"
	      ));
	      return;
	    }
	    this.#cards.replaceChildren(...cards);
	    const firstMatch = query ? this.#cards.querySelector(
	      ".ldp-unwanted-topic-filter-card.is-search-match"
	    ) : null;
	    firstMatch && (firstMatch.classList.add("is-located"), queueMicrotask(() => {
	      firstMatch.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
	    }));
	  }
	  #rulePresentation(value) {
	    if (this.#active === "categories") {
	      const category = this.#catalog.categories().find((entry) => entry.id === Number(value));
	      return Object.freeze(category ? {
	        label: this.#categoryLabel(category),
	        detail: ""
	      } : { label: value, detail: "" });
	    }
	    if (this.#active === "labels")
	      return Object.freeze({ label: `#${value}`, detail: "" });
	    if (this.#active === "topicAuthors")
	      return Object.freeze({ label: `@${value}`, detail: "" });
	    if (this.#active === "postAuthors")
	      return Object.freeze({ label: `@${value}`, detail: "" });
	    const separator = value.indexOf(":"), field = separator > 0 ? value.slice(0, separator) : "title", matcher = separator > 0 ? value.slice(separator + 1) : value, fieldName = (/* @__PURE__ */ new Map([
	      ["title", "主题标题"],
	      ["category", "主题类别"],
	      ["label", "主题标签"],
	      ["user", "OP 用户"],
	      ["topic", "Topic ID"]
	    ])).get(valueKey(field)) ?? field;
	    return Object.freeze({
	      label: `${fieldName}:${matcher}`,
	      detail: ""
	    });
	  }
	  #categoryLabel(category) {
	    const parent = category.parentCategoryId ? this.#catalog.categories().find((entry) => entry.id === category.parentCategoryId) : null;
	    if (parent) {
	      const parentName = valueKey(parent.name), categoryName = valueKey(category.name);
	      if (categoryName === parentName || categoryName.startsWith(`${parentName},`) || categoryName.startsWith(`${parentName},`)) return category.name;
	    }
	    return `${parent ? `${parent.name} / ` : ""}${category.name}`;
	  }
	  #clearLookup() {
	    this.#lookupSequence += 1, this.#lookupTimer !== null && clearTimeout(this.#lookupTimer), this.#lookupTimer = null, this.#candidates = Object.freeze([]), this.#results.replaceChildren(), this.#results.hidden = !0;
	  }
	  #scheduleLookup() {
	    if (this.#clearLookup(), this.#active === "topicFields") {
	      this.#refreshAddState();
	      return;
	    }
	    const query = this.#addInput.value.trim();
	    if (!query) {
	      this.#lookupStatus.textContent = "输入后自动查询宿主,必须选择一个真实候选。";
	      return;
	    }
	    const sequence = this.#lookupSequence;
	    this.#lookupStatus.textContent = "正在查询宿主…", this.#lookupTimer = setTimeout(() => {
	      this.#lookupTimer = null, this.#lookup(query).then((candidates) => {
	        if (sequence !== this.#lookupSequence) return;
	        const existing = new Set(
	          this.#draft[this.#active].map((value) => valueKey(value))
	        );
	        this.#candidates = Object.freeze(candidates.filter((candidate) => !existing.has(valueKey(candidate.value)))), this.#renderCandidates(
	          candidates.length - this.#candidates.length
	        );
	      }).catch((cause) => {
	        sequence === this.#lookupSequence && (this.#onError(cause), this.#lookupStatus.textContent = "宿主查询失败,请稍后重试。", this.#results.replaceChildren(), this.#results.hidden = !0);
	      });
	    }, 240);
	  }
	  async #lookup(query) {
	    if (this.#active === "categories") {
	      const normalized = valueKey(query);
	      return Object.freeze(this.#catalog.categories().filter((category) => valueKey(
	        `${this.#categoryLabel(category)} ${category.slug} ${category.id}`
	      ).includes(normalized)).slice(0, 30).map((category) => Object.freeze({
	        value: String(category.id),
	        label: this.#categoryLabel(category),
	        detail: `${category.slug || "无 slug"} · #${category.id}`,
	        searchText: `${category.name} ${category.slug} ${category.id}`
	      })));
	    }
	    if (this.#active === "labels") {
	      const tags = await this.#catalog.searchTags({
	        query,
	        categoryId: 0,
	        selected: Object.freeze([])
	      });
	      return Object.freeze(tags.slice(0, 30).map((tag) => Object.freeze({
	        value: tag.name,
	        label: `#${tag.name}`,
	        detail: tag.id ? `Label #${tag.id}` : "Label",
	        searchText: `${tag.name} ${tag.id ?? ""}`
	      })));
	    }
	    const users = await this.#catalog.searchUsers(query);
	    return Object.freeze(users.slice(0, 30).map((user) => this.#userCandidate(user)));
	  }
	  #userCandidate(user) {
	    return Object.freeze({
	      value: user.username,
	      label: user.name || `@${user.username}`,
	      detail: `@${user.username}${user.id ? ` · #${user.id}` : ""}`,
	      searchText: `${user.name} ${user.username} ${user.id ?? ""}`
	    });
	  }
	  #renderCandidates(excludedCount = 0) {
	    this.#lookupStatus.textContent = this.#candidates.length ? `找到 ${this.#candidates.length} 个候选,请选择具体项。` : excludedCount > 0 ? "匹配项已经全部添加。" : "宿主中没有匹配的合法字段。", this.#results.hidden = !this.#candidates.length, this.#results.replaceChildren(...this.#candidates.map((candidate, index) => {
	      const button = this.#document.createElement("button");
	      return button.type = "button", button.dataset.unwantedRuleCandidate = String(index), button.setAttribute("role", "option"), button.append(
	        (0, import_html_element.htmlElement)(this.#document, "strong", "", candidate.label),
	        (0, import_html_element.htmlElement)(this.#document, "small", "", candidate.detail)
	      ), button;
	    }));
	  }
	  #addValue(value) {
	    const normalized = value.trim();
	    if (!normalized) return;
	    const values = this.#draft[this.#active], key = valueKey(normalized);
	    values.some((entry) => valueKey(entry) === key) || values.push(normalized), this.#addInput.value = "", this.#clearLookup(), this.#lookupStatus.textContent = "已加入规则草稿;保存设置后生效。", this.#renderCards(), this.#refreshStatus();
	  }
	  #addTopicField() {
	    if (this.#active !== "topicFields") return;
	    const input = this.#addInput.value.trim();
	    if (!input) return;
	    let matcher = input;
	    this.#regexMode.checked && !matcher.startsWith("/") && (matcher = `/${matcher.replace(/\//g, "\\/")}/i`);
	    const rule = `${this.#topicField.value || "title"}:${matcher}`;
	    if (!(0, import_reader_unwanted_topic_filter.readerUnwantedTopicFieldRuleIsValid)(rule)) {
	      this.#lookupStatus.textContent = "正则表达式无效,请检查斜杠、标志或括号。", this.#addButton.disabled = !0;
	      return;
	    }
	    this.#addValue(rule);
	  }
	  #refreshAddState() {
	    if (this.#active !== "topicFields") {
	      this.#addButton.disabled = !0;
	      return;
	    }
	    const input = this.#addInput.value.trim();
	    let matcher = input;
	    this.#regexMode.checked && matcher && !matcher.startsWith("/") && (matcher = `/${matcher.replace(/\//g, "\\/")}/i`);
	    const rule = `${this.#topicField.value || "title"}:${matcher}`, valid = !!(input && (0, import_reader_unwanted_topic_filter.readerUnwantedTopicFieldRuleIsValid)(rule));
	    this.#addButton.disabled = !valid, this.#lookupStatus.textContent = input ? valid ? "规则格式有效,可添加到当前草稿。" : "正则表达式无效,请检查斜杠、标志或括号。" : "输入字符后添加;正则使用 /表达式/标志 形式。";
	  }
	}
}, "32bbea274af3aa2858ec1a89afc696054e8cb25ca80a22acde9bcf515f4ee4e9");

/* Source: lite/src/collection/reader-unwanted-topic-filter.ts */
runtime.register("src/collection/reader-unwanted-topic-filter.js", function(module, exports, require) {
	var reader_unwanted_topic_filter_exports = {};
	__export(reader_unwanted_topic_filter_exports, {
	  DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES: () => DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES,
	  normalizeReaderUnwantedTopicFilterPreferences: () => normalizeReaderUnwantedTopicFilterPreferences,
	  readerPreferencesUnwantedTopicFilterAdapter: () => readerPreferencesUnwantedTopicFilterAdapter,
	  readerUnwantedPostAuthorMatches: () => readerUnwantedPostAuthorMatches,
	  readerUnwantedTopicFieldRuleIsValid: () => readerUnwantedTopicFieldRuleIsValid,
	  readerUnwantedTopicFilterMatch: () => readerUnwantedTopicFilterMatch,
	  readerUnwantedTopicFilterPreferencesEqual: () => readerUnwantedTopicFilterPreferencesEqual
	});
	module.exports = __toCommonJS(reader_unwanted_topic_filter_exports);
	const DEFAULT_READER_UNWANTED_TOPIC_FILTER_PREFERENCES = Object.freeze({
	  enabled: !1,
	  categories: Object.freeze([]),
	  labels: Object.freeze([]),
	  topicAuthors: Object.freeze([]),
	  topicFields: Object.freeze([]),
	  postAuthors: Object.freeze([])
	}), readerPreferencesUnwantedTopicFilterAdapter = Object.freeze({
	  read: (preferences) => normalizeReaderUnwantedTopicFilterPreferences({
	    enabled: preferences.unwantedTopicFilterEnabled,
	    categories: preferences.unwantedTopicFilterCategories,
	    labels: preferences.unwantedTopicFilterLabels,
	    topicAuthors: preferences.unwantedTopicFilterTopicAuthors,
	    topicFields: preferences.unwantedTopicFilterTopicFields,
	    postAuthors: preferences.unwantedTopicFilterPostAuthors
	  }),
	  createPatch: (preferences) => Object.freeze({
	    unwantedTopicFilterEnabled: preferences.enabled,
	    unwantedTopicFilterCategories: preferences.categories,
	    unwantedTopicFilterLabels: preferences.labels,
	    unwantedTopicFilterTopicAuthors: preferences.topicAuthors,
	    unwantedTopicFilterTopicFields: preferences.topicFields,
	    unwantedTopicFilterPostAuthors: preferences.postAuthors
	  })
	});
	function text(value, maximum = 80) {
	  return String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
	}
	function values(value, transform = (entry) => entry) {
	  const source = Array.isArray(value) ? value : String(value ?? "").split(/[\n,,]+/), result = /* @__PURE__ */ new Map();
	  for (const item of source) {
	    const entry = transform(text(item)), key = entry.toLocaleLowerCase("zh-CN");
	    if (key && !result.has(key) && result.set(key, entry), result.size >= 100) break;
	  }
	  return Object.freeze([...result.values()]);
	}
	function userValues(value) {
	  return values(value, (entry) => entry.replace(/^@+/, "").trim());
	}
	function labelValues(value) {
	  return values(value, (entry) => entry.replace(/^[##]+/, "").trim());
	}
	function normalizeReaderUnwantedTopicFilterPreferences(value) {
	  return Object.freeze({
	    enabled: value?.enabled === !0,
	    categories: values(value?.categories),
	    labels: labelValues(value?.labels),
	    topicAuthors: userValues(value?.topicAuthors),
	    topicFields: values(value?.topicFields),
	    postAuthors: userValues(value?.postAuthors)
	  });
	}
	function readerUnwantedTopicFilterPreferencesEqual(left, right) {
	  return left.enabled === right.enabled && [
	    "categories",
	    "labels",
	    "topicAuthors",
	    "topicFields",
	    "postAuthors"
	  ].every((name) => {
	    const key = name;
	    return left[key].length === right[key].length && left[key].every((entry, index) => entry === right[key][index]);
	  });
	}
	function comparisonKey(value) {
	  return text(value).toLocaleLowerCase("zh-CN");
	}
	function matchingSet(values2, candidates) {
	  const candidateKeys = new Set(candidates.map(comparisonKey).filter(Boolean));
	  return Object.freeze(values2.filter((value) => candidateKeys.has(comparisonKey(value))));
	}
	function fieldValue(name, input) {
	  return name === "title" || name === "标题" ? input.title : name === "category" || name === "类别" || name === "分类" ? [input.categoryId, input.categoryName, input.categorySlug].filter((value) => value != null).join(" ") : name === "label" || name === "tag" || name === "标签" ? (input.labels ?? []).join(" ") : name === "user" || name === "author" || name === "用户" || name === "作者" ? input.authorUsername ?? "" : name === "topic" || name === "id" ? String(input.topicId) : "";
	}
	function topicFieldMatch(rule, input) {
	  const separator = rule.indexOf(":");
	  if (separator < 1)
	    return topicFieldValueMatches(rule, input.title);
	  const name = comparisonKey(rule.slice(0, separator)), expected = rule.slice(separator + 1).trim();
	  return !!(expected && topicFieldValueMatches(
	    expected,
	    fieldValue(name, input)
	  ));
	}
	function topicFieldRegularExpression(value) {
	  const source = value.trim();
	  if (!source.startsWith("/")) return null;
	  const match = source.match(/^\/([\s\S]*)\/([imsu]*)$/);
	  if (!match) return !1;
	  try {
	    return new RegExp(match[1] ?? "", match[2] ?? "");
	  } catch {
	    return !1;
	  }
	}
	function topicFieldValueMatches(expected, actual) {
	  const expression = topicFieldRegularExpression(expected);
	  return expression === !1 ? !1 : expression ? expression.test(actual) : comparisonKey(actual).includes(comparisonKey(expected));
	}
	function readerUnwantedTopicFieldRuleIsValid(ruleValue) {
	  const rule = text(ruleValue);
	  if (!rule) return !1;
	  const separator = rule.indexOf(":"), expected = separator < 1 ? rule : rule.slice(separator + 1).trim();
	  return expected ? topicFieldRegularExpression(expected) !== !1 : !1;
	}
	function readerUnwantedTopicFilterMatch(preferencesValue, input) {
	  const preferences = normalizeReaderUnwantedTopicFilterPreferences(
	    preferencesValue
	  );
	  if (!preferences.enabled) return null;
	  const matches = [];
	  for (const category of matchingSet(preferences.categories, [
	    input.categoryId,
	    input.categoryName,
	    input.categorySlug
	  ])) matches.push(Object.freeze({
	    kind: "category",
	    rule: category,
	    label: `类别:${input.categoryName || input.categorySlug || category}`
	  }));
	  for (const label of matchingSet(preferences.labels, input.labels ?? []))
	    matches.push(Object.freeze({
	      kind: "label",
	      rule: label,
	      label: `标签:${label}`
	    }));
	  for (const author of matchingSet(
	    preferences.topicAuthors,
	    [input.authorUsername]
	  )) matches.push(Object.freeze({
	    kind: "topic-author",
	    rule: author,
	    label: `OP:@${author}`
	  }));
	  for (const field of preferences.topicFields.filter((rule) => topicFieldMatch(rule, input))) matches.push(Object.freeze({
	    kind: "topic-field",
	    rule: field,
	    label: `字段:${field}`
	  }));
	  const first = matches[0];
	  return first ? Object.freeze({
	    ...first,
	    label: matches.map((match) => match.label).join(";"),
	    matches: Object.freeze(matches)
	  }) : null;
	}
	function readerUnwantedPostAuthorMatches(preferencesValue, username) {
	  const preferences = normalizeReaderUnwantedTopicFilterPreferences(
	    preferencesValue
	  );
	  if (!preferences.enabled) return !1;
	  const candidate = comparisonKey(String(username ?? "").replace(/^@+/, ""));
	  return !!(candidate && preferences.postAuthors.some((entry) => comparisonKey(entry) === candidate));
	}
}, "2abe1ca947c6902a70fee8e99a0637c6f4705c9fe64ed7f5739e1d3df5fd42da");

/* Source: lite/src/collection/reader-unwanted-topic-repository.ts */
runtime.register("src/collection/reader-unwanted-topic-repository.js", function(module, exports, require) {
	var reader_unwanted_topic_repository_exports = {};
	__export(reader_unwanted_topic_repository_exports, {
	  READER_UNWANTED_TOPIC_MAX_RECORDS: () => READER_UNWANTED_TOPIC_MAX_RECORDS,
	  READER_UNWANTED_TOPIC_STORAGE_KEY: () => READER_UNWANTED_TOPIC_STORAGE_KEY,
	  ReaderUnwantedTopicRepository: () => ReaderUnwantedTopicRepository,
	  mergeReaderUnwantedTopicValues: () => mergeReaderUnwantedTopicValues,
	  normalizeReaderUnwantedTopicRecord: () => normalizeReaderUnwantedTopicRecord
	});
	module.exports = __toCommonJS(reader_unwanted_topic_repository_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_signal = require("../kernel/signal.js"), import_reader_account_scoped_storage = require("../state/reader-account-scoped-storage.js");
	const READER_UNWANTED_TOPIC_STORAGE_KEY = "linuxdo-enhanced-reader:unwanted-topics", READER_UNWANTED_TOPIC_MAX_RECORDS = 2e3;
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function text(value, maximum) {
	  return String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
	}
	function timestamp(value) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
	}
	function categoryId(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function matchedCategoryValue(value) {
	  for (const part of value.split(";")) {
	    const match = part.trim().match(/^类别[::]\s*(.+)$/);
	    if (match?.[1]) return match[1].trim();
	  }
	  return "";
	}
	function normalizeHref(value, topicId) {
	  const href = text(value, 512);
	  if (!href) return `/t/${topicId}`;
	  try {
	    const parsed = new URL(href, "https://reader.invalid");
	    return /^https?:$/.test(parsed.protocol) ? parsed.origin === "https://reader.invalid" ? `${parsed.pathname}${parsed.search}${parsed.hash}` : parsed.href : `/t/${topicId}`;
	  } catch {
	    return `/t/${topicId}`;
	  }
	}
	function normalizeLabel(value) {
	  return text(value, 36).replace(/^[##]+/, "").trim();
	}
	function normalizeLabels(value) {
	  if (!Array.isArray(value)) return Object.freeze([]);
	  const labels = /* @__PURE__ */ new Map();
	  for (const item of value) {
	    const label = normalizeLabel(item), key = label.toLocaleLowerCase("zh-CN");
	    if (key && !labels.has(key) && labels.set(key, label), labels.size >= 24) break;
	  }
	  return Object.freeze([...labels.values()]);
	}
	function searchText(input) {
	  return [
	    input.title,
	    `Topic ${input.topicId}`,
	    input.href,
	    input.note,
	    ...input.labels,
	    input.categoryId === null ? "" : `类别 ${input.categoryId}`,
	    input.categoryName,
	    input.categorySlug,
	    input.matchedRule
	  ].filter(Boolean).join(" ").toLocaleLowerCase("zh-CN");
	}
	function normalizeReaderUnwantedTopicRecord(value) {
	  const source = record(value), topicId = (0, import_identifiers.tryDiscourseTopicId)(source?.topicId);
	  if (!source || topicId === null) return null;
	  const hiddenAt = timestamp(source.hiddenAt), updatedAt = timestamp(source.updatedAt) || hiddenAt;
	  if (!hiddenAt || !updatedAt) return null;
	  const matchedRule = text(source.matchedRule, 2e3), legacyCategory = matchedCategoryValue(matchedRule), normalizedCategoryId = categoryId(source.categoryId) ?? categoryId(legacyCategory), normalizedCategoryName = text(source.categoryName, 120) || (categoryId(legacyCategory) === null ? legacyCategory : ""), base = Object.freeze({
	    topicId,
	    title: text(source.title, 180) || `帖子 #${topicId}`,
	    href: normalizeHref(source.href, topicId),
	    note: text(source.note, 240),
	    labels: normalizeLabels(source.labels),
	    categoryId: normalizedCategoryId,
	    categoryName: normalizedCategoryName,
	    categorySlug: text(source.categorySlug, 120),
	    source: source.source === "automatic" ? "automatic" : "manual",
	    matchedRule,
	    matchedCategory: source.matchedCategory === !0 || !!legacyCategory,
	    hiddenAt: Math.min(hiddenAt, updatedAt),
	    updatedAt: Math.max(hiddenAt, updatedAt)
	  });
	  return Object.freeze({
	    ...base,
	    searchText: searchText(base)
	  });
	}
	function mergeReaderUnwantedTopicValues(local, remote) {
	  const left = normalizeReaderUnwantedTopicRecord(local), right = normalizeReaderUnwantedTopicRecord(remote);
	  if (!left) return right;
	  if (!right) return left;
	  if (left.topicId !== right.topicId)
	    return left.updatedAt >= right.updatedAt ? left : right;
	  const recent = left.updatedAt >= right.updatedAt ? left : right, older = recent === left ? right : left;
	  return normalizeReaderUnwantedTopicRecord({
	    ...older,
	    ...recent,
	    title: recent.title || older.title,
	    href: recent.href || older.href,
	    labels: [...older.labels, ...recent.labels],
	    categoryId: recent.categoryId ?? older.categoryId,
	    categoryName: recent.categoryName || older.categoryName,
	    categorySlug: recent.categorySlug || older.categorySlug,
	    matchedRule: recent.matchedRule || older.matchedRule,
	    matchedCategory: recent.matchedCategory || older.matchedCategory,
	    hiddenAt: Math.min(left.hiddenAt, right.hiddenAt),
	    updatedAt: Math.max(left.updatedAt, right.updatedAt)
	  });
	}
	class ReaderUnwantedTopicRepository {
	  changes = new import_signal.Signal();
	  #storage;
	  #key;
	  #accountStorage;
	  #maxRecords;
	  #now;
	  #snapshot = Object.freeze({
	    records: Object.freeze([]),
	    revision: 0,
	    source: "fallback"
	  });
	  constructor(options) {
	    if (this.#storage = options.storage, this.#accountStorage = options.key === void 0 && options.authScope !== void 0 ? (0, import_reader_account_scoped_storage.readerAccountScopedStorageIdentity)(
	      READER_UNWANTED_TOPIC_STORAGE_KEY,
	      options.authScope
	    ) : null, this.#key = text(
	      options.key ?? this.#accountStorage?.key ?? READER_UNWANTED_TOPIC_STORAGE_KEY,
	      512
	    ), !this.#key) throw new Error("不想看 storage key 不能为空");
	    if (this.#maxRecords = Math.floor(Number(
	      options.maxRecords ?? READER_UNWANTED_TOPIC_MAX_RECORDS
	    )), !Number.isSafeInteger(this.#maxRecords) || this.#maxRecords < 1)
	      throw new RangeError("不想看 maxRecords 必须是正安全整数");
	    this.#now = options.now ?? Date.now;
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  get storageKey() {
	    return this.#key;
	  }
	  load() {
	    return this.#readAndCommit("initial");
	  }
	  reloadExternal() {
	    return this.#readAndCommit("external-reload");
	  }
	  #readAndCommit(source) {
	    try {
	      const stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key), raw = stored === null ? [] : JSON.parse(stored);
	      if (!Array.isArray(raw)) throw new TypeError("不想看存储值必须是数组");
	      const records = this.#normalizeMany(raw);
	      return JSON.stringify(records) !== JSON.stringify(raw) && this.#persist(records), this.#commit(records, source);
	    } catch {
	      return this.#commit([], "fallback");
	    }
	  }
	  has(topicIdValue) {
	    const topicId = (0, import_identifiers.tryDiscourseTopicId)(topicIdValue);
	    return topicId !== null && this.#snapshot.records.some((entry) => entry.topicId === topicId);
	  }
	  isManuallyHidden(topicIdValue) {
	    const topicId = (0, import_identifiers.tryDiscourseTopicId)(topicIdValue);
	    return topicId !== null && this.#snapshot.records.some((entry) => entry.topicId === topicId && entry.source === "manual");
	  }
	  ordered() {
	    return this.#ordered(this.#snapshot.records);
	  }
	  remember(input) {
	    this.#mergeStoredBeforeMutation();
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), previous = this.#snapshot.records.find((entry) => entry.topicId === topicId), now = this.#now(), incoming = normalizeReaderUnwantedTopicRecord({
	      topicId,
	      title: input.title,
	      href: input.href,
	      note: previous?.note ?? "",
	      labels: previous?.labels ?? [],
	      categoryId: input.categoryId === void 0 ? previous?.categoryId ?? null : input.categoryId,
	      categoryName: input.categoryName === void 0 ? previous?.categoryName ?? "" : input.categoryName,
	      categorySlug: input.categorySlug === void 0 ? previous?.categorySlug ?? "" : input.categorySlug,
	      source: input.source ?? previous?.source ?? "manual",
	      matchedRule: input.matchedRule ?? previous?.matchedRule ?? "",
	      matchedCategory: input.matchedCategory ?? previous?.matchedCategory ?? !1,
	      hiddenAt: previous?.hiddenAt ?? now,
	      updatedAt: now
	    });
	    return this.#persistAndCommit([
	      incoming,
	      ...this.#snapshot.records.filter((entry) => entry.topicId !== topicId)
	    ], "remember");
	  }
	  update(topicIdValue, patch) {
	    this.#mergeStoredBeforeMutation();
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), previous = this.#snapshot.records.find((entry) => entry.topicId === topicId);
	    if (!previous) return this.#snapshot;
	    const next = normalizeReaderUnwantedTopicRecord({
	      ...previous,
	      ...Object.hasOwn(patch, "note") ? { note: patch.note } : {},
	      ...Object.hasOwn(patch, "labels") ? { labels: patch.labels } : {},
	      updatedAt: this.#now()
	    });
	    return this.#persistAndCommit([
	      next,
	      ...this.#snapshot.records.filter((entry) => entry.topicId !== topicId)
	    ], "update");
	  }
	  remove(topicIdValue) {
	    return this.removeMany([topicIdValue]);
	  }
	  removeMany(topicIdValues) {
	    this.#mergeStoredBeforeMutation();
	    const topicIds = /* @__PURE__ */ new Set();
	    for (const value of topicIdValues)
	      try {
	        topicIds.add((0, import_identifiers.discourseTopicId)(value));
	      } catch {
	      }
	    if (!topicIds.size) return this.#snapshot;
	    const next = this.#snapshot.records.filter((entry) => !topicIds.has(entry.topicId));
	    return next.length === this.#snapshot.records.length ? this.#snapshot : this.#persistAndCommit(next, "remove");
	  }
	  clear() {
	    return this.#mergeStoredBeforeMutation(), this.#persistAndCommit([], "clear");
	  }
	  replaceExternal(values) {
	    return this.#persistAndCommit(this.#normalizeMany(values), "external-sync");
	  }
	  #normalizeMany(values) {
	    const records = /* @__PURE__ */ new Map();
	    for (const value of values) {
	      const incoming = normalizeReaderUnwantedTopicRecord(value);
	      if (!incoming) continue;
	      const previous = records.get(incoming.topicId), merged = previous ? mergeReaderUnwantedTopicValues(previous, incoming) : incoming;
	      merged && records.set(merged.topicId, merged);
	    }
	    return this.#ordered([...records.values()]);
	  }
	  #mergeStoredBeforeMutation() {
	    let stored;
	    try {
	      stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key);
	    } catch {
	      return;
	    }
	    (stored ?? "[]") !== JSON.stringify(this.#snapshot.records) && this.#readAndCommit("external-reload");
	  }
	  #ordered(values) {
	    return Object.freeze([...values].sort((left, right) => right.hiddenAt - left.hiddenAt || right.topicId - left.topicId).slice(0, this.#maxRecords));
	  }
	  #persistAndCommit(records, source) {
	    const ordered = this.#ordered(records);
	    return this.#persist(ordered), this.#commit(ordered, source);
	  }
	  #persist(records) {
	    if (!records.length && this.#storage.removeItem && !this.#accountStorage) {
	      this.#storage.removeItem(this.#key);
	      return;
	    }
	    this.#storage.setItem(this.#key, JSON.stringify(records));
	  }
	  #commit(records, source) {
	    return this.#snapshot = Object.freeze({
	      records: Object.freeze([...records]),
	      revision: this.#snapshot.revision + 1,
	      source
	    }), this.changes.emit(this.#snapshot), this.#snapshot;
	  }
	}
}, "4052c2d17b20114900ba83d03eddd38803c002a64c71c446c2b7bc4cfcddf00a");

/* Source: lite/src/collection/reader-unwanted-topic-view.ts */
runtime.register("src/collection/reader-unwanted-topic-view.js", function(module, exports, require) {
	var reader_unwanted_topic_view_exports = {};
	__export(reader_unwanted_topic_view_exports, {
	  ReaderUnwantedTopicView: () => ReaderUnwantedTopicView
	});
	module.exports = __toCommonJS(reader_unwanted_topic_view_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_floating_window_frame = require("../shell/reader-floating-window-frame.js"), import_reader_unwanted_topic_filter_editor = require("./reader-unwanted-topic-filter-editor.js"), import_reader_collection_floating_window = require("./reader-collection-floating-window.js");
	function closestTarget(event, selector) {
	  const target = event.target;
	  return typeof target?.closest == "function" ? target.closest(selector) : null;
	}
	function defaultRelativeTime(timestamp) {
	  const elapsed = Math.max(0, Date.now() - timestamp);
	  return elapsed < 6e4 ? "刚刚" : elapsed < 36e5 ? `${Math.floor(elapsed / 6e4)} 分钟前` : elapsed < 864e5 ? `${Math.floor(elapsed / 36e5)} 小时前` : elapsed < 30 * 864e5 ? `${Math.floor(elapsed / 864e5)} 天前` : new Date(timestamp).toLocaleDateString("zh-CN");
	}
	function labelKey(value) {
	  return String(value ?? "").replace(/^[##]+/, "").replace(/\s+/g, " ").trim().slice(0, 36).toLocaleLowerCase("zh-CN");
	}
	function categoryRecordKey(record) {
	  if (!record.matchedCategory) return "";
	  if (record.categoryId !== null) return `id:${record.categoryId}`;
	  const value = record.categoryName || record.categorySlug;
	  return value ? `name:${labelKey(value)}` : "";
	}
	function matchedRuleValues(record, labels) {
	  const accepted = new Set(labels);
	  return Object.freeze(record.matchedRule.split(";").flatMap((part) => {
	    const match = part.trim().match(/^([^::]+)[::]\s*(.+)$/);
	    return match?.[1] && match[2] && accepted.has(match[1].trim()) ? [match[2].trim()] : [];
	  }));
	}
	class ReaderUnwantedTopicView {
	  scope;
	  window;
	  #document;
	  #topics;
	  #filterPreferences;
	  #filterCatalog;
	  #openTarget;
	  #relativeTime;
	  #notify;
	  #onError;
	  #topicPane;
	  #settingsPane;
	  #filterEditor;
	  #settingsButton;
	  #backButton;
	  #search;
	  #searchResult;
	  #filterBar;
	  #addFilter;
	  #labelFilter;
	  #categoryFilter;
	  #topicLabelFilter;
	  #topicAuthorFilter;
	  #topicFieldFilter;
	  #postAuthorFilter;
	  #sourceFilter;
	  #labelOptions;
	  #bulkToggle;
	  #bulkBar;
	  #bulkStatus;
	  #bulkSelectAll;
	  #bulkRestore;
	  #bulkDone;
	  #list;
	  #footerStatus;
	  #loadMore;
	  #pageSize = 40;
	  #visibleLimit = this.#pageSize;
	  #mode = "topics";
	  #topicDraft = null;
	  #bulkMode = !1;
	  #bulkSelection = /* @__PURE__ */ new Set();
	  #activeFilters = /* @__PURE__ */ new Set();
	  #filterControls = /* @__PURE__ */ new Map();
	  #categoryLabels = /* @__PURE__ */ new Map();
	  constructor(options) {
	    this.#document = options.document, this.#topics = options.topics, this.#filterPreferences = options.filterPreferences, this.#filterCatalog = options.filterCatalog, this.#openTarget = options.openTarget ?? (() => !1), this.#relativeTime = options.relativeTime ?? defaultRelativeTime, this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.window = new import_reader_floating_window_frame.ReaderFloatingWindowFrame({
	      document: options.document,
	      mount: options.mount,
	      title: "不想再看",
	      ariaLabel: "不想看的 Topic",
	      icon: "eye-off",
	      variant: "unwanted-topics",
	      tabId: "unwanted-topics",
	      tabOrder: 70,
	      requestOpen: () => this.open(),
	      zIndex: 2147483586,
	      ...options.storage ? { geometryStorage: options.storage } : {},
	      geometryStorageKey: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY,
	      policy: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_POLICY,
	      placement: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_PLACEMENT,
	      notify: this.#notify,
	      onClose: () => {
	        this.#mode === "settings" && this.#filterEditor?.saveIfChanged(!1);
	      },
	      parentScope: this.scope
	    }), this.#backButton = options.document.createElement("button"), this.#backButton.type = "button", this.#backButton.className = "ldp-reader-floating-window-back", this.#backButton.hidden = !0, this.#backButton.setAttribute("aria-label", "返回不想看列表"), this.#backButton.title = "返回", this.#backButton.append((0, import_reader_icon.createReaderIcon)(options.document, "chevron-left")), this.#settingsButton = options.document.createElement("button"), this.#settingsButton.type = "button", this.#settingsButton.className = "ldp-reader-floating-window-extra-action ldp-unwanted-topic-settings-button", this.#settingsButton.hidden = typeof options.filterPreferences?.update != "function" || !options.filterCatalog, this.#settingsButton.setAttribute("aria-label", "设置免打扰与自动过滤"), this.#settingsButton.title = "免打扰设置", this.#settingsButton.append((0, import_reader_icon.createReaderIcon)(options.document, "settings")), this.window.actions.prepend(this.#backButton, this.#settingsButton), this.#topicPane = (0, import_html_element.htmlElement)(options.document, "div", "ldp-unwanted-topic-pane");
	    const tools = (0, import_html_element.htmlElement)(options.document, "div", "ldp-unwanted-topic-tools"), searchLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-unwanted-topic-search"
	    );
	    searchLabel.append((0, import_reader_icon.createReaderIcon)(options.document, "search")), this.#search = options.document.createElement("input"), this.#search.type = "search", this.#search.placeholder = "搜索 Topic、标注、标签、类别、用户或规则", this.#search.setAttribute("aria-label", "搜索不想看的 Topic"), this.#searchResult = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-unwanted-topic-search-result"
	    ), this.#searchResult.hidden = !0, this.#searchResult.setAttribute("aria-live", "polite"), searchLabel.append(this.#search, this.#searchResult), this.#filterBar = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-bar"
	    ), this.#labelFilter = this.#createFilterControl(
	      "labels",
	      "自定义标签",
	      "ldp-unwanted-topic-label-filter"
	    ), this.#categoryFilter = this.#createFilterControl(
	      "categories",
	      "主题类别",
	      "ldp-unwanted-topic-category-filter"
	    ), this.#topicLabelFilter = this.#createFilterControl(
	      "topic-labels",
	      "主题标签",
	      "ldp-unwanted-topic-hit-label-filter"
	    ), this.#topicAuthorFilter = this.#createFilterControl(
	      "topic-authors",
	      "OP 用户",
	      "ldp-unwanted-topic-author-filter"
	    ), this.#topicFieldFilter = this.#createFilterControl(
	      "topic-fields",
	      "字符规则",
	      "ldp-unwanted-topic-field-filter"
	    ), this.#postAuthorFilter = this.#createFilterControl(
	      "post-authors",
	      "楼层用户",
	      "ldp-unwanted-topic-post-author-filter"
	    ), this.#sourceFilter = this.#createFilterControl(
	      "sources",
	      "收纳方式",
	      "ldp-unwanted-topic-source-filter"
	    ), this.#addFilter = options.document.createElement("select"), this.#addFilter.className = "ldp-reader-select ldp-unwanted-topic-add-filter", this.#addFilter.setAttribute("aria-label", "添加不想看筛选"), this.#labelOptions = options.document.createElement("datalist"), this.#labelOptions.id = "ldp-unwanted-topic-label-options", this.#bulkToggle = this.#actionButton(
	      "批量管理",
	      "list-checks",
	      "ldp-unwanted-topic-bulk-toggle"
	    ), this.#bulkToggle.setAttribute("aria-label", "批量管理"), this.#bulkToggle.title = "批量管理";
	    const filterActions = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-actions"
	    );
	    filterActions.append(this.#addFilter, this.#bulkToggle), this.#filterBar.append(
	      ...[...this.#filterControls.values()].map((control) => control.wrapper),
	      filterActions
	    ), tools.append(searchLabel, this.#filterBar, this.#labelOptions), this.#bulkBar = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-bulk-bar"
	    ), this.#bulkBar.hidden = !0, this.#bulkStatus = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-unwanted-topic-bulk-status"
	    );
	    const bulkActions = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-bulk-actions"
	    );
	    this.#bulkSelectAll = this.#actionButton(
	      "全选当前结果",
	      "check-square",
	      "ldp-unwanted-topic-bulk-select-all"
	    ), this.#bulkRestore = this.#actionButton(
	      "批量恢复",
	      "rotate-ccw",
	      "ldp-unwanted-topic-bulk-restore"
	    ), this.#bulkDone = this.#actionButton(
	      "完成",
	      "check",
	      "ldp-unwanted-topic-bulk-done"
	    ), bulkActions.append(
	      this.#bulkSelectAll,
	      this.#bulkRestore,
	      this.#bulkDone
	    ), this.#bulkBar.append(this.#bulkStatus, bulkActions), this.#list = (0, import_html_element.htmlElement)(options.document, "div", "ldp-unwanted-topic-list"), this.#list.setAttribute("role", "feed");
	    const footer = (0, import_html_element.htmlElement)(options.document, "footer", "ldp-unwanted-topic-footer");
	    this.#footerStatus = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-unwanted-topic-footer-status"
	    ), this.#loadMore = options.document.createElement("button"), this.#loadMore.type = "button", this.#loadMore.className = "ldp-unwanted-topic-load-more", this.#loadMore.dataset.unwantedTopicLoadMore = "true", this.#loadMore.textContent = "加载更多", footer.append(this.#footerStatus, this.#loadMore), this.#topicPane.append(tools, this.#bulkBar, this.#list, footer), this.#filterEditor = options.filterPreferences && options.filterCatalog ? new import_reader_unwanted_topic_filter_editor.ReaderUnwantedTopicFilterEditor({
	      document: options.document,
	      preferences: options.filterPreferences,
	      catalog: options.filterCatalog,
	      notify: this.#notify,
	      onError: this.#onError,
	      parentScope: this.scope
	    }) : null, this.#settingsPane = this.#filterEditor?.element ?? (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-unwanted-topic-filter-settings"
	    ), this.#settingsPane.hidden = !0, this.window.body.append(this.#topicPane, this.#settingsPane), this.scope.listen(this.#search, "input", () => {
	      this.#visibleLimit = this.#pageSize, this.#render();
	    });
	    for (const control of this.#filterControls.values())
	      this.scope.listen(control.select, "change", () => {
	        this.#visibleLimit = this.#pageSize, this.#render();
	      });
	    this.scope.listen(this.#addFilter, "change", () => {
	      const kind = this.#addFilter.value;
	      this.#filterControls.has(kind) && this.#activeFilters.add(kind);
	      for (const option of [...this.#addFilter.options])
	        option.selected = option.value === "";
	      this.#visibleLimit = this.#pageSize, this.#render();
	    }), this.scope.listen(this.#bulkToggle, "click", () => {
	      this.#bulkMode = !0, this.#topicDraft = null, this.#bulkSelection.clear(), this.#render();
	    }), this.scope.listen(this.#bulkDone, "click", () => {
	      this.#bulkMode = !1, this.#bulkSelection.clear(), this.#render();
	    }), this.scope.listen(this.#bulkSelectAll, "click", () => {
	      const records = this.#matchingRecords(this.#topics.ordered()), allSelected = records.length > 0 && records.every((record) => this.#bulkSelection.has(record.topicId));
	      for (const record of records)
	        allSelected ? this.#bulkSelection.delete(record.topicId) : this.#bulkSelection.add(record.topicId);
	      this.#render();
	    }), this.scope.listen(this.#bulkRestore, "click", () => {
	      const selected = [...this.#bulkSelection];
	      selected.length && (this.#topics.removeMany(selected), this.#bulkSelection.clear(), this.#notify(`已批量恢复 ${selected.length} 个 Topic`));
	    }), this.scope.listen(this.#topicPane, "click", (event) => this.#onClick(event)), this.scope.listen(this.#topicPane, "input", (event) => this.#onInput(event)), this.scope.listen(this.#topicPane, "change", (event) => this.#onChange(event)), this.scope.listen(this.#topicPane, "keydown", (event) => this.#onKeyDown(
	      event
	    )), this.scope.listen(this.#settingsButton, "click", () => this.#showSettings()), this.scope.listen(this.#backButton, "click", () => {
	      this.#returnToTopics();
	    }), this.scope.listen(options.document, "pointerdown", (event) => {
	      this.window.dismissFromPointerEvent(event);
	    }, !0), this.scope.listen(options.document, "keydown", (event) => {
	      this.window.dismissFromEscapeEvent(event);
	    }, !0), this.#topics.changes.subscribe(() => this.#render(), this.scope), this.#render();
	  }
	  open() {
	    this.#showTopics(), this.window.open();
	  }
	  close() {
	    this.window.close();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #createFilterControl(kind, label, className) {
	    const wrapper = (0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "ldp-unwanted-topic-filter-condition"
	    );
	    wrapper.hidden = !0;
	    const select = this.#document.createElement("select");
	    select.className = `ldp-reader-select ${className}`, select.setAttribute("aria-label", `按${label}筛选不想看的 Topic`);
	    const remove = this.#document.createElement("button");
	    return remove.type = "button", remove.className = "ldp-unwanted-topic-filter-remove", remove.dataset.unwantedTopicFilterRemove = kind, remove.setAttribute("aria-label", `移除${label}筛选条件`), remove.title = `移除${label}条件`, remove.append((0, import_reader_icon.createReaderIcon)(this.#document, "x")), wrapper.append(select, remove), this.#filterControls.set(kind, Object.freeze({
	      kind,
	      label,
	      wrapper,
	      select
	    })), select;
	  }
	  #actionButton(label, icon, className) {
	    const button = this.#document.createElement("button");
	    return button.type = "button", button.className = className, button.append(
	      (0, import_reader_icon.createReaderIcon)(this.#document, icon),
	      (0, import_html_element.htmlElement)(this.#document, "span", "", label)
	    ), button;
	  }
	  #showTopics() {
	    this.#mode = "topics", this.#topicPane.hidden = !1, this.#settingsPane.hidden = !0, this.#backButton.hidden = !0, this.#settingsButton.hidden = typeof this.#filterPreferences?.update != "function" || !this.#filterEditor, this.window.setTitle("不想再看"), this.window.meta.textContent = "", this.window.setIcon("eye-off"), this.#render();
	  }
	  #showSettings() {
	    typeof this.#filterPreferences?.update != "function" || !this.#filterEditor || (this.#mode = "settings", this.#topicPane.hidden = !0, this.#settingsPane.hidden = !1, this.#backButton.hidden = !1, this.#settingsButton.hidden = !0, this.window.setTitle("免打扰与自动过滤"), this.window.meta.textContent = "", this.window.setIcon("settings"), this.#filterEditor.open());
	  }
	  async #returnToTopics() {
	    this.#filterEditor && !await this.#filterEditor.saveIfChanged(!1) || this.#showTopics();
	  }
	  #labelCounts(records) {
	    const counts = /* @__PURE__ */ new Map();
	    for (const record of records)
	      for (const label of record.labels) {
	        const key = labelKey(label);
	        if (!key) continue;
	        const previous = counts.get(key);
	        counts.set(key, {
	          label: previous?.label ?? label,
	          count: (previous?.count ?? 0) + 1
	        });
	      }
	    return Object.freeze([...counts].map(([key, value]) => Object.freeze({ key, ...value })).sort((left, right) => right.count - left.count || left.label.localeCompare(right.label, "zh-CN")));
	  }
	  #valueCounts(records, values) {
	    const counts = /* @__PURE__ */ new Map();
	    for (const record of records) {
	      const seen = /* @__PURE__ */ new Set();
	      for (const value of values(record)) {
	        const label = value.replace(/\s+/g, " ").trim(), key = label.toLocaleLowerCase("zh-CN");
	        if (!key || seen.has(key)) continue;
	        seen.add(key);
	        const previous = counts.get(key);
	        counts.set(key, {
	          label: previous?.label ?? label,
	          count: (previous?.count ?? 0) + 1
	        });
	      }
	    }
	    return Object.freeze([...counts].map(([key, value]) => Object.freeze({ key, ...value })).sort((left, right) => right.count - left.count || left.label.localeCompare(right.label, "zh-CN")));
	  }
	  #refreshCategoryLabels() {
	    this.#categoryLabels.clear();
	    for (const category of this.#filterCatalog?.categories() ?? []) {
	      const label = category.name.trim() || category.slug.trim();
	      label && this.#categoryLabels.set(category.id, label);
	    }
	  }
	  #categoryLabel(record) {
	    return record.matchedCategory ? (record.categoryId === null ? "" : this.#categoryLabels.get(record.categoryId) ?? "") || record.categoryName || record.categorySlug || (record.categoryId === null ? "" : `类别 #${record.categoryId}`) : "";
	  }
	  #categoryCounts(records) {
	    const counts = /* @__PURE__ */ new Map();
	    for (const record of records) {
	      const key = categoryRecordKey(record), label = this.#categoryLabel(record);
	      if (!key || !label) continue;
	      const previous = counts.get(key);
	      counts.set(key, {
	        label: previous?.label ?? label,
	        count: (previous?.count ?? 0) + 1
	      });
	    }
	    return Object.freeze([...counts].map(([key, value]) => Object.freeze({ key, ...value })).sort((left, right) => right.count - left.count || left.label.localeCompare(right.label, "zh-CN")));
	  }
	  #matchingRecords(all) {
	    const query = this.#search.value.trim().toLocaleLowerCase("zh-CN"), labelFilter = this.#labelFilter.value, categoryFilter = this.#categoryFilter.value, topicLabelFilter = this.#topicLabelFilter.value, topicAuthorFilter = this.#topicAuthorFilter.value, topicFieldFilter = this.#topicFieldFilter.value, postAuthorFilter = this.#postAuthorFilter.value, sourceFilter = this.#sourceFilter.value;
	    return Object.freeze(all.filter((entry) => (!query || entry.searchText.includes(query) || this.#categoryLabel(entry).toLocaleLowerCase("zh-CN").includes(query)) && (!labelFilter || entry.labels.some((label) => labelKey(label) === labelFilter)) && (!categoryFilter || categoryRecordKey(entry) === categoryFilter) && (!topicLabelFilter || matchedRuleValues(entry, ["标签"]).some((value) => value.toLocaleLowerCase("zh-CN") === topicLabelFilter)) && (!topicAuthorFilter || matchedRuleValues(entry, ["OP"]).some((value) => value.toLocaleLowerCase("zh-CN") === topicAuthorFilter)) && (!topicFieldFilter || matchedRuleValues(entry, ["字段"]).some((value) => value.toLocaleLowerCase("zh-CN") === topicFieldFilter)) && (!postAuthorFilter || matchedRuleValues(
	      entry,
	      ["楼层用户", "楼层"]
	    ).some((value) => value.toLocaleLowerCase("zh-CN") === postAuthorFilter)) && (!sourceFilter || entry.source === sourceFilter)));
	  }
	  #matchedRuleWithoutCategory(record) {
	    return record.matchedCategory ? record.matchedRule.split(";").map((entry) => entry.trim()).filter((entry) => entry && !/^类别[::]/.test(entry)).join(";") : record.matchedRule;
	  }
	  #renderFilterControl(kind, entries, allLabel) {
	    const control = this.#filterControls.get(kind), previous = control.select.value, allOption = this.#document.createElement("option");
	    allOption.value = "", allOption.textContent = `${allLabel} ${entries.length}`, control.select.replaceChildren(
	      allOption,
	      ...entries.map((entry) => {
	        const option = this.#document.createElement("option");
	        return option.value = entry.key, option.textContent = `${entry.label} ${entry.count}`, option;
	      })
	    );
	    const selected = entries.some((entry) => entry.key === previous) ? previous : "";
	    for (const option of [...control.select.options])
	      option.selected = option.value === selected;
	    entries.length || this.#activeFilters.delete(kind), control.wrapper.hidden = !this.#activeFilters.has(kind);
	  }
	  #render() {
	    const all = this.#topics.ordered();
	    this.#topicDraft && !all.some((entry) => entry.topicId === this.#topicDraft?.topicId) && (this.#topicDraft = null), this.#refreshCategoryLabels();
	    const labels = this.#labelCounts(all), categories = this.#categoryCounts(all), topicLabels = this.#valueCounts(all, (record) => matchedRuleValues(record, ["标签"])), topicAuthors = this.#valueCounts(all, (record) => matchedRuleValues(record, ["OP"])), topicFields = this.#valueCounts(all, (record) => matchedRuleValues(record, ["字段"])), postAuthors = this.#valueCounts(all, (record) => matchedRuleValues(record, ["楼层用户", "楼层"])), sources = Object.freeze([
	      Object.freeze({
	        key: "automatic",
	        label: "自动过滤",
	        count: all.filter((record) => record.source === "automatic").length
	      }),
	      Object.freeze({
	        key: "manual",
	        label: "手动免打扰",
	        count: all.filter((record) => record.source === "manual").length
	      })
	    ].filter((entry) => entry.count > 0));
	    this.#renderFilterControl("labels", labels, "全部标签"), this.#renderFilterControl("categories", categories, "全部类别"), this.#renderFilterControl("topic-labels", topicLabels, "全部主题标签"), this.#renderFilterControl("topic-authors", topicAuthors, "全部 OP 用户"), this.#renderFilterControl("topic-fields", topicFields, "全部字符规则"), this.#renderFilterControl("post-authors", postAuthors, "全部楼层用户"), this.#renderFilterControl("sources", sources, "全部来源"), this.#labelOptions.replaceChildren(...labels.map((entry) => {
	      const option = this.#document.createElement("option");
	      return option.value = entry.label, option.label = `${entry.count} 个 Topic`, option;
	    }));
	    const availableConditions = [...this.#filterControls.values()].filter(
	      (control) => !this.#activeFilters.has(control.kind) && control.select.options.length > 1
	    ), addOption = this.#document.createElement("option");
	    addOption.value = "", addOption.textContent = availableConditions.length ? "+ 添加筛选" : "筛选已全部添加", addOption.hidden = availableConditions.length > 0, this.#addFilter.replaceChildren(
	      addOption,
	      ...availableConditions.map((control) => {
	        const option = this.#document.createElement("option");
	        return option.value = control.kind, option.textContent = control.label, option;
	      })
	    ), this.#addFilter.disabled = availableConditions.length === 0;
	    const records = this.#matchingRecords(all), matchingIds = new Set(records.map((record) => record.topicId));
	    for (const topicId of this.#bulkSelection)
	      matchingIds.has(topicId) || this.#bulkSelection.delete(topicId);
	    this.#mode === "topics" && (this.window.meta.textContent = [
	      `${all.length} 个 Topic`,
	      ...labels.length ? [`${labels.length} 个标签`] : [],
	      ...categories.length ? [`${categories.length} 个命中类别`] : []
	    ].join(" · "));
	    const filtering = !!(this.#search.value.trim() || [...this.#filterControls.values()].some((control) => !!control.select.value));
	    if (this.#searchResult.hidden = !filtering, this.#searchResult.textContent = filtering ? `${records.length} 条` : "", this.#bulkToggle.hidden = this.#bulkMode, this.#bulkToggle.disabled = all.length === 0, this.#bulkBar.hidden = !this.#bulkMode, this.#bulkMode) {
	      const selectedCount = this.#bulkSelection.size, allSelected = records.length > 0 && records.every((record) => this.#bulkSelection.has(record.topicId));
	      this.#bulkStatus.textContent = `当前结果 ${records.length} 个 · 已选 ${selectedCount} 个`, this.#bulkSelectAll.disabled = records.length === 0, this.#bulkSelectAll.querySelector("span").textContent = allSelected ? "取消全选" : "全选当前结果", this.#bulkRestore.disabled = selectedCount === 0, this.#bulkRestore.querySelector("span").textContent = selectedCount ? `批量恢复 ${selectedCount} 个` : "批量恢复";
	    }
	    const visibleRecords = records.slice(0, this.#visibleLimit);
	    this.#list.replaceChildren(...visibleRecords.map((entry) => this.#topicRow(entry))), records.length || this.#list.append((0, import_html_element.htmlElement)(
	      this.#document,
	      "p",
	      "ldp-unwanted-topic-empty",
	      filtering ? "没有匹配的 Topic。" : "点击列表里的免打扰图标后,Topic 会消失并收进这里。"
	    )), this.#footerStatus.textContent = all.length ? this.#bulkMode ? "批量管理只作用于当前搜索与筛选结果。" : "点击 Topic 右侧编辑,可添加标注和多个标签。" : "这里不会修改原站通知级别。", this.#loadMore.hidden = visibleRecords.length >= records.length, this.#loadMore.textContent = this.#loadMore.hidden ? "已全部加载" : `加载更多(${visibleRecords.length}/${records.length})`;
	  }
	  #topicRow(record) {
	    const row = (0, import_html_element.htmlElement)(this.#document, "article", "ldp-unwanted-topic-row");
	    row.dataset.unwantedTopicId = String(record.topicId);
	    const editing = this.#topicDraft?.topicId === record.topicId;
	    row.classList.toggle("is-editing", editing);
	    const heading = (0, import_html_element.htmlElement)(this.#document, "header", "ldp-unwanted-topic-head");
	    if (!editing) {
	      let leading;
	      if (this.#bulkMode) {
	        const select = (0, import_html_element.htmlElement)(
	          this.#document,
	          "label",
	          "ldp-unwanted-topic-select"
	        ), checkbox = this.#document.createElement("input");
	        checkbox.type = "checkbox", checkbox.checked = this.#bulkSelection.has(record.topicId), checkbox.dataset.unwantedTopicSelect = String(record.topicId), checkbox.setAttribute("aria-label", `选择 ${record.title}`), select.append(checkbox), leading = select;
	      } else {
	        const open = this.#document.createElement("button");
	        open.type = "button", open.className = "ldp-unwanted-topic-open", open.dataset.unwantedTopicOpen = String(record.topicId), open.setAttribute("aria-label", `打开 ${record.title}`), open.append((0, import_reader_icon.createReaderIcon)(this.#document, "eye-off")), leading = open;
	      }
	      const copy = (0, import_html_element.htmlElement)(this.#document, "span", "ldp-unwanted-topic-copy"), meta = (0, import_html_element.htmlElement)(this.#document, "small", "");
	      meta.append(
	        this.#document.createTextNode(
	          `Topic ${record.topicId} · ${this.#relativeTime(record.hiddenAt)}`
	        )
	      );
	      const category = this.#categoryLabel(record);
	      category && meta.append(
	        this.#document.createTextNode(" · "),
	        (0, import_html_element.htmlElement)(
	          this.#document,
	          "span",
	          "ldp-unwanted-topic-category",
	          category
	        )
	      );
	      const remainingRule = this.#matchedRuleWithoutCategory(record);
	      record.source === "automatic" && remainingRule && meta.append(this.#document.createTextNode(` · ${remainingRule}`)), copy.append((0, import_html_element.htmlElement)(this.#document, "strong", "", record.title), meta);
	      const rowActions2 = (0, import_html_element.htmlElement)(
	        this.#document,
	        "div",
	        "ldp-unwanted-topic-row-actions"
	      );
	      if (!this.#bulkMode) {
	        rowActions2.append(this.#restoreButton(record));
	        const edit = this.#document.createElement("button");
	        edit.type = "button", edit.className = "ldp-unwanted-topic-edit", edit.dataset.unwantedTopicEdit = String(record.topicId), edit.setAttribute("aria-label", `编辑 ${record.title} 的标注和标签`), edit.title = "编辑标注和标签", edit.append((0, import_reader_icon.createReaderIcon)(this.#document, "pencil")), rowActions2.append(edit);
	      }
	      return heading.append(leading, copy, rowActions2), row.append(heading), row;
	    }
	    heading.classList.add("is-editing");
	    const draft = this.#topicDraft, fields = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-unwanted-topic-fields"), noteLabel = (0, import_html_element.htmlElement)(this.#document, "label", "ldp-unwanted-topic-note"), note = this.#document.createElement("input");
	    note.type = "text", note.maxLength = 240, note.value = draft.note, note.placeholder = "可选自定义标注", note.dataset.unwantedTopicNote = String(record.topicId), note.setAttribute("aria-label", `${record.title} 的自定义标注`), noteLabel.append(note);
	    const tagControl = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-unwanted-topic-labels");
	    for (const label of draft.labels) {
	      const chip = this.#document.createElement("button");
	      chip.type = "button", chip.className = "ldp-unwanted-topic-label", chip.dataset.unwantedTopicRemoveLabel = label, chip.dataset.unwantedTopicId = String(record.topicId), chip.setAttribute("aria-label", `移除标签 ${label}`), chip.append(
	        (0, import_html_element.htmlElement)(this.#document, "span", "", label),
	        (0, import_html_element.htmlElement)(this.#document, "span", "ldp-unwanted-topic-label-remove", "×")
	      ), tagControl.append(chip);
	    }
	    const labelInput = this.#document.createElement("input");
	    labelInput.type = "text", labelInput.maxLength = 36, labelInput.placeholder = "添加标签(可多选)", labelInput.autocomplete = "off", labelInput.setAttribute("list", this.#labelOptions.id), labelInput.dataset.unwantedTopicLabelInput = String(record.topicId), labelInput.setAttribute(
	      "aria-label",
	      `${record.title} 的标签;可输入或下拉搜索,按 Enter 累加`
	    ), tagControl.append(labelInput), fields.append(noteLabel, tagControl);
	    const rowActions = (0, import_html_element.htmlElement)(
	      this.#document,
	      "div",
	      "ldp-unwanted-topic-row-actions"
	    );
	    rowActions.append(this.#restoreButton(record));
	    const confirm = this.#document.createElement("button");
	    return confirm.type = "button", confirm.className = "ldp-unwanted-topic-confirm", confirm.dataset.unwantedTopicConfirm = String(record.topicId), confirm.setAttribute("aria-label", `确认 ${record.title} 的标注和标签`), confirm.title = "确认", confirm.append((0, import_reader_icon.createReaderIcon)(this.#document, "check")), rowActions.append(confirm), heading.append(fields, rowActions), row.append(heading), row;
	  }
	  #restoreButton(record) {
	    const restore = this.#document.createElement("button");
	    return restore.type = "button", restore.className = "ldp-unwanted-topic-restore", restore.dataset.unwantedTopicRestore = String(record.topicId), restore.setAttribute("aria-label", `恢复显示 ${record.title}`), restore.title = "恢复显示", restore.append((0, import_reader_icon.createReaderIcon)(this.#document, "rotate-ccw")), restore;
	  }
	  #onClick(event) {
	    const removeFilter = closestTarget(
	      event,
	      "[data-unwanted-topic-filter-remove]"
	    );
	    if (removeFilter) {
	      const kind = removeFilter.dataset.unwantedTopicFilterRemove, control = this.#filterControls.get(kind);
	      if (control) {
	        for (const option of [...control.select.options])
	          option.selected = option.value === "";
	        this.#activeFilters.delete(kind), this.#visibleLimit = this.#pageSize, this.#render();
	      }
	      return;
	    }
	    if (closestTarget(event, "[data-unwanted-topic-load-more]")) {
	      this.#visibleLimit += this.#pageSize, this.#render();
	      return;
	    }
	    const edit = closestTarget(
	      event,
	      "[data-unwanted-topic-edit]"
	    );
	    if (edit) {
	      const topicId2 = Number(edit.dataset.unwantedTopicEdit), record2 = this.#topics.snapshot.records.find((entry) => entry.topicId === topicId2);
	      record2 && (this.#topicDraft = {
	        topicId: topicId2,
	        note: record2.note,
	        labels: [...record2.labels]
	      }, this.#render());
	      return;
	    }
	    const confirm = closestTarget(
	      event,
	      "[data-unwanted-topic-confirm]"
	    );
	    if (confirm && this.#topicDraft?.topicId === Number(confirm.dataset.unwantedTopicConfirm)) {
	      const draft = this.#topicDraft;
	      this.#topicDraft = null, this.#topics.update(draft.topicId, {
	        note: draft.note,
	        labels: draft.labels
	      }), this.#notify("标注与标签已保存");
	      return;
	    }
	    const restore = closestTarget(
	      event,
	      "[data-unwanted-topic-restore]"
	    );
	    if (restore) {
	      const topicId2 = Number(restore.dataset.unwantedTopicRestore);
	      this.#topicDraft = null, this.#topics.remove(topicId2), this.#notify("已移出不想看;列表下次渲染时恢复显示");
	      return;
	    }
	    const removeLabel = closestTarget(
	      event,
	      "[data-unwanted-topic-remove-label]"
	    );
	    if (removeLabel && this.#topicDraft?.topicId === Number(removeLabel.dataset.unwantedTopicId)) {
	      const target = labelKey(removeLabel.dataset.unwantedTopicRemoveLabel);
	      this.#topicDraft.labels = this.#topicDraft.labels.filter((label) => labelKey(label) !== target), this.#render();
	      return;
	    }
	    const open = closestTarget(
	      event,
	      "[data-unwanted-topic-open]"
	    );
	    if (!open) return;
	    const topicId = Number(open.dataset.unwantedTopicOpen), record = this.#topics.snapshot.records.find((entry) => entry.topicId === topicId);
	    record && Promise.resolve(this.#openTarget(record)).catch((cause) => {
	      this.#onError(cause), this.#notify("打开 Topic 失败,请稍后重试");
	    });
	  }
	  #onInput(event) {
	    const note = closestTarget(
	      event,
	      "[data-unwanted-topic-note]"
	    );
	    note && this.#topicDraft?.topicId === Number(note.dataset.unwantedTopicNote) && (this.#topicDraft.note = note.value);
	  }
	  #onChange(event) {
	    const selection = closestTarget(
	      event,
	      "[data-unwanted-topic-select]"
	    );
	    if (selection) {
	      const topicId = Number(selection.dataset.unwantedTopicSelect);
	      selection.checked ? this.#bulkSelection.add(topicId) : this.#bulkSelection.delete(topicId), this.#render();
	      return;
	    }
	    const input = closestTarget(
	      event,
	      "[data-unwanted-topic-label-input]"
	    );
	    input && this.#commitLabel(input);
	  }
	  #onKeyDown(event) {
	    const input = closestTarget(
	      event,
	      "[data-unwanted-topic-label-input]"
	    );
	    !input || event.key !== "Enter" && event.key !== "," && event.key !== "," || (event.preventDefault(), this.#commitLabel(input));
	  }
	  #commitLabel(input) {
	    const raw = input.value.replace(/[,,]\s*$/, "").trim(), key = labelKey(raw);
	    if (!key) {
	      input.value = "";
	      return;
	    }
	    const topicId = Number(input.dataset.unwantedTopicLabelInput);
	    this.#topicDraft?.topicId === topicId && (input.value = "", !this.#topicDraft.labels.some((label) => labelKey(label) === key) && (this.#topicDraft.labels = [...this.#topicDraft.labels, raw], this.#render()));
	  }
	}
}, "a5b50e51c9df89fb2e912708df37a4c4141837abeee36e75cd5563e734a45249");

/* Source: lite/src/discourse/identifiers.ts */
runtime.register("src/discourse/identifiers.js", function(module, exports, require) {
	var identifiers_exports = {};
	__export(identifiers_exports, {
	  discourseAuthScope: () => discourseAuthScope,
	  discoursePostId: () => discoursePostId,
	  discoursePostIdStream: () => discoursePostIdStream,
	  discoursePostIds: () => discoursePostIds,
	  discoursePostNumber: () => discoursePostNumber,
	  discoursePostNumbers: () => discoursePostNumbers,
	  discoursePostReference: () => discoursePostReference,
	  discourseReplyCursor: () => discourseReplyCursor,
	  discourseTopicId: () => discourseTopicId,
	  tryDiscoursePostId: () => tryDiscoursePostId,
	  tryDiscoursePostNumber: () => tryDiscoursePostNumber,
	  tryDiscourseTopicId: () => tryDiscourseTopicId
	});
	module.exports = __toCommonJS(identifiers_exports);
	function positiveInteger(value, name) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return numeric;
	}
	function discourseTopicId(value) {
	  return positiveInteger(value, "topicId");
	}
	function discoursePostId(value) {
	  return positiveInteger(value, "postId");
	}
	function discoursePostNumber(value) {
	  return positiveInteger(value, "postNumber");
	}
	function discourseReplyCursor(value) {
	  const numeric = Number(value ?? 0);
	  if (!Number.isSafeInteger(numeric) || numeric < 0)
	    throw new RangeError("replyCursor 必须是非负安全整数");
	  return numeric;
	}
	function discourseAuthScope(value) {
	  const normalized = String(value ?? "").trim();
	  if (!normalized) throw new Error("authScope 不能为空");
	  return normalized;
	}
	function tryDiscourseTopicId(value) {
	  try {
	    return discourseTopicId(value);
	  } catch {
	    return null;
	  }
	}
	function tryDiscoursePostId(value) {
	  try {
	    return discoursePostId(value);
	  } catch {
	    return null;
	  }
	}
	function tryDiscoursePostNumber(value) {
	  try {
	    return discoursePostNumber(value);
	  } catch {
	    return null;
	  }
	}
	function discoursePostReference(input) {
	  const postNumber = discoursePostNumber(input.post_number), parent = tryDiscoursePostNumber(input.reply_to_post_number);
	  if (parent === postNumber)
	    throw new Error(`楼层 #${postNumber} 不能回复自身`);
	  return Object.freeze({
	    postId: tryDiscoursePostId(input.id),
	    topicId: tryDiscourseTopicId(input.topic_id),
	    postNumber,
	    replyToPostNumber: parent
	  });
	}
	function discoursePostIds(values) {
	  const normalized = [...new Set(values.map(discoursePostId))].sort((left, right) => left - right);
	  if (!normalized.length) throw new Error("postIds 不能为空");
	  return Object.freeze(normalized);
	}
	function discoursePostIdStream(values) {
	  const seen = /* @__PURE__ */ new Set(), stream = [];
	  for (const value of values) {
	    const postId = discoursePostId(value);
	    seen.has(postId) || (seen.add(postId), stream.push(postId));
	  }
	  return Object.freeze(stream);
	}
	function discoursePostNumbers(values) {
	  const normalized = [...new Set(values.map(discoursePostNumber))].sort((left, right) => left - right);
	  if (!normalized.length) throw new Error("postNumbers 不能为空");
	  return Object.freeze(normalized);
	}
}, "df62eb641b1752e66e82842fdbbd0c8d27ee6ca3384dbfb60a23197a629599a9");

/* Source: lite/src/discourse/ingest-version.ts */
runtime.register("src/discourse/ingest-version.js", function(module, exports, require) {
	var ingest_version_exports = {};
	__export(ingest_version_exports, {
	  discourseObservedAt: () => discourseObservedAt,
	  normalizeDiscourseIngestSource: () => normalizeDiscourseIngestSource,
	  shouldReplaceDiscourseRemoval: () => shouldReplaceDiscourseRemoval,
	  shouldReplaceDiscourseVersion: () => shouldReplaceDiscourseVersion
	});
	module.exports = __toCommonJS(ingest_version_exports);
	const SOURCE_RANK = Object.freeze({
	  "loader-batch": 1,
	  "topic-json": 2,
	  "target-refresh": 3,
	  "action-response": 4,
	  "message-bus": 5
	});
	function discourseObservedAt(value, name = "observedAt") {
	  const numeric = Number(value);
	  if (!Number.isFinite(numeric) || numeric < 0)
	    throw new RangeError(`${name} 必须是非负有限时间戳`);
	  return numeric;
	}
	function normalizeDiscourseIngestSource(value) {
	  return typeof value == "string" && Object.hasOwn(SOURCE_RANK, value) ? value : null;
	}
	function shouldReplaceDiscourseVersion(current, next) {
	  return current ? next.source === "loader-batch" && current.source !== null && current.source !== "loader-batch" ? !1 : next.observedAt !== current.observedAt ? next.observedAt > current.observedAt : current.source === null ? !0 : SOURCE_RANK[next.source] >= SOURCE_RANK[current.source] : !0;
	}
	function shouldReplaceDiscourseRemoval(current, next) {
	  return current.source === "action-response" && (next.source === "loader-batch" || next.source === "topic-json") ? !1 : shouldReplaceDiscourseVersion(current, next);
	}
}, "329717cc7f84a32026374e649cf6c00b22be5286b8e69dc5d2178a55f025dcd1");

/* Source: lite/src/discourse/native-composer.ts */
runtime.register("src/discourse/native-composer.js", function(module, exports, require) {
	var native_composer_exports = {};
	__export(native_composer_exports, {
	  DiscourseComposerCoordinator: () => DiscourseComposerCoordinator,
	  DiscourseComposerEventPort: () => DiscourseComposerEventPort,
	  DiscourseComposerHostIsolation: () => DiscourseComposerHostIsolation,
	  DiscourseComposerTopicSyncController: () => DiscourseComposerTopicSyncController
	});
	module.exports = __toCommonJS(native_composer_exports);
	var import_identifiers = require("./identifiers.js"), import_native_post_model_factory = require("./native-post-model-factory.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_repeat_action_gate = require("../kernel/repeat-action-gate.js"), import_signal = require("../kernel/signal.js"), import_value_record = require("../kernel/value-record.js");
	function modelValue(model, key) {
	  const value = (0, import_value_record.valueRecord)(model);
	  if (!value) return;
	  const getter = value.get;
	  return typeof getter == "function" ? getter.call(value, key) : value[key];
	}
	function composerModelTopicId(model) {
	  const post = modelValue(model, "post"), topic = modelValue(model, "topic") ?? modelValue(post, "topic");
	  return Number(
	    modelValue(topic, "id") ?? modelValue(model, "topic_id") ?? modelValue(model, "topicId") ?? modelValue(post, "topic_id") ?? modelValue(post, "topicId")
	  );
	}
	function setModelValue(model, key, value) {
	  const target = (0, import_value_record.valueRecord)(model);
	  if (!target) throw new Error("Discourse composer model 不可写");
	  const setter = target.set;
	  typeof setter == "function" ? setter.call(target, key, value) : target[key] = value;
	}
	function moduleDefault(host, name) {
	  const module2 = (0, import_value_record.valueRecord)(host.lookupModule(name)), value = (0, import_value_record.valueRecord)(module2?.default);
	  if (!value) throw new Error(`Discourse 原生模块未就绪:${name}`);
	  return value;
	}
	function normalizedDraftSequence(value) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 0)
	    throw new Error("Topic 缺少合法 draft_sequence");
	  return numeric;
	}
	function normalizedDraftKey(value) {
	  const normalized = String(value ?? "").trim();
	  if (!normalized) throw new Error("Topic 缺少 draft_key");
	  return normalized;
	}
	function normalizedMentionUsername(value) {
	  return String(value ?? "").trim().replace(/^@+/, "");
	}
	function rawMentionsUsername(raw, usernameValue) {
	  const username = normalizedMentionUsername(usernameValue);
	  if (!raw || !username) return !1;
	  const escaped = username.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	  return new RegExp(
	    `(^|[^A-Za-z0-9_@-])@${escaped}(?=$|[^A-Za-z0-9_-])`,
	    "i"
	  ).test(raw);
	}
	function composerTextInsertion(currentValue, blockValue, startValue, endValue) {
	  const current = String(currentValue ?? ""), start = Math.min(Math.max(0, startValue), current.length), end = Math.min(Math.max(start, endValue), current.length), before = current.slice(0, start), after = current.slice(end);
	  let inserted = String(blockValue ?? "").trim();
	  return before && !/\s$/.test(before) && (inserted = `

${inserted}`), after && !/^\s/.test(after) && (inserted += `

`), Object.freeze({
	    next: `${before}${inserted}${after}`,
	    inserted,
	    cursor: before.length + inserted.length
	  });
	}
	function composerRequestKey(input) {
	  const topicId = (0, import_identifiers.discourseTopicId)(input.topic.id), post = (0, import_identifiers.discoursePostReference)(input.post);
	  return JSON.stringify([
	    "reply",
	    topicId,
	    post.postId,
	    post.postNumber,
	    String(input.initialRaw ?? "").trim(),
	    String(input.initialRichHtml ?? "").trim(),
	    normalizedMentionUsername(input.dedupeMention),
	    input.replaceRaw === !0
	  ]);
	}
	function composerEditRequestKey(input) {
	  const topicId = (0, import_identifiers.discourseTopicId)(input.topic.id), post = (0, import_identifiers.discoursePostReference)(input.post);
	  return JSON.stringify([
	    "edit",
	    topicId,
	    post.postId,
	    post.postNumber
	  ]);
	}
	function privateMessageRequestKey(username) {
	  return JSON.stringify(["private-message", username]);
	}
	function parseDraft(value) {
	  if (value == null || value === "") return { reply: "" };
	  const parsed = JSON.parse(String(value));
	  if (!parsed || typeof parsed != "object" || Array.isArray(parsed))
	    throw new Error("Discourse composer draft 格式非法");
	  const source = parsed;
	  return Object.freeze({
	    reply: String(source.reply ?? ""),
	    ...source.whisper === void 0 ? {} : { whisper: source.whisper }
	  });
	}
	class DiscourseComposerCoordinator {
	  scope;
	  #host;
	  #document;
	  #models;
	  #isolation;
	  #waitForDelay;
	  #composerOpenTimeoutMs;
	  #onError;
	  #requests = /* @__PURE__ */ new Map();
	  #queue = Promise.resolve();
	  #session = null;
	  #submitPromise = null;
	  #windowPort = null;
	  constructor(options) {
	    this.#host = options.host, this.#document = options.document ?? null, this.#models = new import_native_post_model_factory.DiscourseNativePostModelFactory(options.host), this.#isolation = options.isolation ?? null, this.#waitForDelay = options.waitForDelay ?? ((milliseconds) => new Promise((resolve) => globalThis.setTimeout(resolve, milliseconds))), this.#composerOpenTimeoutMs = Math.max(
	      0,
	      Number(options.composerOpenTimeoutMs) || 1600
	    ), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.#requests.clear(), this.#queue = Promise.resolve(), this.#session = null, this.#submitPromise = null;
	    });
	  }
	  isOpen() {
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), model = modelValue(composer, "model");
	    return modelValue(model, "viewOpen") === !0 || String(modelValue(model, "composeState") ?? "").toLowerCase() === "open";
	  }
	  /**
	   * 在用户点击回复前只读解析宿主 Composer service、Draft 与原生 model。
	   *
	   * BrowserDiscourseHostApiPort 会缓存成功解析的 service/module,因此应用存续期间的
	   * 首次回复不再承担 module resolver 冷启动;这里不会调用 Draft.get、composer.open
	   * 或创建任何 Topic/Post model。
	   */
	  warmReply() {
	    this.#assertActive();
	    try {
	      return this.#replyRuntime(), this.#models.prepareComposerBindings(), !0;
	    } catch {
	      return !1;
	    }
	  }
	  installCloseGuard(options) {
	    this.#assertActive();
	    const scope = options.parentScope ? options.parentScope.child() : this.scope.child(), gate = new import_repeat_action_gate.RepeatActionGate(), handledEvents = /* @__PURE__ */ new WeakSet(), consume = (event) => {
	      event.preventDefault(), event.stopImmediatePropagation();
	    }, requiresConfirmation = (key, message) => options.enabled() ? gate.confirm(key) ? !1 : (options.notify?.(message), !0) : (gate.clear(), !1), discardComposer = (event) => {
	      consume(event), this.discard().catch((error) => {
	        this.#onError(error), options.notify?.("舍弃回复失败,请重试");
	      });
	    }, ownsActiveComposer = () => {
	      const session = this.#session, composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer"));
	      return !!(session && modelValue(composer, "model") === session.model);
	    }, onClick = (event) => {
	      if (handledEvents.has(event)) return;
	      const control = event.target?.closest(
	        "#reply-control .toggle-save-and-close,#reply-control .discard-button"
	      );
	      if (!control) return;
	      handledEvents.add(event);
	      const shouldDiscard = control.classList.contains("discard-button");
	      if (requiresConfirmation(
	        shouldDiscard ? "composer:discard" : "composer:close",
	        shouldDiscard ? "再点一次舍弃回复" : "再点一次关闭回复窗口"
	      )) {
	        consume(event);
	        return;
	      }
	      shouldDiscard && ownsActiveComposer() && discardComposer(event);
	    }, onKeyDown = (event) => {
	      if (handledEvents.has(event)) return;
	      const keyboard = event;
	      if (!(keyboard.key !== "Escape" || keyboard.repeat || keyboard.defaultPrevented || !this.isOpen())) {
	        if (handledEvents.add(event), requiresConfirmation("composer:escape", "再按一次 Esc 舍弃回复")) {
	          consume(event);
	          return;
	        }
	        ownsActiveComposer() && discardComposer(event);
	      }
	    }, captureTarget = options.document.defaultView ?? options.document;
	    return scope.listen(captureTarget, "click", onClick, !0), scope.listen(captureTarget, "keydown", onKeyDown, !0), captureTarget !== options.document && (scope.listen(options.document, "click", onClick, !0), scope.listen(options.document, "keydown", onKeyDown, !0)), scope.add(() => gate.clear()), scope;
	  }
	  async discard() {
	    this.#assertActive();
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), model = modelValue(composer, "model");
	    if (!composer || !(0, import_value_record.valueRecord)(model)) {
	      this.#session = null;
	      return;
	    }
	    const destroyDraft = composer.destroyDraft;
	    if (typeof destroyDraft != "function")
	      throw new Error("Discourse composer.destroyDraft 尚未就绪");
	    const cleanupErrors = [];
	    try {
	      composer.skipAutoSave = !0;
	      try {
	        const runloop = (0, import_value_record.valueRecord)(this.#host.lookupModule("@ember/runloop")), cancel = runloop?.cancel;
	        typeof cancel == "function" && composer._saveDraftDebounce && cancel.call(runloop, composer._saveDraftDebounce);
	      } catch (error) {
	        cleanupErrors.push(error);
	      }
	      if (await destroyDraft.call(composer), modelValue(composer, "model") === model) {
	        const mutableModel = (0, import_value_record.valueRecord)(model);
	        try {
	          typeof mutableModel?.clearState == "function" && mutableModel.clearState.call(mutableModel);
	        } catch (error) {
	          cleanupErrors.push(error);
	        }
	        try {
	          typeof composer.close == "function" && composer.close.call(composer);
	        } catch (error) {
	          cleanupErrors.push(error);
	        }
	        try {
	          const appEvents = (0, import_value_record.valueRecord)(
	            composer.appEvents ?? this.#host.lookup("service:app-events")
	          );
	          typeof appEvents?.trigger == "function" && appEvents.trigger.call(appEvents, "composer:cancelled");
	        } catch (error) {
	          cleanupErrors.push(error);
	        }
	      }
	      this.#session = null;
	    } finally {
	      try {
	        composer.skipAutoSave = !1;
	      } catch (error) {
	        cleanupErrors.push(error);
	      }
	    }
	    if (cleanupErrors.length) {
	      const cleanupError = new AggregateError(
	        cleanupErrors,
	        "Discourse composer 舍弃后清理失败"
	      );
	      try {
	        this.#onError(cleanupError);
	      } catch {
	      }
	    }
	  }
	  installSubmitGuard(options) {
	    if (this.#assertActive(), !this.#isolation)
	      throw new Error("Discourse Composer submit guard 缺少宿主隔离 owner");
	    const scope = options.parentScope ? options.parentScope.child() : this.scope.child(), submit = (event) => {
	      const button = event.target?.closest(
	        "#reply-control .save-or-cancel button.create"
	      ) ?? null;
	      !button || button !== this.#submitButton(options.document) || (event.preventDefault(), event.stopImmediatePropagation(), this.#submit(button));
	    };
	    return scope.listen(options.document, "click", submit, !0), scope.listen(options.document, "keydown", (event) => {
	      const keyboard = event;
	      if (keyboard.isComposing || !(keyboard.ctrlKey || keyboard.metaKey) || keyboard.key !== "Enter" && keyboard.code !== "Enter") return;
	      const button = this.#submitButton(options.document);
	      button && (keyboard.preventDefault(), keyboard.stopImmediatePropagation(), this.#submit(button));
	    }, !0), scope;
	  }
	  openReply(input) {
	    return this.#enqueue(
	      composerRequestKey(input),
	      () => this.#openReply(input)
	    );
	  }
	  openEdit(input) {
	    return this.#enqueue(
	      composerEditRequestKey(input),
	      () => this.#openEdit(input)
	    );
	  }
	  openPrivateMessage(usernameValue) {
	    const username = String(usernameValue).trim().replace(/^@+/, "");
	    if (!username) throw new Error("私信 username 不能为空");
	    return this.#enqueue(
	      privateMessageRequestKey(username),
	      () => this.#openPrivateMessage(username)
	    );
	  }
	  bindWindow(port) {
	    return this.#assertActive(), this.#windowPort = port, this.#presentComposerWindow(), () => {
	      this.#windowPort === port && (this.#windowPort = null);
	    };
	  }
	  #enqueue(key, execute) {
	    this.#assertActive();
	    const existing = this.#requests.get(key);
	    if (existing) return existing;
	    const request = this.#queue.then(
	      () => (this.#assertActive(), execute()),
	      () => (this.#assertActive(), execute())
	    ).catch((error) => {
	      throw this.#onError(error), error;
	    }).finally(() => {
	      this.#requests.get(key) === request && this.#requests.delete(key);
	    });
	    return this.#requests.set(key, request), this.#queue = request.then(
	      () => {
	      },
	      () => {
	      }
	    ), request;
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #openPrivateMessage(username) {
	    if (this.isOpen())
	      throw new Error("Discourse 原生 composer 正在处理另一项编辑,请先完成或关闭");
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), openNewMessage = composer?.openNewMessage;
	    if (!composer || typeof openNewMessage != "function")
	      throw new Error("Discourse 原生 composer.openNewMessage 尚未就绪");
	    if (await openNewMessage.call(composer, { recipients: username }), this.#assertActive(), !await this.#waitForComposerPopup(this.#composerOpenTimeoutMs))
	      throw new Error("Discourse 原生私信浮窗未显示");
	    if (!this.#presentComposerWindow())
	      throw typeof composer.close == "function" && composer.close.call(composer), new Error("Reader 私信浮窗未接管");
	    return this.#focusComposerInput(), Object.freeze({ username });
	  }
	  async #openReply(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topic.id), postReference = (0, import_identifiers.discoursePostReference)(input.post);
	    if (input.post.topic_id !== void 0 && (0, import_identifiers.discourseTopicId)(input.post.topic_id) !== topicId)
	      throw new Error("composer 回复楼层不属于目标 Topic");
	    const {
	      composer,
	      appEvents,
	      replyAction,
	      draft: Draft
	    } = this.#replyRuntime(), topicModel = this.#models.createTopic(input.topic), postModel = this.#models.createPost(input.topic, input.post, topicModel), currentModel = modelValue(composer, "model"), currentTopicId = composerModelTopicId(currentModel), currentOpen = modelValue(currentModel, "viewOpen") === !0 || String(modelValue(currentModel, "composeState") ?? "").toLowerCase() === "open", currentAction = modelValue(currentModel, "action"), currentReply = String(modelValue(currentModel, "reply") ?? ""), initialRaw = String(input.initialRaw ?? "").trim(), initialRichHtml = String(input.initialRichHtml ?? "").trim(), dedupeMention = normalizedMentionUsername(input.dedupeMention);
	    if (currentOpen && currentTopicId === topicId && currentAction === replyAction && await this.#waitForComposerPopup(640)) {
	      setModelValue(
	        currentModel,
	        "post",
	        postReference.postNumber === 1 ? null : postModel
	      ), this.#presentComposerWindow();
	      const duplicateMention = input.replaceRaw !== !0 && initialRaw && rawMentionsUsername(currentReply, dedupeMention);
	      if (!duplicateMention) {
	        if (initialRaw && input.replaceRaw === !0)
	          setModelValue(currentModel, "reply", initialRaw);
	        else if (initialRaw) {
	          const composerInput = await this.#waitForComposerInput(640);
	          if (this.#document && !composerInput)
	            throw new Error("Discourse 原生回复编辑器未就绪");
	          this.#insertComposerBlock(
	            appEvents,
	            currentModel,
	            initialRaw,
	            initialRichHtml,
	            composerInput
	          );
	        }
	      }
	      const session2 = Object.freeze({
	        topicId,
	        parentPostNumber: postReference.postNumber,
	        action: "reply",
	        reused: !0,
	        model: currentModel,
	        ...duplicateMention ? { insertionSkipped: "duplicate-mention" } : {}
	      });
	      return this.#session = session2, this.#focusComposerInput(), session2;
	    }
	    if (currentOpen) {
	      const popupAvailable = this.#composerPopupAvailable();
	      if ((currentTopicId === topicId || !popupAvailable) && typeof composer.close == "function")
	        composer.close.call(composer), await this.#waitForDelay(0);
	      else
	        throw new Error(
	          "Discourse 原生 composer 正在处理另一项编辑,请先完成或关闭"
	        );
	    }
	    const key = normalizedDraftKey(input.topic.draft_key), sequence = normalizedDraftSequence(input.topic.draft_sequence);
	    let insertAfterOpen = !1, insertionSkipped;
	    const options = {
	      action: replyAction,
	      draftKey: key,
	      draftSequence: sequence,
	      skipJumpOnSave: !0,
	      ...postReference.postNumber === 1 ? { topic: topicModel } : { post: postModel }
	    }, draftResult = (0, import_value_record.valueRecord)(await Draft.get.call(Draft, key));
	    if (draftResult?.draft) {
	      const draft = parseDraft(draftResult.draft);
	      options.draftSequence = normalizedDraftSequence(
	        draftResult.draft_sequence ?? sequence
	      ), input.replaceRaw !== !0 && initialRaw && rawMentionsUsername(draft.reply, dedupeMention) ? (options.reply = draft.reply, insertionSkipped = "duplicate-mention") : input.replaceRaw === !0 ? options.reply = initialRaw : initialRaw && initialRichHtml ? (options.reply = draft.reply, insertAfterOpen = !0) : options.reply = `${draft.reply}${initialRaw ? `
${initialRaw}` : ""}`, draft.whisper !== void 0 && (options.whisper = draft.whisper);
	    } else initialRaw && (input.replaceRaw === !0 ? options.reply = initialRaw : initialRichHtml ? insertAfterOpen = !0 : options.quote = initialRaw);
	    if (currentReply && currentOpen && currentTopicId === topicId && (input.replaceRaw !== !0 && initialRaw && rawMentionsUsername(currentReply, dedupeMention) ? (options.reply = currentReply, insertAfterOpen = !1, insertionSkipped = "duplicate-mention") : input.replaceRaw === !0 ? (options.reply = initialRaw, insertAfterOpen = !1) : initialRaw && initialRichHtml ? (options.reply = currentReply, insertAfterOpen = !0) : options.reply = `${currentReply}${initialRaw ? `
${initialRaw}` : ""}`, delete options.quote), await composer.open.call(composer, options), this.#assertActive(), !await this.#waitForComposerPopup(this.#composerOpenTimeoutMs))
	      throw new Error("Discourse 原生回复浮窗未显示");
	    const model = modelValue(composer, "model");
	    if (!(0, import_value_record.valueRecord)(model)) throw new Error("Discourse composer.open 未生成 model");
	    if (this.#presentComposerWindow(), insertAfterOpen && initialRaw) {
	      const composerInput = await this.#waitForComposerInput(
	        this.#composerOpenTimeoutMs
	      );
	      if (this.#document && !composerInput)
	        throw new Error("Discourse 原生回复编辑器未就绪");
	      this.#insertComposerBlock(
	        appEvents,
	        model,
	        initialRaw,
	        initialRichHtml,
	        composerInput
	      );
	    }
	    const session = Object.freeze({
	      topicId,
	      parentPostNumber: postReference.postNumber,
	      action: "reply",
	      reused: !1,
	      model,
	      ...insertionSkipped ? { insertionSkipped } : {}
	    });
	    return this.#session = session, this.#focusComposerInput(), session;
	  }
	  #replyRuntime() {
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), appEvents = (0, import_value_record.valueRecord)(
	      composer?.appEvents ?? this.#host.lookup("service:app-events")
	    );
	    if (!composer || typeof composer.open != "function" || !appEvents)
	      throw new Error("Discourse 原生 composer service 未就绪");
	    const replyAction = moduleDefault(this.#host, "discourse/models/composer").REPLY;
	    if (!replyAction) throw new Error("Discourse Composer.REPLY 未就绪");
	    const draft = moduleDefault(this.#host, "discourse/models/draft");
	    if (typeof draft.get != "function")
	      throw new Error("Discourse Draft.get 未就绪");
	    return Object.freeze({
	      composer,
	      appEvents,
	      replyAction,
	      draft
	    });
	  }
	  #presentComposerWindow() {
	    const popup = this.#document?.querySelector("#reply-control") ?? null;
	    return !popup || !this.#composerPopupAvailable() ? !1 : this.#windowPort?.open(popup) ?? !1;
	  }
	  #composerPopupAvailable() {
	    if (!this.#document) return !0;
	    const popup = this.#document.querySelector("#reply-control");
	    return !(!popup?.isConnected || popup.hidden || popup.classList.contains("closed") || popup.classList.contains("hidden") || popup.classList.contains("d-none") || popup.getAttribute("aria-hidden") === "true" || popup.closest('[hidden],[aria-hidden="true"]'));
	  }
	  async #waitForComposerPopup(timeoutMs) {
	    if (!this.#document) return !0;
	    const deadline = Date.now() + Math.max(0, timeoutMs);
	    do {
	      if (this.#composerPopupAvailable()) return !0;
	      if (Date.now() >= deadline) return !1;
	      await this.#waitForDelay(80), this.#assertActive();
	    } while (Date.now() <= deadline);
	    return !1;
	  }
	  #composerInput() {
	    const document = this.#document;
	    if (!document || !this.#composerPopupAvailable()) return null;
	    const selector = '#reply-control textarea.d-editor-input,#reply-control input.d-editor-input,#reply-control .ProseMirror.d-editor-input[contenteditable="true"],#reply-control [contenteditable="true"].d-editor-input,#reply-control [role="textbox"][contenteditable="true"],#reply-control .ProseMirror[contenteditable="true"],#reply-control textarea', available = (candidate) => {
	      if (!candidate.isConnected || candidate.hidden || candidate.style.display === "none" || candidate.style.visibility === "hidden" || candidate.matches(
	        '[disabled],[aria-disabled="true"],[aria-hidden="true"],.hidden,.d-none'
	      ) || candidate.closest('[hidden],[aria-hidden="true"],.hidden,.d-none')) return !1;
	      const view = document.defaultView;
	      if (typeof view?.getComputedStyle == "function") {
	        const style = view.getComputedStyle(candidate);
	        if (style.display === "none" || style.visibility === "hidden" || typeof candidate.getClientRects == "function" && candidate.getClientRects().length === 0) return !1;
	      }
	      return !0;
	    }, active = document.activeElement;
	    return active?.matches(selector) && available(active) ? active : [...document.querySelectorAll(selector)].find(available) ?? null;
	  }
	  async #waitForComposerInput(timeoutMs) {
	    if (!this.#document) return null;
	    const deadline = Date.now() + Math.max(0, timeoutMs);
	    do {
	      const input = this.#composerInput();
	      if (input) return input;
	      if (Date.now() >= deadline) return null;
	      await this.#waitForDelay(80), this.#assertActive();
	    } while (Date.now() <= deadline);
	    return null;
	  }
	  #focusComposerInput() {
	    const document = this.#document;
	    if (!document) return;
	    const focus = () => {
	      if (!this.#composerPopupAvailable()) return;
	      const input = this.#composerInput();
	      if (input)
	        try {
	          input.focus({ preventScroll: !0 });
	        } catch {
	          input.focus();
	        }
	    }, viewport = document.defaultView;
	    viewport?.requestAnimationFrame ? viewport.requestAnimationFrame(focus) : queueMicrotask(focus);
	  }
	  #insertComposerBlock(appEvents, model, raw, richHtml, composerInput = this.#composerInput()) {
	    const richEditor = richHtml && composerInput?.matches(
	      '.ProseMirror[contenteditable="true"]'
	    ) ? composerInput : null;
	    if (richEditor) {
	      if (this.#insertRichText(richEditor, richHtml, raw)) return;
	      throw new Error("Discourse 富文本 Composer 引用插入失败");
	    }
	    if (composerInput?.matches("textarea,input.d-editor-input")) {
	      this.#insertTextControlBlock(
	        composerInput,
	        model,
	        raw
	      );
	      return;
	    }
	    const trigger = appEvents.trigger;
	    if (typeof trigger != "function")
	      throw new Error("Discourse app-events 缺少 composer insert-block");
	    trigger.call(appEvents, "composer:insert-block", raw);
	  }
	  #insertTextControlBlock(editor, model, raw) {
	    const modelRaw = String(modelValue(model, "reply") ?? ""), current = editor.value || modelRaw, wasFocused = editor.ownerDocument.activeElement === editor;
	    let start = Number.isInteger(editor.selectionStart) ? Number(editor.selectionStart) : current.length, end = Number.isInteger(editor.selectionEnd) ? Number(editor.selectionEnd) : start;
	    !wasFocused && current && start === 0 && end === 0 && (start = current.length, end = current.length);
	    const insertion = composerTextInsertion(
	      current,
	      raw,
	      start,
	      end
	    );
	    editor.focus(), this.#setTextControlValue(editor, insertion.next), this.#dispatchTextControlInput(editor, insertion.inserted), setModelValue(model, "reply", insertion.next);
	    try {
	      editor.setSelectionRange(insertion.cursor, insertion.cursor);
	    } catch {
	    }
	    if (String(modelValue(model, "reply") ?? "") !== insertion.next || editor.value !== insertion.next)
	      throw new Error("Discourse Markdown Composer 引用插入失败");
	  }
	  #setTextControlValue(editor, value) {
	    try {
	      if (editor.value = value, editor.value === value) return;
	    } catch {
	    }
	    const view = editor.ownerDocument.defaultView;
	    for (const prototype of [
	      view?.HTMLTextAreaElement?.prototype,
	      view?.HTMLInputElement?.prototype,
	      Object.getPrototypeOf(editor)
	    ]) {
	      if (!prototype) continue;
	      const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
	      if (setter)
	        try {
	          if (setter.call(editor, value), editor.value === value) return;
	        } catch {
	        }
	    }
	    throw new Error("Discourse Markdown Composer value 不可写");
	  }
	  #dispatchTextControlInput(editor, inserted) {
	    const InputEventConstructor = editor.ownerDocument.defaultView?.InputEvent;
	    if (typeof InputEventConstructor == "function")
	      try {
	        editor.dispatchEvent(new InputEventConstructor("input", {
	          bubbles: !0,
	          inputType: "insertText",
	          data: inserted
	        }));
	      } catch {
	        this.#dispatchComposerInput(editor);
	      }
	    else
	      this.#dispatchComposerInput(editor);
	    const change = editor.ownerDocument.createEvent("Event");
	    change.initEvent("change", !0, !1), editor.dispatchEvent(change);
	  }
	  #insertRichText(editor, html, raw) {
	    const document = this.#document;
	    if (!document) return !1;
	    try {
	      editor.focus({ preventScroll: !0 });
	    } catch {
	      editor.focus();
	    }
	    const view = document.defaultView, DataTransferConstructor = view?.DataTransfer, ClipboardEventConstructor = view?.ClipboardEvent;
	    if (typeof DataTransferConstructor == "function" && typeof ClipboardEventConstructor == "function")
	      try {
	        const before = editor.innerHTML, data = new DataTransferConstructor();
	        data.setData("text/html", html), data.setData("text/plain", raw), data.setData("text/markdown", raw);
	        const event = new ClipboardEventConstructor("paste", {
	          bubbles: !0,
	          cancelable: !0,
	          clipboardData: data
	        });
	        if (editor.dispatchEvent(event), editor.innerHTML !== before) return !0;
	      } catch {
	      }
	    const execute = document.execCommand;
	    if (typeof execute == "function")
	      try {
	        const before = editor.innerHTML;
	        if (execute.call(document, "insertHTML", !1, html) && editor.innerHTML !== before)
	          return this.#dispatchComposerInput(editor), !0;
	      } catch {
	      }
	    const selection = document.getSelection?.() ?? view?.getSelection?.() ?? null;
	    if (!selection) return !1;
	    let range;
	    selection.rangeCount > 0 && editor.contains(selection.anchorNode) ? range = selection.getRangeAt(0) : (range = document.createRange(), range.selectNodeContents(editor), range.collapse(!1), selection.removeAllRanges(), selection.addRange(range));
	    try {
	      const before = editor.innerHTML;
	      range.deleteContents();
	      const fragment = range.createContextualFragment(html), last = fragment.lastChild;
	      return range.insertNode(fragment), last && (range.setStartAfter(last), range.collapse(!0), selection.removeAllRanges(), selection.addRange(range)), this.#dispatchComposerInput(editor), editor.innerHTML !== before;
	    } catch {
	      return !1;
	    }
	  }
	  #dispatchComposerInput(editor) {
	    const event = editor.ownerDocument.createEvent("Event");
	    event.initEvent("input", !0, !1), editor.dispatchEvent(event);
	  }
	  async #openEdit(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topic.id), postReference = (0, import_identifiers.discoursePostReference)(input.post);
	    if (input.post.topic_id !== void 0 && (0, import_identifiers.discourseTopicId)(input.post.topic_id) !== topicId)
	      throw new Error("composer 编辑楼层不属于目标 Topic");
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer"));
	    if (!composer || typeof composer.open != "function")
	      throw new Error("Discourse 原生 composer service 未就绪");
	    const editAction = moduleDefault(this.#host, "discourse/models/composer").EDIT;
	    if (!editAction) throw new Error("Discourse Composer.EDIT 未就绪");
	    const topicModel = this.#models.createTopic(input.topic), postModel = this.#models.createPost(
	      input.topic,
	      input.post,
	      topicModel
	    ), currentModel = modelValue(composer, "model"), currentTopicId = Number(
	      modelValue(
	        modelValue(currentModel, "topic") ?? modelValue(modelValue(currentModel, "post"), "topic"),
	        "id"
	      )
	    ), currentOpen = modelValue(currentModel, "viewOpen") === !0 || String(modelValue(currentModel, "composeState") ?? "").toLowerCase() === "open", currentAction = modelValue(currentModel, "action");
	    if (currentOpen) {
	      const currentPostId = Number(
	        modelValue(modelValue(currentModel, "post"), "id")
	      );
	      if (currentTopicId !== topicId || currentAction !== editAction || currentPostId !== postReference.postId)
	        throw new Error("Discourse 原生 composer 正在处理另一项编辑,请先完成或关闭");
	      const session2 = Object.freeze({
	        topicId,
	        parentPostNumber: postReference.postNumber,
	        action: "edit",
	        reused: !0,
	        model: currentModel
	      });
	      if (!await this.#waitForComposerPopup(this.#composerOpenTimeoutMs))
	        throw new Error("Discourse 原生编辑浮窗未显示");
	      return this.#session = session2, this.#presentComposerWindow(), this.#focusComposerInput(), session2;
	    }
	    if (await composer.open.call(composer, {
	      action: editAction,
	      post: postModel,
	      draftKey: normalizedDraftKey(input.topic.draft_key),
	      draftSequence: normalizedDraftSequence(input.topic.draft_sequence),
	      skipJumpOnSave: !0
	    }), this.#assertActive(), !await this.#waitForComposerPopup(this.#composerOpenTimeoutMs))
	      throw new Error("Discourse 原生编辑浮窗未显示");
	    const model = modelValue(composer, "model");
	    if (!(0, import_value_record.valueRecord)(model)) throw new Error("Discourse composer.open 未生成 edit model");
	    const session = Object.freeze({
	      topicId,
	      parentPostNumber: postReference.postNumber,
	      action: "edit",
	      reused: !1,
	      model
	    });
	    return this.#session = session, this.#presentComposerWindow(), this.#focusComposerInput(), session;
	  }
	  #submitButton(document) {
	    const session = this.#session, composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer"));
	    if (!session || modelValue(composer, "model") !== session.model || typeof composer?.save != "function") return null;
	    const button = document.querySelector(
	      "#reply-control .save-or-cancel button.create"
	    );
	    return button && !button.disabled ? button : null;
	  }
	  #submit(button) {
	    if (button.disabled || this.#submitPromise) return;
	    const session = this.#session, composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), save = composer?.save;
	    if (!session || !this.#isolation || modelValue(composer, "model") !== session.model || typeof save != "function") return;
	    const transaction = this.#isolation.run(
	      session.topicId,
	      session.action === "edit" ? "edited" : "created",
	      () => save.call(composer, !0, { jump: !1 })
	    );
	    this.#submitPromise = transaction, transaction.catch((error) => {
	      this.#onError(error);
	    }).finally(() => {
	      this.#submitPromise === transaction && (this.#submitPromise = null);
	    });
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("DiscourseComposerCoordinator 已销毁");
	  }
	}
	const COMPOSER_SAVE_EVENTS = Object.freeze([
	  Object.freeze({
	    eventName: "composer:created-post",
	    kind: "created"
	  }),
	  Object.freeze({
	    eventName: "composer:edited-post",
	    kind: "edited"
	  }),
	  Object.freeze({
	    eventName: "post:created",
	    kind: "created"
	  })
	]);
	function topicRouteTarget(value) {
	  let segments;
	  try {
	    segments = new URL(String(value ?? ""), "https://reader.invalid").pathname.split("/").filter(Boolean);
	  } catch {
	    return null;
	  }
	  const topicIndex = segments.indexOf("t");
	  if (topicIndex < 0) return null;
	  const tail = segments.slice(topicIndex + 1), topicOffset = /^\d+$/.test(tail[0] ?? "") ? 0 : 1, topicId = Number(tail[topicOffset]), postNumber = Number(tail[topicOffset + 1]);
	  return Number.isSafeInteger(topicId) && topicId > 0 && Number.isSafeInteger(postNumber) && postNumber > 0 ? Object.freeze({ topicId, postNumber }) : null;
	}
	class DiscourseComposerHostIsolation {
	  scope;
	  #host;
	  #events = new import_signal.Signal();
	  #onError;
	  #releaseActive = null;
	  #suspended = null;
	  #running = !1;
	  constructor(options) {
	    this.#host = options.host, this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.#releaseActive?.(), this.#releaseActive = null, this.#resumeTopicController(), this.#events.clear();
	    });
	  }
	  subscribe(listener, scope) {
	    const owned = import_lifecycle.LifecycleScope.ownedBy(scope ?? this.scope);
	    return new DiscourseComposerEventPort(this.#host).subscribe(listener, owned), this.#events.subscribe(listener, owned), () => owned.destroy();
	  }
	  async run(topicIdValue, kind, execute) {
	    if (this.scope.destroyed)
	      throw new Error("Discourse Composer 宿主隔离 owner 已销毁");
	    if (this.#running)
	      throw new Error("Discourse Composer save 事务正在进行");
	    const topicId = Number((0, import_identifiers.discourseTopicId)(topicIdValue)), appEvents = appEventsPort(
	      (0, import_value_record.valueRecord)(this.#host.lookup("service:composer"))?.appEvents ?? this.#host.lookup("service:app-events")
	    ), originalTrigger = appEvents.trigger;
	    if (typeof originalTrigger != "function")
	      throw new Error("Discourse app-events 缺少 trigger");
	    const expectedNames = new Set(
	      kind === "edited" ? ["composer:edited-post"] : ["composer:created-post", "post:created"]
	    ), suppressedNames = /* @__PURE__ */ new Set([
	      "composer:created-post",
	      "composer:edited-post",
	      "post:created",
	      "post:highlight"
	    ]);
	    let observed = !1;
	    const trigger = (eventName, payload) => {
	      const name = String(eventName ?? "");
	      return expectedNames.has(name) && (observed = !0, this.#emit(Object.freeze({
	        kind,
	        payload,
	        eventName: name
	      }))), suppressedNames.has(name) ? appEvents : originalTrigger.call(appEvents, eventName, payload);
	    }, releases = [], release = () => {
	      for (const cleanup of releases.splice(0).reverse())
	        try {
	          cleanup();
	        } catch (cause) {
	          this.#report(cause);
	        }
	      this.#releaseActive === release && (this.#releaseActive = null);
	    };
	    this.#running = !0;
	    try {
	      if (appEvents.trigger = trigger, appEvents.trigger !== trigger)
	        throw new Error("Discourse app-events trigger 无法建立有界隔离");
	      releases.push(() => {
	        appEvents.trigger === trigger && (appEvents.trigger = originalTrigger);
	      });
	      const routeModule = (0, import_value_record.valueRecord)(
	        this.#host.lookupModule("discourse/lib/url")
	      ), routeOwner = (0, import_value_record.valueRecord)(routeModule?.default) ?? routeModule, originalRouteTo = routeOwner?.routeTo;
	      if (routeOwner && typeof originalRouteTo == "function") {
	        const guardedRouteTo = (value, ...args) => {
	          if (topicRouteTarget(value)?.topicId !== topicId)
	            return originalRouteTo.call(routeOwner, value, ...args);
	        };
	        routeOwner.routeTo = guardedRouteTo, routeOwner.routeTo === guardedRouteTo && releases.push(() => {
	          routeOwner.routeTo === guardedRouteTo && (routeOwner.routeTo = originalRouteTo);
	        });
	      }
	      this.#suspendTopicController(topicId), this.#releaseActive = release;
	      const result = await execute();
	      return observed || this.#emit(Object.freeze({
	        kind,
	        payload: result,
	        eventName: kind === "edited" ? "composer:edited-post" : "composer:created-post"
	      })), result;
	    } finally {
	      release(), this.#running = !1;
	    }
	  }
	  runActive(kind, execute) {
	    const composer = (0, import_value_record.valueRecord)(this.#host.lookup("service:composer")), model = modelValue(composer, "model"), topic = modelValue(model, "topic") ?? modelValue(modelValue(model, "post"), "topic");
	    return this.run(
	      Number((0, import_identifiers.discourseTopicId)(modelValue(topic, "id"))),
	      kind,
	      execute
	    );
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #emit(event) {
	    for (const cause of this.#events.emit(event)) this.#report(cause);
	  }
	  #suspendTopicController(topicId) {
	    if (this.#suspended?.topicId === topicId) return;
	    this.#resumeTopicController();
	    const controller = (0, import_value_record.valueRecord)(this.#host.lookup("controller:topic")), hostTopicId = Number(modelValue(modelValue(controller, "model"), "id"));
	    !controller || hostTopicId !== topicId || typeof controller.unsubscribe != "function" || (controller.unsubscribe.call(controller), this.#suspended = Object.freeze({ controller, topicId }));
	  }
	  #resumeTopicController() {
	    const suspended = this.#suspended;
	    if (this.#suspended = null, !(!suspended || typeof suspended.controller.subscribe != "function" || Number(
	      modelValue(modelValue(suspended.controller, "model"), "id")
	    ) !== suspended.topicId))
	      try {
	        suspended.controller.subscribe.call(suspended.controller);
	      } catch (cause) {
	        this.#report(cause);
	      }
	  }
	  #report(cause) {
	    try {
	      this.#onError(cause);
	    } catch {
	    }
	  }
	}
	function appEventsPort(value) {
	  const target = (0, import_value_record.valueRecord)(value);
	  if (!target || typeof target.on != "function" || typeof target.off != "function")
	    throw new Error("Discourse app-events 缺少 on/off");
	  return target;
	}
	function modelJson(value) {
	  const target = (0, import_value_record.valueRecord)(value);
	  if (!target || typeof target.toJSON != "function") return value;
	  try {
	    return target.toJSON.call(target);
	  } catch {
	    return value;
	  }
	}
	function eventPostCandidate(value) {
	  const normalized = modelJson(value), nested = modelValue(normalized, "post");
	  return nested === void 0 ? normalized : modelJson(nested);
	}
	function topicIdForPost(value) {
	  const direct = Number(modelValue(value, "topic_id"));
	  if (Number.isSafeInteger(direct) && direct > 0) return direct;
	  const topic = modelValue(value, "topic"), nested = Number(modelValue(topic, "id"));
	  return Number.isSafeInteger(nested) && nested > 0 ? nested : null;
	}
	class DiscourseComposerEventPort {
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  subscribe(listener, scope) {
	    let appEvents;
	    try {
	      appEvents = appEventsPort(
	        (0, import_value_record.valueRecord)(this.#host.lookup("service:composer"))?.appEvents ?? this.#host.lookup("service:app-events")
	      );
	    } catch {
	      return () => {
	      };
	    }
	    const bindings = [];
	    try {
	      for (const descriptor of COMPOSER_SAVE_EVENTS) {
	        const bound = (payload) => {
	          listener(Object.freeze({
	            kind: descriptor.kind,
	            payload,
	            eventName: descriptor.eventName
	          }));
	        };
	        appEvents.on(descriptor.eventName, bound), bindings.push(Object.freeze({
	          eventName: descriptor.eventName,
	          listener: bound
	        }));
	      }
	    } catch (error) {
	      for (const binding of bindings.reverse())
	        try {
	          appEvents.off(binding.eventName, binding.listener);
	        } catch {
	        }
	      throw error;
	    }
	    let active = !0;
	    const cleanup = () => {
	      if (active) {
	        active = !1;
	        for (const binding of bindings.reverse())
	          try {
	            appEvents.off(binding.eventName, binding.listener);
	          } catch {
	          }
	      }
	    };
	    return scope?.add(cleanup), cleanup;
	  }
	}
	class DiscourseComposerTopicSyncController {
	  scope;
	  changes = new import_signal.Signal();
	  #topicId;
	  #events;
	  #session;
	  #now;
	  #schedule;
	  #cancel;
	  #onError;
	  #pending = /* @__PURE__ */ new Map();
	  #unsubscribe = null;
	  constructor(options) {
	    this.#topicId = (0, import_identifiers.discourseTopicId)(options.topicId), this.#events = options.events, this.#session = options.session, this.#now = options.now ?? Date.now, this.#schedule = options.schedule ?? ((callback) => setTimeout(callback, 0)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.stop(), this.changes.clear();
	    });
	  }
	  get active() {
	    return this.#unsubscribe !== null;
	  }
	  start() {
	    this.#assertActive(), !this.#unsubscribe && (this.#unsubscribe = this.#events.subscribe((event) => {
	      this.#handle(event);
	    }, this.scope));
	  }
	  stop() {
	    const unsubscribe = this.#unsubscribe;
	    this.#unsubscribe = null, unsubscribe?.();
	    for (const pending of this.#pending.values())
	      "handle" in pending && this.#cancel(pending.handle);
	    this.#pending.clear();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #handle(event) {
	    if (this.scope.destroyed) return;
	    const candidate = eventPostCandidate(event.payload), topicId = topicIdForPost(candidate);
	    if (topicId !== null && topicId !== this.#topicId) return;
	    let reference = null;
	    if (topicId === this.#topicId)
	      try {
	        reference = (0, import_identifiers.discoursePostReference)(
	          candidate
	        );
	      } catch {
	        reference = null;
	      }
	    if (reference && reference.postId !== null) {
	      this.#cancelPending(event.kind);
	      try {
	        event.kind === "created" ? this.#session.ingestCreatedPost(
	          candidate,
	          "action-response",
	          this.#now()
	        ) : this.#session.ingestPosts(
	          Object.freeze([candidate]),
	          "action-response",
	          this.#now()
	        ), this.#emit(Object.freeze({
	          kind: event.kind,
	          post: candidate,
	          postNumber: reference.postNumber,
	          source: "native-event"
	        }));
	      } catch (error) {
	        this.#onError(error);
	      }
	      return;
	    }
	    this.#scheduleFallback(event.kind);
	  }
	  #scheduleFallback(kind) {
	    if (this.#pending.has(kind)) return;
	    const pending = {};
	    this.#pending.set(kind, pending);
	    try {
	      pending.handle = this.#schedule(() => {
	        this.#pending.get(kind) === pending && (this.#pending.delete(kind), Promise.resolve().then(async () => {
	          await this.#session.refresh({ background: !1 }), kind === "created" && await this.#session.loadLastPost({ refresh: !0 });
	        }).then(() => {
	          this.scope.destroyed || this.#emit(Object.freeze({
	            kind,
	            post: null,
	            postNumber: null,
	            source: "canonical-refresh"
	          }));
	        }).catch(this.#onError));
	      });
	    } catch (error) {
	      this.#pending.get(kind) === pending && this.#pending.delete(kind), this.#onError(error);
	    }
	  }
	  #cancelPending(kind) {
	    const pending = this.#pending.get(kind);
	    pending && (this.#pending.delete(kind), "handle" in pending && this.#cancel(pending.handle));
	  }
	  #emit(commit) {
	    if (!this.scope.destroyed)
	      for (const error of this.changes.emit(commit)) this.#onError(error);
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("DiscourseComposerTopicSyncController 已销毁");
	  }
	}
}, "41f7e0c2861c4a517ecb56480e90811536d897e16b376788be46012bd6349ea5");

/* Source: lite/src/discourse/native-host-api.ts */
runtime.register("src/discourse/native-host-api.js", function(module, exports, require) {
	var native_host_api_exports = {};
	__export(native_host_api_exports, {
	  BrowserDiscourseBookmarkNativeState: () => BrowserDiscourseBookmarkNativeState,
	  BrowserDiscourseHostApiPort: () => BrowserDiscourseHostApiPort,
	  BrowserDiscourseNativeBookmarkForm: () => BrowserDiscourseNativeBookmarkForm,
	  BrowserDiscourseNotificationNativeState: () => BrowserDiscourseNotificationNativeState,
	  discourseAvatarTemplateUrl: () => discourseAvatarTemplateUrl,
	  discourseDeferredSubscription: () => discourseDeferredSubscription,
	  discourseNativeAppEventSubscription: () => discourseNativeAppEventSubscription,
	  discourseNativeBoostsAvailable: () => discourseNativeBoostsAvailable,
	  discourseNativeCurrentUserBindingAvailable: () => discourseNativeCurrentUserBindingAvailable,
	  discourseNativeCurrentUsername: () => discourseNativeCurrentUsername,
	  discourseNativeDefaultSiteTheme: () => discourseNativeDefaultSiteTheme,
	  discourseNativeEmojiMenu: () => discourseNativeEmojiMenu,
	  discourseNativeEmojiUrl: () => discourseNativeEmojiUrl,
	  discourseNativeExactTimeFormatter: () => discourseNativeExactTimeFormatter,
	  discourseNativeFlagCatalog: () => discourseNativeFlagCatalog,
	  discourseNativeFollowRoute: () => discourseNativeFollowRoute,
	  discourseNativeHostRouteRefresh: () => discourseNativeHostRouteRefresh,
	  discourseNativeIconRenderer: () => discourseNativeIconRenderer,
	  discourseNativeInitialCurrentUsername: () => discourseNativeInitialCurrentUsername,
	  discourseNativeJqueryModule: () => discourseNativeJqueryModule,
	  discourseNativeMenuCloser: () => discourseNativeMenuCloser,
	  discourseNativePostAdminMenu: () => discourseNativePostAdminMenu,
	  discourseNativePostEventModel: () => discourseNativePostEventModel,
	  discourseNativePostRuntimeBindings: () => discourseNativePostRuntimeBindings,
	  discourseNativeRelativeTimeFormatter: () => discourseNativeRelativeTimeFormatter,
	  discourseNativeSiteLogoUrl: () => discourseNativeSiteLogoUrl,
	  discourseNativeTheme: () => discourseNativeTheme,
	  discourseNativeTopicEditCatalog: () => discourseNativeTopicEditCatalog,
	  discourseNativeTopicLinks: () => discourseNativeTopicLinks,
	  discourseNativeTopicPresentation: () => discourseNativeTopicPresentation,
	  discourseNativeUnwantedTopicRuleCatalog: () => discourseNativeUnwantedTopicRuleCatalog,
	  discourseNativeUserActionBinding: () => discourseNativeUserActionBinding,
	  discourseNativeUserModel: () => discourseNativeUserModel
	});
	module.exports = __toCommonJS(native_host_api_exports);
	var import_value_record = require("../kernel/value-record.js"), import_native_message_bus = require("./native-message-bus.js");
	const DEFERRED_SUBSCRIPTION_RETRY_DELAYS_MS = Object.freeze([
	  0,
	  50,
	  100,
	  250,
	  500,
	  1e3,
	  2e3,
	  4e3,
	  7e3
	]);
	function discourseDeferredSubscription(attach) {
	  let active = !0, attachedCleanup = null, retryIndex = 0, retryTimer = null;
	  const tryAttach = () => {
	    if (!active || attachedCleanup) return;
	    const cleanup = attach();
	    if (cleanup) {
	      attachedCleanup = cleanup;
	      return;
	    }
	    if (retryIndex >= DEFERRED_SUBSCRIPTION_RETRY_DELAYS_MS.length) return;
	    const delay = DEFERRED_SUBSCRIPTION_RETRY_DELAYS_MS[retryIndex++];
	    if (delay === 0) {
	      queueMicrotask(tryAttach);
	      return;
	    }
	    retryTimer = setTimeout(() => {
	      retryTimer = null, tryAttach();
	    }, delay);
	  };
	  return tryAttach(), () => {
	    active && (active = !1, retryTimer !== null && clearTimeout(retryTimer), retryTimer = null, attachedCleanup?.(), attachedCleanup = null);
	  };
	}
	function discourseNativeAppEventSubscription(host, eventNameValue, listener, onError) {
	  const eventName = String(eventNameValue).trim();
	  return eventName ? discourseDeferredSubscription(() => {
	    const appEvents = (0, import_value_record.objectRecord)(host.lookup("service:app-events")), on = appEvents?.on, off = appEvents?.off;
	    if (typeof on != "function" || typeof off != "function") return null;
	    const owner = Object.freeze({});
	    try {
	      on.call(appEvents, eventName, owner, listener);
	    } catch (cause) {
	      return onError?.(cause), () => {
	      };
	    }
	    let active = !0;
	    return () => {
	      if (active) {
	        active = !1;
	        try {
	          off.call(appEvents, eventName, owner, listener);
	        } catch (cause) {
	          onError?.(cause);
	        }
	      }
	    };
	  }) : () => {
	  };
	}
	function discourseNativeUserModel(host) {
	  return host.lookupModule("discourse/models/user");
	}
	function discourseNativeFollowRoute(host, kind) {
	  return host.lookupModule(
	    `discourse/plugins/discourse-follow/discourse/routes/${kind}`
	  );
	}
	function discourseNativePostEventModel(host) {
	  return host.lookupModule(
	    "discourse/plugins/discourse-calendar/discourse/models/discourse-post-event-event"
	  );
	}
	function discourseNativeBoostsAvailable(host) {
	  const settings = (0, import_value_record.objectRecord)(host.lookup("service:site-settings"));
	  return settings && Object.hasOwn(settings, "discourse_boosts_enabled") ? settings.discourse_boosts_enabled === !0 : !!host.lookupModule(
	    "discourse/plugins/discourse-boosts/discourse/lib/create-boost"
	  );
	}
	function discourseNativeMenuCloser(host) {
	  const menu = (0, import_value_record.objectRecord)(host.lookup("service:menu")), close = menu?.close;
	  return typeof close != "function" ? null : (identifier) => {
	    const normalized = String(identifier).trim();
	    if (!normalized) throw new Error("Discourse menu identifier 不能为空");
	    return close.call(menu, normalized);
	  };
	}
	function discourseNativeHostRouteRefresh(host) {
	  const router = (0, import_value_record.objectRecord)(host.lookup("service:router")), refresh = router?.refresh;
	  if (typeof refresh != "function") return !1;
	  try {
	    const transition = refresh.call(router), rejection = (0, import_value_record.objectRecord)(transition)?.catch;
	    return typeof rejection == "function" && rejection.call(transition, () => {
	    }), !0;
	  } catch {
	    return !1;
	  }
	}
	function discourseNativeThemeMode(service) {
	  const colorMode = String(service?.colorMode ?? "").trim();
	  if (colorMode === "light" || colorMode === "dark") return colorMode;
	  if (colorMode === "auto") return "system";
	  if (service?.lightModeForced) return "light";
	  if (service?.darkModeForced) return "dark";
	  if (service?.selectorAvailable) return "system";
	  const session = (0, import_value_record.objectRecord)(service?.session);
	  return session ? session.defaultColorSchemeIsDark ? "dark" : "light" : null;
	}
	function discourseNativeTheme(host) {
	  return Object.freeze({
	    apply(mode) {
	      const service = (0, import_value_record.objectRecord)(
	        host.lookup("service:interface-color")
	      ), action = service?.[mode === "light" ? "forceLightMode" : mode === "dark" ? "forceDarkMode" : "useAutoMode"];
	      if (typeof action != "function") return !1;
	      try {
	        return action.call(service), !0;
	      } catch {
	        return !1;
	      }
	    },
	    subscribe(listener, scope) {
	      const cleanup = discourseDeferredSubscription(() => {
	        const service = (0, import_value_record.objectRecord)(
	          host.lookup("service:interface-color")
	        ), appEvents = (0, import_value_record.objectRecord)(service?.appEvents), on = appEvents?.on, off = appEvents?.off;
	        if (typeof on != "function" || typeof off != "function")
	          return null;
	        const context = Object.freeze({}), onChanged = () => {
	          const mode = discourseNativeThemeMode(service);
	          mode && listener(mode);
	        };
	        try {
	          on.call(
	            appEvents,
	            "interface-color:changed",
	            context,
	            onChanged
	          );
	        } catch {
	          return () => {
	          };
	        }
	        let active = !0;
	        return () => {
	          if (active) {
	            active = !1;
	            try {
	              off.call(
	                appEvents,
	                "interface-color:changed",
	                context,
	                onChanged
	              );
	            } catch {
	            }
	          }
	        };
	      });
	      return scope.add(cleanup), cleanup;
	    }
	  });
	}
	function discourseNativeDefaultSiteTheme(host) {
	  const site = host.lookup("service:site"), themeValues = nativeModelValue(site, "user_themes") ?? nativeModelValue(site, "userThemes"), defaultTheme = (Array.isArray(themeValues) ? themeValues : []).find(
	    (value) => nativeModelValue(value, "default") === !0
	  ), defaultThemeId = Number(
	    nativeModelValue(defaultTheme, "theme_id") ?? nativeModelValue(defaultTheme, "themeId")
	  );
	  if (!Number.isSafeInteger(defaultThemeId) || defaultThemeId === 0)
	    return "unavailable";
	  const module2 = (0, import_value_record.objectRecord)(
	    host.lookupModule("discourse/lib/theme-selector")
	  ), defaultExport = (0, import_value_record.objectRecord)(module2?.default), owner = typeof module2?.setLocalTheme == "function" ? module2 : defaultExport, currentThemeId = owner?.currentThemeId, setLocalTheme = owner?.setLocalTheme;
	  if (typeof currentThemeId != "function" || typeof setLocalTheme != "function")
	    return "unavailable";
	  try {
	    if (Number(currentThemeId.call(owner)) === defaultThemeId)
	      return "already-default";
	    const currentUser = discourseNativeCurrentUser(host), themeSequence = Number(
	      nativeModelValue(currentUser, "theme_key_seq") ?? nativeModelValue(currentUser, "themeKeySeq")
	    );
	    return !Number.isSafeInteger(themeSequence) || themeSequence < 0 ? "unavailable" : (setLocalTheme.call(owner, [defaultThemeId], themeSequence), "updated");
	  } catch {
	    return "failed";
	  }
	}
	function discourseNativeTopicEditCatalog(host) {
	  return Object.freeze({
	    categories() {
	      const site = host.lookup("service:site"), source = nativeModelValue(site, "categories");
	      if (!Array.isArray(source)) return Object.freeze([]);
	      const categories = source.map((value) => {
	        const id = Number(nativeModelValue(value, "id")), name = String(nativeModelValue(value, "name") ?? "").trim();
	        if (!Number.isSafeInteger(id) || id < 1 || !name) return null;
	        const parentId = Number(
	          nativeModelValue(value, "parent_category_id")
	        );
	        return Object.freeze({
	          id,
	          name,
	          slug: String(nativeModelValue(value, "slug") ?? "").trim(),
	          color: String(nativeModelValue(value, "color") ?? "").trim().replace(/^#/, ""),
	          parentCategoryId: Number.isSafeInteger(parentId) && parentId > 0 ? parentId : null
	        });
	      }).filter((value) => value !== null);
	      return Object.freeze(categories);
	    },
	    async searchTags(request) {
	      const tagUtils = (0, import_value_record.objectRecord)(host.lookup("service:tag-utils")), search = tagUtils?.searchTags, settings = host.lookup("service:site-settings");
	      if (!tagUtils || typeof search != "function" || !(0, import_value_record.objectRecord)(settings))
	        throw new Error("Discourse 标签搜索服务尚未就绪");
	      const tagUtilsOwner = tagUtils, selectedIds = request.selected.map((tag) => Number(tag.id)).filter((id) => Number.isSafeInteger(id) && id > 0), selectedNames = request.selected.filter((tag) => !(Number(tag.id) > 0)).map((tag) => tag.name), result = await search.call(
	        tagUtils,
	        "/tags/filter/search",
	        Object.freeze({
	          q: String(request.query ?? "").trim(),
	          limit: Math.max(
	            1,
	            Number(nativeModelValue(
	              settings,
	              "max_tag_search_results"
	            )) || 20
	          ),
	          categoryId: request.categoryId > 0 ? request.categoryId : void 0,
	          filterForInput: !0,
	          ...selectedIds.length ? { selected_tag_ids: selectedIds.slice(0, 100) } : {},
	          ...selectedNames.length ? { selected_tags: selectedNames.slice(0, 100) } : {}
	        }),
	        (json) => {
	          const payload = (0, import_value_record.objectRecord)(json), incoming = Array.isArray(payload?.results) ? payload.results : [], sort = tagUtilsOwner.sortSearchResults;
	          return typeof sort == "function" ? sort.call(tagUtilsOwner, incoming) : incoming;
	        }
	      ), values = Array.isArray(result) ? result : [], byName = /* @__PURE__ */ new Map();
	      for (const value of values) {
	        const name = String(
	          nativeModelValue(value, "name") ?? nativeModelValue(value, "text") ?? ""
	        ).trim();
	        if (!name) continue;
	        const id = Number(nativeModelValue(value, "id"));
	        byName.set(name.toLocaleLowerCase(), Object.freeze({
	          id: Number.isSafeInteger(id) && id > 0 ? id : null,
	          name
	        }));
	      }
	      return Object.freeze([...byName.values()]);
	    }
	  });
	}
	function discourseNativeUnwantedTopicRuleCatalog(host) {
	  const topicCatalog = discourseNativeTopicEditCatalog(host);
	  return Object.freeze({
	    categories: () => topicCatalog.categories(),
	    searchTags: (request) => topicCatalog.searchTags(request),
	    async searchUsers(queryValue) {
	      const loaded = host.lookupModule("discourse/lib/user-search"), module2 = (0, import_value_record.valueRecord)(loaded), search = typeof loaded == "function" ? loaded : module2?.default;
	      if (typeof search != "function")
	        throw new Error("Discourse 用户搜索服务尚未就绪");
	      const query = String(queryValue ?? "").replace(/^@+/, "").trim();
	      if (!query) return Object.freeze([]);
	      const result = await search.call(module2, Object.freeze({
	        term: query,
	        includeGroups: !1,
	        includeStagedUsers: !1,
	        limit: 20
	      })), users = (0, import_value_record.valueRecord)(result)?.users, values = Array.isArray(users) ? users : [], byUsername = /* @__PURE__ */ new Map();
	      for (const value of values) {
	        const username = String(
	          nativeModelValue(value, "username") ?? ""
	        ).trim();
	        if (!username) continue;
	        const key = username.toLocaleLowerCase("zh-CN");
	        if (byUsername.has(key)) continue;
	        const id = Number(nativeModelValue(value, "id"));
	        byUsername.set(key, Object.freeze({
	          id: Number.isSafeInteger(id) && id > 0 ? id : null,
	          username,
	          name: String(nativeModelValue(value, "name") ?? "").trim(),
	          avatarTemplate: String(
	            nativeModelValue(value, "avatar_template") ?? nativeModelValue(value, "avatarTemplate") ?? ""
	          ).trim()
	        }));
	      }
	      return Object.freeze([...byUsername.values()]);
	    }
	  });
	}
	const DISCOURSE_ICON_ALIASES = Object.freeze({
	  "alert-triangle": "triangle-exclamation",
	  boost: "rocket",
	  "check-square": "square-check",
	  "circle-x": "circle-xmark",
	  "external-link": "arrow-up-right-from-square",
	  "eye-off": "eye-slash",
	  "header-bell": "far-bell",
	  "header-bookmark": "far-bookmark",
	  "header-settings": "sliders",
	  history: "clock-rotate-left",
	  languages: "language",
	  "list-checks": "list-check",
	  mail: "envelope",
	  "maximize-2": "up-right-and-down-left-from-center",
	  "minimize-2": "down-left-and-up-right-to-center",
	  "message-square": "message",
	  "panel-left": "table-columns",
	  "panel-right": "table-columns",
	  "floating-window": "window-maximize",
	  pin: "thumbtack",
	  "rotate-ccw": "arrow-rotate-left",
	  search: "magnifying-glass",
	  settings: "gear",
	  share: "share-nodes",
	  smile: "face-smile",
	  trash: "trash-can",
	  x: "xmark"
	}), SVG_NAMESPACE = "http://www.w3.org/2000/svg", SAFE_SVG_FRAGMENT_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
	function sanitizeInlinedIcon(root) {
	  const queryAll = root.querySelectorAll;
	  if (typeof queryAll != "function") return;
	  const elements = [root, ...Array.from(queryAll.call(root, "*"))];
	  for (const element of elements) {
	    const localName = String(element.localName ?? element.tagName).toLowerCase();
	    if (localName === "script" || localName === "foreignobject") {
	      element.remove();
	      continue;
	    }
	    for (const attribute of Array.from(element.attributes ?? [])) {
	      const name = attribute.name.toLowerCase();
	      if (name.startsWith("on")) {
	        element.removeAttribute(attribute.name);
	        continue;
	      }
	      (name === "href" || name === "xlink:href") && attribute.value && !attribute.value.startsWith("#") && element.removeAttribute(attribute.name);
	    }
	  }
	}
	function inlineDiscourseIconUse(value, document) {
	  const svg = value;
	  if (svg?.nodeType !== 1 || typeof svg.querySelector != "function" || typeof svg.replaceChildren != "function")
	    return;
	  const use = svg.querySelector("use"), href = use?.getAttribute("href") ?? use?.getAttribute("xlink:href") ?? "";
	  if (!href.startsWith("#")) return;
	  const fragmentId = href.slice(1);
	  if (!SAFE_SVG_FRAGMENT_ID.test(fragmentId)) return;
	  const symbol = document.getElementById(fragmentId);
	  if (!symbol || String(symbol.localName ?? symbol.tagName).toLowerCase() !== "symbol")
	    return;
	  const group = document.createElementNS(SVG_NAMESPACE, "g");
	  for (const child of Array.from(symbol.childNodes))
	    group.append(child.cloneNode(!0));
	  if (sanitizeInlinedIcon(group), !group.childNodes.length) return;
	  const viewBox = symbol.getAttribute("viewBox");
	  viewBox && !svg.getAttribute("viewBox") && svg.setAttribute("viewBox", viewBox), svg.replaceChildren(group);
	}
	function discourseNativeIconRenderer(host) {
	  return (nameValue, document) => {
	    const name = String(nameValue).trim();
	    if (!name) return null;
	    const candidates = Object.freeze([
	      DISCOURSE_ICON_ALIASES[name],
	      name
	    ].filter((candidate2) => !!candidate2)), module2 = (0, import_value_record.objectRecord)(
	      host.lookupModule("discourse/lib/icon-library")
	    ), defaultExport = (0, import_value_record.objectRecord)(module2?.default), owner = typeof module2?.iconElement == "function" ? module2 : defaultExport, iconElement = owner?.iconElement;
	    if (typeof iconElement == "function")
	      for (const candidate2 of candidates)
	        try {
	          const rendered = iconElement.call(owner, candidate2), node = rendered;
	          if (node?.nodeType !== 1 || !node.classList) continue;
	          return node.classList.add("ldp-icon", `ldp-icon-${name}`), node.dataset && (node.dataset.icon = name), node.setAttribute?.("aria-hidden", "true"), inlineDiscourseIconUse(rendered, document), rendered;
	        } catch {
	        }
	    const candidate = candidates[0];
	    if (!candidate) return null;
	    const svg = document.createElementNS(
	      SVG_NAMESPACE,
	      "svg"
	    );
	    svg.classList.add(
	      "svg-icon",
	      "icon",
	      "d-icon",
	      `d-icon-${candidate}`,
	      "ldp-icon",
	      `ldp-icon-${name}`
	    ), svg.dataset.icon = name, svg.setAttribute("aria-hidden", "true");
	    const use = document.createElementNS(
	      SVG_NAMESPACE,
	      "use"
	    );
	    return use.setAttribute("href", `#${candidate}`), svg.append(use), svg;
	  };
	}
	function discourseAvatarTemplateUrl(template, size, baseUrl) {
	  const source = String(template).replace(/\{size\}/g, String(size)).trim();
	  if (!source) return null;
	  try {
	    return new URL(source, baseUrl).href;
	  } catch {
	    return null;
	  }
	}
	function discourseNativeCurrentUser(host) {
	  const serviceUser = host.lookup("service:current-user");
	  if (String(nativeModelValue(serviceUser, "username") ?? "").trim())
	    return serviceUser;
	  const userModule = (0, import_value_record.valueRecord)(host.lookupModule("discourse/models/user"));
	  for (const owner of [(0, import_value_record.valueRecord)(userModule?.default), userModule]) {
	    const current = owner?.current;
	    if (typeof current == "function")
	      try {
	        const user = current.call(owner);
	        if (user) return user;
	      } catch {
	      }
	  }
	  return serviceUser ?? null;
	}
	function discourseNativeCurrentUserBindingAvailable(host) {
	  const currentUser = host.lookup("service:current-user");
	  if (currentUser != null) return !0;
	  const userModule = (0, import_value_record.valueRecord)(host.lookupModule("discourse/models/user"));
	  return [(0, import_value_record.valueRecord)(userModule?.default), userModule].some((owner) => typeof owner?.current == "function");
	}
	function discourseNativeCurrentUsername(host) {
	  return String(
	    nativeModelValue(
	      discourseNativeCurrentUser(host),
	      "username"
	    ) ?? ""
	  ).trim();
	}
	function discourseNativeInitialCurrentUsername(host) {
	  const current = discourseNativeCurrentUsername(host);
	  if (current) return current;
	  const module2 = (0, import_value_record.valueRecord)(
	    host.lookupModule("discourse/lib/preload-store")
	  );
	  for (const owner of [(0, import_value_record.valueRecord)(module2?.default), module2]) {
	    const get = owner?.get;
	    if (typeof get == "function")
	      try {
	        const preloaded = get.call(owner, "currentUser"), username = String(
	          nativeModelValue(preloaded, "username") ?? ""
	        ).trim();
	        if (username) return username;
	      } catch {
	      }
	  }
	  return "";
	}
	function discourseNativeSiteLogoUrl(host, baseUrl, fallbackCandidates = []) {
	  const settings = (0, import_value_record.objectRecord)(host.lookup("service:site-settings")), candidates = [
	    settings?.large_icon,
	    settings?.largeIcon,
	    settings?.apple_touch_icon,
	    settings?.appleTouchIcon,
	    settings?.favicon,
	    ...fallbackCandidates
	  ];
	  for (const candidate of candidates) {
	    const value = String(candidate ?? "").trim();
	    if (value)
	      try {
	        const url = new URL(value, baseUrl);
	        if (url.protocol === "https:" || url.protocol === "http:")
	          return url.href;
	      } catch {
	      }
	  }
	  return new URL("/favicon.ico", baseUrl).href;
	}
	function discourseNativeUserActionBinding(host, usernameValue) {
	  const username = String(usernameValue).trim().replace(/^@+/, "");
	  if (!username) throw new Error("User action username 不能为空");
	  const store = (0, import_value_record.objectRecord)(host.lookup("service:store")), createRecord = store?.createRecord;
	  if (typeof createRecord != "function")
	    throw new Error("Discourse 原生 store.createRecord 尚未就绪");
	  const user = createRecord.call(
	    store,
	    "user",
	    /* Ember Data 会在 createRecord 期间向属性袋补入 store 等内部字段。 */
	    { username }
	  ), actingUser = host.lookup("service:current-user");
	  if (!(0, import_value_record.objectRecord)(user) || !(0, import_value_record.objectRecord)(actingUser))
	    throw new Error("Discourse 原生用户动作 binding 尚未就绪");
	  return Object.freeze({
	    user,
	    actingUser
	  });
	}
	function discourseNativeJqueryModule(host) {
	  return host.lookupModule("jquery");
	}
	function discourseNativeFlagCatalog(host) {
	  const list = (value) => {
	    if (Array.isArray(value)) return value;
	    if (typeof (value !== null && (typeof value == "object" || typeof value == "function") ? value[Symbol.iterator] : void 0) != "function") return Object.freeze([]);
	    try {
	      return Object.freeze(Array.from(value));
	    } catch {
	      return Object.freeze([]);
	    }
	  };
	  return Object.freeze({
	    flagTypes() {
	      const site = host.lookup("service:site");
	      return Object.freeze(list(nativeModelValue(site, "flagTypes")).map((value) => {
	        const id = Number(nativeModelValue(value, "id")), nameKey = String(
	          nativeModelValue(value, "name_key") ?? ""
	        ).trim();
	        if (!Number.isSafeInteger(id) || id <= 0 || !nameKey)
	          return null;
	        const appliesToValue = nativeModelValue(value, "applies_to"), appliesTo = Array.isArray(appliesToValue) ? appliesToValue.map(String).map((entry) => entry.trim()).filter(Boolean) : [];
	        return Object.freeze({
	          id,
	          nameKey,
	          label: String(
	            nativeModelValue(value, "name") ?? nameKey
	          ).trim() || nameKey,
	          description: String(
	            nativeModelValue(value, "description") ?? nativeModelValue(value, "short_description") ?? ""
	          ),
	          requireMessage: nativeModelValue(value, "require_message") === !0,
	          enabled: nativeModelValue(value, "enabled") !== !1,
	          appliesTo: Object.freeze(appliesTo)
	        });
	      }).filter(
	        (value) => value !== null
	      ));
	    },
	    messageMaxLength() {
	      const postActionTypeModule = (0, import_value_record.objectRecord)(
	        host.lookupModule("discourse/models/post-action-type")
	      ), postActionTypeDefault = (0, import_value_record.objectRecord)(
	        postActionTypeModule?.default
	      ), maxMessageLength = Number(
	        postActionTypeModule?.MAX_MESSAGE_LENGTH ?? postActionTypeDefault?.MAX_MESSAGE_LENGTH
	      );
	      return Number.isSafeInteger(maxMessageLength) && maxMessageLength > 0 ? maxMessageLength : 500;
	    }
	  });
	}
	function discourseNativeEmojiMenu(host) {
	  return Object.freeze({
	    async show(anchor, request) {
	      const menu = (0, import_value_record.objectRecord)(host.lookup("service:menu")), component = (0, import_value_record.objectRecord)(host.lookupModule(
	        "discourse/components/emoji-picker/detached"
	      ))?.default, show = menu?.show;
	      if (typeof show != "function" || !component)
	        throw new Error("Discourse 原生表情组件尚未就绪");
	      const identifier = String(request.identifier).trim(), context = String(request.context).trim();
	      if (!identifier || !context)
	        throw new Error("emoji menu identifier/context 不能为空");
	      await Promise.resolve(show.call(menu, anchor, {
	        identifier,
	        groupIdentifier: identifier,
	        component,
	        modalForMobile: !1,
	        strategy: "fixed",
	        fallbackPlacements: Object.freeze([
	          "bottom-start",
	          "top-start",
	          "bottom-end",
	          "top-end"
	        ]),
	        ...request.computePosition ? { computePosition: request.computePosition } : {},
	        data: Object.freeze({
	          term: "",
	          context,
	          didSelectEmoji: request.didSelectEmoji
	        })
	      }));
	    },
	    close(identifier) {
	      const menu = (0, import_value_record.objectRecord)(host.lookup("service:menu")), close = menu?.close, normalized = String(identifier).trim();
	      if (!(typeof close != "function" || !normalized))
	        try {
	          const result = close.call(menu, normalized);
	          result && typeof result.then == "function" && Promise.resolve(result).catch(() => {
	          });
	        } catch {
	        }
	    }
	  });
	}
	function discourseNativePostAdminMenu(host, options = {}) {
	  return Object.freeze({
	    async show(anchor, post, scheduleRerender) {
	      const menu = (0, import_value_record.objectRecord)(host.lookup("service:menu")), component = (0, import_value_record.objectRecord)(
	        host.lookupModule("discourse/components/admin-post-menu")
	      )?.default, topicController = (0, import_value_record.objectRecord)(host.lookup("controller:topic")), show = menu?.show, send = topicController?.send;
	      if (typeof show != "function" || !component || typeof send != "function")
	        throw new Error("Discourse 原生楼层管理菜单尚未就绪");
	      const topicAction = (name) => () => send.call(topicController, name, post);
	      await Promise.resolve(show.call(menu, anchor, {
	        identifier: "admin-post-menu",
	        component,
	        modalForMobile: !0,
	        autofocus: !0,
	        ...options.computePosition ? {
	          strategy: "fixed",
	          fallbackPlacements: Object.freeze([
	            "right-start",
	            "left-start",
	            "right-end",
	            "left-end"
	          ]),
	          computePosition: (content) => options.computePosition?.(anchor, content)
	        } : {},
	        data: Object.freeze({
	          post,
	          changeNotice: topicAction("changeNotice"),
	          changePostOwner: topicAction("changePostOwner"),
	          grantBadge: topicAction("grantBadge"),
	          lockPost: topicAction("lockPost"),
	          permanentlyDeletePost: topicAction("permanentlyDeletePost"),
	          rebakePost: topicAction("rebakePost"),
	          showPagePublish: topicAction("showPagePublish"),
	          togglePostType: topicAction("togglePostType"),
	          toggleWiki: topicAction("toggleWiki"),
	          unhidePost: topicAction("unhidePost"),
	          unlockPost: topicAction("unlockPost"),
	          scheduleRerender
	        })
	      }));
	    }
	  });
	}
	class BrowserDiscourseNativeBookmarkForm {
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  build(subjectType, subjectIdValue) {
	    if (subjectType !== "Post" && subjectType !== "Topic")
	      throw new Error(`未知收藏目标:${String(subjectType)}`);
	    const subjectId = Number(subjectIdValue);
	    if (!Number.isSafeInteger(subjectId) || subjectId < 1)
	      throw new RangeError("bookmark subjectId 必须是正安全整数");
	    const api = (0, import_value_record.objectRecord)(this.#host.lookup("service:bookmark-api")), buildNewBookmark = api?.buildNewBookmark;
	    if (!api || typeof buildNewBookmark != "function")
	      throw new Error(
	        "Discourse 原生收藏依赖未就绪:service:bookmark-api"
	      );
	    const module2 = (0, import_value_record.objectRecord)(
	      this.#host.lookupModule("discourse/lib/bookmark-form-data")
	    ), defaultExport = (0, import_value_record.objectRecord)(module2?.default), Constructor = module2?.BookmarkFormData ?? defaultExport?.BookmarkFormData ?? module2?.default;
	    if (typeof Constructor != "function")
	      throw new Error("Discourse BookmarkFormData 构造器未就绪");
	    const bookmark = (0, import_value_record.objectRecord)(
	      buildNewBookmark.call(api, subjectType, subjectId)
	    );
	    if (!bookmark)
	      throw new Error("bookmark-api.buildNewBookmark 未返回原生模型");
	    return new Constructor(bookmark);
	  }
	}
	function discourseNativePostRuntimeBindings(host) {
	  return Object.freeze({
	    topicModel: host.lookupModule("discourse/models/topic"),
	    topicDetailsModel: host.lookupModule("discourse/models/topic-details"),
	    postModel: host.lookupModule("discourse/models/post"),
	    appEvents: host.lookup("service:app-events"),
	    currentUser: host.lookup("service:current-user"),
	    siteSettings: host.lookup("service:site-settings")
	  });
	}
	function discourseNativeEmojiUrl(host, idValue) {
	  const id = String(idValue).trim().replace(/^:+|:+$/g, "");
	  if (!id) return "";
	  const module2 = (0, import_value_record.objectRecord)(host.lookupModule("discourse/lib/text")), defaultExport = (0, import_value_record.objectRecord)(module2?.default), owner = typeof module2?.emojiUrlFor == "function" ? module2 : defaultExport, emojiUrlFor = owner?.emojiUrlFor;
	  if (typeof emojiUrlFor != "function") return "";
	  try {
	    return String(emojiUrlFor.call(owner, id) ?? "").trim();
	  } catch {
	    return "";
	  }
	}
	function discourseNativeRelativeTimeFormatter(host) {
	  let owner = null;
	  return (timestamp) => {
	    const date = new Date(timestamp);
	    if (!Number.isFinite(date.getTime())) return "";
	    if (!owner || typeof owner.relativeAge != "function") {
	      const module2 = (0, import_value_record.objectRecord)(
	        host.lookupModule("discourse/lib/formatter")
	      ), defaultExport = (0, import_value_record.objectRecord)(module2?.default);
	      owner = typeof module2?.relativeAge == "function" ? module2 : defaultExport;
	    }
	    const relativeAge = owner?.relativeAge;
	    if (typeof relativeAge != "function") return "";
	    try {
	      return String(relativeAge.call(owner, date, {
	        format: "medium-with-ago",
	        wrapInSpan: !1
	      }) ?? "");
	    } catch {
	      return "";
	    }
	  };
	}
	function discourseNativeExactTimeFormatter(host) {
	  let owner = null;
	  return (timestamp) => {
	    const date = new Date(timestamp);
	    if (!Number.isFinite(date.getTime())) return "";
	    if (!owner || typeof owner.longDate != "function") {
	      const module2 = (0, import_value_record.objectRecord)(
	        host.lookupModule("discourse/lib/formatter")
	      ), defaultExport = (0, import_value_record.objectRecord)(module2?.default);
	      owner = typeof module2?.longDate == "function" ? module2 : defaultExport;
	    }
	    const longDate = owner?.longDate;
	    if (typeof longDate != "function") return "";
	    try {
	      return String(longDate.call(owner, date) ?? "");
	    } catch {
	      return "";
	    }
	  };
	}
	function discourseNativeTopicPresentation(host) {
	  const urlOwner = () => {
	    const module2 = (0, import_value_record.objectRecord)(host.lookupModule("discourse/lib/url"));
	    return typeof module2?.getCategoryAndTagUrl == "function" ? module2 : (0, import_value_record.objectRecord)(module2?.default);
	  }, avatarOwner = () => {
	    const module2 = (0, import_value_record.objectRecord)(
	      host.lookupModule("discourse/lib/avatar-utils")
	    );
	    return typeof module2?.avatarUrl == "function" ? module2 : (0, import_value_record.objectRecord)(module2?.default);
	  }, categoryModel = (categoryId) => {
	    const categories = nativeModelValue(
	      host.lookup("service:site"),
	      "categories"
	    );
	    return Array.isArray(categories) ? categories.find((candidate) => Number(nativeModelValue(candidate, "id")) === categoryId) : void 0;
	  }, categoryAndTagUrl = (categoryId, tag) => {
	    const owner = urlOwner(), method = owner?.getCategoryAndTagUrl, category = categoryId > 0 ? categoryModel(categoryId) : null;
	    if (categoryId > 0 && !category) return "";
	    if (typeof method == "function")
	      try {
	        const resolved = String(method.call(
	          owner,
	          category,
	          !0,
	          tag
	        ) ?? "");
	        if (resolved) return resolved;
	      } catch {
	      }
	    const normalizedTag = String(tag ?? "").trim();
	    if (normalizedTag) return `/tag/${encodeURIComponent(normalizedTag)}`;
	    const slug = String(nativeModelValue(category, "slug") ?? "").trim();
	    return categoryId > 0 && slug ? `/c/${encodeURIComponent(slug)}/${categoryId}` : "";
	  };
	  return Object.freeze({
	    avatarSource(template, size) {
	      const normalized = String(template ?? "").trim();
	      if (!normalized) return "";
	      const owner = avatarOwner(), method = owner?.avatarUrl;
	      if (typeof method != "function")
	        return normalized.replace(/\{size\}/g, String(size));
	      try {
	        return (String(
	          method.call(owner, normalized, size) ?? ""
	        ).trim() || normalized).replace(/\{size\}/g, String(size));
	      } catch {
	        return normalized.replace(/\{size\}/g, String(size));
	      }
	    },
	    categoryName(categoryId) {
	      return !Number.isSafeInteger(categoryId) || categoryId < 1 ? "" : String(
	        nativeModelValue(categoryModel(categoryId), "name") ?? ""
	      ).trim();
	    },
	    categoryIcon(categoryId) {
	      return !Number.isSafeInteger(categoryId) || categoryId < 1 ? "" : String(
	        nativeModelValue(categoryModel(categoryId), "icon") ?? ""
	      ).trim();
	    },
	    categoryHref(categoryId, tag) {
	      const normalizedId = Number.isSafeInteger(categoryId) && categoryId > 0 ? categoryId : 0, normalizedTag = String(tag ?? "").trim();
	      return categoryAndTagUrl(
	        normalizedId,
	        normalizedTag || void 0
	      );
	    },
	    tagHref(tag) {
	      const normalized = String(tag ?? "").trim();
	      return normalized ? categoryAndTagUrl(0, normalized) : "";
	    },
	    userHref(username) {
	      const normalized = String(username ?? "").trim();
	      if (!normalized) return "";
	      const owner = urlOwner(), userPath = owner?.userPath;
	      if (typeof userPath == "function")
	        try {
	          return String(userPath.call(owner, normalized) ?? "");
	        } catch {
	          return "";
	        }
	      return `/u/${encodeURIComponent(normalized)}`;
	    }
	  });
	}
	function discourseNativeTopicLinks(host, baseUrl) {
	  let normalizedBase = "";
	  try {
	    normalizedBase = new URL(baseUrl).href;
	  } catch {
	    normalizedBase = "";
	  }
	  return Object.freeze({
	    topicHref(topicIdValue, postNumberValue = 0) {
	      const topicId = Number(topicIdValue), postNumber = Number(postNumberValue);
	      if (!Number.isSafeInteger(topicId) || topicId < 1 || !Number.isSafeInteger(postNumber) || postNumber < 0 || !normalizedBase)
	        return "";
	      const path = `/t/${topicId}${postNumber ? `/${postNumber}` : ""}`;
	      try {
	        const module2 = (0, import_value_record.objectRecord)(
	          host.lookupModule("discourse/lib/get-url")
	        ), defaultExport = module2?.default, getUrl = typeof defaultExport == "function" ? defaultExport : typeof module2?.getURL == "function" ? module2.getURL : null, nativeValue = getUrl ? String(getUrl.call(module2, path) ?? "").trim() : "", candidate = new URL(nativeValue || path, normalizedBase), segments = candidate.pathname.split("/").filter(Boolean), topicSegmentIndex = segments.indexOf("t"), candidateTopicId = Number(segments[topicSegmentIndex + 1]);
	        return candidate.origin === new URL(normalizedBase).origin && topicSegmentIndex >= 0 && candidateTopicId === topicId ? candidate.href : new URL(path, normalizedBase).href;
	      } catch {
	        return new URL(path, normalizedBase).href;
	      }
	    }
	  });
	}
	function nativeModelValue(value, key) {
	  const record = (0, import_value_record.objectRecord)(value), getter = record?.get;
	  if (typeof getter == "function")
	    try {
	      return getter.call(value, key);
	    } catch {
	      return;
	    }
	  return record?.[key];
	}
	function setNativeModelValues(value, values) {
	  const record = (0, import_value_record.objectRecord)(value), setProperties = record?.setProperties;
	  if (typeof setProperties == "function") {
	    setProperties.call(value, values);
	    return;
	  }
	  const set = record?.set;
	  if (typeof set == "function")
	    for (const [key, entry] of Object.entries(values))
	      set.call(value, key, entry);
	}
	function nativeCount(value) {
	  if (value == null || value === "") return null;
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
	}
	function currentUserUnreadCount(currentUser) {
	  const all = nativeCount(nativeModelValue(
	    currentUser,
	    "all_unread_notifications_count"
	  ));
	  if (all !== null) return all;
	  const normal = nativeCount(nativeModelValue(
	    currentUser,
	    "unread_notifications"
	  )), high = nativeCount(nativeModelValue(
	    currentUser,
	    "unread_high_priority_notifications"
	  ));
	  return normal !== null || high !== null ? (normal ?? 0) + (high ?? 0) : nativeCount(nativeModelValue(
	    currentUser,
	    "unread_notification_count"
	  )) ?? 0;
	}
	function nativeNotificationRecord(value) {
	  return (0, import_value_record.objectRecord)(value) ?? Object.freeze({});
	}
	function nativeNotificationData(value) {
	  const raw = nativeNotificationRecord(value).data;
	  if (raw !== null && typeof raw == "object") return raw;
	  if (typeof raw != "string" || !raw.trim()) return Object.freeze({});
	  try {
	    return nativeNotificationRecord(JSON.parse(raw));
	  } catch {
	    return Object.freeze({});
	  }
	}
	function nativePresentationText(value) {
	  const toHTML = (0, import_value_record.objectRecord)(value)?.toHTML;
	  let source = value;
	  if (typeof toHTML == "function")
	    try {
	      source = toHTML.call(value);
	    } catch {
	      return "";
	    }
	  return String(source ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
	}
	function fallbackNotificationPresentation(notification, typeName) {
	  const source = nativeNotificationRecord(notification), data = nativeNotificationData(source);
	  return Object.freeze({
	    actor: data.display_username ?? data.username ?? source.username,
	    typeName,
	    typeLabel: typeName || "通知",
	    summary: data.topic_title ?? typeName ?? "通知",
	    href: data.post_url ?? data.topic_url ?? data.url ?? "",
	    topicId: source.topic_id ?? data.topic_id,
	    postNumber: source.post_number ?? data.post_number
	  });
	}
	class BrowserDiscourseNotificationNativeState {
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  username() {
	    return String(nativeModelValue(
	      discourseNativeCurrentUser(this.#host),
	      "username"
	    ) ?? "").trim().replace(/^@/, "");
	  }
	  unreadCount() {
	    return currentUserUnreadCount(
	      this.#host.lookup("service:current-user")
	    );
	  }
	  markAllRead() {
	    const currentUser = this.#host.lookup("service:current-user");
	    if (!currentUser) return;
	    const values = {
	      all_unread_notifications_count: 0,
	      unread_notifications: 0,
	      unread_high_priority_notifications: 0,
	      grouped_unread_notifications: Object.freeze({})
	    };
	    nativeModelValue(currentUser, "unread_notification_count") !== void 0 && (values.unread_notification_count = 0), setNativeModelValues(currentUser, Object.freeze(values));
	  }
	  markRead(input) {
	    const currentUser = this.#host.lookup("service:current-user");
	    if (!currentUser) return;
	    const normal = nativeCount(nativeModelValue(
	      currentUser,
	      "unread_notifications"
	    )) ?? 0, high = nativeCount(nativeModelValue(
	      currentUser,
	      "unread_high_priority_notifications"
	    )) ?? 0, nextNormal = input.highPriority ? normal : Math.max(0, normal - 1), nextHigh = input.highPriority ? Math.max(0, high - 1) : high, grouped = { ...(0, import_value_record.objectRecord)(nativeModelValue(
	      currentUser,
	      "grouped_unread_notifications"
	    )) ?? {} };
	    if (input.notificationTypeId !== null) {
	      const key = String(input.notificationTypeId), count = nativeCount(grouped[key]) ?? 0;
	      count > 0 && (grouped[key] = count - 1);
	    }
	    const values = {
	      grouped_unread_notifications: Object.freeze(grouped),
	      unread_notifications: nextNormal,
	      unread_high_priority_notifications: nextHigh,
	      all_unread_notifications_count: nextNormal + nextHigh
	    }, legacy = nativeCount(nativeModelValue(
	      currentUser,
	      "unread_notification_count"
	    ));
	    legacy !== null && (values.unread_notification_count = Math.max(0, legacy - 1)), setNativeModelValues(currentUser, Object.freeze(values));
	  }
	  async present(notifications) {
	    const site = this.#host.lookup("service:site"), lookup = (0, import_value_record.objectRecord)(nativeModelValue(site, "notificationLookup")), typeName = (notification) => {
	      const source = nativeNotificationRecord(notification);
	      return String(lookup?.[String(Number(source.notification_type))] ?? "");
	    }, notificationModule = (0, import_value_record.objectRecord)(
	      this.#host.lookupModule("discourse/models/notification")
	    ), Notification = (0, import_value_record.objectRecord)(notificationModule?.default), initialize = Notification?.initializeNotifications, managerModule = (0, import_value_record.objectRecord)(
	      this.#host.lookupModule("discourse/lib/notification-types-manager")
	    ), manager = typeof managerModule?.getRenderDirector == "function" ? managerModule : (0, import_value_record.objectRecord)(managerModule?.default), getRenderDirector = manager?.getRenderDirector, currentUser = this.#host.lookup("service:current-user"), siteSettings = this.#host.lookup("service:site-settings");
	    if (typeof initialize != "function" || typeof getRenderDirector != "function" || !currentUser || !siteSettings || !site)
	      return Object.freeze(notifications.map((notification) => fallbackNotificationPresentation(
	        notification,
	        typeName(notification)
	      )));
	    let models;
	    try {
	      const source = notifications.map((notification) => Object.freeze({
	        ...nativeNotificationRecord(notification),
	        data: nativeNotificationData(notification)
	      })), initialized = await initialize.call(Notification, source);
	      models = Array.isArray(initialized) ? initialized : source;
	    } catch {
	      return Object.freeze(notifications.map((notification) => fallbackNotificationPresentation(
	        notification,
	        typeName(notification)
	      )));
	    }
	    return Object.freeze(notifications.map((notification, index) => {
	      const model = models[index] ?? notification, modelData = nativeNotificationData(model), resolvedType = typeName(model) || typeName(notification);
	      try {
	        const director = (0, import_value_record.objectRecord)(getRenderDirector.call(
	          manager,
	          resolvedType,
	          model,
	          currentUser,
	          siteSettings,
	          site
	        ));
	        if (!director)
	          return fallbackNotificationPresentation(
	            notification,
	            resolvedType
	          );
	        const label = nativePresentationText(director.label), description = nativePresentationText(director.description);
	        return Object.freeze({
	          actor: modelData.display_username ?? modelData.username ?? nativeModelValue(model, "username"),
	          typeName: resolvedType,
	          typeLabel: nativePresentationText(director.linkTitle) || resolvedType || "通知",
	          summary: [label, description].filter(Boolean).join(" · "),
	          href: String(director.linkHref ?? "").trim(),
	          topicId: nativeModelValue(model, "topic_id") ?? nativeModelValue(model, "topicId") ?? modelData.topic_id,
	          postNumber: nativeModelValue(model, "post_number") ?? nativeModelValue(model, "postNumber") ?? modelData.post_number
	        });
	      } catch {
	        return fallbackNotificationPresentation(
	          notification,
	          resolvedType
	        );
	      }
	    }));
	  }
	  subscribeChanged(listener) {
	    const appEvents = (0, import_value_record.objectRecord)(
	      this.#host.lookup("service:app-events")
	    ), on = appEvents?.on, off = appEvents?.off, cleanups = [], context = Object.freeze({});
	    if (typeof on == "function" && typeof off == "function")
	      try {
	        on.call(appEvents, "notifications:changed", context, listener), cleanups.push(() => {
	          try {
	            off.call(appEvents, "notifications:changed", context, listener);
	          } catch {
	          }
	        });
	      } catch {
	      }
	    const userId = Number(nativeModelValue(
	      discourseNativeCurrentUser(this.#host),
	      "id"
	    ));
	    if (Number.isSafeInteger(userId) && userId > 0) {
	      const messageBus = new import_native_message_bus.BrowserDiscourseMessageBusPort(this.#host);
	      let active2 = !0;
	      const onMessage = () => {
	        Promise.resolve().then(() => {
	          active2 && listener();
	        });
	      }, channel = `/notification/${userId}`;
	      try {
	        messageBus.subscribe(channel, onMessage), cleanups.push(() => {
	          active2 = !1;
	          try {
	            messageBus.unsubscribe(channel, onMessage);
	          } catch {
	          }
	        });
	      } catch {
	        active2 = !1;
	      }
	    }
	    let active = !0;
	    return () => {
	      if (active) {
	        active = !1;
	        for (const cleanup of cleanups.splice(0).reverse()) cleanup();
	      }
	    };
	  }
	  subscribeClicked(listener) {
	    return discourseNativeAppEventSubscription(
	      this.#host,
	      "user-menu:notification-click",
	      (payload) => {
	        const source = (0, import_value_record.objectRecord)(payload), notification = source?.notification, notificationId = Number(nativeModelValue(notification, "id"));
	        if (!Number.isSafeInteger(notificationId) || notificationId <= 0) return;
	        const read = nativeModelValue(notification, "read");
	        listener(Object.freeze({
	          notificationId,
	          href: String(source?.href ?? "").trim(),
	          wasRead: typeof read == "boolean" ? read : null
	        }));
	      }
	    );
	  }
	}
	class BrowserDiscourseBookmarkNativeState {
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  username() {
	    return discourseNativeCurrentUsername(this.#host).replace(/^@/, "");
	  }
	  findGivenReactions(usernameValue, beforeReactionUserId) {
	    const username = String(usernameValue).trim().replace(/^@/, "");
	    if (!username)
	      return Promise.reject(new Error("回应记录需要当前登录用户名"));
	    const module2 = (0, import_value_record.objectRecord)(this.#host.lookupModule(
	      "discourse/plugins/discourse-reactions/discourse/models/discourse-reactions-custom-reaction"
	    )), model = (0, import_value_record.valueRecord)(module2?.default), findReactions = model?.findReactions;
	    if (typeof findReactions != "function")
	      return Promise.reject(new Error("Discourse 回应记录接口尚未就绪"));
	    const cursor = Number(beforeReactionUserId);
	    return Promise.resolve(findReactions.call(
	      model,
	      "reactions",
	      username,
	      Object.freeze({
	        ...Number.isSafeInteger(cursor) && cursor > 0 ? { beforeReactionUserId: cursor } : {}
	      })
	    ));
	  }
	  subscribeChanged(listener) {
	    return discourseDeferredSubscription(() => {
	      const appEvents = (0, import_value_record.objectRecord)(
	        this.#host.lookup("service:app-events")
	      ), on = appEvents?.on, off = appEvents?.off;
	      if (typeof on != "function" || typeof off != "function") return null;
	      const context = Object.freeze({}), subscriptions = Object.freeze([
	        ["bookmarks:changed", "bookmarks"],
	        ["discourse-reactions:reaction-toggled", "reactions"]
	      ]), attached = [];
	      for (const [name, source] of subscriptions) {
	        const handler = () => listener(source);
	        try {
	          on.call(appEvents, name, context, handler), attached.push(Object.freeze({ name, handler }));
	        } catch {
	        }
	      }
	      let active = !0;
	      return () => {
	        if (active) {
	          active = !1;
	          for (const entry of attached)
	            try {
	              off.call(
	                appEvents,
	                entry.name,
	                context,
	                entry.handler
	              );
	            } catch {
	            }
	        }
	      };
	    });
	  }
	}
	function readyModule(name, value) {
	  if (typeof value == "function") return !0;
	  const module2 = (0, import_value_record.objectRecord)(value);
	  if (module2 === null || Reflect.ownKeys(module2).length === 0) return !1;
	  if (name !== "discourse/lib/text") return !0;
	  const defaultExport = (0, import_value_record.objectRecord)(module2.default);
	  return typeof module2.emojiUrlFor == "function" || typeof defaultExport?.emojiUrlFor == "function";
	}
	function nonEmptyName(value, kind) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error(`Discourse ${kind} 名称不能为空`);
	  return normalized;
	}
	function moduleName(value) {
	  const normalized = nonEmptyName(value, "module");
	  if (normalized !== "jquery" && !normalized.startsWith("discourse/"))
	    throw new Error(`拒绝解析非 Discourse 原生 module:${normalized}`);
	  return normalized;
	}
	function callable(owner, name) {
	  const candidate = owner?.[name];
	  return typeof candidate == "function" ? candidate : null;
	}
	function moduleContainer(value) {
	  const module2 = (0, import_value_record.objectRecord)(value), defaultExport = (0, import_value_record.objectRecord)(module2?.default), candidate = (0, import_value_record.objectRecord)(defaultExport?.container ?? module2?.container);
	  return typeof candidate?.lookup == "function" ? candidate : null;
	}
	class BrowserDiscourseHostApiPort {
	  #pageWindow;
	  #modules = /* @__PURE__ */ new Map();
	  #lookups = /* @__PURE__ */ new Map();
	  #container = null;
	  constructor(options) {
	    const pageWindow = (0, import_value_record.objectRecord)(options.pageWindow);
	    if (!pageWindow) throw new Error("Discourse page window 不可用");
	    this.#pageWindow = pageWindow;
	  }
	  lookup(nameValue) {
	    const name = nonEmptyName(nameValue, "container lookup"), volatile = name === "service:current-user";
	    if (!volatile && this.#lookups.has(name)) return this.#lookups.get(name);
	    const container = this.#resolveContainer();
	    if (!container) return null;
	    try {
	      const value = container.lookup(name);
	      return !volatile && value !== null && value !== void 0 && this.#lookups.set(name, value), value ?? null;
	    } catch {
	      return null;
	    }
	  }
	  lookupModule(nameValue) {
	    const name = moduleName(nameValue);
	    if (this.#modules.has(name)) return this.#modules.get(name);
	    const broker = (0, import_value_record.objectRecord)(this.#pageWindow.moduleBroker), brokerLookup = callable(broker, "lookup"), requireModule = callable(this.#pageWindow, "require"), requireJsModule = callable(this.#pageWindow, "requirejs"), resolvers = Object.freeze([
	      () => brokerLookup?.call(broker, name, !0),
	      () => requireModule?.call(this.#pageWindow, name),
	      () => requireJsModule?.call(this.#pageWindow, name)
	    ]);
	    for (const resolve of resolvers)
	      try {
	        const value = resolve();
	        if (readyModule(name, value))
	          return this.#modules.set(name, value), value;
	      } catch {
	      }
	    return null;
	  }
	  #resolveContainer() {
	    if (this.#container) return this.#container;
	    const urlContainer = moduleContainer(this.lookupModule("discourse/lib/url")), discourse = (0, import_value_record.objectRecord)(this.#pageWindow.Discourse), fallback = (0, import_value_record.objectRecord)(discourse?.__container__ ?? discourse?.container), fallbackContainer = typeof fallback?.lookup == "function" ? fallback : null;
	    return this.#container = urlContainer ?? fallbackContainer, this.#container;
	  }
	}
}, "b1692ff3151f3c4ea4ff4e7d17999c87a45e84aa11d07134e5583dc2633f6abc");

/* Source: lite/src/discourse/native-message-bus.ts */
runtime.register("src/discourse/native-message-bus.js", function(module, exports, require) {
	var native_message_bus_exports = {};
	__export(native_message_bus_exports, {
	  BrowserDiscourseMessageBusPort: () => BrowserDiscourseMessageBusPort
	});
	module.exports = __toCommonJS(native_message_bus_exports);
	function channelName(value) {
	  const channel = String(value).trim();
	  if (!channel.startsWith("/") || channel.includes("://"))
	    throw new Error("Discourse MessageBus channel 必须是站内 /channel");
	  return channel;
	}
	function resolveMessageBus(host) {
	  const service = host.lookup("service:message-bus");
	  if (!service || typeof service != "object" || typeof service.subscribe != "function" || typeof service.unsubscribe != "function")
	    throw new Error("Discourse 原生 service:message-bus 不可用");
	  return {
	    owner: service,
	    subscribe: service.subscribe,
	    unsubscribe: service.unsubscribe
	  };
	}
	class BrowserDiscourseMessageBusPort {
	  nativeBinding = "service:message-bus";
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  subscribe(channelValue, handler) {
	    const channel = channelName(channelValue);
	    if (typeof handler != "function") throw new TypeError("MessageBus handler 必须是函数");
	    const resolved = resolveMessageBus(this.#host);
	    resolved.subscribe.call(resolved.owner, channel, handler);
	  }
	  unsubscribe(channelValue, handler) {
	    const channel = channelName(channelValue);
	    if (typeof handler != "function") throw new TypeError("MessageBus handler 必须是函数");
	    const resolved = resolveMessageBus(this.#host);
	    resolved.unsubscribe.call(resolved.owner, channel, handler);
	  }
	}
}, "d0bf1383145a984b90fd78cbeddbeeb62db60d8aa3f7058b1742c93585f7f76d");

/* Source: lite/src/discourse/native-post-model-factory.ts */
runtime.register("src/discourse/native-post-model-factory.js", function(module, exports, require) {
	var native_post_model_factory_exports = {};
	__export(native_post_model_factory_exports, {
	  DiscourseNativePostModelFactory: () => DiscourseNativePostModelFactory
	});
	module.exports = __toCommonJS(native_post_model_factory_exports);
	var import_identifiers = require("./identifiers.js"), import_native_host_api = require("./native-host-api.js"), import_native_message_bus = require("./native-message-bus.js"), import_value_record = require("../kernel/value-record.js");
	function moduleDefault(moduleValue, name) {
	  const module2 = (0, import_value_record.valueRecord)(moduleValue), value = (0, import_value_record.valueRecord)(module2?.default);
	  if (!value) throw new Error(`Discourse 原生模块未就绪:${name}`);
	  return value;
	}
	function modelValue(value, key) {
	  const target = (0, import_value_record.valueRecord)(value), getter = target?.get;
	  return typeof getter == "function" ? getter.call(target, key) : target?.[key];
	}
	function setModelValue(value, key, next) {
	  const target = (0, import_value_record.valueRecord)(value), set = target?.set;
	  if (typeof set == "function") {
	    set.call(target, key, next);
	    return;
	  }
	  target && (target[key] = next);
	}
	function reactionId(value) {
	  return String(value ?? "").trim().replace(/^:+|:+$/g, "");
	}
	function modelFactory(moduleValue, name) {
	  const model = moduleDefault(moduleValue, name);
	  if (typeof model.create != "function")
	    throw new Error(`Discourse 原生 model 缺少 create:${name}`);
	  return model;
	}
	class DiscourseNativePostModelFactory {
	  #host;
	  #bindings = null;
	  constructor(host) {
	    this.#host = host;
	  }
	  /**
	   * 只解析并校验回复 Composer 必需的 Topic/Post 原生 model。
	   *
	   * 该预热入口不创建 model、不读取 draft,也不触发任何宿主请求;实际回复仍由
	   * DiscourseComposerCoordinator 在用户操作后创建对应 Topic/Post 实例。
	   */
	  prepareComposerBindings() {
	    const bindings = this.#runtimeBindings();
	    if (modelFactory(bindings.topicModel, "discourse/models/topic"), typeof modelFactory(bindings.postModel, "discourse/models/post").munge != "function")
	      throw new Error("Discourse Post.munge 未就绪");
	  }
	  createTopic(topic) {
	    const bindings = this.#runtimeBindings();
	    return this.#createTopic(topic, bindings.topicModel);
	  }
	  createTopicDetails(topic) {
	    const bindings = this.#runtimeBindings(), TopicDetails = modelFactory(
	      bindings.topicDetailsModel,
	      "discourse/models/topic-details"
	    ), details = (0, import_value_record.valueRecord)(topic.details), topicRecord = topic;
	    return TopicDetails.create({
	      ...details ?? {},
	      topic: this.#createTopic(topic, bindings.topicModel),
	      notification_level: topicRecord.notification_level ?? modelValue(details, "notification_level") ?? 1
	    });
	  }
	  createPost(topic, post, topicModel) {
	    const bindings = this.#runtimeBindings(), owner = topicModel ?? this.#createTopic(topic, bindings.topicModel);
	    return this.#createPost(topic, post, owner, bindings.postModel);
	  }
	  createContext(topic, post) {
	    const bindings = this.#runtimeBindings(), appEvents = (0, import_value_record.valueRecord)(bindings.appEvents);
	    if (!appEvents) throw new Error("Discourse app-events service 未就绪");
	    const topicModel = this.#createTopic(topic, bindings.topicModel);
	    return Object.freeze({
	      topic: topicModel,
	      post: this.#createPost(topic, post, topicModel, bindings.postModel),
	      appEvents
	    });
	  }
	  /**
	   * discourse-calendar 的活动报名必须携带插件原生 Event model。
	   *
	   * 特殊正文 feature 只提交 canonical event JSON;module lookup 与 model.create 继续
	   * 收口在同一原生 model 工厂,不能由 DOM 组件自行解析插件模块。
	   */
	  createPostEvent(event, fallbackPostId) {
	    const EventModel = modelFactory(
	      (0, import_native_host_api.discourseNativePostEventModel)(this.#host),
	      "discourse-post-event-event"
	    ), id = Number(event.id) || Number(fallbackPostId);
	    if (!Number.isSafeInteger(id) || id < 1)
	      throw new RangeError("活动必须具有正安全整数 ID");
	    return EventModel.create({
	      ...event,
	      id
	    });
	  }
	  /**
	   * Discourse `/client_settings` 是回应目录热更新的唯一宿主事件源。
	   *
	   * 收到消息后先更新原生 site-settings service,再通知现有 PostView 重投;本方法
	   * 不请求 emoji 目录、不维护第二份设置快照,也不暴露 MessageBus 给 UI。
	   */
	  subscribeClientSettings(listener) {
	    if (typeof listener != "function")
	      throw new TypeError("client settings listener 必须是函数");
	    const messageBus = new import_native_message_bus.BrowserDiscourseMessageBusPort(this.#host), handler = (message) => {
	      const input = (0, import_value_record.valueRecord)(message), name = String(input?.name ?? "").trim();
	      [
	        "discourse_reactions_enabled_reactions",
	        "discourse_reactions_reaction_for_like"
	      ].includes(name) && (setModelValue(
	        this.#runtimeBindings().siteSettings,
	        name,
	        input?.value
	      ), listener());
	    };
	    try {
	      messageBus.subscribe("/client_settings", handler);
	    } catch {
	      return () => {
	      };
	    }
	    return () => {
	      try {
	        messageBus.unsubscribe("/client_settings", handler);
	      } catch {
	      }
	    };
	  }
	  /**
	   * 需要原生 current-user model 作为参数的动作共用这一窄入口。
	   *
	   * 不把 service lookup 暴露给 UI;匿名态返回 null,由动作组件保持原按钮不可用。
	   */
	  currentUser() {
	    return (0, import_value_record.valueRecord)(this.#runtimeBindings().currentUser);
	  }
	  sharedIssueAllowsMultipleSolutions() {
	    return modelValue(
	      this.#runtimeBindings().siteSettings,
	      "solved_allow_multiple_solutions"
	    ) === !0;
	  }
	  minimumPostLength() {
	    const value = Number(modelValue(
	      this.#runtimeBindings().siteSettings,
	      "min_post_length"
	    ));
	    return Number.isSafeInteger(value) && value > 0 ? value : 16;
	  }
	  reactionRegistry() {
	    const bindings = this.#runtimeBindings(), settings = (0, import_value_record.valueRecord)(bindings.siteSettings), configuredValue = modelValue(
	      settings,
	      "discourse_reactions_enabled_reactions"
	    ), source = Array.isArray(configuredValue) ? configuredValue : String(configuredValue ?? "").split("|"), configuredIds = Object.freeze(
	      source.map(reactionId).filter(Boolean)
	    ), mainReaction = reactionId(modelValue(
	      settings,
	      "discourse_reactions_reaction_for_like"
	    ));
	    return Object.freeze({
	      configuredIds,
	      mainReaction,
	      emojiUrl: (id) => (0, import_native_host_api.discourseNativeEmojiUrl)(this.#host, reactionId(id))
	    });
	  }
	  reportContext(topic, post, nameKeys) {
	    const native = this.createContext(topic, post), actionByName = modelValue(native.post, "actionByName"), actions = [...new Set(nameKeys.map(String).map((name) => name.trim()).filter(Boolean))].map((nameKey) => {
	      const action = modelValue(actionByName, nameKey), actionRecord = (0, import_value_record.valueRecord)(action);
	      return modelValue(action, "can_act") !== !0 || !actionRecord ? null : (Number(post.post_number) === 1 && (actionRecord.flagTopic = native.post), Object.freeze({
	        nameKey,
	        action
	      }));
	    }).filter(
	      (value) => value !== null
	    );
	    return Object.freeze({
	      post: native.post,
	      actions: Object.freeze(actions)
	    });
	  }
	  #createTopic(topic, topicModule) {
	    const topicId = (0, import_identifiers.discourseTopicId)(topic.id), Topic = modelFactory(topicModule, "discourse/models/topic"), details = (0, import_value_record.valueRecord)(topic.details);
	    return Topic.create({
	      id: topicId,
	      title: String(topic.title ?? ""),
	      fancy_title: String(topic.fancy_title ?? topic.title ?? ""),
	      slug: String(topic.slug ?? "topic"),
	      category_id: topic.category_id,
	      archetype: String(topic.archetype ?? "regular"),
	      draft_key: topic.draft_key,
	      draft_sequence: topic.draft_sequence,
	      posts_count: topic.posts_count,
	      highest_post_number: topic.highest_post_number,
	      last_posted_at: topic.last_posted_at,
	      chunk_size: topic.chunk_size,
	      details: { can_create_post: !0, ...details ?? {} },
	      tags: Array.isArray(topic.tags) ? [...topic.tags] : []
	    });
	  }
	  #createPost(topic, post, topicModel, postModule) {
	    const topicId = (0, import_identifiers.discourseTopicId)(topic.id);
	    if (post.topic_id !== void 0 && (0, import_identifiers.discourseTopicId)(post.topic_id) !== topicId)
	      throw new Error("楼层不属于目标 Topic");
	    const Post = modelFactory(postModule, "discourse/models/post");
	    if (typeof Post.munge != "function")
	      throw new Error("Discourse Post.munge 未就绪");
	    const reference = (0, import_identifiers.discoursePostReference)(post), actions = Array.isArray(post.actions_summary) ? post.actions_summary.map((action) => action && typeof action == "object" ? { ...action } : action) : [], attributes = {
	      ...post,
	      id: reference.postId,
	      topic_id: topicId,
	      post_number: reference.postNumber,
	      username: String(post.username ?? ""),
	      name: String(post.name ?? ""),
	      avatar_template: String(post.avatar_template ?? ""),
	      post_type: Number(post.post_type ?? 1),
	      reply_count: Number(post.reply_count ?? 0),
	      topic: topicModel,
	      actions_summary: actions
	    }, munge = Post.munge;
	    return Post.create(munge.call(Post, attributes));
	  }
	  #runtimeBindings() {
	    if (this.#bindings) return this.#bindings;
	    const bindings = (0, import_native_host_api.discourseNativePostRuntimeBindings)(this.#host);
	    return Object.values(bindings).every((value) => value != null) && (this.#bindings = bindings), bindings;
	  }
	}
}, "c492019f71ca4690d179e142f690ae7cd181e8b77625f562c85b5bff3e4330fe");

/* Source: lite/src/discourse/native-presence.ts */
runtime.register("src/discourse/native-presence.js", function(module, exports, require) {
	var native_presence_exports = {};
	__export(native_presence_exports, {
	  BrowserDiscoursePresencePort: () => BrowserDiscoursePresencePort
	});
	module.exports = __toCommonJS(native_presence_exports);
	var import_value_record = require("../kernel/value-record.js");
	function modelValue(value, key) {
	  const source = (0, import_value_record.valueRecord)(value);
	  if (!source) return;
	  const getter = source.get;
	  if (typeof getter == "function")
	    try {
	      const result = getter.call(value, key);
	      if (result !== void 0) return result;
	    } catch {
	    }
	  return source[key];
	}
	function normalizeUsers(value) {
	  let source = [];
	  if (Array.isArray(value)) source = value;
	  else {
	    const toArray = (0, import_value_record.valueRecord)(value)?.toArray;
	    if (typeof toArray == "function")
	      try {
	        const result = toArray.call(value);
	        Array.isArray(result) && (source = result);
	      } catch {
	        source = [];
	      }
	    else value && typeof value[Symbol.iterator] == "function" && (source = [...value]);
	  }
	  const users = source.map((candidate) => {
	    const username = String(
	      modelValue(candidate, "username") ?? ""
	    ).trim();
	    return username ? Object.freeze({
	      username,
	      name: String(
	        modelValue(candidate, "name") ?? username
	      ).trim() || username,
	      avatarTemplate: String(
	        modelValue(candidate, "avatar_template") ?? modelValue(candidate, "avatarTemplate") ?? ""
	      ).trim()
	    }) : null;
	  }).filter((user) => user !== null);
	  return Object.freeze(users);
	}
	function topicIdentifier(value) {
	  const topicId = Number(value);
	  if (!Number.isSafeInteger(topicId) || topicId < 1)
	    throw new RangeError("Presence topicId 必须是正安全整数");
	  return topicId;
	}
	class BrowserDiscoursePresencePort {
	  nativeBinding = "service:presence";
	  #host;
	  constructor(host) {
	    this.#host = host;
	  }
	  watchReplying(topicIdValue, listener, onError = () => {
	  }) {
	    const topicId = topicIdentifier(topicIdValue);
	    if (typeof listener != "function")
	      throw new TypeError("Presence listener 必须是函数");
	    const service = (0, import_value_record.valueRecord)(this.#host.lookup("service:presence")), getChannel = service?.getChannel;
	    if (typeof getChannel != "function")
	      return listener(Object.freeze([])), () => {
	      };
	    let channel = null;
	    try {
	      channel = (0, import_value_record.valueRecord)(
	        getChannel.call(
	          service,
	          `/discourse-presence/reply/${topicId}`
	        )
	      );
	    } catch (error) {
	      onError(error);
	    }
	    if (!channel || typeof channel.subscribe != "function" || typeof channel.unsubscribe != "function")
	      return listener(Object.freeze([])), () => {
	      };
	    const nativeChannel = channel, subscribe = nativeChannel.subscribe, unsubscribe = nativeChannel.unsubscribe;
	    let active = !0, subscriptionSettled = !1, unsubscribeIssued = !1;
	    const publish = () => {
	      if (active)
	        try {
	          listener(normalizeUsers(modelValue(nativeChannel, "users")));
	        } catch (error) {
	          onError(error);
	        }
	    }, change = () => publish();
	    if (typeof nativeChannel.on == "function")
	      try {
	        nativeChannel.on.call(nativeChannel, "change", change);
	      } catch (error) {
	        onError(error);
	      }
	    const issueUnsubscribe = () => {
	      if (!unsubscribeIssued) {
	        unsubscribeIssued = !0;
	        try {
	          Promise.resolve(
	            unsubscribe.call(nativeChannel)
	          ).catch(onError);
	        } catch (error) {
	          onError(error);
	        }
	      }
	    };
	    try {
	      Promise.resolve(
	        subscribe.call(nativeChannel)
	      ).then(() => {
	        subscriptionSettled = !0, active ? publish() : issueUnsubscribe();
	      }).catch((error) => {
	        subscriptionSettled = !0, active && (onError(error), listener(Object.freeze([]))), issueUnsubscribe();
	      });
	    } catch (error) {
	      subscriptionSettled = !0, onError(error), listener(Object.freeze([])), issueUnsubscribe();
	    }
	    return () => {
	      if (active) {
	        if (active = !1, typeof nativeChannel.off == "function")
	          try {
	            nativeChannel.off.call(nativeChannel, "change", change);
	          } catch (error) {
	            onError(error);
	          }
	        subscriptionSettled && issueUnsubscribe();
	      }
	    };
	  }
	}
}, "785adf7cee061f235c01912b31517054fd9e90a0c2a16c7bc51fd86ae56433d1");

/* Source: lite/src/discourse/native-request-descriptors.ts */
runtime.register("src/discourse/native-request-descriptors.js", function(module, exports, require) {
	var native_request_descriptors_exports = {};
	__export(native_request_descriptors_exports, {
	  DISCOURSE_DIRECT_REPLIES_PAGE_SIZE: () => DISCOURSE_DIRECT_REPLIES_PAGE_SIZE,
	  DiscourseNativeRequests: () => DiscourseNativeRequests,
	  assertDiscourseNativeMutationDescriptor: () => assertDiscourseNativeMutationDescriptor,
	  assertDiscourseNativeReadDescriptor: () => assertDiscourseNativeReadDescriptor,
	  discourseBasePath: () => discourseBasePath,
	  discourseNativeTargetFailureIsDefinitive: () => discourseNativeTargetFailureIsDefinitive
	});
	module.exports = __toCommonJS(native_request_descriptors_exports);
	var import_identifiers = require("./identifiers.js");
	const nativeReadDescriptorBrand = Symbol("DiscourseNativeReadDescriptor"), nativeMutationDescriptorBrand = Symbol("DiscourseNativeMutationDescriptor"), nativeReadDescriptors = /* @__PURE__ */ new WeakSet(), nativeMutationDescriptors = /* @__PURE__ */ new WeakSet(), DISCOURSE_DIRECT_REPLIES_PAGE_SIZE = 20;
	function discourseNativeTargetFailureIsDefinitive(input) {
	  return input.scope === "single" && input.endpoint === "post-by-number" && (input.status === 404 || input.status === 410);
	}
	function discourseBasePath(value) {
	  const normalized = String(value ?? "").trim().replace(/\/+$/, "");
	  if (normalized && !normalized.startsWith("/") && !/^https?:\/\//i.test(normalized))
	    throw new Error("basePath 必须是绝对 URL 或以 / 开头的路径");
	  return normalized;
	}
	function encodedSlug(value) {
	  const slug = String(value ?? "topic").trim();
	  if (!slug) throw new Error("slug 不能为空");
	  return encodeURIComponent(slug);
	}
	function readDescriptor(operation, path, options = {}) {
	  const descriptor = Object.freeze({
	    operation,
	    path,
	    headers: Object.freeze({ ...options.headers ?? {} }),
	    browserCache: options.browserCache ?? "default",
	    [nativeReadDescriptorBrand]: !0
	  });
	  return nativeReadDescriptors.add(descriptor), descriptor;
	}
	function assertDiscourseNativeReadDescriptor(value) {
	  if (value === null || typeof value != "object" || !nativeReadDescriptors.has(value))
	    throw new Error("读取请求必须来自 Discourse 原生请求目录");
	}
	function assertDiscourseNativeMutationDescriptor(value) {
	  if (value === null || typeof value != "object" || !nativeMutationDescriptors.has(value))
	    throw new Error("写请求必须来自 Discourse 原生请求目录");
	}
	function targetDescriptor(endpoint, url, refresh) {
	  return Object.freeze({
	    endpoint,
	    url,
	    descriptor: readDescriptor("target-post", url, {
	      headers: { Accept: "application/json" },
	      browserCache: refresh ? "no-store" : "default"
	    })
	  });
	}
	const DiscourseNativeRequests = Object.freeze({
	  topic(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), basePath = discourseBasePath(input.basePath);
	    return readDescriptor(
	      "topic",
	      `${basePath}/t/${topicId}.json?track_visit=true&forceLoad=true`,
	      {
	        headers: {
	          Accept: "application/json",
	          "Discourse-Track-View": "true",
	          "Discourse-Track-View-Topic-Id": String(topicId)
	        }
	      }
	    );
	  },
	  postsById(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), query = (0, import_identifiers.discoursePostIds)(input.postIds).map((postId) => `post_ids[]=${encodeURIComponent(postId)}`).join("&");
	    return readDescriptor(
	      "posts-by-id",
	      `${discourseBasePath(input.basePath)}/t/${topicId}/posts.json?${query}`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  postById(input) {
	    const postId = (0, import_identifiers.discoursePostId)(input.postId);
	    return readDescriptor(
	      "post-by-id",
	      `${discourseBasePath(input.basePath)}/posts/${postId}.json`,
	      {
	        headers: { Accept: "application/json" },
	        browserCache: "no-store"
	      }
	    );
	  },
	  targetCandidates(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), postNumber = (0, import_identifiers.discoursePostNumber)(input.postNumber), basePath = discourseBasePath(input.basePath), slug = encodedSlug(input.slug), refresh = input.refresh === !0, paths = Object.freeze({
	      "post-by-number": `${basePath}/posts/by_number/${topicId}/${postNumber}.json`,
	      "topic-floor": `${basePath}/t/${slug}/${topicId}/${postNumber}.json`,
	      "topic-query": `${basePath}/t/${slug}/${topicId}.json?post_number=${postNumber}`,
	      "topic-id-query": `${basePath}/t/${topicId}.json?post_number=${postNumber}`
	    }), order = input.scope === "around" ? ["topic-floor", "topic-query", "topic-id-query", "post-by-number"] : ["post-by-number", "topic-floor", "topic-id-query"];
	    return Object.freeze(order.map((endpoint) => targetDescriptor(endpoint, paths[endpoint], refresh)));
	  },
	  directReplies(input) {
	    const parentPostId = (0, import_identifiers.discoursePostId)(input.parentPostId), after = (0, import_identifiers.discourseReplyCursor)(input.after), suffix = after > 0 ? `?after=${after}` : "";
	    return readDescriptor(
	      "direct-replies",
	      `${discourseBasePath(input.basePath)}/posts/${parentPostId}/replies.json${suffix}`,
	      {
	        headers: { Accept: "application/json" },
	        browserCache: "no-store"
	      }
	    );
	  },
	  postVotingComments(input) {
	    const postId = (0, import_identifiers.discoursePostId)(input.postId), afterCommentId = Number(input.afterCommentId ?? 0);
	    if (!Number.isSafeInteger(afterCommentId) || afterCommentId < 0)
	      throw new RangeError("afterCommentId 必须是非负安全整数");
	    const query = new URLSearchParams({ post_id: String(postId) });
	    return afterCommentId > 0 && query.set("last_comment_id", String(afterCommentId)), readDescriptor(
	      "post-voting-comments",
	      `${discourseBasePath(input.basePath)}/post_voting/comments?${query}`,
	      {
	        headers: { Accept: "application/json" },
	        browserCache: "no-store"
	      }
	    );
	  },
	  boostReportAccess(input) {
	    const boostId = Number(input.boostId);
	    if (!Number.isSafeInteger(boostId) || boostId <= 0)
	      throw new RangeError("boostId 必须是正安全整数");
	    return readDescriptor(
	      "boost-report-access",
	      `${discourseBasePath(input.basePath)}/discourse-boosts/boosts/${boostId}.json`,
	      {
	        headers: { Accept: "application/json" },
	        browserCache: "no-store"
	      }
	    );
	  },
	  endorsableCategories(input) {
	    const username = String(input.username).trim().replace(/^@+/, "");
	    if (!username) throw new Error("username 不能为空");
	    return readDescriptor(
	      "endorsable-categories",
	      `${discourseBasePath(input.basePath)}/category-experts/endorsable-categories/${encodeURIComponent(username)}.json`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  userFollowList(input) {
	    const username = String(input.username).trim().replace(/^@+/, "");
	    if (!username) throw new Error("username 不能为空");
	    if (input.kind !== "following" && input.kind !== "followers")
	      throw new Error("关注列表类型无效");
	    return readDescriptor(
	      "user-follow-list",
	      `${discourseBasePath(input.basePath)}/u/${encodeURIComponent(username)}/follow/${input.kind}`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  userBadges(input) {
	    const username = String(input.username).trim().replace(/^@+/, "");
	    if (!username) throw new Error("username 不能为空");
	    return readDescriptor(
	      "user-badges",
	      `${discourseBasePath(input.basePath)}/user-badges/${encodeURIComponent(username)}.json`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  userSummary(input) {
	    const username = String(input.username).trim().replace(/^@+/, "");
	    if (!username) throw new Error("username 不能为空");
	    return readDescriptor(
	      "user-summary",
	      `${discourseBasePath(input.basePath)}/u/${encodeURIComponent(username)}/summary.json`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  userDirectoryStats(input) {
	    const username = String(input.username).trim().replace(/^@+/, "");
	    if (!username) throw new Error("username 不能为空");
	    const query = new URLSearchParams({
	      period: "all",
	      order: "likes_received",
	      username
	    });
	    return readDescriptor(
	      "user-directory-stats",
	      `${discourseBasePath(input.basePath)}/directory_items.json?${query}`,
	      { headers: { Accept: "application/json" } }
	    );
	  },
	  topicTimings(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), postNumbers = (0, import_identifiers.discoursePostNumbers)(input.postNumbers), readTimeMs = Number(input.readTimeMs);
	    if (!Number.isSafeInteger(readTimeMs) || readTimeMs < 1 || readTimeMs > 6e4)
	      throw new RangeError("readTimeMs 必须是 1..60000 的安全整数");
	    const timings = Object.fromEntries(postNumbers.map((postNumber) => [
	      String(postNumber),
	      readTimeMs
	    ])), data = Object.freeze({
	      topic_id: topicId,
	      topic_time: readTimeMs * postNumbers.length,
	      timings: Object.freeze(timings)
	    }), descriptor = Object.freeze({
	      operation: "topic-timings",
	      path: `${discourseBasePath(input.basePath)}/topics/timings`,
	      method: "POST",
	      headers: Object.freeze({
	        "Discourse-Background": "true",
	        "X-SILENCE-LOGGER": "true"
	      }),
	      data,
	      [nativeMutationDescriptorBrand]: !0
	    });
	    return nativeMutationDescriptors.add(descriptor), descriptor;
	  },
	  topicSummary(input) {
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), descriptor = Object.freeze({
	      operation: "topic-summary",
	      path: `${discourseBasePath(input.basePath)}/discourse-ai/summarization/t/${topicId}`,
	      method: "POST",
	      headers: Object.freeze({ Accept: "application/json" }),
	      data: Object.freeze({}),
	      [nativeMutationDescriptorBrand]: !0
	    });
	    return nativeMutationDescriptors.add(descriptor), descriptor;
	  },
	  topicSummaryImageUpload(input) {
	    const descriptor = Object.freeze({
	      operation: "topic-summary-image-upload",
	      path: `${discourseBasePath(input.basePath)}/uploads.json`,
	      method: "POST",
	      headers: Object.freeze({ Accept: "application/json" }),
	      data: input.formData,
	      multipart: !0,
	      [nativeMutationDescriptorBrand]: !0
	    });
	    return nativeMutationDescriptors.add(descriptor), descriptor;
	  }
	});
}, "e02199ee304a9a30932ed0e49eb4cc80be4143f88d9fc516734cd45b70331918");

/* Source: lite/src/discourse/native-topic-notification-action.ts */
runtime.register("src/discourse/native-topic-notification-action.js", function(module, exports, require) {
	var native_topic_notification_action_exports = {};
	__export(native_topic_notification_action_exports, {
	  BrowserDiscourseTopicNotificationLevelMutationPort: () => BrowserDiscourseTopicNotificationLevelMutationPort
	});
	module.exports = __toCommonJS(native_topic_notification_action_exports);
	var import_native_post_model_factory = require("./native-post-model-factory.js"), import_discourse_action_descriptors = require("../post/discourse-action-descriptors.js"), import_discourse_action_transport = require("../post/discourse-action-transport.js"), import_value_record = require("../kernel/value-record.js");
	function modelValue(value, key) {
	  const source = (0, import_value_record.valueRecord)(value), getter = source?.get;
	  return typeof getter == "function" ? getter.call(value, key) : source?.[key];
	}
	class BrowserDiscourseTopicNotificationLevelMutationPort {
	  #models;
	  #descriptors = new import_discourse_action_descriptors.DiscourseActionDescriptors();
	  #actions;
	  constructor(host) {
	    this.#models = new import_native_post_model_factory.DiscourseNativePostModelFactory(host), this.#actions = new import_discourse_action_transport.BrowserDiscourseNativeActionPort(host);
	  }
	  async setLevel(topic, level) {
	    const topicId = Number(modelValue(topic, "id"));
	    if (!Number.isSafeInteger(topicId) || topicId < 1)
	      throw new RangeError("宿主话题通知动作缺少有效 Topic ID");
	    const topicDetails = this.#models.createTopicDetails(
	      topic
	    ), mutation = this.#descriptors.topicNotificationLevel({
	      topicId,
	      topicDetails,
	      level
	    }), definition = (0, import_discourse_action_transport.discourseActionTransportDefinition)(
	      mutation.operation,
	      mutation.targetType
	    ), response = await this.#actions.execute({
	      definition,
	      targetId: mutation.targetId,
	      variant: mutation.variant ?? null,
	      payload: mutation.payload,
	      signal: new AbortController().signal,
	      attempt: 0
	    });
	    if (!response.ok)
	      throw new Error(`宿主话题通知设置失败(HTTP ${response.status || 0})`);
	  }
	}
}, "70e2eaa947da735f595616c706700d003b7deb8e12729a0093c9cc1f662f72f7");

/* Source: lite/src/discourse/reader-native-composer-window.ts */
runtime.register("src/discourse/reader-native-composer-window.js", function(module, exports, require) {
	var reader_native_composer_window_exports = {};
	__export(reader_native_composer_window_exports, {
	  DISCOURSE_NATIVE_FLOATING_SELECTOR: () => DISCOURSE_NATIVE_FLOATING_SELECTOR,
	  ReaderNativeComposerWindowController: () => ReaderNativeComposerWindowController,
	  discourseNativeFloatingSurfaceVisible: () => discourseNativeFloatingSurfaceVisible,
	  normalizeReaderNativeComposerGeometry: () => normalizeReaderNativeComposerGeometry,
	  readerNativeComposerFontPixels: () => readerNativeComposerFontPixels,
	  readerNativeTopLayerPort: () => readerNativeTopLayerPort,
	  visibleDiscourseNativeFloatingSurface: () => visibleDiscourseNativeFloatingSurface
	});
	module.exports = __toCommonJS(reader_native_composer_window_exports);
	var import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_event_target = require("../dom/event-target.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_preferences_schema = require("../state/reader-preferences-schema.js");
	const GEOMETRY_PROPERTIES = Object.freeze([
	  "--ldp-composer-left",
	  "--ldp-composer-top",
	  "--ldp-composer-width",
	  "--ldp-composer-height",
	  "--ldp-composer-transform"
	]), APPEARANCE_PROPERTIES = Object.freeze([
	  "--tertiary",
	  "--tertiary-low",
	  "--d-link-color"
	]), RESIZE_DIRECTIONS = Object.freeze([
	  "n",
	  "s",
	  "e",
	  "w",
	  "ne",
	  "nw",
	  "se",
	  "sw"
	]), DISCOURSE_NATIVE_FLOATING_SELECTOR = ".fk-d-menu,.emoji-picker";
	function discourseNativeFloatingSurfaceVisible(element, window = element.ownerDocument.defaultView) {
	  if (!element.isConnected || element.hidden || element.disabled || element.getAttribute("aria-hidden") === "true" || element.classList.contains("hidden") || element.classList.contains("d-none") || element.classList.contains("closed") || element.closest('[hidden],[aria-hidden="true"]')) return !1;
	  try {
	    const style = window?.getComputedStyle?.(element);
	    if (style?.display === "none" || style?.visibility === "hidden" || style?.contentVisibility === "hidden") return !1;
	  } catch {
	  }
	  const rect = element.getBoundingClientRect(), viewportWidth = Number(window?.innerWidth) || Number(element.ownerDocument.documentElement.clientWidth) || 0, viewportHeight = Number(window?.innerHeight) || Number(element.ownerDocument.documentElement.clientHeight) || 0;
	  return rect.width > 0 && rect.height > 0 && rect.right > 0 && rect.bottom > 0 && (viewportWidth <= 0 || rect.left < viewportWidth) && (viewportHeight <= 0 || rect.top < viewportHeight);
	}
	function visibleDiscourseNativeFloatingSurface(document, window = document.defaultView) {
	  return [...document.querySelectorAll(
	    DISCOURSE_NATIVE_FLOATING_SELECTOR
	  )].find(
	    (element) => discourseNativeFloatingSurfaceVisible(element, window)
	  ) ?? null;
	}
	function clamp(value, minimum, maximum) {
	  return Math.max(minimum, Math.min(maximum, value));
	}
	function finite(value, fallback) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? numeric : fallback;
	}
	function pixels(value) {
	  return `${Math.round(value * 100) / 100}px`;
	}
	function viewportLimits(viewport) {
	  const width = Math.max(1, finite(viewport.width, 1)), height = Math.max(1, finite(viewport.height, 1)), margin = Math.max(0, Math.min(
	    16,
	    Math.floor((width - 1) / 2),
	    Math.floor((height - 1) / 2)
	  )), maxWidth = Math.max(1, width - margin * 2), maxHeight = Math.max(1, height - margin * 2);
	  return Object.freeze({
	    left: margin,
	    top: margin,
	    right: width - margin,
	    bottom: height - margin,
	    maxWidth,
	    maxHeight,
	    minWidth: Math.min(520, maxWidth),
	    minHeight: Math.min(300, maxHeight)
	  });
	}
	function normalizeReaderNativeComposerGeometry(viewport, value) {
	  const bounds = viewportLimits(viewport), fallbackWidth = Math.min(1200, bounds.maxWidth), fallbackHeight = clamp(
	    bounds.maxHeight * 0.7,
	    bounds.minHeight,
	    bounds.maxHeight
	  ), fallbackLeft = bounds.left + (bounds.maxWidth - fallbackWidth) / 2, fallbackTop = bounds.top + (bounds.maxHeight - fallbackHeight) / 2, width = clamp(
	    finite(value?.width, fallbackWidth),
	    bounds.minWidth,
	    bounds.maxWidth
	  ), height = clamp(
	    finite(value?.height, fallbackHeight),
	    bounds.minHeight,
	    bounds.maxHeight
	  );
	  return Object.freeze({
	    left: clamp(
	      finite(value?.left, fallbackLeft),
	      bounds.left,
	      bounds.right - width
	    ),
	    top: clamp(
	      finite(value?.top, fallbackTop),
	      bounds.top,
	      bounds.bottom - height
	    ),
	    width,
	    height
	  });
	}
	function readerNativeComposerFontPixels(viewport, geometry, profile) {
	  const bounds = viewportLimits(viewport), widthTarget = Math.min(980, bounds.maxWidth), heightTarget = Math.min(720, bounds.maxHeight), widthProgress = clamp(
	    (geometry.width - bounds.minWidth) / Math.max(1, widthTarget - bounds.minWidth),
	    0,
	    1
	  ), heightProgress = clamp(
	    (geometry.height - bounds.minHeight) / Math.max(1, heightTarget - bounds.minHeight),
	    0,
	    1
	  ), automatic = 12 + Math.min(widthProgress, heightProgress) * 4, ratio = finite(profile.composer, import_reader_preferences_schema.READER_FONT_DEFAULT.composer) / import_reader_preferences_schema.READER_FONT_DEFAULT.composer;
	  return Math.round(clamp(automatic * ratio, 8, 40) * 100) / 100;
	}
	function readerNativeTopLayerPort() {
	  return Object.freeze({
	    isOpen: (element) => {
	      try {
	        return element.matches(":popover-open");
	      } catch {
	        return !1;
	      }
	    },
	    show: (element) => element.showPopover?.(),
	    hide: (element) => element.hidePopover?.()
	  });
	}
	class ReaderNativeComposerWindowController {
	  scope;
	  #document;
	  #window;
	  #mount;
	  #pageRoot;
	  #readPreferences;
	  #updatePreferences;
	  #readFontProfile;
	  #readAppearance;
	  #createMutationObserver;
	  #requestFrame;
	  #cancelFrame;
	  #setTimer;
	  #clearTimer;
	  #topLayer;
	  #onError;
	  #ownedTopLayers = /* @__PURE__ */ new Set();
	  #composer = null;
	  #composerRoot = null;
	  #composerAppearanceOriginal = /* @__PURE__ */ new Map();
	  #composerScope = null;
	  #hostObserver = null;
	  #floatingObserver = null;
	  #overflowLayers = [];
	  #chrome = null;
	  #geometry = null;
	  #pointer = null;
	  #pointerX = 0;
	  #pointerY = 0;
	  #pointerFrame = 0;
	  #chromeFrame = 0;
	  #overflowFrame = 0;
	  #floatingFrame = 0;
	  #persistTimer = 0;
	  #destroyed = !1;
	  constructor(options) {
	    this.#document = options.document, this.#window = options.window, this.#mount = options.mount, this.#pageRoot = options.pageRoot ?? options.document.documentElement, this.#readPreferences = options.readPreferences, this.#updatePreferences = options.updatePreferences ?? null, this.#readFontProfile = options.readFontProfile ?? (() => this.#readPreferences().fontProfile), this.#readAppearance = options.readAppearance ?? null, this.#createMutationObserver = options.createMutationObserver ?? ((callback) => new MutationObserver(callback)), this.#requestFrame = options.requestFrame ?? ((callback) => this.#window.requestAnimationFrame(callback)), this.#cancelFrame = options.cancelFrame ?? ((frameId) => this.#window.cancelAnimationFrame(frameId)), this.#setTimer = options.setTimer ?? ((callback, milliseconds) => this.#window.setTimeout(callback, milliseconds)), this.#clearTimer = options.clearTimer ?? ((timerId) => this.#window.clearTimeout(timerId)), this.#topLayer = options.topLayer ?? readerNativeTopLayerPort(), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => this.#teardown()), this.scope.listen(this.#window, "resize", () => {
	      this.#stopPointer(), this.#geometry ? this.#applyGeometry(this.#geometry) : this.#scheduleChrome(), this.#syncTopLayers(), this.#scheduleOverflow();
	    }), options.preferenceChanges.subscribe(() => {
	      this.#pointer || !this.#composerVisible() || (this.#geometry = this.#preferredGeometry(), this.#applyGeometry(this.#geometry));
	    }, this.scope), options.fontChanges?.subscribe((profile) => {
	      this.syncFont(profile);
	    }, this.scope), options.appearanceChanges?.subscribe((appearance) => {
	      this.#syncAppearance(appearance);
	    }, this.scope), this.#document.body && (this.#hostObserver = this.#createMutationObserver(() => {
	      this.sync(), this.#observeHostTargets();
	    }), this.#observeHostTargets(), this.scope.add(() => this.#hostObserver?.disconnect())), this.sync();
	  }
	  get geometry() {
	    return this.#geometry;
	  }
	  open(element = null) {
	    if (this.#destroyed) return !1;
	    const candidate = element ?? this.#document.querySelector("#reply-control");
	    return candidate !== this.#composer && this.#bindComposer(candidate), this.#composerAvailable() ? (this.#activate(), !0) : (this.#deactivate(), !1);
	  }
	  sync() {
	    if (this.#destroyed) return;
	    const candidate = this.#document.querySelector("#reply-control");
	    if (candidate !== this.#composer && this.#bindComposer(candidate), !this.#composerVisible()) {
	      this.#deactivate();
	      return;
	    }
	    this.#activate();
	  }
	  syncFont(profile = this.#readFontProfile()) {
	    !this.#composer || !this.#geometry || this.#composer.style.setProperty(
	      "--ldp-composer-font-size",
	      `${readerNativeComposerFontPixels(
	        this.#viewport(),
	        this.#geometry,
	        profile
	      )}px`
	    );
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #viewport() {
	    return Object.freeze({
	      width: this.#window.innerWidth,
	      height: this.#window.innerHeight
	    });
	  }
	  #desktop() {
	    return this.#window.innerWidth > 760;
	  }
	  #preferredGeometry() {
	    const preferences = this.#readPreferences(), preferred = {
	      ...preferences.composerWindowWidth ? { width: preferences.composerWindowWidth } : {},
	      ...preferences.composerWindowHeight ? { height: preferences.composerWindowHeight } : {},
	      ...preferences.composerWindowX ? { left: preferences.composerWindowX } : {},
	      ...preferences.composerWindowY ? { top: preferences.composerWindowY } : {}
	    };
	    return normalizeReaderNativeComposerGeometry(
	      this.#viewport(),
	      preferred
	    );
	  }
	  #composerVisible() {
	    const composer = this.#composer;
	    if (!composer || !this.#composerAvailable()) return !1;
	    try {
	      const style = this.#window.getComputedStyle?.(composer), stagedByReader = style?.visibility === "hidden" && this.#pageRoot.classList.contains("ldp-reader-open") && !composer.dataset.ldpReaderComposerPositioned;
	      if (style?.display === "none" || style?.visibility === "hidden" && !stagedByReader)
	        return !1;
	    } catch {
	    }
	    return !0;
	  }
	  #composerAvailable() {
	    const composer = this.#composer;
	    return !!(composer?.isConnected && !composer.hidden && !composer.classList.contains("closed") && !composer.classList.contains("hidden") && !composer.classList.contains("d-none") && composer.getAttribute("aria-hidden") !== "true" && !composer.closest('[hidden],[aria-hidden="true"]'));
	  }
	  #bindComposer(composer) {
	    if (this.#composerScope?.destroy(), this.#composerScope = null, this.#composer && this.#deactivate(), this.#composer = composer, !composer) return;
	    const scope = this.scope.child();
	    this.#composerScope = scope;
	    const observer = this.#createMutationObserver(() => this.sync());
	    observer.observe(composer, {
	      attributes: !0,
	      attributeFilter: ["class", "hidden", "aria-hidden"],
	      childList: !0,
	      subtree: !0
	    }), scope.add(() => observer.disconnect()), scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(composer, { capture: !0 })), scope.add(() => {
	      this.#composer === composer && (this.#deactivate(), this.#composer = null);
	    });
	  }
	  #observeHostTargets() {
	    const observer = this.#hostObserver, body = this.#document.body;
	    if (!observer || !body) return;
	    if (observer.disconnect(), !this.#composer) {
	      const bootstrap = this.#document.querySelector("#ember-app") ?? body;
	      observer.observe(bootstrap, { childList: !0, subtree: !0 });
	      let ancestor2 = bootstrap.parentElement;
	      for (; ancestor2 && (observer.observe(ancestor2, { childList: !0 }), ancestor2 !== body); )
	        ancestor2 = ancestor2.parentElement;
	      return;
	    }
	    let ancestor = this.#composer.parentElement;
	    for (; ancestor && (observer.observe(ancestor, { childList: !0 }), ancestor !== body); )
	      ancestor = ancestor.parentElement;
	  }
	  #activate() {
	    const composer = this.#composer;
	    composer && (this.#ensureChrome(), this.#syncComposerRoot(), this.#applyGeometry(this.#geometry ?? this.#preferredGeometry()), composer.classList.contains("fullscreen") && (this.#stopPointer(), composer.style.removeProperty("translate")), this.#syncTopLayers(), this.#scheduleOverflow());
	  }
	  #deactivate() {
	    this.#stopPointer(), this.#persistGeometry(), this.#releaseTopLayers(), this.#chrome && (this.#chrome.hidden = !0), this.#geometry = null, this.#clearGeometry(), this.#clearOverflow(), this.#clearComposerRoot();
	  }
	  #applyGeometry(value) {
	    const composer = this.#composer;
	    if (!composer) return;
	    const previous = this.#geometry;
	    this.#geometry = normalizeReaderNativeComposerGeometry(
	      this.#viewport(),
	      value
	    );
	    const sizeChanged = !previous || previous.width !== this.#geometry.width || previous.height !== this.#geometry.height;
	    previous || (composer.style.setProperty("--ldp-composer-left", "0px"), composer.style.setProperty("--ldp-composer-top", "0px"), composer.style.setProperty("--ldp-composer-transform", "none")), sizeChanged && (composer.style.setProperty(
	      "--ldp-composer-width",
	      pixels(this.#geometry.width)
	    ), composer.style.setProperty(
	      "--ldp-composer-height",
	      pixels(this.#geometry.height)
	    )), this.#desktop() && !composer.classList.contains("fullscreen") ? composer.style.setProperty(
	      "translate",
	      `${pixels(this.#geometry.left)} ${pixels(this.#geometry.top)}`
	    ) : composer.style.removeProperty("translate"), composer.dataset.ldpReaderComposerPositioned = "1", sizeChanged && this.syncFont(), this.#syncChrome(sizeChanged);
	  }
	  #clearGeometry() {
	    const composer = this.#composer;
	    if (composer) {
	      delete composer.dataset.ldpReaderComposerPositioned;
	      for (const property of GEOMETRY_PROPERTIES)
	        composer.style.removeProperty(property);
	      composer.style.removeProperty("--ldp-composer-font-size"), composer.style.removeProperty("translate");
	    }
	  }
	  #ensureChrome() {
	    if (this.#chrome) return this.#chrome;
	    const chrome = this.#document.createElement("div");
	    chrome.className = "ldp-composer-window-chrome sciapp-ldp-owned", chrome.hidden = !0;
	    const drag = this.#document.createElement("button");
	    drag.type = "button", drag.className = "ldp-composer-drag-handle", drag.setAttribute(
	      "aria-label",
	      "拖动回复窗口;方向键可微调位置"
	    ), chrome.append(drag);
	    for (const direction of RESIZE_DIRECTIONS) {
	      const handle = this.#document.createElement(
	        direction === "se" ? "button" : "span"
	      );
	      handle.className = "ldp-composer-resize-handle", handle.dataset.resize = direction, direction === "se" ? (handle.type = "button", handle.setAttribute(
	        "aria-label",
	        "拖动调整回复窗口大小;方向键可微调"
	      )) : handle.setAttribute("aria-hidden", "true"), chrome.append(handle);
	    }
	    this.#mount.append(chrome), this.scope.listen(chrome, "pointerdown", (event) => {
	      const pointer = event, eventTarget = (0, import_event_target.eventElement)(pointer), target = eventTarget ? eventTarget.closest(
	        ".ldp-composer-drag-handle,[data-resize]"
	      ) : null;
	      !target || !chrome.contains(target) || this.#startPointer(
	        pointer,
	        target.dataset.resize ?? "move",
	        target
	      );
	    }), this.scope.listen(chrome, "pointermove", (event) => {
	      this.#movePointer(event);
	    });
	    for (const type of ["pointerup", "pointercancel", "lostpointercapture"])
	      this.scope.listen(chrome, type, (event) => {
	        this.#stopPointer(event);
	      });
	    return this.scope.listen(chrome, "keydown", (event) => {
	      this.#handleChromeKey(event);
	    }), this.#chrome = chrome, chrome;
	  }
	  #syncChrome(syncSize = !1) {
	    const chrome = this.#chrome, composer = this.#composer, geometry = this.#geometry;
	    if (!chrome) return;
	    const available = !!(geometry && this.#desktop() && this.#composerVisible() && !composer?.classList.contains("fullscreen"));
	    chrome.hidden = !available, !(!available || !geometry) && (syncSize && (chrome.style.setProperty("--ldp-composer-chrome-width", pixels(geometry.width)), chrome.style.setProperty("--ldp-composer-chrome-height", pixels(geometry.height))), chrome.style.transform = `translate3d(${pixels(geometry.left)},${pixels(geometry.top)},0)`);
	  }
	  #scheduleChrome() {
	    this.#chromeFrame || (this.#chromeFrame = this.#requestFrame(() => {
	      this.#chromeFrame = 0, this.#syncChrome();
	    }));
	  }
	  #startPointer(event, mode, target) {
	    !this.#geometry || !this.#desktop() || !this.#composerVisible() || this.#composer?.classList.contains("fullscreen") || event.button !== 0 || (this.#pointer = Object.freeze({
	      id: event.pointerId,
	      mode,
	      startX: event.clientX,
	      startY: event.clientY,
	      geometry: this.#geometry,
	      target
	    }), this.#pointerX = event.clientX, this.#pointerY = event.clientY, target.setPointerCapture?.(event.pointerId), this.#chrome?.classList.add("is-interacting"), this.#pageRoot.classList.add("ldp-composer-window-interacting"), event.preventDefault(), event.stopPropagation());
	  }
	  #movePointer(event) {
	    !this.#pointer || event.pointerId !== this.#pointer.id || (this.#pointerX = event.clientX, this.#pointerY = event.clientY, this.#pointerFrame || (this.#pointerFrame = this.#requestFrame(() => {
	      this.#pointerFrame = 0, this.#applyPointer();
	    })), event.preventDefault());
	  }
	  #stopPointer(event) {
	    const pointer = this.#pointer;
	    if (!pointer) {
	      this.#chrome?.classList.remove("is-interacting"), this.#pageRoot.classList.remove("ldp-composer-window-interacting");
	      return;
	    }
	    if (!(event && event.pointerId !== pointer.id)) {
	      event && Number.isFinite(event.clientX) && Number.isFinite(event.clientY) && (this.#pointerX = event.clientX, this.#pointerY = event.clientY), this.#pointerFrame && this.#cancelFrame(this.#pointerFrame), this.#pointerFrame = 0, this.#applyPointer(), this.#pointer = null;
	      try {
	        pointer.target.hasPointerCapture?.(pointer.id) && pointer.target.releasePointerCapture?.(pointer.id);
	      } catch {
	      }
	      this.#chrome?.classList.remove("is-interacting"), this.#pageRoot.classList.remove("ldp-composer-window-interacting"), this.#scheduleOverflow(), this.#persistGeometry();
	    }
	  }
	  #applyPointer() {
	    const pointer = this.#pointer;
	    if (!pointer) return;
	    const deltaX = this.#pointerX - pointer.startX, deltaY = this.#pointerY - pointer.startY;
	    if (pointer.mode === "move") {
	      const bounds = viewportLimits(this.#viewport());
	      this.#applyGeometry({
	        ...pointer.geometry,
	        left: clamp(
	          pointer.geometry.left + deltaX,
	          bounds.left,
	          bounds.right - pointer.geometry.width
	        ),
	        top: clamp(
	          pointer.geometry.top + deltaY,
	          bounds.top,
	          bounds.bottom - pointer.geometry.height
	        )
	      });
	      return;
	    }
	    this.#applyGeometry(this.#resizeGeometry(
	      pointer.geometry,
	      pointer.mode,
	      deltaX,
	      deltaY
	    ));
	  }
	  #resizeGeometry(start, direction, deltaX, deltaY) {
	    const bounds = viewportLimits(this.#viewport());
	    let left = start.left, top = start.top, right = start.left + start.width, bottom = start.top + start.height;
	    return direction.includes("w") && (left = clamp(start.left + deltaX, bounds.left, right - bounds.minWidth)), direction.includes("e") && (right = clamp(start.left + start.width + deltaX, left + bounds.minWidth, bounds.right)), direction.includes("n") && (top = clamp(start.top + deltaY, bounds.top, bottom - bounds.minHeight)), direction.includes("s") && (bottom = clamp(start.top + start.height + deltaY, top + bounds.minHeight, bounds.bottom)), Object.freeze({
	      left,
	      top,
	      width: right - left,
	      height: bottom - top
	    });
	  }
	  #handleChromeKey(event) {
	    if (!this.#geometry || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
	    const target = (0, import_event_target.eventElement)(event), step = event.shiftKey ? 40 : 12;
	    if (target?.matches(".ldp-composer-drag-handle")) {
	      const next = { ...this.#geometry };
	      event.key === "ArrowLeft" ? next.left -= step : event.key === "ArrowRight" ? next.left += step : event.key === "ArrowUp" ? next.top -= step : next.top += step, this.#applyGeometry(next);
	    } else if (target?.matches('[data-resize="se"]'))
	      this.#applyGeometry(this.#resizeGeometry(
	        this.#geometry,
	        "se",
	        event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0,
	        event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0
	      ));
	    else
	      return;
	    this.#schedulePersist(), event.preventDefault();
	  }
	  #schedulePersist() {
	    this.#persistTimer && this.#clearTimer(this.#persistTimer), this.#persistTimer = this.#setTimer(() => {
	      this.#persistTimer = 0, this.#persistGeometry();
	    }, 160);
	  }
	  #persistGeometry() {
	    if (this.#persistTimer && this.#clearTimer(this.#persistTimer), this.#persistTimer = 0, !(!this.#geometry || !this.#desktop() || !this.#updatePreferences))
	      try {
	        this.#updatePreferences({
	          composerWindowWidth: Math.round(this.#geometry.width),
	          composerWindowHeight: Math.round(this.#geometry.height),
	          composerWindowX: Math.round(this.#geometry.left),
	          composerWindowY: Math.round(this.#geometry.top)
	        });
	      } catch (cause) {
	        this.#onError(cause);
	      }
	  }
	  #syncComposerRoot() {
	    let next = this.#composer;
	    for (; next?.parentElement && next.parentElement !== this.#document.body; )
	      next = next.parentElement;
	    if ((!next || next.parentElement !== this.#document.body) && (next = null), next !== this.#composerRoot && (this.#clearComposerRoot(), this.#composerRoot = next, next)) {
	      next.dataset.ldpReaderComposerRoot = "1";
	      for (const property of APPEARANCE_PROPERTIES) {
	        const priorityReader = next.style;
	        this.#composerAppearanceOriginal.set(property, Object.freeze({
	          value: next.style.getPropertyValue(property),
	          priority: typeof priorityReader.getPropertyPriority == "function" ? priorityReader.getPropertyPriority(property) : ""
	        }));
	      }
	      this.#syncAppearance();
	    }
	  }
	  #clearComposerRoot() {
	    if (this.#composerRoot) {
	      for (const [property, previous] of this.#composerAppearanceOriginal)
	        previous.value ? this.#composerRoot.style.setProperty(
	          property,
	          previous.value,
	          previous.priority
	        ) : this.#composerRoot.style.removeProperty(property);
	      delete this.#composerRoot.dataset.ldpReaderComposerRoot;
	    }
	    this.#composerAppearanceOriginal.clear(), this.#composerRoot = null;
	  }
	  #syncAppearance(appearance = this.#readAppearance?.() ?? null) {
	    !this.#composerRoot || !appearance || (this.#composerRoot.style.setProperty(
	      "--tertiary",
	      appearance.accentColor
	    ), this.#composerRoot.style.setProperty(
	      "--tertiary-low",
	      appearance.accentLowColor
	    ), this.#composerRoot.style.setProperty(
	      "--d-link-color",
	      appearance.linkColor
	    ));
	  }
	  #scheduleOverflow() {
	    this.#overflowFrame || (this.#overflowFrame = this.#requestFrame(() => {
	      this.#overflowFrame = 0, this.#syncOverflow();
	    }));
	  }
	  #syncOverflow() {
	    this.#clearOverflow(!1);
	    const composer = this.#composer;
	    if (!composer || !this.#composerVisible()) return;
	    const composerRect = composer.getBoundingClientRect?.();
	    if (!composerRect || composerRect.width <= 0 || composerRect.height <= 0) return;
	    const candidates = [composer, ...composer.querySelectorAll("*")].map((candidate) => {
	      if (candidate.clientWidth <= 0 || candidate.scrollWidth <= candidate.clientWidth + 1 || candidate.closest('.d-editor-button-bar,[role="toolbar"]')) return null;
	      let overflowX = "";
	      try {
	        overflowX = this.#window.getComputedStyle(candidate).overflowX;
	      } catch {
	        return null;
	      }
	      if (!/(auto|scroll)/.test(overflowX)) return null;
	      const rect = candidate.getBoundingClientRect(), coveredWidth = Math.max(0, Math.min(rect.right, composerRect.right) - Math.max(rect.left, composerRect.left)), coveredHeight = Math.max(0, Math.min(rect.bottom, composerRect.bottom) - Math.max(rect.top, composerRect.top)), widthCoverage = coveredWidth / composerRect.width, heightCoverage = coveredHeight / composerRect.height;
	      return widthCoverage >= 0.85 && heightCoverage >= 0.75 ? Object.freeze({
	        candidate,
	        coverage: widthCoverage * heightCoverage
	      }) : null;
	    }).filter((value) => value !== null).sort((left, right) => right.coverage - left.coverage);
	    this.#overflowLayers = candidates.length ? [candidates[0].candidate] : [];
	    for (const layer of this.#overflowLayers)
	      layer.dataset.ldpReaderComposerOverflow = "1";
	  }
	  #clearOverflow(cancelFrame = !0) {
	    cancelFrame && this.#overflowFrame && this.#cancelFrame(this.#overflowFrame), cancelFrame && (this.#overflowFrame = 0);
	    for (const layer of this.#overflowLayers)
	      delete layer.dataset.ldpReaderComposerOverflow;
	    this.#overflowLayers = [];
	  }
	  #promoteTopLayer(element, kind) {
	    if (element.hasAttribute("popover") && !this.#ownedTopLayers.has(element))
	      return this.#topLayer.isOpen(element);
	    this.#ownedTopLayers.has(element) || (element.setAttribute("popover", "manual"), element.dataset.ldpReaderTopLayer = kind, this.#ownedTopLayers.add(element));
	    try {
	      if (this.#topLayer.isOpen(element) || this.#topLayer.show(element), this.#topLayer.isOpen(element)) return !0;
	    } catch (cause) {
	      this.#onError(cause);
	    }
	    return this.#releaseTopLayer(element), !1;
	  }
	  #releaseTopLayer(element) {
	    if (this.#ownedTopLayers.delete(element)) {
	      try {
	        this.#topLayer.isOpen(element) && this.#topLayer.hide(element);
	      } catch (cause) {
	        this.#onError(cause);
	      }
	      element.removeAttribute("popover"), delete element.dataset.ldpReaderTopLayer;
	    }
	  }
	  #syncTopLayers() {
	    const composer = this.#composer;
	    if (!composer || !this.#composerVisible()) {
	      this.#releaseTopLayers();
	      return;
	    }
	    if (!this.#promoteTopLayer(composer, "composer")) {
	      this.#releaseFloatingTopLayers();
	      return;
	    }
	    this.#chrome && !this.#chrome.hidden && this.#promoteTopLayer(this.#chrome, "chrome"), this.#startFloatingObservation(), this.#scheduleFloating();
	  }
	  #startFloatingObservation() {
	    this.#floatingObserver || !this.#document.body || (this.#floatingObserver = this.#createMutationObserver((mutations) => {
	      mutations.some((mutation) => this.#floatingMutationRelevant(mutation)) && this.#scheduleFloating();
	    }), this.#floatingObserver.observe(this.#document.body, {
	      childList: !0,
	      subtree: !0,
	      attributes: !0,
	      attributeFilter: ["class", "style", "hidden", "aria-hidden", "open"]
	    }));
	  }
	  #floatingMutationRelevant(mutation) {
	    return (mutation.type === "attributes" ? [mutation.target] : [...mutation.addedNodes, ...mutation.removedNodes]).some((node) => {
	      const element = node.nodeType === 1 ? node : node.parentNode?.nodeType === 1 ? node.parentNode : null;
	      return !!(element?.matches(DISCOURSE_NATIVE_FLOATING_SELECTOR) || element?.closest(DISCOURSE_NATIVE_FLOATING_SELECTOR) || element?.querySelector(DISCOURSE_NATIVE_FLOATING_SELECTOR));
	    });
	  }
	  #scheduleFloating() {
	    this.#floatingFrame || (this.#floatingFrame = this.#requestFrame(() => {
	      this.#floatingFrame = 0, this.#syncFloatingTopLayers();
	    }));
	  }
	  #syncFloatingTopLayers() {
	    const composer = this.#composer;
	    if (!composer || !this.#topLayer.isOpen(composer)) return;
	    const visible = [...this.#document.querySelectorAll(
	      DISCOURSE_NATIVE_FLOATING_SELECTOR
	    )].filter((element) => !(element.closest("#reply-control") || element.contains(composer) || element.closest("dialog[open]") || !discourseNativeFloatingSurfaceVisible(element, this.#window))), surfaces = visible.filter((element) => !visible.some((parent) => parent !== element && parent.contains(element)));
	    for (const element of [...this.#ownedTopLayers])
	      element.dataset.ldpReaderTopLayer === "portal" && !surfaces.includes(element) && this.#releaseTopLayer(element);
	    for (const surface of surfaces) this.#promoteTopLayer(surface, "portal");
	  }
	  #releaseFloatingTopLayers() {
	    this.#floatingFrame && this.#cancelFrame(this.#floatingFrame), this.#floatingFrame = 0, this.#floatingObserver?.disconnect(), this.#floatingObserver = null;
	    for (const element of [...this.#ownedTopLayers])
	      element.dataset.ldpReaderTopLayer === "portal" && this.#releaseTopLayer(element);
	  }
	  #releaseTopLayers() {
	    this.#releaseFloatingTopLayers();
	    for (const element of [...this.#ownedTopLayers].reverse())
	      this.#releaseTopLayer(element);
	  }
	  #teardown() {
	    if (!this.#destroyed) {
	      this.#destroyed = !0, this.#stopPointer(), this.#persistGeometry(), this.#composerScope?.destroy(), this.#composerScope = null, this.#hostObserver?.disconnect(), this.#hostObserver = null, this.#releaseTopLayers();
	      for (const frame of [
	        this.#pointerFrame,
	        this.#chromeFrame,
	        this.#overflowFrame,
	        this.#floatingFrame
	      ])
	        frame && this.#cancelFrame(frame);
	      this.#pointerFrame = 0, this.#chromeFrame = 0, this.#overflowFrame = 0, this.#floatingFrame = 0, this.#persistTimer && this.#clearTimer(this.#persistTimer), this.#persistTimer = 0, this.#pageRoot.classList.remove(
	        "ldp-composer-window-interacting"
	      ), this.#clearGeometry(), this.#clearOverflow(), this.#clearComposerRoot(), this.#chrome?.remove(), this.#chrome = null, this.#composer = null, this.#geometry = null;
	    }
	  }
	}
}, "7d6bc57aadd215f81aea59593a589569369ea0826545f18133f7b4fc44597805");

/* Source: lite/src/discourse/reader-native-post-admin-menu.ts */
runtime.register("src/discourse/reader-native-post-admin-menu.js", function(module, exports, require) {
	var reader_native_post_admin_menu_exports = {};
	__export(reader_native_post_admin_menu_exports, {
	  positionReaderNativePostAdminMenu: () => positionReaderNativePostAdminMenu
	});
	module.exports = __toCommonJS(reader_native_post_admin_menu_exports);
	var import_reader_native_composer_window = require("./reader-native-composer-window.js");
	const ADMIN_MENU_SELECTOR = '.fk-d-menu[data-identifier="admin-post-menu"]', ADMIN_MENU_GAP_PX = 8, ADMIN_MENU_BOUNDARY_PADDING_PX = 8, ADMIN_MENU_FALLBACK_WIDTH_PX = 240, ADMIN_MENU_FALLBACK_HEIGHT_PX = 48;
	function positive(value, fallback) {
	  return Number.isFinite(value) && value > 0 ? value : fallback;
	}
	function clamp(value, minimum, maximum) {
	  return Math.max(minimum, Math.min(maximum, value));
	}
	function positionReaderNativePostAdminMenu(options) {
	  const { document, reader, anchor, content } = options, surface = content.closest(ADMIN_MENU_SELECTOR);
	  if (!surface || !surface.isConnected || !reader.isConnected || !anchor.isConnected) return !1;
	  surface.dataset.ldpReaderAdminMenu = "positioned";
	  const topLayer = options.topLayer ?? (0, import_reader_native_composer_window.readerNativeTopLayerPort)();
	  if (!surface.hasAttribute("popover")) {
	    surface.setAttribute("popover", "manual"), surface.dataset.ldpReaderTopLayer = "portal";
	    try {
	      topLayer.show(surface);
	    } catch {
	    }
	    topLayer.isOpen(surface) || (surface.removeAttribute("popover"), delete surface.dataset.ldpReaderTopLayer);
	  }
	  const viewport = document.documentElement, readerRect = reader.getBoundingClientRect(), anchorRect = anchor.getBoundingClientRect(), surfaceRect = surface.getBoundingClientRect(), viewportWidth = positive(
	    viewport.clientWidth,
	    positive(document.defaultView?.innerWidth ?? 0, readerRect.right)
	  ), viewportHeight = positive(
	    viewport.clientHeight,
	    positive(document.defaultView?.innerHeight ?? 0, readerRect.bottom)
	  ), readerLeft = readerRect.width > 0 ? readerRect.left : 0, readerRight = readerRect.width > 0 ? readerRect.right : viewportWidth, readerTop = readerRect.height > 0 ? readerRect.top : 0, readerBottom = readerRect.height > 0 ? readerRect.bottom : viewportHeight, leftBound = Math.max(
	    ADMIN_MENU_BOUNDARY_PADDING_PX,
	    readerLeft + ADMIN_MENU_BOUNDARY_PADDING_PX
	  ), rightBound = Math.max(
	    leftBound,
	    Math.min(
	      viewportWidth - ADMIN_MENU_BOUNDARY_PADDING_PX,
	      readerRight - ADMIN_MENU_BOUNDARY_PADDING_PX
	    )
	  ), topBound = Math.max(
	    ADMIN_MENU_BOUNDARY_PADDING_PX,
	    readerTop + ADMIN_MENU_BOUNDARY_PADDING_PX
	  ), bottomBound = Math.max(
	    topBound,
	    Math.min(
	      viewportHeight - ADMIN_MENU_BOUNDARY_PADDING_PX,
	      readerBottom - ADMIN_MENU_BOUNDARY_PADDING_PX
	    )
	  ), width = Math.min(
	    positive(surfaceRect.width, ADMIN_MENU_FALLBACK_WIDTH_PX),
	    Math.max(1, rightBound - leftBound)
	  ), height = Math.min(
	    positive(surfaceRect.height, ADMIN_MENU_FALLBACK_HEIGHT_PX),
	    Math.max(1, bottomBound - topBound)
	  ), rightCandidate = anchorRect.right + ADMIN_MENU_GAP_PX, leftCandidate = anchorRect.left - ADMIN_MENU_GAP_PX - width, left = rightCandidate + width <= rightBound ? rightCandidate : leftCandidate >= leftBound ? leftCandidate : clamp(anchorRect.left, leftBound, Math.max(leftBound, rightBound - width)), top = clamp(
	    anchorRect.top,
	    topBound,
	    Math.max(topBound, bottomBound - height)
	  );
	  return surface.style.setProperty(
	    "--ldp-reader-admin-menu-left",
	    `${Math.round(left)}px`
	  ), surface.style.setProperty(
	    "--ldp-reader-admin-menu-top",
	    `${Math.round(top)}px`
	  ), !0;
	}
}, "0be025740cd022ddac5632878106f27ed3be3d12861f238973a0ac991a6058cd");

/* Source: lite/src/history/reader-chronicle-repository.ts */
runtime.register("src/history/reader-chronicle-repository.js", function(module, exports, require) {
	var reader_chronicle_repository_exports = {};
	__export(reader_chronicle_repository_exports, {
	  READER_CHRONICLE_MAX_AGE_MS: () => READER_CHRONICLE_MAX_AGE_MS,
	  READER_CHRONICLE_MAX_RECORDS: () => READER_CHRONICLE_MAX_RECORDS,
	  READER_CHRONICLE_STORAGE_KEY: () => READER_CHRONICLE_STORAGE_KEY,
	  ReaderChronicleRepository: () => ReaderChronicleRepository,
	  mergeReaderChronicleValues: () => mergeReaderChronicleValues,
	  readerChronicleRecord: () => readerChronicleRecord,
	  readerChronicleRequestTarget: () => readerChronicleRequestTarget
	});
	module.exports = __toCommonJS(reader_chronicle_repository_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_signal = require("../kernel/signal.js"), import_reader_account_scoped_storage = require("../state/reader-account-scoped-storage.js");
	const READER_CHRONICLE_STORAGE_KEY = "linuxdo-enhanced-reader:chronicle", READER_CHRONICLE_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1e3, READER_CHRONICLE_MAX_RECORDS = 1e3;
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function text(value, maximum = 240) {
	  return String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
	}
	function positiveInteger(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function timestamp(value) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
	}
	function method(value) {
	  const normalized = text(value, 16).toUpperCase();
	  return /^[A-Z]+$/.test(normalized) ? normalized : "GET";
	}
	function kind(value) {
	  return value === "topic" || value === "reply" || value === "boost" ? value : null;
	}
	function hash(value) {
	  let result = 2166136261;
	  for (let index = 0; index < value.length; index += 1)
	    result ^= value.charCodeAt(index), result = Math.imul(result, 16777619);
	  return (result >>> 0).toString(36);
	}
	function chronicleIdentity(input) {
	  const target = input.kind === "topic" ? "topic" : input.kind === "boost" ? `boost-${input.boostId ?? 0}` : `post-${input.postNumber ?? 0}-${input.postId ?? 0}`;
	  return `${input.kind}:${input.topicId}:${target}:${hash(
	    `${input.requestMethod}:${input.requestPath}`
	  )}`;
	}
	function searchText(input) {
	  const label = input.kind === "topic" ? "主题 帖子 Topic" : input.kind === "reply" ? "回复 楼层 Post" : "Boost";
	  return [
	    input.topicTitle,
	    `Topic ${input.topicId}`,
	    label,
	    input.postNumber === null ? "" : `楼层 ${input.postNumber}`,
	    input.postId === null ? "" : `post ${input.postId}`,
	    input.boostId === null ? "" : `boost ${input.boostId}`,
	    "404",
	    input.requestMethod,
	    input.requestPath,
	    input.requestSource,
	    input.callSite
	  ].filter(Boolean).join(" ").toLocaleLowerCase("zh-CN");
	}
	function normalizedTitle(value, topicId) {
	  return text(value, 180) || `帖子 #${topicId}`;
	}
	function preferredTitle(left, right, topicId) {
	  const fallback = `帖子 #${topicId}`;
	  return right && right !== fallback ? right : left || right || fallback;
	}
	function normalizeRecord(value) {
	  const source = record(value), targetKind = kind(source?.kind), topicId = (0, import_identifiers.tryDiscourseTopicId)(source?.topicId);
	  if (!source || !targetKind || topicId === null || Number(source.status) !== 404 || source.bodyCached !== !0)
	    return null;
	  const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(source.postNumber), postId = (0, import_identifiers.tryDiscoursePostId)(source.postId), boostId = positiveInteger(source.boostId);
	  if (targetKind === "reply" && postNumber === null && postId === null || targetKind === "boost" && boostId === null) return null;
	  const requestPath = text(source.requestPath) || `/t/${topicId}`, requestMethod = method(source.requestMethod), firstObservedAt = timestamp(source.firstObservedAt), lastObservedAt = timestamp(source.lastObservedAt) || firstObservedAt;
	  if (!firstObservedAt || !lastObservedAt) return null;
	  const base = Object.freeze({
	    identity: "",
	    kind: targetKind,
	    status: 404,
	    bodyCached: !0,
	    topicId,
	    topicTitle: normalizedTitle(source.topicTitle, topicId),
	    postNumber,
	    postId,
	    boostId,
	    requestPath,
	    requestMethod,
	    requestSource: text(source.requestSource, 40) || "reader",
	    callSite: text(source.callSite, 220),
	    firstObservedAt: Math.min(firstObservedAt, lastObservedAt),
	    lastObservedAt: Math.max(firstObservedAt, lastObservedAt),
	    occurrences: Math.max(1, positiveInteger(source.occurrences) ?? 1)
	  }), normalized = Object.freeze({
	    ...base,
	    identity: chronicleIdentity(base)
	  });
	  return Object.freeze({
	    ...normalized,
	    searchText: searchText(normalized)
	  });
	}
	function inputRecord(input, now) {
	  const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), targetKind = kind(input.kind);
	  if (!targetKind) throw new Error("岁月史书记录类型无效");
	  if (input.status !== void 0 && Number(input.status) !== 404)
	    throw new Error("岁月史书只接受 HTTP 404 信号");
	  if (input.bodyCached !== !0)
	    throw new Error("岁月史书只接受本机仍有正文的 404");
	  const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(input.postNumber), postId = (0, import_identifiers.tryDiscoursePostId)(input.postId), boostId = positiveInteger(input.boostId);
	  if (targetKind === "reply" && postNumber === null && postId === null)
	    throw new Error("回复 404 必须包含楼层或 post.id");
	  if (targetKind === "boost" && boostId === null)
	    throw new Error("Boost 404 必须包含 boost.id");
	  const observedAt = timestamp(input.observedAt) || now, requestPath = text(input.requestPath) || `/t/${topicId}`, requestMethod = method(input.requestMethod), base = Object.freeze({
	    identity: "",
	    kind: targetKind,
	    status: 404,
	    bodyCached: !0,
	    topicId,
	    topicTitle: normalizedTitle(input.topicTitle, topicId),
	    postNumber,
	    postId,
	    boostId,
	    requestPath,
	    requestMethod,
	    requestSource: text(input.requestSource, 40) || "reader",
	    callSite: text(input.callSite, 220),
	    firstObservedAt: observedAt,
	    lastObservedAt: observedAt,
	    occurrences: 1
	  }), normalized = Object.freeze({
	    ...base,
	    identity: chronicleIdentity(base)
	  });
	  return Object.freeze({
	    ...normalized,
	    searchText: searchText(normalized)
	  });
	}
	function readerChronicleRecord(value) {
	  return normalizeRecord(value);
	}
	function mergeReaderChronicleValues(local, remote) {
	  const left = normalizeRecord(local), right = normalizeRecord(remote);
	  if (!left) return right;
	  if (!right) return left;
	  if (left.identity !== right.identity)
	    return left.lastObservedAt >= right.lastObservedAt ? left : right;
	  const recent = left.lastObservedAt >= right.lastObservedAt ? left : right, older = recent === left ? right : left;
	  return normalizeRecord({
	    ...older,
	    ...recent,
	    topicTitle: preferredTitle(
	      older.topicTitle,
	      recent.topicTitle,
	      recent.topicId
	    ),
	    firstObservedAt: Math.min(left.firstObservedAt, right.firstObservedAt),
	    lastObservedAt: Math.max(left.lastObservedAt, right.lastObservedAt),
	    occurrences: Math.max(left.occurrences, right.occurrences)
	  });
	}
	function readerChronicleRequestTarget(pathValue) {
	  const raw = text(pathValue, 512), slash = raw.indexOf("/"), path = slash >= 0 ? raw.slice(slash) : raw, boost = path.match(/\/(?:discourse-boosts\/)?boosts\/(\d+)(?:\.json)?(?:\/|$)/i);
	  if (boost)
	    return Object.freeze({
	      kind: "boost",
	      topicId: null,
	      postNumber: null,
	      postId: null,
	      boostId: positiveInteger(boost[1])
	    });
	  const byNumber = path.match(/\/posts\/by_number\/(\d+)\/(\d+)(?:\.json)?(?:\/|$)/i);
	  if (byNumber) {
	    const topicId = positiveInteger(byNumber[1]), postNumber = positiveInteger(byNumber[2]);
	    if (topicId && postNumber)
	      return Object.freeze({
	        kind: postNumber === 1 ? "topic" : "reply",
	        topicId,
	        postNumber,
	        postId: null,
	        boostId: null
	      });
	  }
	  const topicOffset = path.toLocaleLowerCase().indexOf("/t/");
	  if (topicOffset >= 0) {
	    const segments = path.slice(topicOffset + 3).split("/").filter(Boolean).map((segment) => segment.replace(/\.json$/i, "")), first = positiveInteger(segments[0]), topicId = first ?? positiveInteger(segments[1]), postNumber = positiveInteger(first ? segments[1] : segments[2]);
	    if (topicId)
	      return Object.freeze({
	        kind: postNumber && postNumber > 1 ? "reply" : "topic",
	        topicId,
	        postNumber,
	        postId: null,
	        boostId: null
	      });
	  }
	  const post = path.match(/\/posts\/(\d+)(?:\.json|\/replies(?:\.json)?)?(?:\/|$)/i);
	  return post ? Object.freeze({
	    kind: "reply",
	    topicId: null,
	    postNumber: null,
	    postId: positiveInteger(post[1]),
	    boostId: null
	  }) : null;
	}
	function quotaError(error) {
	  const source = error;
	  return source?.name === "QuotaExceededError" || source?.name === "NS_ERROR_DOM_QUOTA_REACHED" || source?.code === 22 || source?.code === 1014;
	}
	class ReaderChronicleRepository {
	  changes = new import_signal.Signal();
	  diagnostics = new import_signal.Signal();
	  #storage;
	  #key;
	  #accountStorage;
	  #maxAgeMs;
	  #maxRecords;
	  #now;
	  #snapshot = Object.freeze({
	    records: Object.freeze([]),
	    revision: 0,
	    source: "fallback"
	  });
	  constructor(options) {
	    if (this.#storage = options.storage, this.#accountStorage = options.key === void 0 && options.authScope !== void 0 ? (0, import_reader_account_scoped_storage.readerAccountScopedStorageIdentity)(
	      READER_CHRONICLE_STORAGE_KEY,
	      options.authScope
	    ) : null, this.#key = text(options.key ?? this.#accountStorage?.key ?? READER_CHRONICLE_STORAGE_KEY, 512), !this.#key) throw new Error("chronicle storage key 不能为空");
	    if (this.#maxAgeMs = Number(options.maxAgeMs ?? READER_CHRONICLE_MAX_AGE_MS), this.#maxRecords = Math.floor(Number(
	      options.maxRecords ?? READER_CHRONICLE_MAX_RECORDS
	    )), !Number.isFinite(this.#maxAgeMs) || this.#maxAgeMs <= 0)
	      throw new RangeError("chronicle maxAgeMs 必须是正有限数值");
	    if (!Number.isSafeInteger(this.#maxRecords) || this.#maxRecords <= 0)
	      throw new RangeError("chronicle maxRecords 必须是正安全整数");
	    this.#now = options.now ?? Date.now;
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  get storageKey() {
	    return this.#key;
	  }
	  load() {
	    return this.#readAndCommit("initial");
	  }
	  reloadExternal() {
	    return this.#readAndCommit("external-reload");
	  }
	  ordered() {
	    return this.#ordered(this.#snapshot.records);
	  }
	  remember(input) {
	    this.#mergeStoredBeforeMutation();
	    const incoming = inputRecord(input, this.#now()), previous = this.#snapshot.records.find((entry) => entry.identity === incoming.identity), next = previous ? normalizeRecord({
	      ...previous,
	      ...incoming,
	      topicTitle: preferredTitle(
	        previous.topicTitle,
	        incoming.topicTitle,
	        incoming.topicId
	      ),
	      firstObservedAt: previous.firstObservedAt,
	      lastObservedAt: Math.max(
	        previous.lastObservedAt,
	        incoming.lastObservedAt
	      ),
	      occurrences: previous.occurrences + 1
	    }) : incoming;
	    return this.#persistAndCommit([
	      next,
	      ...this.#snapshot.records.filter((entry) => entry.identity !== next.identity)
	    ], "remember");
	  }
	  remove(identity) {
	    this.#mergeStoredBeforeMutation();
	    const normalized = text(identity, 512);
	    return !normalized || !this.#snapshot.records.some((entry) => entry.identity === normalized) ? this.#snapshot : this.#persistAndCommit(
	      this.#snapshot.records.filter((entry) => entry.identity !== normalized),
	      "remove"
	    );
	  }
	  clear() {
	    return this.#mergeStoredBeforeMutation(), this.#persistAndCommit([], "clear");
	  }
	  replaceExternal(values) {
	    return this.#persistAndCommit(
	      this.#normalizeMany(values),
	      "external-sync"
	    );
	  }
	  #readAndCommit(source) {
	    let raw;
	    try {
	      const stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key);
	      raw = stored === null ? [] : JSON.parse(stored);
	    } catch (cause) {
	      return this.#diagnose("read-failed", cause), this.#commit([], "fallback");
	    }
	    if (!Array.isArray(raw))
	      return this.#diagnose(
	        "invalid-stored-value",
	        new TypeError("岁月史书存储值必须是数组")
	      ), this.#commit([], "fallback");
	    const normalized = this.#normalizeMany(raw);
	    if (JSON.stringify(normalized) !== JSON.stringify(raw)) {
	      this.#diagnose("records-normalized", Object.freeze({
	        storedCount: raw.length,
	        acceptedCount: normalized.length
	      }));
	      try {
	        this.#persist(normalized);
	      } catch {
	      }
	    }
	    return this.#commit(normalized, source);
	  }
	  #normalizeMany(values) {
	    const cutoff = this.#now() - this.#maxAgeMs, records = /* @__PURE__ */ new Map();
	    for (const value of values) {
	      const incoming = normalizeRecord(value);
	      if (!incoming || incoming.lastObservedAt < cutoff) continue;
	      const current = records.get(incoming.identity), merged = current ? mergeReaderChronicleValues(current, incoming) : incoming;
	      merged && records.set(merged.identity, merged);
	    }
	    return this.#ordered([...records.values()]);
	  }
	  #mergeStoredBeforeMutation() {
	    let stored;
	    try {
	      stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key);
	    } catch (cause) {
	      this.#diagnose("read-failed", cause);
	      return;
	    }
	    (stored ?? "[]") !== JSON.stringify(this.#snapshot.records) && this.#readAndCommit("external-reload");
	  }
	  #ordered(values) {
	    return Object.freeze([...values].sort((left, right) => right.lastObservedAt - left.lastObservedAt || right.topicId - left.topicId || left.identity.localeCompare(right.identity)).slice(0, this.#maxRecords));
	  }
	  #persistAndCommit(records, source) {
	    return this.#commit(this.#persist(this.#ordered(records)), source);
	  }
	  #persist(records) {
	    const safe = [...records];
	    for (; ; )
	      try {
	        return safe.length || !this.#storage.removeItem || this.#accountStorage ? this.#storage.setItem(this.#key, JSON.stringify(safe)) : this.#storage.removeItem(this.#key), Object.freeze(safe);
	      } catch (cause) {
	        if (!quotaError(cause) || !safe.length)
	          throw this.#diagnose("write-failed", cause), cause;
	        const removed = safe.pop();
	        this.#diagnose("quota-trimmed", Object.freeze({
	          identity: removed?.identity ?? null,
	          remainingCount: safe.length
	        }));
	      }
	  }
	  #commit(records, source) {
	    const snapshot = Object.freeze({
	      records: Object.freeze([...records]),
	      revision: this.#snapshot.revision + 1,
	      source
	    });
	    this.#snapshot = snapshot;
	    for (const cause of this.changes.emit(snapshot))
	      this.#diagnose("consumer-failed", cause);
	    return snapshot;
	  }
	  #diagnose(code, cause) {
	    this.diagnostics.emit(Object.freeze({ code, cause }));
	  }
	}
}, "7cbdfc6ebf4c6461ac643a5f801cb2b116587de05ea017717407860a40ecdf7c");

/* Source: lite/src/history/reader-chronicle-view.ts */
runtime.register("src/history/reader-chronicle-view.js", function(module, exports, require) {
	var reader_chronicle_view_exports = {};
	__export(reader_chronicle_view_exports, {
	  ReaderChronicleView: () => ReaderChronicleView
	});
	module.exports = __toCommonJS(reader_chronicle_view_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_reader_collection_floating_window = require("../collection/reader-collection-floating-window.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_floating_window_frame = require("../shell/reader-floating-window-frame.js");
	const CHRONICLE_BATCH_SIZE = 120, CHRONICLE_TABS = Object.freeze([
	  ["all", "全部"],
	  ["topic", "主题"],
	  ["reply", "回复"],
	  ["boost", "Boost"]
	]);
	function closestTarget(event, selector) {
	  const target = event.target;
	  return typeof target?.closest == "function" ? target.closest(selector) : null;
	}
	function defaultRelativeTime(timestamp) {
	  const elapsed = Math.max(0, Date.now() - timestamp);
	  return elapsed < 6e4 ? "刚刚" : elapsed < 36e5 ? `${Math.floor(elapsed / 6e4)} 分钟前` : elapsed < 864e5 ? `${Math.floor(elapsed / 36e5)} 小时前` : elapsed < 30 * 864e5 ? `${Math.floor(elapsed / 864e5)} 天前` : new Date(timestamp).toLocaleDateString("zh-CN");
	}
	function kindLabel(record) {
	  return record.kind === "topic" ? "主题 404" : record.kind === "reply" ? `回复 #${record.postNumber ?? "?"} · 404` : `Boost #${record.boostId ?? "?"} · 404`;
	}
	function kindIcon(kind) {
	  return kind === "topic" ? "message-square" : kind === "reply" ? "reply" : "rocket";
	}
	function sourceLabel(value) {
	  return value === "reader" ? "Reader" : value === "host" ? "站点" : value === "browser" ? "浏览器" : value || "Reader";
	}
	function topicFloorCount(records) {
	  return new Set(records.flatMap((record) => Number.isSafeInteger(record.postNumber) && Number(record.postNumber) > 0 ? [Number(record.postNumber)] : [])).size;
	}
	function topicDetailLabel(records) {
	  const floorCount = topicFloorCount(records);
	  return floorCount > 0 ? `${floorCount} 个楼层` : `${records.length} 条 Topic 记录`;
	}
	class ReaderChronicleView {
	  scope;
	  window;
	  #document;
	  #chronicle;
	  #openTarget;
	  #relativeTime;
	  #notify;
	  #onError;
	  #tabs;
	  #search;
	  #searchResult;
	  #list;
	  #footer;
	  #activeTab = "all";
	  #visibleLimit = CHRONICLE_BATCH_SIZE;
	  #expandedTopicIds = /* @__PURE__ */ new Set();
	  constructor(options) {
	    this.#document = options.document, this.#chronicle = options.chronicle, this.#openTarget = options.openTarget ?? (() => !1), this.#relativeTime = options.relativeTime ?? defaultRelativeTime, this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.window = new import_reader_floating_window_frame.ReaderFloatingWindowFrame({
	      document: options.document,
	      mount: options.mount,
	      title: "岁月史书",
	      ariaLabel: "岁月史书 404 搜集栏",
	      icon: "history",
	      variant: "chronicle",
	      tabId: "chronicle",
	      tabOrder: 60,
	      requestOpen: () => this.open(),
	      zIndex: 2147483585,
	      ...options.storage ? { geometryStorage: options.storage } : {},
	      geometryStorageKey: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY,
	      policy: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_POLICY,
	      placement: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_PLACEMENT,
	      notify: this.#notify,
	      parentScope: this.scope
	    });
	    const pane = (0, import_html_element.htmlElement)(options.document, "div", "ldp-chronicle-pane");
	    this.#tabs = (0, import_html_element.htmlElement)(options.document, "div", "ldp-chronicle-tabs"), this.#tabs.setAttribute("role", "tablist");
	    const searchLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-chronicle-search"
	    );
	    searchLabel.append((0, import_reader_icon.createReaderIcon)(options.document, "search")), this.#search = options.document.createElement("input"), this.#search.type = "search", this.#search.placeholder = "搜索主题、定位、404 信息", this.#search.setAttribute("aria-label", "搜索岁月史书"), this.#searchResult = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-chronicle-search-result"
	    ), this.#searchResult.hidden = !0, this.#searchResult.setAttribute("aria-live", "polite"), searchLabel.append(this.#search, this.#searchResult);
	    const tools = (0, import_html_element.htmlElement)(options.document, "div", "ldp-chronicle-tools");
	    tools.append(searchLabel), this.#list = (0, import_html_element.htmlElement)(options.document, "div", "ldp-chronicle-list"), this.#list.setAttribute("role", "feed"), this.#footer = (0, import_html_element.htmlElement)(options.document, "footer", "ldp-chronicle-footer"), pane.append(this.#tabs, tools, this.#list, this.#footer), this.window.body.append(pane), this.scope.listen(this.#search, "input", () => {
	      this.#visibleLimit = CHRONICLE_BATCH_SIZE, this.#list.scrollTop = 0, this.#render();
	    }), this.scope.listen(pane, "click", (event) => {
	      this.#onClick(event);
	    }), this.scope.listen(this.#list, "scroll", () => {
	      this.#list.scrollTop + this.#list.clientHeight >= this.#list.scrollHeight - 96 && this.#showMore();
	    }, { passive: !0 }), this.scope.listen(options.document, "pointerdown", (event) => {
	      this.window.dismissFromPointerEvent(event);
	    }, !0), this.scope.listen(options.document, "keydown", (event) => {
	      this.window.dismissFromEscapeEvent(event);
	    }, !0), this.#chronicle.changes.subscribe(() => this.#render(), this.scope), this.#render();
	  }
	  open() {
	    this.#render(), this.window.open();
	  }
	  close() {
	    this.window.close();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #filteredRecords() {
	    const query = this.#search.value.trim().toLocaleLowerCase("zh-CN");
	    return this.#chronicle.ordered().filter((record) => (this.#activeTab === "all" || record.kind === this.#activeTab) && (!query || record.searchText.includes(query)));
	  }
	  #render() {
	    const all = this.#chronicle.ordered(), topicCount = new Set(all.map((record) => record.topicId)).size;
	    this.window.meta.textContent = `${all.length} 条 · ${topicCount} 个 Topic`, this.#tabs.replaceChildren(...CHRONICLE_TABS.map(([tab, label]) => {
	      const button = this.#document.createElement("button");
	      button.type = "button", button.dataset.chronicleTab = tab, button.className = tab === this.#activeTab ? "is-active" : "", button.setAttribute("role", "tab"), button.setAttribute("aria-selected", String(tab === this.#activeTab));
	      const count = tab === "all" ? all.length : all.filter((record) => record.kind === tab).length;
	      return button.textContent = `${label} ${count}`, button;
	    }));
	    const records = this.#filteredRecords(), searching = !!this.#search.value.trim();
	    this.#searchResult.hidden = !searching, this.#searchResult.textContent = searching ? `${records.length} 条` : "";
	    const visible = records.slice(0, this.#visibleLimit), groups = /* @__PURE__ */ new Map();
	    for (const record of visible) {
	      const group = groups.get(record.topicId) ?? [];
	      group.push(record), groups.set(record.topicId, group);
	    }
	    this.#list.replaceChildren(...[...groups.values()].map((group) => this.#topicGroup(group))), records.length || this.#list.append((0, import_html_element.htmlElement)(
	      this.#document,
	      "p",
	      "ldp-chronicle-empty",
	      searching ? "没有匹配的 404 记录。" : "还没有收到可定位的 Topic、回复或 Boost 404 信号。"
	    )), this.#renderFooter(records.length);
	  }
	  #topicGroup(records) {
	    const latest = records[0], section = (0, import_html_element.htmlElement)(this.#document, "section", "ldp-chronicle-topic");
	    section.dataset.chronicleTopic = String(latest.topicId);
	    const expanded = this.#expandedTopicIds.has(latest.topicId);
	    section.classList.toggle("is-expanded", expanded);
	    const heading = this.#document.createElement("button");
	    heading.type = "button", heading.className = "ldp-chronicle-topic-head";
	    const copy = (0, import_html_element.htmlElement)(this.#document, "span", "ldp-chronicle-topic-copy"), detailLabel = topicDetailLabel(records);
	    copy.append(
	      this.#highlighted("strong", latest.topicTitle),
	      this.#highlighted("small", `Topic #${latest.topicId}`)
	    );
	    const time = (0, import_html_element.htmlElement)(
	      this.#document,
	      "time",
	      "",
	      this.#relativeTime(latest.lastObservedAt)
	    ), entriesId = `ldp-chronicle-topic-records-${latest.topicId}`;
	    heading.dataset.chronicleTopicToggle = String(latest.topicId), heading.dataset.chronicleTopicDetail = detailLabel, heading.setAttribute("aria-controls", entriesId), heading.setAttribute("aria-expanded", String(expanded)), heading.setAttribute(
	      "aria-label",
	      `${expanded ? "收起" : "展开"} ${detailLabel}`
	    ), heading.title = `${expanded ? "收起" : "展开"} ${detailLabel}`;
	    const chevron = (0, import_reader_icon.createReaderIcon)(
	      this.#document,
	      expanded ? "chevron-up" : "chevron-down"
	    );
	    chevron.classList.add("ldp-chronicle-topic-chevron"), heading.append(
	      (0, import_reader_icon.createReaderIcon)(this.#document, "message-square"),
	      copy,
	      time,
	      (0, import_html_element.htmlElement)(this.#document, "span", "ldp-chronicle-topic-count", detailLabel),
	      chevron
	    );
	    const entries = (0, import_html_element.htmlElement)(this.#document, "div", "ldp-chronicle-records");
	    return entries.id = entriesId, entries.hidden = !expanded, entries.append(...records.map((record) => this.#record(record))), section.append(heading, entries), section;
	  }
	  #record(record) {
	    const button = this.#document.createElement("button");
	    button.type = "button", button.className = `ldp-chronicle-record is-${record.kind}`, button.dataset.chronicleRecord = record.identity, button.setAttribute(
	      "aria-label",
	      `${kindLabel(record)},${record.topicTitle},${record.requestMethod} ${record.requestPath}`
	    );
	    const copy = (0, import_html_element.htmlElement)(this.#document, "span", "ldp-chronicle-record-copy"), meta = (0, import_html_element.htmlElement)(this.#document, "span", "ldp-chronicle-record-meta");
	    meta.append(
	      this.#highlighted("strong", kindLabel(record)),
	      (0, import_html_element.htmlElement)(
	        this.#document,
	        "small",
	        "",
	        `${this.#relativeTime(record.lastObservedAt)}${record.occurrences > 1 ? ` · ${record.occurrences} 次` : ""}`
	      )
	    );
	    const location = record.kind === "topic" ? `Topic #${record.topicId}` : record.kind === "reply" ? `Topic #${record.topicId} · 楼层 #${record.postNumber ?? "?"}` : `Topic #${record.topicId} · 楼层 #${record.postNumber ?? "?"} · Boost #${record.boostId ?? "?"}`;
	    copy.append(
	      meta,
	      this.#highlighted(
	        "b",
	        `${record.requestMethod} ${record.requestPath}`
	      ),
	      this.#highlighted("small", location)
	    );
	    const diagnostic = [sourceLabel(record.requestSource), record.callSite].filter(Boolean).join(" · ");
	    return diagnostic && copy.append(this.#highlighted("small", diagnostic)), button.append(
	      (0, import_reader_icon.createReaderIcon)(this.#document, kindIcon(record.kind)),
	      copy,
	      (0, import_reader_icon.createReaderIcon)(this.#document, "chevron-right")
	    ), button;
	  }
	  #highlighted(tag, value) {
	    const element = this.#document.createElement(tag), query = this.#search.value.trim();
	    if (!query)
	      return element.textContent = value, element;
	    const source = value.toLocaleLowerCase("zh-CN"), needle = query.toLocaleLowerCase("zh-CN");
	    let cursor = 0, match = source.indexOf(needle);
	    if (match < 0)
	      return element.textContent = value, element;
	    for (; match >= 0; ) {
	      match > cursor && element.append(this.#document.createTextNode(value.slice(cursor, match)));
	      const mark = this.#document.createElement("mark");
	      mark.textContent = value.slice(match, match + needle.length), element.append(mark), cursor = match + needle.length, match = source.indexOf(needle, cursor);
	    }
	    return cursor < value.length && element.append(this.#document.createTextNode(value.slice(cursor))), element;
	  }
	  #renderFooter(total) {
	    if (this.#footer.replaceChildren((0, import_html_element.htmlElement)(
	      this.#document,
	      "span",
	      "",
	      "岁月史书只保留本机仍有正文的 404 记录。"
	    )), this.#visibleLimit >= total) return;
	    const more = this.#document.createElement("button");
	    more.type = "button", more.dataset.chronicleMore = "", more.textContent = `继续显示(${Math.min(this.#visibleLimit, total)} / ${total})`, this.#footer.append(more);
	  }
	  #onClick(event) {
	    const target = closestTarget(
	      event,
	      "[data-chronicle-tab],[data-chronicle-topic-toggle],[data-chronicle-record],[data-chronicle-more]"
	    );
	    if (!target) return;
	    const tab = target.dataset.chronicleTab;
	    if (tab && CHRONICLE_TABS.some(([value]) => value === tab)) {
	      this.#activeTab = tab, this.#visibleLimit = CHRONICLE_BATCH_SIZE, this.#list.scrollTop = 0, this.#render();
	      return;
	    }
	    if (target.dataset.chronicleMore !== void 0) {
	      this.#showMore();
	      return;
	    }
	    const topicToggle = Number(target.dataset.chronicleTopicToggle);
	    if (Number.isSafeInteger(topicToggle) && topicToggle > 0) {
	      const section = target.closest(".ldp-chronicle-topic"), entries = section?.querySelector(
	        ".ldp-chronicle-records"
	      );
	      if (!section || !entries) return;
	      const expanded = target.getAttribute("aria-expanded") !== "true";
	      expanded ? this.#expandedTopicIds.add(topicToggle) : this.#expandedTopicIds.delete(topicToggle), section.classList.toggle("is-expanded", expanded), entries.hidden = !expanded, target.setAttribute("aria-expanded", String(expanded));
	      const detail = target.dataset.chronicleTopicDetail || "楼层记录", label = `${expanded ? "收起" : "展开"} ${detail}`;
	      target.setAttribute("aria-label", label), target.title = label;
	      const currentChevron = target.querySelector(
	        ".ldp-chronicle-topic-chevron"
	      ), nextChevron = (0, import_reader_icon.createReaderIcon)(
	        this.#document,
	        expanded ? "chevron-up" : "chevron-down"
	      );
	      nextChevron.classList.add("ldp-chronicle-topic-chevron"), currentChevron?.replaceWith(nextChevron);
	      return;
	    }
	    const identity = target.dataset.chronicleRecord, record = this.#chronicle.snapshot.records.find((entry) => entry.identity === identity);
	    record && Promise.resolve(this.#openTarget(
	      record.topicId,
	      record.postNumber ?? 1,
	      record
	    )).then((opened) => {
	      opened || (this.#chronicle.remove(record.identity), this.#notify("本地正文已不存在,已从岁月史书移除"));
	    }).catch((cause) => {
	      this.#onError(cause), this.#notify("该 404 定位暂时无法打开");
	    });
	  }
	  #showMore() {
	    const total = this.#filteredRecords().length;
	    this.#visibleLimit >= total || (this.#visibleLimit = Math.min(total, this.#visibleLimit + CHRONICLE_BATCH_SIZE), this.#render());
	  }
	}
}, "cb2d428b803546cd0bee2639c86d6b0aeb60b7d0f8c2a400423a07e307ad9933");

/* Source: lite/src/history/reader-history-model.ts */
runtime.register("src/history/reader-history-model.js", function(module, exports, require) {
	var reader_history_model_exports = {};
	__export(reader_history_model_exports, {
	  normalizeReaderHistoryAnchorPoint: () => normalizeReaderHistoryAnchorPoint,
	  normalizeReaderHistoryAnchorState: () => normalizeReaderHistoryAnchorState,
	  normalizeReaderHistoryAnchorStates: () => normalizeReaderHistoryAnchorStates,
	  normalizeReaderHistoryReplyWindowState: () => normalizeReaderHistoryReplyWindowState,
	  normalizeReaderHistoryViewport: () => normalizeReaderHistoryViewport
	});
	module.exports = __toCommonJS(reader_history_model_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function record(value) {
	  return value && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function finite(value, fallback) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? numeric : fallback;
	}
	function nonNegative(value) {
	  return Math.max(0, finite(value, 0));
	}
	function optionalNonNegative(value) {
	  if (value == null || value === "") return null;
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
	}
	function optionalPostNumber(value) {
	  try {
	    return (0, import_identifiers.discoursePostNumber)(value);
	  } catch {
	    return null;
	  }
	}
	function normalizeReaderHistoryViewport(value) {
	  const source = record(value) ?? Object.freeze({ postNumber: value }), postNumber = optionalPostNumber(source.postNumber);
	  if (postNumber === null) return null;
	  const scrollTop = nonNegative(source.scrollTop), rawScrollRange = optionalNonNegative(source.scrollRange), rawScrollRatio = optionalNonNegative(source.scrollRatio), hasScrollGeometry = rawScrollRange !== null || rawScrollRatio !== null, scrollRange = rawScrollRange ?? 0, scrollRatio = Math.min(
	    1,
	    Math.max(
	      0,
	      rawScrollRatio !== null ? rawScrollRatio : scrollRange > 0 ? scrollTop / scrollRange : 0
	    )
	  );
	  return Object.freeze({
	    postNumber,
	    postOffset: finite(source.postOffset, 0),
	    scrollTop,
	    ...hasScrollGeometry ? { scrollRange, scrollRatio } : {}
	  });
	}
	function normalizeReaderHistoryAnchorPoint(value) {
	  const source = record(value), number = optionalPostNumber(source?.number);
	  return number === null ? null : Object.freeze({
	    number,
	    scrollTop: nonNegative(source?.scrollTop),
	    scrollLeft: nonNegative(source?.scrollLeft),
	    offset: finite(source?.offset, 12)
	  });
	}
	function normalizeReaderHistoryReplyWindowState(value) {
	  const source = record(value), rootPostNumber = optionalPostNumber(source?.rootPostNumber);
	  if (rootPostNumber === null) return null;
	  const descendantRootPostNumber = optionalPostNumber(
	    source?.descendantRootPostNumber
	  );
	  return Object.freeze({
	    rootPostNumber,
	    ...descendantRootPostNumber !== null && descendantRootPostNumber !== rootPostNumber ? { descendantRootPostNumber } : {},
	    point: normalizeReaderHistoryAnchorPoint(source?.point)
	  });
	}
	function normalizeReaderHistoryQuoteSource(value) {
	  const source = record(value);
	  if (!source) return null;
	  let topicId, postNumber;
	  try {
	    topicId = (0, import_identifiers.discourseTopicId)(source.topicId), postNumber = (0, import_identifiers.discoursePostNumber)(source.postNumber);
	  } catch {
	    return null;
	  }
	  const parentPostNumber = optionalPostNumber(source.parentPostNumber), rawAnchor = record(source.anchor), viewport = normalizeReaderHistoryViewport(rawAnchor?.viewport), anchor = viewport === null ? null : Object.freeze({
	    viewport,
	    replyWindow: normalizeReaderHistoryReplyWindowState(
	      rawAnchor?.replyWindow
	    ),
	    quoteHighlight: null
	  });
	  return Object.freeze({
	    topicId,
	    postNumber,
	    parentPostNumber,
	    nested: source.nested === !0 && parentPostNumber !== null,
	    anchor
	  });
	}
	function normalizeReaderHistoryQuoteHighlightState(value) {
	  const source = record(value), postNumber = optionalPostNumber(source?.postNumber), text = String(source?.text ?? source?.quoteText ?? "");
	  return postNumber === null || !text ? null : Object.freeze({
	    postNumber,
	    text,
	    source: normalizeReaderHistoryQuoteSource(source?.source),
	    active: source?.active !== !1
	  });
	}
	function normalizeReaderHistoryAnchorState(value) {
	  const raw = record(value), source = raw && "viewport" in raw ? raw : Object.freeze({ viewport: value }), viewport = normalizeReaderHistoryViewport(source.viewport);
	  return viewport === null ? null : Object.freeze({
	    viewport,
	    replyWindow: normalizeReaderHistoryReplyWindowState(source.replyWindow),
	    quoteHighlight: normalizeReaderHistoryQuoteHighlightState(
	      source.quoteHighlight
	    )
	  });
	}
	function normalizeReaderHistoryAnchorStates(value) {
	  const source = record(value), states = {};
	  if (!source) return Object.freeze(states);
	  for (const [rawTopicId, rawState] of Object.entries(source)) {
	    let topicId;
	    try {
	      topicId = (0, import_identifiers.discourseTopicId)(rawTopicId);
	    } catch {
	      continue;
	    }
	    const state = normalizeReaderHistoryAnchorState(rawState);
	    state && (states[String(topicId)] = state);
	  }
	  return Object.freeze(states);
	}
}, "b3901132bc6c4446966db7d7226c122039b751b5a1ff6123d40942608630d703");

/* Source: lite/src/history/reader-history-navigation-controller.ts */
runtime.register("src/history/reader-history-navigation-controller.js", function(module, exports, require) {
	var reader_history_navigation_controller_exports = {};
	__export(reader_history_navigation_controller_exports, {
	  ReaderHistoryNavigationController: () => ReaderHistoryNavigationController
	});
	module.exports = __toCommonJS(reader_history_navigation_controller_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_history_model = require("./reader-history-model.js");
	function normalizedTopicIds(values, currentTopicId) {
	  const normalized = /* @__PURE__ */ new Set();
	  for (const value of values ?? [])
	    try {
	      const topicId = (0, import_identifiers.discourseTopicId)(value);
	      topicId !== currentTopicId && normalized.add(topicId);
	    } catch {
	    }
	  return Object.freeze([...normalized]);
	}
	function frozenStates(states, topicId, anchor) {
	  return Object.freeze(topicId === void 0 || anchor === void 0 || anchor === null ? { ...states } : {
	    ...states,
	    [String(topicId)]: anchor
	  });
	}
	class ReaderHistoryNavigationController {
	  scope;
	  changes = new import_signal.Signal();
	  #history;
	  #port;
	  #readSortMode;
	  #onError;
	  #snapshot = Object.freeze({
	    activeTopicId: null,
	    back: Object.freeze([]),
	    forward: Object.freeze([]),
	    states: Object.freeze({}),
	    pending: null,
	    revision: 0
	  });
	  #epoch = 0;
	  constructor(options) {
	    this.#history = options.history, this.#port = options.port, this.#readSortMode = options.readSortMode ?? (() => "recent-viewed"), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.scope.add(() => {
	      this.#epoch += 1, this.changes.clear();
	    });
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  activate(topicIdValue, source) {
	    this.#assertActive();
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue);
	    this.#epoch += 1;
	    const ordered = this.#history.ordered(this.#readSortMode()).map((entry) => entry.topicId), currentIndex = ordered.indexOf(topicId), firstViewedFixedPosition = this.#readSortMode() === "first-viewed" && currentIndex >= 0, back = source && (source.back || source.forward) ? normalizedTopicIds(source.back, topicId) : normalizedTopicIds(
	      firstViewedFixedPosition ? ordered.slice(currentIndex + 1) : ordered,
	      topicId
	    ), forward = source && (source.back || source.forward) ? normalizedTopicIds(source.forward, topicId) : normalizedTopicIds(
	      firstViewedFixedPosition ? ordered.slice(0, currentIndex).reverse() : [],
	      topicId
	    );
	    return this.#commit({
	      activeTopicId: topicId,
	      back,
	      forward,
	      states: this.#seedPersistedAnchors(
	        source ? (0, import_reader_history_model.normalizeReaderHistoryAnchorStates)(source.states) : this.#snapshot.states
	      ),
	      pending: null
	    });
	  }
	  refreshOrder() {
	    this.#assertActive();
	    const topicId = this.#snapshot.activeTopicId;
	    if (topicId === null) return this.#snapshot;
	    const states = this.#snapshot.states, refreshed = this.activate(topicId);
	    return this.#commit({
	      ...refreshed,
	      states,
	      pending: null
	    });
	  }
	  captureCurrent() {
	    this.#assertActive();
	    const topicId = this.#snapshot.activeTopicId;
	    if (topicId === null || this.#port.activeTopicId() !== topicId)
	      return null;
	    let anchor;
	    try {
	      anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(
	        this.#port.captureAnchor()
	      );
	    } catch (cause) {
	      return this.#onError(cause), null;
	    }
	    return anchor ? (this.#commit({
	      ...this.#snapshot,
	      states: frozenStates(this.#snapshot.states, topicId, anchor),
	      pending: null
	    }), anchor) : null;
	  }
	  setAnchor(topicIdValue, value) {
	    this.#assertActive();
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(value);
	    if (!anchor) throw new TypeError("历史锚点缺少有效 viewport");
	    return this.#commit({
	      ...this.#snapshot,
	      states: frozenStates(this.#snapshot.states, topicId, anchor)
	    });
	  }
	  async restore(topicIdValue, value, options = {}) {
	    this.#assertActive();
	    const topicId = (0, import_identifiers.discourseTopicId)(topicIdValue), anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(value);
	    if (!anchor) throw new TypeError("历史锚点缺少有效 viewport");
	    if (this.#snapshot.activeTopicId !== topicId || this.#port.activeTopicId() !== topicId)
	      throw new Error(`历史目标 Topic ${topicId} 未处于 active 状态`);
	    try {
	      await this.#port.restoreAnchor(topicId, anchor, options);
	    } catch (cause) {
	      throw this.#onError(cause), cause;
	    }
	    return this.#commit({
	      ...this.#snapshot,
	      states: frozenStates(this.#snapshot.states, topicId, anchor),
	      pending: null
	    }), anchor;
	  }
	  async navigate(direction) {
	    this.#assertActive();
	    const fromTopicId = this.#snapshot.activeTopicId, targetTopicId = (direction === "back" ? this.#snapshot.back : this.#snapshot.forward)[0] ?? null;
	    if (fromTopicId === null || targetTopicId === null)
	      return Object.freeze({
	        direction,
	        fromTopicId,
	        targetTopicId,
	        status: "unavailable"
	      });
	    let states = this.#snapshot.states;
	    if (this.#port.activeTopicId() === fromTopicId)
	      try {
	        const anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)(
	          this.#port.captureAnchor()
	        );
	        anchor && (states = frozenStates(states, fromTopicId, anchor));
	      } catch (cause) {
	        this.#onError(cause);
	      }
	    const epoch = ++this.#epoch, pending = Object.freeze({
	      direction,
	      fromTopicId,
	      targetTopicId
	    });
	    this.#commit({
	      ...this.#snapshot,
	      states,
	      pending
	    });
	    let opened;
	    try {
	      opened = await this.#port.openTopic(targetTopicId);
	    } catch (cause) {
	      return epoch !== this.#epoch || this.scope.destroyed ? this.#result(pending, "superseded") : (this.#onError(cause), this.#commit({ ...this.#snapshot, pending: null }), this.#result(pending, "failed", cause));
	    }
	    if (epoch !== this.#epoch || this.scope.destroyed || opened.status === "superseded")
	      return this.#result(pending, "superseded");
	    if (opened.status === "failed")
	      return this.#commit({ ...this.#snapshot, pending: null }), this.#result(pending, "failed", opened.cause);
	    const back = direction === "back" ? this.#snapshot.back.slice(1) : Object.freeze([fromTopicId, ...this.#snapshot.back]), forward = direction === "back" ? Object.freeze([fromTopicId, ...this.#snapshot.forward]) : this.#snapshot.forward.slice(1);
	    this.#commit({
	      activeTopicId: targetTopicId,
	      back: Object.freeze(back),
	      forward: Object.freeze(forward),
	      states,
	      pending: null
	    });
	    const targetAnchor = states[String(targetTopicId)];
	    if (!targetAnchor) return this.#result(pending, "opened");
	    try {
	      await this.#port.restoreAnchor(targetTopicId, targetAnchor, {
	        highlight: !1,
	        restoreSemanticState: !1
	      });
	    } catch (cause) {
	      return epoch !== this.#epoch || this.scope.destroyed ? this.#result(pending, "superseded") : (this.#onError(cause), this.#result(pending, "restore-failed", cause));
	    }
	    return epoch !== this.#epoch || this.scope.destroyed ? this.#result(pending, "superseded") : this.#result(pending, "restored");
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #result(pending, status, cause) {
	    return Object.freeze({
	      direction: pending.direction,
	      fromTopicId: pending.fromTopicId,
	      targetTopicId: pending.targetTopicId,
	      status,
	      ...cause === void 0 ? {} : { cause }
	    });
	  }
	  #seedPersistedAnchors(states) {
	    const seeded = {
	      ...states
	    };
	    for (const entry of this.#history.snapshot.entries) {
	      const key = String(entry.topicId);
	      if (seeded[key] || entry.viewport === null) continue;
	      const anchor = (0, import_reader_history_model.normalizeReaderHistoryAnchorState)({
	        viewport: entry.viewport
	      });
	      anchor && (seeded[key] = anchor);
	    }
	    return Object.freeze(seeded);
	  }
	  #commit(input) {
	    const snapshot = Object.freeze({
	      activeTopicId: input.activeTopicId,
	      back: Object.freeze([...input.back]),
	      forward: Object.freeze([...input.forward]),
	      states: frozenStates(input.states),
	      pending: input.pending,
	      revision: this.#snapshot.revision + 1
	    });
	    this.#snapshot = snapshot;
	    for (const cause of this.changes.emit(snapshot)) this.#onError(cause);
	    return snapshot;
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderHistoryNavigationController 已销毁");
	  }
	}
}, "e68aa52659f0f5ec018c05510945c866433000ad7f008cf1138aa990c4d34e52");

/* Source: lite/src/history/reader-history-navigation-view.ts */
runtime.register("src/history/reader-history-navigation-view.js", function(module, exports, require) {
	var reader_history_navigation_view_exports = {};
	__export(reader_history_navigation_view_exports, {
	  ReaderHistoryNavigationView: () => ReaderHistoryNavigationView
	});
	module.exports = __toCommonJS(reader_history_navigation_view_exports);
	var import_lifecycle = require("../kernel/lifecycle.js");
	const EDGE_TRIGGER_MIN = 0, EDGE_TRIGGER_MAX = 15, BLOCKING_SURFACE_SELECTOR = ".ldp-settings-popover,.ldp-notifications-popover,.ldp-history-popover,.ldp-bookmarks-popover";
	function normalizedPreferences(value) {
	  const numeric = Number(value.edgeTriggerPercent), edgeTriggerPercent = Number.isFinite(numeric) ? Math.min(
	    EDGE_TRIGGER_MAX,
	    Math.max(EDGE_TRIGGER_MIN, Math.round(numeric))
	  ) : EDGE_TRIGGER_MAX;
	  return Object.freeze({
	    edgeTriggerPercent,
	    buttonsAlwaysVisible: value.buttonsAlwaysVisible === !0
	  });
	}
	function eventElement(value) {
	  return value !== null && typeof value == "object" && value.nodeType === 1 ? value : null;
	}
	class ReaderHistoryNavigationView {
	  scope;
	  #navigation;
	  #elements;
	  #window;
	  #topicTitle;
	  #onError;
	  #preferences;
	  #pointerBounds = null;
	  #backActive = !1;
	  #forwardActive = !1;
	  constructor(options) {
	    this.#navigation = options.navigation, this.#elements = options.elements, this.#window = options.window ?? null, this.#topicTitle = options.topicTitle ?? (() => null), this.#onError = options.onError ?? (() => {
	    }), this.#preferences = normalizedPreferences(options.preferences), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const {
	      root,
	      modal,
	      backButton,
	      forwardButton
	    } = this.#elements;
	    this.#listen(modal, "pointerenter", (event) => {
	      this.#onPointerEnter(event);
	    }), this.#listen(modal, "pointermove", (event) => {
	      this.#onPointerMove(event);
	    }), this.#listen(modal, "pointerleave", () => {
	      this.#clearActive(), this.#invalidateBounds();
	    }), this.#listen(root, "ldp-reader-window-change", () => {
	      this.#invalidateBounds();
	    }), this.#listen(root, "ldp-reader-workspace-change", () => {
	      this.#invalidateBounds();
	    }), this.#window && this.#listen(this.#window, "resize", () => {
	      this.#invalidateBounds();
	    }), this.#listen(backButton, "click", () => {
	      this.#navigate("back");
	    }), this.#listen(forwardButton, "click", () => {
	      this.#navigate("forward");
	    }), this.#navigation.changes.subscribe((snapshot) => {
	      this.#sync(snapshot);
	    }, this.scope), this.scope.add(() => {
	      this.#clearActive(), root.classList.remove("ldp-history-buttons-always-visible");
	    }), this.applyPreferences(this.#preferences), this.#sync(this.#navigation.snapshot);
	  }
	  applyPreferences(value) {
	    this.#assertActive(), this.#preferences = normalizedPreferences(value), this.#elements.root.classList.toggle(
	      "ldp-history-buttons-always-visible",
	      this.#preferences.buttonsAlwaysVisible
	    ), this.#invalidateBounds(), this.#preferences.buttonsAlwaysVisible && this.#clearActive();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #sync(snapshot) {
	    const backTarget = snapshot.back[0] ?? null, forwardTarget = snapshot.forward[0] ?? null, pending = snapshot.pending !== null;
	    this.#syncDirection("back", backTarget, pending), this.#syncDirection("forward", forwardTarget, pending);
	  }
	  #syncDirection(direction, targetTopicId, pending) {
	    const backward = direction === "back", edge = backward ? this.#elements.backEdge : this.#elements.forwardEdge, button = backward ? this.#elements.backButton : this.#elements.forwardButton, hidden = targetTopicId === null;
	    if (edge.hidden = hidden, button.hidden = hidden, button.disabled = pending, button.setAttribute("aria-busy", String(pending)), targetTopicId === null) {
	      button.removeAttribute("title");
	      return;
	    }
	    const title = this.#topicTitle(targetTopicId) ?? `帖子 #${targetTopicId}`, label = `${backward ? "上一条" : "下一条"}历史:${title}`;
	    button.setAttribute("aria-label", label), button.title = label;
	  }
	  #navigate(direction) {
	    this.#navigation.snapshot.pending || this.#navigation.navigate(direction).catch((error) => {
	      this.#report(error);
	    });
	  }
	  #onPointerEnter(event) {
	    this.#preferences.buttonsAlwaysVisible || event.pointerType && event.pointerType !== "mouse" || this.#refreshBounds();
	  }
	  #onPointerMove(event) {
	    if (this.#preferences.buttonsAlwaysVisible || event.pointerType && event.pointerType !== "mouse") {
	      this.#clearActive();
	      return;
	    }
	    const target = eventElement(event.target);
	    if (target?.closest(BLOCKING_SURFACE_SELECTOR)) {
	      this.#clearActive();
	      return;
	    }
	    const bounds = this.#pointerBounds ?? this.#refreshBounds(), overBackButton = target !== null && this.#elements.backButton.contains(target), overForwardButton = target !== null && this.#elements.forwardButton.contains(target), nearBack = bounds.enabled && event.clientX <= bounds.backLimit, nearForward = bounds.enabled && event.clientX >= bounds.forwardLimit;
	    if (!nearBack && !nearForward && !overBackButton && !overForwardButton) {
	      this.#clearActive();
	      return;
	    }
	    this.#setActive(
	      nearBack || overBackButton,
	      nearForward || overForwardButton
	    );
	  }
	  #refreshBounds() {
	    const percent = this.#preferences.edgeTriggerPercent;
	    if (percent <= 0)
	      return this.#pointerBounds = Object.freeze({
	        backLimit: 0,
	        forwardLimit: 0,
	        enabled: !1
	      }), this.#pointerBounds;
	    const rect = this.#elements.modal.getBoundingClientRect(), width = Math.max(0, rect.width), triggerWidth = width * percent / 100;
	    return this.#pointerBounds = Object.freeze({
	      backLimit: rect.left + triggerWidth,
	      forwardLimit: rect.right - triggerWidth,
	      enabled: width > 0
	    }), this.#pointerBounds;
	  }
	  #invalidateBounds() {
	    this.#pointerBounds = null;
	  }
	  #setActive(back, forward) {
	    back !== this.#backActive && (this.#backActive = back, this.#elements.backEdge.classList.toggle("is-active", back)), forward !== this.#forwardActive && (this.#forwardActive = forward, this.#elements.forwardEdge.classList.toggle(
	      "is-active",
	      forward
	    ));
	  }
	  #clearActive() {
	    this.#setActive(!1, !1);
	  }
	  #listen(target, type, listener) {
	    target.addEventListener(type, listener), this.scope.add(() => {
	      target.removeEventListener(type, listener);
	    });
	  }
	  #report(error) {
	    try {
	      this.#onError(error);
	    } catch {
	    }
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderHistoryNavigationView 已销毁");
	  }
	}
}, "4a1c793d3182323a1005971e819bd958a598d250d6442816164cbf2082c7c88e");

/* Source: lite/src/history/reader-history-panel-view.ts */
runtime.register("src/history/reader-history-panel-view.js", function(module, exports, require) {
	var reader_history_panel_view_exports = {};
	__export(reader_history_panel_view_exports, {
	  ReaderHistoryPanelView: () => ReaderHistoryPanelView
	});
	module.exports = __toCommonJS(reader_history_panel_view_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_reader_collection_floating_window = require("../collection/reader-collection-floating-window.js"), import_reader_popover_filter_controls = require("../collection/reader-popover-filter-controls.js"), import_reader_collection_filter_model = require("../collection/reader-collection-filter-model.js"), import_reader_image_fallback = require("../components/reader-image-fallback.js"), import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_search = require("../search/reader-search.js"), import_reader_history_repository = require("./reader-history-repository.js");
	const DEFAULT_PAGE_SIZE = 20;
	function defaultRelativeTime(timestamp, now) {
	  const elapsed = Math.max(0, now - timestamp), minute = 6e4, hour = 60 * minute, day = 24 * hour;
	  return elapsed < minute ? "刚刚" : elapsed < hour ? `${Math.floor(elapsed / minute)} 分钟前` : elapsed < day ? `${Math.floor(elapsed / hour)} 小时前` : `${Math.floor(elapsed / day)} 天前`;
	}
	class ReaderHistoryPanelView {
	  scope;
	  #document;
	  #history;
	  #elements;
	  #pageSize;
	  #topicHref;
	  #openEntry;
	  #changeSortMode;
	  #confirmDelete;
	  #notify;
	  #searchForms;
	  #avatarSource;
	  #relativeTime;
	  #now;
	  #onError;
	  #surface;
	  #filterDisclosure;
	  #scrollWindow;
	  #preferences;
	  #page = 0;
	  #query = "";
	  #categoryFilter = "";
	  #tagFilter = "";
	  #dateFilter = "";
	  #sortDirection = "desc";
	  #multi = !1;
	  #selectionScope = "page";
	  #selection = /* @__PURE__ */ new Set();
	  #visibleTopicIds = Object.freeze([]);
	  #totalMatches = 0;
	  #totalPages = 1;
	  #revision = 0;
	  #openEpoch = 0;
	  constructor(options) {
	    if (this.#document = options.document, this.#history = options.history, this.#elements = options.elements, this.#pageSize = Math.floor(
	      Number(options.pageSize ?? DEFAULT_PAGE_SIZE)
	    ), !Number.isSafeInteger(this.#pageSize) || this.#pageSize <= 0)
	      throw new RangeError("历史面板 pageSize 必须是正整数");
	    this.#topicHref = options.topicHref, this.#openEntry = options.openEntry, this.#changeSortMode = options.changeSortMode, this.#confirmDelete = options.confirmDelete, this.#notify = options.notify ?? (() => {
	    }), this.#searchForms = options.searchForms ?? ((value) => Object.freeze([(0, import_reader_search.normalizeReaderSearchText)(value)])), this.#avatarSource = options.avatarSource ?? ((template, size) => (0, import_native_host_api.discourseAvatarTemplateUrl)(
	      template,
	      size,
	      this.#document.baseURI
	    )), this.#now = options.now ?? Date.now, this.#relativeTime = options.relativeTime ?? ((timestamp) => defaultRelativeTime(timestamp, this.#now())), this.#onError = options.onError ?? (() => {
	    }), this.#preferences = this.#normalizePreferences(options.preferences), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#surface = new import_reader_collection_floating_window.ReaderCollectionFloatingWindow({
	      document: this.#document,
	      mount: options.mount,
	      toggle: this.#elements.toggle,
	      content: this.#elements.popover,
	      title: "浏览历史",
	      ariaLabel: "浏览历史",
	      icon: "history",
	      variant: "history",
	      tabOrder: 20,
	      ...options.storage ? { geometryStorage: options.storage } : {},
	      parentScope: this.scope,
	      isOpen: () => this.#surface?.isOpen ?? !1,
	      requestOpen: () => this.open(),
	      requestClose: () => this.close(),
	      notify: this.#notify
	    }), this.#surface.attachHeaderActions({
	      root: this.#elements.defaultActions,
	      buttons: [
	        this.#elements.sortToggle,
	        this.#elements.multiButton,
	        this.#elements.clearButton
	      ],
	      label: "浏览历史操作"
	    }), this.#surface.attachHeaderActions({
	      root: this.#elements.bulkActions,
	      buttons: [
	        this.#elements.selectToggle,
	        this.#elements.deleteSelected,
	        this.#elements.multiDone
	      ],
	      label: "浏览历史多选操作"
	    });
	    const collectionTitle = this.#elements.popover.querySelector(
	      ".ldp-collection-title"
	    );
	    collectionTitle && (collectionTitle.hidden = !0), this.#filterDisclosure = new import_reader_popover_filter_controls.ReaderPopoverFilterDisclosure({
	      search: this.#elements.search,
	      onDateChange: (value) => {
	        this.#dateFilter = value, this.#resetFilteredPage();
	      },
	      onSortChange: (value) => {
	        const mode = value === "first-viewed" ? "first-viewed" : "recent-viewed";
	        mode !== this.#preferences.sortMode && this.#run(async () => {
	          await this.#changeSortMode(mode), this.scope.destroyed || this.applyPreferences({ sortMode: mode });
	        });
	      },
	      onDirectionChange: (value) => {
	        this.#sortDirection = value, this.#resetFilteredPage();
	      },
	      onReset: () => this.#resetFilters(),
	      parentScope: this.scope
	    });
	    const pager = this.#elements.pageInfo.parentElement;
	    if (!pager) throw new Error("历史面板缺少滚动分页锚点");
	    this.#scrollWindow = new import_reader_collection_floating_window.ReaderCollectionScrollWindow({
	      list: this.#elements.list,
	      pager,
	      identity: (entry) => String(entry.topicId),
	      loadMore: () => {
	        this.#page >= this.#totalPages - 1 || (this.#page += 1, this.#render());
	      },
	      onError: this.#onError,
	      parentScope: this.scope
	    }), this.#bind(), this.#history.changes.subscribe(() => {
	      this.#pruneSelection(), this.#preferences.sortMode === "recent-viewed" && this.#surface.isOpen && (this.#page = 0), this.#render();
	    }, this.scope), this.scope.add(() => {
	      this.#openEpoch += 1, this.#selection.clear();
	    }), this.#render();
	  }
	  get snapshot() {
	    return Object.freeze({
	      open: this.#surface.isOpen,
	      page: this.#page,
	      query: this.#query,
	      categoryFilter: this.#categoryFilter,
	      tagFilter: this.#tagFilter,
	      dateFilter: this.#dateFilter,
	      sortDirection: this.#sortDirection,
	      multi: this.#multi,
	      selectionScope: this.#selectionScope,
	      selectedTopicIds: new Set(this.#selection),
	      visibleTopicIds: this.#visibleTopicIds,
	      totalMatches: this.#totalMatches,
	      totalPages: this.#totalPages,
	      revision: this.#revision
	    });
	  }
	  applyPreferences(preferences) {
	    this.#assertActive();
	    const normalized = this.#normalizePreferences(preferences);
	    normalized.sortMode !== this.#preferences.sortMode && (this.#preferences = normalized, this.#page = 0, this.#selection.clear(), this.#render());
	  }
	  open() {
	    this.#assertActive(), this.#surface.sync(!0), this.#render();
	  }
	  close() {
	    this.scope.destroyed || !this.#surface.isOpen || (this.#surface.sync(!1), this.#multi = !1, this.#selection.clear(), this.#render());
	  }
	  toggle() {
	    this.scope.destroyed || (this.#surface.isOpen ? this.close() : this.open());
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #normalizePreferences(preferences) {
	    return Object.freeze({
	      sortMode: preferences.sortMode === "first-viewed" ? "first-viewed" : "recent-viewed"
	    });
	  }
	  #bind() {
	    this.#listen(this.#elements.toggle, "click", (event) => {
	      event.preventDefault(), event.stopPropagation(), this.#surface.isOpen ? this.close() : this.open();
	    }), this.#listen(this.#elements.sortToggle, "click", () => {
	      const next = this.#preferences.sortMode === "recent-viewed" ? "first-viewed" : "recent-viewed";
	      this.#run(() => this.#changeSortMode(next));
	    }), this.#listen(this.#elements.multiButton, "click", () => {
	      this.#multi = !0, this.#selectionScope = "page", this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.multiDone, "click", () => {
	      this.#multi = !1, this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.selectScope, "change", () => {
	      const selected = [...this.#elements.selectScope.options].find(
	        (option) => option.selected
	      );
	      this.#selectionScope = (selected?.value ?? this.#elements.selectScope.value) === "all" ? "all" : "page", this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.selectToggle, "click", () => {
	      const ids = this.#selectionScopeIds(), allSelected = ids.length > 0 && ids.every((id) => this.#selection.has(id));
	      for (const id of ids)
	        allSelected ? this.#selection.delete(id) : this.#selection.add(id);
	      this.#render();
	    }), this.#listen(this.#elements.search, "input", () => {
	      this.#query = (0, import_reader_search.normalizeReaderSearchText)(this.#elements.search.value), this.#page = 0, this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.searchClear, "click", () => {
	      this.#elements.search.value = "", this.#query = "", this.#page = 0, this.#selection.clear(), this.#render(), this.#elements.search.focus({ preventScroll: !0 });
	    }), this.#listen(this.#elements.categoryFilter, "change", () => {
	      this.#categoryFilter = this.#elements.categoryFilter.value, this.#page = 0, this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.tagFilter, "change", () => {
	      this.#tagFilter = this.#elements.tagFilter.value, this.#page = 0, this.#selection.clear(), this.#render();
	    }), this.#listen(this.#elements.deleteSelected, "click", () => {
	      const selectedTopicIds = [...this.#selection], count = selectedTopicIds.length;
	      count && this.#run(async () => {
	        !await this.#confirmDelete({
	          kind: "selected",
	          count,
	          title: "删除所选浏览历史?",
	          message: `将删除选中的 ${count} 条阅读器浏览历史。`,
	          note: "不会清除浏览器自身的访问历史。",
	          confirmLabel: `删除 ${count} 条`
	        }) || this.scope.destroyed || (this.#history.forgetMany(selectedTopicIds), this.#notify(`已删除 ${count} 条浏览历史`));
	      });
	    }), this.#listen(this.#elements.clearButton, "click", () => {
	      const count = this.#history.snapshot.entries.length;
	      count && this.#run(async () => {
	        !await this.#confirmDelete({
	          kind: "all",
	          count,
	          title: "清空浏览历史?",
	          message: `将删除全部 ${count} 条阅读器浏览历史。`,
	          note: "此操作无法撤销,但不会清除浏览器自身的访问历史。",
	          confirmLabel: "清空全部"
	        }) || this.scope.destroyed || (this.#history.clear(), this.#elements.search.value = "", this.#query = "", this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc", this.#page = 0, this.#selection.clear(), this.#notify("浏览历史已清空"));
	      });
	    }), this.#listen(this.#elements.list, "change", (event) => {
	      const target = event.target;
	      if (!target?.matches?.(
	        ".ldp-history-select-input"
	      )) return;
	      const checkbox = target, item = checkbox.closest(
	        "[data-history-topic-id]"
	      ), topicId = Number(item?.dataset.historyTopicId);
	      if (!(topicId > 0)) return;
	      const entry = this.#history.entry(topicId);
	      entry && (checkbox.checked ? this.#selection.add(entry.topicId) : this.#selection.delete(entry.topicId), this.#render());
	    }), this.#listen(this.#elements.list, "click", (event) => {
	      const mouseEvent = event, target = mouseEvent.target;
	      if (!target?.closest) return;
	      const item = target.closest("[data-history-topic-id]");
	      if (!item) return;
	      const entry = this.#history.entry(item.dataset.historyTopicId);
	      if (!entry || target.closest(".ldp-history-select")) return;
	      if (target.closest(".ldp-history-delete")) {
	        mouseEvent.preventDefault(), this.#history.forget(entry.topicId), this.#selection.delete(entry.topicId), this.#notify("已删除这条浏览历史");
	        return;
	      }
	      mouseEvent.preventDefault();
	      const epoch = ++this.#openEpoch;
	      this.#run(async () => {
	        await this.#openEntry(entry), !(epoch !== this.#openEpoch || this.scope.destroyed) && this.#render();
	      });
	    });
	  }
	  #render() {
	    if (this.scope.destroyed) return;
	    const entries = this.#matchingEntries();
	    this.#pruneSelection(), this.#totalMatches = entries.length, this.#totalPages = Math.max(1, Math.ceil(entries.length / this.#pageSize)), this.#page = Math.max(0, Math.min(this.#page, this.#totalPages - 1));
	    const pageEntries = entries.slice(0, (this.#page + 1) * this.#pageSize);
	    if (this.#visibleTopicIds = Object.freeze(
	      pageEntries.map((entry) => entry.topicId)
	    ), this.#elements.list.replaceChildren(), pageEntries.length)
	      for (const entry of pageEntries)
	        this.#elements.list.append(this.#renderEntry(entry));
	    else {
	      const empty = this.#document.createElement("div");
	      empty.className = "ldp-notification-empty", empty.textContent = this.#query || this.#categoryFilter || this.#tagFilter || this.#dateFilter || this.#sortDirection !== "desc" ? "没有匹配的浏览历史" : "暂无浏览历史", this.#elements.list.append(empty);
	    }
	    this.#syncControls(entries), this.#filterDisclosure.sync({
	      active: !!(this.#categoryFilter || this.#tagFilter || this.#dateFilter || this.#preferences.sortMode !== "recent-viewed" || this.#sortDirection !== "desc"),
	      date: this.#dateFilter,
	      sort: this.#preferences.sortMode,
	      direction: this.#sortDirection,
	      dayCounts: this.#dayCounts()
	    }), this.#surface.frame.meta.textContent = this.#totalMatches > 0 ? `${this.#totalMatches} 条` : "本地历史", this.#scrollWindow.sync({
	      loading: !1,
	      hasMore: this.#page < this.#totalPages - 1
	    }), this.#revision += 1;
	  }
	  #renderEntry(entry) {
	    const archiveMarker = entry.archiveStatus === null ? null : Object.freeze({
	      status: entry.archiveStatus,
	      postNumber: entry.archivePostNumber,
	      topicTitle: entry.title
	    }), displayTitle = (0, import_reader_history_repository.readerHistoryArchiveDisplayTitle)(
	      entry.title,
	      archiveMarker
	    ), item = this.#document.createElement("div");
	    if (item.className = [
	      "ldp-history-item",
	      "ldp-collection-item",
	      this.#multi ? "multi" : "",
	      this.#selection.has(entry.topicId) ? "selected" : ""
	    ].filter(Boolean).join(" "), item.dataset.historyTopicId = String(entry.topicId), item.dataset.historyActor = entry.ownerUsername, entry.archiveStatus !== null && (item.dataset.historyArchiveStatus = String(entry.archiveStatus), entry.archivePostNumber !== null && (item.dataset.historyArchivePostNumber = String(entry.archivePostNumber))), this.#multi) {
	      const select = this.#document.createElement("label");
	      select.className = "ldp-history-select ldp-collection-select", select.dataset.ldpTooltipLabel = "选择这条浏览历史";
	      const checkbox = this.#document.createElement("input");
	      checkbox.className = "ldp-history-select-input ldp-collection-select-input", checkbox.type = "checkbox", checkbox.checked = this.#selection.has(entry.topicId), checkbox.setAttribute("aria-label", `选择《${displayTitle}》`), select.append(checkbox), item.append(select);
	    }
	    const link = this.#document.createElement("a");
	    link.className = "ldp-notification-item ldp-history-link", link.href = this.#topicHref(entry), link.append(this.#renderAvatar(entry));
	    const copy = this.#document.createElement("span");
	    copy.className = "ldp-notification-copy";
	    const title = this.#document.createElement("span");
	    title.className = "ldp-notification-title", title.textContent = displayTitle;
	    const meta = this.#document.createElement("span");
	    meta.className = "ldp-notification-meta ldp-history-subtitle";
	    const sortTime = this.#preferences.sortMode === "first-viewed" ? entry.firstViewedAt : entry.viewedAt, category = entry.categoryName || (entry.categoryId === null ? "" : `类别 #${entry.categoryId}`);
	    if (meta.textContent = [
	      archiveMarker === null ? "" : (0, import_reader_history_repository.readerHistoryArchiveMarkerLabel)(archiveMarker),
	      entry.topicSubtitle || `${entry.postsCount} 帖`,
	      category,
	      ...entry.tags.map((tag) => tag.startsWith("#") ? tag : `#${tag}`),
	      this.#relativeTime(sortTime)
	    ].filter(Boolean).join(" · "), copy.append(title, meta), link.append(copy), item.append(link), !this.#multi) {
	      const remove = this.#document.createElement("button");
	      remove.type = "button", remove.className = "ldp-history-delete ldp-collection-delete", remove.setAttribute("aria-label", "删除这条浏览历史"), remove.append((0, import_reader_icon.createReaderIcon)(this.#document, "trash")), item.append(remove);
	    }
	    return item;
	  }
	  #renderAvatar(entry) {
	    const source = entry.avatarTemplate ? this.#avatarSource(entry.avatarTemplate, 64) : null;
	    let avatar;
	    if (source) {
	      const image = this.#document.createElement("img");
	      image.className = "ldp-notification-avatar", (0, import_reader_image_fallback.replaceImageWithFallbackOnError)(image, () => {
	        const fallback = this.#document.createElement("span");
	        return fallback.className = "ldp-notification-avatar ldp-notification-avatar-fallback", fallback.textContent = entry.ownerUsername.slice(0, 1).toLocaleUpperCase() || "◷", fallback;
	      }), image.src = source, image.alt = "", image.loading = "lazy", image.decoding = "async", avatar = image;
	    } else {
	      const fallback = this.#document.createElement("span");
	      fallback.className = "ldp-notification-avatar ldp-notification-avatar-fallback", entry.avatarTemplate ? fallback.textContent = entry.ownerUsername.slice(0, 1).toLocaleUpperCase() || "?" : fallback.append((0, import_reader_icon.createReaderIcon)(this.#document, "history")), avatar = fallback;
	    }
	    if (!entry.ownerUsername) return avatar;
	    const wrapper = this.#document.createElement("span");
	    return wrapper.className = "ldp-user-avatar-card", wrapper.dataset.userCard = entry.ownerUsername, wrapper.append(avatar), wrapper;
	  }
	  #syncControls(entries) {
	    const allEntries = this.#history.snapshot.entries, recent = this.#preferences.sortMode === "recent-viewed", sortLabel = recent ? "最近打开优先;点击切换为首次打开顺序" : "首次打开顺序固定;点击切换为最近打开优先";
	    this.#elements.sortToggle.replaceChildren((0, import_reader_icon.createReaderIcon)(
	      this.#document,
	      recent ? "history" : "pin"
	    )), this.#elements.sortToggle.setAttribute("aria-label", sortLabel), this.#elements.sortToggle.setAttribute(
	      "aria-pressed",
	      String(!recent)
	    ), this.#elements.sortToggle.title = sortLabel, this.#elements.defaultActions.hidden = this.#multi, this.#elements.bulkActions.hidden = !this.#multi;
	    const collectionTitle = this.#elements.bulkActions.closest(
	      ".ldp-collection-title"
	    );
	    collectionTitle && (collectionTitle.hidden = !this.#multi), this.#elements.multiButton.disabled = allEntries.length === 0, this.#elements.clearButton.disabled = allEntries.length === 0;
	    for (const option of this.#elements.selectScope.options)
	      option.selected = option.value === this.#selectionScope;
	    const scopeIds = this.#selectionScopeIds(entries), allSelected = scopeIds.length > 0 && scopeIds.every((id) => this.#selection.has(id));
	    this.#elements.selectToggle.disabled = scopeIds.length === 0, this.#elements.selectToggle.setAttribute(
	      "aria-pressed",
	      String(allSelected)
	    ), this.#elements.selectToggle.setAttribute(
	      "aria-label",
	      `${allSelected ? "全不选" : "全选"}${this.#selectionScope === "all" ? "全部记录" : "已加载"}浏览历史`
	    ), this.#elements.deleteSelected.disabled = this.#selection.size === 0, this.#elements.deleteSelectedLabel.textContent = String(this.#selection.size), this.#elements.deleteSelectedLabel.hidden = this.#selection.size === 0, this.#elements.searchClear.hidden = this.#query.length === 0, (0, import_reader_popover_filter_controls.syncReaderFilterOptions)(
	      this.#elements.categoryFilter,
	      "类别",
	      "暂无类别",
	      (0, import_reader_history_repository.readerHistoryFilterOptions)(allEntries, "category"),
	      this.#categoryFilter
	    ), (0, import_reader_popover_filter_controls.syncReaderFilterOptions)(
	      this.#elements.tagFilter,
	      "标签",
	      "暂无标签",
	      (0, import_reader_history_repository.readerHistoryFilterOptions)(allEntries, "tag"),
	      this.#tagFilter
	    );
	  }
	  #matchingEntries() {
	    const source = this.#history.ordered(this.#preferences.sortMode);
	    return (this.#sortDirection === "asc" ? [...source].reverse() : source).filter((entry) => {
	      const timestamp = this.#preferences.sortMode === "first-viewed" ? entry.firstViewedAt : entry.viewedAt;
	      return (!this.#categoryFilter || (0, import_reader_history_repository.readerHistoryCategoryFilterKey)(entry) === this.#categoryFilter) && (!this.#tagFilter || entry.tags.some((tag) => (0, import_reader_history_repository.readerHistoryTagFilterKey)(tag) === this.#tagFilter)) && (!this.#dateFilter || (0, import_reader_collection_filter_model.readerCollectionDateKey)(timestamp) === this.#dateFilter) && [
	        entry.title,
	        entry.topicSubtitle,
	        entry.categoryName,
	        ...entry.tags
	      ].some((value) => (0, import_reader_search.readerSearchMatches)(
	        value,
	        this.#query,
	        this.#searchForms,
	        this.#onError
	      ));
	    });
	  }
	  #dayCounts() {
	    const counts = /* @__PURE__ */ new Map();
	    for (const entry of this.#history.snapshot.entries) {
	      const timestamp = this.#preferences.sortMode === "first-viewed" ? entry.firstViewedAt : entry.viewedAt, day = (0, import_reader_collection_filter_model.readerCollectionDateKey)(timestamp);
	      day && counts.set(day, (counts.get(day) ?? 0) + 1);
	    }
	    return new Map([...counts].sort(([left], [right]) => left.localeCompare(right)));
	  }
	  #resetFilteredPage() {
	    this.#page = 0, this.#selection.clear(), this.#render();
	  }
	  #resetFilters() {
	    this.#elements.search.value = "", this.#query = "", this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc", this.#preferences.sortMode !== "recent-viewed" && this.#run(async () => {
	      await this.#changeSortMode("recent-viewed"), this.scope.destroyed || this.applyPreferences({ sortMode: "recent-viewed" });
	    }), this.#resetFilteredPage();
	  }
	  #selectionScopeIds(entries = this.#matchingEntries()) {
	    return this.#selectionScope === "all" ? Object.freeze(entries.map((entry) => entry.topicId)) : this.#visibleTopicIds;
	  }
	  #pruneSelection() {
	    const available = new Set(
	      this.#history.snapshot.entries.map((entry) => entry.topicId)
	    );
	    for (const topicId of [...this.#selection])
	      available.has(topicId) || this.#selection.delete(topicId);
	  }
	  #listen(target, type, listener) {
	    target.addEventListener(type, listener), this.scope.add(() => target.removeEventListener(type, listener));
	  }
	  #run(task) {
	    try {
	      const result = task();
	      result && typeof result.then == "function" && result.catch((cause) => {
	        this.#onError(cause);
	      });
	    } catch (cause) {
	      this.#onError(cause);
	    }
	  }
	  #assertActive() {
	    if (this.scope.destroyed)
	      throw new Error("ReaderHistoryPanelView 已销毁");
	  }
	}
}, "3d8b1ed1e4345d2f9e5cc5741eb4a27d0bc28d2a823e27dca8bda27f68fc5202");

/* Source: lite/src/history/reader-history-repository.ts */
runtime.register("src/history/reader-history-repository.js", function(module, exports, require) {
	var reader_history_repository_exports = {};
	__export(reader_history_repository_exports, {
	  READER_DELETED_TOPIC_TITLE: () => READER_DELETED_TOPIC_TITLE,
	  READER_HISTORY_MAX_AGE_MS: () => READER_HISTORY_MAX_AGE_MS,
	  READER_HISTORY_STORAGE_KEY: () => READER_HISTORY_STORAGE_KEY,
	  ReaderHistoryRepository: () => ReaderHistoryRepository,
	  normalizeReaderHistoryEntry: () => normalizeReaderHistoryEntry,
	  readerHistoryArchiveDisplayTitle: () => readerHistoryArchiveDisplayTitle,
	  readerHistoryArchiveIsDeletedTopic: () => readerHistoryArchiveIsDeletedTopic,
	  readerHistoryArchiveMarkerLabel: () => readerHistoryArchiveMarkerLabel,
	  readerHistoryCategoryFilterKey: () => readerHistoryCategoryFilterKey,
	  readerHistoryFilterOptions: () => readerHistoryFilterOptions,
	  readerHistoryTagFilterKey: () => readerHistoryTagFilterKey
	});
	module.exports = __toCommonJS(reader_history_repository_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_signal = require("../kernel/signal.js"), import_reader_history_model = require("./reader-history-model.js"), import_reader_account_scoped_storage = require("../state/reader-account-scoped-storage.js");
	const READER_HISTORY_STORAGE_KEY = "linuxdo-enhanced-reader:history", READER_HISTORY_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1e3, READER_DELETED_TOPIC_TITLE = "标题已删除";
	function readerHistoryArchiveIsDeletedTopic(marker) {
	  return marker !== null && marker.postNumber === null && (marker.status === 404 || marker.status === 410);
	}
	function readerHistoryArchiveDisplayTitle(title, marker) {
	  return readerHistoryArchiveIsDeletedTopic(marker) ? READER_DELETED_TOPIC_TITLE : title;
	}
	function readerHistoryArchiveMarkerLabel(marker) {
	  const target = marker.postNumber === null ? "Topic" : "楼层";
	  return marker.status === 403 ? `403 不可用 ${target}` : `${marker.status} 已删除 ${target}`;
	}
	function readerHistoryCategoryFilterKey(entry) {
	  if (entry.categoryId !== null) return `category:${entry.categoryId}`;
	  const name = entry.categoryName.trim().toLocaleLowerCase("zh-CN");
	  return name ? `category-name:${name}` : "";
	}
	function readerHistoryTagFilterKey(value) {
	  const tag = value.trim().toLocaleLowerCase("zh-CN");
	  return tag ? `tag:${tag}` : "";
	}
	function readerHistoryFilterOptions(entries, kind) {
	  const options = /* @__PURE__ */ new Map();
	  for (const entry of entries) {
	    const values = kind === "category" ? [[
	      readerHistoryCategoryFilterKey(entry),
	      entry.categoryName || `类别 #${entry.categoryId}`
	    ]] : entry.tags.map((tag) => [
	      readerHistoryTagFilterKey(tag),
	      tag
	    ]);
	    for (const [value, label] of values) {
	      if (!value) continue;
	      const current = options.get(value);
	      options.set(value, Object.freeze({
	        value,
	        label: current?.label.startsWith("类别 #") && entry.categoryName ? entry.categoryName : current?.label ?? label,
	        count: (current?.count ?? 0) + 1
	      }));
	    }
	  }
	  return Object.freeze([...options.values()].sort((left, right) => right.count - left.count || left.label.localeCompare(right.label, "zh-CN")));
	}
	function normalizedTimestamp(value) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
	}
	function nonNegativeInteger(value) {
	  const numeric = Math.floor(Number(value));
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : 0;
	}
	function normalizedHistoryText(value) {
	  return String(value ?? "").replace(/\s+/g, " ").trim();
	}
	function normalizedCategoryId(value) {
	  const numeric = Math.floor(Number(value));
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function normalizedTagNames(value) {
	  if (!Array.isArray(value)) return Object.freeze([]);
	  const names = /* @__PURE__ */ new Map();
	  for (const item of value) {
	    const source = item !== null && typeof item == "object" && !Array.isArray(item) ? item : null, name = normalizedHistoryText(
	      typeof item == "string" ? item : source?.name ?? source?.tag_name ?? source?.slug ?? (typeof source?.id == "string" ? source.id : "")
	    );
	    if (!name) continue;
	    const key = name.toLocaleLowerCase("zh-CN");
	    names.has(key) || names.set(key, name);
	  }
	  return Object.freeze([...names.values()]);
	}
	function normalizedArchiveStatus(value) {
	  const status = Number(value);
	  return status === 403 || status === 404 || status === 410 ? status : null;
	}
	function normalizedArchivePostNumber(value) {
	  try {
	    return (0, import_identifiers.discoursePostNumber)(value);
	  } catch {
	    return null;
	  }
	}
	function normalizedReadPostNumbers(values) {
	  const source = values instanceof Set ? [...values] : Array.isArray(values) ? values : [], normalized = /* @__PURE__ */ new Set();
	  for (const value of source)
	    try {
	      normalized.add((0, import_identifiers.discoursePostNumber)(value));
	    } catch {
	    }
	  return Object.freeze([...normalized].sort((left, right) => left - right));
	}
	function normalizeReaderHistoryEntry(value) {
	  if (!value || typeof value != "object" || Array.isArray(value)) return null;
	  const source = value;
	  let topicId;
	  try {
	    topicId = (0, import_identifiers.discourseTopicId)(source.topicId);
	  } catch {
	    return null;
	  }
	  const viewedAt = normalizedTimestamp(source.viewedAt);
	  if (!viewedAt) return null;
	  const readPostNumbers = normalizedReadPostNumbers(source.readPostNumbers);
	  let postNumber;
	  try {
	    postNumber = (0, import_identifiers.discoursePostNumber)(source.postNumber);
	  } catch {
	    postNumber = readPostNumbers.at(-1) ?? (0, import_identifiers.discoursePostNumber)(1);
	  }
	  const postsCount = Math.max(
	    nonNegativeInteger(source.postsCount),
	    postNumber,
	    readPostNumbers.at(-1) ?? 0
	  ), archiveStatus = normalizedArchiveStatus(source.archiveStatus);
	  return Object.freeze({
	    topicId,
	    title: String(source.title || `帖子 #${topicId}`),
	    postsCount,
	    avatarTemplate: String(source.avatarTemplate || ""),
	    ownerUsername: String(source.ownerUsername || ""),
	    topicSubtitle: normalizedHistoryText(source.topicSubtitle),
	    categoryId: normalizedCategoryId(source.categoryId),
	    categoryName: normalizedHistoryText(source.categoryName),
	    tags: normalizedTagNames(source.tags),
	    viewport: (0, import_reader_history_model.normalizeReaderHistoryViewport)(source.viewport),
	    postNumber,
	    readPostNumbers: normalizedReadPostNumbers([
	      ...readPostNumbers,
	      postNumber
	    ]),
	    archiveStatus,
	    archivePostNumber: archiveStatus === null ? null : normalizedArchivePostNumber(source.archivePostNumber),
	    firstViewedAt: normalizedTimestamp(source.firstViewedAt) || viewedAt,
	    viewedAt
	  });
	}
	function entriesEqual(left, right) {
	  return JSON.stringify(left) === JSON.stringify(right);
	}
	function quotaError(error) {
	  const candidate = error;
	  return candidate?.name === "QuotaExceededError" || candidate?.name === "NS_ERROR_DOM_QUOTA_REACHED" || candidate?.code === 22 || candidate?.code === 1014;
	}
	class ReaderHistoryRepository {
	  changes = new import_signal.Signal();
	  diagnostics = new import_signal.Signal();
	  #storage;
	  #key;
	  #accountStorage;
	  #maxAgeMs;
	  #now;
	  #snapshot = Object.freeze({
	    entries: Object.freeze([]),
	    revision: 0,
	    source: "fallback"
	  });
	  constructor(options) {
	    if (this.#storage = options.storage, this.#accountStorage = options.key === void 0 && options.authScope !== void 0 ? (0, import_reader_account_scoped_storage.readerAccountScopedStorageIdentity)(
	      READER_HISTORY_STORAGE_KEY,
	      options.authScope
	    ) : null, this.#key = String(options.key ?? this.#accountStorage?.key ?? READER_HISTORY_STORAGE_KEY).trim(), !this.#key) throw new Error("history storage key 不能为空");
	    if (this.#maxAgeMs = Number(options.maxAgeMs ?? READER_HISTORY_MAX_AGE_MS), !Number.isFinite(this.#maxAgeMs) || this.#maxAgeMs <= 0)
	      throw new RangeError("history maxAgeMs 必须是正有限数值");
	    this.#now = options.now ?? Date.now;
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  get storageKey() {
	    return this.#key;
	  }
	  load() {
	    return this.#readAndCommit("initial");
	  }
	  reloadExternal() {
	    return this.#readAndCommit("external-reload");
	  }
	  ordered(mode) {
	    const firstViewed = mode === "first-viewed";
	    return Object.freeze([...this.#snapshot.entries].sort(
	      (left, right) => (firstViewed ? right.firstViewedAt - left.firstViewedAt : right.viewedAt - left.viewedAt) || right.topicId - left.topicId
	    ));
	  }
	  entry(topicIdValue) {
	    let topicId;
	    try {
	      topicId = (0, import_identifiers.discourseTopicId)(topicIdValue);
	    } catch {
	      return null;
	    }
	    return this.#snapshot.entries.find(
	      (entry) => entry.topicId === topicId
	    ) ?? null;
	  }
	  archiveMarker(topicIdValue, postNumberValue) {
	    const entry = this.entry(topicIdValue);
	    if (entry?.archiveStatus === null || entry === null) return null;
	    const postNumber = normalizedArchivePostNumber(postNumberValue);
	    return entry.archivePostNumber !== null && entry.archivePostNumber !== postNumber ? null : Object.freeze({
	      status: entry.archiveStatus,
	      postNumber: entry.archivePostNumber,
	      topicTitle: entry.title
	    });
	  }
	  remember(input) {
	    this.#mergeStoredBeforeMutation();
	    const topicId = (0, import_identifiers.discourseTopicId)(input.topicId), previous = this.entry(topicId), now = this.#now(), inputReads = normalizedReadPostNumbers(input.readPostNumbers);
	    let postNumber;
	    try {
	      postNumber = (0, import_identifiers.discoursePostNumber)(
	        input.postNumber ?? previous?.postNumber ?? 1
	      );
	    } catch {
	      postNumber = previous?.postNumber ?? (0, import_identifiers.discoursePostNumber)(1);
	    }
	    const readPostNumbers = normalizedReadPostNumbers([
	      ...previous?.readPostNumbers ?? [],
	      ...inputReads,
	      postNumber
	    ]), archiveStatus = input.archiveStatus === void 0 ? previous?.archiveStatus ?? null : normalizedArchiveStatus(input.archiveStatus), archivePostNumber = archiveStatus === null ? null : input.archiveStatus === void 0 ? previous?.archivePostNumber ?? null : normalizedArchivePostNumber(input.archivePostNumber), entry = Object.freeze({
	      topicId,
	      title: String(
	        input.title || previous?.title || `帖子 #${topicId}`
	      ),
	      postsCount: Math.max(
	        nonNegativeInteger(input.postsCount),
	        previous?.postsCount ?? 0,
	        postNumber,
	        readPostNumbers.at(-1) ?? 0
	      ),
	      avatarTemplate: String(
	        input.avatarTemplate || previous?.avatarTemplate || ""
	      ),
	      ownerUsername: String(
	        input.ownerUsername || previous?.ownerUsername || ""
	      ),
	      topicSubtitle: input.topicSubtitle === void 0 ? previous?.topicSubtitle ?? "" : normalizedHistoryText(input.topicSubtitle),
	      categoryId: input.categoryId === void 0 ? previous?.categoryId ?? null : normalizedCategoryId(input.categoryId),
	      categoryName: input.categoryName === void 0 ? previous?.categoryName ?? "" : normalizedHistoryText(input.categoryName),
	      tags: input.tags === void 0 ? previous?.tags ?? Object.freeze([]) : normalizedTagNames(input.tags),
	      viewport: input.viewport === void 0 ? previous?.viewport ?? null : (0, import_reader_history_model.normalizeReaderHistoryViewport)(input.viewport),
	      postNumber,
	      readPostNumbers,
	      archiveStatus,
	      archivePostNumber,
	      firstViewedAt: previous?.firstViewedAt || now,
	      viewedAt: now
	    });
	    return this.#persistAndCommit(
	      Object.freeze([
	        entry,
	        ...this.#snapshot.entries.filter(
	          (candidate) => candidate.topicId !== topicId
	        )
	      ]),
	      "remember"
	    );
	  }
	  forget(topicIdValue) {
	    return this.forgetMany([topicIdValue]);
	  }
	  forgetMany(topicIdValues) {
	    this.#mergeStoredBeforeMutation();
	    const topicIds = /* @__PURE__ */ new Set();
	    for (const value of topicIdValues)
	      try {
	        topicIds.add((0, import_identifiers.discourseTopicId)(value));
	      } catch {
	      }
	    if (!topicIds.size) return this.#snapshot;
	    const next = this.#snapshot.entries.filter(
	      (entry) => !topicIds.has(entry.topicId)
	    );
	    return next.length === this.#snapshot.entries.length ? this.#snapshot : this.#persistAndCommit(Object.freeze(next), "forget");
	  }
	  clear() {
	    return this.#mergeStoredBeforeMutation(), this.#persistAndCommit(Object.freeze([]), "clear");
	  }
	  replaceExternal(values) {
	    const cutoff = this.#now() - this.#maxAgeMs, entries = [], seen = /* @__PURE__ */ new Set();
	    for (const value of values) {
	      const entry = normalizeReaderHistoryEntry(value);
	      !entry || entry.viewedAt < cutoff || seen.has(entry.topicId) || (seen.add(entry.topicId), entries.push(entry));
	    }
	    const persisted = this.#persist(Object.freeze(entries));
	    return this.#commit(persisted, "external-sync");
	  }
	  #readAndCommit(source) {
	    let raw;
	    try {
	      const stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key);
	      raw = stored === null ? [] : JSON.parse(stored);
	    } catch (cause) {
	      return this.#diagnose("read-failed", cause), this.#commit(Object.freeze([]), "fallback");
	    }
	    if (!Array.isArray(raw))
	      return this.#diagnose(
	        "invalid-stored-value",
	        new TypeError("阅读历史存储值必须是数组")
	      ), this.#commit(Object.freeze([]), "fallback");
	    const cutoff = this.#now() - this.#maxAgeMs, entries = [], seen = /* @__PURE__ */ new Set();
	    for (const value of raw) {
	      const entry = normalizeReaderHistoryEntry(value);
	      !entry || entry.viewedAt < cutoff || seen.has(entry.topicId) || (seen.add(entry.topicId), entries.push(entry));
	    }
	    const frozen = Object.freeze(entries), rawNormalized = raw.map(normalizeReaderHistoryEntry).filter((entry) => entry !== null);
	    if (!entriesEqual(frozen, rawNormalized)) {
	      this.#diagnose(
	        "entries-normalized",
	        Object.freeze({
	          storedCount: raw.length,
	          acceptedCount: frozen.length
	        })
	      );
	      try {
	        this.#persist(frozen);
	      } catch {
	      }
	    }
	    return this.#commit(frozen, source);
	  }
	  #mergeStoredBeforeMutation() {
	    let stored;
	    try {
	      stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(this.#storage, this.#accountStorage) : this.#storage.getItem(this.#key);
	    } catch (cause) {
	      this.#diagnose("read-failed", cause);
	      return;
	    }
	    (stored ?? "[]") !== JSON.stringify(this.#snapshot.entries) && this.#readAndCommit("external-reload");
	  }
	  #persistAndCommit(entries, source) {
	    const persisted = this.#persist(entries);
	    return this.#commit(persisted, source);
	  }
	  #persist(entries) {
	    const safe = [...entries];
	    for (; ; )
	      try {
	        return safe.length ? this.#storage.setItem(this.#key, JSON.stringify(safe)) : this.#storage.removeItem && !this.#accountStorage ? this.#storage.removeItem(this.#key) : this.#storage.setItem(this.#key, "[]"), Object.freeze(safe);
	      } catch (cause) {
	        if (!quotaError(cause) || !safe.length)
	          throw this.#diagnose("write-failed", cause), cause;
	        const removed = safe.pop();
	        this.#diagnose("quota-trimmed", Object.freeze({
	          topicId: removed?.topicId ?? null,
	          remainingCount: safe.length
	        }));
	      }
	  }
	  #commit(entries, source) {
	    const snapshot = Object.freeze({
	      entries: Object.freeze([...entries]),
	      revision: this.#snapshot.revision + 1,
	      source
	    });
	    this.#snapshot = snapshot;
	    for (const cause of this.changes.emit(snapshot))
	      this.#diagnose("consumer-failed", cause);
	    return snapshot;
	  }
	  #diagnose(code, cause) {
	    this.diagnostics.emit(Object.freeze({ code, cause }));
	  }
	}
}, "ae73da022ef835f9162058dd24d22f3bea8357614a327b9aab9033034bc763d0");

/* Source: lite/src/monitor/reader-resource-monitor.ts */
runtime.register("src/monitor/reader-resource-monitor.js", function(module, exports, require) {
	var reader_resource_monitor_exports = {};
	__export(reader_resource_monitor_exports, {
	  ReaderResourceMonitor: () => ReaderResourceMonitor
	});
	module.exports = __toCommonJS(reader_resource_monitor_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_browser_request_observation = require("../network/browser-request-observation.js"), import_reader_icon = require("../components/reader-icon.js"), import_reader_settings_dom = require("../settings/reader-settings-dom.js");
	const RETENTION_MS = 10 * 6e4, REQUEST_RUNTIME_RETENTION_MS = 15 * 6e4, EVIDENCE_WINDOW_MS = 6e4, MAX_EVIDENCE_EVENTS = 1200, MAX_REQUEST_RUNTIME_STATES = 1200, REQUEST_TRACE_MS = 1e4, REQUEST_SLOW_MS = 1800, REQUEST_STUCK_MS = 15e3, REQUEST_TYPE_LABELS = Object.freeze({
	  topic: "正文",
	  nested: "二级回复",
	  avatar: "头像",
	  media: "图片媒体",
	  asset: "静态资源",
	  bookmark: "收藏",
	  notification: "消息",
	  realtime: "实时通道",
	  presence: "在线状态",
	  search: "搜索",
	  read: "已读上报",
	  user: "用户资料",
	  reaction: "回应操作",
	  other: "其他"
	}), REQUEST_WAIT_REASON_LABELS = Object.freeze({
	  scheduler: "中央调度",
	  priority: "优先级",
	  concurrency: "并发槽",
	  interval: "启动间隔",
	  "10s": "10 秒窗口",
	  "60s": "60 秒窗口",
	  challenge: "Cloudflare 验证",
	  "queue-limit": "队列上限",
	  "viewport-change": "视口变化",
	  "priority-upgrade": "快车道升级",
	  "topic-switch": "切换帖子",
	  "topic-close": "离开帖子",
	  "context-close": "上下文结束",
	  "context-closed": "上下文结束",
	  signal: "主动取消",
	  cancelled: "主动取消"
	}), EXPECTED_CANCELLATION_REASONS = /* @__PURE__ */ new Set([
	  "cancelled",
	  "context-close",
	  "context-closed",
	  "priority-upgrade",
	  "signal",
	  "topic-close",
	  "topic-switch",
	  "viewport-change"
	]), EMPTY_REQUEST_LANE_COUNTS = Object.freeze({
	  control: 0,
	  "topic-batch": 0,
	  "nested-replies": 0,
	  "user-card": 0,
	  translation: 0,
	  standard: 0
	}), METRICS = Object.freeze([
	  {
	    id: "heap",
	    label: "页面内存估计",
	    detail: "浏览器原生测量;不可用时明确显示未提供",
	    values: (sample) => [sample.heapBytes],
	    format: ([value]) => value == null ? "浏览器未提供" : formatBytes(value)
	  },
	  {
	    id: "longTasks",
	    label: "主线程卡顿",
	    detail: "最近 10 秒 Long Tasks / Long Animation Frames",
	    values: (sample) => [
	      sample.longTasks + sample.longFrames,
	      sample.longTaskDuration
	    ],
	    format: ([count, duration]) => `${Math.round(count ?? 0)} 次 · ${Math.round(duration ?? 0)} ms`
	  },
	  {
	    id: "dom",
	    label: "页面元素",
	    detail: "阅读器 / 原站当前 DOM",
	    values: (sample) => [sample.readerDom, sample.hostDom],
	    format: ([reader, host]) => `${Math.round(reader ?? 0)} / ${Math.round(host ?? 0)} 个`
	  },
	  {
	    id: "floors",
	    label: "楼层保留",
	    detail: "已挂载 / 当前帖子会话保留(不等于持久缓存总量)",
	    values: (sample) => [sample.mountedFloors, sample.retainedFloors],
	    format: ([mounted, retained]) => `${Math.round(mounted ?? 0)} / ${Math.round(retained ?? 0)} 个`
	  },
	  {
	    id: "nested",
	    label: "树状楼层",
	    detail: "当前回复拓扑中的嵌套关系",
	    values: (sample) => [sample.nestedFloors],
	    format: ([value]) => `${Math.round(value ?? 0)} 个`
	  },
	  {
	    id: "media",
	    label: "媒体资源",
	    detail: "Reader 内图片、音视频与 iframe",
	    values: (sample) => [sample.media],
	    format: ([value]) => `${Math.round(value ?? 0)} 个`
	  },
	  {
	    id: "requests",
	    label: "请求调度",
	    detail: "当前生效调度器活动 / 排队(不是设置目标值)",
	    values: (sample) => [sample.activeRequests, sample.queuedRequests],
	    format: ([active, queued]) => `${Math.round(active ?? 0)} / ${Math.round(queued ?? 0)} 条`
	  },
	  {
	    id: "network",
	    label: "最近网络",
	    detail: "最近 60 秒被动观测次数 / 传输量",
	    values: (sample) => [sample.networkRequests, sample.networkBytes],
	    format: ([count, bytes]) => `${Math.round(count ?? 0)} 次 · ${formatBytes(bytes ?? 0)}`
	  }
	]);
	function formatBytes(rawBytes) {
	  const bytes = Math.max(0, Number(rawBytes) || 0);
	  return bytes < 1024 ? `${Math.round(bytes)} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
	}
	function formatDuration(milliseconds) {
	  const value = Math.max(0, Number(milliseconds) || 0);
	  return value < 1e3 ? `${Math.round(value)} ms` : `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)} s`;
	}
	function formatPolicyNumber(raw) {
	  const value = Number(raw);
	  return Number.isFinite(value) ? String(Number(value.toFixed(2))) : "—";
	}
	function formatRequestTimestamp(raw) {
	  const date = new Date(raw);
	  return `${date.toLocaleTimeString("zh-CN", {
	    hour12: !1,
	    hour: "2-digit",
	    minute: "2-digit",
	    second: "2-digit"
	  })}.${String(date.getMilliseconds()).padStart(3, "0")}`;
	}
	function requestDisplayPath(event) {
	  return `${event.path}${event.queryShape}`;
	}
	const REQUEST_DECISION_LABELS = Object.freeze({
	  complete: "完成",
	  "retry-429": "等待 429 重试",
	  "stop-429": "429 终止",
	  "await-cloudflare": "等待过盾",
	  "require-cloudflare": "转人工验证",
	  "challenge-passed-retry": "过盾后重试",
	  "challenge-required": "验证未通过",
	  "stop-cloudflare-isolated": "Cloudflare 隔离终止",
	  "stop-cloudflare-unhandled": "Cloudflare 无恢复端口",
	  "stop-http": "HTTP 终止",
	  "challenge-probe-passed": "会话探针通过",
	  "challenge-probe-rate-limited-pass": "会话已过盾但仍 429",
	  "challenge-probe-blocked": "会话探针仍被盾拦截",
	  "challenge-probe-failed": "会话探针失败",
	  "challenge-probe-cancelled": "会话探针取消"
	});
	function requestDecisionLabel(decision) {
	  return REQUEST_DECISION_LABELS[decision] ?? REQUEST_WAIT_REASON_LABELS[decision] ?? decision;
	}
	function requestContractDiagnostic(event) {
	  const contract = [event.profile, event.namespace, event.lane].filter(Boolean).join(" / ");
	  return [
	    event.logicalId ? `链 ${event.logicalId}` : "",
	    contract,
	    event.cacheMode ? `缓存 ${event.cacheMode}` : "",
	    event.identity ? `身份 ${event.identity}` : "",
	    event.logicalId ? `尝试 ${event.attempt}/${event.max429Retries + event.maxChallengeRetries + 1}` : event.attempt > 1 ? `第 ${event.attempt} 次尝试` : "",
	    event.logicalId ? `重试上限 429 ${event.max429Retries} · 过盾 ${event.maxChallengeRetries}` : "",
	    event.blockOnCloudflareChallenge === null ? "" : event.blockOnCloudflareChallenge ? "过盾 共享闸门" : "过盾 仅结束本请求",
	    event.suppressAfterChallengeWait ? "盾后不追发" : "",
	    event.droppable === !0 ? "可丢弃" : "",
	    event.promoted ? "已晋升" : "",
	    event.joinedConsumers ? `单飞合并 +${event.joinedConsumers}` : "",
	    event.decision ? `决策 ${requestDecisionLabel(event.decision)}` : "",
	    event.retryAfter ? `Retry-After ${event.retryAfter} 秒` : ""
	  ].filter(Boolean).join(" · ");
	}
	const PERFORMANCE_EVENT_LABELS = Object.freeze({
	  request: "网络请求",
	  longtask: "长任务",
	  "long-animation-frame": "长动画帧",
	  script: "脚本归因",
	  dom: "DOM 变更",
	  visibility: "前后台",
	  capture: "采集状态",
	  gap: "采样空档"
	});
	function performanceEventMetric(event) {
	  return event.kind === "dom" ? `+${event.added ?? 0} / −${event.removed ?? 0}` : event.duration > 0 ? formatDuration(event.duration) : "瞬时";
	}
	function diagnosticIsoTime(at) {
	  return new Date(at).toISOString();
	}
	function diagnosticLogFilename(kind, at) {
	  const timestamp = diagnosticIsoTime(at).replace(/\.\d{3}Z$/, "Z").replace(/[:]/g, "-").replace("T", "_");
	  return `linuxdo-reader-${kind}-log-${timestamp}.jsonl`;
	}
	function diagnosticJsonLines(records) {
	  return `${records.map((record) => JSON.stringify(record)).join(`
`)}
`;
	}
	function requestDiagnosticRecord(event, visibility = "unknown") {
	  return Object.freeze({
	    recordType: "request",
	    id: event.id,
	    logicalId: event.logicalId,
	    phase: event.phase,
	    visibility,
	    queuedAt: event.queuedAt,
	    queuedAtIso: diagnosticIsoTime(event.queuedAt),
	    permittedAt: event.permittedAt,
	    startedAt: event.startedAt,
	    endedAt: event.endedAt,
	    permitWaitMs: event.permitWait,
	    dispatchDurationMs: event.dispatchDuration,
	    durationMs: event.duration,
	    method: event.method,
	    path: event.path,
	    queryShape: event.queryShape,
	    transport: event.transport,
	    source: event.source,
	    type: event.type,
	    sameOrigin: event.sameOrigin,
	    priority: event.priority,
	    attempt: event.attempt,
	    recoveryProbe: event.recoveryProbe,
	    waitReason: event.waitReason,
	    callSite: event.callSite,
	    controlReason: event.controlReason,
	    profile: event.profile,
	    namespace: event.namespace,
	    lane: event.lane,
	    cacheMode: event.cacheMode,
	    identity: event.identity,
	    joinedConsumers: event.joinedConsumers,
	    promoted: event.promoted,
	    max429Retries: event.max429Retries,
	    maxChallengeRetries: event.maxChallengeRetries,
	    blockOnCloudflareChallenge: event.blockOnCloudflareChallenge,
	    suppressAfterChallengeWait: event.suppressAfterChallengeWait,
	    droppable: event.droppable,
	    decision: event.decision,
	    pending: event.pending,
	    status: event.status,
	    cloudflareMitigated: event.cloudflareMitigated,
	    sizeBytes: event.size,
	    error: event.error,
	    rateLimitCode: event.rateLimitCode,
	    retryAfter: event.retryAfter,
	    serverLimit: event.serverLimit,
	    serverRemaining: event.serverRemaining,
	    serverReset: event.serverReset,
	    resourceTimed: event.resourceTimed
	  });
	}
	function schedulerDiagnosticRecord(snapshot) {
	  return snapshot ? Object.freeze({
	    active: snapshot.active,
	    queued: snapshot.queued,
	    maxConcurrent: snapshot.maxConcurrent,
	    queueLimit: snapshot.queueLimit,
	    disposed: snapshot.disposed,
	    activeByLane: snapshot.activeByLane,
	    queuedByLane: snapshot.queuedByLane
	  }) : null;
	}
	function permitDiagnosticRecord(snapshot) {
	  return snapshot ? Object.freeze({
	    coordinationMode: snapshot.coordinationMode,
	    shortCount: snapshot.shortCount,
	    shortBudget: snapshot.shortBudget,
	    longCount: snapshot.longCount,
	    longBudget: snapshot.longBudget,
	    minIntervalMs: snapshot.minIntervalMs,
	    maxConcurrent: snapshot.maxConcurrent,
	    instances: snapshot.instances,
	    queued: snapshot.queued,
	    active: snapshot.active,
	    nextPermitDelayMs: snapshot.nextPermitDelay,
	    blockingReason: snapshot.blockingReason,
	    challengeState: snapshot.challengeState,
	    challengeOwned: snapshot.challengeOwned
	  }) : null;
	}
	function performanceScriptLabel(sourceFunctionName, sourceUrl, baseHref) {
	  const functionName = String(sourceFunctionName ?? "").replace(/[\r\n\t]+/g, " ").trim().slice(0, 80);
	  if (functionName) return functionName;
	  try {
	    const source = String(sourceUrl ?? ""), url = /^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(source) ? new URL(source) : new URL(source, baseHref);
	    return url.username = "", url.password = "", url.search = "", url.hash = "", `${url.origin}${url.pathname}`.slice(0, 120);
	  } catch {
	    return "匿名脚本";
	  }
	}
	function percentile95(values) {
	  if (!values.length) return 0;
	  const sorted = [...values].sort((left, right) => left - right);
	  return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;
	}
	function requestIssue(event, at) {
	  const cancellationReason = event.controlReason || (event.phase === "cancelled" ? event.error : "");
	  if (EXPECTED_CANCELLATION_REASONS.has(cancellationReason)) return null;
	  if (event.controlReason)
	    return (event.controlReason || event.waitReason || "cancelled") === "queue-limit" ? {
	      level: "warning",
	      label: "队列已满",
	      detail: "辅助请求在发出前被队列上限丢弃"
	    } : {
	      level: "warning",
	      label: "调度取消",
	      detail: `请求未发出(${requestWaitReasonLabel(event.waitReason)})`
	    };
	  if (event.status === 429)
	    return {
	      level: "danger",
	      label: event.cloudflareMitigated ? "Cloudflare 429" : "429 限流",
	      detail: event.cloudflareMitigated ? "Cloudflare managed challenge 拒绝了当前请求" : event.rateLimitCode || "服务器拒绝了当前请求速率"
	    };
	  if ((event.status ?? 0) >= 400)
	    return {
	      level: "danger",
	      label: `HTTP ${event.status}`,
	      detail: (event.status ?? 0) >= 500 ? "服务器或上游服务返回错误" : "请求参数、权限或目标状态异常"
	    };
	  if (event.error) {
	    const aborted = /abort/i.test(event.error);
	    return {
	      level: aborted ? "warning" : "danger",
	      label: aborted ? "中止/超时" : "网络错误",
	      detail: event.error
	    };
	  }
	  if (event.phase === "queued") {
	    const wait = Math.max(0, at - event.queuedAt);
	    return wait >= REQUEST_STUCK_MS ? {
	      level: "danger",
	      label: "排队卡住",
	      detail: `等待放行已持续 ${formatDuration(wait)}`
	    } : wait >= 1e3 ? {
	      level: "warning",
	      label: "排队过久",
	      detail: `等待放行已持续 ${formatDuration(wait)}`
	    } : null;
	  }
	  if (event.permitWait >= 1e3)
	    return {
	      level: "warning",
	      label: "排队过久",
	      detail: `${requestWaitReasonLabel(event.waitReason)}等待 ` + formatDuration(event.permitWait)
	    };
	  const duration = event.pending ? Math.max(0, at - event.startedAt) : event.duration;
	  return event.pending && !["realtime", "presence"].includes(event.type) && duration >= REQUEST_STUCK_MS ? {
	    level: "danger",
	    label: "请求卡住",
	    detail: `网络阶段已持续 ${formatDuration(duration)}`
	  } : !event.pending && !["realtime", "presence"].includes(event.type) && duration >= REQUEST_SLOW_MS ? {
	    level: "warning",
	    label: "响应偏慢",
	    detail: `网络阶段耗时 ${formatDuration(duration)}`
	  } : null;
	}
	function requestWaitReasonLabel(reason) {
	  return REQUEST_WAIT_REASON_LABELS[reason] ?? "调度";
	}
	function requestPriorityLabel(priority) {
	  return priority === "critical" ? "核心" : priority === "interactive" ? "交互插队" : priority === "visible" ? "可见" : priority === "nested" ? "树状可见" : priority === "prefetch" ? "辅助预取" : priority === "background" ? "后台" : "";
	}
	function requestTimingLabel(event, at) {
	  const permitWait = event.phase === "queued" ? Math.max(0, at - event.queuedAt) : event.permitWait, queue = permitWait > 0 ? `排 ${formatDuration(permitWait)} · ` : "";
	  if (event.phase === "queued") return `${queue}等待放行`;
	  if (event.controlReason) return `${queue}未发出`;
	  const dispatch = event.dispatchDuration >= 1 ? `放行 ${formatDuration(event.dispatchDuration)} · ` : "", network = event.pending ? Math.max(0, at - event.startedAt) : event.duration;
	  return `${queue}${dispatch}网 ${formatDuration(network)}`;
	}
	function requestStatus(event) {
	  if (event.phase === "queued") return "排队中";
	  if (event.phase === "running") return "进行中";
	  if (event.phase === "cancelled") {
	    const reason = event.controlReason || event.error;
	    return reason === "viewport-change" ? "滚动取消" : reason === "priority-upgrade" ? "快车道升级" : reason === "topic-switch" ? "切帖取消" : ["topic-close", "context-close", "context-closed"].includes(reason) ? "离页取消" : reason === "queue-limit" ? "已丢弃" : "已取消";
	  }
	  return event.status !== null && event.status > 0 ? String(event.status) : event.error ? "ERR" : "完成";
	}
	function aggregate(samples, metric, kind) {
	  const width = metric.values(samples.at(-1) ?? emptySample()).length;
	  return Object.freeze(Array.from({ length: width }, (_, index) => {
	    const values = samples.map((sample) => metric.values(sample)[index]).filter((value) => value !== null);
	    return values.length ? kind === "peak" ? Math.max(...values) : values.reduce((sum, value) => sum + value, 0) / values.length : null;
	  }));
	}
	function chartPoints(samples, value) {
	  const entries = samples.map((sample) => ({ at: sample.at, value: value(sample) })).filter((entry) => entry.value !== null && Number.isFinite(entry.value));
	  if (!entries.length) return "";
	  if (entries.length === 1) return "0,17 240,17";
	  const stride = Math.max(1, Math.ceil(entries.length / 120)), plotted = entries.filter((_entry, index) => index % stride === 0 || index === entries.length - 1), values = plotted.map((entry) => entry.value), minimum = Math.min(...values), range = Math.max(...values) - minimum || 1;
	  return plotted.map((entry, index) => {
	    const x = index / Math.max(1, plotted.length - 1) * 240, y = 31 - (entry.value - minimum) / range * 28;
	    return `${x.toFixed(1)},${y.toFixed(1)}`;
	  }).join(" ");
	}
	function emptySample() {
	  return {
	    at: 0,
	    visibility: "visible",
	    heapBytes: null,
	    longTasks: 0,
	    longTaskDuration: 0,
	    longFrames: 0,
	    readerDom: 0,
	    hostDom: 0,
	    topicId: null,
	    mountedFloors: 0,
	    preparedFloors: 0,
	    retainedFloors: 0,
	    nestedFloors: 0,
	    media: 0,
	    initializedFromCache: !1,
	    expectedFloors: 0,
	    streamFloors: 0,
	    missingFloors: 0,
	    unavailableFloors: Object.freeze([]),
	    mediaDiagnostics: Object.freeze({
	      catalogImages: 0,
	      catalogComplete: !1,
	      catalogPending: !1,
	      catalogFailedBatches: 0,
	      persistentCacheEnabled: !1,
	      objectUrls: 0,
	      objectUrlLimit: 0,
	      boundImages: 0,
	      failedImages: 0,
	      retryingImages: 0,
	      crossOriginFailures: 0,
	      failedPostNumbers: Object.freeze([]),
	      unavailableSourcePostNumbers: Object.freeze([]),
	      hlsSources: 0,
	      nativeHlsSources: 0,
	      activeHlsPlayers: 0,
	      hlsLibraryAvailable: !1,
	      hlsLibrarySupported: !1,
	      nativeManagedMediaSource: !1
	    }),
	    activeRequests: 0,
	    queuedRequests: 0,
	    requestMaxConcurrent: 0,
	    requestQueueLimit: 0,
	    requestActiveByLane: EMPTY_REQUEST_LANE_COUNTS,
	    requestQueuedByLane: EMPTY_REQUEST_LANE_COUNTS,
	    sharedActiveRequests: 0,
	    sharedQueuedRequests: 0,
	    sharedMaxConcurrent: 0,
	    sharedMinIntervalMs: 0,
	    sharedInstances: 0,
	    sharedNextPermitDelay: 0,
	    sharedBlockingReason: "",
	    sharedCoordinationMode: "unavailable",
	    shortWindowCount: 0,
	    shortWindowBudget: 0,
	    longWindowCount: 0,
	    longWindowBudget: 0,
	    challengeState: "idle",
	    challengeOwned: !1,
	    networkRequests: 0,
	    networkBytes: 0
	  };
	}
	class ReaderResourceMonitor {
	  scope;
	  requests;
	  #options;
	  #root;
	  #health;
	  #healthState;
	  #healthDetail;
	  #updated;
	  #performancePolicy;
	  #rows = /* @__PURE__ */ new Map();
	  #trendWindow;
	  #trendRows = /* @__PURE__ */ new Map();
	  #evidenceWindow;
	  #scopeRows = /* @__PURE__ */ new Map();
	  #eventLog;
	  #requestMetrics = /* @__PURE__ */ new Map();
	  #requestWindow;
	  #requestTrace;
	  #requestLegend;
	  #requestBottleneck;
	  #requestBottleneckState;
	  #requestBottleneckDetail;
	  #topicDiagnostics;
	  #mediaDiagnostics;
	  #requestAnomalyWindow;
	  #requestAnomalies;
	  #requestTypes;
	  #requestLog;
	  #requestObserved;
	  #samples = [];
	  #performanceEvents = [];
	  #visibilityTimeline = [];
	  #requestRuntimeStates = [];
	  #requestVisibility = /* @__PURE__ */ new Map();
	  #activeScope = null;
	  #activePanel = null;
	  #selectedPanel = "request";
	  #heapBytes = null;
	  #memoryMeasuring = !1;
	  #lastMemoryAt = 0;
	  #baselineAt = 0;
	  #resourceObservationCapability = "not-attempted";
	  #longTaskCapability = "not-attempted";
	  #longAnimationFrameCapability = "not-attempted";
	  #readerMutationCapability = "not-attempted";
	  #hostMutationCapability = "not-attempted";
	  #evidenceOverflowDrops = 0;
	  #evidenceRetentionDrops = 0;
	  #sampleRetentionDrops = 0;
	  #visibilityRetentionDrops = 0;
	  #requestRuntimeRetentionDrops = 0;
	  #requestRuntimeOverflowDrops = 0;
	  constructor(options) {
	    this.#options = options, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.requests = options.requests;
	    const document = options.document;
	    this.#root = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-resource-monitor"
	    );
	    const tabs = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-log-tabs"
	    );
	    tabs.role = "tablist", tabs.setAttribute("aria-label", "日志记录类型");
	    const requestTab = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "button",
	      "ldp-settings-log-tab active"
	    );
	    requestTab.type = "button", requestTab.role = "tab", requestTab.dataset.settingsLogTab = "request", requestTab.setAttribute("aria-selected", "true"), requestTab.textContent = "请求记录";
	    const performanceTab = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "button",
	      "ldp-settings-log-tab"
	    );
	    performanceTab.type = "button", performanceTab.role = "tab", performanceTab.dataset.settingsLogTab = "performance", performanceTab.setAttribute("aria-selected", "false"), performanceTab.tabIndex = -1, performanceTab.textContent = "性能记录", tabs.append(requestTab, performanceTab);
	    const requestPanel = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-log-panel"
	    );
	    requestPanel.dataset.settingsLogPanel = "request", requestPanel.role = "tabpanel";
	    const performancePanel = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-log-panel"
	    );
	    performancePanel.dataset.settingsLogPanel = "performance", performancePanel.role = "tabpanel", performancePanel.hidden = !0;
	    const requestExport = this.#createExportControl(
	      "request",
	      "导出请求日志",
	      "导出当前内存中的完整脱敏请求账本、调度和共享限流快照(JSONL)。"
	    ), performanceExport = this.#createExportControl(
	      "performance",
	      "导出性能日志",
	      "导出十分钟快照、毫秒事件、前后台时间线、关联请求及能力/缺口声明(JSONL)。"
	    ), selectPanel = (name) => {
	      this.#selectedPanel = name;
	      const requestActive = name === "request";
	      requestTab.classList.toggle("active", requestActive), requestTab.setAttribute("aria-selected", String(requestActive)), requestTab.tabIndex = requestActive ? 0 : -1, performanceTab.classList.toggle("active", !requestActive), performanceTab.setAttribute(
	        "aria-selected",
	        String(!requestActive)
	      ), performanceTab.tabIndex = requestActive ? -1 : 0, requestPanel.hidden = !requestActive, performancePanel.hidden = requestActive, this.active && this.start(name);
	    };
	    this.scope.listen(requestTab, "click", () => selectPanel("request")), this.scope.listen(
	      performanceTab,
	      "click",
	      () => selectPanel("performance")
	    ), this.#health = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-health"
	    ), this.#health.dataset.level = "normal", this.#health.role = "status", this.#healthState = (0, import_reader_settings_dom.settingsElement)(document, "strong"), this.#healthState.textContent = "等待打开日志面板", this.#healthDetail = (0, import_reader_settings_dom.settingsElement)(document, "p"), this.#healthDetail.textContent = "打开本面板后建立新基线,离开面板即停止采集。", this.#updated = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "span",
	      "ldp-resource-monitor-updated"
	    ), this.#updated.textContent = "未采样", this.#health.append(
	      this.#healthState,
	      this.#healthDetail,
	      this.#updated
	    );
	    const performancePolicyBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-limit"
	    ), performancePolicyTitle = (0, import_reader_settings_dom.settingsElement)(document, "h4");
	    performancePolicyTitle.textContent = "当前生效性能策略", performancePolicyBlock.dataset.resourceMonitorPolicyBlock = "", this.#performancePolicy = (0, import_reader_settings_dom.settingsElement)(document, "p"), this.#performancePolicy.dataset.resourceMonitorPolicy = "", this.#performancePolicy.textContent = "等待性能快照。", performancePolicyBlock.append(
	      performancePolicyTitle,
	      this.#performancePolicy
	    );
	    const table = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-table"
	    );
	    table.role = "table", table.setAttribute("aria-label", "阅读器实时资源数据");
	    const tableHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-table-head"
	    );
	    tableHead.role = "row";
	    for (const label of ["指标", "当前", "平均", "峰值", "变化趋势"]) {
	      const cell = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      cell.textContent = label, tableHead.append(cell);
	    }
	    table.append(tableHead);
	    for (const metric of METRICS) {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-resource-monitor-row"
	      );
	      row.role = "row", row.dataset.resourceMonitorRow = metric.id;
	      const copy = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "span",
	        "ldp-resource-monitor-copy"
	      ), label = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	      label.textContent = metric.label;
	      const detail = (0, import_reader_settings_dom.settingsElement)(document, "small");
	      detail.textContent = metric.detail, copy.append(label, detail);
	      const current = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "strong",
	        "ldp-resource-monitor-current"
	      );
	      current.dataset.resourceMonitorMetric = metric.id, current.textContent = "—";
	      const summary = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "span",
	        "ldp-resource-monitor-summary"
	      );
	      for (const key of ["average", "peak", "trend"]) {
	        const value = (0, import_reader_settings_dom.settingsElement)(document, "span");
	        value.dataset[`resourceMonitor${key[0].toUpperCase()}${key.slice(1)}`] = metric.id, value.textContent = "—", summary.append(value);
	      }
	      row.append(copy, current, summary), table.append(row), this.#rows.set(metric.id, row);
	    }
	    const evidence = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-resource-monitor-evidence"
	    ), evidenceHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-evidence-head"
	    ), evidenceTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    evidenceTitle.textContent = "最近 60 秒前后台实测", this.#evidenceWindow = (0, import_reader_settings_dom.settingsElement)(document, "span"), this.#evidenceWindow.textContent = "等待事件", evidenceHead.append(evidenceTitle, this.#evidenceWindow);
	    const scopeTable = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-scope-table"
	    );
	    scopeTable.role = "table", scopeTable.setAttribute(
	      "aria-label",
	      "阅读器与原站前后台资源记录"
	    );
	    const scopeHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-scope-head"
	    );
	    scopeHead.role = "row";
	    for (const copy of ["范围", "当前结构", "前台事件", "后台事件"]) {
	      const cell = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      cell.textContent = copy, scopeHead.append(cell);
	    }
	    scopeTable.append(scopeHead);
	    for (const [scope, label] of [
	      ["reader", "阅读器"],
	      ["host", "原站 / 未标记"],
	      ["shared", "页面共享"]
	    ]) {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-resource-monitor-scope-row"
	      );
	      row.role = "row", row.dataset.resourceMonitorScope = scope;
	      const name = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	      name.textContent = label;
	      const current = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      current.dataset.resourceMonitorScopeCurrent = "", current.textContent = "—";
	      const visible = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      visible.dataset.resourceMonitorScopeVisible = "", visible.textContent = "—";
	      const hidden = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      hidden.dataset.resourceMonitorScopeHidden = "", hidden.textContent = "—", row.append(name, current, visible, hidden), scopeTable.append(row), this.#scopeRows.set(scope, row);
	    }
	    evidence.append(evidenceHead, scopeTable);
	    const trends = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-resource-monitor-trends"
	    ), trendHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-trend-head"
	    ), trendTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    trendTitle.textContent = "最近 10 分钟趋势", this.#trendWindow = (0, import_reader_settings_dom.settingsElement)(document, "span"), this.#trendWindow.textContent = "等待采样", trendHead.append(trendTitle, this.#trendWindow);
	    const trendList = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-trend-list"
	    );
	    for (const [key, label] of [
	      ["heapUsed", "页面内存估计"],
	      ["dom", "阅读器页面元素"],
	      ["retainedFloors", "楼层列表保留"]
	    ]) {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-resource-monitor-trend-row"
	      );
	      row.dataset.resourceMonitorChart = key;
	      const copy = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      copy.textContent = label;
	      const current = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	      current.dataset.resourceMonitorChartCurrent = key, current.textContent = "—";
	      const chart = document.createElementNS(
	        "http://www.w3.org/2000/svg",
	        "svg"
	      );
	      chart.setAttribute("viewBox", "0 0 240 34"), chart.setAttribute("preserveAspectRatio", "none"), chart.setAttribute("aria-hidden", "true"), chart.append(document.createElementNS(
	        "http://www.w3.org/2000/svg",
	        "polyline"
	      )), row.append(copy, current, chart), trendList.append(row), this.#trendRows.set(key, row);
	    }
	    trends.append(trendHead, trendList);
	    const events = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-resource-monitor-events"
	    ), eventHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-events-head"
	    ), eventTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    eventTitle.textContent = "毫秒级事件记录";
	    const eventHint = (0, import_reader_settings_dom.settingsElement)(document, "span");
	    eventHint.textContent = "毫秒时间 · 类型 · 耗时/增减 · 范围 · 原始依据", eventHead.append(eventTitle, eventHint), this.#eventLog = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-resource-monitor-event-log"
	    ), this.#eventLog.role = "log", this.#eventLog.setAttribute("aria-live", "off"), events.append(eventHead, this.#eventLog);
	    const boundary = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "p",
	      "ldp-resource-monitor-boundary"
	    );
	    boundary.textContent = "“前台 / 后台”取自页面可见状态,网络与性能记录按开始时状态归档;页面元素变更没有原生时间戳,只能按观察器回调时状态记录。阅读器请求可明确归因;未标记请求记为“原站 / 未标记”;无法确认来源的内存与主线程事件记为“页面共享”,不以推算冒充独占数据。", performancePanel.append(
	      performanceExport,
	      this.#health,
	      performancePolicyBlock,
	      evidence,
	      table,
	      trends,
	      events,
	      boundary
	    );
	    const requestSummary = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-summary"
	    );
	    requestSummary.setAttribute("aria-label", "请求速率摘要");
	    for (const [id, label] of [
	      ["rate10", "最近 10 秒请求"],
	      ["rate60", "最近 60 秒请求"],
	      ["peak", "100ms 内峰值"],
	      ["transfer", "最近 60 秒传输"],
	      ["issues", "最近 60 秒异常"]
	    ]) {
	      const metric = (0, import_reader_settings_dom.settingsElement)(
	        document,
	        "div",
	        "ldp-request-flow-metric"
	      ), copy = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      copy.textContent = label;
	      const value = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	      value.dataset.requestFlowMetric = id, value.textContent = "0 次", metric.append(copy, value), requestSummary.append(metric), this.#requestMetrics.set(id, value);
	    }
	    const traceBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-chart-block"
	    ), traceHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-block-head"
	    ), traceTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    traceTitle.textContent = "毫秒请求脉络", this.#requestWindow = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "span",
	      "ldp-request-flow-window"
	    ), this.#requestWindow.textContent = "等待采样", traceHead.append(traceTitle, this.#requestWindow), this.#requestTrace = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-trace"
	    ), this.#requestTrace.role = "img", this.#requestTrace.setAttribute(
	      "aria-label",
	      "最近 10 秒按来源显示排队、网络耗时与异常点"
	    );
	    const axis = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-axis"
	    );
	    for (const label of [
	      "10 秒前",
	      "虚线排队 · 短线放行 · 实色网络 · 圆点异常",
	      "现在"
	    ]) {
	      const value = (0, import_reader_settings_dom.settingsElement)(document, "span");
	      value.textContent = label, axis.append(value);
	    }
	    this.#requestLegend = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-legend"
	    ), this.#requestLegend.setAttribute("aria-label", "请求类型图例"), traceBlock.append(
	      traceHead,
	      this.#requestTrace,
	      axis,
	      this.#requestLegend
	    ), this.#requestBottleneck = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-bottleneck"
	    ), this.#requestBottleneck.dataset.level = "normal", this.#requestBottleneck.role = "status", this.#requestBottleneckState = (0, import_reader_settings_dom.settingsElement)(document, "strong"), this.#requestBottleneckState.textContent = "采样中", this.#requestBottleneckDetail = (0, import_reader_settings_dom.settingsElement)(document, "p"), this.#requestBottleneckDetail.textContent = "打开帖子并正常滚动后,这里会判断排队、限流、响应延迟或静态资源瓶颈。", this.#requestBottleneck.append(
	      this.#requestBottleneckState,
	      this.#requestBottleneckDetail
	    );
	    const topicDiagnosticBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-diagnostic-block"
	    ), topicDiagnosticHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-block-head"
	    ), topicDiagnosticTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    topicDiagnosticTitle.textContent = "帖子与楼层诊断";
	    const topicDiagnosticHint = (0, import_reader_settings_dom.settingsElement)(document, "span");
	    topicDiagnosticHint.textContent = "缓存 · 虚拟窗口 · 删除 · 网络 · 限流", topicDiagnosticHead.append(topicDiagnosticTitle, topicDiagnosticHint), this.#topicDiagnostics = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-diagnostics"
	    ), topicDiagnosticBlock.append(
	      topicDiagnosticHead,
	      this.#topicDiagnostics
	    );
	    const mediaDiagnosticBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-diagnostic-block"
	    ), mediaDiagnosticHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-block-head"
	    ), mediaDiagnosticTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    mediaDiagnosticTitle.textContent = "图片与媒体诊断";
	    const mediaDiagnosticHint = (0, import_reader_settings_dom.settingsElement)(document, "span");
	    mediaDiagnosticHint.textContent = "失败 · 缓存 · 来源 · HLS", mediaDiagnosticHead.append(mediaDiagnosticTitle, mediaDiagnosticHint), this.#mediaDiagnostics = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-diagnostics"
	    ), mediaDiagnosticBlock.append(
	      mediaDiagnosticHead,
	      this.#mediaDiagnostics
	    );
	    const anomalyBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-anomaly-block"
	    ), anomalyHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-block-head"
	    ), anomalyTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    anomalyTitle.textContent = "最近异常点", this.#requestAnomalyWindow = (0, import_reader_settings_dom.settingsElement)(document, "span"), this.#requestAnomalyWindow.textContent = "最近 60 秒", anomalyHead.append(anomalyTitle, this.#requestAnomalyWindow), this.#requestAnomalies = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-anomalies"
	    ), anomalyBlock.append(anomalyHead, this.#requestAnomalies);
	    const typesBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-types-block"
	    ), typesHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-type-head"
	    );
	    for (const [tag, label] of [
	      ["strong", "最近 60 秒按类型"],
	      ["span", "数量"],
	      ["span", "95% 请求耗时"],
	      ["span", "异常"]
	    ]) {
	      const value = (0, import_reader_settings_dom.settingsElement)(document, tag);
	      value.textContent = label, typesHead.append(value);
	    }
	    this.#requestTypes = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-types"
	    ), typesBlock.append(typesHead, this.#requestTypes);
	    const logBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-block ldp-request-flow-log-block"
	    ), logHead = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-block-head"
	    ), logTitle = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    logTitle.textContent = "毫秒请求记录";
	    const logHint = (0, import_reader_settings_dom.settingsElement)(document, "span");
	    logHint.textContent = "显示逻辑链、契约、单飞、重试与过盾决策;仅保留查询键形状,不保存查询值、正文、Cookie、授权头或响应内容", logHead.append(logTitle, logHint), this.#requestLog = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-request-flow-log"
	    ), this.#requestLog.role = "log", this.#requestLog.setAttribute("aria-live", "off"), logBlock.append(logHead, this.#requestLog);
	    const limitBlock = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "section",
	      "ldp-request-flow-limit"
	    );
	    limitBlock.setAttribute(
	      "aria-labelledby",
	      "ldp-request-flow-limit-title"
	    );
	    const limitTitle = (0, import_reader_settings_dom.settingsElement)(document, "h4");
	    limitTitle.id = "ldp-request-flow-limit-title", limitTitle.textContent = "Discourse 全局请求窗口与本地车道", this.#requestObserved = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "p",
	      "ldp-request-flow-observed"
	    ), this.#requestObserved.textContent = "当前还没有观察到服务器限流信息。";
	    const laneRules = (0, import_reader_settings_dom.settingsElement)(document, "p"), laneRulesLabel = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    laneRulesLabel.textContent = "当前阅读器规则:", laneRules.append(
	      laneRulesLabel,
	      document.createTextNode(
	        "后台 post_ids 正文单槽;可见缺口会提升并复用已有同键请求,需要新批次时可在总预算允许下占用第二个正文槽;树状回复最多两槽。所有车道继续共用本页与跨标签全局预算。"
	      )
	    );
	    const publicLimit = (0, import_reader_settings_dom.settingsElement)(document, "p"), publicLimitLabel = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    publicLimitLabel.textContent = "Discourse 公开默认:", publicLimit.append(
	      publicLimitLabel,
	      document.createTextNode(
	        "动态应用请求默认 50 次/10 秒、200 次/分钟;头像、CSS 等静态资源默认 200 次/10 秒。站点管理员、插件或反向代理可以覆盖这些数字。正文 post_ids[] 批次与直属回复共用动态请求窗口,阅读器只按实际载荷把它们分为不同并发车道,不虚构独立服务器额度。"
	      )
	    );
	    const limitBoundary = (0, import_reader_settings_dom.settingsElement)(document, "p"), limitFacts = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    limitFacts.textContent = "429、Retry-After 和限流错误码";
	    const limitLink = (0, import_reader_settings_dom.settingsElement)(document, "a");
	    limitLink.href = "https://meta.discourse.org/t/available-settings-for-global-rate-limits-and-throttling/78612", limitLink.target = "_blank", limitLink.rel = "noopener", limitLink.textContent = "查看 Discourse 公开说明", limitBoundary.append(
	      document.createTextNode(
	        "成功响应通常不提供剩余额度,因此这里用跨标签真实启动次数作为预防账本;服务器拒绝仍以本页收到的 "
	      ),
	      limitFacts,
	      document.createTextNode("为准。"),
	      limitLink
	    ), limitBlock.append(
	      limitTitle,
	      this.#requestObserved,
	      laneRules,
	      publicLimit,
	      limitBoundary
	    ), requestPanel.append(
	      requestExport,
	      requestSummary,
	      traceBlock,
	      this.#requestBottleneck,
	      topicDiagnosticBlock,
	      mediaDiagnosticBlock,
	      anomalyBlock,
	      typesBlock,
	      logBlock,
	      limitBlock
	    );
	    const content = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-settings-log-content"
	    );
	    content.append(requestPanel, performancePanel), this.#root.append(tabs, content), options.host.append(this.#root), this.scope.add(() => this.#root.remove()), this.scope.add(() => this.stop());
	  }
	  #createExportControl(kind, title, description) {
	    const document = this.#options.document, control = (0, import_reader_settings_dom.settingsElement)(
	      document,
	      "div",
	      "ldp-log-export-control"
	    );
	    control.dataset.logExportControl = kind;
	    const copy = (0, import_reader_settings_dom.settingsElement)(document, "span", "ldp-log-export-copy"), heading = (0, import_reader_settings_dom.settingsElement)(document, "strong");
	    heading.textContent = title;
	    const detail = (0, import_reader_settings_dom.settingsElement)(document, "small");
	    detail.textContent = description, copy.append(heading, detail);
	    const actions = (0, import_reader_settings_dom.settingsElement)(document, "span", "ldp-log-export-actions"), button = (0, import_reader_settings_dom.settingsButton)(
	      document,
	      "ldp-config-action ldp-log-export-button",
	      "",
	      "download",
	      "导出 JSONL"
	    );
	    button.dataset.logExport = kind;
	    const status = (0, import_reader_settings_dom.settingsElement)(document, "small", "ldp-log-export-status");
	    return status.dataset.logExportStatus = kind, status.role = "status", status.setAttribute("aria-live", "polite"), actions.append(button, status), control.append(copy, actions), this.scope.listen(button, "click", () => {
	      this.#exportLog(kind, button, status);
	    }), control;
	  }
	  async #exportLog(kind, button, status) {
	    if (!button.disabled) {
	      button.disabled = !0, status.textContent = "正在整理…";
	      try {
	        const generatedAt = this.#now(), requests = this.requests.snapshot.events, scheduler = schedulerDiagnosticRecord(
	          this.#options.schedulerSnapshot()
	        );
	        let permitSnapshot = null;
	        try {
	          permitSnapshot = await this.#options.permitSnapshot();
	        } catch {
	        }
	        const permit = permitDiagnosticRecord(permitSnapshot);
	        this.#recordRequestRuntimeState(
	          generatedAt,
	          scheduler,
	          permit,
	          "export"
	        );
	        const file = kind === "request" ? this.#requestLogFile(generatedAt, requests, scheduler, permit) : this.#performanceLogFile(
	          generatedAt,
	          requests,
	          scheduler,
	          permit
	        );
	        await this.#saveDiagnosticLog(file);
	        const count = kind === "request" ? requests.length : this.#samples.length + this.#performanceEvents.length;
	        status.textContent = `已导出 ${count} 条${kind === "request" ? "请求" : "性能事实"}`;
	      } catch (error) {
	        status.textContent = `导出失败:${error instanceof Error ? error.message : "未知错误"}`;
	      } finally {
	        button.disabled = !1;
	      }
	    }
	  }
	  #requestLogFile(generatedAt, requests, scheduler, permit) {
	    const records = [
	      {
	        recordType: "meta",
	        logType: "request",
	        schemaVersion: 1,
	        generatedAt,
	        generatedAtIso: diagnosticIsoTime(generatedAt),
	        retention: "current RequestObserver in-memory snapshot",
	        requestCount: requests.length,
	        requestRuntimeStateCount: this.#requestRuntimeStates.length,
	        privacy: "query keys and counts only; no query values, headers, bodies, cookies, authorization, or response content"
	      },
	      {
	        recordType: "runtime-state",
	        at: generatedAt,
	        atIso: diagnosticIsoTime(generatedAt),
	        scheduler,
	        permit
	      },
	      ...this.#requestRuntimeStates.map((event) => ({
	        recordType: "request-runtime-state",
	        ...event,
	        atIso: diagnosticIsoTime(event.at),
	        lastObservedAtIso: diagnosticIsoTime(event.lastObservedAt)
	      })),
	      ...requests.map((event) => requestDiagnosticRecord(event))
	    ];
	    return Object.freeze({
	      filename: diagnosticLogFilename("request", generatedAt),
	      mimeType: "application/x-ndjson;charset=utf-8",
	      text: diagnosticJsonLines(records)
	    });
	  }
	  #performanceLogFile(generatedAt, requests, scheduler, permit) {
	    const cutoff = generatedAt - RETENTION_MS, policy = this.#options.performancePolicySnapshot?.() ?? null, timeline = [
	      ...this.#samples.map((sample) => ({
	        at: sample.at,
	        record: {
	          recordType: "sample",
	          ...sample,
	          atIso: diagnosticIsoTime(sample.at)
	        }
	      })),
	      ...this.#performanceEvents.map((event) => ({
	        at: event.at,
	        record: {
	          recordType: "performance-event",
	          at: event.at,
	          atIso: diagnosticIsoTime(event.at),
	          kind: event.kind,
	          kindLabel: PERFORMANCE_EVENT_LABELS[event.kind] ?? event.kind,
	          durationMs: event.duration,
	          visibility: event.visibility,
	          scope: event.scope,
	          detail: event.detail,
	          basis: event.basis,
	          ...event.added === void 0 ? {} : { added: event.added },
	          ...event.removed === void 0 ? {} : { removed: event.removed }
	        }
	      })),
	      ...this.#visibilityTimeline.map((marker) => ({
	        at: marker.at,
	        record: {
	          recordType: "visibility-marker",
	          at: marker.at,
	          atIso: diagnosticIsoTime(marker.at),
	          state: marker.state
	        }
	      })),
	      ...this.#requestRuntimeStates.map((event) => ({
	        at: event.lastObservedAt,
	        record: {
	          recordType: "request-runtime-state",
	          ...event,
	          atIso: diagnosticIsoTime(event.at),
	          lastObservedAtIso: diagnosticIsoTime(event.lastObservedAt)
	        }
	      })),
	      ...requests.filter((event) => event.queuedAt >= cutoff).map((event) => ({
	        at: event.queuedAt,
	        record: requestDiagnosticRecord(
	          event,
	          this.#requestVisibility.get(event.id) ?? "unknown"
	        )
	      }))
	    ].filter((entry) => entry.at >= cutoff && entry.at <= generatedAt).sort((left, right) => left.at - right.at), records = [
	      {
	        recordType: "meta",
	        logType: "performance",
	        schemaVersion: 1,
	        generatedAt,
	        generatedAtIso: diagnosticIsoTime(generatedAt),
	        retentionMs: RETENTION_MS,
	        sampleIntervalMs: Math.max(
	          250,
	          this.#options.sampleIntervalMs ?? 1e3
	        ),
	        sampleCount: this.#samples.length,
	        performanceEventCount: this.#performanceEvents.length,
	        associatedRequestCount: requests.filter(
	          (event) => event.queuedAt >= cutoff
	        ).length,
	        privacy: "request query keys and counts only; script URLs exclude credentials, query, and fragment"
	      },
	      {
	        recordType: "runtime-state",
	        at: generatedAt,
	        atIso: diagnosticIsoTime(generatedAt),
	        scheduler,
	        permit,
	        performancePolicy: policy
	      },
	      this.#performanceCapabilities(generatedAt),
	      ...timeline.map((entry) => entry.record)
	    ];
	    return Object.freeze({
	      filename: diagnosticLogFilename("performance", generatedAt),
	      mimeType: "application/x-ndjson;charset=utf-8",
	      text: diagnosticJsonLines(records)
	    });
	  }
	  #performanceCapabilities(at) {
	    const view = this.#options.document.defaultView, performance = this.#options.performance ?? view?.performance, supportedEntryTypes = Array.isArray(
	      view?.PerformanceObserver?.supportedEntryTypes
	    ) ? [...view.PerformanceObserver.supportedEntryTypes].sort() : [], memorySource = performance?.measureUserAgentSpecificMemory ? "measureUserAgentSpecificMemory" : Number.isFinite(Number(performance?.memory?.usedJSHeapSize)) ? "performance.memory" : "unavailable";
	    return Object.freeze({
	      recordType: "capabilities",
	      at,
	      atIso: diagnosticIsoTime(at),
	      captureActive: this.#activePanel === "performance",
	      activePanel: this.#activePanel,
	      sampleIntervalMs: Math.max(
	        250,
	        this.#options.sampleIntervalMs ?? 1e3
	      ),
	      memoryIntervalMs: 1e4,
	      memorySource,
	      supportedEntryTypes,
	      observerInstall: {
	        resource: this.#resourceObservationCapability,
	        longtask: this.#longTaskCapability,
	        longAnimationFrame: this.#longAnimationFrameCapability,
	        readerMutation: this.#readerMutationCapability,
	        hostMutation: this.#hostMutationCapability
	      },
	      retention: {
	        performanceMs: RETENTION_MS,
	        requestRuntimeMs: REQUEST_RUNTIME_RETENTION_MS,
	        maxPerformanceEvents: MAX_EVIDENCE_EVENTS,
	        maxRequestRuntimeStates: MAX_REQUEST_RUNTIME_STATES
	      },
	      discarded: {
	        evidenceOverflow: this.#evidenceOverflowDrops,
	        evidenceRetention: this.#evidenceRetentionDrops,
	        sampleRetention: this.#sampleRetentionDrops,
	        visibilityRetention: this.#visibilityRetentionDrops,
	        requestRuntimeOverflow: this.#requestRuntimeOverflowDrops,
	        requestRuntimeRetention: this.#requestRuntimeRetentionDrops
	      },
	      limitations: [
	        "performance collection runs only while the performance panel is active",
	        "browser background throttling or freezing can create unfilled gaps",
	        "memory is page-level and cannot isolate this userscript",
	        "cross-origin Resource Timing fields can be zero without Timing-Allow-Origin",
	        "CPU, GC, GPU, FPS, server logs, payloads, and unsupported browser entry types are unavailable"
	      ]
	    });
	  }
	  #recordRequestRuntimeState(at, scheduler, permit, source) {
	    const signature = JSON.stringify({ scheduler, permit }), previous = this.#requestRuntimeStates.at(-1);
	    if (previous && JSON.stringify({
	      scheduler: previous.scheduler,
	      permit: previous.permit
	    }) === signature) {
	      this.#requestRuntimeStates[this.#requestRuntimeStates.length - 1] = Object.freeze({
	        ...previous,
	        lastObservedAt: at,
	        observations: previous.observations + 1,
	        lastSource: source
	      });
	      return;
	    }
	    if (this.#requestRuntimeStates.push(Object.freeze({
	      at,
	      lastObservedAt: at,
	      observations: 1,
	      source,
	      lastSource: source,
	      scheduler,
	      permit
	    })), this.#requestRuntimeStates.length > MAX_REQUEST_RUNTIME_STATES) {
	      const overflow = this.#requestRuntimeStates.length - MAX_REQUEST_RUNTIME_STATES;
	      this.#requestRuntimeStates.splice(0, overflow), this.#requestRuntimeOverflowDrops += overflow;
	    }
	  }
	  async #saveDiagnosticLog(file) {
	    if (this.#options.saveLog) {
	      await this.#options.saveLog(file);
	      return;
	    }
	    const document = this.#options.document, view = document.defaultView, urlApi = view?.URL ?? globalThis.URL;
	    if (typeof urlApi.createObjectURL != "function")
	      throw new Error("当前浏览器不支持本地日志下载");
	    const BlobConstructor = view?.Blob ?? globalThis.Blob, source = urlApi.createObjectURL(new BlobConstructor([file.text], {
	      type: file.mimeType
	    })), link = document.createElement("a");
	    link.href = source, link.download = file.filename, link.hidden = !0, (document.body ?? this.#root).append(link), link.click(), link.remove();
	    const timer = setTimeout(() => urlApi.revokeObjectURL(source), 6e4);
	    this.scope.timer(timer);
	  }
	  get active() {
	    return this.#activeScope !== null;
	  }
	  get samples() {
	    return Object.freeze([...this.#samples]);
	  }
	  start(panel = this.#selectedPanel) {
	    if (this.scope.destroyed || this.#activeScope && this.#activePanel === panel) return;
	    this.#activeScope && this.stop(), this.#selectedPanel = panel, this.#activePanel = panel, this.#memoryMeasuring = !1, this.#lastMemoryAt = 0, this.#baselineAt = this.#now(), this.#prune(this.#baselineAt);
	    const active = this.scope.child();
	    this.#activeScope = active, panel === "performance" && (this.#recordVisibility("开始取证"), active.listen(this.#options.document, "visibilitychange", () => {
	      this.#recordVisibility("页面可见状态变化");
	    }), this.requests.changes.subscribe((snapshot) => {
	      for (const event of snapshot.events)
	        this.#requestVisibility.has(event.id) || this.#requestVisibility.set(event.id, this.#visibility());
	    }, active));
	    const performance = this.#options.performance ?? this.#options.document.defaultView?.performance ?? null, installResourceObservation = (scope) => {
	      if (!performance) {
	        this.#resourceObservationCapability = "unavailable";
	        return;
	      }
	      const installed = new import_browser_request_observation.BrowserResourceObservationAdapter({
	        observer: this.requests,
	        performance,
	        ...this.#options.createPerformanceObserver ? {
	          createObserver: this.#options.createPerformanceObserver
	        } : {}
	      }).install(scope);
	      this.#resourceObservationCapability = installed ? "available" : "unavailable";
	    }, installSampler = (scope) => {
	      this.#sample();
	      const timer = setInterval(
	        () => void this.#sample(),
	        Math.max(250, this.#options.sampleIntervalMs ?? 1e3)
	      );
	      scope.timer(timer, clearInterval);
	    };
	    if (panel === "request") {
	      let capture = null;
	      const syncCapture = () => {
	        if (capture?.destroy(), capture = null, this.#visibility() === "hidden") return;
	        const currentCapture = active.child();
	        capture = currentCapture, installResourceObservation(currentCapture);
	        let refreshQueued = !1;
	        this.requests.changes.subscribe(() => {
	          refreshQueued || (refreshQueued = !0, queueMicrotask(() => {
	            refreshQueued = !1, !(currentCapture.destroyed || this.#activeScope !== active || this.#activePanel !== "request" || this.#visibility() === "hidden") && this.#sample();
	          }));
	        }, currentCapture), installSampler(currentCapture);
	      };
	      active.add(() => {
	        capture?.destroy(), capture = null;
	      }), active.listen(
	        this.#options.document,
	        "visibilitychange",
	        syncCapture
	      ), syncCapture();
	      return;
	    }
	    installResourceObservation(active), this.#longTaskCapability = this.#observePerformance(active, "longtask") ? "available" : "unavailable", this.#longAnimationFrameCapability = this.#observePerformance(
	      active,
	      "long-animation-frame"
	    ) ? "available" : "unavailable";
	    const mutationCapabilities = this.#observeMutations(active);
	    this.#readerMutationCapability = mutationCapabilities.reader ? "available" : "unavailable", this.#hostMutationCapability = mutationCapabilities.host ? "available" : "unavailable", this.#healthState.textContent = "建立基线", this.#healthDetail.textContent = "连续取得 10 个真实快照后开始判断资源压力。", installSampler(active);
	  }
	  stop() {
	    const active = this.#activeScope;
	    if (!active) return;
	    if (this.#activePanel === "performance") {
	      const at = this.#now();
	      this.#pushEvidence({
	        at,
	        visibility: this.#visibilityAt(at),
	        scope: "shared",
	        kind: "capture",
	        duration: 0,
	        detail: "资源取证暂停",
	        basis: "监控面板生命周期"
	      }), this.#visibilityTimeline.push({ at, state: "stopped" });
	    }
	    this.#activeScope = null;
	    const panel = this.#activePanel;
	    this.#activePanel = null, active.destroy(), panel === "performance" && (this.#healthState.textContent = "采集已暂停", this.#healthDetail.textContent = "监控面板已离开或关闭;observer 与计时器均已释放。");
	  }
	  sampleNow() {
	    return this.#sample();
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #observePerformance(scope, type) {
	    const factory = this.#options.createPerformanceObserver ?? ((callback) => {
	      const Observer = this.#options.document.defaultView?.PerformanceObserver;
	      if (!Observer) throw new Error("PerformanceObserver unavailable");
	      return new Observer(callback);
	    });
	    let observer = null;
	    try {
	      observer = factory((list) => {
	        const at = this.#now(), timeOrigin = Number(
	          (this.#options.performance ?? this.#options.document.defaultView?.performance)?.timeOrigin
	        );
	        for (const entry of list.getEntries()) {
	          const startTime = Number(entry.startTime), duration = Math.max(0, Number(entry.duration) || 0), eventAt = Number.isFinite(timeOrigin) && Number.isFinite(startTime) ? timeOrigin + startTime : at - duration;
	          eventAt < this.#baselineAt || (this.#pushEvidence({
	            at: eventAt,
	            kind: type,
	            duration,
	            visibility: this.#visibilityAt(eventAt),
	            scope: "shared",
	            detail: type === "longtask" ? `页面共享主线程长任务 ${formatDuration(duration)}` : `长动画帧 ${formatDuration(duration)}`,
	            basis: type === "longtask" ? "PerformanceLongTaskTiming" : "Long Animation Frames"
	          }), type === "long-animation-frame" && this.#recordFrameScripts(entry, eventAt));
	        }
	        this.#prune(at);
	      }), observer.observe({ type, buffered: !0 });
	    } catch {
	      return observer?.disconnect(), !1;
	    }
	    const installed = observer;
	    return scope.add(() => installed.disconnect()), !0;
	  }
	  #observeMutations(scope) {
	    const factory = this.#options.createMutationObserver ?? ((callback) => {
	      const Observer = this.#options.document.defaultView?.MutationObserver;
	      if (!Observer) throw new Error("MutationObserver unavailable");
	      return new Observer(callback);
	    }), install = (target, evidenceScope) => {
	      let observer = null;
	      try {
	        observer = factory((records) => {
	          let added = 0, removed = 0;
	          const readerDocumentRoot = this.#readerDocumentRoot();
	          for (const record of records)
	            evidenceScope === "host" && readerDocumentRoot?.contains(record.target) || (added += this.#elementCount(
	              record.addedNodes,
	              evidenceScope === "host"
	            ), removed += this.#elementCount(
	              record.removedNodes,
	              evidenceScope === "host"
	            ));
	          if (!added && !removed) return;
	          const at = this.#now();
	          this.#pushEvidence({
	            at,
	            visibility: this.#visibilityAt(at),
	            scope: evidenceScope,
	            kind: "dom",
	            duration: 0,
	            added,
	            removed,
	            detail: `页面元素 +${added} / −${removed} · ${records.length} 个变更记录`,
	            basis: "MutationObserver 回调"
	          });
	        }), observer.observe(target, { childList: !0, subtree: !0 });
	      } catch {
	        return observer?.disconnect(), !1;
	      }
	      const installed = observer;
	      return scope.add(() => installed.disconnect()), !0;
	    }, reader = install(this.#options.readerRoot, "reader"), hostRoot = this.#options.document.documentElement, host = hostRoot ? install(hostRoot, "host") : !1;
	    return Object.freeze({ reader, host });
	  }
	  #readerDocumentRoot() {
	    const shadowHost = this.#options.readerRoot.getRootNode().host;
	    return shadowHost?.nodeType === 1 ? shadowHost : this.#options.readerRoot;
	  }
	  #elementTreeCount(element) {
	    return 1 + element.querySelectorAll("*").length;
	  }
	  #elementCount(nodes, excludeReaderRoot = !1) {
	    let total = 0;
	    const readerDocumentRoot = excludeReaderRoot ? this.#readerDocumentRoot() : null;
	    for (const node of Array.from(nodes)) {
	      if (node.nodeType !== 1) continue;
	      const element = node;
	      let count = this.#elementTreeCount(element);
	      if (readerDocumentRoot) {
	        if (element === readerDocumentRoot) continue;
	        element.contains(readerDocumentRoot) && (count -= this.#elementTreeCount(readerDocumentRoot));
	      }
	      total += Math.max(0, count);
	    }
	    return total;
	  }
	  #hostDomCount(readerDom) {
	    const documentRoot = this.#options.document.documentElement;
	    if (!documentRoot) return 0;
	    const total = this.#elementTreeCount(documentRoot), readerDocumentRoot = this.#readerDocumentRoot(), readerDocumentElements = readerDocumentRoot && documentRoot.contains(readerDocumentRoot) ? this.#elementTreeCount(readerDocumentRoot) : this.#options.readerRoot.getRootNode() === this.#options.document ? readerDom : 0;
	    return Math.max(0, total - readerDocumentElements);
	  }
	  #recordFrameScripts(entry, at) {
	    const scripts = entry.scripts ?? [], groups = /* @__PURE__ */ new Map();
	    for (const script of scripts) {
	      const duration = Math.max(0, Number(script.duration) || 0);
	      if (!duration) continue;
	      const evidenceScope = this.#scriptScope(script.sourceURL), group = groups.get(evidenceScope) ?? {
	        duration: 0,
	        forced: 0,
	        count: 0,
	        labels: []
	      };
	      group.duration += duration, group.forced += Math.max(
	        0,
	        Number(script.forcedStyleAndLayoutDuration) || 0
	      ), group.count += 1;
	      const label = performanceScriptLabel(
	        script.sourceFunctionName,
	        script.sourceURL,
	        this.#options.document.baseURI
	      );
	      group.labels.length < 2 && group.labels.push(label), groups.set(evidenceScope, group);
	    }
	    for (const [evidenceScope, group] of groups)
	      this.#pushEvidence({
	        at,
	        visibility: this.#visibilityAt(at),
	        scope: evidenceScope,
	        kind: "script",
	        duration: group.duration,
	        detail: `长帧内已归因脚本 ${formatDuration(group.duration)}${group.forced ? ` · 强制布局 ${formatDuration(group.forced)}` : ""} · ${group.labels.join(" / ")}`,
	        basis: `PerformanceScriptTiming · ${group.count} 段`
	      });
	  }
	  #scriptScope(sourceUrl) {
	    const source = String(sourceUrl ?? "");
	    if (/Awesome LinuxDo Reader|main-lite|mian-lite|katex@0\.16\.22|pinyin-pro@3\.18\.2|hls\.js@1\.6\.16/i.test(source)) return "reader";
	    try {
	      const url = new URL(source, this.#options.document.baseURI), page = new URL(this.#options.document.baseURI);
	      if (url.origin === page.origin || /(?:^|\.)linux\.do$/i.test(url.hostname) || /(?:^|\.)ldstatic\.com$/i.test(url.hostname)) return "host";
	    } catch {
	    }
	    return "shared";
	  }
	  #recordVisibility(reason) {
	    const at = this.#now(), state = this.#visibility();
	    this.#visibilityTimeline.at(-1)?.state !== state && (this.#visibilityTimeline.push({ at, state }), this.#pushEvidence({
	      at,
	      visibility: state,
	      scope: "shared",
	      kind: "visibility",
	      duration: 0,
	      detail: `${reason} · 当前${state === "visible" ? "前台" : "后台"}`,
	      basis: "Page Visibility"
	    }));
	  }
	  #visibilityAt(at) {
	    for (let index = this.#visibilityTimeline.length - 1; index >= 0; index -= 1) {
	      const marker = this.#visibilityTimeline[index];
	      if (!(marker.at > at))
	        return marker.state === "stopped" ? "unknown" : marker.state;
	    }
	    return "unknown";
	  }
	  #pushEvidence(event) {
	    if (this.#performanceEvents.push(Object.freeze(event)), this.#performanceEvents.length > MAX_EVIDENCE_EVENTS) {
	      const overflow = this.#performanceEvents.length - MAX_EVIDENCE_EVENTS;
	      this.#performanceEvents.splice(
	        0,
	        overflow
	      ), this.#evidenceOverflowDrops += overflow;
	    }
	    this.#prune(event.at);
	  }
	  async #sample() {
	    const active = this.#activeScope;
	    if (!active || active.destroyed) return;
	    const panel = this.#activePanel;
	    if (!panel || panel === "request" && this.#visibility() === "hidden") return;
	    const at = this.#now();
	    this.#prune(at), panel === "performance" && this.#measureMemory(at, active);
	    const recentPerformance = this.#performanceEvents.filter(
	      (event) => event.at >= at - 1e4 && (event.kind === "longtask" || event.kind === "long-animation-frame")
	    ), requestEvents = this.requests.snapshot.events.filter(
	      (event) => event.phase !== "queued" && !event.controlReason && event.startedAt >= Math.max(at - 6e4, this.#baselineAt)
	    ), scheduler = this.#options.schedulerSnapshot();
	    let permit = null;
	    try {
	      permit = await this.#options.permitSnapshot();
	    } catch {
	    }
	    if (this.#activeScope !== active || active.destroyed) return;
	    this.#recordRequestRuntimeState(
	      at,
	      schedulerDiagnosticRecord(scheduler),
	      permitDiagnosticRecord(permit),
	      panel
	    );
	    const topic = this.#options.topicSnapshot(), readerDom = panel === "performance" ? this.#options.readerRoot.querySelectorAll("*").length + 1 : 0, sample = Object.freeze({
	      at,
	      visibility: this.#visibility(),
	      heapBytes: this.#heapBytes,
	      longTasks: recentPerformance.filter(
	        (event) => event.kind === "longtask"
	      ).length,
	      longTaskDuration: recentPerformance.filter(
	        (event) => event.kind === "longtask"
	      ).reduce(
	        (sum, event) => sum + event.duration,
	        0
	      ),
	      longFrames: recentPerformance.filter(
	        (event) => event.kind === "long-animation-frame"
	      ).length,
	      readerDom,
	      hostDom: panel === "performance" ? this.#hostDomCount(readerDom) : 0,
	      ...topic,
	      activeRequests: scheduler?.active ?? 0,
	      queuedRequests: scheduler?.queued ?? 0,
	      requestMaxConcurrent: scheduler?.maxConcurrent ?? 0,
	      requestQueueLimit: scheduler?.queueLimit ?? 0,
	      requestActiveByLane: scheduler?.activeByLane ?? EMPTY_REQUEST_LANE_COUNTS,
	      requestQueuedByLane: scheduler?.queuedByLane ?? EMPTY_REQUEST_LANE_COUNTS,
	      sharedActiveRequests: permit?.active ?? 0,
	      sharedQueuedRequests: permit?.queued ?? 0,
	      sharedMaxConcurrent: permit?.maxConcurrent ?? 0,
	      sharedMinIntervalMs: permit?.minIntervalMs ?? 0,
	      sharedInstances: permit?.instances ?? 0,
	      sharedNextPermitDelay: permit?.nextPermitDelay ?? 0,
	      sharedBlockingReason: permit?.blockingReason ?? "",
	      sharedCoordinationMode: permit?.coordinationMode ?? "unavailable",
	      shortWindowCount: permit?.shortCount ?? 0,
	      shortWindowBudget: permit?.shortBudget ?? 0,
	      longWindowCount: permit?.longCount ?? 0,
	      longWindowBudget: permit?.longBudget ?? 0,
	      challengeState: permit?.challengeState ?? "idle",
	      challengeOwned: permit?.challengeOwned ?? !1,
	      networkRequests: requestEvents.length,
	      networkBytes: requestEvents.reduce(
	        (sum, event) => sum + event.size,
	        0
	      )
	    });
	    if (panel === "performance") {
	      const previous = this.#samples.at(-1);
	      if (previous && at - previous.at > 1800) {
	        const crossedStopped = this.#visibilityTimeline.some((marker) => marker.at > previous.at && marker.at <= at && marker.state === "stopped"), crossedHidden = previous.visibility === "hidden" || this.#visibilityTimeline.some((marker) => marker.at > previous.at && marker.at <= at && marker.state === "hidden");
	        this.#pushEvidence({
	          at,
	          visibility: crossedStopped ? "unknown" : crossedHidden ? "hidden" : this.#visibilityAt(at),
	          scope: "shared",
	          kind: "gap",
	          duration: at - previous.at,
	          detail: `快照空档 ${formatDuration(at - previous.at)}${crossedStopped ? "(包含取证暂停)" : crossedHidden ? "(包含后台节流或冻结)" : ""};未补造中间样本`,
	          basis: "High Resolution Time"
	        });
	      }
	      this.#samples.push(sample), this.#prune(at), this.#renderPerformance(sample, requestEvents);
	    } else
	      this.#renderRequests(sample, this.requests.snapshot.events);
	  }
	  #measureMemory(at, active) {
	    if (this.#memoryMeasuring || at - this.#lastMemoryAt < 1e4) return;
	    this.#lastMemoryAt = at;
	    const performance = this.#options.performance ?? this.#options.document.defaultView?.performance, fallback = Number(performance?.memory?.usedJSHeapSize);
	    Number.isFinite(fallback) && fallback >= 0 && (this.#heapBytes = fallback), performance?.measureUserAgentSpecificMemory && (this.#memoryMeasuring = !0, performance.measureUserAgentSpecificMemory().then((result) => {
	      if (this.#activeScope !== active || active.destroyed) return;
	      const bytes = Number(result.bytes);
	      Number.isFinite(bytes) && bytes >= 0 && (this.#heapBytes = bytes);
	    }).catch(() => {
	    }).finally(() => {
	      this.#activeScope === active && (this.#memoryMeasuring = !1);
	    }));
	  }
	  #renderPerformance(current, requestEvents) {
	    const samples = this.#samples;
	    for (const metric of METRICS) {
	      const row = this.#rows.get(metric.id), currentValues = metric.values(current), average = aggregate(samples, metric, "average"), peak = aggregate(samples, metric, "peak"), first2 = metric.values(samples[0] ?? current), trend = currentValues.map((value, index) => value === null || first2[index] === null ? null : value - first2[index]);
	      row.querySelector(
	        "[data-resource-monitor-metric]"
	      ).textContent = metric.format(currentValues), row.querySelector(
	        "[data-resource-monitor-average]"
	      ).textContent = metric.format(average), row.querySelector(
	        "[data-resource-monitor-peak]"
	      ).textContent = metric.format(peak), row.querySelector(
	        "[data-resource-monitor-trend]"
	      ).textContent = trend.every((value) => value === null) ? "—" : trend.map(
	        (value) => value === null ? "—" : `${value >= 0 ? "+" : ""}${Math.round(value)}`
	      ).join(" / ");
	    }
	    const captureSamples = samples.filter(
	      (sample) => sample.at >= this.#baselineAt
	    ), baselineSamples = captureSamples.filter(
	      (sample) => sample.at >= current.at - 12e3
	    ), first = captureSamples.filter(
	      (sample) => sample.at >= current.at - 6e4
	    )[0] ?? current, domGrowth = current.readerDom - first.readerDom, retainedFloorGrowth = current.retainedFloors - first.retainedFloors, warnings = [];
	    let level = "normal";
	    current.longTaskDuration >= 2e3 ? (level = "danger", warnings.push(`近 10 秒主线程阻塞 ${Math.round(
	      current.longTaskDuration
	    )} ms`)) : current.longTaskDuration >= 500 && (level = "warning", warnings.push(`近 10 秒主线程阻塞 ${Math.round(
	      current.longTaskDuration
	    )} ms`)), current.queuedRequests >= 10 ? (level = "danger", warnings.push(`中央请求队列 ${current.queuedRequests} 条`)) : current.queuedRequests >= 3 && (level === "normal" && (level = "warning"), warnings.push(`中央请求队列 ${current.queuedRequests} 条`)), domGrowth >= 1e3 && retainedFloorGrowth >= 50 && (level === "normal" && (level = "warning"), warnings.push(
	      `近一分钟阅读器页面元素增加 ${domGrowth} 个,保留楼层增加 ${retainedFloorGrowth} 个`
	    ));
	    const establishing = baselineSamples.length < 10;
	    this.#health.dataset.level = establishing ? "normal" : level, this.#healthState.textContent = establishing ? "建立基线" : warnings.length ? level === "danger" ? "资源压力高" : "需要关注" : "采样正常", this.#healthDetail.textContent = establishing ? `最近 12 秒已取得 ${baselineSamples.length}/10 个真实快照。` : warnings.length ? `${warnings.join(";")}。继续观察趋势,回落后会自动恢复。` : "未发现阅读器页面元素快速膨胀、页面共享主线程卡顿或阅读器请求积压;内存估计不参与自动判定。", this.#updated.textContent = `最近快照 ${new Date(current.at).toLocaleTimeString()} · ${current.visibility === "visible" ? "前台" : "后台"} · 仅内存保留`;
	    const performancePolicy = this.#options.performancePolicySnapshot?.() ?? null;
	    this.#performancePolicy.textContent = performancePolicy ? `已含设备与网络自适应:正文批次 ${performancePolicy.pageSize} 楼 · DOM 最多 ${performancePolicy.streamMaxMountedPostCount} 楼 · 屏外预留 ${formatPolicyNumber(performancePolicy.streamOverscanScreens)} 屏 · API 提前 ${formatPolicyNumber(performancePolicy.nestedPrefetchScreens)} 屏 · 本页请求策略上限 ${performancePolicy.requestMaxConcurrent} 路 / ${performancePolicy.requestMinIntervalMs}ms · 窗口目标 ${performancePolicy.requestRateTargetPercent}%;跨标签与服务器实时约束见请求记录。` : "当前运行环境未提供性能策略快照;下方仍显示实际 DOM、请求与主线程记录。", this.#renderTrendCharts(current), this.#renderEvidence(current, requestEvents);
	  }
	  #renderTrendCharts(current) {
	    const rows = {
	      heapUsed: {
	        value: (sample) => sample.heapBytes,
	        current: current.heapBytes === null ? "浏览器未提供" : formatBytes(current.heapBytes)
	      },
	      dom: {
	        value: (sample) => sample.readerDom,
	        current: `${current.readerDom} 个`
	      },
	      retainedFloors: {
	        value: (sample) => sample.retainedFloors,
	        current: `${current.retainedFloors} 个`
	      }
	    };
	    for (const [key, definition] of Object.entries(rows)) {
	      const row = this.#trendRows.get(key);
	      row.querySelector("polyline")?.setAttribute(
	        "points",
	        chartPoints(this.#samples, definition.value)
	      ), row.querySelector(
	        "[data-resource-monitor-chart-current]"
	      ).textContent = definition.current;
	    }
	    const covered = this.#samples.length > 1 ? this.#samples.at(-1).at - this.#samples[0].at : 0, maximumGap = this.#samples.slice(1).reduce(
	      (maximum, sample, index) => Math.max(maximum, sample.at - this.#samples[index].at),
	      0
	    ), visible = this.#samples.filter(
	      (sample) => sample.visibility === "visible"
	    ).length, hidden = this.#samples.length - visible;
	    this.#trendWindow.textContent = `${covered >= 6e4 ? `${(covered / 6e4).toFixed(1)} 分钟` : `${Math.round(covered / 1e3)} 秒`} · 前 ${visible} / 后 ${hidden}${maximumGap > 1800 ? ` · 最大空档 ${formatDuration(maximumGap)}` : ""}`;
	  }
	  #renderEvidence(current, requestEvents) {
	    const createCell = () => ({
	      requests: 0,
	      requestDuration: 0,
	      bytes: 0,
	      scripts: 0,
	      longTasks: 0,
	      longTaskDuration: 0,
	      domChanges: 0
	    }), scopes = {
	      reader: { visible: createCell(), hidden: createCell() },
	      host: { visible: createCell(), hidden: createCell() },
	      shared: { visible: createCell(), hidden: createCell() }
	    }, cutoff = current.at - EVIDENCE_WINDOW_MS, requests = requestEvents.map((event) => ({
	      at: event.startedAt,
	      kind: "request",
	      visibility: this.#requestVisibility.get(event.id) ?? "unknown",
	      scope: event.source === "reader" ? "reader" : event.source === "host" ? "host" : "shared",
	      duration: event.pending ? Math.max(0, current.at - event.startedAt) : event.duration,
	      bytes: event.size,
	      diagnostic: requestContractDiagnostic(event),
	      event
	    }));
	    for (const request of requests) {
	      if (request.at < cutoff || request.at > current.at || request.visibility === "unknown") continue;
	      const cell = scopes[request.scope][request.visibility];
	      cell.requests += 1, cell.requestDuration += request.duration, cell.bytes += request.bytes;
	    }
	    for (const event of this.#performanceEvents) {
	      if (event.at < cutoff || event.at > current.at || event.visibility === "unknown") continue;
	      const cell = scopes[event.scope][event.visibility];
	      event.kind === "script" && (cell.scripts += event.duration), event.kind === "longtask" && (cell.longTasks += 1, cell.longTaskDuration += event.duration), event.kind === "dom" && (cell.domChanges += (event.added ?? 0) + (event.removed ?? 0));
	    }
	    const cellLabel = (cell, evidenceScope) => {
	      const parts = [];
	      return cell.requests && parts.push(
	        `请求 ${cell.requests} / ${formatDuration(cell.requestDuration)}${cell.bytes ? ` / ${formatBytes(cell.bytes)}` : ""}`
	      ), cell.scripts && parts.push(`已归因脚本 ${formatDuration(cell.scripts)}`), evidenceScope === "shared" && cell.longTasks && parts.push(
	        `长任务 ${cell.longTasks} / ${formatDuration(cell.longTaskDuration)}`
	      ), cell.domChanges && parts.push(`页面元素变更 ${cell.domChanges}`), parts.join(" · ") || "—";
	    }, currentLabels = {
	      reader: `${current.readerDom} 个页面元素 · ${current.retainedFloors} 个楼层`,
	      host: `${current.hostDom} 个页面元素`,
	      shared: current.heapBytes === null ? "浏览器未提供" : formatBytes(current.heapBytes)
	    };
	    for (const evidenceScope of ["reader", "host", "shared"]) {
	      const row = this.#scopeRows.get(evidenceScope);
	      row.querySelector(
	        "[data-resource-monitor-scope-current]"
	      ).textContent = currentLabels[evidenceScope], row.querySelector(
	        "[data-resource-monitor-scope-visible]"
	      ).textContent = cellLabel(scopes[evidenceScope].visible, evidenceScope), row.querySelector(
	        "[data-resource-monitor-scope-hidden]"
	      ).textContent = cellLabel(scopes[evidenceScope].hidden, evidenceScope);
	    }
	    const events = [
	      ...requests.map((request) => ({
	        at: request.at,
	        kind: request.kind,
	        visibility: request.visibility,
	        scope: request.scope,
	        duration: request.duration,
	        detail: `${request.event.method} ${requestDisplayPath(request.event)} · ${requestStatus(request.event)} · ${formatDuration(request.duration)}${request.bytes ? ` · ${formatBytes(request.bytes)}` : ""}${request.diagnostic ? ` · ${request.diagnostic}` : ""}`,
	        basis: request.event.resourceTimed ? "PerformanceResourceTiming" : request.scope === "reader" ? "显式 reader 元数据" : request.scope === "host" ? "未标记宿主请求" : "Resource Timing"
	      })),
	      ...this.#performanceEvents
	    ].filter((event) => event.at >= current.at - RETENTION_MS && event.at <= current.at).sort((left, right) => right.at - left.at).slice(0, 48), recent = events.filter((event) => event.at >= cutoff), visibleCount = recent.filter(
	      (event) => event.visibility === "visible"
	    ).length, hiddenCount = recent.filter(
	      (event) => event.visibility === "hidden"
	    ).length, unknownCount = recent.length - visibleCount - hiddenCount;
	    if (this.#evidenceWindow.textContent = `${current.visibility === "visible" ? "当前前台" : "当前后台"} · 前 ${visibleCount} / 后 ${hiddenCount}${unknownCount ? ` / 未归档 ${unknownCount}` : ""} 个原始事件`, this.#eventLog.replaceChildren(...events.map((event) => {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-resource-monitor-event-row"
	      );
	      row.dataset.performanceEventKind = event.kind, row.dataset.performanceEventScope = event.scope, row.dataset.performanceEventVisibility = event.visibility;
	      const time = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "time");
	      time.dateTime = new Date(event.at).toISOString(), time.textContent = formatRequestTimestamp(event.at);
	      const visibility = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-resource-monitor-event-state"
	      );
	      visibility.dataset.visibility = event.visibility, visibility.textContent = event.visibility === "visible" ? "前台" : event.visibility === "hidden" ? "后台" : "未知";
	      const scope = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-resource-monitor-event-scope"
	      );
	      scope.textContent = event.scope === "reader" ? "阅读器" : event.scope === "host" ? "原站" : "页面共享";
	      const kind = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-resource-monitor-event-kind"
	      );
	      kind.textContent = PERFORMANCE_EVENT_LABELS[event.kind] ?? event.kind;
	      const metric = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-resource-monitor-event-metric"
	      );
	      metric.textContent = performanceEventMetric(event);
	      const detail = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-resource-monitor-event-detail"
	      ), detailText = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "strong",
	        "ldp-resource-monitor-event-copy"
	      );
	      detailText.textContent = event.detail;
	      const eventBasis = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "small",
	        "ldp-resource-monitor-event-basis"
	      );
	      return eventBasis.textContent = event.basis, detail.append(detailText, eventBasis), row.append(time, visibility, scope, kind, metric, detail), row;
	    })), !events.length) {
	      const empty = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-resource-monitor-event-empty"
	      );
	      empty.textContent = "等待浏览器性能、网络资源、页面元素或前后台切换事件。", this.#eventLog.append(empty);
	    }
	  }
	  #renderRequests(current, events) {
	    const at = current.at, sent60 = events.filter(
	      (event) => (event.endedAt || event.queuedAt) >= at - 6e4
	    ).filter(
	      (event) => event.phase !== "queued" && !event.controlReason
	    ), recent10 = sent60.filter(
	      (event) => event.startedAt >= at - REQUEST_TRACE_MS
	    ), issues = events.filter(
	      (event) => (event.endedAt || event.startedAt) >= at - 6e4
	    ).map((event) => ({ event, issue: requestIssue(event, at) })).filter((entry) => entry.issue !== null), bucketStart = Math.ceil(at / 100) * 100 - REQUEST_TRACE_MS, buckets = Array.from({ length: REQUEST_TRACE_MS / 100 }, () => 0);
	    for (const event of recent10) {
	      const bucket = Math.floor((event.startedAt - bucketStart) / 100);
	      bucket >= 0 && bucket < buckets.length && (buckets[bucket] += 1);
	    }
	    const transfer = sent60.reduce(
	      (total, event) => total + event.size,
	      0
	    ), metricValues = {
	      rate10: `${recent10.length} 次 · ${(recent10.length / 10).toFixed(1)}/秒`,
	      rate60: `${sent60.length} 次 · ${(sent60.length / 60).toFixed(1)}/秒`,
	      peak: `${Math.max(0, ...buckets)} 次/100ms`,
	      transfer: formatBytes(transfer),
	      issues: `${issues.length} 次${issues.some(({ event }) => event.status === 429) ? ` · 429 ${issues.filter(({ event }) => event.status === 429).length}` : ""}`
	    };
	    for (const [id, value] of Object.entries(metricValues)) {
	      const metric = this.#requestMetrics.get(id);
	      metric && (metric.textContent = value);
	    }
	    const queued = recent10.filter(
	      (event) => event.permitWait > 0.5
	    ).length, liveQueued = events.filter(
	      (event) => event.phase === "queued"
	    ).length, liveRunning = events.filter(
	      (event) => event.phase === "running"
	    ).length, laneStatus = [
	      ["交互", "user-card"],
	      ["控制", "control"],
	      ["树状", "nested-replies"],
	      ["正文批次", "topic-batch"],
	      ["翻译", "translation"],
	      ["其他", "standard"]
	    ].map(
	      ([label, lane]) => `${label} ${current.requestActiveByLane[lane]}/${current.requestQueuedByLane[lane]}`
	    ).join(" · ");
	    this.#requestWindow.textContent = `近 10 秒 ${recent10.length} 已发出 · 排队 ${queued} · 实时 ${liveRunning} 运行 · ${liveQueued} 排队 · 本页生效槽 ${current.activeRequests}/${current.requestMaxConcurrent} · 队列 ${current.queuedRequests}/${current.requestQueueLimit} · 规则 后台正文单槽 · 总预算允许时可见缺口可用第 2 正文槽 · 树状最多 2 槽 · 车道运行/排队:${laneStatus} · 跨标签共享 ${current.sharedActiveRequests} 运行 / ${current.sharedQueuedRequests} 排队 · 实例 ${current.sharedInstances || 1} · 共享生效上限 ${current.sharedMaxConcurrent} · 生效间隔 ${current.sharedMinIntervalMs}ms${current.sharedCoordinationMode === "best-effort" ? "(协调降级)" : ""} · 额度 ${current.shortWindowCount}/${current.shortWindowBudget}(10秒)· ${current.longWindowCount}/${current.longWindowBudget}(60秒)${current.sharedBlockingReason ? ` · ${REQUEST_WAIT_REASON_LABELS[current.sharedBlockingReason] ?? current.sharedBlockingReason}` + (current.sharedNextPermitDelay > 0 ? ` ${formatDuration(current.sharedNextPermitDelay)}` : "") : ""}${current.challengeState === "required" ? " · 等待人工验证" : current.challengeState === "active" ? ` · 验证中${current.challengeOwned ? "(本页)" : "(其他页)"}` : current.challengeState === "passed" ? " · 验证已通过" : ""}`;
	    const traceEvents = events.filter((event) => {
	      const lifecycleStart = event.queuedAt || event.startedAt, lifecycleEnd = event.pending ? at : event.endedAt || event.startedAt;
	      return lifecycleStart <= at && lifecycleEnd >= at - REQUEST_TRACE_MS;
	    });
	    this.#renderRequestTrace(traceEvents, at), this.#renderRequestTypes(sent60, at), this.#renderRequestIssues(issues, at), this.#renderRequestLog(events, at), this.#renderTopicDiagnostics(current, issues), this.#renderMediaDiagnostics(current, issues);
	    const completedDurations = sent60.filter(
	      (event) => !event.pending && !["realtime", "presence"].includes(event.type) && event.duration > 0
	    ).map((event) => event.duration), p95 = percentile95(completedDurations), active = events.filter(
	      (event) => event.phase === "running"
	    ).length, resourceCount = sent60.filter(
	      (event) => ["avatar", "media", "asset"].includes(event.type)
	    ).length, latestLimit = [...events].reverse().find(
	      (event) => event.status === 429 || !!event.retryAfter || !!event.rateLimitCode || !!event.serverLimit
	    );
	    if (latestLimit?.status === 429) {
	      const details = [
	        latestLimit.rateLimitCode ? `错误码 ${latestLimit.rateLimitCode}` : "",
	        latestLimit.retryAfter ? `Retry-After ${latestLimit.retryAfter} 秒` : ""
	      ].filter(Boolean).join(",");
	      this.#requestObserved.textContent = `本页最近观测:${REQUEST_TYPE_LABELS[latestLimit.type] ?? "请求"} ${latestLimit.method} ${requestDisplayPath(latestLimit)} 收到 ${latestLimit.cloudflareMitigated ? "Cloudflare challenge 429" : "429"}${details ? `(${details})` : ""}${latestLimit.decision ? `;决策 ${requestDecisionLabel(latestLimit.decision)}` : ""}。`;
	    } else latestLimit?.serverLimit ? this.#requestObserved.textContent = `服务器最近返回:上限 ${latestLimit.serverLimit}${latestLimit.serverRemaining ? `,剩余 ${latestLimit.serverRemaining}` : ""}${latestLimit.serverReset ? `,重置 ${latestLimit.serverReset}` : ""}。` : this.#requestObserved.textContent = `当前未收到 429 或服务器上限头;所有实例共同使用 ${current.shortWindowBudget} 次/10 秒、${current.longWindowBudget} 次/分钟、并发 ${current.sharedMaxConcurrent} 路(${current.sharedInstances || 1} 个实例)。`;
	    const danger = [...issues].reverse().find(
	      ({ issue }) => issue.level === "danger"
	    );
	    if (danger) {
	      this.#requestBottleneck.dataset.level = "danger", this.#requestBottleneckState.textContent = danger.issue.label;
	      const caller = danger.event.callSite ? `;发起点 ${danger.event.callSite}` : "", diagnostic = requestContractDiagnostic(danger.event);
	      this.#requestBottleneckDetail.textContent = `${REQUEST_TYPE_LABELS[danger.event.type] ?? danger.event.type} ${danger.event.method} ${requestDisplayPath(danger.event)}:${danger.issue.detail}${caller}${diagnostic ? `;${diagnostic}` : ""}${danger.event.retryAfter ? `;Retry-After ${danger.event.retryAfter} 秒` : ""}。`;
	    } else current.challengeState === "required" || current.challengeState === "active" || current.sharedNextPermitDelay > 0 || Math.max(current.queuedRequests, current.sharedQueuedRequests) >= 3 || completedDurations.length >= 5 && p95 >= REQUEST_SLOW_MS ? (this.#requestBottleneck.dataset.level = "warning", this.#requestBottleneckState.textContent = current.challengeState === "required" ? "等待人工验证" : current.challengeState === "active" ? "Cloudflare 验证中" : current.sharedNextPermitDelay > 0 ? REQUEST_WAIT_REASON_LABELS[current.sharedBlockingReason] ?? "共享许可等待" : Math.max(
	      current.queuedRequests,
	      current.sharedQueuedRequests
	    ) >= 3 ? "请求排队" : "响应偏慢", this.#requestBottleneckDetail.textContent = `${current.challengeState === "required" ? "已暂停新的 Reader 请求,请点击唯一人工验证入口;" : current.challengeState === "active" ? current.challengeOwned ? "请在本页打开的验证窗口完成验证;" : "另一标签页正在处理唯一验证窗口;" : ""}${current.sharedNextPermitDelay > 0 ? `共享许可仍需等待 ${formatDuration(
	      current.sharedNextPermitDelay
	    )};` : ""}本页活动/排队 ${current.activeRequests}/${current.queuedRequests},共享活动/排队 ${current.sharedActiveRequests}/${current.sharedQueuedRequests};最近一分钟 P95 ${p95 ? formatDuration(p95) : "暂无"}。`) : sent60.length >= 12 && resourceCount / sent60.length >= 0.7 && transfer >= 4 * 1024 * 1024 ? (this.#requestBottleneck.dataset.level = "warning", this.#requestBottleneckState.textContent = "资源占用", this.#requestBottleneckDetail.textContent = `头像、图片和静态资源占最近一分钟请求的 ${Math.round(
	      resourceCount / sent60.length * 100
	    )}%,已传输 ${formatBytes(transfer)}。`) : liveQueued ? (this.#requestBottleneck.dataset.level = "normal", this.#requestBottleneckState.textContent = "等待放行", this.#requestBottleneckDetail.textContent = `${liveQueued} 条请求已进入中央队列,尚未占用网络连接;下方日志会同步显示放行、取消或完成结果。`) : sent60.length ? (this.#requestBottleneck.dataset.level = "normal", this.#requestBottleneckState.textContent = "节奏正常", this.#requestBottleneckDetail.textContent = `最近一分钟 ${sent60.length} 次请求,${active} 运行、${liveQueued} 排队,P95 ${p95 ? formatDuration(p95) : "暂无"};未发现明显限流、错误或排队瓶颈。`) : (this.#requestBottleneck.dataset.level = "normal", this.#requestBottleneckState.textContent = "等待采样", this.#requestBottleneckDetail.textContent = "打开帖子并滚动后,这里会根据排队、响应耗时、传输量和 429 判断主要瓶颈。");
	  }
	  #renderTopicDiagnostics(current, issues) {
	    const topicLabel = current.topicId === null ? "尚未打开帖子" : `Topic #${current.topicId}`, virtualCount = Math.max(
	      0,
	      current.retainedFloors - current.mountedFloors
	    ), missingStreamFloors = Math.max(
	      0,
	      current.expectedFloors - current.streamFloors
	    ), hasCoverageGap = missingStreamFloors > 0 || current.missingFloors > 0, networkIssue = [...issues].reverse().find(
	      ({ event }) => event.status !== 429 && !event.controlReason
	    ), rateLimited = issues.some(({ event }) => event.status === 429), rows = [
	      {
	        label: "缓存",
	        level: hasCoverageGap ? "warning" : "normal",
	        detail: current.topicId === null ? "没有活动 Topic,不存在需要清理的当前帖子缓存。" : missingStreamFloors > 0 ? `${topicLabel} 的 canonical stream 尚缺 ${missingStreamFloors} 个楼层索引(当前 ${current.streamFloors}/${current.expectedFloors});先等待同一 Topic stream 刷新,再按已取得索引补正文,不建议清缓存。` : current.missingFloors > 0 ? `${topicLabel} 的 canonical stream 仍缺 ${current.missingFloors} 条正文;由同一补流链继续请求,不建议先清缓存。` : current.initializedFromCache ? `${topicLabel} 从完整快照启动,已归并 ${current.retainedFloors}/${current.expectedFloors || current.streamFloors} 条正文。` : `${topicLabel} 已由网络与缓存归并,当前没有 stream 正文缺口。`
	      },
	      {
	        label: "虚拟窗口",
	        level: "normal",
	        detail: current.topicId === null ? "打开帖子后显示 canonical 保留量与当前 DOM 挂载量。" : virtualCount > 0 ? `当前挂载 ${current.mountedFloors}、已准备 ${current.preparedFloors}、canonical 保留 ${current.retainedFloors};其余 ${virtualCount} 条为离屏停放或惰性 DOM,不是楼层丢失。` : `当前 ${current.mountedFloors} 条 canonical 楼层均在挂载窗口内。`
	      },
	      {
	        label: "已删除楼层",
	        level: current.unavailableFloors.length ? "danger" : "normal",
	        detail: current.unavailableFloors.length ? `已由 404/410 确认不可用:${current.unavailableFloors.slice(0, 12).map((postNumber) => `#${postNumber}`).join("、")}${current.unavailableFloors.length > 12 ? " 等" : ""};当前会话不会重复请求。` : "当前会话没有被 404/410 明确判定为不可用的楼层。"
	      },
	      {
	        label: "网络",
	        level: networkIssue?.issue.level ?? "normal",
	        detail: networkIssue ? `${networkIssue.issue.label}:${networkIssue.event.method} ${requestDisplayPath(networkIssue.event)}。先检查原站和网络,错误请求会保留在下方记录。` : current.activeRequests || current.queuedRequests ? `本页活动/排队 ${current.activeRequests}/${current.queuedRequests},请求仍由中央调度器处理。` : "最近一分钟没有普通 HTTP、网络中止或慢响应异常。"
	      },
	      {
	        label: "429 / Cloudflare",
	        level: rateLimited || current.challengeState === "required" || current.challengeState === "active" ? "warning" : "normal",
	        detail: current.challengeState === "required" ? "后台请求已建立共享硬闸门,等待用户点击唯一 Cloudflare 验证入口;滚动和预取不会继续自动打开新页面。" : current.challengeState === "active" ? `唯一 Cloudflare 验证由${current.challengeOwned ? "本页" : "其他标签页"}处理;不要重复刷新或打开验证窗口。` : rateLimited ? "最近一分钟出现 429;同端点重复或跨端点证据会进入共享冷却,恢复时只放行一个探针。" : "当前未发现 429 或 Cloudflare 验证阻塞。"
	      }
	    ];
	    this.#topicDiagnostics.replaceChildren(
	      ...rows.map((diagnostic) => {
	        const row = (0, import_reader_settings_dom.settingsElement)(
	          this.#options.document,
	          "div",
	          "ldp-request-flow-bottleneck"
	        );
	        row.dataset.level = diagnostic.level;
	        const label = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "strong");
	        label.textContent = diagnostic.label;
	        const detail = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "p");
	        return detail.textContent = diagnostic.detail, row.append(label, detail), row;
	      })
	    );
	  }
	  #renderMediaDiagnostics(current, issues) {
	    const media = current.mediaDiagnostics, mediaIssue = [...issues].reverse().find(
	      ({ event }) => event.type === "media" && !event.controlReason
	    ), hlsSupported = media.nativeHlsSources > 0 || media.hlsLibrarySupported, rows = [
	      {
	        label: "图片加载",
	        level: media.failedImages ? "danger" : "normal",
	        detail: media.failedImages ? `当前挂载楼层有 ${media.failedImages} 张图片失败${media.retryingImages ? `,其中 ${media.retryingImages} 张正在重试` : ""}${media.crossOriginFailures ? `;${media.crossOriginFailures} 张来自跨域地址` : ""}。失败楼层:${media.failedPostNumbers.slice(0, 12).map((postNumber) => `#${postNumber}`).join("、") || "未识别"}。` : `当前绑定 ${media.boundImages} 张正文图片,未发现可见重试入口。`
	      },
	      {
	        label: "资源缓存",
	        level: media.catalogFailedBatches ? "warning" : "normal",
	        detail: `图片目录 ${media.catalogImages} 项${media.catalogComplete ? "(全帖完整)" : media.catalogPending ? "(正在补齐)" : "(已加载范围)"};本会话 Object URL ${media.objectUrls}/${media.objectUrlLimit || 0};持久 Blob 缓存${media.persistentCacheEnabled ? "已启用" : "未配置"}${media.catalogFailedBatches ? `;全帖目录有 ${media.catalogFailedBatches} 个失败批次` : ""}。`
	      },
	      {
	        label: "来源楼层",
	        level: media.unavailableSourcePostNumbers.length ? "danger" : "normal",
	        detail: media.unavailableSourcePostNumbers.length ? `目录中的图片来源楼层已由 404/410 确认不可用:${media.unavailableSourcePostNumbers.slice(0, 12).map((postNumber) => `#${postNumber}`).join("、")};已有 CDN/缓存图片仍可继续显示,不重复请求楼层。` : "当前图片目录没有指向已确认删除楼层的来源。"
	      },
	      {
	        label: "音视频 / HLS",
	        level: mediaIssue?.issue.level ?? (media.hlsSources > 0 && !hlsSupported ? "warning" : "normal"),
	        detail: mediaIssue ? `${mediaIssue.issue.detail}:${requestDisplayPath(mediaIssue.event)}。浏览器资源错误可能来自 CDN、CORS、编码或源地址失效。` : media.hlsSources > 0 ? `当前挂载 ${media.hlsSources} 个 HLS 源、原生可播 ${media.nativeHlsSources} 个、活动 Hls.js 实例 ${media.activeHlsPlayers};${hlsSupported ? "原生或 Hls.js capability 可用" : media.hlsLibraryAvailable ? "Hls.js 已加载但当前浏览器报告不支持" : "未发现可用的原生或 Hls.js capability"}。离屏停放时播放器为 0 属于正常释放。` : "当前挂载楼层没有 HLS 源,也没有最近一分钟媒体请求异常。"
	      }
	    ];
	    this.#mediaDiagnostics.replaceChildren(
	      ...rows.map((diagnostic) => {
	        const row = (0, import_reader_settings_dom.settingsElement)(
	          this.#options.document,
	          "div",
	          "ldp-request-flow-bottleneck"
	        );
	        row.dataset.level = diagnostic.level;
	        const label = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "strong");
	        label.textContent = diagnostic.label;
	        const detail = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "p");
	        return detail.textContent = diagnostic.detail, row.append(label, detail), row;
	      })
	    );
	  }
	  #renderRequestTrace(events, at) {
	    const windowStart = at - REQUEST_TRACE_MS, position = (value) => Math.max(
	      0,
	      Math.min(100, (value - windowStart) / REQUEST_TRACE_MS * 100)
	    ), lanes = [
	      ["reader", "阅读器"],
	      ["host", "原站"],
	      ["browser", "资源"]
	    ];
	    this.#requestTrace.replaceChildren(...lanes.map(([source, label]) => {
	      const lane = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-request-flow-trace-lane"
	      );
	      lane.dataset.requestFlowSource = source;
	      const copy = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-trace-label"
	      );
	      copy.textContent = label;
	      const track = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-trace-track"
	      ), layerEnds = [
	        Number.NEGATIVE_INFINITY,
	        Number.NEGATIVE_INFINITY,
	        Number.NEGATIVE_INFINITY
	      ], sourceEvents = events.filter((event) => event.source === source).sort(
	        (left, right) => (left.queuedAt || left.startedAt) - (right.queuedAt || right.startedAt)
	      ).slice(-80);
	      for (const event of sourceEvents) {
	        const rawLifecycleStart = event.queuedAt || event.startedAt, rawLifecycleEnd = event.pending ? at : event.endedAt || event.startedAt;
	        let layer = layerEnds.findIndex(
	          (end) => end <= rawLifecycleStart
	        );
	        layer < 0 && (layer = layerEnds.indexOf(Math.min(...layerEnds))), layerEnds[layer] = rawLifecycleEnd;
	        const top = 3 + layer * 10, lifecycleStart = Math.max(windowStart, rawLifecycleStart), queueEnd = Math.min(
	          at,
	          event.phase === "queued" ? at : event.permittedAt
	        ), wireEnd = ["queued", "running"].includes(event.phase) ? at : Math.max(event.startedAt, event.endedAt), priority = requestPriorityLabel(event.priority), tooltip = [
	          `${formatRequestTimestamp(event.queuedAt)} · ${event.method} ${requestDisplayPath(event)}`,
	          `${source === "reader" ? "阅读器" : source === "host" ? "原站" : "资源"} / ${REQUEST_TYPE_LABELS[event.type] ?? event.type} / ${requestStatus(event)}`,
	          requestTimingLabel(event, at),
	          priority ? `${priority}优先级` : "",
	          event.attempt > 1 ? `第 ${event.attempt} 次尝试` : "",
	          event.callSite ? `发起点 ${event.callSite}` : "",
	          requestContractDiagnostic(event)
	        ].filter(Boolean).join(" · ");
	        if (queueEnd > lifecycleStart + 0.5) {
	          const queue = (0, import_reader_settings_dom.settingsElement)(
	            this.#options.document,
	            "i",
	            "ldp-request-flow-trace-queue"
	          );
	          if (queue.style.setProperty(
	            "--ldp-request-flow-left",
	            `${position(lifecycleStart).toFixed(3)}%`
	          ), queue.style.setProperty(
	            "--ldp-request-flow-width",
	            `${Math.max(
	              0,
	              position(queueEnd) - position(lifecycleStart)
	            ).toFixed(3)}%`
	          ), queue.style.setProperty("--ldp-request-flow-top", `${top}px`), queue.dataset.ldpTooltipLabel = `${event.method} ${requestDisplayPath(event)} · ${formatRequestTimestamp(rawLifecycleStart)} → ${formatRequestTimestamp(queueEnd)} · ${requestWaitReasonLabel(event.waitReason)}排队 ` + formatDuration(queueEnd - rawLifecycleStart), track.append(queue), event.phase !== "queued" && !event.controlReason) {
	            const permit = (0, import_reader_settings_dom.settingsElement)(
	              this.#options.document,
	              "i",
	              "ldp-request-flow-trace-permit"
	            );
	            permit.style.setProperty(
	              "--ldp-request-flow-left",
	              `${position(queueEnd).toFixed(3)}%`
	            ), permit.style.setProperty(
	              "--ldp-request-flow-top",
	              `${top}px`
	            ), permit.dataset.ldpTooltipLabel = `${formatRequestTimestamp(queueEnd)} · ${event.method} ${requestDisplayPath(event)} · 调度放行`, track.append(permit);
	          }
	        }
	        if (event.phase !== "queued" && !event.controlReason) {
	          const wire = (0, import_reader_settings_dom.settingsElement)(
	            this.#options.document,
	            "i",
	            "ldp-request-flow-trace-wire"
	          );
	          wire.dataset.requestFlowType = event.type, wire.dataset.pending = String(event.pending), wire.style.setProperty(
	            "--ldp-request-flow-left",
	            `${position(Math.max(windowStart, event.startedAt)).toFixed(3)}%`
	          ), wire.style.setProperty(
	            "--ldp-request-flow-width",
	            `${Math.max(
	              0,
	              position(wireEnd) - position(Math.max(windowStart, event.startedAt))
	            ).toFixed(3)}%`
	          ), wire.style.setProperty("--ldp-request-flow-top", `${top}px`), wire.dataset.ldpTooltipLabel = tooltip, track.append(wire);
	        }
	        const issue = requestIssue(event, at);
	        if (issue) {
	          const marker = (0, import_reader_settings_dom.settingsElement)(
	            this.#options.document,
	            "i",
	            "ldp-request-flow-trace-anomaly"
	          );
	          marker.dataset.level = issue.level, marker.style.setProperty(
	            "--ldp-request-flow-left",
	            `${Math.max(
	              0.6,
	              Math.min(99.4, position(wireEnd))
	            ).toFixed(3)}%`
	          ), marker.style.setProperty(
	            "--ldp-request-flow-top",
	            `${top}px`
	          ), marker.dataset.ldpTooltipLabel = `${formatRequestTimestamp(Math.min(at, Math.max(
	            windowStart,
	            rawLifecycleEnd
	          )))} · ${issue.label} · ${event.method} ${requestDisplayPath(event)} · ` + issue.detail, track.append(marker);
	        }
	      }
	      return lane.append(copy, track), lane;
	    }));
	    const semanticItems = [
	      ["ldp-request-flow-queue-key", "list", "排队"],
	      ["ldp-request-flow-warning-key", "history", "慢/久候"],
	      ["ldp-request-flow-danger-key", "circle-x", "错误/限流"]
	    ].map(([
	      className,
	      iconName,
	      copy
	    ]) => {
	      const item = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "span"), key = (0, import_reader_icon.createReaderIcon)(
	        this.#options.document,
	        iconName,
	        `ldp-request-flow-key ${className}`
	      );
	      return item.append(key, this.#options.document.createTextNode(copy)), item;
	    }), types = [...new Set(events.map((event) => event.type))].slice(0, 8), typeItems = (types.length ? types : ["other"]).map((type) => {
	      const item = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "span");
	      item.dataset.requestFlowType = type;
	      const dot = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "i",
	        "ldp-request-flow-dot"
	      );
	      return item.append(
	        dot,
	        this.#options.document.createTextNode(
	          REQUEST_TYPE_LABELS[type] ?? type
	        )
	      ), item;
	    });
	    this.#requestLegend.replaceChildren(...semanticItems, ...typeItems);
	  }
	  #renderRequestTypes(events, at) {
	    const byType = /* @__PURE__ */ new Map();
	    for (const event of events) {
	      const list = byType.get(event.type) ?? [];
	      list.push(event), byType.set(event.type, list);
	    }
	    const entries = [...byType].map(([type, list]) => ({
	      type,
	      list,
	      issues: list.filter((event) => requestIssue(event, at)).length,
	      p95: percentile95(
	        list.filter(
	          (event) => !event.pending && !["realtime", "presence"].includes(event.type) && event.duration > 0
	        ).map((event) => event.duration)
	      )
	    })).sort((left, right) => right.list.length - left.list.length || left.type.localeCompare(right.type));
	    this.#requestTypes.replaceChildren(...entries.map((entry) => {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-request-flow-type-row"
	      );
	      row.dataset.requestFlowType = entry.type;
	      const name = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-type-name"
	      ), dot = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "i",
	        "ldp-request-flow-dot"
	      ), copy = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "strong");
	      copy.textContent = REQUEST_TYPE_LABELS[entry.type] ?? entry.type, name.append(dot, copy);
	      for (const text of [
	        `${entry.list.length} 次`,
	        entry.p95 ? formatDuration(entry.p95) : "—",
	        entry.issues ? String(entry.issues) : "—"
	      ]) {
	        const value = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "span");
	        value.textContent = text, row.append(value);
	      }
	      return row.prepend(name), row;
	    })), entries.length || this.#requestTypes.append(this.#requestEmpty("还没有可统计的请求。"));
	  }
	  #renderRequestIssues(entries, at) {
	    this.#requestAnomalyWindow.textContent = entries.length ? `最近 60 秒 ${entries.length} 个 · 红色为失败/限流` : "最近 60 秒未发现异常";
	    const latest = entries.slice(-8).reverse();
	    this.#requestAnomalies.replaceChildren(...latest.map(({ event, issue }) => {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-request-flow-anomaly-row"
	      );
	      row.dataset.level = issue.level, row.dataset.requestFlowType = event.type;
	      const caller = event.callSite || `${event.transport} 发起`, diagnostic = requestContractDiagnostic(event);
	      row.dataset.ldpTooltipLabel = `${event.method} ${requestDisplayPath(event)} · ${issue.detail} · 发起点 ${caller}${diagnostic ? ` · ${diagnostic}` : ""}`;
	      const time = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "time",
	        "ldp-request-flow-anomaly-time"
	      );
	      time.dateTime = new Date(event.endedAt || event.startedAt).toISOString(), time.textContent = formatRequestTimestamp(
	        event.endedAt || event.startedAt
	      );
	      const kind = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-anomaly-kind"
	      );
	      kind.textContent = issue.label;
	      const detail = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-anomaly-detail"
	      ), path = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "strong");
	      path.textContent = `${event.method} ${requestDisplayPath(event)}`;
	      const timing = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "small");
	      return timing.textContent = `${event.source === "reader" ? "阅读器" : event.source === "host" ? "原站" : "资源"} / ${REQUEST_TYPE_LABELS[event.type] ?? event.type} · ${requestTimingLabel(event, at)} · ${issue.detail} · 发起点 ${caller}` + (diagnostic ? ` · ${diagnostic}` : ""), detail.append(path, timing), row.append(time, kind, detail), row;
	    })), latest.length || this.#requestAnomalies.append(
	      this.#requestEmpty(
	        "最近 60 秒没有 HTTP 错误、网络中止、慢响应或长时间排队。"
	      )
	    );
	  }
	  #renderRequestLog(events, at) {
	    const scrollTop = this.#requestLog.scrollTop, latest = events.slice(-60).reverse();
	    this.#requestLog.replaceChildren(...latest.map((event) => {
	      const row = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "div",
	        "ldp-request-flow-log-row"
	      ), issue = requestIssue(event, at);
	      issue && (row.dataset.level = issue.level), row.dataset.requestFlowType = event.type, row.dataset.requestPhase = event.phase, event.logicalId && (row.dataset.requestLogicalId = event.logicalId), event.decision && (row.dataset.requestDecision = event.decision);
	      const priority = requestPriorityLabel(event.priority), caller = event.callSite || `${event.transport} 发起`, diagnostic = requestContractDiagnostic(event);
	      row.dataset.ldpTooltipLabel = [
	        `${event.method} ${requestDisplayPath(event)}`,
	        priority ? `${priority}优先级` : "",
	        requestTimingLabel(event, at),
	        `发起点 ${caller}`,
	        diagnostic
	      ].filter(Boolean).join(" · ");
	      const time = (0, import_reader_settings_dom.settingsElement)(this.#options.document, "time");
	      time.dateTime = new Date(event.queuedAt).toISOString(), time.textContent = formatRequestTimestamp(event.queuedAt);
	      const source = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-source"
	      );
	      source.textContent = event.source === "reader" ? "阅读器" : event.source === "host" ? "原站" : "资源";
	      const type = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-type-name"
	      ), dot = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "i",
	        "ldp-request-flow-dot"
	      );
	      type.append(
	        dot,
	        this.#options.document.createTextNode(
	          REQUEST_TYPE_LABELS[event.type] ?? event.type
	        )
	      );
	      const priorityName = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-priority"
	      );
	      priorityName.textContent = priority || "—";
	      const status = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-status"
	      );
	      status.textContent = requestStatus(event), status.classList.toggle(
	        "is-error",
	        issue?.level === "danger"
	      );
	      const timing = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-timing"
	      );
	      timing.textContent = requestTimingLabel(event, at);
	      const path = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "span",
	        "ldp-request-flow-path"
	      ), target = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "strong",
	        "ldp-request-flow-target"
	      );
	      target.textContent = `${requestDisplayPath(event)} ← ${caller}`;
	      const contract = (0, import_reader_settings_dom.settingsElement)(
	        this.#options.document,
	        "small",
	        "ldp-request-flow-contract"
	      );
	      return contract.textContent = diagnostic || `${event.transport} · 未标记契约`, path.append(target, contract), row.append(time, source, type, priorityName, status, timing, path), row;
	    })), latest.length || this.#requestLog.append(
	      this.#requestEmpty(
	        "打开帖子或操作原站页面后,请求会按时间出现在这里。"
	      )
	    ), scrollTop && (this.#requestLog.scrollTop = scrollTop);
	  }
	  #requestEmpty(text) {
	    const empty = (0, import_reader_settings_dom.settingsElement)(
	      this.#options.document,
	      "div",
	      "ldp-request-flow-empty"
	    );
	    return empty.textContent = text, empty;
	  }
	  #prune(at) {
	    const cutoff = at - RETENTION_MS;
	    for (; this.#samples.length && this.#samples[0].at < cutoff; )
	      this.#samples.shift(), this.#sampleRetentionDrops += 1;
	    for (let index = this.#performanceEvents.length - 1; index >= 0; index -= 1)
	      this.#performanceEvents[index].at < cutoff && (this.#performanceEvents.splice(index, 1), this.#evidenceRetentionDrops += 1);
	    for (let index = this.#visibilityTimeline.length - 1; index >= 0; index -= 1)
	      this.#visibilityTimeline[index].at < cutoff && index !== this.#visibilityTimeline.length - 1 && (this.#visibilityTimeline.splice(index, 1), this.#visibilityRetentionDrops += 1);
	    const requestRuntimeCutoff = at - REQUEST_RUNTIME_RETENTION_MS;
	    for (let index = this.#requestRuntimeStates.length - 1; index >= 0; index -= 1)
	      this.#requestRuntimeStates[index].lastObservedAt < requestRuntimeCutoff && (this.#requestRuntimeStates.splice(index, 1), this.#requestRuntimeRetentionDrops += 1);
	    const retainedRequestIds = new Set(
	      this.requests.snapshot.events.map((event) => event.id)
	    );
	    for (const id of this.#requestVisibility.keys())
	      retainedRequestIds.has(id) || this.#requestVisibility.delete(id);
	  }
	  #now() {
	    return this.#options.now?.() ?? Date.now();
	  }
	  #visibility() {
	    return this.#options.document.visibilityState === "hidden" ? "hidden" : "visible";
	  }
	}
}, "7450ca3f7fc1d41ee2644517bb2e30a1a7abd16b7be0c13fe662f126d912605f");

/* Source: lite/src/network/browser-request-observation.ts */
runtime.register("src/network/browser-request-observation.js", function(module, exports, require) {
	var browser_request_observation_exports = {};
	__export(browser_request_observation_exports, {
	  BrowserResourceObservationAdapter: () => BrowserResourceObservationAdapter,
	  DiscourseNativeAjaxObservationAdapter: () => DiscourseNativeAjaxObservationAdapter
	});
	module.exports = __toCommonJS(browser_request_observation_exports);
	function jqueryModule(value) {
	  const candidate = value && typeof value == "object" && "default" in value ? value.default : value;
	  return typeof candidate == "function" ? candidate : null;
	}
	function ajaxResponseHeader(response, name) {
	  try {
	    return String(response.getResponseHeader?.(name) ?? "");
	  } catch {
	    return "";
	  }
	}
	function ajaxFinish(response) {
	  const status = Number(response.status), cloudflareMitigated = ajaxResponseHeader(response, "cf-mitigated").trim().toLowerCase() === "challenge";
	  return Object.freeze({
	    ...Number.isFinite(status) && status >= 0 ? { status } : {},
	    cloudflareMitigated,
	    rateLimitCode: ajaxResponseHeader(response, "Discourse-Rate-Limit-Error-Code") || ajaxResponseHeader(response, "X-Discourse-Rate-Limit-Error-Code"),
	    retryAfter: ajaxResponseHeader(response, "Retry-After"),
	    serverLimit: ajaxResponseHeader(response, "RateLimit-Limit") || ajaxResponseHeader(response, "X-RateLimit-Limit"),
	    serverRemaining: ajaxResponseHeader(response, "RateLimit-Remaining") || ajaxResponseHeader(response, "X-RateLimit-Remaining"),
	    serverReset: ajaxResponseHeader(response, "RateLimit-Reset") || ajaxResponseHeader(response, "X-RateLimit-Reset")
	  });
	}
	class DiscourseNativeAjaxObservationAdapter {
	  #observer;
	  #jqueryModule;
	  #document;
	  #namespace;
	  #hostRequestBudget;
	  constructor(options) {
	    this.#observer = options.observer, this.#jqueryModule = options.jqueryModule, this.#document = options.document ?? document, this.#hostRequestBudget = options.hostRequestBudget ?? null;
	    const namespace = String(options.namespace ?? "mianLiteRequestObserver").trim();
	    if (!/^[A-Za-z][A-Za-z0-9]*$/.test(namespace))
	      throw new Error("jQuery ajax observation namespace 非法");
	    this.#namespace = namespace;
	  }
	  install(scope) {
	    let module2 = null;
	    try {
	      module2 = jqueryModule(this.#jqueryModule);
	    } catch {
	      return !1;
	    }
	    if (!module2) return !1;
	    let target;
	    try {
	      target = module2(this.#document);
	    } catch {
	      return !1;
	    }
	    if (!target || typeof target.on != "function" || typeof target.off != "function")
	      return !1;
	    const active = /* @__PURE__ */ new Map(), borrowedIds = /* @__PURE__ */ new Set(), send = (_event, rawResponse, rawSettings) => {
	      if (!rawResponse || typeof rawResponse != "object") return;
	      const settings = rawSettings ?? {}, href = String(settings.url ?? "");
	      if (!href) return;
	      const method = String(settings.type ?? settings.method ?? "GET"), readerId = this.#observer.matchActive({
	        href,
	        method,
	        source: "reader",
	        excludedIds: borrowedIds
	      });
	      readerId !== null && borrowedIds.add(readerId);
	      const id = readerId ?? this.#observer.begin({
	        href,
	        method,
	        transport: "xmlhttprequest",
	        source: "host"
	      }), event = this.#observer.snapshot.events.find(
	        (candidate) => candidate.id === id
	      ), sharedApiResponse = !!(event?.sameOrigin && !["avatar", "media", "asset", "realtime", "presence"].includes(
	        event.type
	      )), countsAgainstSharedBudget = sharedApiResponse && event?.source === "host" && event.transport === "xmlhttprequest";
	      let hostLease = null;
	      if (countsAgainstSharedBudget && this.#hostRequestBudget && event)
	        try {
	          hostLease = this.#hostRequestBudget.recordHostStart({
	            startedAt: event.startedAt
	          });
	        } catch {
	        }
	      active.set(rawResponse, {
	        id,
	        borrowed: readerId !== null,
	        hostLease,
	        sharedResponse: sharedApiResponse && event ? Object.freeze({
	          source: event.source === "reader" ? "reader" : "host",
	          href: event.href,
	          method: event.method,
	          recoveryProbe: event.recoveryProbe,
	          blockOnCloudflareChallenge: event.type !== "read"
	        }) : null
	      });
	    }, complete = (_event, rawResponse) => {
	      if (!rawResponse || typeof rawResponse != "object") return;
	      const current = active.get(rawResponse);
	      if (!current) return;
	      active.delete(rawResponse), current.borrowed && borrowedIds.delete(current.id), current.hostLease?.release();
	      const finish = ajaxFinish(rawResponse);
	      if (this.#observer.finish(
	        current.id,
	        finish
	      ), current.sharedResponse && this.#hostRequestBudget)
	        try {
	          this.#hostRequestBudget.noteObservedResponse({
	            ...current.sharedResponse,
	            status: finish.status ?? 0,
	            cloudflareMitigated: finish.cloudflareMitigated === !0,
	            retryAfter: finish.retryAfter ?? "",
	            rateLimitCode: finish.rateLimitCode ?? "",
	            serverLimit: finish.serverLimit ?? "",
	            serverRemaining: finish.serverRemaining ?? "",
	            serverReset: finish.serverReset ?? ""
	          });
	        } catch {
	        }
	    }, sendEvent = `ajaxSend.${this.#namespace}`, completeEvent = `ajaxComplete.${this.#namespace}`;
	    try {
	      target.on(sendEvent, send), target.on(completeEvent, complete);
	    } catch {
	      try {
	        target.off(sendEvent, send), target.off(completeEvent, complete);
	      } catch {
	      }
	      return !1;
	    }
	    return scope.add(() => {
	      target.off(sendEvent, send), target.off(completeEvent, complete);
	      for (const current of active.values())
	        current.hostLease?.release(), current.borrowed || this.#observer.finish(current.id, {
	          error: "observer-detached"
	        });
	      active.clear(), borrowedIds.clear();
	    }), !0;
	  }
	}
	class BrowserResourceObservationAdapter {
	  #observer;
	  #performance;
	  #createObserver;
	  constructor(options) {
	    this.#observer = options.observer, this.#performance = options.performance ?? performance, this.#createObserver = options.createObserver ?? ((callback) => new PerformanceObserver(callback));
	  }
	  install(scope) {
	    let nativeObserver = null;
	    try {
	      nativeObserver = this.#createObserver((list) => {
	        for (const entry of list.getEntries()) {
	          if (entry.entryType !== "resource") continue;
	          const resource = entry;
	          this.#observer.recordResource({
	            href: resource.name,
	            initiatorType: resource.initiatorType,
	            startedAt: this.#performance.timeOrigin + resource.startTime,
	            endedAt: this.#performance.timeOrigin + (resource.responseEnd || resource.startTime + resource.duration),
	            status: Number(resource.responseStatus) || 0,
	            size: Number(resource.transferSize || resource.encodedBodySize) || 0
	          });
	        }
	      }), nativeObserver.observe({ type: "resource", buffered: !0 });
	    } catch {
	      return nativeObserver?.disconnect(), !1;
	    }
	    const installedObserver = nativeObserver;
	    return scope.add(() => installedObserver.disconnect()), !0;
	  }
	}
}, "593c4a76fa7321cbe6674854a09ea2cf801136a3491954502eb5c071f8f3bf9c");

/* Source: lite/src/network/browser-shared-request-permit.ts */
runtime.register("src/network/browser-shared-request-permit.js", function(module, exports, require) {
	var browser_shared_request_permit_exports = {};
	__export(browser_shared_request_permit_exports, {
	  BrowserSharedRequestPermit: () => BrowserSharedRequestPermit,
	  READER_BACKGROUND_REQUEST_IDLE_INTERVAL_MS: () => READER_BACKGROUND_REQUEST_IDLE_INTERVAL_MS,
	  READER_BACKGROUND_REQUEST_MAX_DEFER_MS: () => READER_BACKGROUND_REQUEST_MAX_DEFER_MS,
	  READER_CLOUDFLARE_CHALLENGE_WINDOW_NAME: () => READER_CLOUDFLARE_CHALLENGE_WINDOW_NAME,
	  READER_REQUEST_PERMIT_CHANNEL: () => READER_REQUEST_PERMIT_CHANNEL,
	  READER_REQUEST_PERMIT_LOCK: () => READER_REQUEST_PERMIT_LOCK,
	  READER_REQUEST_PERMIT_STORAGE_KEY: () => READER_REQUEST_PERMIT_STORAGE_KEY,
	  browserCloudflareChallengeFeatures: () => browserCloudflareChallengeFeatures,
	  browserCloudflareChallengeHref: () => browserCloudflareChallengeHref,
	  isReaderCloudflareChallengeWindow: () => isReaderCloudflareChallengeWindow,
	  monitorReaderCloudflareChallengeWindow: () => monitorReaderCloudflareChallengeWindow
	});
	module.exports = __toCommonJS(browser_shared_request_permit_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_request_rate_limit_policy = require("./request-rate-limit-policy.js"), import_request_scheduler = require("./request-scheduler.js");
	const READER_REQUEST_PERMIT_STORAGE_KEY = "linuxdo-enhanced-reader:request-permit:v1", READER_REQUEST_PERMIT_LOCK = "linuxdo-enhanced-reader:request-permit-lock:v1", READER_REQUEST_PERMIT_CHANNEL = "linuxdo-enhanced-reader:request-permit-channel:v1", READER_CLOUDFLARE_CHALLENGE_WINDOW_NAME = "ldp-cloudflare-challenge", READER_BACKGROUND_REQUEST_IDLE_INTERVAL_MS = 2500, READER_BACKGROUND_REQUEST_MAX_DEFER_MS = 15e3, READER_CLOUDFLARE_CHALLENGE_MAX_PROBE_INTERVAL_MS = 1e4, READER_RATE_LIMIT_EVIDENCE_WINDOW_MS = 4e3, READER_RATE_LIMIT_MAX_BACKOFF_MS = 6e4, READER_RATE_LIMIT_PROBE_FAILURE_WAIT_MS = 1e3, READER_RATE_LIMIT_PROBE_RECHECK_MS = 500;
	function isReaderCloudflareChallengeWindow(window) {
	  return window.name === READER_CLOUDFLARE_CHALLENGE_WINDOW_NAME;
	}
	function readerCloudflareChallengeLeaseState(storage, now) {
	  try {
	    const stored = storage.getItem(READER_REQUEST_PERMIT_STORAGE_KEY);
	    if (!stored) return "released";
	    const parsed = JSON.parse(stored);
	    if (!parsed || typeof parsed != "object") return "unknown";
	    const challenge = parsed.challenge;
	    if (challenge == null) return "released";
	    if (typeof challenge != "object") return "unknown";
	    const source = challenge, state = String(source.state ?? ""), expiresAt = Number(source.expiresAt);
	    return Number.isFinite(expiresAt) ? expiresAt <= now || state === "passed" ? "released" : state === "active" || state === "required" ? "active" : "unknown" : "unknown";
	  } catch {
	    return "unknown";
	  }
	}
	function monitorReaderCloudflareChallengeWindow(options) {
	  const schedule = options.schedule ?? ((callback, intervalMs2) => setInterval(callback, intervalMs2)), cancel = options.cancel ?? ((handle) => clearInterval(handle)), intervalMs = Math.max(250, Number(options.intervalMs ?? 1e3)), now = options.now ?? Date.now, onError = options.onError ?? (() => {
	  });
	  let timer = null, stopped = !1;
	  const stop = () => {
	    stopped || (stopped = !0, timer !== null && cancel(timer), timer = null, options.storageEvents?.removeEventListener("storage", onStorage));
	  }, check = () => {
	    if (!(stopped || readerCloudflareChallengeLeaseState(options.storage, now()) !== "released"))
	      try {
	        options.close(), stop();
	      } catch (error) {
	        onError(error);
	      }
	  }, onStorage = (event) => {
	    const key = event.key;
	    (key == null || key === READER_REQUEST_PERMIT_STORAGE_KEY) && check();
	  };
	  return options.storageEvents?.addEventListener("storage", onStorage), check(), stopped || (timer = schedule(check, intervalMs)), stop;
	}
	function storedChallengeProbeState(source) {
	  if (!source) return Object.freeze({});
	  const token = typeof source.probeToken == "string" ? source.probeToken.trim() : "", notBefore = Number(source.probeNotBefore), backoffMs = Number(source.probeBackoffMs);
	  return Object.freeze({
	    ...token ? { probeToken: token } : {},
	    ...Number.isFinite(notBefore) && notBefore > 0 ? { probeNotBefore: notBefore } : {},
	    ...Number.isSafeInteger(backoffMs) && backoffMs > 0 ? { probeBackoffMs: backoffMs } : {}
	  });
	}
	const CLEAR_RATE_LIMIT_GATE = Object.freeze({
	  waitMs: 0,
	  recoveryProbe: !1,
	  global: !1,
	  probeWaiting: !1
	}), PRIORITY_WEIGHT = Object.freeze({
	  critical: 0,
	  interactive: 1,
	  nested: 2,
	  visible: 3,
	  prefetch: 4,
	  background: 5
	});
	function positiveInteger(value, fallback, name) {
	  const normalized = Number(value ?? fallback);
	  if (!Number.isSafeInteger(normalized) || normalized < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return normalized;
	}
	function nonNegativeInteger(value, name) {
	  const normalized = Number(value);
	  if (!Number.isSafeInteger(normalized) || normalized < 0)
	    throw new RangeError(`${name} 必须是非负安全整数`);
	  return normalized;
	}
	function unitInterval(value, fallback, name) {
	  const normalized = Number(value ?? fallback);
	  if (!Number.isFinite(normalized) || normalized < 0 || normalized > 1)
	    throw new RangeError(`${name} 必须位于 0 到 1`);
	  return normalized;
	}
	function authoritativeRetryAfter(value, now) {
	  const raw = String(value ?? "").trim();
	  if (!raw) return !1;
	  const seconds = Number(raw);
	  return Number.isFinite(seconds) ? seconds > 0 : Date.parse(raw) > now;
	}
	function observedRateLimitWindowWaitMs(window) {
	  return window === "10s" ? 1e4 : window === "60s" || window === "10s+60s" ? 6e4 : 0;
	}
	function normalizedSourceId(value) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error("request permit sourceId 不能为空");
	  return normalized;
	}
	function activeRateLimitLease(lease) {
	  return lease.scope === "global" || lease.authoritative || lease.hits >= 2;
	}
	function emptyState() {
	  return {
	    schemaVersion: 1,
	    updatedAt: 0,
	    events: [],
	    intents: [],
	    active: [],
	    policies: [],
	    rateLimits: [],
	    challenge: null
	  };
	}
	function normalizeState(raw, now, longWindowMs) {
	  const source = raw && typeof raw == "object" ? raw : {}, events = Array.isArray(source.events) ? source.events.map(Number).filter((at) => Number.isFinite(at) && at > now - longWindowMs && at <= now).sort((left, right) => left - right).slice(-1e3) : [], intents = Array.isArray(source.intents) ? source.intents.filter((intent) => !!intent && typeof intent == "object" && typeof intent.id == "string" && typeof intent.ownerId == "string" && intent.priority in PRIORITY_WEIGHT && (intent.rateLimitRoute === void 0 || typeof intent.rateLimitRoute == "string") && Number.isFinite(intent.queuedAt) && Number(intent.expiresAt) > now).map((intent) => Object.freeze({
	    ...intent,
	    rateLimitRoute: String(intent.rateLimitRoute ?? "").trim()
	  })).slice(-256) : [], active = Array.isArray(source.active) ? source.active.filter((permit) => !!permit && typeof permit == "object" && typeof permit.id == "string" && typeof permit.ownerId == "string" && Number(permit.expiresAt) > now).slice(-128) : [], policies = Array.isArray(source.policies) ? source.policies.filter((policy) => !!policy && typeof policy == "object" && typeof policy.ownerId == "string" && Number(policy.expiresAt) > now && Number.isSafeInteger(Number(policy.shortBudget)) && Number(policy.shortBudget) > 0 && Number.isSafeInteger(Number(policy.longBudget)) && Number(policy.longBudget) > 0 && Number.isSafeInteger(Number(policy.minIntervalMs)) && Number(policy.minIntervalMs) >= 0 && Number.isSafeInteger(Number(policy.maxConcurrent)) && Number(policy.maxConcurrent) > 0).map((policy) => Object.freeze({
	    ownerId: policy.ownerId,
	    shortBudget: Number(policy.shortBudget),
	    longBudget: Number(policy.longBudget),
	    minIntervalMs: Number(policy.minIntervalMs),
	    maxConcurrent: Number(policy.maxConcurrent),
	    expiresAt: Number(policy.expiresAt)
	  })).slice(-64) : [], rateLimits = Array.isArray(source.rateLimits) ? source.rateLimits.filter((lease) => !!lease && typeof lease == "object" && ["endpoint", "global"].includes(String(lease.scope)) && typeof lease.route == "string" && lease.route.trim() !== "" && Number.isSafeInteger(Number(lease.hits)) && Number(lease.hits) > 0 && Number.isFinite(Number(lease.lastObservedAt)) && Number.isFinite(Number(lease.retryAt)) && Number(lease.expiresAt) > now).map((lease) => Object.freeze({
	    scope: lease.scope === "global" ? "global" : "endpoint",
	    route: String(lease.route),
	    hits: Number(lease.hits),
	    authoritative: lease.authoritative === !0,
	    lastObservedAt: Number(lease.lastObservedAt),
	    retryAt: Number(lease.retryAt),
	    expiresAt: Number(lease.expiresAt),
	    ...typeof lease.probeOwnerId == "string" && lease.probeOwnerId ? { probeOwnerId: lease.probeOwnerId } : {},
	    ...Number(lease.probeExpiresAt) > now ? { probeExpiresAt: Number(lease.probeExpiresAt) } : {}
	  })).slice(-128) : [], challengeSource = source.challenge && typeof source.challenge == "object" ? source.challenge : null, challenge = challengeSource && typeof challengeSource.ownerId == "string" && ["required", "active", "passed"].includes(String(challengeSource.state)) && Number(challengeSource.expiresAt) > now ? Object.freeze({
	    ownerId: challengeSource.ownerId,
	    state: String(challengeSource.state) === "passed" ? "passed" : "active",
	    required: String(challengeSource.state) === "required" || String(challengeSource.state) === "active" && (challengeSource.required === !0 || challengeSource.ownerId === ""),
	    automaticAttempted: typeof challengeSource.automaticAttempted == "boolean" ? challengeSource.automaticAttempted : challengeSource.ownerId === "",
	    recoveryProbeAttempted: challengeSource.recoveryProbeAttempted === !0,
	    ...storedChallengeProbeState(challengeSource),
	    updatedAt: Math.max(
	      0,
	      Number(challengeSource.updatedAt) || 0
	    ),
	    expiresAt: Number(challengeSource.expiresAt)
	  }) : null;
	  return {
	    schemaVersion: 1,
	    updatedAt: Math.max(0, Number(source.updatedAt) || 0),
	    events,
	    intents,
	    active,
	    policies,
	    rateLimits,
	    /* 旧 v1 的 cooldown/学习字段不会被复制;只保留新式有界 429 lease。 */
	    challenge
	  };
	}
	function challengeOrigin(value) {
	  const url = new URL(String(value));
	  if (!["http:", "https:"].includes(url.protocol))
	    throw new Error("Cloudflare 验证 origin 必须是 HTTP(S)");
	  return url.origin;
	}
	function challengeHrefMatchesOrigin(href, origin) {
	  try {
	    return new URL(String(href), `${origin}/`).origin === origin;
	  } catch {
	    return !1;
	  }
	}
	function browserCloudflareChallengeHref(originValue, redirectHref) {
	  const origin = challengeOrigin(originValue);
	  let redirect = new URL("/", origin);
	  try {
	    const candidate = new URL(String(redirectHref ?? ""), `${origin}/`);
	    candidate.origin === origin && ["http:", "https:"].includes(candidate.protocol) && !/^\/challenge(?:\/|$)/i.test(candidate.pathname) && (candidate.username = "", candidate.password = "", redirect = candidate);
	  } catch {
	  }
	  const challenge = new URL(
	    new URL(origin).hostname.toLowerCase() === "linux.do" ? "/challenge" : "/",
	    origin
	  );
	  return challenge.pathname === "/challenge" && challenge.searchParams.set("redirect", redirect.href), challenge.href;
	}
	function inspectChallengeWindow(popup) {
	  try {
	    const document = popup.document;
	    if (!document || !!(document.querySelector(
	      'script[src*="/cdn-cgi/challenge-platform/"],#challenge-running,iframe[src*="challenges.cloudflare.com"],.cf-turnstile,input[name="cf-turnstile-response"]'
	    ) || /^(?:Just a moment|请稍候)/i.test(String(document.title ?? "")))) return "pending";
	    if (document.querySelector(
	      'meta[name="discourse-base-uri"],meta[name="generator"],#main-outlet,.d-header'
	    )) return "passed";
	    const href = String(popup.location?.href ?? ""), location = new URL(href);
	    return ["http:", "https:"].includes(location.protocol) && !/^\/challenge(?:\/|$)/i.test(location.pathname) && !/^\/cdn-cgi\/challenge-platform(?:\/|$)/i.test(location.pathname) && document.readyState !== "loading" && document.body ? "passed" : "pending";
	  } catch {
	    return "pending";
	  }
	}
	function browserCloudflareChallengeFeatures(screen) {
	  const availableWidth = Math.max(0, Number(screen?.availWidth) || 760), availableHeight = Math.max(0, Number(screen?.availHeight) || 720), width = Math.min(760, Math.max(420, availableWidth)), height = Math.min(720, Math.max(520, availableHeight)), left = Math.max(
	    0,
	    Math.round(
	      (Number(screen?.availLeft) || 0) + (availableWidth - width) / 2
	    )
	  ), top = Math.max(
	    0,
	    Math.round(
	      (Number(screen?.availTop) || 0) + (availableHeight - height) / 2
	    )
	  );
	  return `popup=yes,width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
	}
	class BrowserSharedRequestPermit {
	  coordinationMode;
	  scope;
	  #storage;
	  #sourceId;
	  #locks;
	  #shortWindowMs;
	  #longWindowMs;
	  #shortBudget;
	  #longBudget;
	  #minIntervalMs;
	  #maxConcurrent;
	  #backgroundIdleIntervalMs;
	  #backgroundMaxDeferMs;
	  #rateLimitEvidenceWindowMs;
	  #rateLimitMaxBackoffMs;
	  #rateLimitJitterRatio;
	  #random;
	  #intentTtlMs;
	  #permitTtlMs;
	  #policyTtlMs;
	  #now;
	  #createId;
	  #onError;
	  #challenge;
	  #challengeOrigin;
	  #challengeHref;
	  #challengeLeaseTtlMs;
	  #challengePassedTtlMs;
	  #challengePollIntervalMs;
	  #challengeVerifyIntervalMs;
	  #challengeMaxWaitMs;
	  #inspectChallenge;
	  #verifyChallenge;
	  #waiters = /* @__PURE__ */ new Set();
	  #channel;
	  #fallbackState = emptyState();
	  #localTransactionTail = Promise.resolve();
	  #sequence = 0;
	  #closed = !1;
	  #challengePromise = null;
	  #challengeController = null;
	  #challengeFocusRequested = !1;
	  #challengeWindow = null;
	  #challengeReconcilePromise = null;
	  #challengeProbePromise = null;
	  #challengeProbeController = null;
	  constructor(options) {
	    if (this.#storage = options.storage, this.#sourceId = normalizedSourceId(options.sourceId), this.#locks = options.locks ?? null, this.coordinationMode = this.#locks ? "atomic" : "best-effort", this.#shortWindowMs = positiveInteger(options.shortWindowMs, 1e4, "shortWindowMs"), this.#longWindowMs = positiveInteger(options.longWindowMs, 6e4, "longWindowMs"), this.#longWindowMs < this.#shortWindowMs)
	      throw new RangeError("longWindowMs 不能小于 shortWindowMs");
	    this.#shortBudget = positiveInteger(options.shortBudget, 40, "shortBudget"), this.#longBudget = positiveInteger(options.longBudget, 160, "longBudget"), this.#minIntervalMs = nonNegativeInteger(options.minIntervalMs, "minIntervalMs"), this.#maxConcurrent = positiveInteger(options.maxConcurrent, 3, "maxConcurrent"), this.#backgroundIdleIntervalMs = nonNegativeInteger(
	      options.backgroundIdleIntervalMs ?? READER_BACKGROUND_REQUEST_IDLE_INTERVAL_MS,
	      "backgroundIdleIntervalMs"
	    ), this.#backgroundMaxDeferMs = nonNegativeInteger(
	      options.backgroundMaxDeferMs ?? READER_BACKGROUND_REQUEST_MAX_DEFER_MS,
	      "backgroundMaxDeferMs"
	    ), this.#rateLimitEvidenceWindowMs = positiveInteger(
	      options.rateLimitEvidenceWindowMs,
	      READER_RATE_LIMIT_EVIDENCE_WINDOW_MS,
	      "rateLimitEvidenceWindowMs"
	    ), this.#rateLimitMaxBackoffMs = positiveInteger(
	      options.rateLimitMaxBackoffMs,
	      READER_RATE_LIMIT_MAX_BACKOFF_MS,
	      "rateLimitMaxBackoffMs"
	    ), this.#rateLimitJitterRatio = unitInterval(
	      options.rateLimitJitterRatio,
	      0.2,
	      "rateLimitJitterRatio"
	    ), this.#random = options.random ?? Math.random, this.#intentTtlMs = positiveInteger(options.intentTtlMs, 15e3, "intentTtlMs"), this.#permitTtlMs = positiveInteger(options.permitTtlMs, 35e3, "permitTtlMs"), this.#policyTtlMs = positiveInteger(options.policyTtlMs, 3e4, "policyTtlMs"), this.#now = options.now ?? Date.now, this.#createId = options.createId ?? (() => `${this.#sourceId}:${this.#now().toString(36)}:${++this.#sequence}`), this.#onError = options.onError ?? (() => {
	    }), this.#challenge = options.challenge ?? null, this.#challengeOrigin = this.#challenge ? challengeOrigin(this.#challenge.origin) : "", this.#challengeHref = this.#challenge ? browserCloudflareChallengeHref(
	      this.#challengeOrigin,
	      this.#challenge.redirectHref
	    ) : "", this.#challengeLeaseTtlMs = positiveInteger(
	      this.#challenge?.leaseTtlMs,
	      15e3,
	      "challenge.leaseTtlMs"
	    ), this.#challengePassedTtlMs = positiveInteger(
	      this.#challenge?.passedTtlMs,
	      1e4,
	      "challenge.passedTtlMs"
	    ), this.#challengePollIntervalMs = positiveInteger(
	      this.#challenge?.pollIntervalMs,
	      250,
	      "challenge.pollIntervalMs"
	    ), this.#challengeVerifyIntervalMs = positiveInteger(
	      this.#challenge?.verifyIntervalMs,
	      1e3,
	      "challenge.verifyIntervalMs"
	    ), this.#challengeMaxWaitMs = positiveInteger(
	      this.#challenge?.maxWaitMs,
	      12e4,
	      "challenge.maxWaitMs"
	    ), this.#inspectChallenge = this.#challenge?.inspect ?? inspectChallengeWindow, this.#verifyChallenge = this.#challenge?.verify ?? null, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const factory = options.broadcastChannelFactory === void 0 ? typeof BroadcastChannel > "u" ? null : (name) => new BroadcastChannel(name) : options.broadcastChannelFactory;
	    let channel = null;
	    try {
	      channel = factory?.(READER_REQUEST_PERMIT_CHANNEL) ?? null, channel?.addEventListener("message", this.#onChannelMessage);
	    } catch (error) {
	      this.#onError(error);
	    }
	    this.#channel = channel, options.storageEvents && this.scope.listen(
	      options.storageEvents,
	      "storage",
	      this.#onStorage
	    ), this.scope.add(() => {
	      this.#channel?.removeEventListener("message", this.#onChannelMessage), this.#channel?.close();
	    }), this.scope.add(() => {
	      this.#closed = !0, this.#challengeController?.abort(
	        new DOMException("request permit 已销毁", "AbortError")
	      ), this.#challengeController = null, this.#challengeProbeController?.abort(
	        new DOMException("request permit 已销毁", "AbortError")
	      ), this.#challengeProbeController = null;
	      try {
	        this.#challengeWindow?.close?.();
	      } catch {
	      }
	      this.#challengeWindow = null, this.#wake(), this.#removeOwnedState();
	    });
	  }
	  async acquire(input) {
	    if (this.#assertOpen(), input.signal.aborted) throw this.#abortReason(input.signal);
	    const intentId = `${this.#sourceId}:intent:${this.#createId()}`, queuedAt = this.#now();
	    let granted = !1, waitReason = "";
	    try {
	      for (; !this.#closed; ) {
	        if (input.signal.aborted) throw this.#abortReason(input.signal);
	        const decision = await this.#transact((state, now) => this.#tryGrant(
	          state,
	          now,
	          intentId,
	          queuedAt,
	          input.priority,
	          String(input.rateLimitRoute ?? "").trim()
	        ));
	        if (decision.granted && decision.permitId)
	          return granted = !0, this.#permit(
	            decision.permitId,
	            decision.recoveryProbe === !0,
	            waitReason
	          );
	        if (waitReason = decision.reason || waitReason, decision.defer)
	          throw new import_request_scheduler.RequestStartDeferredError(
	            decision.waitMs,
	            decision.reason
	          );
	        await this.#wait(decision.waitMs, input.signal);
	      }
	      throw new Error("BrowserSharedRequestPermit 已销毁");
	    } finally {
	      granted || this.#transact((state) => {
	        state.intents = state.intents.filter((intent) => intent.id !== intentId);
	      }).catch(this.#onError);
	    }
	  }
	  async noteRateLimit(decision) {
	    this.#assertOpen();
	    const route = String(decision.route).trim();
	    route && await this.#transact((state, now) => {
	      const scope = decision.scope === "global" ? "global" : "endpoint", leaseRoute = scope === "global" ? "*" : route, index = state.rateLimits.findIndex(
	        (lease) => lease.scope === scope && lease.route === leaseRoute
	      ), previous = index >= 0 ? state.rateLimits[index] : void 0, repeated = !!(previous && previous.lastObservedAt >= now - this.#rateLimitEvidenceWindowMs), hits = repeated ? previous.hits + 1 : 1, authoritative = scope === "global" || decision.authoritative === !0 || previous?.authoritative === !0 && repeated, exponent = Math.max(0, Math.min(6, hits - 1)), boundedWaitMs = Math.min(
	        this.#rateLimitMaxBackoffMs,
	        Math.max(1, decision.waitMs) * 2 ** exponent
	      ), random = Math.max(0, Math.min(1, Number(this.#random()) || 0)), jitterMs = Math.floor(
	        boundedWaitMs * this.#rateLimitJitterRatio * random
	      ), retryAt = Math.max(
	        decision.retryAt,
	        now + boundedWaitMs + jitterMs
	      ), next = Object.freeze({
	        scope,
	        route: leaseRoute,
	        hits,
	        authoritative,
	        lastObservedAt: now,
	        retryAt,
	        expiresAt: Math.max(
	          now + this.#rateLimitEvidenceWindowMs,
	          retryAt + this.#permitTtlMs
	        )
	      });
	      if (index >= 0 ? state.rateLimits[index] = next : state.rateLimits.push(next), scope === "endpoint") {
	        const corroborating = state.rateLimits.find((lease) => lease.scope === "endpoint" && lease.route !== leaseRoute && lease.lastObservedAt >= now - this.#rateLimitEvidenceWindowMs);
	        if (corroborating) {
	          const globalIndex = state.rateLimits.findIndex(
	            (lease) => lease.scope === "global"
	          ), globalPrevious = globalIndex >= 0 ? state.rateLimits[globalIndex] : void 0, globalRetryAt = Math.max(
	            retryAt,
	            corroborating.retryAt,
	            globalPrevious?.retryAt ?? 0
	          ), globalLease = Object.freeze({
	            scope: "global",
	            route: "*",
	            hits: (globalPrevious?.hits ?? 0) + 1,
	            authoritative: authoritative || corroborating.authoritative,
	            lastObservedAt: now,
	            retryAt: globalRetryAt,
	            expiresAt: Math.max(
	              now + this.#rateLimitEvidenceWindowMs,
	              globalRetryAt + this.#permitTtlMs
	            )
	          });
	          globalIndex >= 0 ? state.rateLimits[globalIndex] = globalLease : state.rateLimits.push(globalLease);
	        }
	      }
	      state.rateLimits.length > 128 && state.rateLimits.splice(0, state.rateLimits.length - 128);
	    });
	  }
	  async noteRateLimitProbeResult(input) {
	    this.#assertOpen();
	    const route = String(input.route).trim();
	    route && await this.#transact((state, now) => {
	      if (input.recovered) {
	        state.rateLimits = state.rateLimits.filter(
	          (lease) => lease.scope !== "global" && lease.route !== route
	        );
	        return;
	      }
	      state.rateLimits = state.rateLimits.map((lease) => {
	        if (lease.probeOwnerId !== this.#sourceId || lease.scope !== "global" && lease.route !== route) return lease;
	        const retryAt = Math.max(
	          lease.retryAt,
	          now + READER_RATE_LIMIT_PROBE_FAILURE_WAIT_MS
	        );
	        return Object.freeze({
	          scope: lease.scope,
	          route: lease.route,
	          hits: lease.hits,
	          authoritative: lease.authoritative,
	          lastObservedAt: lease.lastObservedAt,
	          retryAt,
	          expiresAt: Math.max(lease.expiresAt, retryAt + this.#permitTtlMs)
	        });
	      });
	    });
	  }
	  noteObservedResponse(input) {
	    if (!(input.source !== "host" || !input.href)) {
	      if (input.status === 429) {
	        const now = this.#now(), window = (0, import_request_rate_limit_policy.rateLimitWindowFromCode)(input.rateLimitCode), hasAuthoritativeRetryAfter = authoritativeRetryAfter(
	          input.retryAfter,
	          now
	        ), waitMs = Math.max(
	          (0, import_request_rate_limit_policy.parseRetryAfterMs)(input.retryAfter, {
	            now,
	            fallbackMs: 1500,
	            minMs: 1e3,
	            maxMs: this.#rateLimitMaxBackoffMs
	          }),
	          observedRateLimitWindowWaitMs(window)
	        ), identity = (0, import_request_rate_limit_policy.endpointRequestIdentity)(
	          input.href,
	          input.method,
	          this.#challengeOrigin || void 0
	        );
	        this.noteRateLimit(Object.freeze({
	          scope: window === "unknown" ? "endpoint" : "global",
	          waitMs,
	          retryAt: now + waitMs,
	          ...identity,
	          window,
	          authoritative: hasAuthoritativeRetryAfter || window !== "unknown"
	        })).catch(this.#onError);
	      }
	      input.cloudflareMitigated === !0 && input.blockOnCloudflareChallenge !== !1 && this.noteCloudflareChallenge({ href: input.href }).catch(this.#onError);
	    }
	  }
	  async noteCloudflareChallenge(input) {
	    this.#assertOpen(), !(!this.#challenge || !challengeHrefMatchesOrigin(input.href, this.#challengeOrigin)) && await this.#transact((state, now) => {
	      if (state.challenge?.state === "passed" && input.force !== !0 || state.challenge?.state === "active" && !state.challenge.required)
	        return;
	      const newGeneration = state.challenge?.state === "passed" && input.force === !0;
	      state.challenge = Object.freeze({
	        ownerId: "",
	        state: "active",
	        required: !0,
	        automaticAttempted: !newGeneration && state.challenge?.automaticAttempted === !0,
	        recoveryProbeAttempted: !newGeneration && state.challenge?.recoveryProbeAttempted === !0,
	        ...newGeneration ? {} : storedChallengeProbeState(state.challenge),
	        updatedAt: now,
	        expiresAt: now + this.#challengeMaxWaitMs
	      });
	    });
	  }
	  /**
	   * 页面重载可能销毁原验证 owner,却留下已完成验证的命名窗口与 required 状态。
	   * 每个 challenge 世代只允许一个新 context 做一次原生 session 探针;成功即解闸,
	   * 失败仍保留人工按钮。它不打开窗口、不重放业务请求,并与人工入口共享探针退避。
	   */
	  reconcileCloudflareChallenge() {
	    if (this.#assertOpen(), !this.#verifyChallenge) return Promise.resolve(!1);
	    if (this.#challengeReconcilePromise) return this.#challengeReconcilePromise;
	    const controller = this.scope.abortController(
	      new DOMException("request permit 已销毁", "AbortError")
	    ), promise = this.#reconcileRequiredChallenge(controller.signal).finally(() => {
	      this.#challengeReconcilePromise === promise && (this.#challengeReconcilePromise = null);
	    });
	    return this.#challengeReconcilePromise = promise, promise;
	  }
	  recordHostStart(input) {
	    this.#assertOpen();
	    const activeId = `${this.#sourceId}:host:${this.#createId()}`, registration = this.#transact((state, now) => {
	      this.#rememberPolicy(state, now);
	      const startedAt = Math.max(
	        now - 5e3,
	        Math.min(now, Number(input.startedAt) || now)
	      );
	      state.events.push(startedAt), state.events.sort((left, right) => left - right), state.active.push(Object.freeze({
	        id: activeId,
	        ownerId: this.#sourceId,
	        expiresAt: now + this.#permitTtlMs
	      }));
	    });
	    let released = !1;
	    return Object.freeze({
	      release: (input2) => {
	        released || (released = !0, input2 && this.noteObservedResponse(input2), registration.then(() => this.#transact((state) => {
	          state.active = state.active.filter(
	            (permit) => permit.id !== activeId
	          );
	        })).catch(this.#onError));
	      }
	    });
	  }
	  async resolveCloudflareChallenge(input) {
	    if (this.#assertOpen(), !this.#challenge || !challengeHrefMatchesOrigin(input.href, this.#challengeOrigin))
	      return !1;
	    if (input.signal.aborted)
	      throw this.#abortReason(input.signal);
	    if (input.focus !== !0 && !this.#locks && (await this.noteCloudflareChallenge({ href: input.href }), await this.#transact((state) => {
	      state.challenge?.state === "active" && state.challenge.required && (state.challenge = Object.freeze({
	        ...state.challenge,
	        automaticAttempted: !0
	      }));
	    })), input.focus === !0 && (this.#challengeFocusRequested = !0, this.#focusChallengeWindow(), this.#postChannelMessage("challenge-focus"), this.#wake()), !this.#challengePromise) {
	      const controller = new AbortController();
	      this.#challengeController = controller;
	      const promise = this.#runChallenge(controller.signal).finally(() => {
	        this.#challengePromise === promise && (this.#challengePromise = null, this.#challengeController = null, this.#challengeFocusRequested = !1);
	      });
	      this.#challengePromise = promise;
	    }
	    const shared = this.#challengePromise;
	    let abort = () => {
	    };
	    const cancelled = new Promise((_resolve, reject) => {
	      abort = () => reject(this.#abortReason(input.signal)), input.signal.addEventListener("abort", abort, { once: !0 });
	    });
	    return Promise.race([shared, cancelled]).finally(() => {
	      input.signal.removeEventListener("abort", abort);
	    });
	  }
	  /** 清除 429 反馈 lease;固定预防窗口必须原样保留,避免恢复后形成追赶突发。 */
	  async resetRateLimits() {
	    this.#assertOpen(), await this.#transact((state) => {
	      state.rateLimits = [];
	    });
	  }
	  async snapshot() {
	    const now = this.#now(), state = this.#read(now), policy = this.#effectivePolicy(state), blocking = this.#blockingState(state, now, policy), rateLimitBlocking = this.#rateLimitGate(state, now, "", !1), instances = /* @__PURE__ */ new Set([
	      this.#sourceId,
	      ...state.policies.map((entry) => entry.ownerId),
	      ...state.intents.map((entry) => entry.ownerId),
	      ...state.active.map((entry) => entry.ownerId)
	    ]);
	    return Object.freeze({
	      coordinationMode: this.coordinationMode,
	      shortBudget: policy.shortBudget,
	      longBudget: policy.longBudget,
	      minIntervalMs: policy.minIntervalMs,
	      maxConcurrent: policy.maxConcurrent,
	      instances: Math.max(1, instances.size),
	      queued: state.intents.length,
	      active: state.active.length,
	      shortCount: state.events.filter((at) => at > now - this.#shortWindowMs).length,
	      longCount: state.events.length,
	      challengeState: state.challenge ? state.challenge.state === "active" && state.challenge.required ? "required" : state.challenge.state : "idle",
	      challengeOwned: state.challenge?.state === "active" && !state.challenge.required && state.challenge.ownerId === this.#sourceId,
	      nextPermitDelay: Math.max(blocking.waitMs, rateLimitBlocking.waitMs),
	      blockingReason: (rateLimitBlocking.waitMs > blocking.waitMs ? "rate-limit" : blocking.reason) || (state.intents.length ? "priority" : "")
	    });
	  }
	  /**
	   * 更新跨标签许可策略,但保留 intent、active lease 与固定窗口事件。
	   *
	   * 收紧后的策略只阻止后续 permit;不会中止已在执行的原站请求。
	   */
	  applyRuntimePolicy(policy) {
	    this.#closed || (this.#shortBudget = positiveInteger(
	      policy.shortBudget,
	      this.#shortBudget,
	      "shortBudget"
	    ), this.#longBudget = positiveInteger(
	      policy.longBudget,
	      this.#longBudget,
	      "longBudget"
	    ), this.#minIntervalMs = nonNegativeInteger(
	      policy.minIntervalMs,
	      "minIntervalMs"
	    ), this.#maxConcurrent = positiveInteger(
	      policy.maxConcurrent,
	      this.#maxConcurrent,
	      "maxConcurrent"
	    ), this.#transact((state, now) => {
	      this.#rememberPolicy(state, now);
	    }).catch(this.#onError));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #runChallenge(signal) {
	    const startedAt = this.#now();
	    for (; !this.#closed && this.#now() - startedAt < this.#challengeMaxWaitMs; ) {
	      if (signal.aborted) throw this.#abortReason(signal);
	      const ownership = await this.#transact((state, now) => state.challenge?.state === "passed" ? "passed" : state.challenge?.state === "active" && state.challenge.required && state.challenge.automaticAttempted && !this.#challengeFocusRequested || state.challenge?.state === "active" && !state.challenge.required && state.challenge.ownerId !== this.#sourceId ? "waiting" : (state.challenge = Object.freeze({
	        ownerId: this.#sourceId,
	        state: "active",
	        required: !1,
	        automaticAttempted: !0,
	        recoveryProbeAttempted: state.challenge?.recoveryProbeAttempted === !0,
	        ...storedChallengeProbeState(state.challenge),
	        updatedAt: now,
	        expiresAt: now + this.#challengeLeaseTtlMs
	      }), "owner"));
	      if (ownership === "passed") return !0;
	      if (ownership === "waiting") {
	        await this.#wait(this.#challengePollIntervalMs, signal);
	        continue;
	      }
	      try {
	        return await this.#challengePassesProbe(signal) ? (await this.#completeChallengeLease(), !0) : await this.#ownChallengeWindow(signal, startedAt);
	      } catch (error) {
	        try {
	          this.#challengeWindow?.close?.();
	        } catch {
	        }
	        throw this.#challengeWindow = null, await this.#releaseChallengeLease(), error;
	      }
	    }
	    return !1;
	  }
	  async #reconcileRequiredChallenge(signal) {
	    if (!await this.#transact((state, now) => state.challenge?.state !== "active" || !state.challenge.required || !state.challenge.automaticAttempted || state.challenge.recoveryProbeAttempted === !0 ? !1 : (state.challenge = Object.freeze({
	      ...state.challenge,
	      ownerId: this.#sourceId,
	      required: !1,
	      recoveryProbeAttempted: !0,
	      updatedAt: now,
	      expiresAt: now + this.#challengeLeaseTtlMs
	    }), !0))) return !1;
	    if (signal.aborted) throw this.#abortReason(signal);
	    let verified = !1;
	    try {
	      verified = await this.#challengePassesProbe(signal);
	    } catch (error) {
	      if (signal.aborted) throw error;
	      this.#onError(error);
	    }
	    return verified ? (await this.#completeChallengeLease(), !0) : (await this.#releaseChallengeLease(), !1);
	  }
	  async #ownChallengeWindow(signal, startedAt) {
	    const challenge = this.#challenge;
	    if (!challenge) return !1;
	    if (!this.#challengeWindow || this.#challengeWindow.closed === !0)
	      try {
	        this.#challengeWindow = challenge.open(
	          this.#challengeHref,
	          READER_CLOUDFLARE_CHALLENGE_WINDOW_NAME,
	          browserCloudflareChallengeFeatures(challenge.screen)
	        );
	      } catch (error) {
	        this.#onError(error), this.#challengeWindow = null;
	      }
	    const popup = this.#challengeWindow;
	    if (!popup)
	      return await this.#releaseChallengeLease(), !1;
	    let verifyDelayMs = this.#challengeVerifyIntervalMs;
	    const maxVerifyDelayMs = Math.max(
	      verifyDelayMs,
	      READER_CLOUDFLARE_CHALLENGE_MAX_PROBE_INTERVAL_MS
	    );
	    let nextVerifyAt = this.#now() + verifyDelayMs;
	    const onPopupLoad = () => {
	      nextVerifyAt = this.#now(), this.#expediteChallengeProbeAfterLoad();
	    };
	    popup.addEventListener?.("load", onPopupLoad), this.#focusChallengeWindow();
	    try {
	      for (; !this.#closed && this.#now() - startedAt < this.#challengeMaxWaitMs; ) {
	        if (signal.aborted) throw this.#abortReason(signal);
	        if (popup.closed === !0)
	          return this.#challengeWindow = null, await this.#challengePassesProbe(signal) ? (await this.#completeChallengeLease(), !0) : (await this.#releaseChallengeLease(), !1);
	        const inspectedPassed = this.#inspectChallenge(popup) === "passed", probeDue = !!this.#verifyChallenge && (inspectedPassed || this.#now() >= nextVerifyAt);
	        if (inspectedPassed && !this.#verifyChallenge) {
	          await this.#completeChallengeLease();
	          try {
	            popup.close?.();
	          } catch {
	          }
	          return this.#challengeWindow = null, !0;
	        }
	        if (probeDue) {
	          const verified = await this.#challengePassesProbe(signal);
	          if (verifyDelayMs = Math.min(maxVerifyDelayMs, verifyDelayMs * 2), nextVerifyAt = this.#now() + verifyDelayMs, verified) {
	            await this.#completeChallengeLease();
	            try {
	              popup.close?.();
	            } catch {
	            }
	            return this.#challengeWindow = null, !0;
	          }
	        }
	        if (!await this.#transact((state, now) => state.challenge?.state !== "active" || state.challenge.ownerId !== this.#sourceId ? !1 : (state.challenge = Object.freeze({
	          ...state.challenge,
	          updatedAt: now,
	          expiresAt: now + this.#challengeLeaseTtlMs
	        }), !0))) {
	          try {
	            popup.close?.();
	          } catch {
	          }
	          return this.#challengeWindow = null, !1;
	        }
	        await this.#wait(this.#challengePollIntervalMs, signal);
	      }
	      if (await this.#challengePassesProbe(signal)) {
	        await this.#completeChallengeLease();
	        try {
	          popup.close?.();
	        } catch {
	        }
	        return this.#challengeWindow = null, !0;
	      }
	      try {
	        popup.close?.();
	      } catch {
	      }
	      return this.#challengeWindow = null, await this.#releaseChallengeLease(), !1;
	    } finally {
	      popup.removeEventListener?.("load", onPopupLoad);
	    }
	  }
	  #challengePassesProbe(signal) {
	    if (!this.#verifyChallenge) return Promise.resolve(!1);
	    if (signal.aborted)
	      return Promise.reject(this.#abortReason(signal));
	    if (!this.#challengeProbePromise) {
	      const controller = new AbortController();
	      this.#challengeProbeController = controller;
	      const promise = this.#runChallengeProbe(controller.signal).catch((error) => {
	        if (controller.signal.aborted)
	          throw controller.signal.reason ?? error;
	        return this.#onError(error), !1;
	      }).finally(() => {
	        this.#challengeProbePromise === promise && (this.#challengeProbePromise = null, this.#challengeProbeController = null);
	      });
	      this.#challengeProbePromise = promise;
	    }
	    const shared = this.#challengeProbePromise;
	    if (!shared) return Promise.resolve(!1);
	    let abort = () => {
	    };
	    const cancelled = new Promise((_resolve, reject) => {
	      abort = () => reject(this.#abortReason(signal)), signal.addEventListener("abort", abort, { once: !0 });
	    });
	    return Promise.race([shared, cancelled]).finally(() => {
	      signal.removeEventListener("abort", abort);
	    });
	  }
	  async #expediteChallengeProbeAfterLoad() {
	    try {
	      await this.#challengeProbePromise;
	    } catch {
	    }
	    try {
	      await this.#transact((state, now) => {
	        state.challenge?.state !== "active" || state.challenge.ownerId !== this.#sourceId || (state.challenge = Object.freeze({
	          ...state.challenge,
	          probeNotBefore: now,
	          probeBackoffMs: this.#challengeVerifyIntervalMs,
	          updatedAt: now
	        }));
	      });
	    } catch (error) {
	      this.#onError(error);
	    } finally {
	      this.#wake();
	    }
	  }
	  async #runChallengeProbe(signal) {
	    const verify = this.#verifyChallenge;
	    if (!verify) return !1;
	    if (signal.aborted) throw this.#abortReason(signal);
	    const probeToken = `${this.#sourceId}:challenge-probe:${this.#createId()}`, maxBackoffMs = Math.max(
	      this.#challengeVerifyIntervalMs,
	      READER_CLOUDFLARE_CHALLENGE_MAX_PROBE_INTERVAL_MS
	    ), reservation = await this.#transact((state, now) => {
	      const challenge = state.challenge;
	      if (challenge?.state === "passed")
	        return Object.freeze({ status: "passed", backoffMs: 0 });
	      if (challenge?.state !== "active" || challenge.ownerId !== this.#sourceId || Number(challenge.probeNotBefore ?? 0) > now)
	        return Object.freeze({ status: "deferred", backoffMs: 0 });
	      const storedBackoffMs = Number(challenge.probeBackoffMs), backoffMs = Number.isSafeInteger(storedBackoffMs) && storedBackoffMs > 0 ? Math.max(
	        this.#challengeVerifyIntervalMs,
	        Math.min(maxBackoffMs, storedBackoffMs)
	      ) : this.#challengeVerifyIntervalMs;
	      return state.challenge = Object.freeze({
	        ...challenge,
	        probeToken,
	        probeNotBefore: now + backoffMs,
	        probeBackoffMs: backoffMs,
	        updatedAt: now,
	        expiresAt: Math.max(
	          challenge.expiresAt,
	          now + this.#challengeLeaseTtlMs
	        )
	      }), Object.freeze({ status: "reserved", backoffMs });
	    });
	    if (reservation.status === "passed") return !0;
	    if (reservation.status !== "reserved") return !1;
	    if (signal.aborted) throw this.#abortReason(signal);
	    let verified = !1;
	    try {
	      verified = await verify(signal);
	    } catch (error) {
	      if (signal.aborted) throw error;
	      this.#onError(error);
	    }
	    const settlement = await this.#transact((state, now) => {
	      const challenge = state.challenge;
	      if (challenge?.state === "passed") return "passed";
	      if (!challenge || challenge.ownerId !== this.#sourceId || challenge.probeToken !== probeToken) return "stale";
	      const backoffMs = verified ? this.#challengeVerifyIntervalMs : Math.min(
	        maxBackoffMs,
	        Math.max(
	          this.#challengeVerifyIntervalMs,
	          reservation.backoffMs * 2
	        )
	      );
	      return state.challenge = Object.freeze({
	        ...challenge,
	        probeNotBefore: now + backoffMs,
	        probeBackoffMs: backoffMs,
	        updatedAt: now,
	        expiresAt: Math.max(
	          challenge.expiresAt,
	          now + this.#challengeLeaseTtlMs
	        )
	      }), "updated";
	    });
	    return settlement === "passed" || verified && settlement === "updated";
	  }
	  #focusChallengeWindow() {
	    if (!(!this.#challengeFocusRequested || !this.#challengeWindow)) {
	      this.#challengeFocusRequested = !1;
	      try {
	        this.#challengeWindow.focus?.();
	      } catch {
	      }
	    }
	  }
	  async #completeChallengeLease() {
	    await this.#transact((state, now) => {
	      state.challenge?.state !== "active" || state.challenge.ownerId !== this.#sourceId || (state.challenge = Object.freeze({
	        ownerId: this.#sourceId,
	        state: "passed",
	        required: !1,
	        automaticAttempted: !0,
	        recoveryProbeAttempted: state.challenge.recoveryProbeAttempted === !0,
	        updatedAt: now,
	        expiresAt: now + this.#challengePassedTtlMs
	      }));
	    });
	  }
	  async #releaseChallengeLease() {
	    await this.#transact((state, now) => {
	      state.challenge?.state === "active" && state.challenge.ownerId === this.#sourceId && (state.challenge = Object.freeze({
	        ownerId: "",
	        state: "active",
	        required: !0,
	        automaticAttempted: !0,
	        recoveryProbeAttempted: state.challenge.recoveryProbeAttempted === !0,
	        ...storedChallengeProbeState(state.challenge),
	        updatedAt: now,
	        expiresAt: now + this.#challengeMaxWaitMs
	      }));
	    });
	  }
	  #tryGrant(state, now, intentId, queuedAt, priority, rateLimitRoute) {
	    this.#rememberPolicy(state, now);
	    const policy = this.#effectivePolicy(state);
	    state.intents.find((intent) => intent.id === intentId) ? state.intents = state.intents.map((intent) => intent.id === intentId ? Object.freeze({
	      ...intent,
	      rateLimitRoute,
	      expiresAt: now + this.#intentTtlMs
	    }) : intent) : state.intents.push(Object.freeze({
	      id: intentId,
	      ownerId: this.#sourceId,
	      priority,
	      rateLimitRoute,
	      queuedAt,
	      expiresAt: now + this.#intentTtlMs
	    })), state.intents.sort((left, right) => PRIORITY_WEIGHT[left.priority] - PRIORITY_WEIGHT[right.priority] || left.queuedAt - right.queuedAt || left.id.localeCompare(right.id));
	    const firstEligibleIntent = state.intents.find((intent) => this.#rateLimitGate(
	      state,
	      now,
	      intent.rateLimitRoute,
	      !1
	    ).waitMs === 0);
	    if (firstEligibleIntent && firstEligibleIntent.id !== intentId)
	      return Object.freeze({
	        granted: !1,
	        waitMs: 80,
	        reason: "priority"
	      });
	    if (!firstEligibleIntent) {
	      const rateLimitBlocking2 = this.#rateLimitGate(
	        state,
	        now,
	        rateLimitRoute,
	        !1
	      );
	      return Object.freeze({
	        granted: !1,
	        waitMs: Math.max(
	          25,
	          rateLimitBlocking2.probeWaiting && !rateLimitBlocking2.global ? Math.min(
	            rateLimitBlocking2.waitMs,
	            READER_RATE_LIMIT_PROBE_RECHECK_MS
	          ) : rateLimitBlocking2.waitMs
	        ),
	        reason: "rate-limit",
	        defer: !rateLimitBlocking2.global
	      });
	    }
	    const blocking = this.#blockingState(
	      state,
	      now,
	      policy,
	      priority,
	      queuedAt
	    ), rateLimitBlocking = this.#rateLimitGate(
	      state,
	      now,
	      rateLimitRoute,
	      !1
	    ), waitMs = Math.max(blocking.waitMs, rateLimitBlocking.waitMs);
	    if (waitMs > 0) {
	      const rateLimitDominates = rateLimitBlocking.waitMs > blocking.waitMs;
	      return Object.freeze({
	        granted: !1,
	        waitMs: rateLimitDominates && rateLimitBlocking.probeWaiting && !rateLimitBlocking.global ? Math.min(waitMs, READER_RATE_LIMIT_PROBE_RECHECK_MS) : waitMs,
	        reason: rateLimitDominates ? "rate-limit" : blocking.reason,
	        defer: rateLimitDominates && !rateLimitBlocking.global
	      });
	    }
	    const rateLimitRecoveryProbe = this.#rateLimitGate(
	      state,
	      now,
	      rateLimitRoute,
	      !0
	    ).recoveryProbe;
	    state.intents = state.intents.filter((intent) => intent.id !== intentId), state.events.push(now);
	    const permitId = `${this.#sourceId}:permit:${this.#createId()}`;
	    return state.active.push(Object.freeze({
	      id: permitId,
	      ownerId: this.#sourceId,
	      expiresAt: now + this.#permitTtlMs
	    })), Object.freeze({
	      granted: !0,
	      permitId,
	      waitMs: 0,
	      reason: "",
	      recoveryProbe: rateLimitRecoveryProbe
	    });
	  }
	  #rateLimitGate(state, now, route, claim) {
	    if (!state.rateLimits.length) return CLEAR_RATE_LIMIT_GATE;
	    const matching = state.rateLimits.filter((lease) => activeRateLimitLease(lease) && (lease.scope === "global" || !!route && lease.route === route));
	    if (!matching.length)
	      return CLEAR_RATE_LIMIT_GATE;
	    const global = matching.some((lease) => lease.scope === "global");
	    let waitMs = 0, probeWaiting = !1;
	    for (const lease of matching) {
	      if (lease.retryAt > now) {
	        waitMs = Math.max(waitMs, lease.retryAt - now);
	        continue;
	      }
	      lease.probeOwnerId && Number(lease.probeExpiresAt) > now && (probeWaiting = !0, waitMs = Math.max(waitMs, Number(lease.probeExpiresAt) - now));
	    }
	    return waitMs > 0 || !claim ? Object.freeze({
	      waitMs,
	      recoveryProbe: waitMs === 0,
	      global,
	      probeWaiting
	    }) : (state.rateLimits = state.rateLimits.map((lease) => matching.includes(lease) ? Object.freeze({
	      ...lease,
	      probeOwnerId: this.#sourceId,
	      probeExpiresAt: now + this.#permitTtlMs,
	      expiresAt: Math.max(lease.expiresAt, now + this.#permitTtlMs)
	    }) : lease), Object.freeze({
	      waitMs: 0,
	      recoveryProbe: !0,
	      global,
	      probeWaiting: !1
	    }));
	  }
	  #rememberPolicy(state, now) {
	    const policy = Object.freeze({
	      ownerId: this.#sourceId,
	      shortBudget: this.#shortBudget,
	      longBudget: this.#longBudget,
	      minIntervalMs: this.#minIntervalMs,
	      maxConcurrent: this.#maxConcurrent,
	      expiresAt: now + this.#policyTtlMs
	    }), index = state.policies.findIndex(
	      (candidate) => candidate.ownerId === this.#sourceId
	    );
	    index >= 0 ? state.policies[index] = policy : state.policies.push(policy);
	  }
	  #effectivePolicy(state) {
	    return Object.freeze(this.#configuredPolicy(state));
	  }
	  #configuredPolicy(state) {
	    const effective = {
	      shortBudget: this.#shortBudget,
	      longBudget: this.#longBudget,
	      minIntervalMs: this.#minIntervalMs,
	      maxConcurrent: this.#maxConcurrent
	    };
	    for (const policy of state.policies)
	      policy.ownerId !== this.#sourceId && (effective.shortBudget = Math.min(
	        effective.shortBudget,
	        policy.shortBudget
	      ), effective.longBudget = Math.min(
	        effective.longBudget,
	        policy.longBudget
	      ), effective.minIntervalMs = Math.max(
	        effective.minIntervalMs,
	        policy.minIntervalMs
	      ), effective.maxConcurrent = Math.min(
	        effective.maxConcurrent,
	        policy.maxConcurrent
	      ));
	    return effective;
	  }
	  #blockingState(state, now, policy, priority = "visible", queuedAt = now) {
	    if (state.challenge?.state === "active")
	      return Object.freeze({
	        waitMs: Math.max(25, state.challenge.expiresAt - now),
	        reason: "challenge",
	        recoveryProbe: !1
	      });
	    const activeDelay = state.active.length >= policy.maxConcurrent ? Math.max(
	      25,
	      Math.min(...state.active.map((permit) => permit.expiresAt - now))
	    ) : 0, backgroundActiveDelay = priority === "background" && state.active.length ? Math.max(
	      25,
	      Math.min(...state.active.map((permit) => permit.expiresAt - now))
	    ) : 0, shortEvents = state.events.filter(
	      (at) => at > now - this.#shortWindowMs
	    ), shortWindowDelay = this.#windowDelay(
	      shortEvents,
	      policy.shortBudget,
	      this.#shortWindowMs,
	      now
	    ), longWindowDelay = this.#windowDelay(
	      state.events,
	      policy.longBudget,
	      this.#longWindowMs,
	      now
	    ), latest = state.events.at(-1) ?? 0, backgroundDeferRemainingMs = priority === "background" ? Math.max(0, queuedAt + this.#backgroundMaxDeferMs - now) : 0, enforceBackgroundIdle = priority === "background" && backgroundDeferRemainingMs > 0, requestIntervalMs = enforceBackgroundIdle ? Math.max(policy.minIntervalMs, this.#backgroundIdleIntervalMs) : policy.minIntervalMs;
	    let intervalDelay = Math.max(
	      0,
	      latest + requestIntervalMs - now
	    );
	    enforceBackgroundIdle && (intervalDelay = Math.min(
	      intervalDelay,
	      backgroundDeferRemainingMs
	    ));
	    const candidates = [
	      ["concurrency", Math.max(activeDelay, backgroundActiveDelay)],
	      ["interval", intervalDelay],
	      ["10s", shortWindowDelay],
	      ["60s", longWindowDelay]
	    ];
	    let reason = "", waitMs = 0;
	    for (const [candidateReason, delay] of candidates)
	      delay > waitMs && (reason = candidateReason, waitMs = delay);
	    return Object.freeze({ waitMs, reason, recoveryProbe: !1 });
	  }
	  #windowDelay(events, budget, windowMs, now) {
	    if (events.length < budget) return 0;
	    const boundary = events[events.length - budget];
	    return boundary === void 0 ? 0 : Math.max(0, boundary + windowMs - now + 1);
	  }
	  #permit(permitId, recoveryProbe, waitReason) {
	    let released = !1;
	    return Object.freeze({
	      recoveryProbe,
	      waitReason,
	      release: () => {
	        released || (released = !0, this.#transact((state) => {
	          state.active = state.active.filter((permit) => permit.id !== permitId);
	        }).catch(this.#onError));
	      }
	    });
	  }
	  async #transact(operation) {
	    const execute = async () => {
	      const now = this.#now(), state = this.#read(now), result = await operation(state, now);
	      return state.updatedAt = now, this.#write(state), this.#publish(), this.#wake(), result;
	    };
	    if (this.#locks)
	      return this.#locks.request(
	        READER_REQUEST_PERMIT_LOCK,
	        { mode: "exclusive" },
	        execute
	      );
	    const transaction = this.#localTransactionTail.catch(() => {
	    }).then(execute);
	    return this.#localTransactionTail = transaction.then(
	      () => {
	      },
	      () => {
	      }
	    ), transaction;
	  }
	  #read(now) {
	    try {
	      const stored = this.#storage.getItem(READER_REQUEST_PERMIT_STORAGE_KEY);
	      if (stored)
	        return normalizeState(JSON.parse(stored), now, this.#longWindowMs);
	    } catch (error) {
	      if (this.#onError(error), this.#locks) throw error;
	    }
	    return normalizeState(
	      this.#locks ? null : this.#fallbackState,
	      now,
	      this.#longWindowMs
	    );
	  }
	  #write(state) {
	    this.#locks || (this.#fallbackState = normalizeState(state, this.#now(), this.#longWindowMs));
	    try {
	      this.#storage.setItem(
	        READER_REQUEST_PERMIT_STORAGE_KEY,
	        JSON.stringify(state)
	      );
	    } catch (error) {
	      if (this.#onError(error), this.#locks) throw error;
	    }
	  }
	  #publish() {
	    this.#postChannelMessage("updated");
	  }
	  #postChannelMessage(type) {
	    try {
	      this.#channel?.postMessage(Object.freeze({
	        schemaVersion: 1,
	        sourceId: this.#sourceId,
	        type
	      }));
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  #wait(milliseconds, signal) {
	    return new Promise((resolve, reject) => {
	      let settled = !1;
	      const earliestWakeAt = this.#now() + 25, finish = (error) => {
	        settled || (settled = !0, clearTimeout(timer), this.#waiters.delete(wake), signal.removeEventListener("abort", abort), error !== void 0 ? reject(error) : resolve());
	      }, wake = () => {
	        const remaining = earliestWakeAt - this.#now();
	        if (remaining > 0) {
	          clearTimeout(timer), timer = setTimeout(wake, remaining);
	          return;
	        }
	        finish();
	      }, abort = () => finish(this.#abortReason(signal));
	      let timer = setTimeout(
	        wake,
	        Math.max(25, Math.min(1e3, milliseconds || 80))
	      );
	      this.#waiters.add(wake), signal.addEventListener("abort", abort, { once: !0 });
	    });
	  }
	  async #removeOwnedState() {
	    try {
	      await this.#transact((state, now) => {
	        state.intents = state.intents.filter(
	          (intent) => intent.ownerId !== this.#sourceId
	        ), state.active = state.active.filter(
	          (permit) => permit.ownerId !== this.#sourceId
	        ), state.policies = state.policies.filter(
	          (policy) => policy.ownerId !== this.#sourceId
	        ), state.rateLimits = state.rateLimits.map((lease) => lease.probeOwnerId !== this.#sourceId ? lease : Object.freeze({
	          scope: lease.scope,
	          route: lease.route,
	          hits: lease.hits,
	          authoritative: lease.authoritative,
	          lastObservedAt: lease.lastObservedAt,
	          retryAt: Math.max(
	            lease.retryAt,
	            now + READER_RATE_LIMIT_PROBE_FAILURE_WAIT_MS
	          ),
	          expiresAt: Math.max(
	            lease.expiresAt,
	            now + this.#permitTtlMs
	          )
	        })), state.challenge?.state === "active" && state.challenge.ownerId === this.#sourceId && (state.challenge = Object.freeze({
	          ownerId: "",
	          state: "active",
	          required: !0,
	          automaticAttempted: !0,
	          recoveryProbeAttempted: state.challenge.recoveryProbeAttempted === !0,
	          ...storedChallengeProbeState(state.challenge),
	          updatedAt: now,
	          expiresAt: now + this.#challengeMaxWaitMs
	        }));
	      });
	    } catch (error) {
	      this.#onError(error);
	    }
	  }
	  #wake() {
	    for (const waiter of this.#waiters) waiter();
	    this.#waiters.clear();
	  }
	  #assertOpen() {
	    if (this.#closed || this.scope.destroyed)
	      throw new Error("BrowserSharedRequestPermit 已销毁");
	  }
	  #abortReason(signal) {
	    return signal.reason ?? new DOMException("Aborted", "AbortError");
	  }
	  #onChannelMessage = (event) => {
	    const message = event.data;
	    message?.schemaVersion === 1 && message.sourceId !== this.#sourceId && message.type === "challenge-focus" && this.#challengePromise && (this.#challengeFocusRequested = !0, this.#focusChallengeWindow()), this.#wake();
	  };
	  #onStorage = (event) => {
	    const key = event.key;
	    (key === null || key === READER_REQUEST_PERMIT_STORAGE_KEY) && this.#wake();
	  };
	}
}, "adba9b8251b6c7fa63800935d91f471ed0b617fe5dffcb47286acfe406e0a859");

/* Source: lite/src/network/coordinated-request-client.ts */
runtime.register("src/network/coordinated-request-client.js", function(module, exports, require) {
	var coordinated_request_client_exports = {};
	__export(coordinated_request_client_exports, {
	  CoordinatedRequestClient: () => CoordinatedRequestClient,
	  RequestChallengeWaitSuppressedError: () => RequestChallengeWaitSuppressedError,
	  RequestCloudflareChallengeError: () => RequestCloudflareChallengeError,
	  RequestRateLimitError: () => RequestRateLimitError,
	  RequestStatusError: () => RequestStatusError,
	  abortableDelay: () => abortableDelay,
	  requestFailureKind: () => requestFailureKind
	});
	module.exports = __toCommonJS(coordinated_request_client_exports);
	var import_request_scheduler = require("./request-scheduler.js");
	const PRIORITY_WEIGHT = Object.freeze({
	  critical: 0,
	  interactive: 1,
	  nested: 2,
	  visible: 3,
	  prefetch: 4,
	  background: 5
	});
	function requestFailureKind(status) {
	  return status === 400 || status === 422 ? "validation" : status === 401 ? "authentication" : status === 403 ? "forbidden" : status === 404 || status === 410 ? "not-found" : status === 408 ? "timeout" : status === 409 || status === 412 ? "conflict" : status === 429 ? "rate-limit" : status === 425 || status >= 500 && status <= 599 ? "server" : status >= 400 && status <= 499 ? "client" : "unknown";
	}
	class RequestStatusError extends Error {
	  status;
	  cloudflareMitigated;
	  kind;
	  constructor(status, options = {}) {
	    super(`HTTP ${status}`), this.name = "RequestStatusError", this.status = status, this.cloudflareMitigated = options.cloudflareMitigated === !0, this.kind = options.kind ?? requestFailureKind(status);
	  }
	}
	class RequestCloudflareChallengeError extends RequestStatusError {
	  href;
	  constructor(status, href) {
	    super(status, {
	      cloudflareMitigated: !0,
	      kind: "cloudflare"
	    }), this.name = "RequestCloudflareChallengeError", this.href = String(href);
	  }
	}
	class RequestChallengeWaitSuppressedError extends Error {
	  code = "challenge-superseded";
	  cloudflareMitigated = !0;
	  constructor() {
	    super("排队期间已进入 Cloudflare 验证,本次写入不在过盾后自动追发"), this.name = "RequestChallengeWaitSuppressedError";
	  }
	}
	class RequestRateLimitError extends RequestStatusError {
	  decision;
	  constructor(decision) {
	    super(429), this.name = "RequestRateLimitError", this.decision = decision;
	  }
	}
	function nonNegativeInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 0)
	    throw new RangeError(`${name} 必须是非负安全整数`);
	  return value;
	}
	function abortableDelay(milliseconds, signal) {
	  return new Promise((resolve, reject) => {
	    if (signal.aborted) {
	      reject(signal.reason);
	      return;
	    }
	    const onAbort = () => {
	      clearTimeout(timer), reject(signal.reason);
	    }, timer = setTimeout(() => {
	      signal.removeEventListener("abort", onAbort), resolve();
	    }, milliseconds);
	    signal.addEventListener("abort", onAbort, { once: !0 });
	  });
	}
	class CoordinatedRequestClient {
	  scheduler;
	  rateLimitPolicy;
	  permitPort;
	  #delay;
	  #defaultMax429Retries;
	  #defaultMaxChallengeRetries;
	  #observer;
	  #now;
	  #onCoordinationError;
	  #requests = /* @__PURE__ */ new Map();
	  #disposed = !1;
	  #logicalSequence = 0;
	  constructor(options) {
	    this.rateLimitPolicy = options.rateLimitPolicy, this.permitPort = options.permitPort, this.scheduler = new import_request_scheduler.RequestScheduler({
	      ...options.scheduler,
	      startGate: options.permitPort
	    }), this.#delay = options.delay ?? abortableDelay, this.#defaultMax429Retries = nonNegativeInteger(
	      options.defaultMax429Retries ?? 1,
	      "defaultMax429Retries"
	    ), this.#defaultMaxChallengeRetries = nonNegativeInteger(
	      options.defaultMaxChallengeRetries ?? 1,
	      "defaultMaxChallengeRetries"
	    ), this.#observer = options.observer ?? null, this.#now = options.now ?? Date.now, this.#onCoordinationError = options.onCoordinationError ?? (() => {
	    });
	  }
	  request(options, transport) {
	    const key = String(options.key).trim();
	    if (!key) return Promise.reject(new Error("request key 不能为空"));
	    const priority = options.priority ?? "visible";
	    if (this.#disposed)
	      return Promise.reject(new Error("request client 已销毁"));
	    if (options.signal?.aborted)
	      return Promise.reject(options.signal.reason);
	    const existing = this.#requests.get(key);
	    if (existing && !existing.controller.signal.aborted)
	      return existing.joinedConsumers += 1, this.#promoteLogical(existing, {
	        priority,
	        droppable: options.droppable === !0,
	        max429Retries: options.max429Retries ?? this.#defaultMax429Retries,
	        maxChallengeRetries: options.maxChallengeRetries ?? this.#defaultMaxChallengeRetries
	      }), this.#updateObservation(existing, {
	        joinedConsumers: existing.joinedConsumers
	      }), this.#join(existing, options.signal);
	    const controller = new AbortController(), logical = {
	      promise: Promise.resolve(void 0),
	      controller,
	      consumers: /* @__PURE__ */ new Set(),
	      unabortableConsumer: !1,
	      settled: !1,
	      priority,
	      droppable: options.droppable === !0,
	      max429Retries: 0,
	      maxChallengeRetries: 0,
	      currentAttemptKey: "",
	      logicalId: `L${++this.#logicalSequence}`,
	      observationId: null,
	      joinedConsumers: 0,
	      promoted: !1
	    }, promise = this.#run(logical, options, transport).finally(() => {
	      logical.settled = !0, this.#requests.get(key) === logical && this.#requests.delete(key);
	    });
	    return logical.promise = promise, this.#requests.set(key, logical), this.#join(logical, options.signal);
	  }
	  promote(keyValue, promotion) {
	    const key = String(keyValue).trim();
	    if (!key || this.#disposed) return !1;
	    const logical = this.#requests.get(key);
	    return !logical || logical.controller.signal.aborted || logical.settled ? !1 : (this.#promoteLogical(logical, promotion), !0);
	  }
	  #join(logical, signal) {
	    if (!signal)
	      return logical.unabortableConsumer = !0, logical.promise;
	    if (signal.aborted) return Promise.reject(signal.reason);
	    const consumer = Symbol("request-consumer");
	    return logical.consumers.add(consumer), new Promise((resolve, reject) => {
	      let settled = !1;
	      const finish = (complete, value) => {
	        settled || (settled = !0, signal.removeEventListener("abort", onAbort), logical.consumers.delete(consumer), complete(value));
	      }, fail = (cause) => {
	        settled || (settled = !0, signal.removeEventListener("abort", onAbort), logical.consumers.delete(consumer), reject(cause));
	      }, onAbort = () => {
	        fail(signal.reason), !logical.settled && !logical.unabortableConsumer && logical.consumers.size === 0 && !logical.controller.signal.aborted && logical.controller.abort(signal.reason);
	      };
	      signal.addEventListener("abort", onAbort, { once: !0 }), logical.promise.then(
	        (value) => finish(resolve, value),
	        fail
	      );
	    });
	  }
	  applyRuntimePolicy(policy) {
	    this.scheduler.applyRuntimePolicy(policy);
	  }
	  /**
	   * 把中央 429 错误投影成长任务可等待的续传凭据。
	   *
	   * 使用 retryAt 计算剩余时间,避免错误沿调用栈返回后再次等待完整 Retry-After;
	   * 等待结束只允许长任务重新排队,绝不替代下一次请求的 scheduler/shared permit。
	   */
	  rateLimitResume(error) {
	    if (this.#disposed || !(error instanceof RequestRateLimitError)) return null;
	    const waitMs = Math.max(
	      0,
	      Math.ceil(error.decision.retryAt - this.#now())
	    );
	    return Object.freeze({
	      decision: error.decision,
	      waitMs,
	      wait: (signal) => this.#delay(waitMs, signal)
	    });
	  }
	  /**
	   * 把长任务中断统一投影为可取消恢复凭据。
	   *
	   * 普通 429 复用 Retry-After;Cloudflare 只等待共享 challenge lease,不在长任务
	   * 内重放探针、打开第二个窗口或绕过 scheduler。
	   */
	  requestResume(error) {
	    const rateLimit = this.rateLimitResume(error);
	    if (rateLimit)
	      return Object.freeze({
	        ...rateLimit,
	        kind: "rate-limit"
	      });
	    if (this.#disposed || !(error instanceof RequestCloudflareChallengeError) || !this.permitPort.resolveCloudflareChallenge) return null;
	    const resolve = this.permitPort.resolveCloudflareChallenge.bind(
	      this.permitPort
	    );
	    return Object.freeze({
	      kind: "cloudflare-challenge",
	      waitMs: 0,
	      wait: async (signal) => {
	        if (signal.aborted) throw signal.reason;
	        if (!await resolve({
	          href: error.href,
	          signal
	        })) throw error;
	      }
	    });
	  }
	  /** 清除本实例的 429 范围判定证据;共享固定窗口与已记录启动次数原样保留。 */
	  async resetRateLimits() {
	    this.rateLimitPolicy.reset(), await this.permitPort.resetRateLimits?.();
	  }
	  destroy() {
	    if (!this.#disposed) {
	      this.#disposed = !0;
	      for (const request of this.#requests.values())
	        request.controller.abort(new Error("request client 已销毁"));
	      this.scheduler.destroy();
	    }
	  }
	  async #run(logical, options, transport) {
	    const method = String(options.method ?? "GET").toUpperCase(), rateLimitRoute = this.rateLimitPolicy.identity(
	      options.input,
	      method
	    ).route;
	    logical.max429Retries = nonNegativeInteger(
	      options.max429Retries ?? this.#defaultMax429Retries,
	      "max429Retries"
	    ), logical.maxChallengeRetries = nonNegativeInteger(
	      options.maxChallengeRetries ?? this.#defaultMaxChallengeRetries,
	      "maxChallengeRetries"
	    );
	    let rateLimitRetries = 0, challengeRetries = 0;
	    for (let attempt = 0; ; attempt += 1) {
	      logical.currentAttemptKey = `${options.key}:attempt:${attempt}`;
	      const queuedAt = this.#now();
	      let observationId = this.#beginQueuedObservation(
	        logical,
	        options,
	        method,
	        attempt,
	        logical.priority,
	        queuedAt
	      );
	      logical.observationId = observationId, this.#updateObservation(logical, {
	        joinedConsumers: logical.joinedConsumers,
	        promoted: logical.promoted,
	        max429Retries: logical.max429Retries,
	        maxChallengeRetries: logical.maxChallengeRetries,
	        droppable: logical.droppable
	      });
	      let observationStarted = !1, attemptWaitReason = "", attemptRateLimitRecoveryProbe = !1, response;
	      try {
	        response = await this.scheduler.schedule(
	          {
	            key: logical.currentAttemptKey,
	            priority: logical.priority,
	            lane: options.lane ?? "standard",
	            rateLimitRoute,
	            ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
	            signal: logical.controller.signal,
	            droppable: logical.droppable,
	            onStart: (timing) => {
	              observationStarted = !0, attemptWaitReason = timing.waitReason, attemptRateLimitRecoveryProbe = timing.recoveryProbe, observationId = this.#markObservationStarted(
	                observationId,
	                logical,
	                options,
	                method,
	                attempt,
	                logical.priority,
	                timing
	              ), logical.observationId = observationId;
	            }
	          },
	          (signal) => {
	            if (options.suppressAfterChallengeWait === !0 && attemptWaitReason === "challenge")
	              throw new RequestChallengeWaitSuppressedError();
	            return transport({ signal, attempt });
	          }
	        );
	      } catch (error) {
	        logical.currentAttemptKey = "", attemptRateLimitRecoveryProbe && await this.#noteRateLimitProbeResult(rateLimitRoute, !1);
	        const reason = this.#errorCode(error);
	        throw observationId === null ? this.#recordControlled(
	          logical,
	          options,
	          method,
	          attempt,
	          reason,
	          queuedAt
	        ) : observationStarted ? this.#finishObservation(observationId, {
	          error: reason,
	          decision: reason,
	          cloudflareMitigated: !!error && typeof error == "object" && "cloudflareMitigated" in error && error.cloudflareMitigated === !0
	        }) : this.#cancelObservation(observationId, reason), error;
	      }
	      logical.currentAttemptKey = "", attemptRateLimitRecoveryProbe && (response.status !== 429 || response.cloudflareMitigated === !0) && await this.#noteRateLimitProbeResult(rateLimitRoute, !0);
	      const rateLimitRetryEligible = response.status === 429 && response.cloudflareMitigated !== !0 && [
	        "critical",
	        "interactive",
	        "nested",
	        "visible"
	      ].includes(logical.priority) && rateLimitRetries < logical.max429Retries, challengeResolutionEligible = response.cloudflareMitigated === !0 && options.blockOnCloudflareChallenge !== !1 && !!this.permitPort.resolveCloudflareChallenge && challengeRetries < logical.maxChallengeRetries, decision = response.ok ? "complete" : response.cloudflareMitigated === !0 ? options.blockOnCloudflareChallenge === !1 ? "stop-cloudflare-isolated" : challengeResolutionEligible ? "await-cloudflare" : this.permitPort.noteCloudflareChallenge || this.permitPort.resolveCloudflareChallenge ? "require-cloudflare" : "stop-cloudflare-unhandled" : response.status === 429 ? rateLimitRetryEligible ? "retry-429" : "stop-429" : "stop-http";
	      if (this.#finishObservation(observationId, {
	        status: response.status,
	        cloudflareMitigated: response.cloudflareMitigated === !0,
	        decision,
	        ...response.retryAfter === void 0 ? {} : { retryAfter: String(response.retryAfter ?? "") },
	        ...response.rateLimitCode === void 0 ? {} : { rateLimitCode: String(response.rateLimitCode ?? "") },
	        ...response.serverLimit === void 0 ? {} : { serverLimit: String(response.serverLimit ?? "") },
	        ...response.serverRemaining === void 0 ? {} : { serverRemaining: String(response.serverRemaining ?? "") },
	        ...response.serverReset === void 0 ? {} : { serverReset: String(response.serverReset ?? "") }
	      }), !response.ok && response.cloudflareMitigated === !0) {
	        if (options.blockOnCloudflareChallenge === !1)
	          throw new RequestStatusError(response.status, {
	            cloudflareMitigated: !0
	          });
	        if (!this.permitPort.noteCloudflareChallenge && !this.permitPort.resolveCloudflareChallenge)
	          throw new RequestStatusError(response.status, {
	            cloudflareMitigated: !0,
	            kind: "cloudflare"
	          });
	        let passed = !1;
	        try {
	          this.permitPort.resolveCloudflareChallenge && challengeRetries < logical.maxChallengeRetries ? passed = await this.permitPort.resolveCloudflareChallenge({
	            href: String(options.input),
	            signal: logical.controller.signal
	          }) : await this.permitPort.noteCloudflareChallenge?.({
	            href: String(options.input),
	            force: challengeRetries > 0
	          });
	        } catch (error) {
	          if (logical.controller.signal.aborted) throw error;
	          this.#onCoordinationError(error);
	          try {
	            await this.permitPort.noteCloudflareChallenge?.({
	              href: String(options.input),
	              force: challengeRetries > 0
	            });
	          } catch (fallbackError) {
	            this.#onCoordinationError(fallbackError);
	          }
	        }
	        if (passed) {
	          this.rateLimitPolicy.reset(), challengeRetries += 1, this.#updateObservation(logical, {
	            decision: "challenge-passed-retry"
	          });
	          continue;
	        }
	        throw this.#updateObservation(logical, {
	          decision: "challenge-required"
	        }), new RequestCloudflareChallengeError(
	          response.status,
	          String(options.input)
	        );
	      }
	      if (response.status === 429) {
	        const decision2 = this.rateLimitPolicy.noteRateLimit({
	          input: options.input,
	          method,
	          ...response.retryAfter === void 0 ? {} : { retryAfter: response.retryAfter },
	          knownGlobalWindow: response.knownGlobalRateLimitWindow === !0,
	          ...response.rateLimitWindow === void 0 ? {} : { globalWindow: response.rateLimitWindow }
	        });
	        try {
	          await this.permitPort.noteRateLimit(decision2);
	        } catch (error) {
	          this.#onCoordinationError(error);
	        }
	        if (!rateLimitRetryEligible) throw new RequestRateLimitError(decision2);
	        rateLimitRetries += 1, await this.#delay(decision2.waitMs, logical.controller.signal);
	        continue;
	      }
	      if (!response.ok)
	        throw new RequestStatusError(response.status, {
	          cloudflareMitigated: response.cloudflareMitigated === !0
	        });
	      return response.value;
	    }
	  }
	  async #noteRateLimitProbeResult(route, recovered) {
	    try {
	      await this.permitPort.noteRateLimitProbeResult?.({ route, recovered });
	    } catch (error) {
	      this.#onCoordinationError(error);
	    }
	  }
	  #promoteLogical(logical, promotion) {
	    const previousPriority = logical.priority, previousDroppable = logical.droppable, previous429Retries = logical.max429Retries, previousChallengeRetries = logical.maxChallengeRetries;
	    logical.droppable = logical.droppable && promotion.droppable, promotion.max429Retries !== void 0 && (logical.max429Retries = Math.max(
	      logical.max429Retries,
	      nonNegativeInteger(promotion.max429Retries, "max429Retries")
	    )), promotion.maxChallengeRetries !== void 0 && (logical.maxChallengeRetries = Math.max(
	      logical.maxChallengeRetries,
	      nonNegativeInteger(
	        promotion.maxChallengeRetries,
	        "maxChallengeRetries"
	      )
	    )), PRIORITY_WEIGHT[promotion.priority] < PRIORITY_WEIGHT[logical.priority] && (logical.priority = promotion.priority), (logical.priority !== previousPriority || logical.droppable !== previousDroppable || logical.max429Retries !== previous429Retries || logical.maxChallengeRetries !== previousChallengeRetries) && (logical.promoted = !0, this.#updateObservation(logical, {
	      priority: logical.priority,
	      promoted: !0,
	      max429Retries: logical.max429Retries,
	      maxChallengeRetries: logical.maxChallengeRetries,
	      droppable: logical.droppable
	    })), logical.currentAttemptKey && this.scheduler.promoteQueued(
	      logical.currentAttemptKey,
	      logical.priority,
	      logical.droppable
	    );
	  }
	  #beginQueuedObservation(logical, options, method, attempt, priority, queuedAt) {
	    if (!this.#observer) return null;
	    try {
	      return this.#observer.begin({
	        href: String(options.input),
	        method,
	        transport: "scheduler",
	        source: "reader",
	        phase: "queued",
	        queuedAt,
	        priority,
	        attempt: attempt + 1,
	        recoveryProbe: attempt > 0,
	        callSite: options.callSite ?? "",
	        logicalId: logical.logicalId,
	        profile: options.profile ?? "",
	        namespace: options.namespace ?? "",
	        lane: options.lane ?? "standard",
	        cacheMode: options.cacheMode ?? "",
	        ...options.identity === void 0 ? {} : { identity: options.identity },
	        max429Retries: logical.max429Retries,
	        maxChallengeRetries: logical.maxChallengeRetries,
	        blockOnCloudflareChallenge: options.blockOnCloudflareChallenge !== !1,
	        suppressAfterChallengeWait: options.suppressAfterChallengeWait === !0,
	        droppable: logical.droppable
	      });
	    } catch (error) {
	      return this.#onCoordinationError(error), null;
	    }
	  }
	  #markObservationStarted(id, logical, options, method, attempt, priority, timing) {
	    if (!this.#observer) return null;
	    try {
	      return id !== null && this.#observer.markStarted({
	        id,
	        queuedAt: timing.queuedAt,
	        permittedAt: timing.permittedAt,
	        startedAt: timing.startedAt,
	        priority,
	        recoveryProbe: timing.recoveryProbe,
	        waitReason: timing.waitReason || (timing.permittedAt - timing.queuedAt > 0.5 ? "scheduler" : "")
	      }) ? id : this.#observer.begin({
	        href: String(options.input),
	        method,
	        transport: "scheduler",
	        source: "reader",
	        phase: "running",
	        queuedAt: timing.queuedAt,
	        permittedAt: timing.permittedAt,
	        startedAt: timing.startedAt,
	        priority,
	        attempt: attempt + 1,
	        recoveryProbe: timing.recoveryProbe,
	        waitReason: timing.waitReason || (timing.permittedAt - timing.queuedAt > 0.5 ? "scheduler" : ""),
	        callSite: options.callSite ?? "",
	        logicalId: logical.logicalId,
	        profile: options.profile ?? "",
	        namespace: options.namespace ?? "",
	        lane: options.lane ?? "standard",
	        cacheMode: options.cacheMode ?? "",
	        ...options.identity === void 0 ? {} : { identity: options.identity },
	        max429Retries: logical.max429Retries,
	        maxChallengeRetries: logical.maxChallengeRetries,
	        blockOnCloudflareChallenge: options.blockOnCloudflareChallenge !== !1,
	        suppressAfterChallengeWait: options.suppressAfterChallengeWait === !0,
	        droppable: logical.droppable
	      });
	    } catch (error) {
	      return this.#onCoordinationError(error), null;
	    }
	  }
	  #cancelObservation(id, reason) {
	    if (this.#observer)
	      try {
	        this.#observer.cancel(id, { reason }) || this.#observer.finish(id, { error: reason });
	      } catch (error) {
	        this.#onCoordinationError(error);
	      }
	  }
	  #finishObservation(id, input) {
	    if (!(id === null || !this.#observer))
	      try {
	        this.#observer.finish(id, input);
	      } catch (error) {
	        this.#onCoordinationError(error);
	      }
	  }
	  #recordControlled(logical, options, method, attempt, reason, queuedAt = this.#now()) {
	    if (this.#observer)
	      try {
	        this.#observer.begin({
	          href: String(options.input),
	          method,
	          transport: "scheduler",
	          source: "reader",
	          queuedAt,
	          startedAt: this.#now(),
	          priority: options.priority ?? "visible",
	          attempt: attempt + 1,
	          recoveryProbe: attempt > 0,
	          waitReason: reason,
	          callSite: options.callSite ?? "",
	          controlReason: reason,
	          logicalId: logical.logicalId,
	          profile: options.profile ?? "",
	          namespace: options.namespace ?? "",
	          lane: options.lane ?? "standard",
	          cacheMode: options.cacheMode ?? "",
	          ...options.identity === void 0 ? {} : { identity: options.identity },
	          max429Retries: logical.max429Retries,
	          maxChallengeRetries: logical.maxChallengeRetries,
	          blockOnCloudflareChallenge: options.blockOnCloudflareChallenge !== !1,
	          suppressAfterChallengeWait: options.suppressAfterChallengeWait === !0,
	          droppable: logical.droppable
	        });
	      } catch (error) {
	        this.#onCoordinationError(error);
	      }
	  }
	  #updateObservation(logical, input) {
	    if (!(logical.observationId === null || !this.#observer))
	      try {
	        this.#observer.update(logical.observationId, input);
	      } catch (error) {
	        this.#onCoordinationError(error);
	      }
	  }
	  #errorCode(error) {
	    if (error && typeof error == "object") {
	      const message = "message" in error ? String(error.message ?? "") : "";
	      if (message.includes("滚出当前视口")) return "viewport-change";
	      if (message.includes("升级为可见快车道")) return "priority-upgrade";
	      if (message.includes("已切换") || message.includes("打开已被替代") || message.includes("新的打开事务")) return "topic-switch";
	      if (message.includes("已关闭") || message.includes("离开 Topic") || message.includes("读取链已结束") || message.includes("Topic session closed")) return "topic-close";
	      if (message.includes("request client 已销毁") || message.includes("Reader runtime 已销毁") || message.includes("Reader application 已销毁")) return "context-close";
	      const code = "code" in error && typeof error.code == "string" ? error.code : "";
	      if (code) return code.slice(0, 80);
	      const name = "name" in error ? String(error.name ?? "") : "";
	      if (name) return name.slice(0, 80);
	    }
	    return "request-failed";
	  }
	}
}, "4a89e55d79098b3bef1201dae3652bb342c9bdd9a480f4ed7668b60a1ce7e828");

/* Source: lite/src/network/discourse-native-read-transport.ts */
runtime.register("src/network/discourse-native-read-transport.js", function(module, exports, require) {
	var discourse_native_read_transport_exports = {};
	__export(discourse_native_read_transport_exports, {
	  BrowserDiscourseNativeAjaxPort: () => BrowserDiscourseNativeAjaxPort,
	  BrowserDiscourseNativeMutationTransport: () => BrowserDiscourseNativeMutationTransport,
	  BrowserDiscourseNativeReadTransport: () => BrowserDiscourseNativeReadTransport,
	  discourseNativeAjaxAvailable: () => discourseNativeAjaxAvailable,
	  discourseNativeFailureResponse: () => discourseNativeFailureResponse
	});
	module.exports = __toCommonJS(discourse_native_read_transport_exports);
	var import_native_request_descriptors = require("../discourse/native-request-descriptors.js"), import_request_rate_limit_policy = require("./request-rate-limit-policy.js"), import_value_record = require("../kernel/value-record.js");
	const nativeReadTransportBrand = Symbol("DiscourseNativeReadTransport"), nativeMutationTransportBrand = Symbol("DiscourseNativeMutationTransport");
	function statusFromError(error) {
	  const record = (0, import_value_record.objectRecord)(error), response = (0, import_value_record.objectRecord)(record?.response), xhr = (0, import_value_record.objectRecord)(record?.jqXHR);
	  for (const value of [
	    record?.status,
	    record?.statusCode,
	    response?.status,
	    xhr?.status
	  ]) {
	    const status = Number(value);
	    if (Number.isSafeInteger(status) && status >= 100 && status <= 599)
	      return status;
	  }
	  return 0;
	}
	function responseHeader(error, name) {
	  const record = (0, import_value_record.objectRecord)(error), candidates = [error, record?.jqXHR, record?.response];
	  for (const candidate of candidates) {
	    const getResponseHeader = (0, import_value_record.objectRecord)(candidate)?.getResponseHeader;
	    if (typeof getResponseHeader == "function")
	      try {
	        const value = getResponseHeader.call(candidate, name);
	        if (value != null && String(value).trim())
	          return String(value);
	      } catch {
	      }
	  }
	  return null;
	}
	function retryAfterFromError(error) {
	  const record = (0, import_value_record.objectRecord)(error), response = (0, import_value_record.objectRecord)(record?.response);
	  for (const value of [
	    record?.retryAfter,
	    record?.retry_after,
	    response?.retryAfter,
	    response?.retry_after,
	    responseHeader(error, "Retry-After")
	  ])
	    if (value != null && String(value).trim())
	      return String(value);
	  return null;
	}
	function responseTextFromError(error) {
	  const record = (0, import_value_record.objectRecord)(error), candidates = [error, record?.jqXHR, record?.response];
	  for (const candidate of candidates) {
	    const response = (0, import_value_record.objectRecord)(candidate);
	    for (const value of [
	      response?.responseText,
	      response?.body,
	      response?.data
	    ])
	      if (typeof value == "string" && value.trim()) return value;
	  }
	  return "";
	}
	function cloudflareChallengeHtml(error) {
	  const body = responseTextFromError(error);
	  return body ? /<title>\s*(?:Just a moment(?:\.\.\.)?|请稍候(?:…|\.\.\.)?)\s*<\/title>/i.test(body) || /window\._cf_chl_opt\b/.test(body) && /\/cdn-cgi\/challenge-platform\//.test(body) : !1;
	}
	function cloudflareMitigatedFromError(error) {
	  const record = (0, import_value_record.objectRecord)(error), response = (0, import_value_record.objectRecord)(record?.response), xhr = (0, import_value_record.objectRecord)(record?.jqXHR);
	  return record?.cloudflareMitigated === !0 || response?.cloudflareMitigated === !0 || xhr?.cloudflareMitigated === !0 || responseHeader(error, "cf-mitigated")?.trim().toLowerCase() === "challenge" || cloudflareChallengeHtml(error);
	}
	function discourseNativeFailureResponse(error) {
	  const status = statusFromError(error);
	  if (!status) return null;
	  const rateLimitCode = responseHeader(error, "Discourse-Rate-Limit-Error-Code") ?? responseHeader(error, "X-Discourse-Rate-Limit-Error-Code") ?? "", rateLimitWindow = (0, import_request_rate_limit_policy.rateLimitWindowFromCode)(rateLimitCode);
	  return Object.freeze({
	    ok: !1,
	    status,
	    value: void 0,
	    retryAfter: retryAfterFromError(error),
	    rateLimitCode,
	    rateLimitWindow,
	    knownGlobalRateLimitWindow: rateLimitWindow !== "unknown",
	    cloudflareMitigated: cloudflareMitigatedFromError(error)
	  });
	}
	function normalizedOrigin(value) {
	  const normalized = String(value ?? "").trim();
	  return normalized ? new URL(normalized).origin : "";
	}
	function nativePath(value, origin) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error("Discourse 原生读取 path 不能为空");
	  if (/^[\\/]{2}/.test(normalized))
	    throw new Error("Discourse 原生读取拒绝跨源 URL");
	  if (normalized.startsWith("/")) return normalized;
	  let parsed;
	  try {
	    parsed = new URL(normalized);
	  } catch {
	    throw new Error("Discourse 原生读取只接受站内绝对路径");
	  }
	  if (!origin || parsed.origin !== origin)
	    throw new Error("Discourse 原生读取拒绝跨源 URL");
	  return `${parsed.pathname}${parsed.search}`;
	}
	function resolveNativeAjax(host) {
	  const loaded = host.lookupModule("discourse/lib/ajax"), module2 = (0, import_value_record.objectRecord)(loaded), defaultExport = (0, import_value_record.objectRecord)(module2?.default), owner = module2 && typeof module2.ajax == "function" ? module2 : defaultExport && typeof defaultExport.ajax == "function" ? defaultExport : null;
	  if (!owner)
	    throw new Error("Discourse 原生模块 discourse/lib/ajax#ajax 不可用");
	  return {
	    owner,
	    ajax: owner.ajax
	  };
	}
	function discourseNativeAjaxAvailable(host) {
	  try {
	    return resolveNativeAjax(host), !0;
	  } catch {
	    return !1;
	  }
	}
	async function executeNativeAjax(resolved, origin, input) {
	  if (input.signal.aborted) throw input.signal.reason;
	  const path = nativePath(input.path, origin), options = {
	    type: input.method,
	    ...input.headers && Object.keys(input.headers).length ? { headers: { ...input.headers } } : {},
	    ...input.data === void 0 ? {} : { data: input.multipart ? input.data : { ...input.data } },
	    ...input.multipart ? { processData: !1, contentType: !1 } : {},
	    ...input.noStore ? { cache: !1 } : {}
	  };
	  let pending;
	  try {
	    pending = resolved.ajax.call(resolved.owner, path, options);
	  } catch (error) {
	    const failure = discourseNativeFailureResponse(error);
	    if (!failure) throw error;
	    return failure;
	  }
	  const abort = () => {
	    try {
	      pending.abort?.();
	    } catch {
	    }
	  };
	  input.signal.addEventListener("abort", abort, { once: !0 });
	  try {
	    const value = await pending;
	    if (input.signal.aborted) throw input.signal.reason;
	    return { ok: !0, status: 200, value };
	  } catch (error) {
	    if (input.signal.aborted) throw input.signal.reason;
	    const failure = discourseNativeFailureResponse(error);
	    if (!failure) throw error;
	    return failure;
	  } finally {
	    input.signal.removeEventListener("abort", abort);
	  }
	}
	class BrowserDiscourseNativeAjaxPort {
	  nativeBinding = "discourse/lib/ajax#ajax";
	  #host;
	  #origin;
	  #resolved = null;
	  constructor(host, options = {}) {
	    this.#host = host, this.#origin = normalizedOrigin(options.origin);
	  }
	  request(input) {
	    return this.#resolved ??= resolveNativeAjax(this.#host), executeNativeAjax(this.#resolved, this.#origin, input);
	  }
	}
	class BrowserDiscourseNativeReadTransport {
	  nativeBinding = "discourse/lib/ajax#ajax";
	  [nativeReadTransportBrand] = !0;
	  #ajax;
	  constructor(host, options = {}) {
	    this.#ajax = host instanceof BrowserDiscourseNativeAjaxPort ? host : new BrowserDiscourseNativeAjaxPort(host, options);
	  }
	  async request(input) {
	    return (0, import_native_request_descriptors.assertDiscourseNativeReadDescriptor)(input.descriptor), this.#ajax.request({
	      path: input.descriptor.path,
	      method: "GET",
	      signal: input.signal,
	      headers: input.descriptor.headers,
	      noStore: input.descriptor.browserCache === "no-store"
	    });
	  }
	}
	class BrowserDiscourseNativeMutationTransport {
	  nativeBinding = "discourse/lib/ajax#ajax";
	  [nativeMutationTransportBrand] = !0;
	  #ajax;
	  constructor(host, options = {}) {
	    this.#ajax = host instanceof BrowserDiscourseNativeAjaxPort ? host : new BrowserDiscourseNativeAjaxPort(host, options);
	  }
	  request(input) {
	    return (0, import_native_request_descriptors.assertDiscourseNativeMutationDescriptor)(input.descriptor), this.#ajax.request({
	      path: input.descriptor.path,
	      method: input.descriptor.method,
	      signal: input.signal,
	      headers: input.descriptor.headers,
	      data: input.descriptor.data,
	      multipart: input.descriptor.multipart === !0,
	      noStore: !0
	    });
	  }
	}
}, "f7a36c4992ee35870fd61e99a2b31a5eef89e31eecf72338f77e8a4f07d70e59");

/* Source: lite/src/network/domain-request-gateway.ts */
runtime.register("src/network/domain-request-gateway.js", function(module, exports, require) {
	var domain_request_gateway_exports = {};
	__export(domain_request_gateway_exports, {
	  DomainRequestGateway: () => DomainRequestGateway
	});
	module.exports = __toCommonJS(domain_request_gateway_exports);
	var import_request_contract = require("./request-contract.js"), import_request_identities = require("./request-identities.js");
	const PRIORITY_WEIGHT = Object.freeze({
	  critical: 0,
	  interactive: 1,
	  nested: 2,
	  visible: 3,
	  prefetch: 4,
	  background: 5
	});
	function cachePolicy(contract, settings) {
	  return Object.freeze({
	    id: contract.cacheKey,
	    kind: settings.kind,
	    tags: Object.freeze([...new Set(settings.tags.map(String))].sort()),
	    freshForMs: settings.freshForMs,
	    retainForMs: settings.retainForMs,
	    persist: settings.persist
	  });
	}
	class DomainRequestGateway {
	  #client;
	  #responses;
	  #executions = /* @__PURE__ */ new Map();
	  constructor(client, responses) {
	    this.#client = client, this.#responses = responses;
	  }
	  loadTopicPosts(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "topic-visible",
	      lane: "topic-batch",
	      namespace: "topic-posts",
	      identity: (0, import_request_identities.topicPostsRequestIdentity)(input)
	    });
	  }
	  loadTopicTarget(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "topic-visible",
	      lane: "topic-batch",
	      namespace: "topic-target",
	      identity: (0, import_request_identities.topicRequestIdentity)(input)
	    });
	  }
	  async cachedTopicTarget(input) {
	    const contract = (0, import_request_contract.createRequestContract)(input.profile ?? "topic-visible", {
	      namespace: "topic-target",
	      identity: (0, import_request_identities.topicRequestIdentity)(input)
	    }), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  loadNestedReplies(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "nested-visible",
	      lane: "nested-replies",
	      namespace: "topic-nested",
	      identity: (0, import_request_identities.nestedRequestIdentity)(input)
	    });
	  }
	  promoteTopicPosts(input) {
	    const contract = (0, import_request_contract.createRequestContract)(
	      input.profile ?? "topic-visible",
	      {
	        namespace: "topic-posts",
	        identity: (0, import_request_identities.topicPostsRequestIdentity)(input),
	        ...input.cacheMode === void 0 ? {} : { cacheMode: input.cacheMode }
	      }
	    );
	    return this.#promoteExecution(contract);
	  }
	  promoteNestedReplies(input) {
	    const contract = (0, import_request_contract.createRequestContract)(
	      input.profile ?? "nested-visible",
	      {
	        namespace: "topic-nested",
	        identity: (0, import_request_identities.nestedRequestIdentity)(input),
	        ...input.cacheMode === void 0 ? {} : { cacheMode: input.cacheMode }
	      }
	    );
	    return this.#promoteExecution(contract);
	  }
	  loadNotificationPage(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "notification-visible",
	      lane: "standard",
	      namespace: "notifications",
	      identity: (0, import_request_identities.notificationRequestIdentity)(input)
	    });
	  }
	  async cachedNotificationPage(input) {
	    const contract = (0, import_request_contract.createRequestContract)(
	      input.profile ?? "notification-visible",
	      {
	        namespace: "notifications",
	        identity: (0, import_request_identities.notificationRequestIdentity)(input)
	      }
	    ), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  loadCollectionPage(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "collection-visible",
	      lane: "standard",
	      namespace: "reader-collection",
	      identity: (0, import_request_identities.collectionRequestIdentity)(input)
	    });
	  }
	  async cachedCollectionPage(input) {
	    const contract = (0, import_request_contract.createRequestContract)(
	      input.profile ?? "collection-visible",
	      {
	        namespace: "reader-collection",
	        identity: (0, import_request_identities.collectionRequestIdentity)(input)
	      }
	    ), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  mutate(input) {
	    return this.#execute({
	      ...input,
	      cacheMode: "no-store",
	      profile: "action-critical",
	      lane: "control",
	      namespace: "reader-action",
	      identity: (0, import_request_identities.actionRequestIdentity)(input)
	    });
	  }
	  loadActionPermission(input) {
	    return this.#execute({
	      ...input,
	      cacheMode: "no-store",
	      profile: "action-permission",
	      lane: "control",
	      namespace: "reader-action-permission",
	      identity: (0, import_request_identities.actionRequestIdentity)(input)
	    });
	  }
	  submitReadState(input) {
	    return this.#execute({
	      ...input,
	      cacheMode: "no-store",
	      profile: "read-critical",
	      lane: "control",
	      namespace: "topic-read-state",
	      identity: (0, import_request_identities.readRequestIdentity)(input)
	    });
	  }
	  translate(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "translation-visible",
	      lane: "translation",
	      namespace: "reader-translation",
	      identity: (0, import_request_identities.translationRequestIdentity)(input)
	    });
	  }
	  loadResource(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "resource-visible",
	      lane: "standard",
	      namespace: "reader-resource",
	      identity: (0, import_request_identities.resourceRequestIdentity)(input)
	    });
	  }
	  loadUserResource(input) {
	    return this.#execute({
	      ...input,
	      profile: input.profile ?? "resource-visible",
	      lane: input.profile === "user-card-interactive" ? "user-card" : "standard",
	      namespace: "reader-user",
	      identity: (0, import_request_identities.userRequestIdentity)(input)
	    });
	  }
	  async cachedUserResource(input) {
	    const contract = (0, import_request_contract.createRequestContract)(input.profile ?? "resource-visible", {
	      namespace: "reader-user",
	      identity: (0, import_request_identities.userRequestIdentity)(input)
	    }), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  async cachedResource(input) {
	    const contract = this.#resourceContract(input), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  async cachedTranslation(input) {
	    const contract = this.#translationCacheContract(input), cached = await this.#responses.read(
	      cachePolicy(contract, input.cache)
	    );
	    return cached.state === "miss" ? null : cached.value;
	  }
	  cacheTranslation(input, value) {
	    const contract = this.#translationCacheContract(input);
	    return this.#responses.write(cachePolicy(contract, input.cache), value);
	  }
	  invalidateResource(input) {
	    return this.#responses.invalidate({
	      ids: [this.#resourceContract(input).cacheKey]
	    });
	  }
	  invalidateResourceWithReport(input) {
	    return this.invalidateResourcesWithReport([input]);
	  }
	  invalidateResourcesWithReport(inputs) {
	    const ids = Object.freeze([...new Set(inputs.map(
	      (input) => this.#resourceContract(input).cacheKey
	    ))]);
	    return ids.length ? this.#responses.invalidateWithReport({
	      ids
	    }) : Promise.resolve(Object.freeze({
	      memoryEntries: 0,
	      failures: Object.freeze([]),
	      complete: !0
	    }));
	  }
	  #resourceContract(input) {
	    return (0, import_request_contract.createRequestContract)("resource-visible", {
	      namespace: "reader-resource",
	      identity: (0, import_request_identities.resourceRequestIdentity)(input)
	    });
	  }
	  #translationCacheContract(input) {
	    return (0, import_request_contract.createRequestContract)("translation-visible", {
	      namespace: "reader-translation-section",
	      identity: (0, import_request_identities.translationRequestIdentity)(input)
	    });
	  }
	  #execute(input) {
	    if (input.signal.aborted) return Promise.reject(input.signal.reason);
	    const contract = (0, import_request_contract.createRequestContract)(input.profile, {
	      namespace: input.namespace,
	      identity: input.identity,
	      ...input.cacheMode === void 0 ? {} : { cacheMode: input.cacheMode },
	      ...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs }
	    });
	    if (contract.cacheMode !== "no-store" && !input.cache)
	      return Promise.reject(new Error(`${input.profile} 缺少 response cache settings`));
	    const execution = this.#acquireExecution(contract), network = async (signal) => {
	      if (input.beforeNetwork && execution.contract.priority === "background" && (await input.beforeNetwork(signal), signal.aborted))
	        throw signal.reason;
	      execution.started = !0;
	      const effective = execution.contract, requestOptions = {
	        key: effective.key,
	        input: input.input,
	        priority: effective.priority,
	        lane: input.lane,
	        timeoutMs: effective.timeoutMs,
	        droppable: effective.droppable,
	        max429Retries: effective.max429Retries,
	        maxChallengeRetries: effective.maxChallengeRetries,
	        blockOnCloudflareChallenge: effective.blockOnCloudflareChallenge !== !1,
	        suppressAfterChallengeWait: effective.suppressAfterChallengeWait === !0,
	        callSite: `${effective.profile} / ${input.namespace} / ${input.lane}`,
	        profile: effective.profile,
	        namespace: input.namespace,
	        cacheMode: effective.cacheMode,
	        identity: input.identity,
	        ...input.method === void 0 ? {} : { method: input.method }
	      };
	      return this.#client.request(
	        { ...requestOptions, signal },
	        input.transport
	      );
	    };
	    return (contract.cacheMode === "no-store" ? network(input.signal) : this.#responses.getOrLoad(
	      cachePolicy(contract, input.cache),
	      network,
	      {
	        cacheMode: contract.cacheMode,
	        signal: input.signal,
	        ...input.allowStaleOnError === void 0 ? {} : { allowStaleOnError: input.allowStaleOnError },
	        ...input.canFallback === void 0 ? {} : { canFallback: input.canFallback },
	        ...input.mapStaleFallback === void 0 ? {} : { mapStaleFallback: input.mapStaleFallback }
	      }
	    )).finally(() => this.#releaseExecution(contract.key, execution));
	  }
	  #acquireExecution(contract) {
	    const existing = this.#executions.get(contract.key);
	    if (existing)
	      return existing.consumers += 1, this.#upgradeExecution(existing, contract), existing;
	    const created = {
	      contract,
	      consumers: 1,
	      started: !1
	    };
	    return this.#executions.set(contract.key, created), created;
	  }
	  #promoteExecution(contract) {
	    const execution = this.#executions.get(contract.key);
	    return execution ? (this.#upgradeExecution(execution, contract), !0) : this.#client.promote?.(
	      contract.key,
	      this.#requestPromotion(contract)
	    ) ?? !1;
	  }
	  #upgradeExecution(execution, incoming) {
	    const current = execution.contract, winner = PRIORITY_WEIGHT[incoming.priority] < PRIORITY_WEIGHT[current.priority] ? incoming : current, merged = Object.freeze({
	      ...winner,
	      droppable: current.droppable && incoming.droppable,
	      max429Retries: Math.max(
	        current.max429Retries,
	        incoming.max429Retries
	      ),
	      maxChallengeRetries: Math.max(
	        current.maxChallengeRetries,
	        incoming.maxChallengeRetries
	      ),
	      timeoutMs: Math.min(current.timeoutMs, incoming.timeoutMs)
	    });
	    (merged.priority !== current.priority || merged.droppable !== current.droppable || merged.max429Retries !== current.max429Retries || merged.maxChallengeRetries !== current.maxChallengeRetries || merged.timeoutMs !== current.timeoutMs) && (execution.contract = merged, execution.started && this.#client.promote?.(
	      merged.key,
	      this.#requestPromotion(merged)
	    ));
	  }
	  #requestPromotion(contract) {
	    return Object.freeze({
	      priority: contract.priority,
	      droppable: contract.droppable,
	      max429Retries: contract.max429Retries,
	      maxChallengeRetries: contract.maxChallengeRetries
	    });
	  }
	  #releaseExecution(key, execution) {
	    execution.consumers = Math.max(0, execution.consumers - 1), execution.consumers === 0 && this.#executions.get(key) === execution && this.#executions.delete(key);
	  }
	}
}, "bb5459015c86190f3bcc63a743091ec5cbd9b83e2d765c0ba2d93434fd630b3d");

/* Source: lite/src/network/public-resource-request-adapter.ts */
runtime.register("src/network/public-resource-request-adapter.js", function(module, exports, require) {
	var public_resource_request_adapter_exports = {};
	__export(public_resource_request_adapter_exports, {
	  BrowserPublicResourceHttpPort: () => BrowserPublicResourceHttpPort,
	  PublicResourceRequestAdapter: () => PublicResourceRequestAdapter
	});
	module.exports = __toCommonJS(public_resource_request_adapter_exports);
	var import_coordinated_request_client = require("./coordinated-request-client.js"), import_request_rate_limit_policy = require("./request-rate-limit-policy.js");
	const publicResourceDescriptorBrand = Symbol("PublicResourceHttpDescriptor"), publicResourceDescriptors = /* @__PURE__ */ new WeakSet();
	function normalizedSource(rawSource, baseUrl) {
	  const source = String(rawSource).trim();
	  if (!source) throw new Error("资源 URL 不能为空");
	  const url = new URL(source, baseUrl);
	  if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
	    throw new Error(`不支持的资源协议:${url.protocol}`);
	  return url.hash = "", url.href;
	}
	function descriptor(source, validation) {
	  const value = Object.freeze({
	    url: source,
	    ...validation === void 0 ? {} : { validation },
	    [publicResourceDescriptorBrand]: !0
	  });
	  return publicResourceDescriptors.add(value), value;
	}
	function assertDescriptor(input) {
	  if (!publicResourceDescriptors.has(input))
	    throw new Error("公共资源请求 descriptor 未登记");
	  const protocol = new URL(input.url).protocol;
	  if (!["http:", "https:", "blob:", "data:"].includes(protocol))
	    throw new Error(`公共资源请求拒绝协议 ${protocol}`);
	}
	class BrowserPublicResourceHttpPort {
	  #request;
	  constructor(options) {
	    this.#request = options.request;
	  }
	  async execute(input, request) {
	    if (assertDescriptor(input), request.signal.aborted) throw request.signal.reason;
	    const response = await this.#request(input.url, {
	      credentials: "omit",
	      cache: "force-cache",
	      signal: request.signal
	    }), contentType = String(response.headers.get("Content-Type") ?? "").trim().toLowerCase(), lastModified = Date.parse(String(
	      response.headers.get("Last-Modified") ?? ""
	    )), discourseBlankAvatar = input.validation === "discourse-avatar" && Number.isFinite(lastModified) && new Date(lastModified).getUTCFullYear() <= 1990, accepted = response.ok && (contentType === "" || contentType.startsWith("image/")) && !discourseBlankAvatar, value = accepted ? await response.blob() : new Blob(), rateLimitCode = response.headers.get("Discourse-Rate-Limit-Error-Code") ?? response.headers.get("X-Discourse-Rate-Limit-Error-Code") ?? "", rateLimitWindow = (0, import_request_rate_limit_policy.rateLimitWindowFromCode)(rateLimitCode);
	    return {
	      ok: accepted,
	      status: response.ok && !accepted ? 415 : response.status,
	      value,
	      retryAfter: response.headers.get("Retry-After"),
	      rateLimitCode,
	      rateLimitWindow,
	      knownGlobalRateLimitWindow: rateLimitWindow !== "unknown",
	      serverLimit: response.headers.get("X-RateLimit-Limit"),
	      serverRemaining: response.headers.get("X-RateLimit-Remaining"),
	      serverReset: response.headers.get("X-RateLimit-Reset"),
	      cloudflareMitigated: response.headers.get("cf-mitigated")?.trim().toLowerCase() === "challenge"
	    };
	  }
	}
	class PublicResourceRequestAdapter {
	  #gateway;
	  #http;
	  #baseUrl;
	  #cache;
	  constructor(options) {
	    this.#gateway = options.gateway, this.#http = options.http, this.#baseUrl = options.baseUrl, this.#cache = options.cache;
	  }
	  async load(rawSource, options) {
	    const source = normalizedSource(rawSource, this.#baseUrl), requestDescriptor = descriptor(source, options.validation), variant = options.validation === "discourse-avatar" ? "avatar-blob" : "blob";
	    if (source.startsWith("blob:") || source.startsWith("data:")) {
	      const response = await this.#http.execute(requestDescriptor, {
	        signal: options.signal,
	        attempt: 0
	      });
	      if (!response.ok) throw new import_coordinated_request_client.RequestStatusError(response.status);
	      return response.value;
	    }
	    return this.#gateway.loadResource({
	      resourceId: source,
	      variant,
	      input: source,
	      signal: options.signal,
	      cache: this.#cache,
	      ...options.cacheMode === void 0 ? {} : { cacheMode: options.cacheMode },
	      ...options.profile === void 0 ? {} : { profile: options.profile },
	      transport: (request) => this.#http.execute(requestDescriptor, request)
	    });
	  }
	  cached(rawSource) {
	    const source = normalizedSource(rawSource, this.#baseUrl);
	    return source.startsWith("blob:") || source.startsWith("data:") ? Promise.resolve(null) : this.#gateway.cachedResource({
	      resourceId: source,
	      variant: "blob",
	      cache: this.#cache
	    });
	  }
	  invalidate(rawSource) {
	    const source = normalizedSource(rawSource, this.#baseUrl);
	    return source.startsWith("blob:") || source.startsWith("data:") ? Promise.resolve() : this.#gateway.invalidateResource({
	      resourceId: source,
	      variant: "blob",
	      cache: this.#cache
	    });
	  }
	  invalidateWithReport(rawSource) {
	    return this.invalidateManyWithReport([rawSource]);
	  }
	  invalidateManyWithReport(rawSources) {
	    const sources = [...new Set(rawSources.map(
	      (rawSource) => normalizedSource(rawSource, this.#baseUrl)
	    ))].filter(
	      (source) => !source.startsWith("blob:") && !source.startsWith("data:")
	    );
	    return sources.length ? this.#gateway.invalidateResourcesWithReport(
	      sources.map((source) => ({
	        resourceId: source,
	        variant: "blob"
	      }))
	    ) : Promise.resolve(Object.freeze({
	      memoryEntries: 0,
	      failures: Object.freeze([]),
	      complete: !0
	    }));
	  }
	  normalize(rawSource) {
	    return normalizedSource(rawSource, this.#baseUrl);
	  }
	}
}, "f4ce9f1cb8ba6a9e8c20b379a43ee1d44816760142d61347ee497ed2000549d5");

/* Source: lite/src/network/request-contract.ts */
runtime.register("src/network/request-contract.js", function(module, exports, require) {
	var request_contract_exports = {};
	__export(request_contract_exports, {
	  createRequestContract: () => createRequestContract,
	  requestProfileContract: () => requestProfileContract
	});
	module.exports = __toCommonJS(request_contract_exports);
	function cacheModes(...values) {
	  return Object.freeze(values);
	}
	const PROFILES = Object.freeze({
	  "bootstrap-critical": Object.freeze({
	    priority: "critical",
	    lifecycle: "application",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 8e3,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "action-critical": Object.freeze({
	    priority: "critical",
	    lifecycle: "action",
	    droppable: !1,
	    defaultCacheMode: "no-store",
	    allowedCacheModes: cacheModes("no-store"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "action-permission": Object.freeze({
	    priority: "visible",
	    lifecycle: "action",
	    droppable: !1,
	    defaultCacheMode: "no-store",
	    allowedCacheModes: cacheModes("no-store"),
	    defaultTimeoutMs: 12e3,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "read-critical": Object.freeze({
	    priority: "critical",
	    lifecycle: "topic",
	    droppable: !1,
	    /*
	     * timings 可以被 Cloudflare 单独拒绝;通用 session 探针的 200
	     * 不能证明该端点已解除。保留 checkpoint 并结束本请求,
	     * 不建立共享验证世代,也不在其他请求过盾后追发旧批次。
	     */
	    blockOnCloudflareChallenge: !1,
	    suppressAfterChallengeWait: !0,
	    defaultCacheMode: "no-store",
	    allowedCacheModes: cacheModes("no-store"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 0
	  }),
	  "topic-visible": Object.freeze({
	    priority: "visible",
	    lifecycle: "topic",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh", "no-store"),
	    defaultTimeoutMs: 2e4,
	    /*
	     * Topic 正文/目标楼层把 429 与 cf-mitigated 当作本次用户意图的终态。
	     * 验证闸门仍会阻止后续启动,但同一逻辑请求不得在过盾后自动重放;
	     * 下一次请求只能来自新的物理滚动或显式导航。
	     */
	    max429Retries: 0,
	    maxChallengeRetries: 0
	  }),
	  "nested-visible": Object.freeze({
	    priority: "nested",
	    lifecycle: "topic",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 12e3,
	    max429Retries: 0,
	    maxChallengeRetries: 0
	  }),
	  "user-card-interactive": Object.freeze({
	    priority: "interactive",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 12e3,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "translation-visible": Object.freeze({
	    priority: "visible",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "translation-access": Object.freeze({
	    priority: "interactive",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "no-store",
	    allowedCacheModes: cacheModes("no-store"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "translation-prefetch": Object.freeze({
	    priority: "prefetch",
	    lifecycle: "surface",
	    droppable: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 3e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "notification-visible": Object.freeze({
	    priority: "visible",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "collection-visible": Object.freeze({
	    priority: "visible",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 1,
	    maxChallengeRetries: 1
	  }),
	  "resource-visible": Object.freeze({
	    priority: "visible",
	    lifecycle: "surface",
	    droppable: !1,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh", "no-store"),
	    defaultTimeoutMs: 3e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "surface-prefetch": Object.freeze({
	    priority: "prefetch",
	    lifecycle: "surface",
	    droppable: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "user-prefetch": Object.freeze({
	    priority: "prefetch",
	    lifecycle: "surface",
	    droppable: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "resource-prefetch": Object.freeze({
	    priority: "prefetch",
	    lifecycle: "surface",
	    droppable: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 3e4,
	    max429Retries: 0,
	    maxChallengeRetries: 1
	  }),
	  "nearby-prefetch": Object.freeze({
	    priority: "prefetch",
	    lifecycle: "topic",
	    droppable: !0,
	    /*
	     * 近视口宿主 Topic 可以先于普通后台任务,但仍必须让位于真实打开、交互与
	     * critical 请求。它与普通预取一样不能冻结 Reader 或拉起人工验证。
	     */
	    blockOnCloudflareChallenge: !1,
	    suppressAfterChallengeWait: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 2e4,
	    max429Retries: 0,
	    maxChallengeRetries: 0
	  }),
	  "background-prefetch": Object.freeze({
	    priority: "background",
	    lifecycle: "topic",
	    droppable: !0,
	    /*
	     * 自动预取和实时增强没有权力冻结全局 Reader 或拉起人工验证。
	     * 它们命中 cf-mitigated 时只结束自身,且不得在其他请求过盾后追发旧工作。
	     */
	    blockOnCloudflareChallenge: !1,
	    suppressAfterChallengeWait: !0,
	    defaultCacheMode: "default",
	    allowedCacheModes: cacheModes("default", "refresh"),
	    defaultTimeoutMs: 3e4,
	    max429Retries: 0,
	    maxChallengeRetries: 0
	  })
	});
	function nonEmptyToken(value, name) {
	  const token = String(value).trim();
	  if (!token) throw new Error(`${name} 不能为空`);
	  return token;
	}
	function encodedIdentity(identity) {
	  const entries = Object.entries(identity).sort(([left], [right]) => left.localeCompare(right));
	  if (!entries.length) throw new Error("request identity 不能为空");
	  return entries.map(([rawKey, rawValue]) => {
	    const key = nonEmptyToken(rawKey, "identity key"), value = typeof rawValue == "string" ? rawValue.trim() : String(rawValue);
	    if (!value) throw new Error(`identity ${key} 不能为空`);
	    return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
	  }).join("&");
	}
	function timeout(value, fallback) {
	  const resolved = value ?? fallback;
	  if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > 12e4)
	    throw new RangeError("timeoutMs 必须是 1..120000 的安全整数");
	  return resolved;
	}
	function requestProfileContract(profile) {
	  return PROFILES[profile];
	}
	function createRequestContract(profile, input) {
	  const contract = requestProfileContract(profile), cacheMode = input.cacheMode ?? contract.defaultCacheMode;
	  if (!contract.allowedCacheModes.includes(cacheMode))
	    throw new Error(`${profile} 不允许 cache mode ${cacheMode}`);
	  const cacheKey = `${nonEmptyToken(input.namespace, "request namespace")}?${encodedIdentity(input.identity)}`;
	  return Object.freeze({
	    ...contract,
	    profile,
	    cacheKey,
	    key: `${cacheKey}&cacheMode=${encodeURIComponent(cacheMode)}`,
	    cacheMode,
	    timeoutMs: timeout(input.timeoutMs, contract.defaultTimeoutMs)
	  });
	}
}, "a0920906dc4828164718274ee9f09feb1e460e175a7c29b309a6cad15bbc963d");

/* Source: lite/src/network/request-identities.ts */
runtime.register("src/network/request-identities.js", function(module, exports, require) {
	var request_identities_exports = {};
	__export(request_identities_exports, {
	  actionRequestIdentity: () => actionRequestIdentity,
	  collectionRequestIdentity: () => collectionRequestIdentity,
	  nestedRequestIdentity: () => nestedRequestIdentity,
	  notificationRequestIdentity: () => notificationRequestIdentity,
	  readRequestIdentity: () => readRequestIdentity,
	  resourceRequestIdentity: () => resourceRequestIdentity,
	  topicPostsRequestIdentity: () => topicPostsRequestIdentity,
	  topicRequestIdentity: () => topicRequestIdentity,
	  translationRequestIdentity: () => translationRequestIdentity,
	  userRequestIdentity: () => userRequestIdentity
	});
	module.exports = __toCommonJS(request_identities_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function token(value, name) {
	  const normalized = String(value).trim();
	  if (!normalized) throw new Error(`${name} 不能为空`);
	  return normalized;
	}
	function nonNegative(value, name) {
	  const numeric = Number(value);
	  if (!Number.isSafeInteger(numeric) || numeric < 0)
	    throw new RangeError(`${name} 必须是非负安全整数`);
	  return numeric;
	}
	function topicRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    topicId: (0, import_identifiers.discourseTopicId)(input.topicId),
	    operation: token(input.operation, "operation")
	  };
	  return input.postId !== void 0 && (identity.postId = (0, import_identifiers.discoursePostId)(input.postId)), input.postNumber !== void 0 && (identity.postNumber = (0, import_identifiers.discoursePostNumber)(input.postNumber)), input.cursor !== void 0 && (identity.cursor = (0, import_identifiers.discourseReplyCursor)(input.cursor)), Object.freeze(identity);
	}
	function topicPostsRequestIdentity(input) {
	  return Object.freeze({
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    topicId: (0, import_identifiers.discourseTopicId)(input.topicId),
	    postIds: (0, import_identifiers.discoursePostIds)(input.postIds).join(",")
	  });
	}
	function nestedRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    topicId: (0, import_identifiers.discourseTopicId)(input.topicId),
	    parentPostNumber: (0, import_identifiers.discoursePostNumber)(input.parentPostNumber),
	    after: (0, import_identifiers.discourseReplyCursor)(input.after)
	  };
	  return input.parentPostId !== void 0 && (identity.parentPostId = (0, import_identifiers.discoursePostId)(input.parentPostId)), Object.freeze(identity);
	}
	function notificationRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    group: token(input.group, "group"),
	    page: nonNegative(input.page, "page")
	  };
	  return input.variant !== void 0 && (identity.variant = token(input.variant, "variant")), Object.freeze(identity);
	}
	function collectionRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    collection: token(input.collection, "collection"),
	    page: nonNegative(input.page, "page")
	  };
	  return input.cursor !== void 0 && (identity.cursor = token(input.cursor, "cursor")), input.variant !== void 0 && (identity.variant = token(input.variant, "variant")), Object.freeze(identity);
	}
	function userRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    username: token(input.username, "username").replace(/^@/, "").toLocaleLowerCase(),
	    resource: token(input.resource, "resource")
	  };
	  return input.page !== void 0 && (identity.page = nonNegative(input.page, "page")), Object.freeze(identity);
	}
	function resourceRequestIdentity(input) {
	  return Object.freeze({
	    resourceId: token(input.resourceId, "resourceId"),
	    variant: token(input.variant, "variant")
	  });
	}
	function translationRequestIdentity(input) {
	  return Object.freeze({
	    provider: token(input.provider, "provider"),
	    textFingerprint: token(input.textFingerprint, "textFingerprint"),
	    sourceLanguage: token(input.sourceLanguage, "sourceLanguage"),
	    targetLanguage: token(input.targetLanguage, "targetLanguage")
	  });
	}
	function actionRequestIdentity(input) {
	  const identity = {
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    operation: token(input.operation, "operation"),
	    targetType: token(input.targetType, "targetType"),
	    targetId: token(input.targetId, "targetId")
	  };
	  return input.variant !== void 0 && (identity.variant = token(input.variant, "variant")), Object.freeze(identity);
	}
	function readRequestIdentity(input) {
	  return Object.freeze({
	    authScope: (0, import_identifiers.discourseAuthScope)(input.authScope),
	    topicId: (0, import_identifiers.discourseTopicId)(input.topicId),
	    postNumbers: (0, import_identifiers.discoursePostNumbers)(input.postNumbers).join(",")
	  });
	}
}, "0c95c749957127d654451875f452e302de72d614741b0cfe75b73c8a41238ea8");

/* Source: lite/src/network/request-observer.ts */
runtime.register("src/network/request-observer.js", function(module, exports, require) {
	var request_observer_exports = {};
	__export(request_observer_exports, {
	  RequestObserver: () => RequestObserver,
	  requestObservationType: () => requestObservationType
	});
	module.exports = __toCommonJS(request_observer_exports);
	var import_signal = require("../kernel/signal.js");
	function nonNegative(value, fallback = 0) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback;
	}
	function positiveInteger(value, fallback) {
	  const numeric = Math.trunc(Number(value));
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : fallback;
	}
	function requestUrl(href, baseHref) {
	  try {
	    return new URL(String(href), baseHref);
	  } catch {
	    return null;
	  }
	}
	function requestObservationHref(url) {
	  return url ? ["http:", "https:"].includes(url.protocol) ? `${url.origin}${url.pathname}`.slice(0, 512) : `${url.protocol.replace(":", "") || "本地"}资源` : "未知请求";
	}
	function requestObservationPath(url, baseOrigin) {
	  return url ? ["http:", "https:"].includes(url.protocol) ? `${url.origin === baseOrigin ? "" : url.host}${url.pathname || "/"}`.slice(0, 180) : `${url.protocol.replace(":", "") || "本地"}资源` : "未知请求";
	}
	const SENSITIVE_QUERY_KEY = /(?:auth|authorization|cookie|key|password|secret|signature|token)/i, DIAGNOSTIC_IDENTITY_KEYS = /* @__PURE__ */ new Set([
	  "after",
	  "collection",
	  "group",
	  "operation",
	  "page",
	  "parentPostId",
	  "parentPostNumber",
	  "postId",
	  "postNumber",
	  "postIds",
	  "postNumbers",
	  "resource",
	  "targetType",
	  "topicId"
	]);
	function requestObservationQueryShape(url) {
	  if (!url || !["http:", "https:"].includes(url.protocol) || !url.search) return "";
	  const counts = /* @__PURE__ */ new Map();
	  for (const [rawKey] of url.searchParams) {
	    const candidate = String(rawKey).trim(), key = SENSITIVE_QUERY_KEY.test(candidate) ? "credential" : /^[A-Za-z_][A-Za-z0-9_.\[\]-]{0,39}$/.test(candidate) ? candidate : "param";
	    counts.set(key, (counts.get(key) ?? 0) + 1);
	  }
	  const entries = [...counts].sort(([left], [right]) => left.localeCompare(right));
	  if (!entries.length) return "";
	  const visible = entries.slice(0, 8).map(([key, count]) => count > 1 ? `${key}×${count}` : key);
	  return entries.length > visible.length && visible.push("…"), `?${visible.join("&")}`.slice(0, 180);
	}
	function requestObservationIdentity(identity) {
	  if (!identity) return "";
	  const details = [];
	  for (const [key, rawValue] of Object.entries(identity).sort(([left], [right]) => left.localeCompare(right))) {
	    if (!DIAGNOSTIC_IDENTITY_KEYS.has(key)) continue;
	    const value = String(rawValue).trim();
	    if (value) {
	      if (key === "postIds" || key === "postNumbers") {
	        const count = value.split(",").filter(Boolean).length;
	        details.push(`${key}=${count}项`);
	        continue;
	      }
	      if (["collection", "group", "operation", "resource", "targetType"].includes(key)) {
	        const token = diagnosticCode(value);
	        token && details.push(`${key}=${token}`);
	        continue;
	      }
	      /^\d+$/.test(value) && details.push(`${key}=${value.slice(0, 16)}`);
	    }
	  }
	  return details.join(", ").slice(0, 220);
	}
	function diagnosticText(value, maximum = 220) {
	  return String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/(?:https?|chrome-extension):\/\/[^\s)]+/g, (raw) => {
	    try {
	      const url = new URL(raw);
	      return `${url.origin}${url.pathname}`;
	    } catch {
	      return raw.split(/[?#]/, 1)[0] ?? "";
	    }
	  }).trim().slice(0, maximum);
	}
	function diagnosticCode(value, fallback = "") {
	  const normalized = diagnosticText(value, 80);
	  return /^[A-Za-z0-9_.:-]+$/.test(normalized) ? normalized : fallback;
	}
	function diagnosticError(value) {
	  const normalized = diagnosticText(value, 80);
	  return normalized ? /^[A-Za-z0-9_.:-]+$/.test(normalized) || /^(?:Failed to fetch|Load failed|NetworkError)$/i.test(normalized) ? normalized : "request-failed" : "";
	}
	const CANCELLATION_CODES = /* @__PURE__ */ new Set([
	  "AbortError",
	  "cancelled",
	  "context-close",
	  "context-closed",
	  "priority-upgrade",
	  "signal",
	  "topic-close",
	  "topic-switch",
	  "viewport-change"
	]);
	function isCancellationCode(value) {
	  return CANCELLATION_CODES.has(value);
	}
	function requestObservationType(href, options) {
	  const url = requestUrl(href, options.baseHref), path = String(url?.pathname ?? "").toLowerCase(), host = String(url?.hostname ?? "").toLowerCase(), initiator = String(options.initiatorType ?? "").toLowerCase(), method = String(options.method ?? "GET").toUpperCase();
	  return /\/posts\/\d+\/replies(?:\.json)?$/.test(path) ? "nested" : /\/user_avatar\/|\/letter_avatar\//.test(path) || /avatar/.test(host) ? "avatar" : /\/(?:uploads|optimized)\//.test(path) || /\.(?:avif|gif|jpe?g|png|webp|svg|mp4|webm|mp3|ogg)(?:$|\/)/.test(path) ? "media" : /\/bookmarks?(?:\/|\.|$)|remove_bookmarks/.test(path) ? "bookmark" : /\/notifications?(?:\/|\.|$)/.test(path) ? "notification" : /\/message-bus(?:\/|$)/.test(path) ? "realtime" : /\/presence\//.test(path) ? "presence" : /\/search(?:\/|\.|$)|\/filter(?:\/|\.|$)/.test(path) ? "search" : /topic-timings|\/timings(?:\/|\.|$)/.test(path) ? "read" : /\/u\/|\/directory_items|\/session\/current|\/category-experts\//.test(path) ? "user" : /post_actions|user_actions|discourse-reactions|\/boosts?(?:\/|\.|$)|\/emojis\.json$/.test(path) ? "reaction" : /\/t\/|\/posts(?:\/|\.|$)|\/posts\/by_number\//.test(path) ? "topic" : ["GET", "HEAD"].includes(method) ? ["img", "image", "video", "audio"].includes(initiator) ? "media" : ["css", "link", "script", "font"].includes(initiator) ? "asset" : "other" : "reaction";
	}
	class RequestObserver {
	  changes = new import_signal.Signal();
	  #baseHref;
	  #retentionMs;
	  #maxEntries;
	  #now;
	  #events = [];
	  #active = /* @__PURE__ */ new Map();
	  #sequence = 0;
	  #revision = 0;
	  constructor(options) {
	    const base = new URL(options.baseHref);
	    base.username = "", base.password = "", base.search = "", base.hash = "", this.#baseHref = base.href, this.#retentionMs = positiveInteger(options.retentionMs, 5 * 6e4), this.#maxEntries = positiveInteger(options.maxEntries, 500), this.#now = options.now ?? Date.now;
	  }
	  get snapshot() {
	    return this.#snapshot();
	  }
	  begin(input) {
	    const observedAt = this.#now(), requestedStart = nonNegative(input.startedAt, observedAt), requestedQueue = nonNegative(input.queuedAt, requestedStart), controlReason = diagnosticCode(input.controlReason), phase = controlReason ? "cancelled" : input.phase ?? "running", queuedAt = Math.min(requestedStart, requestedQueue), startedAt = phase === "queued" ? queuedAt : Math.max(queuedAt, requestedStart), permittedAt = phase === "queued" ? queuedAt : Math.min(
	      startedAt,
	      Math.max(queuedAt, nonNegative(input.permittedAt, startedAt))
	    ), method = String(input.method ?? "GET").trim().toUpperCase().slice(0, 16) || "GET", url = requestUrl(input.href, this.#baseHref), normalizedHref = requestObservationHref(url), baseOrigin = new URL(this.#baseHref).origin, event = Object.freeze({
	      id: ++this.#sequence,
	      href: normalizedHref,
	      path: requestObservationPath(url, baseOrigin),
	      queryShape: requestObservationQueryShape(url),
	      method,
	      transport: input.transport,
	      source: input.source,
	      phase,
	      type: input.type ?? requestObservationType(input.href, {
	        baseHref: this.#baseHref,
	        method,
	        initiatorType: input.transport
	      }),
	      sameOrigin: !!(url && ["http:", "https:"].includes(url.protocol) && url.origin === baseOrigin),
	      queuedAt,
	      permittedAt,
	      startedAt,
	      endedAt: controlReason ? startedAt : 0,
	      permitWait: permittedAt - queuedAt,
	      dispatchDuration: startedAt - permittedAt,
	      duration: 0,
	      priority: input.priority ?? null,
	      attempt: positiveInteger(input.attempt, 1),
	      recoveryProbe: input.recoveryProbe === !0,
	      waitReason: diagnosticCode(input.waitReason),
	      callSite: diagnosticText(input.callSite),
	      controlReason,
	      logicalId: diagnosticCode(input.logicalId),
	      profile: diagnosticCode(input.profile),
	      namespace: diagnosticCode(input.namespace),
	      lane: diagnosticCode(input.lane),
	      cacheMode: diagnosticCode(input.cacheMode),
	      identity: requestObservationIdentity(input.identity),
	      joinedConsumers: 0,
	      promoted: !1,
	      max429Retries: Math.trunc(nonNegative(input.max429Retries)),
	      maxChallengeRetries: Math.trunc(nonNegative(input.maxChallengeRetries)),
	      blockOnCloudflareChallenge: typeof input.blockOnCloudflareChallenge == "boolean" ? input.blockOnCloudflareChallenge : null,
	      suppressAfterChallengeWait: input.suppressAfterChallengeWait === !0,
	      droppable: typeof input.droppable == "boolean" ? input.droppable : null,
	      decision: controlReason,
	      pending: !controlReason,
	      status: controlReason ? 0 : null,
	      cloudflareMitigated: !1,
	      size: 0,
	      error: "",
	      rateLimitCode: "",
	      retryAfter: "",
	      serverLimit: "",
	      serverRemaining: "",
	      serverReset: "",
	      resourceTimed: !1
	    });
	    return this.#insert(event), event.pending && this.#active.set(event.id, event), this.#prune(startedAt), this.#publish(), event.id;
	  }
	  markStarted(input) {
	    const current = this.#active.get(input.id);
	    if (!current || current.phase !== "queued") return !1;
	    const startedAt = Math.max(
	      current.queuedAt,
	      nonNegative(input.startedAt, this.#now())
	    ), queuedAt = Math.min(
	      startedAt,
	      nonNegative(input.queuedAt, current.queuedAt)
	    ), permittedAt = Math.min(
	      startedAt,
	      Math.max(queuedAt, nonNegative(input.permittedAt, startedAt))
	    ), running = Object.freeze({
	      ...current,
	      phase: "running",
	      queuedAt,
	      permittedAt,
	      startedAt,
	      permitWait: permittedAt - queuedAt,
	      dispatchDuration: startedAt - permittedAt,
	      priority: input.priority === void 0 ? current.priority : input.priority,
	      recoveryProbe: input.recoveryProbe === !0,
	      waitReason: diagnosticCode(input.waitReason)
	    });
	    return this.#replace(current, running), this.#active.set(input.id, running), this.#prune(startedAt), this.#publish(), !0;
	  }
	  cancel(id, input) {
	    const current = this.#active.get(id);
	    if (!current || current.phase !== "queued") return !1;
	    const endedAt = Math.max(
	      current.queuedAt,
	      nonNegative(input.endedAt, this.#now())
	    ), reason = diagnosticCode(input.reason, "cancelled"), cancelled = Object.freeze({
	      ...current,
	      phase: "cancelled",
	      permittedAt: endedAt,
	      startedAt: endedAt,
	      endedAt,
	      permitWait: endedAt - current.queuedAt,
	      dispatchDuration: 0,
	      duration: 0,
	      waitReason: reason,
	      controlReason: reason,
	      decision: reason,
	      pending: !1,
	      status: 0
	    });
	    return this.#replace(current, cancelled), this.#active.delete(id), this.#prune(endedAt), this.#publish(), !0;
	  }
	  matchActive(input) {
	    const href = requestObservationHref(requestUrl(input.href, this.#baseHref)), method = String(input.method ?? "GET").trim().toUpperCase() || "GET", startedAt = nonNegative(input.startedAt, this.#now()), toleranceMs = nonNegative(input.toleranceMs, 250);
	    return [...this.#active.values()].filter(
	      (event) => event.phase === "running" && event.href === href && event.method === method && !input.excludedIds?.has(event.id) && (input.source === void 0 || event.source === input.source) && Math.abs(event.startedAt - startedAt) <= toleranceMs
	    ).sort((left, right) => Math.abs(left.startedAt - startedAt) - Math.abs(right.startedAt - startedAt))[0]?.id ?? null;
	  }
	  finish(id, input = {}) {
	    const current = this.#active.get(id);
	    if (!current) return !1;
	    const endedAt = Math.max(current.startedAt, nonNegative(input.endedAt, this.#now())), error = diagnosticError(input.error), cancelled = isCancellationCode(error), controlledCancellation = cancelled && error !== "AbortError", completed = Object.freeze({
	      ...current,
	      phase: cancelled ? "cancelled" : "finished",
	      endedAt,
	      duration: endedAt - current.startedAt,
	      pending: !1,
	      status: cancelled ? 0 : input.status === void 0 ? current.status : Math.trunc(nonNegative(input.status)),
	      cloudflareMitigated: input.cloudflareMitigated === !0,
	      size: nonNegative(input.size, current.size),
	      error,
	      controlReason: controlledCancellation ? error : current.controlReason,
	      rateLimitCode: diagnosticCode(input.rateLimitCode),
	      retryAfter: diagnosticText(input.retryAfter, 80),
	      serverLimit: diagnosticText(input.serverLimit, 80),
	      serverRemaining: diagnosticText(input.serverRemaining, 80),
	      serverReset: diagnosticText(input.serverReset, 80),
	      decision: diagnosticCode(input.decision, current.decision)
	    }), index = this.#events.findIndex((event) => event.id === id);
	    return index >= 0 && (this.#events[index] = completed), this.#active.delete(id), this.#prune(endedAt), this.#publish(), !0;
	  }
	  update(id, input) {
	    const current = this.#events.find((event) => event.id === id);
	    if (!current) return !1;
	    const next = Object.freeze({
	      ...current,
	      priority: input.priority === void 0 ? current.priority : input.priority,
	      joinedConsumers: input.joinedConsumers === void 0 ? current.joinedConsumers : Math.trunc(nonNegative(input.joinedConsumers)),
	      promoted: current.promoted || input.promoted === !0,
	      max429Retries: input.max429Retries === void 0 ? current.max429Retries : Math.trunc(nonNegative(input.max429Retries)),
	      maxChallengeRetries: input.maxChallengeRetries === void 0 ? current.maxChallengeRetries : Math.trunc(nonNegative(input.maxChallengeRetries)),
	      droppable: input.droppable === void 0 ? current.droppable : input.droppable,
	      decision: input.decision === void 0 ? current.decision : diagnosticCode(input.decision, current.decision)
	    }), index = this.#events.findIndex((event) => event.id === id);
	    return index < 0 ? !1 : (this.#events[index] = next, current.pending && this.#active.set(id, next), this.#publish(), !0);
	  }
	  recordResource(input) {
	    const initiator = String(input.initiatorType ?? "").toLowerCase(), resourceUrl = requestUrl(input.href, this.#baseHref);
	    if (!resourceUrl || !["http:", "https:"].includes(resourceUrl.protocol)) return 0;
	    const normalizedHref = requestObservationHref(
	      resourceUrl
	    ), resourceDuration = Math.max(0, input.endedAt - input.startedAt), repeated = this.#events.find(
	      (event) => event.resourceTimed && event.href === normalizedHref && Math.abs(event.startedAt - input.startedAt) <= 0.1 && Math.abs(event.duration - resourceDuration) <= 0.1
	    );
	    if (repeated) return repeated.id;
	    if (["fetch", "xmlhttprequest"].includes(initiator)) {
	      const match = this.#events.filter(
	        (event) => !event.resourceTimed && event.href === normalizedHref && (event.transport === initiator || initiator === "xmlhttprequest" && event.transport === "scheduler") && Math.abs(event.startedAt - input.startedAt) <= 100
	      ).sort((left, right) => Math.abs(left.startedAt - input.startedAt) - Math.abs(right.startedAt - input.startedAt))[0];
	      if (match) {
	        const enriched = Object.freeze({
	          ...match,
	          duration: resourceDuration,
	          size: nonNegative(input.size, match.size),
	          resourceTimed: !0
	        }), index2 = this.#events.findIndex((event) => event.id === match.id);
	        return index2 >= 0 && (this.#events[index2] = enriched), match.pending && this.#active.set(match.id, enriched), this.#publish(), match.id;
	      }
	    }
	    const id = this.begin({
	      href: input.href,
	      transport: "resource",
	      source: "browser",
	      startedAt: input.startedAt,
	      type: requestObservationType(input.href, {
	        baseHref: this.#baseHref,
	        ...input.initiatorType === void 0 ? {} : { initiatorType: input.initiatorType }
	      }),
	      callSite: input.initiatorType ? `${input.initiatorType} 资源加载` : "浏览器资源加载"
	    });
	    this.finish(id, input);
	    const index = this.#events.findIndex((event) => event.id === id), completed = this.#events[index];
	    return index >= 0 && completed && (this.#events[index] = Object.freeze({ ...completed, resourceTimed: !0 }), this.#publish()), id;
	  }
	  clearCompleted() {
	    for (let index = this.#events.length - 1; index >= 0; index -= 1)
	      this.#events[index].pending || this.#events.splice(index, 1);
	    this.#publish();
	  }
	  #insert(event) {
	    let low = 0, high = this.#events.length;
	    for (; low < high; ) {
	      const middle = Math.floor((low + high) / 2), current = this.#events[middle];
	      current.queuedAt < event.queuedAt || current.queuedAt === event.queuedAt && current.id <= event.id ? low = middle + 1 : high = middle;
	    }
	    this.#events.splice(low, 0, event);
	  }
	  #replace(current, next) {
	    const index = this.#events.findIndex((event) => event.id === current.id);
	    index < 0 || (this.#events.splice(index, 1), this.#insert(next));
	  }
	  #prune(at) {
	    const cutoff = at - this.#retentionMs;
	    for (let index = this.#events.length - 1; index >= 0; index -= 1) {
	      const event = this.#events[index], retainedAt = event.endedAt || event.startedAt;
	      !event.pending && retainedAt < cutoff && this.#events.splice(index, 1);
	    }
	    if (this.#events.length <= this.#maxEntries) return;
	    let overflow = this.#events.length - this.#maxEntries;
	    for (let index = 0; index < this.#events.length && overflow > 0; ) {
	      if (this.#events[index].pending) {
	        index += 1;
	        continue;
	      }
	      this.#events.splice(index, 1), overflow -= 1;
	    }
	  }
	  #publish() {
	    this.#revision += 1, this.changes.emit(this.#snapshot());
	  }
	  #snapshot() {
	    const queued = [...this.#active.values()].filter(
	      (event) => event.phase === "queued"
	    ).length, running = this.#active.size - queued;
	    return Object.freeze({
	      revision: this.#revision,
	      queued,
	      running,
	      active: running,
	      completed: this.#events.length - this.#active.size,
	      events: Object.freeze([...this.#events])
	    });
	  }
	}
}, "64374a589ac7e6bc0726c98e064bcb3965bf317bd095010e034716e8d8cd2222");

/* Source: lite/src/network/request-rate-limit-policy.ts */
runtime.register("src/network/request-rate-limit-policy.js", function(module, exports, require) {
	var request_rate_limit_policy_exports = {};
	__export(request_rate_limit_policy_exports, {
	  RequestRateLimitPolicy: () => RequestRateLimitPolicy,
	  endpointRequestIdentity: () => endpointRequestIdentity,
	  parseRetryAfterMs: () => parseRetryAfterMs,
	  rateLimitWindowFromCode: () => rateLimitWindowFromCode
	});
	module.exports = __toCommonJS(request_rate_limit_policy_exports);
	function rateLimitWindowFromCode(value) {
	  const short = /10[_-]?(?:secs?|seconds?)/i.test(String(value ?? "")), long = /60[_-]?(?:secs?|seconds?)|minute/i.test(String(value ?? ""));
	  return short && long ? "10s+60s" : short ? "10s" : long ? "60s" : "unknown";
	}
	function positiveFinite(value, name) {
	  if (!Number.isFinite(value) || value <= 0)
	    throw new RangeError(`${name} 必须是正有限数值`);
	  return value;
	}
	function positiveInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function usableRetryAfter(value, now) {
	  const raw = String(value ?? "").trim();
	  if (!raw) return !1;
	  const seconds = Number(raw);
	  return Number.isFinite(seconds) ? seconds > 0 : Date.parse(raw) > now;
	}
	function rateLimitWindowWaitMs(window) {
	  return window === "10s" ? 1e4 : window === "60s" || window === "10s+60s" ? 6e4 : 0;
	}
	function parseRetryAfterMs(value, options) {
	  const minMs = positiveFinite(options.minMs ?? 1e3, "minMs"), maxMs = positiveFinite(options.maxMs ?? 6e4, "maxMs");
	  if (maxMs < minMs) throw new RangeError("maxMs 不能小于 minMs");
	  const fallbackMs = positiveFinite(options.fallbackMs, "fallbackMs"), raw = String(value ?? "").trim();
	  let waitMs = 0;
	  if (raw) {
	    const seconds = Number(raw);
	    waitMs = Number.isFinite(seconds) ? seconds * 1e3 : Math.max(0, Date.parse(raw) - options.now);
	  }
	  return waitMs || (waitMs = fallbackMs), Math.max(minMs, Math.min(maxMs, waitMs));
	}
	function endpointRequestIdentity(input, method = "GET", baseUrl) {
	  let url;
	  try {
	    url = input instanceof URL ? new URL(input.href) : new URL(input, baseUrl);
	  } catch {
	    const invalid = `${String(method || "GET").toUpperCase()}:invalid:${String(input).slice(0, 160)}`;
	    return Object.freeze({ fingerprint: invalid, route: invalid });
	  }
	  const normalizedMethod = String(method || "GET").toUpperCase(), params = [...url.searchParams.entries()].filter(([key]) => key !== "_ldp_retry"), identity = (routeOnly) => {
	    const path = routeOnly ? url.pathname.replace(/\b\d+\b/g, ":id").replace(/[0-9a-f]{8}-[0-9a-f-]{27,}/gi, ":uuid") : url.pathname, sorted = params.map(([key, value]) => routeOnly ? [key, ""] : [key, value]).sort(([leftKey, leftValue], [rightKey, rightValue]) => leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue)), query = routeOnly ? [...new Set(sorted.map(([key]) => encodeURIComponent(key)))].join("&") : sorted.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
	    return `${normalizedMethod}:${url.origin}${path}${query ? `?${query}` : ""}`;
	  };
	  return Object.freeze({ fingerprint: identity(!1), route: identity(!0) });
	}
	class RequestRateLimitPolicy {
	  #options;
	  #unknownEvidence = [];
	  constructor(options) {
	    const retryAfterMinMs = positiveFinite(options.retryAfterMinMs ?? 1e3, "retryAfterMinMs"), retryAfterMaxMs = positiveFinite(
	      options.retryAfterMaxMs ?? 6e4,
	      "retryAfterMaxMs"
	    );
	    if (retryAfterMaxMs < retryAfterMinMs)
	      throw new RangeError("retryAfterMaxMs 不能小于 retryAfterMinMs");
	    this.#options = {
	      evidenceWindowMs: positiveFinite(options.evidenceWindowMs, "evidenceWindowMs"),
	      maxEndpointEntries: positiveInteger(options.maxEndpointEntries, "maxEndpointEntries"),
	      retryAfterFallbackMs: positiveFinite(options.retryAfterFallbackMs, "retryAfterFallbackMs"),
	      retryAfterMinMs,
	      retryAfterMaxMs,
	      now: options.now ?? Date.now,
	      ...options.baseUrl === void 0 ? {} : { baseUrl: options.baseUrl }
	    };
	  }
	  noteRateLimit(observation) {
	    const at = this.#options.now();
	    this.#prune(at);
	    const identity = endpointRequestIdentity(
	      observation.input,
	      observation.method,
	      this.#options.baseUrl
	    ), hasAuthoritativeRetryAfter = usableRetryAfter(
	      observation.retryAfter,
	      at
	    ), retryAfterMs = parseRetryAfterMs(observation.retryAfter, {
	      now: at,
	      fallbackMs: this.#options.retryAfterFallbackMs,
	      minMs: this.#options.retryAfterMinMs,
	      maxMs: this.#options.retryAfterMaxMs
	    }), authoritativeWindow = observation.knownGlobalWindow ? observation.globalWindow ?? "unknown" : "unknown", waitMs = hasAuthoritativeRetryAfter ? retryAfterMs : Math.max(retryAfterMs, rateLimitWindowWaitMs(authoritativeWindow)), corroboratedGlobal = observation.knownGlobalWindow || this.#unknownEvidence.some(
	      (event) => event.route !== identity.route && event.at >= at - this.#options.evidenceWindowMs
	    );
	    return this.#unknownEvidence.push(Object.freeze({ at, route: identity.route })), this.#prune(at), this.#decision(
	      corroboratedGlobal ? "global" : "endpoint",
	      waitMs,
	      at + waitMs,
	      identity,
	      observation.globalWindow ?? "unknown",
	      hasAuthoritativeRetryAfter || observation.knownGlobalWindow
	    );
	  }
	  identity(input, method = "GET") {
	    return endpointRequestIdentity(input, method, this.#options.baseUrl);
	  }
	  reset() {
	    this.#unknownEvidence.length = 0;
	  }
	  #decision(scope, waitMs, retryAt, identity, window = "unknown", authoritative = !1) {
	    return Object.freeze({
	      scope,
	      waitMs,
	      retryAt,
	      ...identity,
	      window,
	      authoritative
	    });
	  }
	  #prune(at) {
	    const evidenceCutoff = at - this.#options.evidenceWindowMs;
	    for (; this.#unknownEvidence.length && this.#unknownEvidence[0].at < evidenceCutoff; )
	      this.#unknownEvidence.shift();
	    this.#unknownEvidence.length > this.#options.maxEndpointEntries && this.#unknownEvidence.splice(
	      0,
	      this.#unknownEvidence.length - this.#options.maxEndpointEntries
	    );
	  }
	}
}, "697bfe4cf60edafb78614a1526813cf427a545b7c16b54506866af6ad0c512b4");

/* Source: lite/src/network/request-scheduler.ts */
runtime.register("src/network/request-scheduler.js", function(module, exports, require) {
	var request_scheduler_exports = {};
	__export(request_scheduler_exports, {
	  RequestControlError: () => RequestControlError,
	  RequestScheduler: () => RequestScheduler,
	  RequestStartDeferredError: () => RequestStartDeferredError,
	  RequestTimeoutError: () => RequestTimeoutError
	});
	module.exports = __toCommonJS(request_scheduler_exports);
	const REQUEST_LANES = Object.freeze([
	  "control",
	  "topic-batch",
	  "nested-replies",
	  "user-card",
	  "translation",
	  "standard"
	]), LANE_CONCURRENCY_CAP = Object.freeze({
	  control: 1,
	  /* 当前 Topic 与两批近视口预热可以并行;全局 permit 仍统一限流。 */
	  "topic-batch": 3,
	  /* replies.json 最多并行两个父楼;全局 permit 仍统一约束启动间隔与额度。 */
	  "nested-replies": 2,
	  "user-card": 2,
	  /* 五路预加载之外为滚动到眼前的正文预留一路;共享 permit 仍限制启动。 */
	  translation: 6,
	  standard: 1
	}), PRIORITY_WEIGHT = Object.freeze({
	  critical: 0,
	  interactive: 1,
	  nested: 2,
	  visible: 3,
	  prefetch: 4,
	  background: 5
	});
	class RequestControlError extends Error {
	  code;
	  constructor(code) {
	    super(code), this.name = "AbortError", this.code = code;
	  }
	}
	class RequestTimeoutError extends Error {
	  constructor() {
	    super("请求超时"), this.name = "TimeoutError";
	  }
	}
	class RequestStartDeferredError extends Error {
	  waitMs;
	  reason;
	  constructor(waitMs, reason) {
	    super(reason || "request-start-deferred"), this.name = "RequestStartDeferredError", this.waitMs = Math.max(1, Math.ceil(Number(waitMs) || 0)), this.reason = String(reason || "deferred");
	  }
	}
	function positiveInteger(value, name) {
	  if (!Number.isSafeInteger(value) || value < 1)
	    throw new RangeError(`${name} 必须是正安全整数`);
	  return value;
	}
	function normalizedKey(value) {
	  const key = String(value).trim();
	  if (!key) throw new Error("request key 不能为空");
	  return key;
	}
	function abortReason(signal, fallback) {
	  return signal.reason ?? new RequestControlError(fallback);
	}
	class RequestScheduler {
	  #maxConcurrent;
	  #queueLimit;
	  #defaultTimeoutMs;
	  #now;
	  #startGate;
	  #onInternalError;
	  #queue = [];
	  #tasksByKey = /* @__PURE__ */ new Map();
	  #activeByLane = /* @__PURE__ */ new Map();
	  #activeCount = 0;
	  #sequence = 0;
	  #pumpQueued = !1;
	  #permitPending = !1;
	  #permitLane = null;
	  #permitTask = null;
	  #deferredPumpTimer = null;
	  #deferredPumpAt = 0;
	  #disposed = !1;
	  constructor(options) {
	    this.#maxConcurrent = positiveInteger(options.maxConcurrent, "maxConcurrent"), this.#queueLimit = positiveInteger(options.queueLimit, "queueLimit"), this.#defaultTimeoutMs = positiveInteger(options.defaultTimeoutMs, "defaultTimeoutMs"), this.#now = options.now ?? Date.now, this.#startGate = options.startGate, this.#onInternalError = options.onInternalError ?? (() => {
	    }), options.scope?.add(() => this.destroy());
	  }
	  schedule(options, operation) {
	    const key = normalizedKey(options.key), priority = options.priority ?? "visible", lane = options.lane ?? "standard";
	    if (this.#disposed)
	      return Promise.reject(new RequestControlError("context-closed"));
	    const existing = this.#tasksByKey.get(key);
	    if (existing && !existing.externalSignal?.aborted)
	      return options.droppable !== !0 && (existing.droppable = !1), this.#promoteTask(existing, priority), existing.promise;
	    if (options.signal?.aborted)
	      return Promise.reject(abortReason(options.signal, "signal"));
	    if (this.#waitingCount() >= this.#queueLimit) {
	      const evicted = options.droppable === !0 ? null : this.#evictLowerPriorityDroppable(priority);
	      if (!evicted && !this.#preemptDroppablePermit({
	        priority,
	        lane,
	        droppable: options.droppable === !0
	      }))
	        return Promise.reject(new RequestControlError("queue-limit"));
	      evicted && this.#finish(evicted, new RequestControlError("queue-limit"));
	    }
	    let resolve, reject;
	    const promise = new Promise((done, fail) => {
	      resolve = done, reject = fail;
	    }), queuedAt = this.#now(), task = {
	      key,
	      priority,
	      lane,
	      rateLimitRoute: String(options.rateLimitRoute ?? "").trim(),
	      sequence: this.#sequence++,
	      queuedAt,
	      timeoutMs: positiveInteger(
	        options.timeoutMs ?? this.#defaultTimeoutMs,
	        "timeoutMs"
	      ),
	      externalSignal: options.signal,
	      operation,
	      onStart: options.onStart,
	      droppable: options.droppable === !0,
	      promise,
	      resolve,
	      reject,
	      state: "queued",
	      controller: null,
	      externalAbort: null,
	      permitRestart: !1,
	      notBefore: queuedAt,
	      deferredWaitReason: ""
	    };
	    return task.externalSignal && (task.externalAbort = () => {
	      task.state === "queued" ? (this.#removeQueued(task), this.#finish(
	        task,
	        abortReason(task.externalSignal, "signal")
	      )) : task.controller?.abort(abortReason(task.externalSignal, "signal"));
	    }, task.externalSignal.addEventListener(
	      "abort",
	      task.externalAbort,
	      { once: !0 }
	    )), this.#tasksByKey.set(key, task), this.#queue.push(task), this.#preemptDroppablePermit(task), this.#queuePump(), promise;
	  }
	  cancelQueued(key) {
	    const task = this.#tasksByKey.get(String(key));
	    return !task || task.state !== "queued" ? !1 : (this.#removeQueued(task), this.#finish(task, new RequestControlError("cancelled")), !0);
	  }
	  /**
	   * 原地更新只影响尚未启动的任务;已经取得 permit 的请求自然完成。
	   *
	   * 设置切换不得重建 scheduler,否则会丢失 single-flight、排队顺序和当前请求。
	   */
	  applyRuntimePolicy(policy) {
	    this.#disposed || (this.#maxConcurrent = positiveInteger(
	      policy.maxConcurrent,
	      "maxConcurrent"
	    ), this.#queuePump());
	  }
	  promoteQueued(key, priority, droppable) {
	    const task = this.#tasksByKey.get(String(key));
	    return task ? (droppable === !1 && (task.droppable = !1), this.#promoteTask(task, priority)) : !1;
	  }
	  snapshot() {
	    return this.#sortQueue(), Object.freeze({
	      active: this.#activeCount,
	      queued: this.#queue.length + (this.#permitPending ? 1 : 0),
	      maxConcurrent: this.#maxConcurrent,
	      queueLimit: this.#queueLimit,
	      disposed: this.#disposed,
	      queuedKeys: Object.freeze(this.#queue.map((task) => task.key)),
	      activeByLane: this.#laneCounts((lane) => this.#activeByLane.get(lane) ?? 0),
	      queuedByLane: this.#laneCounts((lane) => this.#queue.reduce(
	        (total, task) => total + (task.lane === lane ? 1 : 0),
	        this.#permitLane === lane ? 1 : 0
	      ))
	    });
	  }
	  destroy() {
	    if (!this.#disposed) {
	      this.#disposed = !0, this.#deferredPumpTimer !== null && (clearTimeout(this.#deferredPumpTimer), this.#deferredPumpTimer = null, this.#deferredPumpAt = 0);
	      for (const task of this.#queue.splice(0))
	        this.#finish(task, new RequestControlError("context-closed"));
	      for (const task of this.#tasksByKey.values())
	        (task.state === "active" || task.state === "permit") && task.controller?.abort(new RequestControlError("context-closed"));
	    }
	  }
	  #queuePump() {
	    this.#pumpQueued || this.#disposed || (this.#pumpQueued = !0, queueMicrotask(() => {
	      this.#pumpQueued = !1, this.#pump();
	    }));
	  }
	  #pump() {
	    if (!(this.#disposed || this.#permitPending))
	      for (this.#sortQueue(); this.#activeCount < this.#maxConcurrent && this.#queue.length; ) {
	        const now = this.#now(), nextIndex = this.#queue.findIndex(
	          (candidate) => candidate.notBefore <= now && this.#taskCanStart(candidate)
	        );
	        if (nextIndex < 0) {
	          this.#scheduleDeferredPump(now);
	          return;
	        }
	        const [task] = this.#queue.splice(nextIndex, 1);
	        if (!task) return;
	        if (task.externalSignal?.aborted) {
	          this.#finish(task, abortReason(task.externalSignal, "signal"));
	          continue;
	        }
	        const controller = new AbortController();
	        if (task.controller = controller, !this.#startGate) {
	          this.#run(task, controller, null, task.queuedAt);
	          continue;
	        }
	        task.state = "permit", this.#permitPending = !0, this.#permitLane = task.lane, this.#permitTask = task;
	        const permitPromise = Promise.resolve().then(() => this.#startGate.acquire({
	          key: task.key,
	          priority: task.priority,
	          lane: task.lane,
	          ...task.rateLimitRoute ? { rateLimitRoute: task.rateLimitRoute } : {},
	          signal: controller.signal
	        })).then((permit) => {
	          if (!permit || typeof permit.release != "function")
	            throw new Error("start gate 返回了无效 permit");
	          if (controller.signal.aborted)
	            throw this.#releasePermit(permit), abortReason(controller.signal, "signal");
	          return permit;
	        }), permitAbort = new Promise((_resolve, reject) => {
	          controller.signal.addEventListener(
	            "abort",
	            () => reject(abortReason(controller.signal, "signal")),
	            { once: !0 }
	          );
	        });
	        Promise.race([permitPromise, permitAbort]).then((permit) => {
	          this.#clearPermitTask(task);
	          try {
	            if (this.#disposed || controller.signal.aborted)
	              throw this.#releasePermit(permit), abortReason(controller.signal, "context-closed");
	            this.#run(task, controller, permit, this.#now());
	          } catch (error) {
	            task.state !== "done" && this.#finish(task, error);
	          } finally {
	            this.#queuePump();
	          }
	        }, (error) => {
	          this.#clearPermitTask(task), task.permitRestart && !this.#disposed && !task.externalSignal?.aborted ? (task.permitRestart = !1, task.state = "queued", task.controller = null, this.#queue.push(task)) : error instanceof RequestStartDeferredError && !this.#disposed && !task.externalSignal?.aborted ? (task.state = "queued", task.controller = null, task.notBefore = this.#now() + error.waitMs, task.deferredWaitReason = error.reason, this.#queue.push(task)) : task.state !== "done" && this.#finish(
	            task,
	            this.#disposed ? new RequestControlError("context-closed") : error
	          ), this.#queuePump();
	        });
	        break;
	      }
	  }
	  #scheduleDeferredPump(now) {
	    let nextAt = Number.POSITIVE_INFINITY;
	    for (const task of this.#queue)
	      task.notBefore > now && (nextAt = Math.min(nextAt, task.notBefore));
	    Number.isFinite(nextAt) && (this.#deferredPumpTimer !== null && this.#deferredPumpAt <= nextAt || (this.#deferredPumpTimer !== null && clearTimeout(this.#deferredPumpTimer), this.#deferredPumpAt = nextAt, this.#deferredPumpTimer = setTimeout(() => {
	      this.#deferredPumpTimer = null, this.#deferredPumpAt = 0, this.#queuePump();
	    }, Math.max(0, nextAt - now))));
	  }
	  #sortQueue() {
	    this.#queue.sort(
	      (left, right) => PRIORITY_WEIGHT[left.priority] - PRIORITY_WEIGHT[right.priority] || left.sequence - right.sequence
	    );
	  }
	  #promoteTask(task, priority) {
	    return task.state !== "queued" && task.state !== "permit" || PRIORITY_WEIGHT[priority] >= PRIORITY_WEIGHT[task.priority] ? !1 : (task.priority = priority, task.state === "permit" ? (task.permitRestart = !0, task.controller?.abort(new RequestControlError("cancelled"))) : this.#queuePump(), !0);
	  }
	  #preemptDroppablePermit(incoming) {
	    const waiting = this.#permitTask, controller = waiting?.controller;
	    return incoming.droppable || !waiting || !waiting.droppable || waiting.state !== "permit" || !controller || controller.signal.aborted || this.#activeCount >= this.#maxConcurrent || !this.#taskCanStart(incoming) || PRIORITY_WEIGHT[incoming.priority] >= PRIORITY_WEIGHT[waiting.priority] ? !1 : (waiting.permitRestart = !1, controller.abort(new RequestControlError("cancelled")), !0);
	  }
	  #clearPermitTask(task) {
	    this.#permitTask === task && (this.#permitPending = !1, this.#permitLane = null, this.#permitTask = null);
	  }
	  #waitingCount() {
	    return this.#queue.length + (this.#permitPending ? 1 : 0);
	  }
	  #taskCanStart(task) {
	    let cap = Math.min(
	      this.#maxConcurrent,
	      LANE_CONCURRENCY_CAP[task.lane]
	    );
	    return task.lane === "topic-batch" && task.priority === "background" && (cap = Math.min(cap, 1)), (this.#activeByLane.get(task.lane) ?? 0) < cap;
	  }
	  #laneCounts(read) {
	    return Object.freeze(Object.fromEntries(
	      REQUEST_LANES.map((lane) => [lane, read(lane)])
	    ));
	  }
	  #evictLowerPriorityDroppable(incomingPriority) {
	    const incomingWeight = PRIORITY_WEIGHT[incomingPriority], candidate = this.#queue.filter((task) => task.droppable && PRIORITY_WEIGHT[task.priority] > incomingWeight).sort((left, right) => PRIORITY_WEIGHT[right.priority] - PRIORITY_WEIGHT[left.priority] || right.sequence - left.sequence)[0];
	    return candidate ? (this.#removeQueued(candidate), candidate) : null;
	  }
	  async #run(task, controller, permit, permittedAt) {
	    task.state = "active", task.controller = controller, task.permitRestart = !1, this.#activeCount += 1, this.#activeByLane.set(
	      task.lane,
	      (this.#activeByLane.get(task.lane) ?? 0) + 1
	    );
	    const startedAt = this.#now();
	    try {
	      const waitReason = String(
	        permit?.waitReason || task.deferredWaitReason || ""
	      );
	      task.deferredWaitReason = "", task.onStart?.(Object.freeze({
	        queuedAt: task.queuedAt,
	        permittedAt: Math.max(task.queuedAt, permittedAt),
	        startedAt: Math.max(permittedAt, startedAt),
	        recoveryProbe: permit?.recoveryProbe === !0,
	        waitReason
	      }));
	    } catch (error) {
	      this.#onInternalError(error);
	    }
	    const timeoutError = new RequestTimeoutError();
	    let timedOut = !1;
	    const timeout = setTimeout(() => {
	      timedOut = !0, controller.abort(timeoutError);
	    }, task.timeoutMs), abortPromise = new Promise((_resolve, reject) => {
	      controller.signal.addEventListener(
	        "abort",
	        () => reject(abortReason(controller.signal, "signal")),
	        { once: !0 }
	      );
	    });
	    try {
	      const value = await Promise.race([task.operation(controller.signal), abortPromise]);
	      task.resolve(value);
	    } catch (error) {
	      task.reject(timedOut ? timeoutError : error);
	    } finally {
	      clearTimeout(timeout), this.#releasePermit(permit), this.#activeCount = Math.max(0, this.#activeCount - 1);
	      const laneActive = Math.max(
	        0,
	        (this.#activeByLane.get(task.lane) ?? 0) - 1
	      );
	      laneActive ? this.#activeByLane.set(task.lane, laneActive) : this.#activeByLane.delete(task.lane), this.#finish(task), this.#queuePump();
	    }
	  }
	  #releasePermit(permit) {
	    if (permit)
	      try {
	        permit.release();
	      } catch (error) {
	        this.#onInternalError(error);
	      }
	  }
	  #removeQueued(task) {
	    const index = this.#queue.indexOf(task);
	    index >= 0 && this.#queue.splice(index, 1);
	  }
	  #finish(task, error) {
	    task.state !== "done" && (task.state = "done", task.externalSignal && task.externalAbort && task.externalSignal.removeEventListener("abort", task.externalAbort), task.externalAbort = null, task.controller = null, task.permitRestart = !1, this.#tasksByKey.get(task.key) === task && this.#tasksByKey.delete(task.key), error !== void 0 && task.reject(error));
	  }
	}
}, "327537da25b41fe287048edb5b96f8f0976e42a61bccf41c9cbfa438bc8cc0c7");

/* Source: lite/src/notification/discourse-notification-adapter.ts */
runtime.register("src/notification/discourse-notification-adapter.js", function(module, exports, require) {
	var discourse_notification_adapter_exports = {};
	__export(discourse_notification_adapter_exports, {
	  BrowserDiscourseNotificationNativeState: () => import_native_host_api.BrowserDiscourseNotificationNativeState,
	  DiscourseNotificationRequestAdapter: () => DiscourseNotificationRequestAdapter
	});
	module.exports = __toCommonJS(discourse_notification_adapter_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_native_host_api = require("../discourse/native-host-api.js"), import_native_request_descriptors = require("../discourse/native-request-descriptors.js"), import_reader_notification_model = require("./reader-notification-model.js");
	const notificationDescriptorBrand = Symbol(
	  "DiscourseNotificationPageDescriptor"
	), notificationDescriptors = /* @__PURE__ */ new WeakSet();
	function nonNegativePage(value) {
	  const page = Number(value);
	  if (!Number.isSafeInteger(page) || page < 0)
	    throw new RangeError("通知页码必须是非负安全整数");
	  return page;
	}
	function notificationPageCacheSettings(group, page) {
	  return Object.freeze({
	    kind: "discourse-notification-page",
	    tags: Object.freeze([
	      // 仅头页随实时事件失效;历史记录本身稳定,深页保留给水位续取。
	      page === 0 ? "notifications" : "notification-history",
	      `notification-group:${group}`
	    ]),
	    freshForMs: page === 0 ? 30 * 6e4 : 4320 * 60 * 60 * 1e3,
	    retainForMs: 4320 * 60 * 60 * 1e3,
	    persist: !0
	  });
	}
	function notificationPageVariant(group) {
	  return group.source === "user-actions" ? `user-actions-limit-${group.pageSize}-v1` : void 0;
	}
	function username(value) {
	  const normalized = String(value ?? "").trim().replace(/^@/, "");
	  if (!normalized) throw new Error("通知分类请求需要当前登录用户名");
	  return normalized;
	}
	function notificationDescriptor(groupKey, pageValue, currentUsername, previousCursor) {
	  const group = (0, import_reader_notification_model.readerNotificationGroup)(groupKey), page = nonNegativePage(pageValue), offset = page * group.pageSize, query = new URLSearchParams();
	  let path;
	  if (group.source === "notifications")
	    query.set("offset", String(offset)), query.set("limit", String(group.pageSize)), currentUsername && query.set("username", currentUsername), path = "/notifications.json";
	  else if (group.source === "user-actions") {
	    const actor = username(currentUsername);
	    query.set("offset", String(offset)), query.set("limit", String(group.pageSize)), query.set("username", actor), query.set("filter", group.actionTypes.join(",")), path = "/user_actions.json";
	  } else if (group.source === "boosts-received") {
	    const actor = username(currentUsername);
	    previousCursor && query.set("before_boost_id", previousCursor), path = `/discourse-boosts/users/${encodeURIComponent(actor)}/boosts-received.json`;
	  } else if (group.source === "reactions-received")
	    query.set("username", username(currentUsername)), previousCursor && query.set("before_reaction_user_id", previousCursor), path = "/discourse-reactions/posts/reactions-received.json";
	  else {
	    const actor = username(currentUsername);
	    if (!group.path) throw new Error(`私信分类 ${group.key} 缺少原生 path`);
	    query.set("page", String(page)), path = `/topics/${group.path}/${encodeURIComponent(actor)}.json`;
	  }
	  const descriptor = Object.freeze({
	    group: group.key,
	    page,
	    path: `${path}${query.size ? `?${query.toString()}` : ""}`,
	    [notificationDescriptorBrand]: !0
	  });
	  return notificationDescriptors.add(descriptor), descriptor;
	}
	function assertNotificationDescriptor(value) {
	  if (value === null || typeof value != "object" || !notificationDescriptors.has(value))
	    throw new Error("通知读取必须来自具名 Discourse 请求目录");
	}
	function pickedEntries(payload, source) {
	  if (source === "notifications")
	    return Array.isArray(payload.notifications) ? payload.notifications : [];
	  if (source === "user-actions")
	    return Array.isArray(payload.user_actions) ? payload.user_actions : [];
	  if (source === "boosts-received")
	    return Array.isArray(payload.boosts) ? payload.boosts : [];
	  if (source === "private-messages") {
	    const topicList = (0, import_reader_notification_model.notificationRecord)(payload.topic_list);
	    return Array.isArray(topicList.topics) ? topicList.topics : [];
	  }
	  return Array.isArray(payload) ? payload : Array.isArray(payload.reactions) ? payload.reactions : [];
	}
	function positiveTotal(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : 0;
	}
	function positiveInteger(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function consolidatedReplyInfo(value, presented) {
	  if (presented.typeName !== "replied") return null;
	  const source = (0, import_reader_notification_model.notificationRecord)(value), data = (0, import_reader_notification_model.notificationData)(source), count = positiveInteger(data.consolidated_count), parentPostNumber = positiveInteger(data.reply_to_post_number), topicId = positiveInteger(
	    presented.topicId ?? source.topic_id ?? data.topic_id
	  ), latestPostNumber = positiveInteger(
	    presented.postNumber ?? source.post_number ?? data.post_number
	  ), sourceNotificationId = positiveInteger(source.id);
	  return count === null || count <= 1 || parentPostNumber === null || topicId === null || latestPostNumber === null || sourceNotificationId === null ? null : Object.freeze({
	    topicId,
	    latestPostNumber,
	    parentPostNumber,
	    count,
	    sourceNotificationId
	  });
	}
	function topicPosts(value) {
	  const payload = (0, import_reader_notification_model.notificationRecord)(value), stream = (0, import_reader_notification_model.notificationRecord)(payload.post_stream), candidates = Array.isArray(stream.posts) ? stream.posts : Array.isArray(payload.posts) ? payload.posts : [];
	  return Object.freeze(candidates.map(import_reader_notification_model.notificationRecord));
	}
	function topicTaxonomyEntries(value) {
	  const payload = (0, import_reader_notification_model.notificationRecord)(value), topicList = (0, import_reader_notification_model.notificationRecord)(payload.topic_list), candidates = Array.isArray(topicList.topics) ? topicList.topics : Array.isArray(payload.topics) ? payload.topics : [];
	  return Object.freeze(candidates.map(import_reader_notification_model.notificationRecord));
	}
	function replyMatchesBucket(post, parentPostNumber, latestPostNumber) {
	  const postNumber = positiveInteger(post.post_number);
	  if (postNumber === null || postNumber > latestPostNumber) return !1;
	  const replyTo = Math.max(0, Number(post.reply_to_post_number) || 0);
	  return parentPostNumber === 1 ? replyTo <= 1 && postNumber > 1 : replyTo === parentPostNumber;
	}
	function expandedReplyValue(notificationValue, post) {
	  const notification = (0, import_reader_notification_model.notificationRecord)(notificationValue), data = (0, import_reader_notification_model.notificationData)(notification), actor = String(post.username ?? "").trim().replace(/^@/, ""), postNumber = positiveInteger(post.post_number);
	  return postNumber === null ? notificationValue : Object.freeze({
	    ...notification,
	    data: Object.freeze({
	      ...data,
	      consolidated_count: 1,
	      display_username: actor,
	      original_username: actor,
	      acting_user_name: actor,
	      username: actor,
	      post_number: postNumber
	    }),
	    post_number: postNumber,
	    created_at: post.created_at ?? notification.created_at,
	    username: actor,
	    acting_user_avatar_template: post.avatar_template ?? notification.acting_user_avatar_template
	  });
	}
	class DiscourseNotificationRequestAdapter {
	  authScope;
	  #gateway;
	  #ajax;
	  #native;
	  #signal;
	  #replyExpansionCache;
	  #basePath;
	  #categoryNameFor;
	  constructor(options) {
	    this.#gateway = options.gateway, this.#ajax = options.ajax, this.#native = options.native, this.authScope = (0, import_identifiers.discourseAuthScope)(options.authScope), this.#signal = options.signal, this.#replyExpansionCache = Object.freeze({
	      ...options.replyExpansionCache,
	      tags: Object.freeze([...options.replyExpansionCache.tags])
	    }), this.#basePath = String(options.basePath ?? "").trim().replace(/\/+$/, ""), this.#categoryNameFor = options.categoryNameFor ?? (() => "");
	  }
	  groups() {
	    return import_reader_notification_model.READER_NOTIFICATION_GROUP_ORDER;
	  }
	  async enrichTopicTaxonomy(pages, options = {}) {
	    return this.#enrichTopicTaxonomy(pages, options, !1);
	  }
	  async enrichCachedTopicTaxonomy(pages) {
	    return this.#enrichTopicTaxonomy(pages, {}, !0);
	  }
	  async #enrichTopicTaxonomy(pages, options, cachedOnly) {
	    const topicIds = [...new Set(pages.flatMap((page) => page.records.flatMap((record) => record.source !== "private-messages" && record.target !== null && !record.tags.length ? [Number(record.target.topicId)] : [])))].filter((topicId) => positiveInteger(topicId) !== null).sort((left, right) => left - right);
	    if (!topicIds.length) return pages;
	    const batches = [];
	    for (let index = 0; index < topicIds.length; index += 100)
	      batches.push(topicIds.slice(index, index + 100));
	    const payloads = await Promise.all(batches.map(async (topicIdBatch) => {
	      const query = new URLSearchParams({
	        per_page: String(topicIdBatch.length)
	      });
	      for (const topicId of topicIdBatch)
	        query.append("topic_ids[]", String(topicId));
	      const path = `/latest.json?${query}`, cache = Object.freeze({
	        kind: "discourse-notification-topic-taxonomy",
	        tags: Object.freeze([
	          "notification-taxonomy",
	          ...topicIdBatch.map((topicId) => `topic:${topicId}`)
	        ]),
	        freshForMs: this.#replyExpansionCache.freshForMs,
	        retainForMs: this.#replyExpansionCache.retainForMs,
	        persist: this.#replyExpansionCache.persist
	      });
	      if (cachedOnly) {
	        const cachedCollectionPage = this.#gateway.cachedCollectionPage;
	        return typeof cachedCollectionPage != "function" ? null : cachedCollectionPage.call(this.#gateway, {
	          authScope: this.authScope,
	          collection: "notification-topic-taxonomy",
	          page: 0,
	          variant: `v1:${topicIdBatch.join(",")}`,
	          cache
	        });
	      }
	      return this.#gateway.loadCollectionPage({
	        authScope: this.authScope,
	        collection: "notification-topic-taxonomy",
	        page: 0,
	        variant: `v1:${topicIdBatch.join(",")}`,
	        profile: options.background || options.history ? "background-prefetch" : "collection-visible",
	        input: path,
	        signal: this.#signal,
	        timeoutMs: 2e4,
	        cache,
	        allowStaleOnError: !0,
	        transport: (request) => this.#ajax.request({
	          path,
	          method: "GET",
	          signal: request.signal,
	          noStore: !1
	        })
	      });
	    })), topics = /* @__PURE__ */ new Map();
	    for (const payload of payloads)
	      if (payload !== null)
	        for (const topic of topicTaxonomyEntries(payload)) {
	          const topicId = positiveInteger(topic.id ?? topic.topic_id);
	          topicId !== null && topics.set(topicId, topic);
	        }
	    return Object.freeze(pages.map((page) => {
	      let changed = !1;
	      const records = page.records.map((record) => {
	        const topic = record.target === null ? void 0 : topics.get(Number(record.target.topicId));
	        if (!topic) return record;
	        const enriched = (0, import_reader_notification_model.withReaderNotificationTopicTaxonomy)(
	          record,
	          topic,
	          this.#categoryNameFor
	        );
	        return enriched !== record && (changed = !0), enriched;
	      });
	      return changed ? Object.freeze({ ...page, records: Object.freeze(records) }) : page;
	    }));
	  }
	  async #loadConsolidatedReplyPosts(info, refresh) {
	    const candidate = import_native_request_descriptors.DiscourseNativeRequests.targetCandidates({
	      basePath: this.#basePath,
	      topicId: info.topicId,
	      postNumber: info.latestPostNumber,
	      scope: "around",
	      refresh
	    }).find((entry) => entry.endpoint === "topic-id-query");
	    if (!candidate)
	      throw new Error("合并回复缺少 Discourse topic-id-query 目录");
	    const payload = await this.#gateway.loadTopicTarget({
	      authScope: this.authScope,
	      topicId: info.topicId,
	      operation: "target:around:topic-id-query",
	      postNumber: info.latestPostNumber,
	      profile: "background-prefetch",
	      input: candidate.url,
	      signal: this.#signal,
	      cacheMode: refresh ? "refresh" : "default",
	      timeoutMs: 15e3,
	      cache: this.#consolidatedReplyCache(info.topicId),
	      allowStaleOnError: !refresh,
	      transport: (request) => this.#ajax.request({
	        path: candidate.descriptor.path,
	        method: "GET",
	        signal: request.signal,
	        headers: candidate.descriptor.headers,
	        noStore: candidate.descriptor.browserCache === "no-store"
	      })
	    });
	    return topicPosts(payload);
	  }
	  #consolidatedReplyCache(topicId) {
	    return Object.freeze({
	      ...this.#replyExpansionCache,
	      tags: Object.freeze([.../* @__PURE__ */ new Set([
	        ...this.#replyExpansionCache.tags,
	        "notifications",
	        `topic:${topicId}`
	      ])].sort())
	    });
	  }
	  async #loadCachedConsolidatedReplyPosts(info) {
	    const cachedTopicTarget = this.#gateway.cachedTopicTarget;
	    if (typeof cachedTopicTarget != "function" || !import_native_request_descriptors.DiscourseNativeRequests.targetCandidates({
	      basePath: this.#basePath,
	      topicId: info.topicId,
	      postNumber: info.latestPostNumber,
	      scope: "around",
	      refresh: !1
	    }).find((entry) => entry.endpoint === "topic-id-query")) return null;
	    const payload = await cachedTopicTarget.call(this.#gateway, {
	      authScope: this.authScope,
	      topicId: info.topicId,
	      operation: "target:around:topic-id-query",
	      postNumber: info.latestPostNumber,
	      profile: "background-prefetch",
	      cache: this.#consolidatedReplyCache(info.topicId)
	    });
	    return payload === null ? null : topicPosts(payload);
	  }
	  async #expandNativeNotifications(entries, presented, refresh, cachedOnly = !1) {
	    const expanded = (await Promise.all(entries.map(async (value, index) => {
	      const initialPresented = presented[index] ?? Object.freeze({}), info = consolidatedReplyInfo(value, initialPresented);
	      if (!info)
	        return Object.freeze([Object.freeze({ value })]);
	      try {
	        const seenPostNumbers = /* @__PURE__ */ new Set(), loaded = cachedOnly ? await this.#loadCachedConsolidatedReplyPosts(info) : await this.#loadConsolidatedReplyPosts(info, refresh);
	        if (loaded === null)
	          return Object.freeze([Object.freeze({ value })]);
	        const replies = [...loaded].filter((post) => replyMatchesBucket(
	          post,
	          info.parentPostNumber,
	          info.latestPostNumber
	        )).sort((left, right) => Number(right.post_number) - Number(left.post_number)).filter((post) => {
	          const postNumber = positiveInteger(post.post_number);
	          return postNumber === null || seenPostNumbers.has(postNumber) ? !1 : (seenPostNumbers.add(postNumber), !0);
	        }).slice(0, info.count);
	        return replies.length !== info.count ? Object.freeze([Object.freeze({ value })]) : Object.freeze(replies.map((post) => {
	          const postNumber = positiveInteger(post.post_number);
	          return Object.freeze({
	            value: expandedReplyValue(value, post),
	            identity: `notification:${info.sourceNotificationId}:reply:${postNumber}`,
	            sourceNotificationId: info.sourceNotificationId
	          });
	        }));
	      } catch {
	        return Object.freeze([Object.freeze({ value })]);
	      }
	    }))).flat(), expandedPresented = await this.#native.present(
	      expanded.map((entry) => entry.value)
	    );
	    return Object.freeze(expanded.map((entry, index) => Object.freeze({
	      ...entry,
	      presented: expandedPresented[index] ?? Object.freeze({})
	    })));
	  }
	  async loadCached(groupValue, pageValue, options = {}) {
	    const cachedNotificationPage = this.#gateway.cachedNotificationPage;
	    if (typeof cachedNotificationPage != "function") return null;
	    const group = (0, import_reader_notification_model.readerNotificationGroup)(groupValue), page = nonNegativePage(pageValue), variant = notificationPageVariant(group), payload = await cachedNotificationPage.call(this.#gateway, {
	      authScope: this.authScope,
	      group: group.key,
	      page,
	      ...variant ? { variant } : {},
	      cache: notificationPageCacheSettings(group.key, page)
	    });
	    return payload === null ? null : this.#pageFromPayload(group.key, page, payload, options, !0);
	  }
	  async load(groupValue, pageValue, options = {}) {
	    const group = (0, import_reader_notification_model.readerNotificationGroup)(groupValue), page = nonNegativePage(pageValue), variant = notificationPageVariant(group);
	    let previousCursor = null;
	    if (page > 0 && (group.source === "boosts-received" || group.source === "reactions-received")) {
	      let previous = null;
	      if (!options.refresh)
	        try {
	          previous = await this.loadCached(group.key, page - 1, options);
	        } catch {
	          previous = null;
	        }
	      if (previous ??= await this.load(group.key, page - 1, options), previousCursor = previous.nextCursor, !previous.hasNext || previousCursor === null)
	        return Object.freeze({
	          group: group.key,
	          page,
	          records: Object.freeze([]),
	          total: previous.total,
	          hasNext: !1,
	          nextCursor: null
	        });
	    }
	    const descriptor = notificationDescriptor(
	      group.key,
	      page,
	      this.#native.username(),
	      previousCursor
	    );
	    assertNotificationDescriptor(descriptor);
	    const payload = await this.#gateway.loadNotificationPage({
	      authScope: this.authScope,
	      group: group.key,
	      page,
	      ...variant ? { variant } : {},
	      ...options.history ? {
	        profile: options.visibleHistory ? "surface-prefetch" : "background-prefetch"
	      } : options.background ? { profile: "surface-prefetch" } : {},
	      input: descriptor.path,
	      signal: this.#signal,
	      ...options.refresh ? { cacheMode: "refresh" } : {},
	      timeoutMs: group.source === "reactions-received" ? 3e4 : 15e3,
	      cache: notificationPageCacheSettings(group.key, page),
	      transport: (request) => this.#ajax.request({
	        path: descriptor.path,
	        method: "GET",
	        signal: request.signal,
	        noStore: options.refresh === !0
	      })
	    });
	    return this.#pageFromPayload(group.key, page, payload, options, !1);
	  }
	  async #pageFromPayload(groupValue, page, payload, options, cachedOnly) {
	    const group = (0, import_reader_notification_model.readerNotificationGroup)(groupValue), source = (0, import_reader_notification_model.notificationRecord)(payload), rawEntries = pickedEntries(source, group.source);
	    let records;
	    if (group.source === "notifications") {
	      const presented = await this.#native.present(rawEntries);
	      records = (options.expandConsolidated === !1 ? Object.freeze(rawEntries.map((value, index) => Object.freeze({
	        value,
	        presented: presented[index] ?? Object.freeze({})
	      }))) : await this.#expandNativeNotifications(
	        rawEntries,
	        presented,
	        options.refresh === !0,
	        cachedOnly
	      )).map((entry) => (0, import_reader_notification_model.normalizeNativeNotification)(
	        entry.value,
	        entry.presented,
	        group.key,
	        {
	          ...entry.identity === void 0 ? {} : { identity: entry.identity },
	          ...entry.sourceNotificationId === void 0 ? {} : {
	            sourceNotificationId: entry.sourceNotificationId
	          },
	          categoryNameFor: this.#categoryNameFor
	        }
	      )).filter((record) => !group.typeNames.length || group.typeNames.includes(record.typeName));
	    } else group.source === "user-actions" ? records = rawEntries.map((entry) => (0, import_reader_notification_model.normalizeUserActionNotification)(
	      entry,
	      group.key,
	      this.#categoryNameFor
	    )) : group.source === "boosts-received" ? records = rawEntries.map((entry) => (0, import_reader_notification_model.normalizeBoostNotification)(entry, this.#categoryNameFor)) : group.source === "reactions-received" ? records = rawEntries.map((entry) => (0, import_reader_notification_model.normalizeReactionNotification)(entry, this.#categoryNameFor)) : records = rawEntries.map((entry) => (0, import_reader_notification_model.normalizePrivateMessageNotification)(
	      entry,
	      source,
	      group.key,
	      this.#native.username(),
	      this.#categoryNameFor
	    ));
	    const topicList = (0, import_reader_notification_model.notificationRecord)(source.topic_list), serverTotal = positiveTotal(
	      source.total_rows_notifications ?? topicList.total_rows
	    ), hasNext = group.source === "notifications" ? source.load_more_notifications === !0 || serverTotal > 0 && (page + 1) * group.pageSize < serverTotal : group.source === "private-messages" && !!topicList.more_topics_url || rawEntries.length >= group.pageSize, total = serverTotal > 0 ? serverTotal + (group.source === "notifications" ? Math.max(0, records.length - rawEntries.length) : 0) : page * group.pageSize + records.length + (hasNext ? group.pageSize : 0), last = (0, import_reader_notification_model.notificationRecord)(rawEntries.at(-1)), nextCursorValue = last.reaction_user_id ?? last.id, nextCursor = String(nextCursorValue ?? "").trim() || null;
	    return Object.freeze({
	      group: group.key,
	      page,
	      records: (0, import_reader_notification_model.sortReaderNotifications)(records),
	      total,
	      hasNext,
	      nextCursor
	    });
	  }
	}
}, "02761084dedf0c1bb146318bc9e6e5fd21639625031bd1bc377f25e48221e048");

/* Source: lite/src/notification/reader-notification-controller.ts */
runtime.register("src/notification/reader-notification-controller.js", function(module, exports, require) {
	var reader_notification_controller_exports = {};
	__export(reader_notification_controller_exports, {
	  ReaderNotificationController: () => ReaderNotificationController,
	  readerNotificationRequestCanAutoRetry: () => readerNotificationRequestCanAutoRetry
	});
	module.exports = __toCommonJS(reader_notification_controller_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_collection_filter_model = require("../collection/reader-collection-filter-model.js"), import_reader_collection_hydration = require("../collection/reader-collection-hydration.js"), import_discourse_action_descriptors = require("../post/discourse-action-descriptors.js"), import_notification_action_feature_commands = require("../post/notification-action-feature-commands.js"), import_reader_search = require("../search/reader-search.js"), import_reader_notification_model = require("./reader-notification-model.js");
	const DEFAULT_MAX_CACHED_PAGES = 32, DEFAULT_LIVE_REFRESH_DELAY_MS = 240, DEFAULT_RETRY_DELAY_MS = 600, DEFAULT_OPEN_REVALIDATE_MS = 30 * 6e4, DEFAULT_NATIVE_POLL_INTERVAL_MS = 30 * 6e4, DEFAULT_SYNTHETIC_POLL_INTERVAL_MS = 30 * 6e4, DEFAULT_HISTORY_STEP_DELAY_MS = 250, DEFAULT_HISTORY_RETRY_DELAY_MS = 15e3, LEGACY_USER_ACTION_PAGE_SIZE = 30, HISTORY_PROJECTION_BATCH_PAGES = 4, VISIBLE_HISTORY_LEASE_ROUNDS = 2, READER_NOTIFICATION_REACTION_LIKE_GROUPS = Object.freeze([
	  "likes",
	  "reactions"
	]);
	function notificationProjectionCheckpointNeedsRepair(group, snapshot) {
	  const descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group);
	  if (!snapshot.complete || descriptor.source !== "user-actions" || snapshot.sourceOffset === void 0) return !1;
	  const sourcePageSize = Math.max(
	    1,
	    Math.floor(Number(snapshot.sourcePageSize ?? descriptor.pageSize) || 0)
	  ), sourceNextPage = Math.max(
	    0,
	    Math.floor(Number(snapshot.sourceNextPage) || 0)
	  ), sourceOffset = Math.max(
	    0,
	    Math.floor(Number(snapshot.sourceOffset) || 0)
	  ), minimumCompleteRecords = Math.max(0, sourceOffset - sourcePageSize);
	  return sourceNextPage > 1 && snapshot.records.length < minimumCompleteRecords;
	}
	function readerNotificationRequestCanAutoRetry(cause) {
	  const source = cause !== null && typeof cause == "object" ? cause : Object.freeze({}), status = Number(source.status ?? 0);
	  if (status === 429) return !1;
	  if (status === 408 || status === 425 || status >= 500) return !0;
	  const name = String(source.name ?? ""), message = String(source.message ?? "");
	  return name === "TimeoutError" || name === "TypeError" || /failed to fetch|network|timeout|timed out|请求超时/i.test(message);
	}
	function readerNotificationPollBackoffMs(cause) {
	  const source = cause !== null && typeof cause == "object" ? cause : Object.freeze({});
	  if (Number(source.status ?? 0) !== 429) return 0;
	  const explicitMs = Number(source.retryAfterMs ?? source.retry_after_ms), explicitSeconds = Number(source.retryAfter ?? source.retry_after), explicit = Number.isFinite(explicitMs) && explicitMs > 0 ? explicitMs : Number.isFinite(explicitSeconds) && explicitSeconds > 0 ? explicitSeconds * 1e3 : 0;
	  return explicit > 0 ? Math.max(6e4, explicit) : 6e4;
	}
	function pageKey(group, page) {
	  return `${group}:${page}`;
	}
	function nativePageKey(page) {
	  return `native:${page}`;
	}
	function pageRecordSignature(page) {
	  return page.records.map((record) => record.identity).join("");
	}
	function sameTarget(left, right) {
	  return !!(left.target && right.target && left.target.topicId === right.target.topicId && left.target.postNumber === right.target.postNumber);
	}
	function readRecordKey(record) {
	  return record.sourceNotificationId === null || record.target === null ? null : [
	    record.sourceNotificationId,
	    record.group,
	    record.target.topicId,
	    record.target.postNumber,
	    record.actor.toLocaleLowerCase()
	  ].join(":");
	}
	class ReaderNotificationController {
	  scope;
	  changes = new import_signal.Signal();
	  #requests;
	  #projection;
	  #native;
	  #actions;
	  #cache;
	  #target;
	  #descriptors = new import_discourse_action_descriptors.DiscourseActionDescriptors();
	  #commands;
	  #maxCachedPages;
	  #liveRefreshDelayMs;
	  #backgroundWarmDelayMs;
	  #openRevalidateMs;
	  #nativePollIntervalMs;
	  #syntheticPollIntervalMs;
	  #historyStepDelayMs;
	  #historyRetryDelayMs;
	  #visibleHistoryConcurrency;
	  #historyCoordination;
	  #historyCoordinationKey;
	  #retryDelayMs;
	  #delay;
	  #schedule;
	  #cancel;
	  #now;
	  #activity;
	  #searchForms;
	  #onError;
	  #historyAbort;
	  #pages = /* @__PURE__ */ new Map();
	  #taxonomyFlights = /* @__PURE__ */ new Map();
	  #historyRecords = /* @__PURE__ */ new Map();
	  #projectionRecords = /* @__PURE__ */ new Map();
	  #historyGroups = new Map(import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.map((group) => [
	    group,
	    {
	      pages: /* @__PURE__ */ new Set(),
	      nextPage: 0,
	      terminalPage: null,
	      estimatedPages: 1,
	      complete: !1,
	      retryAt: null,
	      error: null
	    }
	  ]));
	  #lastAuthoritativeAt = /* @__PURE__ */ new Map();
	  #readRecordKeys = /* @__PURE__ */ new Set();
	  #groups = {
	    notifications: "all",
	    messages: "inbox"
	  };
	  #open = !1;
	  #mode = "notifications";
	  #group = "all";
	  #page = 0;
	  #query = "";
	  #categoryFilter = "";
	  #tagFilter = "";
	  #dateFilter = "";
	  #sortDirection = "desc";
	  #records = Object.freeze([]);
	  #total = 0;
	  #hasNext = !1;
	  #loading = !1;
	  #refreshing = !1;
	  #retrying = !1;
	  #markingAll = !1;
	  #stale = !1;
	  #error = null;
	  #unreadCount = 0;
	  #revision = 0;
	  #snapshotCache = null;
	  #loadEpoch = 0;
	  #navigationEpoch = 0;
	  #selectionFlight = null;
	  #liveRefresh = null;
	  #poll = null;
	  #pollNotBefore = 0;
	  #historySchedule = null;
	  #historyLoading = !1;
	  #historyEpoch = 0;
	  #historyCursor = 0;
	  #historyStatus = "idle";
	  #historyCurrentGroup = null;
	  #historyError = null;
	  #historyRetryAt = null;
	  #backgroundWarm = null;
	  #backgroundWarming = !1;
	  #backgroundWarmPending = !1;
	  #backgroundWarmEpoch = 0;
	  #backgroundCacheActive = !1;
	  #backgroundRestore = null;
	  #projectionPersistAfterRestore = /* @__PURE__ */ new Set();
	  #projectionCheckpointReplacements = /* @__PURE__ */ new Set();
	  #historyInFlightGroups = /* @__PURE__ */ new Set();
	  #nativeRefreshPending = !1;
	  #nativeChangePending = !1;
	  #nativeChangeEpoch = 0;
	  constructor(options) {
	    if (this.#requests = options.requests, this.#projection = options.projection ?? null, this.#native = options.native, this.#actions = options.actions, this.#cache = options.cache, this.#target = options.target, this.#maxCachedPages = Math.floor(
	      Number(options.maxCachedPages ?? DEFAULT_MAX_CACHED_PAGES)
	    ), !Number.isSafeInteger(this.#maxCachedPages) || this.#maxCachedPages < 1)
	      throw new RangeError("通知热缓存页数必须是正安全整数");
	    if (this.#liveRefreshDelayMs = Number(
	      options.liveRefreshDelayMs ?? DEFAULT_LIVE_REFRESH_DELAY_MS
	    ), !Number.isFinite(this.#liveRefreshDelayMs) || this.#liveRefreshDelayMs < 0)
	      throw new RangeError("通知实时刷新延迟必须是非负有限数值");
	    if (this.#backgroundWarmDelayMs = options.backgroundWarmDelayMs === void 0 ? null : Number(options.backgroundWarmDelayMs), this.#backgroundWarmDelayMs !== null && (!Number.isFinite(this.#backgroundWarmDelayMs) || this.#backgroundWarmDelayMs < 0))
	      throw new RangeError("通知后台预热延迟必须是非负有限数值");
	    this.#openRevalidateMs = Number(
	      options.openRevalidateMs ?? DEFAULT_OPEN_REVALIDATE_MS
	    ), this.#nativePollIntervalMs = Number(
	      options.nativePollIntervalMs ?? DEFAULT_NATIVE_POLL_INTERVAL_MS
	    ), this.#syntheticPollIntervalMs = Number(
	      options.syntheticPollIntervalMs ?? DEFAULT_SYNTHETIC_POLL_INTERVAL_MS
	    ), this.#historyStepDelayMs = Number(
	      options.historyStepDelayMs ?? DEFAULT_HISTORY_STEP_DELAY_MS
	    ), this.#historyRetryDelayMs = Number(
	      options.historyRetryDelayMs ?? DEFAULT_HISTORY_RETRY_DELAY_MS
	    ), this.#visibleHistoryConcurrency = Number(
	      options.visibleHistoryConcurrency ?? 1
	    ), this.#historyCoordination = options.historyCoordination, this.#historyCoordinationKey = String(
	      options.historyCoordinationKey ?? ""
	    ).trim();
	    for (const [label, value] of [
	      ["通知打开回查间隔", this.#openRevalidateMs],
	      ["原生通知轮询间隔", this.#nativePollIntervalMs],
	      ["合成通知轮询间隔", this.#syntheticPollIntervalMs],
	      ["通知历史回填步进", this.#historyStepDelayMs],
	      ["通知历史回填重试", this.#historyRetryDelayMs]
	    ])
	      if (!Number.isFinite(value) || value < 0)
	        throw new RangeError(`${label}必须是非负有限数值`);
	    if (!Number.isSafeInteger(this.#visibleHistoryConcurrency) || this.#visibleHistoryConcurrency < 1 || this.#visibleHistoryConcurrency > import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.length)
	      throw new RangeError("通知可见历史并发数必须位于 1 到 7");
	    if (this.#retryDelayMs = Number(
	      options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS
	    ), !Number.isFinite(this.#retryDelayMs) || this.#retryDelayMs < 0)
	      throw new RangeError("通知自动重试延迟必须是非负有限数值");
	    this.#delay = options.delay ?? ((delayMs) => new Promise((resolve) => {
	      setTimeout(resolve, delayMs);
	    })), this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.#now = options.now ?? Date.now, this.#activity = options.activity ?? null, this.#searchForms = options.searchForms ?? ((value) => Object.freeze([(0, import_reader_search.normalizeReaderSearchText)(value)])), this.#onError = options.onError ?? (() => {
	    }), this.#unreadCount = this.#native.unreadCount(), this.#commands = new import_notification_action_feature_commands.NotificationActionFeatureCommands({
	      state: {
	        markAllRead: () => this.#commitAllRead(),
	        markRead: (notificationId) => this.#commitRead(notificationId),
	        refresh: () => this.refresh()
	      }
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#historyAbort = this.scope.abortController(
	      new Error("通知历史采集已关闭")
	    ), this.scope.add(this.#native.subscribeChanged(() => {
	      this.#onNativeChanged();
	    })), this.#native.subscribeClicked && this.scope.add(this.#native.subscribeClicked((click) => {
	      this.#onNativeClicked(click);
	    })), this.#activity && this.scope.add(this.#activity.subscribe(() => {
	      this.#onActivityChanged();
	    })), this.scope.add(() => {
	      this.#loadEpoch += 1, this.#navigationEpoch += 1, this.#liveRefresh !== null && this.#cancel(this.#liveRefresh), this.#poll !== null && this.#cancel(this.#poll), this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#backgroundWarm !== null && this.#cancel(this.#backgroundWarm), this.#liveRefresh = null, this.#poll = null, this.#historySchedule = null, this.#historyEpoch += 1, this.#historyInFlightGroups.clear(), this.#backgroundWarm = null, this.#backgroundWarmEpoch += 1, this.#backgroundCacheActive = !1, this.#backgroundRestore = null, this.#projectionPersistAfterRestore.clear(), this.#taxonomyFlights.clear(), this.#pages.clear(), this.#historyRecords.clear(), this.#lastAuthoritativeAt.clear(), this.#readRecordKeys.clear(), this.changes.clear();
	    });
	  }
	  get snapshot() {
	    if (this.#snapshotCache?.revision === this.#revision)
	      return this.#snapshotCache;
	    const totalPages = this.#hasLocalFilters() ? Math.max(1, Math.ceil(this.#matchingRecords().length / (0, import_reader_notification_model.readerNotificationGroup)(this.#group).pageSize)) : Math.max(
	      1,
	      Math.ceil(
	        this.#total / (0, import_reader_notification_model.readerNotificationGroup)(this.#group).pageSize
	      )
	    );
	    return this.#snapshotCache = Object.freeze({
	      open: this.#open,
	      mode: this.#mode,
	      group: this.#group,
	      groupCounts: this.#groupCounts(),
	      page: this.#page,
	      query: this.#query,
	      categoryFilter: this.#categoryFilter,
	      tagFilter: this.#tagFilter,
	      dateFilter: this.#dateFilter,
	      sortDirection: this.#sortDirection,
	      dayCounts: this.#dayCounts(),
	      categoryOptions: this.#categoryOptions(),
	      tagOptions: this.#tagOptions(),
	      records: this.#records,
	      total: this.#total,
	      totalPages,
	      hasNext: this.#hasNext,
	      loading: this.#loading,
	      refreshing: this.#refreshing,
	      retrying: this.#retrying,
	      markingAll: this.#markingAll,
	      stale: this.#stale,
	      unreadCount: this.#unreadCount,
	      error: this.#error,
	      history: this.#historySnapshot(),
	      revision: this.#revision
	    }), this.#snapshotCache;
	  }
	  cacheStats() {
	    const indexedRecords = [...this.#historyRecords.values()].reduce(
	      (total, records) => total + records.size,
	      0
	    ), projectedRecords = new Set(
	      [...this.#projectionRecords.values()].flatMap((records) => [...records.values()].map((record) => `${record.group}:${record.identity}`))
	    ).size, pagedRecords = new Set(
	      [...this.#pages.values()].flatMap((entry) => entry.page.records.map((record) => `${record.group}:${record.identity}`))
	    ).size;
	    return Object.freeze({
	      pages: this.#pages.size,
	      records: Math.max(indexedRecords, projectedRecords, pagedRecords)
	    });
	  }
	  #groupCounts() {
	    const counts = /* @__PURE__ */ new Map(), remember = (group, count) => {
	      counts.set(group, Math.max(counts.get(group) ?? 0, count));
	    };
	    for (const [group, records] of this.#projectionRecords)
	      remember(group, records.size);
	    for (const [group, records] of this.#historyRecords)
	      remember(group, records.size);
	    for (const [key, entry] of this.#pages)
	      key.startsWith("native:") || remember(entry.page.group, entry.page.total);
	    const reactionLikes = READER_NOTIFICATION_REACTION_LIKE_GROUPS.reduce(
	      (total, group) => total + (counts.get(group) ?? 0),
	      0
	    );
	    remember("reactionLikes", reactionLikes);
	    const all = import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.reduce(
	      (total, group) => total + (counts.get(group) ?? 0),
	      0
	    );
	    return remember("all", all), counts;
	  }
	  /** application 启动时恢复持久投影;随后与浮窗开关无关地渐进续传。 */
	  startBackgroundCache() {
	    if (this.scope.destroyed || this.#backgroundCacheActive || this.#backgroundWarmDelayMs === null || this.#activity !== null && !this.#activityVisible()) return;
	    this.#backgroundCacheActive = !0;
	    const restore = this.#restoreBackgroundProjections();
	    this.#backgroundRestore = restore, restore.catch(this.#onError).finally(() => {
	      if (this.#backgroundRestore !== restore) return;
	      this.#backgroundRestore = null;
	      const pendingGroups = [...this.#projectionPersistAfterRestore];
	      if (this.#projectionPersistAfterRestore.clear(), !(this.scope.destroyed || !this.#backgroundCacheActive)) {
	        for (const group of pendingGroups) this.#persistProjection(group);
	        this.#scheduleHistoryHydration(this.#historyContinuationDelay());
	      }
	    });
	  }
	  /** 保留已提交分类页断点,只提前重排续传;中央限流仍拥有最终许可。 */
	  retryBackgroundCache() {
	    if (!(this.scope.destroyed || this.#historyStatus === "complete")) {
	      this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#historySchedule = null;
	      for (const state of this.#historyGroups.values())
	        state.retryAt = null, state.error = null;
	      this.#historyError = null, this.#historyRetryAt = null, this.#historyLoading || (this.#historyStatus = "idle", this.#historyCurrentGroup = null), this.#emit(), this.#scheduleHistoryHydration(0);
	    }
	  }
	  reloadExternalProjection() {
	    if (!this.#projection || this.scope.destroyed) return Promise.resolve();
	    const restore = (this.#backgroundRestore ?? Promise.resolve()).catch(() => {
	    }).then(() => this.#restoreBackgroundProjections(!0));
	    return this.#backgroundRestore = restore, restore.finally(() => {
	      this.#backgroundRestore === restore && (this.#backgroundRestore = null);
	    });
	  }
	  async #restoreBackgroundProjections(fresh = !1) {
	    if (!this.#projection || this.scope.destroyed) return;
	    const restored = await Promise.all(
	      import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.map(async (group) => {
	        try {
	          return Object.freeze({
	            group,
	            snapshot: await this.#projection.read(
	              group,
	              fresh ? { fresh: !0 } : void 0
	            )
	          });
	        } catch (cause) {
	          return this.#onError(cause), Object.freeze({ group, snapshot: null });
	        }
	      })
	    );
	    if (this.scope.destroyed) return;
	    const checkpointRepairs = [];
	    for (const { group, snapshot } of restored) {
	      if (!snapshot) continue;
	      const state = this.#historyGroups.get(group), repairCheckpoint = notificationProjectionCheckpointNeedsRepair(group, snapshot);
	      state.retryAt = null, state.error = null, this.#rememberProjectionRecords(group, snapshot.records, fresh);
	      const records = /* @__PURE__ */ new Map();
	      for (const record of snapshot.records)
	        records.set(record.identity, record);
	      if (!fresh)
	        for (const record of this.#historyRecords.get(group)?.values() ?? [])
	          records.set(record.identity, record);
	      this.#historyRecords.set(group, records), state.estimatedPages = Math.max(
	        state.estimatedPages,
	        repairCheckpoint ? Math.max(1, Math.floor(Number(snapshot.sourceNextPage) || 0)) : 1,
	        Math.max(
	          1,
	          Math.ceil(
	            Math.max(snapshot.totalHint, records.size) / (0, import_reader_notification_model.readerNotificationGroup)(group).pageSize
	          )
	        )
	      );
	      const expectedSourcePageSize = (0, import_reader_notification_model.readerNotificationGroup)(group).pageSize, resumed = (0, import_reader_collection_hydration.readerCollectionResumePosition)(
	        snapshot,
	        expectedSourcePageSize,
	        (0, import_reader_notification_model.readerNotificationGroup)(group).source === "user-actions" ? LEGACY_USER_ACTION_PAGE_SIZE : expectedSourcePageSize
	      ), sourceNextPage = snapshot.complete ? Math.max(0, Math.floor(Number(snapshot.sourceNextPage ?? 0))) : resumed.page;
	      state.nextPage = repairCheckpoint ? 0 : sourceNextPage, state.pages.clear();
	      for (let page = 0; page < state.nextPage; page += 1)
	        state.pages.add(page);
	      if (state.terminalPage = null, state.complete = !1, (!snapshot.complete || repairCheckpoint) && state.nextPage > 0 && (state.estimatedPages = Math.max(
	        state.estimatedPages,
	        state.nextPage + 1
	      )), snapshot.complete && !repairCheckpoint) {
	        const completedPages = Math.max(
	          1,
	          sourceNextPage > 0 ? sourceNextPage : state.estimatedPages
	        );
	        state.complete = !0, state.estimatedPages = completedPages, state.nextPage = completedPages, state.terminalPage = completedPages - 1, state.pages.clear();
	        for (let page = 0; page < completedPages; page += 1)
	          state.pages.add(page);
	      } else repairCheckpoint && (this.#projectionCheckpointReplacements.add(group), checkpointRepairs.push(group));
	      for (const entry of this.#pages.values())
	        entry.page.group === group && this.#indexHistoryPage(entry.page);
	    }
	    this.#refreshAggregateHistoryPages(), this.#historyStatus = [...this.#historyGroups.values()].every(
	      (state) => state.complete
	    ) ? "complete" : "idle", this.#historyCurrentGroup = null, this.#historyError = null, this.#historyRetryAt = null, checkpointRepairs.length && await Promise.all(checkpointRepairs.map((group) => this.#persistProjection(group))), this.#raiseUnreadCountForCachedRecords(), this.#open && this.#pages.has(pageKey(this.#group, this.#page)) ? this.#renderFromCache() : this.#emit();
	  }
	  /** WebDAV 只同步逐条历史投影;原生通知 ID 与已读状态仍由 Discourse 裁决。 */
	  syncHistoryRecords() {
	    const records = /* @__PURE__ */ new Map();
	    for (const group of import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER) {
	      for (const entry of this.#projectionRecords.get(group)?.values() ?? [])
	        records.set(entry.identity, entry);
	      for (const entry of this.#historyRecords.get(group)?.values() ?? [])
	        records.set(entry.identity, entry);
	    }
	    return (0, import_reader_notification_model.sortReaderNotifications)([...records.values()]);
	  }
	  applySyncedHistoryRecords(records) {
	    if (!this.scope.destroyed) {
	      for (const incoming of records) {
	        if (!incoming.identity || !import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.includes(incoming.group)) continue;
	        const indexed = this.#historyRecords.get(incoming.group) ?? /* @__PURE__ */ new Map(), current = indexed.get(incoming.identity);
	        indexed.set(incoming.identity, current ?? Object.freeze({
	          ...incoming,
	          sourceNotificationId: null,
	          read: null
	        })), this.#historyRecords.set(incoming.group, indexed);
	      }
	      this.#refreshAggregateHistoryPages();
	      for (const group of import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER)
	        this.#persistProjection(group);
	      this.#persistProjection("all"), this.#hasLocalFilters() && this.#open ? this.#renderFromCache() : this.#emit();
	    }
	  }
	  #historySnapshot() {
	    const states = [...this.#historyGroups.values()], completedGroups = states.filter((state) => state.complete).length, loadedPages = states.reduce(
	      (total, state) => total + state.pages.size,
	      0
	    ), estimatedPages = states.reduce(
	      (total, state) => total + Math.max(state.pages.size, state.estimatedPages),
	      0
	    ), cachedRecords = [...this.#historyRecords.values()].reduce(
	      (total, records) => total + records.size,
	      0
	    ), totalGroups = import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.length;
	    return Object.freeze({
	      status: this.#historyStatus,
	      currentGroup: this.#historyCurrentGroup,
	      completedGroups,
	      totalGroups,
	      loadedPages,
	      estimatedPages,
	      cachedRecords,
	      progress: totalGroups > 0 ? completedGroups / totalGroups : 1,
	      error: this.#historyError,
	      retryAt: this.#historyRetryAt
	    });
	  }
	  #resetHistoryHydration() {
	    this.#historyEpoch += 1, this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#historySchedule = null, this.#historyLoading = !1, this.#historyInFlightGroups.clear(), this.#historyCursor = 0, this.#historyStatus = "idle", this.#historyCurrentGroup = null, this.#historyError = null, this.#historyRetryAt = null, this.#historyRecords.clear();
	    for (const state of this.#historyGroups.values())
	      state.pages.clear(), state.nextPage = 0, state.terminalPage = null, state.estimatedPages = 1, state.complete = !1, state.retryAt = null, state.error = null;
	  }
	  clearCache() {
	    this.scope.destroyed || (this.#loadEpoch += 1, this.#navigationEpoch += 1, this.#backgroundWarmEpoch += 1, this.#liveRefresh !== null && this.#cancel(this.#liveRefresh), this.#poll !== null && this.#cancel(this.#poll), this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#backgroundWarm !== null && this.#cancel(this.#backgroundWarm), this.#liveRefresh = null, this.#poll = null, this.#historySchedule = null, this.#backgroundWarm = null, this.#backgroundWarmPending = !1, this.#nativeRefreshPending = !1, this.#nativeChangePending = !1, this.#pollNotBefore = 0, this.#pages.clear(), this.#resetHistoryHydration(), this.#projectionRecords.clear(), this.#lastAuthoritativeAt.clear(), this.#readRecordKeys.clear(), this.#records = Object.freeze([]), this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc", this.#total = 0, this.#hasNext = !1, this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#stale = !1, this.#error = null, this.#emit(), this.#schedulePoll(), this.#scheduleBackgroundWarm(), this.#scheduleHistoryHydration(this.#historyContinuationDelay()));
	  }
	  async open() {
	    if (this.scope.destroyed) throw new Error("通知控制器已销毁");
	    this.#open || (this.#open = !0, this.#emit());
	    const key = pageKey(this.#group, this.#page), cached = this.#pages.has(key);
	    cached && this.#renderFromCache(), cached ? this.#nativeChangePending && await this.#runSelectedRequest(() => this.#refreshAfterNativeChange()) : (this.#loading = !0, this.#emit(), this.#projection && await this.#restoreSelectedProjection(this.#navigationEpoch), await this.#runSelectedRequest(() => this.#showSelectedPage()));
	  }
	  close() {
	    this.#open && (this.#open = !1, this.#loadEpoch += 1, this.#navigationEpoch += 1, this.#cancelPoll(), this.#backgroundCacheActive || (this.#backgroundWarmEpoch += 1, this.#backgroundWarmPending = !1, this.#backgroundWarm !== null && this.#cancel(this.#backgroundWarm), this.#backgroundWarm = null, this.#historyEpoch += 1, this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#historySchedule = null, this.#historyStatus !== "complete" && (this.#historyStatus = "paused", this.#historyCurrentGroup = null)), this.#emit());
	  }
	  async toggle() {
	    this.#open ? this.close() : await this.open();
	  }
	  async selectMode(mode) {
	    if (mode !== "notifications" && mode !== "messages")
	      throw new Error("未知消息模式");
	    this.#mode = mode, this.#group = this.#groups[mode], this.#page = 0, this.#query = "", this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc";
	    const epoch = this.#beginNavigation();
	    await this.#runNavigation(epoch);
	  }
	  async selectGroup(groupValue) {
	    const group = (0, import_reader_notification_model.readerNotificationGroup)(groupValue);
	    this.#mode = group.mode, this.#group = group.key, this.#groups[group.mode] = group.key, this.#page = 0, this.#query = "", this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc";
	    const epoch = this.#beginNavigation();
	    await this.#runNavigation(epoch);
	  }
	  setQuery(value) {
	    const query = (0, import_reader_search.normalizeReaderSearchText)(value);
	    query !== this.#query && (this.#query = query, this.#localFilterChanged());
	  }
	  setCategoryFilter(value) {
	    const filter = String(value ?? "").trim();
	    filter !== this.#categoryFilter && (this.#categoryFilter = filter, this.#localFilterChanged());
	  }
	  setTagFilter(value) {
	    const filter = String(value ?? "").trim();
	    filter !== this.#tagFilter && (this.#tagFilter = filter, this.#localFilterChanged());
	  }
	  setDateFilter(value) {
	    const filter = String(value ?? "").trim();
	    filter !== this.#dateFilter && (this.#dateFilter = filter, this.#localFilterChanged());
	  }
	  setSortDirection(value) {
	    const direction = value === "asc" ? "asc" : "desc";
	    direction !== this.#sortDirection && (this.#sortDirection = direction, this.#localFilterChanged());
	  }
	  resetFilters() {
	    this.#hasLocalFilters() && (this.#query = "", this.#categoryFilter = "", this.#tagFilter = "", this.#dateFilter = "", this.#sortDirection = "desc", this.#localFilterChanged());
	  }
	  #localFilterChanged() {
	    if (this.#page = 0, !this.#hasLocalFilters() && this.#consumeNativeRefreshPending() && this.#open) {
	      this.#runSelectedRequest(() => this.#refreshAfterNativeChange());
	      return;
	    }
	    this.#renderFromCache();
	  }
	  async previousPage() {
	    if (this.#page <= 0) return;
	    this.#page -= 1;
	    const epoch = this.#beginNavigation();
	    await this.#runNavigation(epoch);
	  }
	  async nextPage() {
	    const snapshot = this.snapshot;
	    if (this.#page >= snapshot.totalPages - 1 && !snapshot.hasNext) return;
	    this.#page += 1;
	    const epoch = this.#beginNavigation();
	    await this.#runNavigation(epoch);
	  }
	  async refresh() {
	    this.scope.destroyed || (await this.#runSelectedRequest(() => this.#refreshAfterNativeChange()), this.#schedulePoll());
	  }
	  async markAllAsRead() {
	    if (!(this.#unreadCount <= 0 || this.scope.destroyed || this.#markingAll)) {
	      this.#markingAll = !0, this.#emit();
	      try {
	        await this.#actions.dispatch(
	          this.#commands.markAllRead(this.#descriptors.notificationsMarkRead())
	        );
	      } finally {
	        this.scope.destroyed || (this.#markingAll = !1, this.#emit());
	      }
	    }
	  }
	  async markRecordRead(record) {
	    const notificationId = record.sourceNotificationId;
	    if (notificationId === null || record.read !== !1 || this.scope.destroyed)
	      return;
	    const childScoped = this.#childReadSourceIds().has(notificationId);
	    if (this.#setReadRecordState(record, !0), !(childScoped && !this.#allSourceRecordsRead(notificationId)))
	      try {
	        await this.#actions.dispatch(this.#commands.markRead(
	          notificationId,
	          this.#descriptors.notificationMarkRead({ notificationId })
	        ));
	      } catch (cause) {
	        throw this.scope.destroyed || this.#setReadRecordState(record, !1), cause;
	      }
	  }
	  async openRecord(record) {
	    if (!record.target) return;
	    const boostId = record.group === "boosts" ? Number(record.identity.match(/^boosts:(\d+)$/)?.[1]) : 0;
	    await this.#target.openTarget({
	      topicId: record.target.topicId,
	      postNumber: record.target.postNumber,
	      source: record.source === "private-messages" ? "message" : "notification",
	      ...Number.isSafeInteger(boostId) && boostId > 0 ? { boostId } : {},
	      focus: !0,
	      highlight: !0
	    }) && record.sourceNotificationId !== null && record.read === !1 && this.markRecordRead(record).catch((cause) => this.#onError(cause));
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #beginNavigation() {
	    const epoch = ++this.#navigationEpoch;
	    return this.#loadEpoch += 1, this.#cancelPoll(), !this.#backgroundCacheActive && this.#historySchedule !== null && (this.#cancel(this.#historySchedule), this.#historySchedule = null), this.#loading = !this.#pages.has(pageKey(this.#group, this.#page)), this.#refreshing = !1, this.#retrying = !1, this.#stale = !1, this.#error = null, this.#renderFromCache(), epoch;
	  }
	  async #runNavigation(epoch) {
	    const valid = () => !this.scope.destroyed && epoch === this.#navigationEpoch;
	    try {
	      if (this.#projection && await this.#restoreSelectedProjection(epoch), !valid()) return;
	      await this.#runSelectedRequest(async () => {
	        if (this.#consumeNativeRefreshPending()) {
	          await this.#refreshAfterNativeChange();
	          return;
	        }
	        await this.#showSelectedPage();
	      }, valid);
	    } finally {
	      valid() && (this.#schedulePoll(), this.#scheduleHistoryHydration(this.#historyContinuationDelay()));
	    }
	  }
	  async #restoreSelectedProjection(epoch) {
	    if (!this.#projection) return !1;
	    const group = this.#group, selectedPage = this.#page, key = pageKey(group, selectedPage);
	    if (this.#pages.has(key)) return !0;
	    let stored;
	    try {
	      if (group === "reactionLikes") {
	        const [likes, reactions] = await Promise.all([
	          this.#projection.read("likes"),
	          this.#projection.read("reactions")
	        ]);
	        stored = likes || reactions ? Object.freeze({
	          records: (0, import_reader_notification_model.sortReaderNotifications)([
	            ...likes?.records ?? [],
	            ...reactions?.records ?? []
	          ]),
	          totalHint: (likes?.totalHint ?? 0) + (reactions?.totalHint ?? 0),
	          complete: likes?.complete === !0 && reactions?.complete === !0,
	          updatedAt: Math.max(
	            likes?.updatedAt ?? 0,
	            reactions?.updatedAt ?? 0
	          )
	        }) : null;
	      } else
	        stored = await this.#projection.read(group);
	    } catch (cause) {
	      return !this.scope.destroyed && epoch === this.#navigationEpoch && this.#onError(cause), !1;
	    }
	    if (this.scope.destroyed || epoch !== this.#navigationEpoch || this.#group !== group || this.#page !== selectedPage || !stored) return !1;
	    const descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group), start = selectedPage * descriptor.pageSize;
	    if (start >= stored.records.length && !(selectedPage === 0 && stored.complete)) return !1;
	    const end = start + descriptor.pageSize;
	    this.#rememberProjectionRecords(group, stored.records);
	    const page = Object.freeze({
	      group,
	      page: selectedPage,
	      records: Object.freeze(stored.records.slice(start, end)),
	      total: Math.max(stored.records.length, stored.totalHint),
	      hasNext: end < stored.records.length || !stored.complete,
	      nextCursor: null
	    });
	    return this.#cachePage(page, stored.updatedAt, !1), this.#applyPage(page), this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#stale = !1, this.#error = null, this.#raiseUnreadCountForCachedRecords(), this.#emit(), !0;
	  }
	  async #runSelectedRequest(task, valid = () => !this.scope.destroyed) {
	    for (; this.#selectionFlight !== null; ) {
	      const active = this.#selectionFlight;
	      try {
	        await active;
	      } catch {
	      }
	      if (!valid()) return;
	    }
	    if (!valid() || this.#deferForRateLimit()) return;
	    const flight = (async () => {
	      await task();
	    })();
	    this.#selectionFlight = flight;
	    try {
	      await flight;
	    } finally {
	      this.#selectionFlight === flight && (this.#selectionFlight = null);
	    }
	  }
	  #deferForRateLimit() {
	    const remainingMs = this.#pollNotBefore - this.#now();
	    if (!(remainingMs > 0)) return !1;
	    const cached = this.#pages.get(pageKey(this.#group, this.#page));
	    return cached ? this.#applyPage(cached.page) : (this.#records = Object.freeze([]), this.#total = 0, this.#hasNext = !1), this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#stale = !!cached, this.#error = Object.assign(
	      new Error("请求冷却中,将自动重试"),
	      { status: 429, retryAfterMs: remainingMs }
	    ), this.#emit(), this.#schedulePoll(), this.#scheduleHistoryHydration(
	      Math.max(this.#historyRetryDelayMs, remainingMs)
	    ), !0;
	  }
	  async #showSelectedPage() {
	    if (this.#hasLocalFilters()) {
	      this.#renderFromCache();
	      return;
	    }
	    const key = pageKey(this.#group, this.#page);
	    if (this.#pages.has(key)) {
	      this.#renderFromCache();
	      return;
	    }
	    const restored = await this.#restoreSelectedPageFromPersistentCache();
	    if (!(restored === null || restored)) {
	      if (this.#pages.has(key)) {
	        this.#renderFromCache();
	        return;
	      }
	      this.#open ? await this.#refreshAfterNativeChange() : await this.#load(!0);
	    }
	  }
	  async #restoreSelectedPageFromPersistentCache() {
	    const epoch = ++this.#loadEpoch;
	    this.#loading = !0, this.#refreshing = !1, this.#retrying = !1, this.#error = null, this.#stale = !1, this.#emit();
	    let page;
	    try {
	      page = await this.#loadCachedRequestedPage(this.#group, this.#page);
	    } catch (cause) {
	      if (this.scope.destroyed || epoch !== this.#loadEpoch) return null;
	      this.#onError(cause), page = null;
	    }
	    return this.scope.destroyed || epoch !== this.#loadEpoch ? null : page ? (this.#cachePage(page), page.group !== "all" && page.group !== "reactionLikes" && this.#queueTopicTaxonomyEnrichment([page]), page.group === "all" && this.#inheritAllSyntheticPages(), this.#raiseUnreadCountForCachedRecords(), this.#applyPage(this.#pages.get(
	      pageKey(this.#group, this.#page)
	    )?.page ?? page), this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#stale = !1, this.#error = null, this.#emit(), this.#scheduleHistoryHydration(this.#historyContinuationDelay()), !0) : !1;
	  }
	  async #loadRequestedPage(group, page, options = {}) {
	    if (group === "reactionLikes")
	      return this.#loadReactionLikePage(page, options);
	    if (group !== "all") {
	      const history = this.#historyGroups.get(group);
	      if (page > 0 && history?.complete) {
	        const descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group), records = (0, import_reader_notification_model.sortReaderNotifications)([
	          ...this.#historyRecords.get(group)?.values() ?? []
	        ]), start = page * descriptor.pageSize, end = start + descriptor.pageSize;
	        return Object.freeze({
	          group,
	          page,
	          records: Object.freeze(records.slice(start, end)),
	          total: records.length,
	          hasNext: end < records.length,
	          nextCursor: null
	        });
	      }
	      return this.#requests.load(group, page, options);
	    }
	    return this.#loadAggregatePage(page, options);
	  }
	  async #loadCachedRequestedPage(group, page) {
	    const requests = this.#requests;
	    if (typeof requests.loadCached != "function") return null;
	    const loadCached = (groupKey, groupPage) => requests.loadCached.call(this.#requests, groupKey, groupPage);
	    if (group === "all") {
	      if (page > 0) return null;
	      const [cachedGroups, nativePage] = await Promise.all([
	        Promise.all(import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.map(
	          (groupKey) => loadCached(groupKey, 0)
	        )),
	        loadCached("all", 0)
	      ]), loadedPages = cachedGroups.filter(
	        (loaded2) => loaded2 !== null
	      );
	      if (!loadedPages.length) return null;
	      nativePage && this.#cacheNativeAssociationPage(nativePage);
	      for (const loaded2 of loadedPages) this.#cachePage(loaded2);
	      return this.#queueTopicTaxonomyEnrichment(loadedPages), this.#indexedAggregatePage(0);
	    }
	    if (group === "reactionLikes") {
	      const loadedPages = (await Promise.all(
	        READER_NOTIFICATION_REACTION_LIKE_GROUPS.map((groupKey) => loadCached(groupKey, page))
	      )).filter((loaded2) => loaded2 !== null);
	      if (!loadedPages.length) return null;
	      for (const loaded2 of loadedPages) this.#cachePage(loaded2);
	      return this.#queueTopicTaxonomyEnrichment(loadedPages), this.#indexedReactionLikePage(page);
	    }
	    const loaded = await loadCached(group, page);
	    return loaded || null;
	  }
	  async #enrichTopicTaxonomy(pages, options = {}) {
	    const requests = this.#requests;
	    if (typeof requests.enrichTopicTaxonomy != "function") return pages;
	    try {
	      const enriched = await requests.enrichTopicTaxonomy.call(
	        this.#requests,
	        pages,
	        options
	      );
	      return enriched.length === pages.length ? enriched : pages;
	    } catch (cause) {
	      return this.#onError(cause), pages;
	    }
	  }
	  #queueTopicTaxonomyEnrichment(pages, options = {}) {
	    const candidates = pages.filter((page) => page.records.length > 0);
	    if (!candidates.length || this.scope.destroyed || typeof this.#requests.enrichTopicTaxonomy != "function") return;
	    const key = candidates.map(
	      (page) => `${pageKey(page.group, page.page)}:${pageRecordSignature(page)}`
	    ).join("|");
	    if (this.#taxonomyFlights.has(key)) return;
	    const flight = this.#enrichTopicTaxonomy(candidates, {
	      ...options,
	      background: !0
	    }).then((enrichedPages) => {
	      if (this.scope.destroyed) return;
	      let changed = !1;
	      for (let index = 0; index < candidates.length; index += 1) {
	        const base = candidates[index], enriched = enrichedPages[index] ?? base, cached = this.#pages.get(pageKey(base.group, base.page))?.page;
	        !cached || pageRecordSignature(cached) !== pageRecordSignature(base) || enriched === base || (this.#cachePage(enriched), changed = !0);
	      }
	      changed && (this.#open ? this.#renderFromCache() : this.#emit());
	    }).finally(() => {
	      this.#taxonomyFlights.delete(key);
	    });
	    this.#taxonomyFlights.set(key, flight);
	  }
	  async #loadReactionLikePage(page, options = {}) {
	    const aggregate = (0, import_reader_notification_model.readerNotificationGroup)("reactionLikes"), end = (page + 1) * aggregate.pageSize, results = await Promise.all(
	      READER_NOTIFICATION_REACTION_LIKE_GROUPS.map(async (groupKey) => {
	        const group = (0, import_reader_notification_model.readerNotificationGroup)(groupKey), state = this.#historyGroups.get(groupKey), pages = [];
	        let error = null;
	        const requiredPages = Math.max(1, Math.ceil(end / group.pageSize));
	        for (let groupPage = 0; groupPage < requiredPages && !(state?.complete && state.terminalPage !== null && groupPage > state.terminalPage); groupPage += 1)
	          if (!(state?.pages.has(groupPage) && !(options.refresh && groupPage === 0)))
	            try {
	              const loaded = await this.#requests.load(groupKey, groupPage, {
	                ...options.refresh && groupPage === 0 ? { refresh: !0 } : {},
	                ...options.background ? { background: !0 } : {},
	                ...options.history ? { history: !0 } : {},
	                ...options.visibleHistory ? { visibleHistory: !0 } : {}
	              });
	              if (pages.push(loaded), !loaded.hasNext) break;
	            } catch (cause) {
	              error = cause;
	              break;
	            }
	        return Object.freeze({ pages: Object.freeze(pages), error });
	      })
	    ), loadedPages = results.flatMap((result) => result.pages), failures = results.map((result) => result.error).filter((cause) => cause !== null);
	    if (!loadedPages.length && failures.length) throw failures[0];
	    for (const loaded of loadedPages) this.#cachePage(loaded);
	    this.#queueTopicTaxonomyEnrichment(loadedPages, options);
	    for (const cause of failures) this.#onError(cause);
	    return this.#indexedReactionLikePage(page);
	  }
	  async #loadAggregatePage(page, options = {}) {
	    if (page > 0) return this.#indexedAggregatePage(page);
	    const end = (0, import_reader_notification_model.readerNotificationGroup)("all").pageSize, results = await Promise.all(
	      import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.map(async (groupKey) => {
	        const group = (0, import_reader_notification_model.readerNotificationGroup)(groupKey), pages = [];
	        let error = null;
	        const requiredPages = Math.max(1, Math.ceil(end / group.pageSize));
	        for (let groupPage = 0; groupPage < requiredPages; groupPage += 1)
	          try {
	            const loaded = await this.#requests.load(
	              groupKey,
	              groupPage,
	              {
	                ...options.refresh ? { refresh: !0 } : {},
	                ...options.background ? { background: !0 } : {},
	                ...options.history ? { history: !0 } : {},
	                ...options.visibleHistory ? { visibleHistory: !0 } : {}
	              }
	            );
	            if (pages.push(loaded), !loaded.hasNext) break;
	          } catch (cause) {
	            error = cause;
	            break;
	          }
	        return Object.freeze({ pages: Object.freeze(pages), error });
	      })
	    ), loadedPages = results.flatMap((result) => result.pages), failures = results.map((result) => result.error).filter((cause) => cause !== null);
	    if (!loadedPages.length && failures.length) throw failures[0];
	    for (const cause of failures) this.#onError(cause);
	    if (!options.valid || options.valid()) {
	      for (const loaded of loadedPages) this.#cachePage(loaded);
	      this.#queueTopicTaxonomyEnrichment(loadedPages, options);
	    }
	    const indexed = this.#indexedAggregatePage(page);
	    if (indexed.total > 0 || !loadedPages.some((loaded) => loaded.records.length))
	      return indexed;
	    const seen = /* @__PURE__ */ new Set(), records = (0, import_reader_notification_model.sortReaderNotifications)(loadedPages.flatMap((loaded) => loaded.records.filter((record) => seen.has(record.identity) ? !1 : (seen.add(record.identity), !0))));
	    return Object.freeze({
	      group: "all",
	      page,
	      records: Object.freeze(records.slice(0, end)),
	      total: records.length,
	      hasNext: records.length > end,
	      nextCursor: null
	    });
	  }
	  #indexedAggregateRecords() {
	    const seen = /* @__PURE__ */ new Set();
	    return (0, import_reader_notification_model.sortReaderNotifications)(
	      import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.flatMap((group) => [...this.#historyRecords.get(group)?.values() ?? []].filter((record) => seen.has(record.identity) ? !1 : (seen.add(record.identity), !0)))
	    );
	  }
	  #indexedReactionLikeRecords() {
	    const seen = /* @__PURE__ */ new Set();
	    return (0, import_reader_notification_model.sortReaderNotifications)(
	      READER_NOTIFICATION_REACTION_LIKE_GROUPS.flatMap((group) => [...this.#historyRecords.get(group)?.values() ?? []].filter((record) => seen.has(record.identity) ? !1 : (seen.add(record.identity), !0)))
	    );
	  }
	  #indexedReactionLikePage(page, indexed = this.#indexedReactionLikeRecords()) {
	    const aggregate = (0, import_reader_notification_model.readerNotificationGroup)("reactionLikes"), start = page * aggregate.pageSize, end = start + aggregate.pageSize, total = READER_NOTIFICATION_REACTION_LIKE_GROUPS.reduce(
	      (sum, group) => sum + Math.max(
	        this.#historyRecords.get(group)?.size ?? 0,
	        this.#pages.get(pageKey(group, 0))?.page.total ?? 0
	      ),
	      0
	    ), incomplete = READER_NOTIFICATION_REACTION_LIKE_GROUPS.some(
	      (group) => !this.#historyGroups.get(group)?.complete
	    );
	    return Object.freeze({
	      group: "reactionLikes",
	      page,
	      records: Object.freeze(indexed.slice(start, end)),
	      total: Math.max(total, indexed.length),
	      hasNext: end < Math.max(total, indexed.length) || incomplete,
	      nextCursor: null
	    });
	  }
	  #indexedAggregatePage(page, indexed = this.#indexedAggregateRecords()) {
	    const aggregate = (0, import_reader_notification_model.readerNotificationGroup)("all"), start = page * aggregate.pageSize, end = start + aggregate.pageSize;
	    return Object.freeze({
	      group: "all",
	      page,
	      records: Object.freeze(indexed.slice(start, end)),
	      total: indexed.length,
	      hasNext: end < indexed.length,
	      nextCursor: null
	    });
	  }
	  #refreshAggregateHistoryPages() {
	    const indexed = this.#indexedAggregateRecords(), reactionLikes = this.#indexedReactionLikeRecords();
	    for (const [key, entry] of [...this.#pages])
	      key.startsWith("reactionLikes:") && this.#pages.set(key, Object.freeze({
	        page: this.#indexedReactionLikePage(entry.page.page, reactionLikes),
	        loadedAt: entry.loadedAt
	      }));
	    if (reactionLikes.length && !this.#pages.has(pageKey("reactionLikes", 0)) && this.#pages.set(pageKey("reactionLikes", 0), Object.freeze({
	      page: this.#indexedReactionLikePage(0, reactionLikes),
	      loadedAt: this.#now()
	    })), this.#group === "reactionLikes" && !this.#hasLocalFilters()) {
	      const pageSize2 = (0, import_reader_notification_model.readerNotificationGroup)("reactionLikes").pageSize, totalPages2 = Math.max(1, Math.ceil(
	        this.#indexedReactionLikePage(0, reactionLikes).total / pageSize2
	      ));
	      this.#page >= totalPages2 && (this.#page = totalPages2 - 1);
	      const page2 = this.#indexedReactionLikePage(this.#page, reactionLikes);
	      this.#pages.set(pageKey("reactionLikes", this.#page), Object.freeze({
	        page: page2,
	        loadedAt: this.#now()
	      })), this.#applyPage(page2);
	    }
	    if (!indexed.length) return;
	    for (const [key, entry] of [...this.#pages])
	      key.startsWith("all:") && this.#pages.set(key, Object.freeze({
	        page: this.#indexedAggregatePage(entry.page.page, indexed),
	        loadedAt: entry.loadedAt
	      }));
	    if (this.#pages.has(pageKey("all", 0)) || this.#pages.set(pageKey("all", 0), Object.freeze({
	      page: this.#indexedAggregatePage(0, indexed),
	      loadedAt: this.#now()
	    })), this.#group !== "all" || this.#hasLocalFilters()) return;
	    const pageSize = (0, import_reader_notification_model.readerNotificationGroup)("all").pageSize, totalPages = Math.max(1, Math.ceil(indexed.length / pageSize));
	    this.#page >= totalPages && (this.#page = totalPages - 1);
	    const page = this.#indexedAggregatePage(this.#page, indexed);
	    this.#pages.set(pageKey("all", this.#page), Object.freeze({
	      page,
	      loadedAt: this.#now()
	    })), this.#applyPage(page);
	  }
	  async #load(refresh) {
	    if (this.scope.destroyed) return;
	    if (this.#hasLocalFilters()) {
	      this.#renderFromCache();
	      return;
	    }
	    this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#historySchedule = null;
	    const epoch = ++this.#loadEpoch, key = pageKey(this.#group, this.#page), cached = this.#pages.get(key), authoritative = refresh || !!cached;
	    this.#loading = !cached, this.#refreshing = !!cached, this.#retrying = !1, this.#error = null, this.#stale = !1, cached && this.#applyPage(cached.page), this.#emit();
	    try {
	      let page = null;
	      for (let attempt = 0; attempt < 2; attempt += 1)
	        try {
	          page = await this.#loadRequestedPage(
	            this.#group,
	            this.#page,
	            authoritative && this.#page === 0 ? { refresh: !0 } : {}
	          );
	          break;
	        } catch (cause) {
	          if (attempt > 0 || !readerNotificationRequestCanAutoRetry(cause)) throw cause;
	          if (this.scope.destroyed || epoch !== this.#loadEpoch || (this.#retrying = !0, this.#emit(), await this.#delay(this.#retryDelayMs), this.scope.destroyed || epoch !== this.#loadEpoch)) return;
	        }
	      if (!page) throw new Error("通知自动重试未返回结果");
	      if (this.scope.destroyed || epoch !== this.#loadEpoch) return;
	      this.#cachePage(page), page.group !== "all" && page.group !== "reactionLikes" && this.#queueTopicTaxonomyEnrichment([page]), page.group === "all" && this.#inheritAllSyntheticPages(), authoritative && this.#lastAuthoritativeAt.set(key, this.#now()), this.#raiseUnreadCountForCachedRecords(), this.#applyPage(this.#pages.get(key)?.page ?? page), this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#stale = !1, this.#error = null, this.#emit(), this.#scheduleHistoryHydration(this.#historyContinuationDelay());
	    } catch (cause) {
	      if (this.scope.destroyed || epoch !== this.#loadEpoch) return;
	      this.#loading = !1, this.#refreshing = !1, this.#retrying = !1, this.#error = cause, this.#stale = !!cached;
	      const pollBackoffMs = readerNotificationPollBackoffMs(cause);
	      pollBackoffMs > 0 && (this.#pollNotBefore = Math.max(
	        this.#pollNotBefore,
	        this.#now() + pollBackoffMs
	      )), this.#onError(cause), this.#emit(), this.#schedulePoll(), this.#scheduleHistoryHydration(
	        Math.max(this.#historyRetryDelayMs, pollBackoffMs)
	      );
	    }
	  }
	  #indexHistoryPage(page) {
	    const state = this.#historyGroups.get(page.group);
	    if (!state) return;
	    state.retryAt = null, state.error = null;
	    const records = this.#historyRecords.get(page.group) ?? /* @__PURE__ */ new Map();
	    for (const record of page.records) records.set(record.identity, record);
	    this.#historyRecords.set(page.group, records), state.pages.add(page.page);
	    const group = (0, import_reader_notification_model.readerNotificationGroup)(page.group);
	    for (state.estimatedPages = Math.max(
	      state.estimatedPages,
	      page.page + 1 + (page.hasNext ? 1 : 0),
	      Math.ceil(page.total / group.pageSize)
	    ), page.hasNext || (state.terminalPage = page.page); state.pages.has(state.nextPage); ) state.nextPage += 1;
	    state.complete = state.terminalPage !== null && state.nextPage > state.terminalPage, state.complete && state.terminalPage !== null && (state.estimatedPages = Math.max(1, state.terminalPage + 1));
	  }
	  #cachePage(page, loadedAt = this.#now(), persist = !0) {
	    const key = pageKey(page.group, page.page), nativeRecords = this.#inheritNativeState(page.records), records = this.#inheritReadRecordState(nativeRecords), inherited = records === page.records ? page : Object.freeze({ ...page, records });
	    this.#pages.delete(key), this.#pages.set(key, Object.freeze({
	      page: inherited,
	      loadedAt
	    })), this.#indexHistoryPage(inherited), inherited.group !== "all" && this.#refreshAggregateHistoryPages(), this.#trimCachedPages(), persist && (this.#persistProjection(page.group), import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.includes(page.group) && this.#persistProjection("all"));
	  }
	  #persistProjection(group) {
	    if (!this.#projection || group === "reactionLikes")
	      return Promise.resolve();
	    if (this.#backgroundRestore !== null)
	      return this.#projectionPersistAfterRestore.add(group), Promise.resolve();
	    const prefix = `${group}:`, byIdentity = /* @__PURE__ */ new Map();
	    for (const record of this.#projectionRecords.get(group)?.values() ?? [])
	      byIdentity.set(record.identity, record);
	    for (const record of this.#historyRecords.get(group)?.values() ?? [])
	      byIdentity.set(record.identity, record);
	    for (const [key, entry] of this.#pages)
	      if (key.startsWith(prefix))
	        for (const record of entry.page.records)
	          byIdentity.set(record.identity, record);
	    const state = this.#historyGroups.get(group), cachedPages = [...this.#pages.entries()].filter(([key]) => key.startsWith(prefix)).map(([, entry]) => entry.page), complete = state?.complete === !0 || cachedPages.length > 0 && cachedPages.some((page) => !page.hasNext) && cachedPages.every((_, index, pages) => pages.some((candidate) => candidate.page === index)), exactReplacement = complete && (0, import_reader_notification_model.readerNotificationGroup)(group).source === "private-messages";
	    if (exactReplacement) {
	      byIdentity.clear();
	      for (const page of cachedPages)
	        for (const record of page.records)
	          byIdentity.set(record.identity, record);
	    }
	    const committedRecords = (0, import_reader_notification_model.sortReaderNotifications)([...byIdentity.values()]);
	    this.#projectionRecords.set(
	      group,
	      new Map(committedRecords.map((record) => [record.identity, record]))
	    );
	    const totalHint = cachedPages.reduce(
	      (total, page) => Math.max(total, page.total),
	      committedRecords.length
	    ), replaceCheckpoint = this.#projectionCheckpointReplacements.has(group);
	    return this.#projection.write(group, committedRecords, {
	      mergeStored: !exactReplacement,
	      totalHint,
	      complete,
	      updatedAt: this.#now(),
	      checkpointMode: replaceCheckpoint ? "replace" : "advance",
	      ...state ? { sourceNextPage: state.nextPage } : {},
	      ...state ? { sourcePageSize: (0, import_reader_notification_model.readerNotificationGroup)(group).pageSize } : {},
	      ...state ? {
	        sourceOffset: state.nextPage * (0, import_reader_notification_model.readerNotificationGroup)(group).pageSize
	      } : {}
	    }).then(() => {
	      replaceCheckpoint && this.#projectionCheckpointReplacements.delete(group);
	    }).catch(this.#onError);
	  }
	  #rememberProjectionRecords(group, records, replace = !1) {
	    const remember = (partition, values) => {
	      const indexed = replace ? /* @__PURE__ */ new Map() : this.#projectionRecords.get(partition) ?? /* @__PURE__ */ new Map();
	      for (const record of values) indexed.set(record.identity, record);
	      this.#projectionRecords.set(partition, indexed);
	    };
	    if (remember(group, records), group === "all")
	      for (const child of import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER)
	        remember(child, records.filter((record) => record.group === child));
	  }
	  #persistCachedProjections() {
	    const groups = /* @__PURE__ */ new Set();
	    for (const [key, entry] of this.#pages)
	      key.startsWith("native:") || groups.add(entry.page.group);
	    for (const group of groups) this.#persistProjection(group);
	  }
	  #trimCachedPages() {
	    for (; this.#pages.size > this.#maxCachedPages; ) {
	      const keys = [...this.#pages.keys()], selectedKey = pageKey(this.#group, this.#page), oldest = keys.find((key) => !key.startsWith("native:") && !key.endsWith(":0")) ?? keys.find((key) => !key.startsWith("native:") && key !== selectedKey) ?? keys.find((key) => key !== selectedKey) ?? keys[0];
	      if (oldest === void 0) break;
	      this.#pages.delete(oldest);
	    }
	  }
	  #cacheNativeAssociationPage(page) {
	    const key = nativePageKey(page.page), existing = this.#pages.get(key)?.page ?? null, records = existing ? page.records.flatMap((incoming) => {
	      const notificationId = incoming.sourceNotificationId;
	      if (notificationId === null) return [incoming];
	      const matches = existing.records.filter((record) => record.sourceNotificationId === notificationId);
	      return matches.some((record) => record.identity.startsWith(`notification:${notificationId}:reply:`)) ? matches.map((record) => Object.freeze({
	        ...record,
	        notificationTypeId: incoming.notificationTypeId,
	        highPriority: incoming.highPriority,
	        read: incoming.read,
	        stateLabel: incoming.read === !0 ? "已读" : incoming.read === !1 ? "未读" : record.stateLabel
	      })) : [incoming];
	    }) : page.records, inherited = this.#inheritReadRecordState(
	      (0, import_reader_notification_model.sortReaderNotifications)(records)
	    );
	    this.#pages.set(key, Object.freeze({
	      page: Object.freeze({
	        ...page,
	        records: inherited
	      }),
	      loadedAt: this.#now()
	    }));
	    for (const [cachedKey, entry] of [...this.#pages])
	      !cachedKey.startsWith("all:") || entry.page.total >= page.total || this.#pages.set(cachedKey, Object.freeze({
	        ...entry,
	        page: Object.freeze({ ...entry.page, total: page.total })
	      }));
	    this.#trimCachedPages();
	  }
	  #nativeAssociationNeedsExpansion(page) {
	    const existing = this.#pages.get(nativePageKey(page.page))?.page.records ?? [];
	    return page.records.some((record) => {
	      if (record.sourceNotificationId === null || record.aggregateCount === null) return !1;
	      const prefix = `notification:${record.sourceNotificationId}:reply:`;
	      return existing.filter((candidate) => candidate.identity.startsWith(prefix)).length < record.aggregateCount;
	    });
	  }
	  #rememberReadRecord(record) {
	    const key = readRecordKey(record);
	    if (key === null) return;
	    this.#readRecordKeys.delete(key), this.#readRecordKeys.add(key);
	    const maxRecords = Math.max(64, this.#maxCachedPages * 64);
	    for (; this.#readRecordKeys.size > maxRecords; ) {
	      const oldest = this.#readRecordKeys.values().next().value;
	      if (oldest === void 0) break;
	      this.#readRecordKeys.delete(oldest);
	    }
	  }
	  #childReadSourceIds(additionalRecords = Object.freeze([])) {
	    const keys = /* @__PURE__ */ new Map(), records = [
	      ...[...this.#pages.values()].flatMap((entry) => entry.page.records),
	      ...additionalRecords
	    ];
	    for (const record of records) {
	      const notificationId = record.sourceNotificationId, key = readRecordKey(record);
	      if (notificationId === null || key === null) continue;
	      const sourceKeys = keys.get(notificationId) ?? /* @__PURE__ */ new Set();
	      sourceKeys.add(key), keys.set(notificationId, sourceKeys);
	    }
	    return new Set(
	      [...keys].filter(([, sourceKeys]) => sourceKeys.size > 1).map(([notificationId]) => notificationId)
	    );
	  }
	  #inheritReadRecordState(records) {
	    let changed = !1;
	    const inherited = records.map((record) => {
	      const key = readRecordKey(record);
	      return record.read === !0 ? (this.#rememberReadRecord(record), record) : record.sourceNotificationId === null || key === null || !this.#readRecordKeys.has(key) ? record : (changed = !0, Object.freeze({
	        ...record,
	        read: !0,
	        stateLabel: "已读"
	      }));
	    });
	    return changed ? Object.freeze(inherited) : records;
	  }
	  #setReadRecordState(record, read) {
	    const targetKey = readRecordKey(record);
	    if (targetKey !== null) {
	      read ? this.#rememberReadRecord(record) : this.#readRecordKeys.delete(targetKey);
	      for (const [key, entry] of [...this.#pages]) {
	        let changed = !1;
	        const records = Object.freeze(entry.page.records.map((candidate) => readRecordKey(candidate) !== targetKey ? candidate : (changed = !0, Object.freeze({
	          ...candidate,
	          read,
	          stateLabel: read ? "已读" : "未读"
	        }))));
	        changed && this.#pages.set(key, Object.freeze({
	          ...entry,
	          page: Object.freeze({ ...entry.page, records })
	        }));
	      }
	      this.#persistCachedProjections(), this.#renderFromCache();
	    }
	  }
	  #allSourceRecordsRead(notificationId) {
	    const states = /* @__PURE__ */ new Map();
	    for (const entry of this.#pages.values())
	      for (const record of entry.page.records) {
	        if (record.sourceNotificationId !== notificationId) continue;
	        const key = readRecordKey(record);
	        key !== null && states.set(key, (states.get(key) ?? !0) && record.read === !0);
	      }
	    return states.size > 0 && [...states.values()].every(Boolean);
	  }
	  #inheritNativeState(records) {
	    const nativeRecords = [...this.#pages.entries()].filter(([key]) => key.startsWith("native:")).flatMap(([, entry]) => entry.page.records).filter((record) => record.sourceNotificationId !== null);
	    return nativeRecords.length ? Object.freeze(records.map((record) => {
	      const candidates = nativeRecords.filter((native) => native.group === record.group && sameTarget(native, record)), actor = record.actor.toLocaleLowerCase(), match = candidates.find((native) => native.actor.toLocaleLowerCase() === actor) ?? null;
	      return match ? Object.freeze({
	        ...record,
	        sourceNotificationId: match.sourceNotificationId,
	        notificationTypeId: match.notificationTypeId,
	        highPriority: match.highPriority,
	        read: match.read,
	        stateLabel: match.read === !0 ? "已读" : match.read === !1 ? "未读" : record.stateLabel
	      }) : record.sourceNotificationId === null ? record : Object.freeze({
	        ...record,
	        sourceNotificationId: null,
	        notificationTypeId: null,
	        highPriority: !1,
	        read: null,
	        stateLabel: ""
	      });
	    })) : records;
	  }
	  #inheritAllSyntheticPages() {
	    for (const [key, entry] of [...this.#pages]) {
	      if (key.startsWith("native:")) continue;
	      const records = this.#inheritReadRecordState(
	        this.#inheritNativeState(entry.page.records)
	      );
	      records !== entry.page.records && (this.#pages.set(key, Object.freeze({
	        ...entry,
	        page: Object.freeze({ ...entry.page, records })
	      })), this.#indexHistoryPage(Object.freeze({ ...entry.page, records })));
	    }
	    this.#refreshAggregateHistoryPages();
	  }
	  #cachedGroupRecords() {
	    const seen = /* @__PURE__ */ new Set(), records = [], indexedGroups = this.#group === "all" ? import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER : this.#group === "reactionLikes" ? READER_NOTIFICATION_REACTION_LIKE_GROUPS : Object.freeze([this.#group]);
	    for (const group of indexedGroups)
	      for (const record of this.#historyRecords.get(group)?.values() ?? [])
	        seen.has(record.identity) || (seen.add(record.identity), records.push(record));
	    const prefix = `${this.#group}:`;
	    for (const [key, entry] of this.#pages)
	      if (key.startsWith(prefix))
	        for (const record of entry.page.records)
	          seen.has(record.identity) || (seen.add(record.identity), records.push(record));
	    return (0, import_reader_notification_model.sortReaderNotifications)(this.#inheritReadRecordState(records));
	  }
	  #hasLocalFilters() {
	    return !!(this.#query || this.#categoryFilter || this.#tagFilter || this.#dateFilter || this.#sortDirection !== "desc");
	  }
	  #dayCounts() {
	    const counts = /* @__PURE__ */ new Map();
	    for (const record of this.#cachedGroupRecords()) {
	      const day = (0, import_reader_collection_filter_model.readerCollectionDateKey)(record.createdAt);
	      day && counts.set(day, (counts.get(day) ?? 0) + 1);
	    }
	    return new Map([...counts].sort(([left], [right]) => left.localeCompare(right)));
	  }
	  #categoryOptions() {
	    return this.#filterOptions("category");
	  }
	  #tagOptions() {
	    return this.#filterOptions("tag");
	  }
	  #filterOptions(kind) {
	    const options = /* @__PURE__ */ new Map();
	    for (const record of this.#cachedGroupRecords()) {
	      const values = kind === "category" ? [[
	        (0, import_reader_notification_model.readerNotificationCategoryFilterKey)(record),
	        record.categoryName || `类别 #${record.categoryId}`
	      ]] : record.tags.map((tag) => [
	        (0, import_reader_notification_model.readerNotificationTagFilterKey)(tag),
	        tag
	      ]);
	      for (const [value, label] of values) {
	        if (!value) continue;
	        const current = options.get(value);
	        options.set(value, Object.freeze({
	          value,
	          label: current?.label.startsWith("类别 #") && record.categoryName ? record.categoryName : current?.label ?? label,
	          count: (current?.count ?? 0) + 1
	        }));
	      }
	    }
	    return Object.freeze([...options.values()].sort((left, right) => right.count - left.count || left.label.localeCompare(right.label, "zh-CN")));
	  }
	  #matchingRecords() {
	    const records = this.#cachedGroupRecords().filter((record) => (0, import_reader_search.readerSearchMatches)(
	      record.searchText,
	      this.#query,
	      this.#searchForms,
	      this.#onError
	    ) && (!this.#categoryFilter || (0, import_reader_notification_model.readerNotificationCategoryFilterKey)(record) === this.#categoryFilter) && (!this.#tagFilter || record.tags.some((tag) => (0, import_reader_notification_model.readerNotificationTagFilterKey)(tag) === this.#tagFilter)) && (!this.#dateFilter || (0, import_reader_collection_filter_model.readerCollectionDateKey)(record.createdAt) === this.#dateFilter));
	    return this.#sortDirection === "asc" ? Object.freeze([...records].reverse()) : records;
	  }
	  #renderFromCache() {
	    if (this.#hasLocalFilters()) {
	      const pageSize = (0, import_reader_notification_model.readerNotificationGroup)(this.#group).pageSize, matches = this.#matchingRecords(), totalPages = Math.max(1, Math.ceil(matches.length / pageSize));
	      this.#page >= totalPages && (this.#page = totalPages - 1);
	      const start = this.#page * pageSize;
	      this.#records = Object.freeze(matches.slice(start, start + pageSize)), this.#total = matches.length, this.#hasNext = this.#page < totalPages - 1, this.#loading = !1, this.#refreshing = !1, this.#error = null, this.#stale = !1, this.#emit();
	      return;
	    }
	    const cached = this.#pages.get(pageKey(this.#group, this.#page));
	    cached ? this.#applyPage(cached.page) : (this.#records = Object.freeze([]), this.#total = 0, this.#hasNext = !1), this.#emit();
	  }
	  #applyPage(page) {
	    this.#records = page.records, this.#total = page.total, this.#hasNext = page.hasNext, this.#loading = !1;
	  }
	  #raiseUnreadCountForCachedRecords() {
	    const unreadSourceIds = /* @__PURE__ */ new Set();
	    for (const entry of this.#pages.values())
	      for (const record of entry.page.records)
	        record.sourceNotificationId !== null && record.read === !1 && unreadSourceIds.add(record.sourceNotificationId);
	    this.#unreadCount = Math.max(this.#unreadCount, unreadSourceIds.size);
	  }
	  #markSourceReadInCache(notificationId) {
	    let committed = null, changed = !1;
	    for (const [key, entry] of [...this.#pages]) {
	      let pageChanged = !1;
	      const records = Object.freeze(entry.page.records.map((record) => record.sourceNotificationId !== notificationId || (committed ??= record, this.#rememberReadRecord(record), record.read === !0) ? record : (changed = !0, pageChanged = !0, Object.freeze({
	        ...record,
	        read: !0,
	        stateLabel: "已读"
	      }))));
	      pageChanged && this.#pages.set(key, Object.freeze({
	        ...entry,
	        page: Object.freeze({ ...entry.page, records })
	      }));
	    }
	    return Object.freeze({ committed, changed });
	  }
	  #commitAllRead() {
	    for (const [key, entry] of [...this.#pages]) {
	      const records = Object.freeze(entry.page.records.map((record) => record.sourceNotificationId === null ? record : (this.#rememberReadRecord(record), Object.freeze({
	        ...record,
	        read: !0,
	        stateLabel: "已读"
	      }))));
	      this.#pages.set(key, Object.freeze({
	        ...entry,
	        page: Object.freeze({ ...entry.page, records })
	      }));
	    }
	    this.#native.markAllRead(), this.#unreadCount = 0, this.#persistCachedProjections(), this.#renderFromCache();
	  }
	  #commitRead(notificationId) {
	    const { committed } = this.#markSourceReadInCache(notificationId);
	    committed && (this.#native.markRead({
	      notificationTypeId: committed.notificationTypeId,
	      highPriority: committed.highPriority
	    }), this.#unreadCount = Math.max(0, this.#unreadCount - 1), this.#raiseUnreadCountForCachedRecords(), this.#persistCachedProjections(), this.#renderFromCache());
	  }
	  #onNativeClicked(click) {
	    if (this.scope.destroyed) return;
	    const { changed } = this.#markSourceReadInCache(click.notificationId);
	    click.wasRead === !1 && (changed || this.#unreadCount > 0) && (this.#unreadCount = Math.max(0, this.#unreadCount - 1), this.#raiseUnreadCountForCachedRecords(), this.#persistCachedProjections(), this.#renderFromCache()), this.#onNativeChanged(!0);
	  }
	  #consumeNativeRefreshPending() {
	    return !this.#nativeRefreshPending && !this.#nativeChangePending ? !1 : (this.#nativeRefreshPending = !1, this.#liveRefresh !== null && this.#cancel(this.#liveRefresh), this.#liveRefresh = null, this.#invalidateLivePages(), !0);
	  }
	  #invalidateLivePages() {
	    for (const key of [...this.#pages.keys()]) {
	      if (key.startsWith("native:") || key.startsWith("all:")) {
	        this.#pages.delete(key);
	        continue;
	      }
	      const separator = key.lastIndexOf(":");
	      separator >= 0 && Number(key.slice(separator + 1)) === 0 && this.#pages.delete(key);
	    }
	    for (const key of [...this.#lastAuthoritativeAt.keys()])
	      (key.startsWith("native:") || key.startsWith("all:") || key.endsWith(":0")) && this.#lastAuthoritativeAt.delete(key);
	  }
	  async #refreshAfterNativeChange() {
	    if (this.scope.destroyed) return;
	    if (this.#hasLocalFilters()) {
	      this.#nativeRefreshPending = !0, this.#renderFromCache();
	      return;
	    }
	    const changeEpoch = this.#nativeChangeEpoch, selectedGroup = this.#group, selectedPage = this.#page, source = (0, import_reader_notification_model.readerNotificationGroup)(selectedGroup).source;
	    try {
	      if (selectedGroup === "all" || source === "user-actions" || source === "boosts-received" || source === "reactions-received") {
	        const selectedRefresh = this.#load(!0), nativeKey = nativePageKey(0), nativeRefresh = this.#nativeChangePending || this.#shouldRevalidate(nativeKey) ? this.#refreshNativeAssociation(
	          changeEpoch,
	          selectedGroup,
	          selectedPage
	        ) : Promise.resolve();
	        await selectedRefresh, await nativeRefresh;
	        return;
	      }
	      await this.#load(!0);
	    } finally {
	      !this.scope.destroyed && changeEpoch === this.#nativeChangeEpoch && (this.#nativeChangePending = !1, this.#nativeRefreshPending = !1);
	    }
	  }
	  async #refreshAllHeadAfterNativeChange() {
	    if (this.scope.destroyed) return;
	    const changeEpoch = this.#nativeChangeEpoch, selectedGroup = this.#group, selectedPage = this.#page, selectedFiltering = this.#hasLocalFilters(), valid = () => !this.scope.destroyed && changeEpoch === this.#nativeChangeEpoch;
	    try {
	      const page = await this.#loadAggregatePage(0, {
	        refresh: !0,
	        ...this.#open && selectedGroup === "all" && !selectedFiltering ? {} : { background: !0 },
	        valid
	      });
	      if (!valid() || (this.#cachePage(page), this.#lastAuthoritativeAt.set(pageKey("all", 0), this.#now()), this.#raiseUnreadCountForCachedRecords(), this.#open && !selectedFiltering && selectedPage === 0 && selectedGroup === this.#group && (selectedGroup === "all" || selectedGroup === "reactionLikes" || import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.includes(selectedGroup)) && this.#renderFromCache(), await this.#refreshNativeAssociation(
	        changeEpoch,
	        selectedGroup,
	        selectedPage
	      ), !valid())) return;
	      const inboxKey = pageKey("inbox", 0);
	      if (this.#native.username().trim() && !this.#pages.has(inboxKey)) {
	        const inbox = await this.#requests.load("inbox", 0, {
	          refresh: !0,
	          background: !0
	        });
	        if (!valid()) return;
	        this.#cachePage(inbox), this.#lastAuthoritativeAt.set(inboxKey, this.#now());
	      }
	      const selectedSource = (0, import_reader_notification_model.readerNotificationGroup)(selectedGroup).source;
	      this.#open && !selectedFiltering && selectedGroup === this.#group && selectedPage === this.#page && selectedSource === "private-messages" && selectedGroup !== "inbox" ? await this.#load(!0) : this.#open && !selectedFiltering && selectedGroup === "inbox" && selectedGroup === this.#group && selectedPage === this.#page && this.#renderFromCache();
	    } catch (cause) {
	      if (!valid()) return;
	      const pollBackoffMs = readerNotificationPollBackoffMs(cause);
	      pollBackoffMs > 0 && (this.#pollNotBefore = Math.max(
	        this.#pollNotBefore,
	        this.#now() + pollBackoffMs
	      )), this.#onError(cause);
	    } finally {
	      valid() && (this.#nativeChangePending = !1, this.#nativeRefreshPending = !!(this.#hasLocalFilters() && (0, import_reader_notification_model.readerNotificationGroup)(this.#group).source === "private-messages"));
	    }
	  }
	  async #refreshNativeAssociation(changeEpoch, selectedGroup, selectedPage) {
	    try {
	      const nativePage = await this.#requests.load("all", 0, {
	        refresh: !0,
	        background: !0,
	        expandConsolidated: !1
	      });
	      if (this.scope.destroyed || changeEpoch !== this.#nativeChangeEpoch) return;
	      const needsExpansion = this.#nativeAssociationNeedsExpansion(nativePage);
	      if (this.#cacheNativeAssociationPage(nativePage), this.#lastAuthoritativeAt.set(nativePageKey(0), this.#now()), this.#inheritAllSyntheticPages(), this.#raiseUnreadCountForCachedRecords(), selectedGroup === this.#group && selectedPage === this.#page && this.#pages.has(pageKey(selectedGroup, selectedPage)) && this.#renderFromCache(), !needsExpansion) return;
	      const expandedPage = await this.#requests.load("all", 0, {
	        background: !0,
	        expandConsolidated: !0
	      });
	      if (this.scope.destroyed || changeEpoch !== this.#nativeChangeEpoch) return;
	      this.#cacheNativeAssociationPage(expandedPage), this.#inheritAllSyntheticPages(), this.#raiseUnreadCountForCachedRecords(), selectedGroup === this.#group && selectedPage === this.#page && this.#pages.has(pageKey(selectedGroup, selectedPage)) && this.#renderFromCache();
	    } catch (cause) {
	      if (this.scope.destroyed || changeEpoch !== this.#nativeChangeEpoch) return;
	      const pollBackoffMs = readerNotificationPollBackoffMs(cause);
	      pollBackoffMs > 0 && (this.#pollNotBefore = Math.max(
	        this.#pollNotBefore,
	        this.#now() + pollBackoffMs
	      )), this.#onError(cause);
	    }
	  }
	  #shouldRevalidate(key) {
	    const observedAt = this.#lastAuthoritativeAt.get(key) ?? this.#pages.get(key)?.loadedAt;
	    return observedAt === void 0 || this.#now() - observedAt >= this.#openRevalidateMs;
	  }
	  #activityVisible() {
	    if (!this.#activity) return !1;
	    try {
	      return this.#activity.visible();
	    } catch (cause) {
	      return this.#onError(cause), !1;
	    }
	  }
	  #cancelPoll() {
	    this.#poll !== null && (this.#cancel(this.#poll), this.#poll = null);
	  }
	  #schedulePoll() {
	    if (this.#cancelPoll(), !this.#open || !this.#activityVisible() || this.scope.destroyed) return;
	    const delayMs = this.#pollNotBefore - this.#now();
	    delayMs > 0 && (this.#poll = this.#schedule(() => {
	      if (this.#poll = null, !(!this.#open || !this.#activityVisible() || this.scope.destroyed)) {
	        if (this.#loading || this.#refreshing || this.#selectionFlight !== null) {
	          this.#schedulePoll();
	          return;
	        }
	        this.#unreadCount = this.#native.unreadCount(), this.#runSelectedRequest(() => this.#refreshAfterNativeChange()).finally(() => {
	          this.#schedulePoll();
	        });
	      }
	    }, delayMs));
	  }
	  #activityRecoveryDue(key) {
	    const observedAt = this.#lastAuthoritativeAt.get(key) ?? this.#pages.get(key)?.loadedAt;
	    if (observedAt === void 0) return !1;
	    const source = (0, import_reader_notification_model.readerNotificationGroup)(this.#group).source, recoveryMs = this.#group === "all" || source === "user-actions" || source === "boosts-received" || source === "reactions-received" ? this.#syntheticPollIntervalMs : this.#nativePollIntervalMs;
	    return this.#now() - observedAt >= Math.max(
	      this.#openRevalidateMs,
	      recoveryMs
	    );
	  }
	  #onActivityChanged() {
	    if (this.scope.destroyed) return;
	    const visible = this.#activityVisible(), backgroundWasActive = this.#backgroundCacheActive;
	    if (visible ? this.#backgroundCacheActive || this.startBackgroundCache() : (this.#backgroundWarm !== null && this.#cancel(this.#backgroundWarm), this.#backgroundWarm = null, this.#backgroundWarmPending = !1, this.#backgroundWarmEpoch += 1), this.#backgroundCacheActive && (this.#activity && !visible ? (this.#historySchedule !== null && this.#cancel(this.#historySchedule), this.#historySchedule = null, this.#historyStatus !== "complete" && (this.#historyStatus = "paused", this.#historyCurrentGroup = null, this.#emit())) : backgroundWasActive && (this.#scheduleHistoryHydration(0), this.#scheduleBackgroundWarm(0))), !visible) {
	      this.#cancelPoll();
	      return;
	    }
	    this.#schedulePoll();
	    const key = this.#open ? pageKey(this.#group, this.#page) : pageKey("all", 0);
	    this.#open && this.#hasLocalFilters() || this.#loading || this.#refreshing || this.#selectionFlight !== null || !this.#activityRecoveryDue(key) || (this.#unreadCount = this.#native.unreadCount(), this.#runSelectedRequest(() => this.#open ? this.#refreshAfterNativeChange() : this.#refreshAllHeadAfterNativeChange()));
	  }
	  #historyContinuationDelay() {
	    return this.#open && this.#visibleHistoryConcurrency > 1 ? 0 : this.#historyStepDelayMs;
	  }
	  #historyHasReadyGroup(now = this.#now()) {
	    return [...this.#historyGroups.values()].some((state) => !state.complete && (state.retryAt === null || state.retryAt <= now));
	  }
	  #historyRecoveryState() {
	    let retryAt = null, error = null;
	    for (const state of this.#historyGroups.values())
	      state.complete || state.retryAt === null || retryAt !== null && state.retryAt >= retryAt || (retryAt = state.retryAt, error = state.error);
	    return Object.freeze({ error, retryAt });
	  }
	  #scheduleHistoryHydration(delayMs = this.#historyContinuationDelay()) {
	    this.#backgroundWarmDelayMs === null || !this.#backgroundCacheActive || this.scope.destroyed || this.#historyLoading || this.#historySchedule !== null || this.#historyStatus === "complete" || !this.#native.username().trim() || this.#activity !== null && !this.#activityVisible() || (this.#historySchedule = this.#schedule(() => {
	      this.#historySchedule = null, this.#runHistoryHydrationStep();
	    }, Math.max(0, delayMs)));
	  }
	  #nextHistoryGroups(limit) {
	    const groups = [];
	    let lastIndex = -1;
	    for (let offset = 0; offset < import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.length; offset += 1) {
	      const index = (this.#historyCursor + offset) % import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.length, group = import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER[index], state = this.#historyGroups.get(group);
	      if (!(this.#historyInFlightGroups.has(group) || state.complete || state.retryAt !== null && state.retryAt > this.#now()) && (state.retryAt = null, state.error = null, groups.push(group), lastIndex = index, groups.length >= limit))
	        break;
	    }
	    return lastIndex >= 0 && (this.#historyCursor = (lastIndex + 1) % import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.length), Object.freeze(groups);
	  }
	  async #hydrateHistoryGroup(group, epoch, visibleHistory) {
	    const state = this.#historyGroups.get(group), descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group), pages = [state.nextPage];
	    if (visibleHistory && descriptor.source === "user-actions") {
	      const upperBound = Math.max(state.nextPage + 1, state.estimatedPages);
	      for (let page = state.nextPage + 1; page < upperBound && pages.length < this.#visibleHistoryConcurrency; page += 1)
	        state.pages.has(page) || pages.push(page);
	    }
	    try {
	      const loadedPages = await Promise.all(pages.map(async (page) => {
	        let loaded = null;
	        try {
	          loaded = await this.#loadCachedRequestedPage(group, page);
	        } catch (cause) {
	          this.#onError(cause);
	        }
	        const cacheHit = loaded !== null;
	        return loaded ??= await this.#loadRequestedPage(group, page, {
	          history: !0,
	          ...visibleHistory ? { visibleHistory: !0 } : {}
	        }), Object.freeze({ page, loaded, cacheHit });
	      }));
	      if (this.scope.destroyed || epoch !== this.#historyEpoch) return null;
	      for (const result of loadedPages.sort((left, right) => left.page - right.page))
	        state.terminalPage !== null && result.page > state.terminalPage || (this.#cachePage(result.loaded, this.#now(), !1), result.cacheHit || this.#queueTopicTaxonomyEnrichment([result.loaded], { history: !0 }));
	      return (state.complete || state.nextPage % HISTORY_PROJECTION_BATCH_PAGES === 0) && await this.#persistProjection(group), loadedPages.every((result) => result.cacheHit);
	    } catch (cause) {
	      if (this.scope.destroyed || epoch !== this.#historyEpoch) return null;
	      this.#onError(cause);
	      const backoffMs = readerNotificationPollBackoffMs(cause), retryDelay = Math.max(this.#historyRetryDelayMs, backoffMs);
	      return state.retryAt = this.#now() + retryDelay, state.error = cause, null;
	    }
	  }
	  async #runHistoryHydrationStep() {
	    if (!this.#backgroundCacheActive || this.scope.destroyed || this.#historyLoading) return;
	    if (this.#activity && !this.#activityVisible()) {
	      this.#historyStatus = "paused", this.#historyCurrentGroup = null, this.#emit();
	      return;
	    }
	    const rateLimitBackoffMs = this.#pollNotBefore - this.#now();
	    if (rateLimitBackoffMs > 0) {
	      this.#historyStatus = "paused", this.#historyCurrentGroup = null, this.#historyRetryAt = this.#pollNotBefore, this.#emit(), this.#scheduleHistoryHydration(
	        Math.max(this.#historyRetryDelayMs, rateLimitBackoffMs)
	      );
	      return;
	    }
	    if (this.#loading || this.#refreshing || this.#retrying || this.#markingAll || this.#selectionFlight !== null) {
	      this.#historyStatus = "paused", this.#historyCurrentGroup = null, this.#historyRetryAt = null, this.#emit(), this.#scheduleHistoryHydration(this.#historyStepDelayMs);
	      return;
	    }
	    if (!this.#historyHasReadyGroup()) {
	      const complete2 = [...this.#historyGroups.values()].every(
	        (state) => state.complete
	      ), recovery2 = this.#historyRecoveryState();
	      this.#historyStatus = complete2 ? "complete" : recovery2.retryAt === null ? "paused" : "error", this.#historyCurrentGroup = null, this.#historyError = recovery2.error, this.#historyRetryAt = recovery2.retryAt, this.#emit(), !complete2 && recovery2.retryAt !== null && this.#scheduleHistoryHydration(
	        Math.max(0, recovery2.retryAt - this.#now())
	      );
	      return;
	    }
	    const epoch = this.#historyEpoch, visibleHistory = this.#open && this.#visibleHistoryConcurrency > 1, openAtStart = this.#open, concurrency = visibleHistory ? this.#visibleHistoryConcurrency : 1;
	    this.#historyLoading = !0, this.#historyStatus = "loading", this.#historyCurrentGroup = null;
	    const pendingRecovery = this.#historyRecoveryState();
	    this.#historyError = pendingRecovery.error, this.#historyRetryAt = pendingRecovery.retryAt, this.#emit();
	    const results = [], dirtyGroups = /* @__PURE__ */ new Set();
	    try {
	      await (0, import_reader_collection_hydration.runReaderCollectionHydrationLease)({
	        coordination: this.#historyCoordination ?? null,
	        token: this.#historyCoordinationKey,
	        signal: this.#historyAbort.signal,
	        onError: this.#onError,
	        beforeRun: () => this.#restoreBackgroundProjections(!0),
	        run: async () => {
	          await (0, import_reader_collection_hydration.runReaderCollectionWorkers)({
	            concurrency,
	            maxTasks: visibleHistory ? concurrency * VISIBLE_HISTORY_LEASE_ROUNDS : 1,
	            shouldContinue: () => !this.scope.destroyed && epoch === this.#historyEpoch && this.#open === openAtStart && !this.#loading && !this.#refreshing && !this.#retrying && !this.#markingAll && this.#selectionFlight === null && (!this.#activity || this.#activityVisible()),
	            claim: () => {
	              const group = this.#nextHistoryGroups(1)[0] ?? null;
	              return group !== null && this.#historyInFlightGroups.add(group), group;
	            },
	            release: (group) => {
	              this.#historyInFlightGroups.delete(group);
	            },
	            run: async (group) => {
	              concurrency === 1 && (this.#historyCurrentGroup = group, this.#emit()), results.push(await this.#hydrateHistoryGroup(
	                group,
	                epoch,
	                visibleHistory
	              ));
	              const state = this.#historyGroups.get(group);
	              state.complete || state.nextPage % HISTORY_PROJECTION_BATCH_PAGES === 0 ? dirtyGroups.delete(group) : dirtyGroups.add(group), !(this.scope.destroyed || epoch !== this.#historyEpoch) && (this.#hasLocalFilters() && this.#open ? this.#renderFromCache() : this.#emit());
	            }
	          }), dirtyGroups.size && (await Promise.all([...dirtyGroups].map((group) => this.#persistProjection(group))), dirtyGroups.clear());
	        }
	      }) !== "producer" && !this.scope.destroyed && epoch === this.#historyEpoch && await this.#restoreBackgroundProjections(!0);
	    } catch (cause) {
	      !this.scope.destroyed && epoch === this.#historyEpoch && (this.#onError(cause), this.#historyError = cause, this.#historyRetryAt = this.#now() + this.#historyRetryDelayMs);
	    } finally {
	      this.#historyLoading = !1, this.#historyInFlightGroups.clear();
	    }
	    if (this.scope.destroyed || epoch !== this.#historyEpoch) return;
	    const complete = [...this.#historyGroups.values()].every((candidate) => candidate.complete), recovery = this.#historyRecoveryState(), ready = this.#historyHasReadyGroup();
	    if (this.#historyStatus = complete ? "complete" : ready ? "loading" : recovery.retryAt === null ? "paused" : "error", this.#historyCurrentGroup = null, this.#historyError = recovery.error, this.#historyRetryAt = recovery.retryAt, this.#hasLocalFilters() && this.#open ? this.#renderFromCache() : this.#emit(), complete) return;
	    let nextDelay = results.length > 0 && results.every((result) => result === !0) ? 0 : this.#historyContinuationDelay();
	    !ready && recovery.retryAt !== null && (nextDelay = Math.max(0, recovery.retryAt - this.#now())), this.#scheduleHistoryHydration(nextDelay);
	  }
	  #scheduleBackgroundWarm(delayMs = this.#backgroundWarmDelayMs ?? 0) {
	    if (!(this.#backgroundWarmDelayMs === null || !this.#backgroundCacheActive || this.scope.destroyed || this.#activity !== null && !this.#activityVisible())) {
	      if (this.#backgroundWarming) {
	        this.#backgroundWarmPending = !0;
	        return;
	      }
	      this.#backgroundWarm !== null && this.#cancel(this.#backgroundWarm), this.#backgroundWarm = this.#schedule(() => {
	        this.#backgroundWarm = null, this.#warmBackgroundCollections();
	      }, Math.max(0, delayMs));
	    }
	  }
	  async #warmBackgroundCollections() {
	    if (!this.#backgroundCacheActive || this.scope.destroyed || this.#backgroundWarming || this.#activity !== null && !this.#activityVisible()) return;
	    const rateLimitBackoffMs = this.#pollNotBefore - this.#now();
	    if (rateLimitBackoffMs > 0) {
	      this.#scheduleBackgroundWarm(rateLimitBackoffMs);
	      return;
	    }
	    this.#backgroundWarming = !0, this.#backgroundWarmPending = !1;
	    const epoch = ++this.#backgroundWarmEpoch, valid = () => !this.scope.destroyed && epoch === this.#backgroundWarmEpoch && (this.#activity === null || this.#activityVisible());
	    try {
	      const signedIn = !!this.#native.username().trim();
	      let needsNativeExpansion = !1;
	      try {
	        const key = nativePageKey(0), cached = this.#pages.get(key)?.page ?? null;
	        if (cached) needsNativeExpansion = this.#nativeAssociationNeedsExpansion(cached);
	        else {
	          const loaded = await this.#requests.load("all", 0, {
	            background: !0,
	            expandConsolidated: !1
	          });
	          if (!valid()) return;
	          needsNativeExpansion = this.#nativeAssociationNeedsExpansion(loaded), this.#pages.has(key) || this.#cacheNativeAssociationPage(loaded);
	        }
	      } catch (cause) {
	        valid() && this.#onError(cause);
	      }
	      if (!signedIn || !valid()) return;
	      try {
	        const key = pageKey("inbox", 0);
	        if (!this.#pages.has(key)) {
	          const loaded = await this.#requests.load("inbox", 0, {
	            background: !0
	          });
	          if (!valid()) return;
	          this.#pages.has(key) || (this.#cachePage(loaded), this.#queueTopicTaxonomyEnrichment([loaded], {
	            background: !0
	          }));
	        }
	      } catch (cause) {
	        valid() && this.#onError(cause);
	      }
	      try {
	        const key = pageKey("all", 0);
	        if (!this.#pages.has(key)) {
	          const loaded = await this.#loadAggregatePage(0, {
	            background: !0,
	            history: !0,
	            valid
	          });
	          if (!valid()) return;
	          this.#pages.has(key) || this.#cachePage(loaded);
	        }
	      } catch (cause) {
	        valid() && this.#onError(cause);
	      }
	      if (needsNativeExpansion && valid())
	        try {
	          const expanded = await this.#requests.load("all", 0, {
	            background: !0,
	            expandConsolidated: !0
	          });
	          if (!valid()) return;
	          this.#cacheNativeAssociationPage(expanded);
	        } catch (cause) {
	          valid() && this.#onError(cause);
	        }
	      this.#inheritAllSyntheticPages(), this.#raiseUnreadCountForCachedRecords();
	    } finally {
	      this.#backgroundWarming = !1, this.#backgroundWarmPending && !this.scope.destroyed && (this.#backgroundWarmPending = !1, this.#scheduleBackgroundWarm()), this.#scheduleHistoryHydration(this.#historyContinuationDelay());
	    }
	  }
	  async #onNativeChanged(preserveUnreadCount = !1) {
	    if (this.scope.destroyed) return;
	    this.#nativeChangeEpoch += 1, preserveUnreadCount || (this.#unreadCount = this.#native.unreadCount());
	    const alreadyPending = this.#nativeChangePending;
	    if (this.#nativeChangePending = !0, alreadyPending)
	      this.#hasLocalFilters() && (this.#nativeRefreshPending = !0);
	    else {
	      try {
	        await this.#cache.invalidate({ tags: ["notifications"] });
	      } catch (cause) {
	        this.#onError(cause);
	      }
	      this.#hasLocalFilters() ? this.#nativeRefreshPending = !0 : this.#invalidateLivePages(), this.#backgroundWarmEpoch += 1, this.#backgroundWarm !== null && (this.#cancel(this.#backgroundWarm), this.#backgroundWarm = null);
	    }
	    if (this.#emit(), this.#liveRefresh !== null) return;
	    const refreshSelectedImmediately = this.#open && !this.#hasLocalFilters() && (!this.#activity || this.#activityVisible());
	    this.#liveRefresh = this.#schedule(() => {
	      this.#liveRefresh = null, !this.scope.destroyed && this.#runSelectedRequest(() => refreshSelectedImmediately ? this.#refreshAfterNativeChange() : this.#refreshAllHeadAfterNativeChange());
	    }, refreshSelectedImmediately ? 0 : this.#liveRefreshDelayMs);
	  }
	  #emit() {
	    this.#revision += 1, this.#snapshotCache = null, this.changes.emit(this.snapshot).forEach(this.#onError);
	  }
	}
}, "5074f813cfa85bdfa8b92ab2c1b7cda77a42ae44914ec9a37487de8b179d65b2");

/* Source: lite/src/notification/reader-notification-model.ts */
runtime.register("src/notification/reader-notification-model.js", function(module, exports, require) {
	var reader_notification_model_exports = {};
	__export(reader_notification_model_exports, {
	  READER_NOTIFICATION_AGGREGATE_GROUP_ORDER: () => READER_NOTIFICATION_AGGREGATE_GROUP_ORDER,
	  READER_NOTIFICATION_GROUPS: () => READER_NOTIFICATION_GROUPS,
	  READER_NOTIFICATION_GROUP_ORDER: () => READER_NOTIFICATION_GROUP_ORDER,
	  READER_NOTIFICATION_PANEL_GROUP_ORDER: () => READER_NOTIFICATION_PANEL_GROUP_ORDER,
	  normalizeBoostNotification: () => normalizeBoostNotification,
	  normalizeNativeNotification: () => normalizeNativeNotification,
	  normalizePrivateMessageNotification: () => normalizePrivateMessageNotification,
	  normalizeReactionNotification: () => normalizeReactionNotification,
	  normalizeStoredReaderNotification: () => normalizeStoredReaderNotification,
	  normalizeUserActionNotification: () => normalizeUserActionNotification,
	  notificationData: () => notificationData,
	  notificationRecord: () => notificationRecord,
	  notificationSearchText: () => notificationSearchText,
	  notificationText: () => notificationText,
	  notificationUsername: () => notificationUsername,
	  readerNotificationCategoryFilterKey: () => readerNotificationCategoryFilterKey,
	  readerNotificationGroup: () => readerNotificationGroup,
	  readerNotificationTagFilterKey: () => readerNotificationTagFilterKey,
	  sortReaderNotifications: () => sortReaderNotifications,
	  withReaderNotificationTopicTaxonomy: () => withReaderNotificationTopicTaxonomy
	});
	module.exports = __toCommonJS(reader_notification_model_exports);
	var import_identifiers = require("../discourse/identifiers.js");
	function group(input) {
	  return Object.freeze({
	    ...input,
	    typeNames: Object.freeze([...input.typeNames ?? []]),
	    actionTypes: Object.freeze([...input.actionTypes ?? []]),
	    path: input.path ?? null
	  });
	}
	const READER_NOTIFICATION_GROUPS = Object.freeze({
	  all: group({
	    key: "all",
	    mode: "notifications",
	    source: "notifications",
	    label: "全部",
	    icon: "bell",
	    pageSize: 24
	  }),
	  replies: group({
	    key: "replies",
	    mode: "notifications",
	    source: "user-actions",
	    label: "回复",
	    icon: "reply",
	    pageSize: 100,
	    typeNames: ["replied", "quoted"],
	    actionTypes: [6, 9]
	  }),
	  likes: group({
	    key: "likes",
	    mode: "notifications",
	    source: "user-actions",
	    label: "赞",
	    icon: "heart",
	    pageSize: 100,
	    typeNames: ["liked", "liked_consolidated"],
	    actionTypes: [2]
	  }),
	  mentions: group({
	    key: "mentions",
	    mode: "notifications",
	    source: "user-actions",
	    label: "@提及",
	    icon: "at",
	    pageSize: 100,
	    typeNames: ["mentioned", "group_mentioned"],
	    actionTypes: [7]
	  }),
	  edits: group({
	    key: "edits",
	    mode: "notifications",
	    source: "user-actions",
	    label: "编辑",
	    icon: "pencil",
	    pageSize: 100,
	    typeNames: ["edited"],
	    actionTypes: [11]
	  }),
	  links: group({
	    key: "links",
	    mode: "notifications",
	    source: "user-actions",
	    label: "链接",
	    icon: "link",
	    pageSize: 100,
	    typeNames: ["linked", "linked_consolidated"],
	    actionTypes: [17]
	  }),
	  boosts: group({
	    key: "boosts",
	    mode: "notifications",
	    source: "boosts-received",
	    label: "Boost",
	    icon: "rocket",
	    pageSize: 20,
	    typeNames: ["boost"]
	  }),
	  reactions: group({
	    key: "reactions",
	    mode: "notifications",
	    source: "reactions-received",
	    label: "回应",
	    icon: "smile",
	    pageSize: 20,
	    typeNames: ["reaction"]
	  }),
	  reactionLikes: group({
	    key: "reactionLikes",
	    mode: "notifications",
	    source: "user-actions",
	    label: "回应与赞",
	    icon: "smile",
	    pageSize: 30,
	    typeNames: ["reaction", "liked", "liked_consolidated"],
	    actionTypes: [2]
	  }),
	  inbox: group({
	    key: "inbox",
	    mode: "messages",
	    source: "private-messages",
	    label: "最新",
	    icon: "mail",
	    pageSize: 30,
	    path: "private-messages"
	  }),
	  sent: group({
	    key: "sent",
	    mode: "messages",
	    source: "private-messages",
	    label: "已发送",
	    icon: "reply",
	    pageSize: 30,
	    path: "private-messages-sent"
	  }),
	  newMessages: group({
	    key: "newMessages",
	    mode: "messages",
	    source: "private-messages",
	    label: "新",
	    icon: "plus",
	    pageSize: 30,
	    path: "private-messages-new"
	  }),
	  unreadMessages: group({
	    key: "unreadMessages",
	    mode: "messages",
	    source: "private-messages",
	    label: "未读",
	    icon: "bell",
	    pageSize: 30,
	    path: "private-messages-unread"
	  }),
	  archive: group({
	    key: "archive",
	    mode: "messages",
	    source: "private-messages",
	    label: "归档",
	    icon: "database",
	    pageSize: 30,
	    path: "private-messages-archive"
	  }),
	  botMessages: group({
	    key: "botMessages",
	    mode: "messages",
	    source: "private-messages",
	    label: "机器人聊天",
	    icon: "message-square",
	    pageSize: 30,
	    path: "private-messages-warnings"
	  })
	}), READER_NOTIFICATION_GROUP_ORDER = Object.freeze([
	  "all",
	  "replies",
	  "boosts",
	  "mentions",
	  "likes",
	  "reactions",
	  "edits",
	  "links",
	  "inbox",
	  "sent",
	  "newMessages",
	  "unreadMessages",
	  "archive",
	  "botMessages"
	]), READER_NOTIFICATION_PANEL_GROUP_ORDER = Object.freeze([
	  "all",
	  "replies",
	  "boosts",
	  "reactionLikes",
	  "mentions",
	  "edits",
	  "links",
	  "inbox",
	  "sent",
	  "newMessages",
	  "unreadMessages",
	  "archive",
	  "botMessages"
	]), READER_NOTIFICATION_AGGREGATE_GROUP_ORDER = Object.freeze([
	  "replies",
	  "likes",
	  "mentions",
	  "edits",
	  "links",
	  "boosts",
	  "reactions"
	]);
	function readerNotificationGroup(value) {
	  const key = String(value ?? "");
	  return READER_NOTIFICATION_GROUPS[key] ?? READER_NOTIFICATION_GROUPS.all;
	}
	function nativeNotificationGroup(typeName, requested) {
	  if (requested !== "all") return readerNotificationGroup(requested);
	  for (const key of READER_NOTIFICATION_GROUP_ORDER) {
	    const candidate = READER_NOTIFICATION_GROUPS[key];
	    if (candidate.mode === "notifications" && candidate.typeNames.includes(typeName))
	      return candidate;
	  }
	  return READER_NOTIFICATION_GROUPS.all;
	}
	function notificationRecord(value) {
	  return value !== null && typeof value == "object" ? value : Object.freeze({});
	}
	function notificationData(value) {
	  const raw = notificationRecord(value).data;
	  if (raw !== null && typeof raw == "object") return raw;
	  if (typeof raw != "string" || !raw.trim()) return Object.freeze({});
	  try {
	    return notificationRecord(JSON.parse(raw));
	  } catch {
	    return Object.freeze({});
	  }
	}
	function notificationText(value) {
	  return String(value ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
	}
	function notificationUsername(value) {
	  return String(value ?? "").trim().replace(/^@/, "");
	}
	function notificationSearchText(values) {
	  return values.map((value) => String(value ?? "").toLocaleLowerCase()).join(" ").replace(/\s+/g, "").trim();
	}
	function positiveId(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function tagNames(...values) {
	  const names = /* @__PURE__ */ new Map(), visit = (value) => {
	    if (Array.isArray(value)) {
	      for (const item of value) visit(item);
	      return;
	    }
	    const source = notificationRecord(value), name = notificationText(
	      typeof value == "string" ? value : source.name ?? source.tag_name ?? source.slug
	    );
	    if (!name) return;
	    const key = name.toLocaleLowerCase("zh-CN");
	    names.has(key) || names.set(key, name);
	  };
	  for (const value of values) visit(value);
	  return Object.freeze([...names.values()]);
	}
	function notificationTaxonomy(categoryNameFor, ...values) {
	  let categoryId = null, categoryName = "";
	  const tags = [];
	  for (const value of values) {
	    const source = notificationRecord(value), category = notificationRecord(source.category);
	    categoryId ??= positiveId(source.category_id ?? category.id), categoryName ||= notificationText(
	      source.category_name ?? source.categoryName ?? category.name ?? source.category_slug ?? category.slug
	    ), tags.push(source.tags, source.topic_tags);
	  }
	  return !categoryName && categoryId !== null && categoryNameFor && (categoryName = notificationText(categoryNameFor(categoryId))), Object.freeze({
	    categoryId,
	    categoryName,
	    tags: tagNames(tags)
	  });
	}
	function readerNotificationCategoryFilterKey(record) {
	  if (record.categoryId !== null) return `category:${record.categoryId}`;
	  const name = record.categoryName.trim().toLocaleLowerCase("zh-CN");
	  return name ? `category-name:${name}` : "";
	}
	function readerNotificationTagFilterKey(value) {
	  const tag = value.trim().toLocaleLowerCase("zh-CN");
	  return tag ? `tag:${tag}` : "";
	}
	function aggregateCount(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 1 ? numeric : null;
	}
	function createdAt(value) {
	  const normalized = String(value ?? "").trim();
	  return Number.isFinite(Date.parse(normalized)) ? normalized : (/* @__PURE__ */ new Date(0)).toISOString();
	}
	function targetFrom(topicIdValue, postNumberValue) {
	  const topicId = (0, import_identifiers.tryDiscourseTopicId)(topicIdValue);
	  if (!topicId) return null;
	  const postNumber = (0, import_identifiers.tryDiscoursePostNumber)(postNumberValue) ?? (0, import_identifiers.tryDiscoursePostNumber)(1);
	  return postNumber ? Object.freeze({ topicId, postNumber }) : null;
	}
	function targetFromHref(value) {
	  const href = String(value ?? "").trim();
	  if (!href) return null;
	  let pathname;
	  try {
	    pathname = new URL(href, "https://reader.invalid/").pathname;
	  } catch {
	    return null;
	  }
	  const segments = pathname.split("/").filter(Boolean), topicIndex = segments.indexOf("t");
	  if (topicIndex < 0) return null;
	  const tail = segments.slice(topicIndex + 1), idIndex = tail.findIndex((segment) => Number.isSafeInteger(Number(segment)) && Number(segment) > 0);
	  return idIndex < 0 ? null : targetFrom(tail[idIndex], tail[idIndex + 1] ?? 1);
	}
	const TYPE_ICONS = Object.freeze({
	  mentioned: "at",
	  group_mentioned: "at",
	  replied: "reply",
	  quoted: "message-square",
	  posted: "message-square",
	  liked: "heart",
	  liked_consolidated: "heart",
	  reaction: "smile",
	  boost: "rocket",
	  private_message: "mail",
	  invited_to_private_message: "user-plus",
	  group_message_summary: "mail",
	  edited: "pencil",
	  linked: "link",
	  linked_consolidated: "link"
	});
	function recordResult(input) {
	  return Object.freeze({
	    ...input,
	    searchText: notificationSearchText([
	      input.actor,
	      input.summary,
	      input.excerpt,
	      input.stateLabel,
	      input.typeLabel,
	      input.target?.topicId,
	      input.target?.postNumber,
	      input.categoryName,
	      ...input.tags
	    ])
	  });
	}
	function normalizeStoredReaderNotification(value) {
	  const source = notificationRecord(value), identity = String(source.identity ?? "").trim(), group2 = String(source.group ?? ""), notificationSource = String(
	    source.source ?? ""
	  );
	  if (!identity || !Object.hasOwn(READER_NOTIFICATION_GROUPS, group2) || ![
	    "notifications",
	    "user-actions",
	    "boosts-received",
	    "reactions-received",
	    "private-messages"
	  ].includes(notificationSource)) return null;
	  const targetValue = notificationRecord(source.target), topicId = (0, import_identifiers.tryDiscourseTopicId)(targetValue.topicId), postNumber = (0, import_identifiers.tryDiscoursePostNumber)(targetValue.postNumber), target = topicId && postNumber ? Object.freeze({ topicId, postNumber }) : null, tags = Object.freeze((Array.isArray(source.tags) ? source.tags : []).map(notificationText).filter(Boolean)), read = source.read === !0 ? !0 : source.read === !1 ? !1 : null;
	  return recordResult({
	    identity,
	    group: group2,
	    source: notificationSource,
	    sourceNotificationId: positiveId(source.sourceNotificationId),
	    notificationTypeId: positiveId(source.notificationTypeId),
	    highPriority: source.highPriority === !0,
	    typeName: notificationText(source.typeName),
	    typeLabel: notificationText(source.typeLabel),
	    aggregateCount: aggregateCount(source.aggregateCount),
	    icon: notificationText(source.icon) || "bell",
	    actor: notificationUsername(source.actor),
	    avatarFallback: notificationText(source.avatarFallback),
	    avatarTemplate: String(source.avatarTemplate ?? "").trim(),
	    summary: notificationText(source.summary),
	    excerpt: notificationText(source.excerpt),
	    stateLabel: notificationText(source.stateLabel),
	    createdAt: createdAt(source.createdAt),
	    read,
	    href: String(source.href ?? "").trim(),
	    target,
	    categoryId: positiveId(source.categoryId),
	    categoryName: notificationText(source.categoryName),
	    tags
	  });
	}
	function withReaderNotificationTopicTaxonomy(record, value, categoryNameFor) {
	  const topic = notificationRecord(value), taxonomy = notificationTaxonomy(categoryNameFor, topic), categoryId = record.categoryId ?? taxonomy.categoryId, categoryName = record.categoryName || (categoryId !== null && categoryId === taxonomy.categoryId ? taxonomy.categoryName : ""), hasTopicTags = Object.hasOwn(topic, "tags") || Object.hasOwn(topic, "topic_tags"), tags = record.tags.length || !hasTopicTags ? record.tags : taxonomy.tags;
	  return categoryId === record.categoryId && categoryName === record.categoryName && tags.length === record.tags.length && tags.every((tag, index) => tag === record.tags[index]) ? record : recordResult({
	    ...record,
	    categoryId,
	    categoryName,
	    tags
	  });
	}
	function notificationActivityVerb(group2, typeName) {
	  return typeName === "quoted" ? "引用了你" : {
	    replies: "回复了你",
	    likes: "赞了你的帖子",
	    mentions: "@提及了你",
	    edits: "编辑了帖子",
	    links: "链接了你的帖子",
	    boosts: "Boost 了你的帖子",
	    reactions: "回应了你的帖子"
	  }[group2] ?? "";
	}
	function notificationActivitySummary(input) {
	  const verb = notificationActivityVerb(input.group, input.typeName);
	  return verb ? `${input.actor ? `@${input.actor} · ` : ""}${verb}${input.title ? ` · ${input.title}` : ""}` : "";
	}
	function normalizeNativeNotification(value, presented, groupValue, options = {}) {
	  const source = notificationRecord(value), data = notificationData(source), typeName = String(presented.typeName ?? source.type_name ?? "").trim(), group2 = nativeNotificationGroup(typeName, groupValue), presentedActor = notificationUsername(
	    presented.actor ?? data.display_username ?? data.original_username ?? data.acting_user_name ?? data.username ?? source.username
	  ), count = typeName === "replied" ? aggregateCount(data.consolidated_count) : null, aggregateActor = count === null ? "" : notificationUsername(
	    data.original_username ?? data.acting_user_name ?? data.username ?? data.display_username ?? presented.actor
	  ), namedAggregateActor = aggregateActor && aggregateActor !== String(count) && !/^[0-9]+$/.test(aggregateActor) ? aggregateActor : "", actor = count === null ? presentedActor : namedAggregateActor, target = targetFrom(
	    presented.topicId ?? source.topic_id ?? data.topic_id,
	    presented.postNumber ?? source.post_number ?? data.post_number
	  ) ?? targetFromHref(
	    presented.href ?? data.post_url ?? data.topic_url ?? data.url
	  ), id = positiveId(options.sourceNotificationId ?? source.id), timestamp = createdAt(source.created_at), href = String(presented.href ?? "").trim(), title = notificationText(data.topic_title), summary = (count === null ? notificationActivitySummary({
	    actor: presentedActor,
	    group: group2.key,
	    typeName,
	    title
	  }) : `${namedAggregateActor ? `@${namedAggregateActor} 等 · ` : ""}${count} 条回复${title ? ` · ${title}` : ""}`) || notificationText(
	    presented.summary ?? data.topic_title ?? presented.typeLabel ?? typeName ?? "通知"
	  ), taxonomy = notificationTaxonomy(
	    options.categoryNameFor,
	    data,
	    source
	  );
	  return recordResult({
	    identity: options.identity ?? (id ? `notification:${id}` : `notification:${target?.topicId ?? 0}:${target?.postNumber ?? 1}:${timestamp}:${actor}`),
	    group: group2.key,
	    source: "notifications",
	    sourceNotificationId: id,
	    notificationTypeId: positiveId(source.notification_type),
	    highPriority: source.high_priority === !0,
	    typeName,
	    typeLabel: notificationText(presented.typeLabel ?? (typeName || "通知")),
	    aggregateCount: count,
	    icon: TYPE_ICONS[typeName] ?? group2.icon,
	    actor,
	    avatarFallback: count === null ? actor.slice(0, 1).toLocaleUpperCase() || "?" : String(count),
	    avatarTemplate: count === null ? String(
	      source.acting_user_avatar_template ?? source.avatar_template ?? data.acting_user_avatar_template ?? data.avatar_template ?? ""
	    ).trim() : "",
	    summary,
	    excerpt: "",
	    stateLabel: source.read === !0 ? "已读" : source.read === !1 ? "未读" : "",
	    createdAt: timestamp,
	    read: typeof source.read == "boolean" ? source.read : null,
	    href,
	    target,
	    ...taxonomy
	  });
	}
	function syntheticRecord(input) {
	  const group2 = readerNotificationGroup(input.group), actor = notificationUsername(input.actor), timestamp = createdAt(input.createdAt), target = targetFrom(input.topicId, input.postNumber), summary = notificationText(input.summary || group2.label), excerpt = notificationText(input.excerpt), stateLabel = notificationText(input.stateLabel);
	  return recordResult({
	    identity: `${group2.key}:${input.identity || `${target?.topicId ?? 0}:${target?.postNumber ?? 1}:${timestamp}`}`,
	    group: group2.key,
	    source: group2.source,
	    sourceNotificationId: null,
	    notificationTypeId: null,
	    highPriority: !1,
	    typeName: input.typeName ?? group2.typeNames[0] ?? group2.key,
	    typeLabel: group2.label,
	    aggregateCount: null,
	    icon: input.icon ?? group2.icon,
	    actor,
	    avatarFallback: actor.slice(0, 1).toLocaleUpperCase() || "?",
	    avatarTemplate: String(input.avatarTemplate ?? "").trim(),
	    summary,
	    excerpt,
	    stateLabel,
	    createdAt: timestamp,
	    read: input.read ?? null,
	    href: "",
	    target,
	    categoryId: positiveId(input.categoryId),
	    categoryName: notificationText(input.categoryName),
	    tags: tagNames(input.tags)
	  });
	}
	function normalizeUserActionNotification(value, groupKey, categoryNameFor) {
	  const action = notificationRecord(value), taxonomy = notificationTaxonomy(categoryNameFor, action), actionType = Number(action.action_type) || 0, actor = notificationUsername(action.acting_username ?? action.username), title = notificationText(action.title), typeName = actionType === 9 ? "quoted" : readerNotificationGroup(groupKey).typeNames[0] ?? groupKey, summary = notificationActivitySummary({
	    actor,
	    group: groupKey,
	    typeName,
	    title
	  });
	  return syntheticRecord({
	    group: groupKey,
	    identity: [
	      actionType,
	      action.post_id,
	      action.acting_user_id,
	      action.created_at
	    ].filter(Boolean).join(":"),
	    actor,
	    avatarTemplate: action.acting_avatar_template ?? action.avatar_template,
	    createdAt: action.created_at,
	    topicId: action.topic_id,
	    postNumber: action.post_number,
	    summary: summary || readerNotificationGroup(groupKey).label,
	    excerpt: action.excerpt,
	    typeName,
	    ...taxonomy
	  });
	}
	function normalizeReactionNotification(value, categoryNameFor) {
	  const reaction = notificationRecord(value), post = notificationRecord(reaction.post), topic = notificationRecord(post.topic), user = notificationRecord(reaction.user), taxonomy = notificationTaxonomy(
	    categoryNameFor,
	    reaction,
	    post,
	    topic
	  ), reactionValue = notificationRecord(reaction.reaction).reaction_value ?? reaction.reaction_value ?? (typeof reaction.reaction == "string" ? reaction.reaction : ""), reactionId = String(reactionValue ?? "").trim().replace(/^:+|:+$/g, ""), actor = notificationUsername(user.username), title = notificationText(
	    notificationRecord(post.topic).title ?? post.topic_title
	  ), target = targetFromHref(post.url);
	  return syntheticRecord({
	    group: "reactions",
	    identity: String(reaction.id ?? reaction.reaction_user_id ?? ""),
	    actor,
	    avatarTemplate: user.avatar_template,
	    createdAt: reaction.created_at,
	    topicId: post.topic_id ?? target?.topicId,
	    postNumber: post.post_number ?? target?.postNumber,
	    summary: `${actor ? `@${actor} · ` : ""}回应了你的帖子${title ? ` · ${title}` : ""}`,
	    excerpt: post.excerpt,
	    typeName: "reaction",
	    icon: reactionId ? `emoji:${reactionId}` : "smile",
	    ...taxonomy
	  });
	}
	function normalizeBoostNotification(value, categoryNameFor) {
	  const boost = notificationRecord(value), post = notificationRecord(boost.post), topic = notificationRecord(post.topic), user = notificationRecord(boost.user), taxonomy = notificationTaxonomy(
	    categoryNameFor,
	    boost,
	    post,
	    topic
	  ), actor = notificationUsername(user.username), title = notificationText(post.topic_title), target = targetFromHref(post.url);
	  return syntheticRecord({
	    group: "boosts",
	    identity: String(boost.id ?? ""),
	    actor,
	    avatarTemplate: user.avatar_template,
	    createdAt: boost.created_at,
	    topicId: post.topic_id ?? target?.topicId,
	    postNumber: post.post_number ?? target?.postNumber,
	    summary: `${actor ? `@${actor} · ` : ""}Boost 了你的帖子${title ? ` · ${title}` : ""}`,
	    excerpt: boost.cooked ?? post.excerpt,
	    typeName: "boost",
	    ...taxonomy
	  });
	}
	function normalizePrivateMessageNotification(value, payloadValue, groupKey, currentUsernameValue, categoryNameFor) {
	  const topic = notificationRecord(value), taxonomy = notificationTaxonomy(categoryNameFor, topic), payload = notificationRecord(payloadValue), users = new Map(
	    (Array.isArray(payload.users) ? payload.users : []).map((userValue) => {
	      const user = notificationRecord(userValue);
	      return [Number(user.id), user];
	    })
	  ), participants = (Array.isArray(topic.participants) ? topic.participants : Array.isArray(topic.posters) ? topic.posters : []).map((participantValue) => {
	    const participant = notificationRecord(participantValue);
	    return participant.username ? participant : users.get(Number(participant.user_id)) ?? participant;
	  }), currentUsername = notificationUsername(
	    currentUsernameValue
	  ).toLocaleLowerCase(), lastPoster = [...users.values()].find(
	    (user) => notificationUsername(user.username) === notificationUsername(topic.last_poster_username)
	  ), actor = [...participants].reverse().find(
	    (user) => notificationUsername(user.username).toLocaleLowerCase() !== currentUsername
	  ) ?? lastPoster ?? participants.at(-1) ?? Object.freeze({}), highest = Math.max(
	    1,
	    Number(topic.highest_post_number) || Number(topic.posts_count) || 1
	  ), lastRead = Math.max(0, Number(topic.last_read_post_number) || 0), unread = topic.unseen === !0 || Number(topic.unread) > 0 || Number(topic.new_posts) > 0, stateLabel = topic.unseen === !0 || Number(topic.new_posts) > 0 ? "新" : unread ? "未读" : "已读", target = unread && lastRead < highest ? lastRead + 1 : highest, title = notificationText(topic.fancy_title ?? topic.title) || "私信";
	  return syntheticRecord({
	    group: groupKey,
	    identity: String(topic.id ?? ""),
	    actor: actor.username,
	    avatarTemplate: actor.avatar_template,
	    createdAt: topic.last_posted_at ?? topic.bumped_at ?? topic.created_at,
	    topicId: topic.id,
	    postNumber: target,
	    summary: title,
	    excerpt: [
	      actor.username ? `@${notificationUsername(actor.username)}` : "",
	      highest > 1 ? `${highest - 1} 条回复` : ""
	    ].filter(Boolean).join(" · "),
	    read: !unread,
	    stateLabel,
	    typeName: "private_message",
	    ...taxonomy
	  });
	}
	function sortReaderNotifications(records) {
	  return Object.freeze([...records].sort((left, right) => (Date.parse(right.createdAt) || 0) - (Date.parse(left.createdAt) || 0) || right.identity.localeCompare(left.identity)));
	}
}, "992b745f59dbfbaab399af7dabbcc5cf897fef4d41d9cc650cfba1a0a894c935");

/* Source: lite/src/notification/reader-notification-panel-view.ts */
runtime.register("src/notification/reader-notification-panel-view.js", function(module, exports, require) {
	var reader_notification_panel_view_exports = {};
	__export(reader_notification_panel_view_exports, {
	  ReaderNotificationPanelView: () => ReaderNotificationPanelView
	});
	module.exports = __toCommonJS(reader_notification_panel_view_exports);
	var import_native_host_api = require("../discourse/native-host-api.js"), import_reader_collection_floating_window = require("../collection/reader-collection-floating-window.js"), import_reader_popover_filter_controls = require("../collection/reader-popover-filter-controls.js"), import_event_target = require("../dom/event-target.js"), import_reader_image_fallback = require("../components/reader-image-fallback.js"), import_reader_icon = require("../components/reader-icon.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_history_repository = require("../history/reader-history-repository.js"), import_reader_notification_model = require("./reader-notification-model.js");
	function recordHref(record, baseUrl) {
	  if (record.target)
	    return new URL(
	      `/t/${record.target.topicId}/${record.target.postNumber}`,
	      baseUrl
	    ).href;
	  try {
	    return new URL(record.href || "/my/notifications", baseUrl).href;
	  } catch {
	    return new URL("/my/notifications", baseUrl).href;
	  }
	}
	function errorMessage(cause) {
	  return cause instanceof Error ? cause.message : String(cause || "未知错误");
	}
	function recordDisplayTitle(record, marker) {
	  if (!(0, import_reader_history_repository.readerHistoryArchiveIsDeletedTopic)(marker)) return record.summary;
	  const topicTitle = marker?.topicTitle?.trim() ?? "";
	  return topicTitle && record.summary.endsWith(topicTitle) ? record.summary.slice(0, -topicTitle.length) + import_reader_history_repository.READER_DELETED_TOPIC_TITLE : import_reader_history_repository.READER_DELETED_TOPIC_TITLE;
	}
	function dateGroup(createdAt, now) {
	  const timestamp = Date.parse(createdAt);
	  if (!Number.isFinite(timestamp)) return "更早";
	  const today = new Date(now);
	  today.setHours(0, 0, 0, 0);
	  const yesterday = today.getTime() - 1440 * 60 * 1e3;
	  return timestamp >= today.getTime() ? "今天" : timestamp >= yesterday ? "昨天" : "更早";
	}
	class ReaderNotificationPanelView {
	  scope;
	  #document;
	  #controller;
	  #elements;
	  #baseUrl;
	  #relativeTime;
	  #renderIcon;
	  #avatarSource;
	  #emojiSource;
	  #archiveMarker;
	  #schedule;
	  #cancel;
	  #notify;
	  #onError;
	  #surface;
	  #progress;
	  #filterDisclosure;
	  #markAllHeaderActions;
	  #scrollWindow;
	  #recordNodes = new import_reader_collection_floating_window.ReaderCollectionNodeCache();
	  #relativeTimer = null;
	  #historyCacheCompleted = !1;
	  constructor(options) {
	    this.#document = options.document, this.#controller = options.controller, this.#elements = options.elements, this.#baseUrl = new URL(options.baseUrl).href, this.#relativeTime = options.relativeTime, this.#renderIcon = options.renderIcon ?? null, this.#avatarSource = options.avatarSource ?? ((template, size) => (0, import_native_host_api.discourseAvatarTemplateUrl)(template, size, this.#baseUrl)), this.#emojiSource = options.emojiSource ?? (() => ""), this.#archiveMarker = options.archiveMarker ?? (() => null), this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.#notify = options.notify ?? (() => {
	    }), this.#onError = options.onError ?? (() => {
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#surface = new import_reader_collection_floating_window.ReaderCollectionFloatingWindow({
	      document: this.#document,
	      mount: options.mount,
	      toggle: this.#elements.toggle,
	      content: this.#elements.popover,
	      title: "通知私信",
	      ariaLabel: "通知与私信",
	      icon: "bell",
	      variant: "notifications",
	      tabOrder: 10,
	      ...options.storage ? { geometryStorage: options.storage } : {},
	      parentScope: this.scope,
	      isOpen: () => this.#controller.snapshot.open,
	      requestOpen: () => this.#controller.open(),
	      requestClose: () => this.#controller.close(),
	      notify: this.#notify
	    }), this.#markAllHeaderActions = this.#document.createElement("div"), this.#markAllHeaderActions.append(this.#elements.markAll), this.#surface.attachHeaderActions({
	      root: this.#markAllHeaderActions,
	      buttons: [this.#elements.markAll],
	      label: "通知操作"
	    }), this.#progress = new import_reader_collection_floating_window.ReaderCollectionProgressView({
	      document: this.#document,
	      onError: this.#onError,
	      retry: async () => {
	        if (this.#controller.snapshot.stale) {
	          await this.#controller.refresh();
	          return;
	        }
	        this.#controller.retryBackgroundCache();
	      },
	      parentScope: this.scope
	    }), this.#elements.popover.prepend(this.#progress.element), this.#filterDisclosure = new import_reader_popover_filter_controls.ReaderPopoverFilterDisclosure({
	      search: this.#elements.search,
	      onDateChange: (value) => this.#controller.setDateFilter(value),
	      onSortChange: () => {
	      },
	      onDirectionChange: (value) => this.#controller.setSortDirection(value),
	      onReset: () => this.#controller.resetFilters(),
	      parentScope: this.scope
	    });
	    const pager = this.#elements.pageInfo.parentElement;
	    if (!pager) throw new Error("通知面板缺少滚动分页锚点");
	    this.#scrollWindow = new import_reader_collection_floating_window.ReaderCollectionScrollWindow({
	      list: this.#elements.list,
	      pager,
	      identity: (record) => record.identity,
	      loadMore: () => this.#controller.nextPage(),
	      onError: this.#onError,
	      parentScope: this.scope
	    }), this.#bind(), this.#controller.changes.subscribe((snapshot) => {
	      this.#render(snapshot);
	    }, this.scope), this.scope.add(() => {
	      this.#stopRelativeTimer(), this.#recordNodes.clear(), this.#elements.list.replaceChildren();
	    }), this.#render(this.#controller.snapshot);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  syncArchiveMarkers() {
	    this.scope.destroyed || this.#render(this.#controller.snapshot);
	  }
	  #bind() {
	    this.scope.listen(this.#elements.toggle, "click", () => {
	      this.#controller.toggle().catch((cause) => {
	        this.#onError(cause), this.#notify("消息加载失败,请重试");
	      });
	    });
	    for (const tab of this.#elements.modeTabs)
	      this.scope.listen(tab, "click", () => {
	        const mode = tab.dataset.notificationMode;
	        this.#controller.selectMode(mode).catch((cause) => {
	          this.#onError(cause), this.#notify("消息分类加载失败");
	        });
	      });
	    for (const tab of this.#elements.groupTabs)
	      this.scope.listen(tab, "click", () => {
	        const group = tab.dataset.notificationGroup;
	        this.#controller.selectGroup(group).catch((cause) => {
	          this.#onError(cause), this.#notify("消息分类加载失败");
	        });
	      });
	    this.scope.listen(this.#elements.search, "input", () => {
	      this.#controller.setQuery(this.#elements.search.value);
	    }), this.scope.listen(this.#elements.searchClear, "click", () => {
	      this.#elements.search.value = "", this.#controller.setQuery(""), this.#elements.search.focus();
	    }), this.scope.listen(this.#elements.categoryFilter, "change", () => {
	      this.#controller.setCategoryFilter(
	        this.#elements.categoryFilter.value
	      );
	    }), this.scope.listen(this.#elements.tagFilter, "change", () => {
	      this.#controller.setTagFilter(this.#elements.tagFilter.value);
	    }), this.scope.listen(this.#elements.markAll, "click", () => {
	      this.#controller.markAllAsRead().then(() => {
	        this.#scrollWindow.replaceWhere(
	          (record) => record.sourceNotificationId !== null,
	          (record) => Object.freeze({ ...record, read: !0 })
	        ), this.#render(this.#controller.snapshot), this.#notify("消息已全部标为已读");
	      }).catch((cause) => {
	        this.#onError(cause), this.#notify(`标记已读失败:${errorMessage(cause)}`);
	      });
	    }), this.scope.listen(this.#elements.list, "click", (eventValue) => {
	      const event = eventValue, target = event.target, item = target?.closest ? target.closest(".ldp-notification-item") : null;
	      if (!item || !this.#elements.list.contains(item)) return;
	      const identity = item.dataset.notificationKey, record = this.#scrollWindow.records.find((candidate) => candidate.identity === identity);
	      if (record) {
	        if (record.read === !1 && this.#scrollWindow.update(record.identity, (current) => Object.freeze({ ...current, read: !0 })), !record.target || (0, import_event_target.usesNativeLinkNavigation)(event)) {
	          this.#controller.markRecordRead(record).catch(this.#onError);
	          return;
	        }
	        event.preventDefault(), this.#controller.openRecord(record).catch((cause) => {
	          this.#onError(cause), this.#notify("消息目标暂时无法打开");
	        });
	      }
	    });
	  }
	  #render(snapshot) {
	    const {
	      toggle,
	      badge,
	      unreadStatus,
	      markAll,
	      newMessage,
	      search,
	      searchClear,
	      categoryFilter,
	      tagFilter
	    } = this.#elements;
	    this.#surface.sync(snapshot.open), this.#syncWindowStatus(snapshot), toggle.classList.toggle("active", snapshot.open);
	    const badgeText = snapshot.unreadCount > 99 ? "99+" : String(snapshot.unreadCount);
	    badge.hidden = snapshot.unreadCount <= 0, badge.textContent = snapshot.unreadCount > 0 ? badgeText : "", badge.setAttribute(
	      "aria-label",
	      snapshot.unreadCount > 0 ? `未读 ${snapshot.unreadCount} 条` : "没有未读消息"
	    ), toggle.setAttribute(
	      "aria-label",
	      snapshot.unreadCount > 0 ? `消息,${snapshot.unreadCount} 条未读` : "消息"
	    ), unreadStatus.hidden = snapshot.unreadCount <= 0, unreadStatus.textContent = snapshot.unreadCount > 0 ? `未读 ${snapshot.unreadCount} 条` : "", unreadStatus.parentElement?.classList.toggle(
	      "is-empty",
	      snapshot.unreadCount <= 0
	    ), markAll.disabled = snapshot.unreadCount <= 0 || snapshot.refreshing || snapshot.markingAll, markAll.dataset.ldpRequestBusy = snapshot.markingAll ? "1" : "0", markAll.setAttribute("aria-busy", String(snapshot.markingAll));
	    const markAllLabel = this.#document.createElement("span");
	    markAllLabel.textContent = snapshot.markingAll ? "处理中" : "全部已读", markAll.replaceChildren(
	      (0, import_reader_icon.renderReaderIcon)(
	        this.#document,
	        snapshot.markingAll ? "loader" : "check-square",
	        this.#renderIcon
	      ),
	      markAllLabel
	    ), markAll.hidden = (0, import_reader_notification_model.readerNotificationGroup)(snapshot.group).source !== "notifications", this.#markAllHeaderActions.hidden = markAll.hidden, unreadStatus.hidden = markAll.hidden || snapshot.unreadCount <= 0, newMessage.hidden = snapshot.mode !== "messages", this.#elements.toolbar.hidden = unreadStatus.hidden && newMessage.hidden;
	    for (const tab of this.#elements.modeTabs) {
	      const active = tab.dataset.notificationMode === snapshot.mode;
	      tab.classList.toggle("active", active), tab.setAttribute("aria-selected", String(active));
	    }
	    for (const panel of this.#elements.groupPanels)
	      panel.hidden = panel.dataset.notificationModePanel !== snapshot.mode;
	    for (const tab of this.#elements.groupTabs) {
	      const group = tab.dataset.notificationGroup, active = group === snapshot.group;
	      tab.classList.toggle("active", active), tab.setAttribute("aria-selected", String(active)), this.#syncGroupTabCount(
	        tab,
	        (0, import_reader_notification_model.readerNotificationGroup)(group).label,
	        snapshot.groupCounts.get(group) ?? 0
	      );
	    }
	    search.value !== snapshot.query && (search.value = snapshot.query), searchClear.hidden = !snapshot.query, (0, import_reader_popover_filter_controls.syncReaderFilterOptions)(
	      categoryFilter,
	      "类别",
	      "暂无类别",
	      snapshot.categoryOptions,
	      snapshot.categoryFilter
	    ), (0, import_reader_popover_filter_controls.syncReaderFilterOptions)(
	      tagFilter,
	      "标签",
	      "暂无标签",
	      snapshot.tagOptions,
	      snapshot.tagFilter
	    ), this.#filterDisclosure.sync({
	      active: !!(snapshot.categoryFilter || snapshot.tagFilter || snapshot.dateFilter || snapshot.sortDirection !== "desc"),
	      date: snapshot.dateFilter,
	      sort: "time",
	      direction: snapshot.sortDirection,
	      dayCounts: snapshot.dayCounts
	    });
	    const records = this.#scrollWindow.project({
	      streamKey: JSON.stringify([
	        snapshot.mode,
	        snapshot.group,
	        snapshot.query,
	        snapshot.categoryFilter,
	        snapshot.tagFilter,
	        snapshot.dateFilter,
	        snapshot.sortDirection
	      ]),
	      page: snapshot.page,
	      records: snapshot.records,
	      loading: snapshot.loading,
	      hasMore: snapshot.hasNext || snapshot.page < snapshot.totalPages - 1
	    });
	    this.#renderRecords(snapshot, records), snapshot.open ? this.#startRelativeTimer() : this.#stopRelativeTimer();
	  }
	  #syncGroupTabCount(tab, label, count) {
	    let counter = tab.querySelector(
	      ".ldp-collection-tab-count"
	    );
	    counter || (counter = this.#document.createElement("span"), counter.className = "ldp-collection-tab-count", counter.setAttribute("aria-hidden", "true"), tab.append(counter)), counter.textContent = String(count), tab.setAttribute("aria-label", `${label},${count} 条`);
	  }
	  #syncWindowStatus(snapshot) {
	    const history = snapshot.history, complete = history.status === "complete", totalStatus = snapshot.total > 0 && snapshot.total !== history.cachedRecords ? `${snapshot.total} 条` : "", cacheStatus = history.cachedRecords > 0 ? `已缓存 ${history.cachedRecords} 条` : "";
	    if (this.#surface.frame.meta.textContent = [
	      snapshot.unreadCount > 0 ? `未读 ${snapshot.unreadCount}` : "",
	      totalStatus,
	      cacheStatus,
	      complete ? "历史已到底" : ""
	    ].filter(Boolean).join(" · "), history.status === "idle" && history.completedGroups === 0 && history.cachedRecords === 0 && (this.#historyCacheCompleted = !1), complete && (this.#historyCacheCompleted = !0), this.#historyCacheCompleted) {
	      this.#progress.render({
	        visible: !1,
	        label: "",
	        detail: "",
	        state: "complete",
	        completed: history.totalGroups,
	        total: history.totalGroups,
	        valueText: "消息历史缓存已完成"
	      });
	      return;
	    }
	    if (snapshot.stale) {
	      this.#progress.render({
	        visible: !0,
	        label: "当前页缓存更新失败",
	        detail: snapshot.error instanceof Error ? snapshot.error.message : "正在显示上次已加载内容",
	        state: "error",
	        completed: 0,
	        total: 1,
	        valueText: "缓存更新失败",
	        retryable: !0
	      });
	      return;
	    }
	    if (snapshot.refreshing) {
	      this.#progress.render({
	        visible: !0,
	        label: "更新当前页缓存",
	        detail: "后台刷新中,当前内容可继续浏览",
	        state: "running",
	        completed: 0,
	        total: 1,
	        valueText: "正在更新当前页缓存"
	      });
	      return;
	    }
	    const failed = history.status === "error", autoRecovering = failed && history.retryAt !== null, running = history.status === "loading", current = history.currentGroup ? (0, import_reader_notification_model.readerNotificationGroup)(history.currentGroup).label : running ? "消息历史" : "等待后台缓存", pageProgress = `已缓存 ${history.loadedPages} 页`, exploring = "总页数探测中";
	    this.#progress.render({
	      visible: !complete,
	      label: autoRecovering ? "消息历史自动续传" : failed ? "消息历史缓存中断" : current,
	      detail: failed ? autoRecovering ? "断点已保存,将按中央请求许可自动恢复" : "可从已保存断点继续" : `${history.completedGroups} / ${history.totalGroups} 来源 · ${pageProgress} · ${exploring}`,
	      state: autoRecovering ? "waiting" : failed ? "error" : running ? "running" : "waiting",
	      completed: history.completedGroups,
	      total: history.totalGroups,
	      valueText: `${history.completedGroups}/${history.totalGroups} 来源,${pageProgress},${exploring}` + (autoRecovering ? ",等待自动续传" : ""),
	      retryable: failed && !autoRecovering
	    });
	  }
	  #renderRecords(snapshot, records) {
	    const list = this.#elements.list, scrollTop = list.scrollTop;
	    if (snapshot.retrying && !records.length) {
	      this.#recordNodes.clear();
	      const message = this.#document.createElement("div");
	      message.className = "ldp-notification-empty", message.textContent = "消息加载暂时中断,正在自动重试…", list.replaceChildren(message);
	      return;
	    }
	    if (snapshot.loading && !records.length) {
	      this.#recordNodes.clear();
	      const message = this.#document.createElement("div");
	      message.className = "ldp-notification-empty", message.textContent = "正在加载消息…", list.replaceChildren(message);
	      return;
	    }
	    if (snapshot.error && !snapshot.stale && !records.length) {
	      this.#recordNodes.clear();
	      const message = this.#document.createElement("div");
	      message.className = "ldp-notification-empty", message.textContent = "消息加载失败,请重试", list.replaceChildren(message);
	      return;
	    }
	    if (!records.length) {
	      this.#recordNodes.clear();
	      const message = this.#document.createElement("div");
	      message.className = "ldp-notification-empty", message.textContent = snapshot.query || snapshot.categoryFilter || snapshot.tagFilter || snapshot.dateFilter || snapshot.sortDirection !== "desc" ? "本地缓存中没有匹配消息" : "暂无消息", list.replaceChildren(message);
	      return;
	    }
	    const grouped = /* @__PURE__ */ new Map(), now = Date.now();
	    for (const record of records) {
	      const label = dateGroup(record.createdAt, now), records2 = grouped.get(label) ?? [];
	      records2.push(record), grouped.set(label, records2);
	    }
	    const fragment = this.#document.createDocumentFragment(), renderedKeys = [];
	    for (const [label, records2] of grouped) {
	      const section = this.#document.createElement("section");
	      section.className = "ldp-notification-date-group";
	      const heading = this.#document.createElement("div");
	      heading.className = "ldp-notification-date-label", heading.textContent = label, section.append(heading);
	      for (const record of records2) {
	        const marker = record.target ? this.#archiveMarker(
	          record.target.topicId,
	          record.target.postNumber
	        ) : null, variant = marker ? `${marker.status}:${marker.topicTitle ?? ""}:${marker.postNumber ?? ""}` : "";
	        renderedKeys.push(record.identity), section.append(this.#recordNodes.node(
	          record.identity,
	          record,
	          variant,
	          () => this.#recordNode(record, marker)
	        ));
	      }
	      fragment.append(section);
	    }
	    this.#recordNodes.prune(renderedKeys), list.replaceChildren(fragment), list.scrollTop = scrollTop;
	  }
	  #recordNode(record, markerValue) {
	    const item = this.#document.createElement("a");
	    item.className = "ldp-notification-item ldp-notification-message-item";
	    const unread = record.read === !1, readStateLabel = unread ? "未读" : "已读";
	    item.classList.toggle("unread", unread), item.classList.toggle("read", !unread), item.dataset.notificationReadState = unread ? "unread" : "read", item.href = recordHref(record, this.#baseUrl), item.dataset.notificationSource = record.sourceNotificationId === null ? record.source : "notifications", item.dataset.notificationId = String(record.sourceNotificationId ?? 0), item.dataset.notificationKey = record.identity, item.dataset.readerTargetSource = record.source === "private-messages" ? "message" : "notification", item.dataset.readerTargetInterception = "off", item.dataset.ldpPreserveTargetPost = "1", record.target && (item.dataset.notificationTopicId = String(record.target.topicId), item.dataset.notificationPostNumber = String(record.target.postNumber));
	    const archiveMarker = markerValue !== void 0 ? markerValue : record.target ? this.#archiveMarker(record.target.topicId, record.target.postNumber) : null, archiveLabel = archiveMarker ? (0, import_reader_history_repository.readerHistoryArchiveMarkerLabel)(archiveMarker) : "";
	    archiveMarker && (item.dataset.localArchiveStatus = String(archiveMarker.status), item.dataset.localArchiveScope = archiveMarker.postNumber === null ? "topic" : "post");
	    const avatarUrl = this.#avatarSource(record.avatarTemplate, 48);
	    let avatar;
	    if (avatarUrl) {
	      const image = this.#document.createElement("img");
	      image.className = "ldp-notification-avatar", (0, import_reader_image_fallback.replaceImageWithFallbackOnError)(image, () => {
	        const fallback = this.#document.createElement("span");
	        return fallback.className = "ldp-notification-avatar ldp-avatar-fallback", fallback.textContent = record.avatarFallback, fallback.setAttribute("aria-hidden", "true"), fallback;
	      }), image.src = avatarUrl, image.alt = "", image.loading = "lazy", image.decoding = "async", avatar = image;
	    } else {
	      const fallback = this.#document.createElement("span");
	      fallback.className = "ldp-notification-avatar ldp-avatar-fallback", fallback.textContent = record.avatarFallback, fallback.setAttribute("aria-hidden", "true"), avatar = fallback;
	    }
	    if (record.actor) {
	      const trigger = this.#document.createElement("span");
	      trigger.className = "ldp-user-avatar-card", trigger.dataset.userCard = record.actor, trigger.dataset.userCardHoverOnly = "", trigger.append(avatar), item.append(trigger);
	    } else
	      avatar.setAttribute("aria-hidden", "true"), item.append(avatar);
	    const typeIcon = this.#document.createElement("span");
	    typeIcon.className = "ldp-notification-type-icon", typeIcon.dataset.notificationGroup = record.group, typeIcon.setAttribute("aria-hidden", "true");
	    const reactionEmojiId = record.group === "reactions" && record.icon.startsWith("emoji:") ? record.icon.slice(6) : "";
	    if (record.group === "likes") {
	      const emoji = this.#document.createElement("span");
	      emoji.dataset.notificationEmojiText = "heart", emoji.textContent = "❤️", typeIcon.append(emoji);
	    } else {
	      let emojiSource = "";
	      try {
	        emojiSource = reactionEmojiId ? this.#emojiSource(reactionEmojiId) : "";
	      } catch {
	      }
	      if (emojiSource) {
	        const emoji = this.#document.createElement("img");
	        emoji.className = "emoji", emoji.src = emojiSource, emoji.alt = "", emoji.loading = "lazy", emoji.decoding = "async", (0, import_reader_image_fallback.replaceImageWithFallbackOnError)(emoji, () => {
	          const fallback = this.#document.createElement("span");
	          return fallback.append((0, import_reader_icon.renderReaderIcon)(
	            this.#document,
	            "smile",
	            this.#renderIcon
	          )), fallback;
	        }), typeIcon.append(emoji);
	      } else
	        typeIcon.append((0, import_reader_icon.renderReaderIcon)(
	          this.#document,
	          reactionEmojiId ? "smile" : record.icon,
	          this.#renderIcon
	        ));
	    }
	    const copy = this.#document.createElement("span");
	    copy.className = "ldp-notification-copy";
	    const title = this.#document.createElement("span");
	    title.className = "ldp-notification-title";
	    const titleText = this.#document.createElement("span");
	    if (titleText.className = "ldp-notification-title-text", titleText.textContent = recordDisplayTitle(record, archiveMarker), title.append(titleText), copy.append(title), record.excerpt) {
	      const excerpt = this.#document.createElement("span");
	      excerpt.className = "ldp-notification-excerpt", excerpt.textContent = record.excerpt, copy.append(excerpt);
	    }
	    const meta = this.#document.createElement("span");
	    meta.className = "ldp-notification-meta", meta.dataset.notificationCreatedAt = record.createdAt, meta.dataset.notificationArchiveLabel = archiveLabel, meta.textContent = `${archiveLabel ? `${archiveLabel} · ` : ""}${this.#relativeTime(record.createdAt)}`, copy.append(meta), item.append(typeIcon, copy);
	    const state = this.#document.createElement("span");
	    return state.className = "ldp-notification-read-state", state.textContent = readStateLabel, state.setAttribute("aria-label", `消息状态:${readStateLabel}`), item.append(state), item;
	  }
	  #startRelativeTimer() {
	    if (this.#relativeTimer !== null) return;
	    const update = () => {
	      if (this.#relativeTimer = null, !(!this.#controller.snapshot.open || this.scope.destroyed)) {
	        for (const node of this.#elements.list.querySelectorAll(
	          "[data-notification-created-at]"
	        )) {
	          const archiveLabel = node.dataset.notificationArchiveLabel ?? "";
	          node.textContent = `${archiveLabel ? `${archiveLabel} · ` : ""}${this.#relativeTime(node.dataset.notificationCreatedAt ?? "")}`;
	        }
	        this.#relativeTimer = this.#schedule(update, 3e4);
	      }
	    };
	    this.#relativeTimer = this.#schedule(update, 3e4);
	  }
	  #stopRelativeTimer() {
	    this.#relativeTimer !== null && (this.#cancel(this.#relativeTimer), this.#relativeTimer = null);
	  }
	}
}, "81b939d637c8e644b8e0c9803d798d3e1fc8525d732c18180fbfc4bb83e8dfc3");

/* Source: lite/src/queue/reader-open-queue-session.ts */
runtime.register("src/queue/reader-open-queue-session.js", function(module, exports, require) {
	var reader_open_queue_session_exports = {};
	__export(reader_open_queue_session_exports, {
	  READER_QUEUE_RESET_SURFACE_POSITIONS_EVENT: () => READER_QUEUE_RESET_SURFACE_POSITIONS_EVENT,
	  READER_QUEUE_STORAGE_KEY: () => READER_QUEUE_STORAGE_KEY,
	  ReaderOpenQueueSession: () => ReaderOpenQueueSession,
	  requestReaderQueueSurfacePositionsReset: () => requestReaderQueueSurfacePositionsReset
	});
	module.exports = __toCommonJS(reader_open_queue_session_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_reader_icon = require("../components/reader-icon.js"), import_event_target = require("../dom/event-target.js"), import_floating_surface_wheel = require("../dom/floating-surface-wheel.js"), import_html_element = require("../dom/html-element.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_repeat_action_gate = require("../kernel/repeat-action-gate.js"), import_reader_workspace = require("../shell/reader-workspace.js"), import_reader_account_scoped_storage = require("../state/reader-account-scoped-storage.js"), import_reader_userscript_target_adapter = require("../userscript/reader-userscript-target-adapter.js"), import_reader_topic_download_manager = require("./reader-topic-download-manager.js");
	const READER_QUEUE_STORAGE_KEY = "linuxdo-enhanced-reader:reader-queue:v1", READER_QUEUE_RESET_SURFACE_POSITIONS_EVENT = "ldp-reader-queue-reset-surface-positions", READER_QUEUE_DOCK_THRESHOLD_PX = 2, READER_QUEUE_PANEL_SHOW_DELAY_MS = 180, READER_QUEUE_PANEL_HIDE_GRACE_MS = 480, READER_QUEUE_CLEAR_CONFIRM_MS = 3e3;
	function requestReaderQueueSurfacePositionsReset(document) {
	  const EventConstructor = document.defaultView?.Event ?? Event;
	  document.dispatchEvent(new EventConstructor(
	    READER_QUEUE_RESET_SURFACE_POSITIONS_EVENT
	  ));
	}
	const TOPIC_ROW = "tr.topic-list-item,.topic-list-item,.latest-topic-list-item,.search-result-topic,.fps-result,.category-topic-link", TOPIC_LINK = 'a.raw-topic-link[href*="/t/"],a.title[href*="/t/"],.link-top-line a[href*="/t/"],a[href*="/t/"]';
	function readerActionLayerOwnsEscape(event) {
	  return event.composedPath().some((value) => {
	    const candidate = value;
	    return typeof candidate.matches == "function" && candidate.matches(".ldp-reader-action-layer");
	  });
	}
	function icon(document, name) {
	  return (0, import_reader_icon.createReaderIcon)(document, name);
	}
	function button(document, className, label, iconName) {
	  const result = (0, import_html_element.htmlElement)(document, "button", className);
	  return result.type = "button", result.setAttribute("aria-label", label), result.append(icon(document, iconName)), result;
	}
	function reconcileElementChildren(container, children) {
	  const expected = new Set(children);
	  for (let index = 0; index < children.length; index += 1) {
	    const child = children[index], current = container.children.item(index);
	    current !== child && container.insertBefore(child, current);
	  }
	  for (const child of [...container.children])
	    expected.has(child) || child.remove();
	}
	function mutationAffectsQueueScan(mutation, rail) {
	  if (mutation.target === rail || rail.contains(mutation.target)) return !1;
	  const selector = `${TOPIC_ROW},${TOPIC_LINK},.d-header-icons,.current-user`;
	  return [...mutation.addedNodes, ...mutation.removedNodes].some((value) => {
	    if (value.nodeType !== 1) return !1;
	    const element = value;
	    return element.matches(selector) || element.querySelector(selector) !== null;
	  });
	}
	function mutationAffectsQueueGeometry(mutation) {
	  const selector = ".ldp-header,.ldp-topic-action-rail";
	  return [...mutation.addedNodes, ...mutation.removedNodes].some((value) => {
	    if (value.nodeType !== 1) return !1;
	    const element = value;
	    return element.matches(selector) || element.querySelector(selector) !== null;
	  });
	}
	function normalizedSurface(value) {
	  const source = value && typeof value == "object" ? value : {}, numeric = (key, fallback) => {
	    const result = Number(source[key]);
	    return Number.isFinite(result) ? Math.min(1, Math.max(0, result)) : fallback;
	  }, x = numeric("x", 0.02), y = numeric("y", 0.12), dock = source.dock, normalizedDock = [
	    "left",
	    "right",
	    "top",
	    "bottom",
	    "title"
	  ].includes(String(dock)) ? dock : Object.hasOwn(source, "dock") ? "" : "left";
	  return {
	    x,
	    y,
	    // v2 会把当时的默认标题锚点随队列条目一起保存;只迁移这组
	    // canonical 默认值,保留用户拖动后的 title 与其他自定义位置。
	    dock: x === 0.02 && y === 0.12 && normalizedDock === "title" ? "left" : normalizedDock
	  };
	}
	function normalizedSurfaces(value, legacyValue) {
	  const source = value && typeof value == "object" ? value : {};
	  return {
	    floating: normalizedSurface(
	      Object.hasOwn(source, "floating") ? source.floating : legacyValue
	    ),
	    fullpage: normalizedSurface(
	      Object.hasOwn(source, "fullpage") ? source.fullpage : legacyValue
	    ),
	    embedded: normalizedSurface(
	      Object.hasOwn(source, "embedded") ? source.embedded : legacyValue
	    )
	  };
	}
	function defaultSurface(surface) {
	  return surface.x === 0.02 && surface.y === 0.12 && surface.dock === "left";
	}
	function normalizedEntry(value, baseUrl) {
	  if (!value || typeof value != "object") return null;
	  const source = value, topicId = (0, import_identifiers.tryDiscourseTopicId)(source.topicId);
	  if (!topicId) return null;
	  const href = String(source.href ?? `/t/${topicId}`), route = (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(href, baseUrl);
	  return {
	    topicId,
	    title: String(source.title ?? `帖子 #${topicId}`).replace(/\s+/g, " ").trim(),
	    href: route?.href ?? new URL(`/t/${topicId}`, baseUrl).href,
	    avatarTemplate: String(source.avatarTemplate ?? ""),
	    avatarSource: String(source.avatarSource ?? ""),
	    ownerUsername: String(source.ownerUsername ?? ""),
	    postNumber: (0, import_identifiers.tryDiscoursePostNumber)(source.postNumber) ?? route?.postNumber ?? null,
	    addedAt: Math.max(0, Number(source.addedAt) || 0) || Date.now(),
	    pinned: source.pinned === !0,
	    loadState: "queued",
	    loadedCount: 0,
	    totalCount: 0,
	    nestedLoadedCount: 0,
	    nestedTotalCount: 0,
	    mediaLoadedCount: 0,
	    mediaTotalCount: 0,
	    error: ""
	  };
	}
	function queueProgress(history) {
	  return !history || history.postsCount <= 0 ? 0 : Math.max(
	    0,
	    Math.min(100, history.readPostNumbers.length / history.postsCount * 100)
	  );
	}
	function queueStatus(entry, history, active) {
	  const readCount = history?.readPostNumbers.length ?? 0, total = history?.postsCount || entry.totalCount || 0, progress = Math.round(queueProgress(history)), pinned = entry.pinned ? "已固定 · " : "";
	  if (active) {
	    const currentFloor = history?.postNumber ?? entry.postNumber ?? null;
	    return `${pinned}阅读进度 ${progress}% · 已读 ${readCount}/${total || "?"}` + (currentFloor ? ` · 当前 #${currentFloor}` : "");
	  }
	  const preload = `正文 ${entry.loadedCount}/${entry.totalCount || "?"}`, nested = entry.nestedTotalCount > 0 ? ` · 二级回复 ${entry.nestedLoadedCount}/${entry.nestedTotalCount}` : "", media = entry.mediaTotalCount > 0 ? ` · 图片 ${entry.mediaLoadedCount}/${entry.mediaTotalCount}` : "";
	  return entry.loadState === "ready" ? `${pinned}已预加载 · ${preload}${nested}${media} · 阅读进度 ${progress}%` : entry.loadState === "loading" ? `${pinned}正在预加载 · ${preload}${nested}${media} · 阅读进度 ${progress}%` : entry.loadState === "partial" ? `${pinned}已分层预加载 · ${preload}${nested}${media} · 阅读进度 ${progress}%` : entry.loadState === "error" ? `${pinned}预加载失败 · 阅读进度 ${progress}%,点击重试` : `${pinned}等待预加载 · 阅读进度 ${progress}%`;
	}
	class ReaderOpenQueueSession {
	  scope;
	  #options;
	  #workspaceRoot;
	  #storageKey;
	  #accountStorage;
	  #entries = /* @__PURE__ */ new Map();
	  #rail;
	  #toggleShell;
	  #toggle;
	  #badge;
	  #dismiss;
	  #bubbles;
	  #scrollHint;
	  #panel;
	  #count;
	  #clear;
	  #list;
	  #downloadManager;
	  #avatarIdentity = /* @__PURE__ */ new WeakMap();
	  #surfaces;
	  #prefetchTail = Promise.resolve();
	  #prefetching = /* @__PURE__ */ new Set();
	  #prefetchControllers = /* @__PURE__ */ new Map();
	  #closeGate = new import_repeat_action_gate.RepeatActionGate();
	  #requestFrame;
	  #cancelFrame;
	  #resizeObserver;
	  #observedSurfaceElements = /* @__PURE__ */ new WeakSet();
	  #scanQueued = !1;
	  #panelOpen = !1;
	  #panelPinned = !1;
	  #renderKey = "";
	  #surfaceFrame = 0;
	  #dragFrame = 0;
	  #syncFrame = 0;
	  #dragGeometry = null;
	  #dragging = !1;
	  #suppressToggleClick = !1;
	  #closePinnedPanelOnClick = !1;
	  #hoverOpenTimer = 0;
	  #hoverCloseTimer = 0;
	  #clearConfirmTimer = 0;
	  #clearConfirmationPending = !1;
	  #activeTopicId = null;
	  #nativeTriggerItem = null;
	  #nativeTriggerButton = null;
	  constructor(options) {
	    if (this.#options = options, this.#workspaceRoot = options.workspaceRoot ?? options.root.closest("[data-reader-workspace-mode]") ?? options.root, this.#accountStorage = options.storageKey === void 0 && options.authScope !== void 0 ? (0, import_reader_account_scoped_storage.readerAccountScopedStorageIdentity)(
	      READER_QUEUE_STORAGE_KEY,
	      options.authScope
	    ) : null, this.#storageKey = String(options.storageKey ?? this.#accountStorage?.key ?? READER_QUEUE_STORAGE_KEY).trim(), !this.#storageKey) throw new Error("reader queue storage key 不能为空");
	    const view = options.document.defaultView;
	    this.#requestFrame = options.requestFrame ?? ((callback) => {
	      const request = view?.requestAnimationFrame;
	      return typeof request == "function" ? request.call(view, callback) : (callback(0), 0);
	    }), this.#cancelFrame = options.cancelFrame ?? ((id) => {
	      const cancel = view?.cancelAnimationFrame;
	      typeof cancel == "function" && cancel.call(view, id);
	    }), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const restored = this.#restore();
	    this.#surfaces = restored.surfaces;
	    for (const entry of restored.entries) this.#entries.set(entry.topicId, entry);
	    const document = options.document;
	    this.scope.listen(
	      document,
	      READER_QUEUE_RESET_SURFACE_POSITIONS_EVENT,
	      () => this.resetSurfacePositions()
	    ), this.#rail = (0, import_html_element.htmlElement)(document, "aside", "ldp-reader-queue"), this.#toggle = button(
	      document,
	      "ldp-reader-queue-toggle",
	      "阅读队列",
	      "book-open"
	    ), this.#toggle.setAttribute("aria-expanded", "false"), this.#toggle.setAttribute("aria-pressed", "false"), this.#toggle.setAttribute("aria-haspopup", "listbox"), this.#badge = (0, import_html_element.htmlElement)(document, "b"), this.#toggle.append(this.#badge), this.#dismiss = button(
	      document,
	      "ldp-reader-queue-dismiss",
	      "关闭阅读队列",
	      "x"
	    ), this.#toggleShell = (0, import_html_element.htmlElement)(
	      document,
	      "span",
	      "ldp-reader-queue-toggle-shell"
	    ), this.#toggleShell.append(this.#toggle, this.#dismiss), this.#bubbles = (0, import_html_element.htmlElement)(document, "div", "ldp-reader-queue-bubbles"), this.#bubbles.setAttribute("aria-label", "队列文章头像,可滚动查看"), this.#scrollHint = button(
	      document,
	      "ldp-reader-queue-scroll-hint",
	      "显示下方更多队列头像",
	      "chevron-down"
	    ), this.#scrollHint.hidden = !0, this.#scrollHint.dataset.scrollDirection = "1", this.#panel = (0, import_html_element.htmlElement)(document, "section", "ldp-reader-queue-panel"), this.#panel.setAttribute("aria-label", "阅读队列文章列表"), this.#panel.hidden = !0;
	    const head = (0, import_html_element.htmlElement)(document, "header", "ldp-reader-queue-panel-head"), title = (0, import_html_element.htmlElement)(document, "strong");
	    title.textContent = "阅读队列", this.#count = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-queue-panel-count"), this.#clear = button(
	      document,
	      "ldp-reader-queue-clear",
	      "移除未固定主题",
	      "trash"
	    );
	    const close = button(
	      document,
	      "ldp-reader-queue-close",
	      "关闭阅读队列",
	      "x"
	    );
	    head.append(title, this.#count), head.append(this.#clear, close), this.#list = (0, import_html_element.htmlElement)(document, "div", "ldp-reader-queue-list"), this.#list.setAttribute("role", "listbox"), this.#list.setAttribute("aria-label", "队列文章"), this.#panel.append(head, this.#list), this.#downloadManager = options.topicDownloads ? new import_reader_topic_download_manager.ReaderTopicDownloadManager({
	      ...options.topicDownloads,
	      document,
	      mount: options.topicDownloads.mount ?? this.#panel,
	      geometryStorage: options.storage,
	      currentTopic: () => {
	        const topicId = this.#options.currentTopicId();
	        if (!topicId) return null;
	        const entry = this.#entries.get(topicId), history = this.#options.historyEntry(topicId);
	        return Object.freeze({
	          topicId,
	          title: entry?.title || history?.title || `Topic #${topicId}`
	        });
	      },
	      ...options.notify ? { notify: options.notify } : {},
	      parentScope: this.scope
	    }) : null, this.#downloadManager?.changes.subscribe(() => {
	      this.#scheduleSurfaceMeasure();
	    }, this.scope), this.#rail.append(
	      this.#toggleShell,
	      this.#bubbles,
	      this.#scrollHint,
	      this.#panel
	    ), options.root.append(this.#rail), this.scope.listen(
	      this.#workspaceRoot,
	      "ldp-reader-workspace-change",
	      () => {
	        this.#cancelSurfaceFrames(), this.#scheduleSurfaceMeasure();
	      }
	    ), this.#resizeObserver = options.createResizeObserver?.(() => this.#scheduleSurfaceMeasure()) ?? (typeof ResizeObserver == "function" ? new ResizeObserver(() => this.#scheduleSurfaceMeasure()) : null), this.#observeSurfaceElement(options.root), this.#observeSurfaceElement(this.#rail), this.#scheduleSurfaceMeasure(), this.scope.add(() => this.#rail.remove()), this.scope.add(() => {
	      for (const controller of this.#prefetchControllers.values())
	        controller.abort(new DOMException("阅读队列已销毁", "AbortError"));
	      this.#prefetchControllers.clear();
	    }), this.scope.add(() => this.#closeGate.clear()), this.scope.add(() => {
	      this.#resizeObserver?.disconnect(), this.#cancelPanelPreview(), this.#cancelPanelClose(), this.#resetClearConfirmation(), this.#cancelSurfaceFrames(), this.#syncFrame && this.#cancelFrame(this.#syncFrame), this.#syncFrame = 0, this.#nativeTriggerItem?.remove(), this.#nativeTriggerItem = null, this.#nativeTriggerButton = null, options.document.documentElement.classList.remove(
	        "ldp-native-reader-trigger-visible"
	      );
	    }), this.scope.listen(this.#rail, "click", (event) => this.#click(event)), this.scope.listen(this.#toggle, "pointerenter", (event) => this.#schedulePanelPreview(event)), this.scope.listen(this.#toggle, "pointerleave", () => this.#cancelPanelPreview()), this.scope.listen(this.#rail, "pointerenter", () => {
	      this.#rail.classList.add("is-dock-revealed"), this.#cancelPanelClose();
	    }), this.scope.listen(this.#rail, "pointerleave", () => {
	      this.#panelOpen || this.#rail.classList.remove("is-dock-revealed"), this.#cancelPanelPreview(), this.#schedulePanelClose();
	    }), this.scope.listen(this.#panel, "focusin", () => this.#cancelPanelClose()), this.scope.listen(this.#panel, "pointerenter", () => {
	      this.#cancelPanelClose();
	    }), this.scope.listen(this.#panel, "focusout", (event) => {
	      const next = event.relatedTarget;
	      next && typeof next.nodeType == "number" && this.#panel.contains(next) || this.#schedulePanelClose();
	    }), this.scope.listen(this.#bubbles, "scroll", () => this.#syncScrollHint(), { passive: !0 }), this.scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(this.#bubbles)), this.scope.add((0, import_floating_surface_wheel.bindFloatingSurfaceWheel)(this.#panel)), this.scope.listen(options.root, "pointerdown", (event) => {
	      const target = (0, import_event_target.eventElement)(event);
	      this.#panelOpen && !target?.closest(".ldp-reader-queue") && this.#setPanelOpen(!1);
	    }), this.scope.listen(this.#toggle, "pointerdown", (event) => this.#drag(event)), this.scope.listen(document, "click", (event) => this.#documentClick(event), !0), this.scope.listen(document, "keydown", (event) => this.#keydown(event), !0);
	    const observer = options.createMutationObserver?.((mutations) => {
	      mutations.some(mutationAffectsQueueGeometry) && this.#scheduleSurfaceMeasure(), mutations.some((mutation) => mutationAffectsQueueScan(mutation, this.#rail)) && this.#queueScan();
	    }) ?? (typeof MutationObserver == "function" ? new MutationObserver((mutations) => {
	      mutations.some(mutationAffectsQueueGeometry) && this.#scheduleSurfaceMeasure(), mutations.some((mutation) => mutationAffectsQueueScan(mutation, this.#rail)) && this.#queueScan();
	    }) : null);
	    observer && document.body && this.scope.observe(observer, document.body, {
	      childList: !0,
	      subtree: !0
	    }), this.#scan(), this.sync();
	  }
	  get size() {
	    return this.#entries.size;
	  }
	  get storageKey() {
	    return this.#storageKey;
	  }
	  get #surface() {
	    return this.#surfaces[(0, import_reader_workspace.readerWorkspacePositionMode)(
	      this.#workspaceRoot.dataset.readerWorkspaceMode
	    )];
	  }
	  syncEntries() {
	    return Object.freeze([...this.#entries.values()].sort((left, right) => left.addedAt - right.addedAt || left.topicId - right.topicId).map((entry) => Object.freeze({
	      topicId: entry.topicId,
	      title: entry.title,
	      href: entry.href,
	      avatarTemplate: entry.avatarTemplate,
	      avatarSource: entry.avatarSource,
	      ownerUsername: entry.ownerUsername,
	      postNumber: entry.postNumber,
	      addedAt: entry.addedAt,
	      pinned: entry.pinned
	    })));
	  }
	  replaceExternal(values) {
	    const entries = values.map((value) => normalizedEntry(value, this.#options.document.baseURI)).filter((entry) => entry !== null);
	    for (const [topicId, controller] of this.#prefetchControllers)
	      entries.some((entry) => entry.topicId === topicId) || controller.abort(new DOMException("队列已由 WebDAV 更新", "AbortError"));
	    this.#entries.clear();
	    for (const entry of entries) this.#entries.set(entry.topicId, entry);
	    this.#persist(), this.sync();
	  }
	  reloadExternal() {
	    if (this.scope.destroyed) return;
	    const restored = this.#restore();
	    for (const [topicId, controller] of this.#prefetchControllers)
	      restored.entries.some((entry) => entry.topicId === topicId) || controller.abort(
	        new DOMException("队列已由其他标签更新", "AbortError")
	      );
	    this.#entries.clear();
	    for (const entry of restored.entries) this.#entries.set(entry.topicId, entry);
	    for (const mode of Object.keys(this.#surfaces))
	      Object.assign(this.#surfaces[mode], restored.surfaces[mode]);
	    this.#cancelSurfaceFrames(), this.sync(), this.#scheduleSurfaceMeasure();
	  }
	  reloadExternalDownloads() {
	    return this.#downloadManager?.reloadExternal() ?? Promise.resolve();
	  }
	  sync() {
	    this.#syncNativeReaderTrigger();
	    const active = this.#options.currentTopicId();
	    this.#downloadManager?.syncCurrent();
	    const entries = [...this.#entries.values()].sort((left, right) => Number(right.pinned) - Number(left.pinned) || left.addedAt - right.addedAt), alwaysVisible = this.#options.readPreferences().readerQueueAlwaysVisibleWhenEmpty, renderKey = JSON.stringify({
	      active,
	      alwaysVisible,
	      entries: entries.map((entry) => {
	        const history = this.#options.historyEntry(entry.topicId);
	        return [
	          entry.topicId,
	          entry.title,
	          entry.href,
	          entry.avatarTemplate,
	          entry.avatarSource,
	          entry.ownerUsername,
	          entry.postNumber,
	          entry.addedAt,
	          entry.pinned,
	          this.#prefetching.has(entry.topicId),
	          entry.loadState,
	          entry.loadedCount,
	          entry.totalCount,
	          entry.nestedLoadedCount,
	          entry.nestedTotalCount,
	          entry.mediaLoadedCount,
	          entry.mediaTotalCount,
	          entry.error,
	          history?.avatarTemplate ?? "",
	          history?.ownerUsername ?? "",
	          history?.readPostNumbers.length ?? -1,
	          history?.postsCount ?? -1
	        ];
	      })
	    });
	    if (renderKey !== this.#renderKey) {
	      const previousBubbleScrollTop = this.#bubbles.scrollTop, activeChanged = active !== this.#activeTopicId, bubbleShells = this.#bubbleShellsByTopic(), rowAvatars = this.#avatarsByTopic(this.#list);
	      this.#renderKey = renderKey, this.#syncRailPresence(), this.#rail.classList.toggle("is-empty", !entries.length), this.#count.textContent = `${entries.length} 篇`, this.#resetClearConfirmation(), this.#clear.disabled = !entries.some((entry) => !entry.pinned);
	      const bubbles = entries.map((entry) => this.#bubbleShell(
	        entry,
	        active,
	        bubbleShells.get(entry.topicId)
	      ));
	      if (reconcileElementChildren(this.#bubbles, bubbles), this.#list.replaceChildren(
	        ...entries.map((entry) => this.#row(
	          entry,
	          active,
	          rowAvatars.get(entry.topicId)
	        ))
	      ), this.#activeTopicId = active, activeChanged) {
	        const activeBubble = this.#bubbles.querySelector(
	          ".ldp-reader-queue-bubble.is-active"
	        );
	        if (activeBubble) {
	          const top = activeBubble.offsetTop, bottom = top + activeBubble.offsetHeight;
	          top < this.#bubbles.scrollTop ? this.#bubbles.scrollTop = Math.max(0, top - 4) : bottom > this.#bubbles.scrollTop + this.#bubbles.clientHeight && (this.#bubbles.scrollTop = Math.max(
	            0,
	            bottom - this.#bubbles.clientHeight + 4
	          ));
	        }
	      } else
	        this.#bubbles.scrollTop = Math.min(
	          previousBubbleScrollTop,
	          Math.max(
	            0,
	            this.#bubbles.scrollHeight - this.#bubbles.clientHeight
	          )
	        );
	      this.#syncScrollHint(), this.#syncToggleState(), this.#scheduleSurfaceMeasure();
	    }
	    for (const add of this.#options.document.querySelectorAll(
	      ".ldp-reader-queue-add[data-reader-queue-topic-id]"
	    )) {
	      const topicId = (0, import_identifiers.tryDiscourseTopicId)(
	        add.dataset.readerQueueTopicId
	      ), added = topicId ? this.#entries.has(topicId) : !1, stateChanged = add.getAttribute("aria-pressed") !== String(added), label = added ? "移出阅读队列" : "加入阅读队列并后台预加载", labelChanged = add.dataset.ldpTooltipLabel !== label;
	      if (stateChanged && (add.classList.toggle("is-added", added), add.setAttribute("aria-pressed", String(added)), add.replaceChildren(icon(
	        this.#options.document,
	        added ? "check" : "plus"
	      ))), add.setAttribute("aria-label", label), add.dataset.ldpTooltipLabel = label, labelChanged) {
	        const EventConstructor = add.ownerDocument.defaultView?.Event ?? Event;
	        add.dispatchEvent(new EventConstructor(
	          "ldp-tooltip-refresh",
	          { bubbles: !0 }
	        ));
	      }
	    }
	  }
	  #syncRailPresence() {
	    const alwaysVisible = this.#options.readPreferences().readerQueueAlwaysVisibleWhenEmpty;
	    this.#rail.hidden = !this.#entries.size && !alwaysVisible, this.#badge.hidden = this.#entries.size === 0, this.#badge.textContent = this.#entries.size ? String(this.#entries.size) : "", this.#dismiss.setAttribute(
	      "aria-label",
	      this.#entries.size ? "收起阅读队列头像" : "隐藏空阅读队列入口"
	    );
	  }
	  downloadCurrentTopic() {
	    const prepared = this.#downloadManager?.prepareCurrentDownload() ?? !1;
	    return prepared && this.#options.notify?.("请选择下载范围后开始后台下载"), prepared;
	  }
	  openTopicDownloadManager() {
	    return this.#downloadManager?.openManager() ?? !1;
	  }
	  refreshSurface() {
	    this.#scheduleSurfaceMeasure();
	  }
	  resetSurfacePositions() {
	    this.#reloadStoredEntriesForMutation();
	    for (const surface of Object.values(this.#surfaces))
	      surface.x = 0.02, surface.y = 0.12, surface.dock = "left";
	    this.#persist(), this.#cancelSurfaceFrames(), this.#scheduleSurfaceMeasure();
	  }
	  toggle() {
	    if (!(this.scope.destroyed || this.#rail.hidden)) {
	      if (this.#rail.classList.contains("is-preview-collapsed") && this.#setPreviewExpanded(!0), this.#panelPinned) {
	        this.#setPanelOpen(!1);
	        return;
	      }
	      this.#panelPinned = !0, this.#setPanelOpen(!0);
	    }
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  #row(entry, active, reusableAvatar) {
	    const document = this.#options.document, row = (0, import_html_element.htmlElement)(document, "article", "ldp-reader-queue-row");
	    row.dataset.queueOpen = String(entry.topicId), row.tabIndex = 0, row.setAttribute("role", "option"), row.setAttribute("aria-selected", String(entry.topicId === active)), row.classList.toggle("is-active", entry.topicId === active), row.classList.toggle("is-pinned", entry.pinned);
	    const progress = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-queue-row-progress"), history = this.#options.historyEntry(entry.topicId), progressValue = queueProgress(history);
	    progress.classList.toggle("is-progress-complete", progressValue >= 100), progress.style.setProperty(
	      "--ldp-reader-queue-progress",
	      `${progressValue * 3.6}deg`
	    ), this.#avatar(progress, entry, history, reusableAvatar);
	    const copy = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-queue-row-copy"), title = (0, import_html_element.htmlElement)(document, "strong");
	    title.textContent = entry.title;
	    const status = (0, import_html_element.htmlElement)(document, "small");
	    status.textContent = queueStatus(
	      entry,
	      history,
	      entry.topicId === active
	    ), copy.append(title, status);
	    const actions = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-queue-row-actions"), pin = button(
	      document,
	      "ldp-reader-queue-pin",
	      entry.pinned ? "取消固定,离开后自动移出队列" : "固定文章,离开后保留在队列",
	      "pin"
	    );
	    pin.dataset.queuePin = String(entry.topicId), pin.classList.toggle("active", entry.pinned), pin.setAttribute("aria-pressed", String(entry.pinned));
	    const remove = button(
	      document,
	      "ldp-reader-queue-remove",
	      `从阅读队列移除 ${entry.title}`,
	      "x"
	    );
	    if (remove.dataset.queueRemove = String(entry.topicId), entry.loadState === "error") {
	      const retry = button(
	        document,
	        "ldp-reader-queue-retry",
	        "重新预加载",
	        "rotate-ccw"
	      );
	      retry.dataset.queueRetry = String(entry.topicId), actions.append(retry);
	    }
	    return actions.append(pin, remove), row.append(progress, copy, actions), row;
	  }
	  #bubbleShell(entry, active, reusable) {
	    const document = this.#options.document, shell = reusable ?? (0, import_html_element.htmlElement)(
	      document,
	      "span",
	      "ldp-reader-queue-bubble-shell"
	    );
	    shell.className = "ldp-reader-queue-bubble-shell", shell.classList.toggle("is-pinned", entry.pinned);
	    let bubble = shell.querySelector(
	      ":scope > .ldp-reader-queue-bubble"
	    );
	    bubble || (bubble = button(
	      document,
	      "ldp-reader-queue-bubble",
	      entry.title,
	      "message-square"
	    )), bubble.className = "ldp-reader-queue-bubble", bubble.dataset.queueOpen = String(entry.topicId), bubble.dataset.readerQueueTopicId = String(entry.topicId), bubble.classList.toggle("is-active", entry.topicId === active), bubble.setAttribute(
	      "aria-current",
	      String(entry.topicId === active)
	    ), bubble.classList.add(`is-${entry.loadState}`);
	    const history = this.#options.historyEntry(entry.topicId), progress = queueProgress(history);
	    bubble.classList.toggle("is-progress-complete", progress >= 100), bubble.style.setProperty(
	      "--ldp-reader-queue-progress",
	      `${progress * 3.6}deg`
	    );
	    const label = `${entry.title},${queueStatus(
	      entry,
	      history,
	      entry.topicId === active
	    )}`;
	    bubble.setAttribute("aria-label", label), bubble.dataset.ldpTooltipLabel = label, this.#avatar(
	      bubble,
	      entry,
	      history,
	      bubble.querySelector(".ldp-reader-queue-avatar") ?? void 0
	    );
	    let status = bubble.querySelector(":scope > i");
	    status || (status = (0, import_html_element.htmlElement)(document, "i")), reconcileElementChildren(bubble, [
	      bubble.querySelector(".ldp-reader-queue-avatar"),
	      status
	    ]);
	    let remove = shell.querySelector(
	      ":scope > .ldp-reader-queue-bubble-remove"
	    );
	    remove || (remove = button(
	      document,
	      "ldp-reader-queue-bubble-remove",
	      `从阅读队列移除 ${entry.title}`,
	      "x"
	    )), remove.setAttribute("aria-label", `从阅读队列移除 ${entry.title}`), remove.dataset.queueRemove = String(entry.topicId);
	    let pin = shell.querySelector(
	      ":scope > .ldp-reader-queue-bubble-pin"
	    );
	    return entry.pinned && !pin && (pin = (0, import_html_element.htmlElement)(document, "span", "ldp-reader-queue-bubble-pin"), pin.append(icon(document, "pin"))), reconcileElementChildren(shell, [
	      bubble,
	      remove,
	      ...entry.pinned && pin ? [pin] : []
	    ]), shell;
	  }
	  #avatar(host, entry, history, reusableAvatar) {
	    const fallbackText = (entry.ownerUsername || history?.ownerUsername || entry.title).trim().slice(0, 1).toUpperCase() || "?", template = entry.avatarTemplate || history?.avatarTemplate || "", source = entry.avatarSource || (template ? this.#options.avatarSource?.(template, 64) ?? "" : ""), identity = JSON.stringify([fallbackText, source]);
	    if (reusableAvatar && this.#avatarIdentity.get(reusableAvatar) === identity) {
	      reusableAvatar.parentElement !== host && host.replaceChildren(reusableAvatar);
	      return;
	    }
	    const avatar = (0, import_html_element.htmlElement)(
	      this.#options.document,
	      "span",
	      "ldp-reader-queue-avatar"
	    );
	    this.#avatarIdentity.set(avatar, identity);
	    const fallback = (0, import_html_element.htmlElement)(
	      this.#options.document,
	      "span",
	      "ldp-reader-queue-avatar-fallback"
	    );
	    if (fallback.textContent = fallbackText, avatar.append(fallback), source) {
	      const image = (0, import_html_element.htmlElement)(this.#options.document, "img");
	      image.addEventListener("load", () => {
	        image.classList.add("is-loaded");
	      }), image.addEventListener("error", () => {
	        image.remove();
	      }), image.src = source, image.alt = "", image.loading = "eager", image.decoding = "async", avatar.append(image);
	    }
	    host.replaceChildren(avatar);
	  }
	  #avatarsByTopic(container) {
	    const avatars = /* @__PURE__ */ new Map();
	    for (const host of container.querySelectorAll(
	      "[data-queue-open]"
	    )) {
	      const topicId = (0, import_identifiers.tryDiscourseTopicId)(host.dataset.queueOpen), avatar = host.querySelector(
	        ".ldp-reader-queue-avatar"
	      );
	      topicId && avatar && avatars.set(topicId, avatar);
	    }
	    return avatars;
	  }
	  #bubbleShellsByTopic() {
	    const shells = /* @__PURE__ */ new Map();
	    for (const shell of this.#bubbles.querySelectorAll(
	      ":scope > .ldp-reader-queue-bubble-shell"
	    )) {
	      const topicId = (0, import_identifiers.tryDiscourseTopicId)(
	        shell.querySelector("[data-queue-open]")?.dataset.queueOpen
	      );
	      topicId && shells.set(topicId, shell);
	    }
	    return shells;
	  }
	  #click(event) {
	    const action = (0, import_event_target.eventElement)(event)?.closest(
	      "[data-queue-open],[data-queue-pin],[data-queue-remove],[data-queue-retry],.ldp-reader-queue-clear,.ldp-reader-queue-close,.ldp-reader-queue-dismiss,.ldp-reader-queue-toggle,.ldp-reader-queue-scroll-hint"
	    );
	    if (!action) return;
	    if (action === this.#scrollHint) {
	      const direction = Number(this.#scrollHint.dataset.scrollDirection) || 1, behavior = this.#options.document.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)").matches === !0 ? "auto" : "smooth";
	      if (direction < 0)
	        typeof this.#bubbles.scrollTo == "function" ? this.#bubbles.scrollTo({ top: 0, behavior }) : this.#bubbles.scrollTop = 0;
	      else {
	        const distance = Math.max(
	          38,
	          Math.round(this.#bubbles.clientHeight * 0.72)
	        );
	        typeof this.#bubbles.scrollBy == "function" ? this.#bubbles.scrollBy({ top: distance, behavior }) : this.#bubbles.scrollTop += distance;
	      }
	      return;
	    }
	    if (action === this.#toggle) {
	      if (this.#suppressToggleClick) {
	        this.#suppressToggleClick = !1, this.#closePinnedPanelOnClick = !1;
	        return;
	      }
	      if (this.#closePinnedPanelOnClick) {
	        this.#closePinnedPanelOnClick = !1;
	        return;
	      }
	      this.toggle();
	      return;
	    }
	    if (action === this.#dismiss) {
	      this.#setPanelOpen(!1), this.#entries.size ? this.#setPreviewExpanded(!1) : (this.#options.updatePreferences({
	        readerQueueAlwaysVisibleWhenEmpty: !1
	      }), this.sync());
	      return;
	    }
	    if (action.classList.contains("ldp-reader-queue-close")) {
	      this.#setPanelOpen(!1);
	      return;
	    }
	    if ((action === this.#clear || action.dataset.queuePin !== void 0 || action.dataset.queueRemove !== void 0) && this.#reloadStoredEntriesForMutation(), action === this.#clear) {
	      const removable = [...this.#entries.values()].filter((entry) => !entry.pinned);
	      if (!removable.length) {
	        this.#resetClearConfirmation();
	        return;
	      }
	      if (!this.#clearConfirmationPending) {
	        this.#armClearConfirmation(removable.length), this.#options.notify?.(
	          `再点一次垃圾桶,移除 ${removable.length} 篇未固定主题`
	        );
	        return;
	      }
	      this.#resetClearConfirmation();
	      for (const [topicId, entry] of this.#entries)
	        entry.pinned || this.#remove(topicId);
	      this.#persist(), this.sync();
	      return;
	    }
	    const pinId = (0, import_identifiers.tryDiscourseTopicId)(action.dataset.queuePin);
	    if (pinId) {
	      const entry = this.#entries.get(pinId);
	      entry && (entry.pinned = !entry.pinned), this.#persist(), this.sync();
	      return;
	    }
	    const removeId = (0, import_identifiers.tryDiscourseTopicId)(action.dataset.queueRemove);
	    if (removeId) {
	      this.#remove(removeId), this.#persist(), this.sync();
	      return;
	    }
	    const retryId = (0, import_identifiers.tryDiscourseTopicId)(action.dataset.queueRetry);
	    if (retryId) {
	      this.#queuePrefetch(retryId, !0);
	      return;
	    }
	    const openId = (0, import_identifiers.tryDiscourseTopicId)(action.dataset.queueOpen);
	    openId && this.#open(openId);
	  }
	  #documentClick(event) {
	    const target = (0, import_event_target.eventElement)(event), add = target?.closest(".ldp-reader-queue-add");
	    if (add) {
	      event.preventDefault(), event.stopPropagation();
	      const topicId2 = (0, import_identifiers.tryDiscourseTopicId)(add.dataset.readerQueueTopicId);
	      if (!topicId2) return;
	      if (this.#reloadStoredEntriesForMutation(), this.#entries.has(topicId2)) this.#remove(topicId2);
	      else {
	        const href = String(add.dataset.readerQueueHref ?? `/t/${topicId2}`);
	        this.#entries.set(topicId2, {
	          topicId: topicId2,
	          title: String(add.dataset.readerQueueTitle ?? `帖子 #${topicId2}`),
	          href,
	          avatarTemplate: String(
	            add.dataset.readerQueueAvatarTemplate ?? ""
	          ),
	          avatarSource: String(add.dataset.readerQueueAvatar ?? ""),
	          ownerUsername: String(add.dataset.readerQueueOwner ?? ""),
	          postNumber: (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(
	            href,
	            this.#options.document.baseURI
	          )?.postNumber ?? null,
	          addedAt: Date.now(),
	          pinned: !1,
	          loadState: "queued",
	          loadedCount: 0,
	          totalCount: 0,
	          nestedLoadedCount: 0,
	          nestedTotalCount: 0,
	          mediaLoadedCount: 0,
	          mediaTotalCount: 0,
	          error: ""
	        }), this.#queuePrefetch(topicId2);
	      }
	      this.#persist(), this.sync();
	      return;
	    }
	    const native = target?.closest(".ldp-native-reader-trigger");
	    if (!native) return;
	    event.preventDefault();
	    const button2 = native, topicId = (0, import_identifiers.tryDiscourseTopicId)(button2.dataset.topicId);
	    topicId && this.#open(
	      topicId,
	      button2.dataset.triggerSource === "history" ? null : (0, import_identifiers.tryDiscoursePostNumber)(button2.dataset.postNumber),
	      button2.dataset.triggerSource === "route" ? "link" : "restore"
	    );
	  }
	  #keydown(event) {
	    const target = (0, import_event_target.eventElement)(event);
	    if (target === this.#toggle && event.key === "ArrowDown") {
	      event.preventDefault(), this.#setPanelOpen(!0), this.#list.querySelector(
	        ".ldp-reader-queue-row[data-queue-open]"
	      )?.focus();
	      return;
	    }
	    const queueRow = target?.closest(
	      ".ldp-reader-queue-row[data-queue-open]"
	    );
	    if (queueRow && this.#panel.contains(queueRow)) {
	      const rows = [...this.#panel.querySelectorAll(
	        ".ldp-reader-queue-row[data-queue-open]"
	      )], index = rows.indexOf(queueRow);
	      if (event.key === "Enter" || event.key === " ") {
	        event.preventDefault();
	        const topicId = (0, import_identifiers.tryDiscourseTopicId)(queueRow.dataset.queueOpen);
	        topicId && this.#open(topicId);
	        return;
	      }
	      if (event.key === "ArrowDown" || event.key === "ArrowUp") {
	        event.preventDefault();
	        const offset = event.key === "ArrowDown" ? 1 : -1;
	        rows[(index + offset + rows.length) % rows.length]?.focus();
	        return;
	      }
	    }
	    if (!(event.key !== "Escape" || event.defaultPrevented)) {
	      if (event.repeat) {
	        if (this.#options.readerLightboxOpen?.()) return;
	        event.preventDefault(), event.stopImmediatePropagation();
	        return;
	      }
	      if (!readerActionLayerOwnsEscape(event) && !this.#options.composerOpen() && !this.#options.readerSurfaceOpen?.()) {
	        if (this.#panelOpen) {
	          event.preventDefault(), event.stopImmediatePropagation(), this.#setPanelOpen(!1), this.#toggle.focus();
	          return;
	        }
	        if (!(target?.matches?.(
	          'input,textarea,select,[contenteditable="true"]'
	        ) || !this.#options.currentTopicId())) {
	          if (this.#options.closeExpandedReply?.()) {
	            event.preventDefault(), event.stopImmediatePropagation();
	            return;
	          }
	          if (this.#options.readPreferences().doubleEscapeToCloseReader && !this.#closeGate.confirm("reader:escape")) {
	            event.preventDefault(), event.stopImmediatePropagation(), this.#options.notify?.("再按一次 Esc 关闭阅读器");
	            return;
	          }
	          event.preventDefault(), event.stopImmediatePropagation(), this.#options.closeReader();
	        }
	      }
	    }
	  }
	  #drag(event) {
	    if (this.#reloadStoredEntriesForMutation(), event.button !== 0) return;
	    this.#cancelPanelPreview(), this.#closePinnedPanelOnClick = this.#panelPinned, this.#setPanelOpen(!1), this.#cancelSurfaceFrames();
	    const geometry = this.#measureSurface(), { rail: start, parent } = geometry, offsetX = event.clientX - start.left, offsetY = event.clientY - start.top, originX = event.clientX, originY = event.clientY;
	    let moved = !1;
	    this.#suppressToggleClick = !1, this.#toggle.setPointerCapture?.(event.pointerId);
	    const move = (next) => {
	      if (next.pointerId !== event.pointerId) return;
	      if (!moved) {
	        if (Math.hypot(
	          next.clientX - originX,
	          next.clientY - originY
	        ) < 5) return;
	        moved = !0, this.#dragging = !0, this.#suppressToggleClick = !0, this.#closePinnedPanelOnClick = !1, this.#setPanelOpen(!1), this.#rail.classList.remove(
	          "is-docked-left",
	          "is-docked-right",
	          "is-docked-top",
	          "is-docked-bottom",
	          "is-docked-title",
	          "is-dock-revealed"
	        ), this.#dragGeometry = geometry, this.#rail.classList.add("is-dragging");
	      }
	      const width = Math.max(1, parent.width - start.width), height = Math.max(1, parent.height - start.height);
	      this.#surface.x = Math.min(
	        1,
	        Math.max(0, (next.clientX - parent.left - offsetX) / width)
	      ), this.#surface.y = Math.min(
	        1,
	        Math.max(0, (next.clientY - parent.top - offsetY) / height)
	      );
	      const railLeft = this.#surface.x * width, railTop = this.#surface.y * height, edgeDistance = READER_QUEUE_DOCK_THRESHOLD_PX, titleTop = geometry.header ? geometry.header.bottom - parent.top + parent.height * 8e-3 : Number.POSITIVE_INFINITY, titleLeft = geometry.action ? geometry.action.left + geometry.action.width / 2 - parent.left - start.width / 2 : Number.POSITIVE_INFINITY;
	      this.#surface.dock = Math.abs(railLeft - titleLeft) <= edgeDistance && Math.abs(railTop - titleTop) <= edgeDistance ? "title" : railLeft <= edgeDistance ? "left" : width - railLeft <= edgeDistance ? "right" : railTop <= edgeDistance ? "top" : height - railTop <= edgeDistance ? "bottom" : "", this.#scheduleDragProjection();
	    };
	    let cleanup = () => {
	    };
	    const finish = (next) => {
	      next.pointerId === event.pointerId && (cleanup(), (moved || next.type !== "pointerup") && (this.#closePinnedPanelOnClick = !1), this.#dragging = !1, this.#rail.classList.remove("is-dragging"), this.#toggle.hasPointerCapture?.(event.pointerId) && this.#toggle.releasePointerCapture?.(event.pointerId), moved && (this.#persist(), this.#scheduleSurfaceMeasure()));
	    };
	    cleanup = this.scope.add(() => {
	      this.#options.document.removeEventListener("pointermove", move), this.#options.document.removeEventListener("pointerup", finish), this.#options.document.removeEventListener("pointercancel", finish), this.#toggle.removeEventListener("lostpointercapture", finish), this.#dragging = !1, this.#rail.classList.remove("is-dragging"), this.#dragGeometry = null;
	    }), this.#options.document.addEventListener("pointermove", move), this.#options.document.addEventListener("pointerup", finish), this.#options.document.addEventListener("pointercancel", finish), this.#toggle.addEventListener("lostpointercapture", finish);
	  }
	  #setPanelOpen(open) {
	    this.#cancelPanelPreview(), this.#cancelPanelClose(), open || (this.#panelPinned = !1), this.#panelOpen = open, this.#rail.classList.toggle("is-dock-revealed", open), this.#panel.hidden = !open, this.#toggle.setAttribute("aria-expanded", String(open)), this.#syncToggleState(), open && this.#scheduleSurfaceMeasure();
	  }
	  #setPreviewExpanded(expanded) {
	    this.#rail.classList.toggle("is-preview-collapsed", !expanded), this.#syncToggleState(), expanded && this.#requestFrame(() => this.#syncScrollHint()), this.#scheduleSurfaceMeasure();
	  }
	  #cancelPanelPreview() {
	    this.#hoverOpenTimer && clearTimeout(this.#hoverOpenTimer), this.#hoverOpenTimer = 0;
	  }
	  #schedulePanelPreview(event) {
	    this.#cancelPanelPreview(), !(event.pointerType === "touch" || this.#dragging || this.#panelOpen || this.scope.destroyed) && (this.#hoverOpenTimer = setTimeout(() => {
	      this.#hoverOpenTimer = 0, !(this.#dragging || this.scope.destroyed) && this.#setPanelOpen(!0);
	    }, READER_QUEUE_PANEL_SHOW_DELAY_MS));
	  }
	  #cancelPanelClose() {
	    this.#hoverCloseTimer && clearTimeout(this.#hoverCloseTimer), this.#hoverCloseTimer = 0;
	  }
	  #schedulePanelClose() {
	    if (this.#cancelPanelClose(), this.#panelPinned) return;
	    const active = (0, import_event_target.deepActiveElement)(this.#options.document);
	    active && this.#panel.contains(active) || (this.#hoverCloseTimer = setTimeout(() => {
	      this.#hoverCloseTimer = 0, !this.#panelPinned && this.#setPanelOpen(!1);
	    }, READER_QUEUE_PANEL_HIDE_GRACE_MS));
	  }
	  #armClearConfirmation(count) {
	    this.#resetClearConfirmation(), this.#clearConfirmationPending = !0, this.#clear.classList.add("is-confirming");
	    const label = `确认移除 ${count} 篇未固定主题`;
	    this.#clear.setAttribute("aria-label", label), this.#clear.dataset.ldpTooltipLabel = label, this.#clearConfirmTimer = setTimeout(() => {
	      this.#clearConfirmTimer = 0, this.#resetClearConfirmation();
	    }, READER_QUEUE_CLEAR_CONFIRM_MS);
	  }
	  #resetClearConfirmation() {
	    this.#clearConfirmTimer && clearTimeout(this.#clearConfirmTimer), this.#clearConfirmTimer = 0, this.#clearConfirmationPending = !1, this.#clear.classList.remove("is-confirming"), this.#clear.setAttribute("aria-label", "移除未固定主题"), delete this.#clear.dataset.ldpTooltipLabel;
	  }
	  #syncToggleState() {
	    this.#toggle.setAttribute("aria-pressed", String(this.#panelPinned));
	    const action = this.#rail.classList.contains("is-preview-collapsed") ? "展开收纳箱" : this.#panelPinned ? "关闭收纳箱" : this.#panelOpen ? "固定打开收纳箱" : "打开收纳箱";
	    this.#toggle.setAttribute(
	      "aria-label",
	      this.#entries.size ? `${action};长按拖动可移动,贴边可隐藏;悬停可预览,共 ${this.#entries.size} 篇` : this.#downloadManager ? `${action},可下载或管理当前 Topic;队列 0 篇` : `${action};当前 0 篇`
	    );
	  }
	  #syncScrollHint() {
	    const maxScroll = Math.max(
	      0,
	      this.#bubbles.scrollHeight - this.#bubbles.clientHeight
	    ), hasOverflow = maxScroll >= 2;
	    if (this.#scrollHint.hidden = !hasOverflow, !hasOverflow) return;
	    const scrollUp = this.#bubbles.scrollTop >= maxScroll - 2;
	    this.#scrollHint.classList.toggle("is-up", scrollUp);
	    const iconName = scrollUp ? "chevron-up" : "chevron-down";
	    this.#scrollHint.querySelector(`.ldp-icon-${iconName}`) || this.#scrollHint.replaceChildren(icon(
	      this.#options.document,
	      iconName
	    )), this.#scrollHint.dataset.scrollDirection = scrollUp ? "-1" : "1", this.#scrollHint.setAttribute(
	      "aria-label",
	      scrollUp ? "回到队列上方头像" : "显示下方更多队列头像"
	    );
	  }
	  #observeSurfaceElement(element) {
	    !element || !this.#resizeObserver || this.#observedSurfaceElements.has(element) || (this.#observedSurfaceElements.add(element), this.#resizeObserver.observe(element));
	  }
	  #cancelSurfaceFrames() {
	    this.#surfaceFrame && this.#cancelFrame(this.#surfaceFrame), this.#dragFrame && this.#cancelFrame(this.#dragFrame), this.#surfaceFrame = 0, this.#dragFrame = 0;
	  }
	  #scheduleSurfaceMeasure() {
	    if (this.scope.destroyed || this.#dragging || this.#surfaceFrame) return;
	    let completed = !1;
	    const frame = this.#requestFrame(() => {
	      completed = !0, this.#surfaceFrame = 0, !(this.scope.destroyed || this.#dragging) && this.#projectSurface(this.#measureSurface());
	    });
	    completed || (this.#surfaceFrame = frame);
	  }
	  #scheduleDragProjection() {
	    if (this.#dragFrame || !this.#dragGeometry) return;
	    let completed = !1;
	    const frame = this.#requestFrame(() => {
	      completed = !0, this.#dragFrame = 0;
	      const geometry = this.#dragGeometry;
	      !geometry || this.scope.destroyed || this.#projectSurface(geometry);
	    });
	    completed || (this.#dragFrame = frame);
	  }
	  #measureSurface() {
	    const header = this.#options.root.querySelector(
	      ":scope > .ldp-header"
	    ), action = this.#options.root.querySelector(
	      ".ldp-topic-action-rail:not([hidden])"
	    );
	    return this.#observeSurfaceElement(header), this.#observeSurfaceElement(action), Object.freeze({
	      parent: this.#options.root.getBoundingClientRect(),
	      rail: this.#rail.getBoundingClientRect(),
	      toggle: this.#toggle.getBoundingClientRect(),
	      header: header?.getBoundingClientRect() ?? null,
	      action: action?.getBoundingClientRect() ?? null
	    });
	  }
	  #projectSurface(geometry) {
	    this.#rail.classList.toggle(
	      "is-docked-left",
	      this.#surface.dock === "left"
	    ), this.#rail.classList.toggle(
	      "is-docked-right",
	      this.#surface.dock === "right"
	    );
	    for (const dock of ["top", "bottom", "title"])
	      this.#rail.classList.toggle(
	        `is-docked-${dock}`,
	        this.#surface.dock === dock
	      );
	    this.#rail.classList.add("is-runtime-positioned");
	    const { parent, rail, toggle, header, action } = geometry, previewHeight = Math.min(
	      this.#bubbles.scrollHeight,
	      this.#bubbles.clientHeight > 0 ? this.#bubbles.clientHeight : this.#bubbles.scrollHeight
	    ), spaceBelow = parent.bottom - toggle.bottom;
	    this.#rail.classList.toggle(
	      "is-preview-reversed",
	      !this.#rail.classList.contains("is-preview-collapsed") && spaceBelow < previewHeight && toggle.top - parent.top > spaceBelow
	    );
	    const availableWidth = Math.max(1, parent.width - rail.width), availableHeight = Math.max(1, parent.height - rail.height);
	    let x = this.#surface.x, y = this.#surface.y;
	    if (parent.width > 0 && parent.height > 0) {
	      if (this.#surface.dock === "left" && (x = 0), this.#surface.dock === "right" && (x = 1), header && header.height > 0) {
	        const headerDockY = Math.min(
	          1,
	          (header.bottom - parent.top + parent.height * 8e-3) / availableHeight
	        );
	        this.#surface.dock === "top" || this.#surface.dock === "title" ? y = headerDockY : this.#surface.dock !== "bottom" && (y = Math.max(y, headerDockY));
	      }
	      if (this.#surface.dock === "bottom" && (y = 1), this.#surface.dock === "title" && action && action.width > 0)
	        x = Math.min(1, Math.max(
	          0,
	          (action.left + action.width / 2 - parent.left - rail.width / 2) / availableWidth
	        ));
	      else if (this.#surface.dock !== "left" && this.#surface.dock !== "right" && action && action.width > 0 && action.height > 0) {
	        const left = parent.left + x * availableWidth, top = parent.top + y * availableHeight, right = left + rail.width, bottom = top + rail.height;
	        if (left < action.right && right > action.left && top < action.bottom && bottom > action.top) {
	          const gap = parent.width * 8e-3;
	          x = action.left + action.width / 2 < parent.left + parent.width / 2 ? (action.right - parent.left + gap) / availableWidth : (action.left - parent.left - rail.width - gap) / availableWidth, x = Math.min(1, Math.max(0, x));
	        }
	      }
	    }
	    this.#rail.style.left = `${x * availableWidth / Math.max(1, parent.width) * 100}%`, this.#rail.style.top = `${y * availableHeight / Math.max(1, parent.height) * 100}%`, this.#projectPanel(
	      geometry,
	      parent.left + x * availableWidth,
	      parent.top + y * availableHeight
	    );
	  }
	  #projectPanel(geometry, railLeft, railTop) {
	    if (!this.#panelOpen || this.#panel.hidden) return;
	    const panelRect = this.#panel.getBoundingClientRect(), panelWidth = Math.max(
	      Number(this.#panel.offsetWidth) || 0,
	      panelRect.width
	    ), panelHeight = Math.max(
	      Number(this.#panel.offsetHeight) || 0,
	      panelRect.height
	    );
	    if (!(panelWidth > 0) || !(panelHeight > 0)) return;
	    const { parent, rail, header } = geometry, gap = 10, clamp = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, value)), minimumLeft = parent.left + gap, maximumLeft = Math.max(
	      minimumLeft,
	      parent.right - gap - panelWidth
	    ), minimumTop = parent.top + gap, maximumTop = Math.max(
	      minimumTop,
	      parent.bottom - gap - panelHeight
	    ), railRight = railLeft + rail.width, leftSpace = railLeft - minimumLeft - gap, rightSpace = parent.right - gap - railRight - gap, openLeft = this.#surface.dock === "right" || rightSpace < panelWidth && leftSpace > rightSpace, left = clamp(
	      openLeft ? railLeft - gap - panelWidth : railRight + gap,
	      minimumLeft,
	      maximumLeft
	    );
	    let top = clamp(railTop, minimumTop, maximumTop);
	    const blockers = [
	      header,
	      Object.freeze({
	        left: railLeft,
	        right: railRight,
	        top: railTop,
	        bottom: railTop + rail.height
	      })
	    ].filter((blocker) => blocker !== null);
	    for (const blocker of blockers) {
	      if (left >= blocker.right + gap || left + panelWidth <= blocker.left - gap || top >= blocker.bottom + gap || top + panelHeight <= blocker.top - gap) continue;
	      const below = clamp(blocker.bottom + gap, minimumTop, maximumTop);
	      top = below + panelHeight <= parent.bottom - gap ? below : clamp(blocker.top - gap - panelHeight, minimumTop, maximumTop);
	    }
	    this.#panel.classList.add("is-collision-positioned"), this.#panel.style.left = `${Math.round(left - railLeft)}px`, this.#panel.style.top = `${Math.round(top - railTop)}px`;
	  }
	  async #open(topicId, preferredPostNumber = null, source = "restore") {
	    const entry = this.#entries.get(topicId), history = this.#options.historyEntry(topicId), anchor = this.#options.historyAnchor(topicId), current = this.#options.currentTopicId(), previous = current ? this.#entries.get(current) : null, postNumber = preferredPostNumber ?? (source === "restore" && (anchor !== null || history !== null) ? null : anchor?.viewport.postNumber ?? history?.postNumber ?? entry?.postNumber) ?? null;
	    try {
	      const result = await this.#options.target.openTarget({
	        topicId,
	        ...postNumber ? { postNumber } : {},
	        source
	      });
	      if (result.topic.status === "failed")
	        throw result.topic.cause ?? new Error(`Reader 目标 Topic ${topicId} 打开失败`);
	      if (result.topic.status !== "opened" && result.topic.status !== "reused") return;
	      source === "restore" && anchor && await this.#options.restoreHistoryAnchor(topicId, anchor), previous && previous.topicId !== topicId && !previous.pinned && (this.#reloadStoredEntriesForMutation(), this.#entries.delete(previous.topicId), this.#persist()), this.sync();
	    } catch (error) {
	      this.#options.notify?.(`主题 #${topicId} 打开失败:${String(error)}`);
	    }
	  }
	  #queuePrefetch(topicId, retry = !1) {
	    const entry = this.#entries.get(topicId);
	    if (!entry || this.#prefetching.has(topicId)) return;
	    retry && (entry.error = ""), entry.loadState = "loading", entry.loadedCount = 0, entry.totalCount = 0, entry.nestedLoadedCount = 0, entry.nestedTotalCount = 0, entry.mediaLoadedCount = 0, entry.mediaTotalCount = 0, this.#prefetching.add(topicId);
	    const controller = new AbortController();
	    this.#prefetchControllers.set(topicId, controller), this.sync(), this.#prefetchTail = this.#prefetchTail.catch(() => {
	    }).then(() => this.#options.prefetch(
	      topicId,
	      entry.postNumber,
	      controller.signal,
	      (progress) => {
	        controller.signal.aborted || this.#entries.get(topicId) !== entry || (this.#applyPrefetchProgress(entry, progress), this.#scheduleSync());
	      }
	    )).then((result) => {
	      controller.signal.aborted || this.#entries.get(topicId) !== entry || (entry.loadedCount = Math.max(0, Math.floor(result.loadedCount)), entry.totalCount = Math.max(0, Math.floor(result.totalCount)), entry.nestedLoadedCount = Math.max(
	        0,
	        Math.floor(result.nestedLoadedCount ?? 0)
	      ), entry.nestedTotalCount = Math.max(
	        0,
	        Math.floor(result.nestedTotalCount ?? 0)
	      ), entry.mediaLoadedCount = Math.max(
	        0,
	        Math.floor(result.mediaLoadedCount ?? 0)
	      ), entry.mediaTotalCount = Math.max(
	        0,
	        Math.floor(result.mediaTotalCount ?? 0)
	      ), entry.loadState = result.complete ? "ready" : "partial", entry.error = "");
	    }).catch((error) => {
	      controller.signal.aborted || this.#entries.get(topicId) !== entry || (entry.loadState = "error", entry.error = String(error), this.#options.notify?.(
	        `主题 #${topicId} 预加载失败:${String(error)}`
	      ));
	    }).finally(() => {
	      this.#prefetchControllers.get(topicId) === controller && (this.#prefetchControllers.delete(topicId), this.#prefetching.delete(topicId)), this.scope.destroyed || this.sync();
	    });
	  }
	  #applyPrefetchProgress(entry, progress) {
	    for (const key of [
	      "loadedCount",
	      "totalCount",
	      "nestedLoadedCount",
	      "nestedTotalCount",
	      "mediaLoadedCount",
	      "mediaTotalCount"
	    ]) {
	      const value = progress[key];
	      value !== void 0 && (entry[key] = Math.max(0, Math.floor(value)));
	    }
	  }
	  #scheduleSync() {
	    if (this.scope.destroyed || this.#syncFrame) return;
	    let completed = !1;
	    const frame = this.#requestFrame(() => {
	      completed = !0, this.#syncFrame = 0, this.scope.destroyed || this.sync();
	    });
	    completed || (this.#syncFrame = frame);
	  }
	  #remove(topicId) {
	    this.#prefetchControllers.get(topicId)?.abort(
	      new DOMException(`主题 ${topicId} 已移出阅读队列`, "AbortError")
	    ), this.#prefetchControllers.delete(topicId), this.#prefetching.delete(topicId), this.#entries.delete(topicId);
	  }
	  #queueScan() {
	    this.#scanQueued || (this.#scanQueued = !0, queueMicrotask(() => {
	      this.#scanQueued = !1, this.scope.destroyed || this.#scan();
	    }));
	  }
	  #scan() {
	    const document = this.#options.document;
	    for (const row of document.querySelectorAll(TOPIC_ROW)) {
	      if (row.querySelector(".ldp-reader-queue-add")) continue;
	      const link = row.querySelector(TOPIC_LINK), route = link && (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(
	        link.href || link.getAttribute("href") || "",
	        document.baseURI
	      );
	      if (!link || !route || route.bypassReader) continue;
	      const add = button(
	        document,
	        "ldp-reader-queue-add",
	        "加入阅读队列并后台预加载",
	        "plus"
	      );
	      add.dataset.ldpTooltipLabel = "加入阅读队列并后台预加载", add.dataset.readerQueueTopicId = String(route.topicId), add.dataset.readerQueueHref = route.href, add.dataset.readerQueueTitle = String(link.textContent ?? "").replace(/\s+/g, " ").trim() || `帖子 #${route.topicId}`;
	      const avatar = row.querySelector("img.avatar");
	      add.dataset.readerQueueAvatarTemplate = avatar?.dataset.ldpAvatarOriginalTemplate || avatar?.dataset.ldpAvatarTemplate || avatar?.dataset.avatarTemplate || "", add.dataset.readerQueueAvatar = avatar?.currentSrc || avatar?.src || "", add.dataset.readerQueueOwner = row.querySelector("[data-user-card]")?.dataset.userCard ?? "", link.after(add);
	    }
	    const currentUser = document.querySelector(
	      ".d-header-icons .current-user:has(img.avatar)"
	    );
	    if (currentUser) {
	      let item = currentUser.querySelector(
	        ".ldp-native-reader-trigger-item"
	      ), trigger = item?.querySelector(
	        ".ldp-native-reader-trigger"
	      ) ?? null;
	      (!item || !trigger) && (item?.remove(), item = (0, import_html_element.htmlElement)(document, "span", "ldp-native-reader-trigger-item"), trigger = button(
	        document,
	        "ldp-native-reader-trigger",
	        "打开阅读器",
	        "maximize-2"
	      ), item.append(trigger), currentUser.append(item)), this.#nativeTriggerItem = item, this.#nativeTriggerButton = trigger;
	    }
	    this.sync();
	  }
	  #syncNativeReaderTrigger() {
	    const document = this.#options.document, item = this.#nativeTriggerItem, button2 = this.#nativeTriggerButton, avatarHost = document.querySelector(
	      ".d-header-icons .current-user:has(img.avatar)"
	    );
	    if (!item || !button2 || !avatarHost || this.#options.readerOpen()) {
	      item && (item.hidden = !0), document.documentElement.classList.remove(
	        "ldp-native-reader-trigger-visible"
	      );
	      return;
	    }
	    item.parentElement !== avatarHost && avatarHost.append(item);
	    const route = (0, import_reader_userscript_target_adapter.parseReaderUserscriptTopicRoute)(
	      document.location?.href ?? document.baseURI,
	      document.baseURI
	    ), routeTarget = route && !route.bypassReader ? Object.freeze({
	      topicId: route.topicId,
	      postNumber: this.#options.readPreferences().openTopicsAtFirstPost ? (0, import_identifiers.tryDiscoursePostNumber)(1) : route.postNumber,
	      source: "route",
	      title: ""
	    }) : null, history = routeTarget ? null : this.#options.historyEntry(), queue = routeTarget || history ? null : this.#entries.values().next().value, target = routeTarget ?? (history ? Object.freeze({
	      topicId: history.topicId,
	      postNumber: null,
	      source: "history",
	      title: history.title
	    }) : queue ? Object.freeze({
	      topicId: queue.topicId,
	      postNumber: queue.postNumber,
	      source: "queue",
	      title: queue.title
	    }) : null);
	    item.hidden = !1, button2.hidden = !1, button2.dataset.topicId = target ? String(target.topicId) : "", button2.dataset.postNumber = target?.postNumber ? String(target.postNumber) : "", button2.dataset.triggerSource = target?.source ?? "empty-history";
	    const label = target ? target.source === "history" ? target.title ? `打开历史首项:${target.title}` : "打开历史首项" : target.source === "queue" ? target.title ? `打开队列首项:${target.title}` : "打开队列首项" : target.postNumber ? `从 #${target.postNumber} 打开浮窗阅读器` : "打开浮窗阅读器" : "暂无浏览历史";
	    button2.setAttribute("aria-label", label), button2.dataset.ldpTooltipLabel = label, document.documentElement.classList.add(
	      "ldp-native-reader-trigger-visible"
	    );
	  }
	  #restore() {
	    try {
	      const stored = this.#accountStorage ? (0, import_reader_account_scoped_storage.readReaderAccountScopedString)(
	        this.#options.storage,
	        this.#accountStorage
	      ) : this.#options.storage.getItem(this.#storageKey), value = JSON.parse(
	        stored ?? "null"
	      ), source = Array.isArray(value) ? value : value && typeof value == "object" ? value.entries : [], entries = Array.isArray(source) ? source.map((entry) => normalizedEntry(entry, this.#options.document.baseURI)).filter((entry) => entry !== null) : [], unique = new Map(entries.map((entry) => [entry.topicId, entry])), record = value && !Array.isArray(value) && typeof value == "object" ? value : {}, surfaces = normalizedSurfaces(
	        record.surfaces,
	        record.surface
	      );
	      return { entries: [...unique.values()], surfaces };
	    } catch {
	      return { entries: [], surfaces: normalizedSurfaces(null, null) };
	    }
	  }
	  #reloadStoredEntriesForMutation() {
	    if (this.scope.destroyed) return;
	    const restored = this.#restore();
	    this.#entries.clear();
	    for (const entry of restored.entries) this.#entries.set(entry.topicId, entry);
	  }
	  #persist() {
	    try {
	      const entries = [...this.#entries.values()];
	      if (!entries.length && Object.values(this.#surfaces).every(defaultSurface) && this.#options.storage.removeItem && !this.#accountStorage) {
	        this.#options.storage.removeItem(this.#storageKey);
	        return;
	      }
	      this.#options.storage.setItem(
	        this.#storageKey,
	        JSON.stringify({
	          version: 2,
	          entries,
	          surfaces: this.#surfaces
	        })
	      );
	    } catch (error) {
	      this.#options.notify?.(
	        `阅读队列保存失败:${String(error)}`
	      );
	    }
	  }
	}
}, "afd82564a21c347c6f3c72f83d364486a2b444017c4209c0ca6ce08336863f1c");

/* Source: lite/src/queue/reader-topic-download-manager.ts */
runtime.register("src/queue/reader-topic-download-manager.js", function(module, exports, require) {
	var reader_topic_download_manager_exports = {};
	__export(reader_topic_download_manager_exports, {
	  READER_TOPIC_DOWNLOAD_WINDOW_GEOMETRY_STORAGE_KEY: () => READER_TOPIC_DOWNLOAD_WINDOW_GEOMETRY_STORAGE_KEY,
	  ReaderTopicDownloadManager: () => ReaderTopicDownloadManager,
	  parseReaderTopicDownloadPostSelection: () => parseReaderTopicDownloadPostSelection,
	  readerTopicDownloadCoverage: () => readerTopicDownloadCoverage,
	  readerTopicDownloadLocalArchivePlan: () => readerTopicDownloadLocalArchivePlan,
	  selectReaderTopicDownloadPosts: () => selectReaderTopicDownloadPosts
	});
	module.exports = __toCommonJS(reader_topic_download_manager_exports);
	var import_reader_icon = require("../components/reader-icon.js"), import_reader_collection_floating_window = require("../collection/reader-collection-floating-window.js"), import_reader_topic_offline_document = require("../archive/reader-topic-offline-document.js"), import_event_target = require("../dom/event-target.js"), import_html_element = require("../dom/html-element.js"), import_identifiers = require("../discourse/identifiers.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_signal = require("../kernel/signal.js"), import_reader_floating_window_frame = require("../shell/reader-floating-window-frame.js"), import_reader_escape_surface = require("../shell/reader-escape-surface.js"), import_reader_select_surface = require("../shell/reader-select-surface.js");
	const MAX_CUSTOM_POST_NUMBERS = 1e5, DOWNLOAD_HISTORY_PAGE_SIZE = 8, DOWNLOAD_REQUEST_AUTO_RESUME_LIMIT = 8, DOWNLOAD_CHALLENGE_AUTO_RESUME_LIMIT = 1, READER_TOPIC_DOWNLOAD_WINDOW_GEOMETRY_STORAGE_KEY = import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY, ALL_POSTS_SELECTION = Object.freeze({
	  mode: "all",
	  expression: "",
	  postNumbers: Object.freeze([])
	});
	function parseReaderTopicDownloadPostSelection(rawValue) {
	  const value = String(rawValue);
	  if (!value) throw new Error("请输入楼层,例如 1,3,8-12");
	  if (/[^0-9,-]/.test(value))
	    throw new Error("仅支持数字、英文逗号 , 和连字符 -");
	  const selected = /* @__PURE__ */ new Set();
	  for (const token of value.split(",")) {
	    if (!token) throw new Error("楼层列表中存在空项");
	    const single = /^(\d+)$/.exec(token), range = /^(\d+)-(\d+)$/.exec(token);
	    if (!single && !range) throw new Error(`无法识别楼层“${token}”`);
	    const start = Number(single?.[1] ?? range?.[1]), end = Number(single?.[1] ?? range?.[2]);
	    if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) throw new Error(`楼层范围“${token}”无效`);
	    for (let postNumber = start; postNumber <= end; postNumber += 1) {
	      if (!selected.has(postNumber) && selected.size >= MAX_CUSTOM_POST_NUMBERS)
	        throw new Error(`自定义楼层最多选择 ${MAX_CUSTOM_POST_NUMBERS} 层`);
	      selected.add(postNumber);
	    }
	  }
	  return Object.freeze([...selected].sort((left, right) => left - right));
	}
	function normalizedSelection(selection = ALL_POSTS_SELECTION) {
	  if (selection.mode === "all") return ALL_POSTS_SELECTION;
	  if (selection.mode === "op")
	    return Object.freeze({
	      mode: "op",
	      expression: "",
	      postNumbers: Object.freeze([])
	    });
	  const expression = String(selection.expression);
	  return Object.freeze({
	    mode: "custom",
	    expression,
	    postNumbers: parseReaderTopicDownloadPostSelection(expression)
	  });
	}
	function sameSelection(left, right) {
	  return left.mode !== right.mode ? !1 : left.mode !== "custom" ? !0 : left.postNumbers.length === right.postNumbers.length && left.postNumbers.every((postNumber, index) => postNumber === right.postNumbers[index]);
	}
	function selectionLabel(selection) {
	  return selection.mode === "all" ? "全部楼层" : selection.mode === "op" ? "只看楼主" : `自定义 ${selection.expression}`;
	}
	function readerTopicDownloadLocalArchivePlan(input) {
	  const topicStatus = Number(input.topicStatus), completed = Math.max(0, Math.floor(Number(input.cachedPostCount) || 0));
	  if (![403, 404, 410].includes(topicStatus) || completed < 1) return null;
	  const total = Math.max(
	    completed,
	    Math.max(0, Math.floor(Number(input.expectedPostCount) || 0)),
	    Math.max(0, Math.floor(Number(input.streamPostCount) || 0))
	  ), missingCanonicalPostCount = Math.max(
	    Math.max(0, Math.floor(Number(input.missingStreamPostCount) || 0)),
	    total - completed
	  );
	  return Object.freeze({
	    completed,
	    total,
	    missingCanonicalPostCount,
	    streamComplete: input.streamComplete === !0 && missingCanonicalPostCount === 0
	  });
	}
	function readerTopicDownloadCoverage(input) {
	  const missingCanonicalPostCount = Math.max(
	    0,
	    Math.floor(Number(input.missingCanonicalPostCount) || 0)
	  );
	  if (!input.streamComplete && !input.archived)
	    throw new Error(input.selectionMode === "all" ? `全帖正文尚未补齐:缺少 ${missingCanonicalPostCount} 个 canonical 楼层` : `所选楼层的正文上下文尚未补齐:缺少 ${missingCanonicalPostCount} 个 canonical 楼层`);
	  const warnings = [];
	  return input.streamComplete || warnings.push(missingCanonicalPostCount > 0 ? `仅保留当前可用正文,缺少 ${missingCanonicalPostCount} 个楼层` : "仅保留当前可用正文,完整性未能确认"), input.repliesComplete || warnings.push("正文已补齐,部分回复关系无法确认"), Object.freeze({
	    complete: input.streamComplete && input.repliesComplete,
	    warning: warnings.join(";")
	  });
	}
	function selectReaderTopicDownloadPosts(availablePosts, selection, ownerUsername = "") {
	  const selected = normalizedSelection(selection);
	  if (selected.mode === "all")
	    return Object.freeze({
	      posts: Object.freeze([...availablePosts]),
	      mainPostNumbers: null,
	      expectedPostCount: availablePosts.length,
	      filenameScope: ""
	    });
	  if (selected.mode === "op") {
	    const starter = availablePosts.find((post) => Number(post.post_number) === 1), opUsername = String(ownerUsername || starter?.username || "").trim();
	    if (!opUsername) throw new Error("无法识别 Topic 楼主 OP");
	    const anchors2 = Object.freeze(availablePosts.filter((post) => String(post.username ?? "") === opUsername));
	    if (!anchors2.length) throw new Error("所选下载范围没有可用正文");
	    return Object.freeze({
	      posts: Object.freeze([...availablePosts]),
	      mainPostNumbers: Object.freeze(anchors2.map((post) => Number(post.post_number))),
	      expectedPostCount: anchors2.length,
	      filenameScope: "op"
	    });
	  }
	  const selectedNumbers = new Set(selected.postNumbers), anchors = Object.freeze(availablePosts.filter((post) => selectedNumbers.has(Number(post.post_number)))), foundNumbers = new Set(anchors.map((post) => Number(post.post_number))), missing = selected.postNumbers.filter((postNumber) => !foundNumbers.has(postNumber));
	  if (missing.length)
	    throw new Error(`未找到自定义楼层:${missing.slice(0, 12).join(",")}`);
	  if (!anchors.length) throw new Error("所选下载范围没有可用正文");
	  return Object.freeze({
	    posts: Object.freeze([...availablePosts]),
	    mainPostNumbers: Object.freeze(anchors.map((post) => Number(post.post_number))),
	    expectedPostCount: selected.postNumbers.length,
	    filenameScope: `floors-${selected.expression}`.replace(/[,,\s]+/g, "_").replace(/[^0-9_-]/g, "").slice(0, 64)
	  });
	}
	function restoredSelection(entry) {
	  try {
	    return normalizedSelection({
	      mode: entry.selectionMode ?? "all",
	      expression: entry.selectionExpression ?? "",
	      postNumbers: Object.freeze([])
	    });
	  } catch {
	    return ALL_POSTS_SELECTION;
	  }
	}
	function button(document, className, label, iconName) {
	  const result = (0, import_html_element.htmlElement)(document, "button", className);
	  return result.type = "button", result.setAttribute("aria-label", label), result.append((0, import_reader_icon.createReaderIcon)(document, iconName)), result;
	}
	function phaseLabel(task) {
	  const scope = selectionLabel(task.selection), withProgress = (label) => {
	    if (task.total < 1) return label;
	    const percentage = Math.min(
	      100,
	      Math.max(0, Math.round(task.completed / task.total * 100))
	    );
	    return `${label} · ${percentage}%`;
	  };
	  return task.phase === "queued" ? `等待后台下载 · ${scope}` : task.phase === "loading-topic" ? task.detail || "正在读取 Topic" : task.phase === "loading-posts" ? withProgress(
	    task.detail || `正在检查缓存并补齐正文 ${task.completed}/${task.total || "?"}`
	  ) : task.phase === "loading-replies" ? withProgress(task.detail || "正在检查回复关系") : task.phase === "waiting-rate-limit" ? withProgress(
	    task.detail || "遇到 HTTP 429,已保存断点并等待自动续传"
	  ) : task.phase === "waiting-challenge" ? withProgress(
	    task.detail || "等待 Cloudflare 验证通过后自动续传"
	  ) : task.phase === "serializing" ? task.detail || "正在生成离线 HTML" : task.phase === "ready" ? task.detail ? task.detail : task.complete ? `已完成 · ${task.completed}/${task.total} 楼 · ${scope}` : `已导出可用存档 · ${task.completed}/${task.total || "?"} 楼 · ${scope}` : task.phase === "cancelled" ? "已取消" : task.error ? `失败 · ${task.error}` : "下载失败";
	}
	function normalizedArchiveStatus(value) {
	  const status = Number(value);
	  return status === 403 || status === 404 || status === 410 ? status : null;
	}
	class ReaderTopicDownloadManager {
	  scope;
	  changes = new import_signal.Signal();
	  windowGeometry;
	  windowPointer;
	  #options;
	  #now;
	  #floatingWindow;
	  #details;
	  #summaryCount;
	  #downloadCurrent;
	  #downloadCurrentLabel;
	  #downloadPreview;
	  #downloadPreviewTitle;
	  #downloadPreviewMeta;
	  #selectionMode;
	  #selectSurface;
	  #customSelection;
	  #selectionError;
	  #historySearch;
	  #historyCount;
	  #historyBatchToggle;
	  #historyBatchBar;
	  #historySelectPage;
	  #historySelectionCount;
	  #historyRemoveSelected;
	  #list;
	  #historyPagination;
	  #historyPagePrevious;
	  #historyPageLabel;
	  #historyPageNext;
	  #tasks = /* @__PURE__ */ new Map();
	  #requestFrame;
	  #cancelFrame;
	  #tail = Promise.resolve();
	  #renderFrame = 0;
	  #renderKey = "";
	  #managerVisible = !1;
	  #managerOpen = !1;
	  #historyPage = 0;
	  #historyBatchMode = !1;
	  #visibleHistoryTopicIds = Object.freeze([]);
	  #selectedHistoryTopics = /* @__PURE__ */ new Set();
	  #removing = /* @__PURE__ */ new Set();
	  #externalRestore = null;
	  #viewObjectUrls = /* @__PURE__ */ new Set();
	  constructor(options) {
	    this.#options = options, this.#now = options.now ?? Date.now, this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope);
	    const view = options.document.defaultView;
	    this.#requestFrame = options.requestFrame ?? ((callback) => typeof view?.requestAnimationFrame == "function" ? view.requestAnimationFrame(callback) : (callback(0), 0)), this.#cancelFrame = options.cancelFrame ?? ((id) => view?.cancelAnimationFrame?.(id)), this.#floatingWindow = options.floating ? new import_reader_floating_window_frame.ReaderFloatingWindowFrame({
	      document: options.document,
	      mount: options.mount,
	      title: "主题下载",
	      ariaLabel: "Topic 下载管理",
	      icon: "download",
	      variant: "topic-downloads",
	      tabId: "topic-downloads",
	      tabOrder: 40,
	      requestOpen: () => {
	        this.openManager();
	      },
	      zIndex: 2147483584,
	      ...options.geometryStorage ? { geometryStorage: options.geometryStorage } : {},
	      geometryStorageKey: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_GEOMETRY_KEY,
	      policy: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_POLICY,
	      placement: import_reader_collection_floating_window.READER_COLLECTION_FLOATING_WINDOW_PLACEMENT,
	      ...options.notify ? { notify: options.notify } : {},
	      onClose: () => this.#closeFromFrame(),
	      parentScope: this.scope
	    }) : null, this.#details = (0, import_html_element.htmlElement)(
	      options.document,
	      "section",
	      "ldp-topic-download-manager"
	    ), this.#managerVisible = options.floating !== !0, options.floating && (this.#details.hidden = !0);
	    const summary = (0, import_html_element.htmlElement)(
	      options.document,
	      "header",
	      "ldp-topic-download-summary"
	    );
	    summary.append((0, import_reader_icon.createReaderIcon)(options.document, "download"));
	    const summaryLabel = (0, import_html_element.htmlElement)(options.document, "span");
	    summaryLabel.textContent = "Topic 下载", this.#floatingWindow ? this.#summaryCount = this.#floatingWindow.meta : (this.#summaryCount = (0, import_html_element.htmlElement)(options.document, "b"), summary.append(summaryLabel, this.#summaryCount));
	    const toolbar = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-toolbar"
	    ), selection = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-selection"
	    ), selectionLabelNode = (0, import_html_element.htmlElement)(options.document, "label"), selectionLabelText = (0, import_html_element.htmlElement)(options.document, "span");
	    selectionLabelText.textContent = "下载范围", this.#selectionMode = options.document.createElement("select"), this.#selectionMode.className = "ldp-reader-select ldp-topic-download-selection-mode", this.#selectionMode.setAttribute("aria-label", "选择 Topic 下载范围");
	    for (const [value, label] of [
	      ["all", "全部楼层(默认)"],
	      ["custom", "自定义楼层"]
	    ]) {
	      const option = options.document.createElement("option");
	      option.value = value, option.textContent = label, option.selected = value === "all", this.#selectionMode.append(option);
	    }
	    selectionLabelNode.append(selectionLabelText, this.#selectionMode);
	    const customRow = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-custom-selection"
	    );
	    this.#customSelection = options.document.createElement("input"), this.#customSelection.type = "text", this.#customSelection.inputMode = "text", this.#customSelection.placeholder = "输入楼层,例如 1,3,8-12", this.#customSelection.setAttribute("aria-label", "输入自定义下载楼层"), this.#customSelection.pattern = "[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*", this.#customSelection.autocomplete = "off", this.#customSelection.spellcheck = !1, this.#customSelection.enterKeyHint = "done", customRow.append(this.#customSelection), this.#selectionError = (0, import_html_element.htmlElement)(
	      options.document,
	      "small",
	      "ldp-topic-download-selection-error"
	    ), this.#selectionError.hidden = !0, this.#selectionError.setAttribute("aria-live", "polite"), selection.append(selectionLabelNode, customRow, this.#selectionError), this.#downloadCurrent = button(
	      options.document,
	      "ldp-topic-download-current",
	      "开始后台下载当前 Topic",
	      "download"
	    ), this.#downloadCurrentLabel = (0, import_html_element.htmlElement)(options.document, "span"), this.#downloadCurrentLabel.textContent = "开始后台下载", this.#downloadCurrent.append(this.#downloadCurrentLabel), selection.insertBefore(this.#downloadCurrent, customRow), this.#downloadPreview = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-preview"
	    ), this.#downloadPreview.hidden = !0, this.#downloadPreview.setAttribute("role", "note"), this.#downloadPreview.setAttribute("aria-live", "polite"), this.#downloadPreview.setAttribute("aria-atomic", "true"), this.#downloadPreview.append((0, import_reader_icon.createReaderIcon)(options.document, "download"));
	    const downloadPreviewCopy = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-topic-download-preview-copy"
	    ), downloadPreviewKicker = (0, import_html_element.htmlElement)(
	      options.document,
	      "small",
	      "ldp-topic-download-preview-kicker"
	    );
	    downloadPreviewKicker.textContent = "即将下载", this.#downloadPreviewTitle = (0, import_html_element.htmlElement)(options.document, "strong"), this.#downloadPreviewMeta = (0, import_html_element.htmlElement)(options.document, "small"), downloadPreviewCopy.append(
	      downloadPreviewKicker,
	      this.#downloadPreviewTitle,
	      this.#downloadPreviewMeta
	    ), this.#downloadPreview.append(downloadPreviewCopy), selection.append(this.#downloadPreview), toolbar.append(selection);
	    const history = (0, import_html_element.htmlElement)(
	      options.document,
	      "section",
	      "ldp-topic-download-history"
	    ), historyHead = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-history-head"
	    ), historySearchLabel = (0, import_html_element.htmlElement)(
	      options.document,
	      "label",
	      "ldp-topic-download-search"
	    );
	    historySearchLabel.append((0, import_reader_icon.createReaderIcon)(options.document, "search")), this.#historySearch = options.document.createElement("input"), this.#historySearch.type = "search", this.#historySearch.placeholder = "搜索标题、Topic ID 或文件名", this.#historySearch.setAttribute("aria-label", "搜索 Topic 下载历史"), historySearchLabel.append(this.#historySearch), this.#historyCount = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-topic-download-history-count"
	    ), this.#historyBatchToggle = button(
	      options.document,
	      "ldp-topic-download-batch-toggle",
	      "进入批量管理",
	      "list-checks"
	    );
	    const batchToggleLabel = (0, import_html_element.htmlElement)(options.document, "span");
	    batchToggleLabel.textContent = "批量管理", this.#historyBatchToggle.append(batchToggleLabel);
	    const historyMeta = (0, import_html_element.htmlElement)(
	      options.document,
	      "span",
	      "ldp-topic-download-history-meta"
	    );
	    historyMeta.append(this.#historyCount, this.#historyBatchToggle), historyHead.append(historySearchLabel, historyMeta), this.#historyBatchBar = (0, import_html_element.htmlElement)(
	      options.document,
	      "div",
	      "ldp-topic-download-batch-bar"
	    ), this.#historyBatchBar.hidden = !0, this.#historySelectPage = button(
	      options.document,
	      "ldp-topic-download-select-page",
	      "选择当前页",
	      "check-square"
	    );
	    const selectPageLabel = (0, import_html_element.htmlElement)(options.document, "span");
	    selectPageLabel.textContent = "全选本页", this.#historySelectPage.append(selectPageLabel), this.#historySelectionCount = (0, import_html_element.htmlElement)(options.document, "span"), this.#historyRemoveSelected = button(
	      options.document,
	      "ldp-topic-download-remove-selected",
	      "移除已选下载记录",
	      "trash"
	    );
	    const removeSelectedLabel = (0, import_html_element.htmlElement)(options.document, "span");
	    removeSelectedLabel.textContent = "移除已选", this.#historyRemoveSelected.append(removeSelectedLabel), this.#historyBatchBar.append(
	      this.#historySelectPage,
	      this.#historySelectionCount,
	      this.#historyRemoveSelected
	    ), this.#list = (0, import_html_element.htmlElement)(options.document, "div", "ldp-topic-download-list"), this.#list.setAttribute("aria-live", "polite"), this.#historyPagination = (0, import_html_element.htmlElement)(
	      options.document,
	      "nav",
	      "ldp-topic-download-pagination"
	    ), this.#historyPagination.setAttribute("aria-label", "Topic 下载历史分页"), this.#historyPagePrevious = button(
	      options.document,
	      "ldp-topic-download-page-previous",
	      "上一页",
	      "chevron-left"
	    ), this.#historyPageLabel = (0, import_html_element.htmlElement)(options.document, "span"), this.#historyPageNext = button(
	      options.document,
	      "ldp-topic-download-page-next",
	      "下一页",
	      "chevron-right"
	    ), this.#historyPagination.append(
	      this.#historyPagePrevious,
	      this.#historyPageLabel,
	      this.#historyPageNext
	    ), history.append(
	      historyHead,
	      this.#historyBatchBar,
	      this.#list,
	      this.#historyPagination
	    ), this.#floatingWindow ? (this.#details.append(toolbar, history), this.#floatingWindow.body.append(this.#details)) : (this.#details.append(summary, toolbar, history), options.mount.append(this.#details)), this.#selectSurface = new import_reader_select_surface.ReaderSelectSurface({
	      document: options.document,
	      root: this.#details,
	      parentScope: this.scope
	    }), this.windowGeometry = this.#floatingWindow?.geometry ?? null, this.windowPointer = this.#floatingWindow?.pointer ?? null, this.scope.add(() => {
	      this.#selectSurface.destroy(), this.#renderFrame && this.#cancelFrame(this.#renderFrame);
	      for (const task of this.#tasks.values())
	        task.controller?.abort(
	          new DOMException("Topic 下载管理已关闭", "AbortError")
	        );
	      for (const objectUrl of this.#viewObjectUrls)
	        objectUrl.urlApi.revokeObjectURL(objectUrl.value);
	      this.#viewObjectUrls.clear(), this.#details.remove(), this.changes.clear();
	    }), this.scope.listen(this.#details, "click", (event) => this.#click(event)), options.floating && (this.scope.listen(options.document, "pointerdown", (event) => {
	      if (!(!this.#managerVisible || this.#removing.size > 0)) {
	        if (this.#floatingWindow) {
	          this.#floatingWindow.dismissFromPointerEvent(event);
	          return;
	        }
	        (0, import_event_target.eventPathIncludes)(event, this.#details) || this.closeManager();
	      }
	    }, !0), this.scope.listen(options.document, "keydown", (event) => {
	      const keyboard = event;
	      if (!(!this.#managerVisible || this.#removing.size > 0)) {
	        if (this.#floatingWindow) {
	          this.#floatingWindow.dismissFromEscapeEvent(keyboard);
	          return;
	        }
	        keyboard.key !== "Escape" || !(0, import_reader_escape_surface.readerEscapeOwnedBy)(options.document, [this.#details]) || (event.preventDefault(), event.stopImmediatePropagation(), this.closeManager());
	      }
	    }, !0)), this.scope.listen(this.#selectionMode, "change", () => {
	      this.#syncSelectionControls(), this.syncCurrent();
	    }), this.scope.listen(this.#customSelection, "input", () => {
	      this.#validateCustomSelection(!1), this.syncCurrent();
	    }), this.scope.listen(this.#historySearch, "input", () => {
	      this.#historyPage = 0, this.#scheduleRender();
	    }), this.scope.listen(this.#historyBatchToggle, "click", () => {
	      this.#historyBatchMode = !this.#historyBatchMode, this.#historyBatchMode || this.#selectedHistoryTopics.clear(), this.#scheduleRender();
	    }), this.scope.listen(this.#historySelectPage, "click", () => {
	      const allSelected = this.#visibleHistoryTopicIds.length > 0 && this.#visibleHistoryTopicIds.every((topicId) => this.#selectedHistoryTopics.has(topicId));
	      for (const topicId of this.#visibleHistoryTopicIds)
	        allSelected ? this.#selectedHistoryTopics.delete(topicId) : this.#selectedHistoryTopics.add(topicId);
	      this.#scheduleRender();
	    }), this.scope.listen(this.#historyRemoveSelected, "click", () => {
	      this.#confirmAndRemoveSelected();
	    }), this.scope.listen(this.#list, "change", (event) => {
	      const input = (0, import_event_target.eventElement)(event)?.closest(
	        "[data-topic-download-select]"
	      );
	      if (!input) return;
	      const topicId = (0, import_identifiers.discourseTopicId)(Number(input.dataset.topicId));
	      input.checked ? this.#selectedHistoryTopics.add(topicId) : this.#selectedHistoryTopics.delete(topicId), this.#scheduleRender();
	    }), this.scope.listen(this.#historyPagePrevious, "click", () => {
	      this.#historyPage <= 0 || (this.#historyPage -= 1, this.#scheduleRender());
	    }), this.scope.listen(this.#historyPageNext, "click", () => {
	      this.#historyPage += 1, this.#scheduleRender();
	    }), this.#syncSelectionControls(), this.syncCurrent(), this.#render(), this.#restoreArtifacts();
	  }
	  get element() {
	    return this.#floatingWindow?.element ?? this.#details;
	  }
	  snapshot() {
	    return Object.freeze({
	      open: this.#managerOpen,
	      tasks: Object.freeze([...this.#tasks.values()].sort((left, right) => right.createdAt - left.createdAt).map((task) => this.#taskSnapshot(task)))
	    });
	  }
	  syncCurrent() {
	    const current = this.#options.currentTopic(), task = current ? this.#tasks.get(current.topicId) : null, selection = this.#selectionForDuplicateCheck(), active = !!(task && !["ready", "error", "cancelled"].includes(task.phase)), duplicateReady = !!(task?.phase === "ready" && selection && sameSelection(task.selection, selection));
	    this.#downloadCurrent.disabled = current === null || active, this.#downloadCurrent.classList.toggle("is-active", active), this.#downloadCurrentLabel.textContent = duplicateReady ? "重新生成离线 HTML" : active ? task?.phase === "waiting-rate-limit" ? "等待断点续传" : task?.phase === "waiting-challenge" ? "等待过盾续传" : task?.phase === "queued" ? "已加入下载队列" : "正在后台下载" : "开始后台下载", this.#downloadCurrent.setAttribute(
	      "aria-label",
	      duplicateReady && current ? `重新生成 ${current.title} 的${selectionLabel(selection)}离线 HTML` : active && current ? `${current.title} 正在后台下载` : "开始后台下载当前 Topic"
	    ), this.#downloadCurrent.dataset.topicId = current ? String(current.topicId) : "", this.#downloadCurrent.dataset.topicTitle = current?.title ?? "", this.#downloadPreview.hidden = current === null || active || duplicateReady, this.#downloadPreviewTitle.textContent = current?.title ?? "", this.#downloadPreviewMeta.textContent = current ? `Topic #${current.topicId} · ${this.#selectionPreviewLabel()}` : "";
	  }
	  #selectionForDuplicateCheck() {
	    const mode = this.#selectionMode.value;
	    try {
	      return normalizedSelection({
	        mode,
	        expression: mode === "custom" ? this.#customSelection.value : "",
	        postNumbers: Object.freeze([])
	      });
	    } catch {
	      return null;
	    }
	  }
	  #selectionPreviewLabel() {
	    const mode = this.#selectionMode.value;
	    if (mode === "all") return "全部楼层";
	    if (mode === "op") return "只看楼主";
	    const expression = this.#customSelection.value.trim();
	    return expression ? `自定义 ${expression}` : "自定义楼层(等待输入)";
	  }
	  #syncSelectionControls() {
	    const custom = this.#selectionMode.value === "custom";
	    this.#customSelection.parentElement.hidden = !custom, custom ? this.#validateCustomSelection(!1) : this.#clearSelectionError();
	  }
	  #clearSelectionError() {
	    this.#selectionError.hidden = !0, this.#selectionError.textContent = "", this.#customSelection.removeAttribute("aria-invalid");
	  }
	  #validateCustomSelection(requireValue) {
	    if (this.#clearSelectionError(), this.#selectionMode.value !== "custom" || !requireValue && !this.#customSelection.value) return !0;
	    try {
	      return parseReaderTopicDownloadPostSelection(this.#customSelection.value), !0;
	    } catch (error) {
	      const message = error instanceof Error ? error.message : String(error);
	      return this.#selectionError.textContent = message, this.#selectionError.hidden = !1, this.#customSelection.setAttribute("aria-invalid", "true"), !1;
	    }
	  }
	  #readSelection() {
	    const mode = this.#selectionMode.value;
	    if (mode === "custom" && !this.#validateCustomSelection(!0))
	      return this.#customSelection.focus(), null;
	    try {
	      return normalizedSelection({
	        mode,
	        expression: mode === "custom" ? this.#customSelection.value : "",
	        postNumbers: Object.freeze([])
	      });
	    } catch (error) {
	      const message = error instanceof Error ? error.message : String(error);
	      return this.#selectionError.textContent = message, this.#selectionError.hidden = !1, this.#customSelection.setAttribute("aria-invalid", "true"), this.#customSelection.focus(), null;
	    }
	  }
	  openManager() {
	    return this.scope.destroyed ? !1 : (this.#managerVisible = !0, this.#managerOpen = !0, this.#details.hidden = !1, this.#details.classList.add("is-open"), this.#floatingWindow?.open(), this.#emit(), !0);
	  }
	  closeManager() {
	    return this.scope.destroyed || !this.#options.floating ? !1 : this.#floatingWindow?.isOpen ? (this.#floatingWindow.close(), !0) : (this.#closeFromFrame(), !0);
	  }
	  #closeFromFrame() {
	    !this.#managerVisible && !this.#managerOpen || (this.#managerVisible = !1, this.#managerOpen = !1, this.#details.classList.remove("is-open"), this.#details.hidden = !0, this.#emit());
	  }
	  prepareCurrentDownload() {
	    return this.syncCurrent(), this.openManager() ? (this.#selectionMode.focus(), !0) : !1;
	  }
	  reloadExternal() {
	    if (this.scope.destroyed) return Promise.resolve();
	    if (this.#externalRestore) return this.#externalRestore;
	    const restore = this.#restoreArtifacts(!0).finally(() => {
	      this.#externalRestore === restore && (this.#externalRestore = null);
	    });
	    return this.#externalRestore = restore, restore;
	  }
	  enqueueCurrent(regenerateDuplicateReady = !1) {
	    const current = this.#options.currentTopic();
	    if (!current) return null;
	    const selection = this.#readSelection();
	    return selection ? this.enqueue(
	      current.topicId,
	      current.title,
	      selection,
	      regenerateDuplicateReady
	    ) : null;
	  }
	  enqueue(rawTopicId, rawTitle, selection = ALL_POSTS_SELECTION, regenerateDuplicateReady = !1) {
	    const topicId = (0, import_identifiers.discourseTopicId)(rawTopicId), title = String(rawTitle || `Topic #${topicId}`).replace(/\s+/g, " ").trim(), selected = normalizedSelection(selection);
	    this.openManager();
	    const existing = this.#tasks.get(topicId);
	    if (existing && (!["ready", "error", "cancelled"].includes(existing.phase) || !regenerateDuplicateReady && existing.phase === "ready" && sameSelection(existing.selection, selected)))
	      return this.#scheduleRender(), this.#taskSnapshot(existing);
	    const resumeFromCheckpoint = !!(existing?.resumeAvailable && sameSelection(existing.selection, selected)), task = existing ?? {
	      topicId,
	      title,
	      selection: selected,
	      phase: "queued",
	      completed: 0,
	      total: 0,
	      detail: "",
	      error: "",
	      filename: "",
	      complete: !1,
	      archiveStatus: null,
	      createdAt: this.#now(),
	      finishedAt: 0,
	      localDownloadRequestedAt: 0,
	      artifact: null,
	      controller: null,
	      requestResumeCount: 0,
	      challengeResumeCount: 0,
	      resumeAvailable: !1
	    };
	    return task.phase = "queued", task.selection = selected, task.completed = resumeFromCheckpoint ? task.completed : 0, task.total = resumeFromCheckpoint ? task.total : 0, task.detail = resumeFromCheckpoint ? task.total > 0 ? `正在从 ${Math.min(task.completed, task.total)}/${task.total} 楼断点继续` : "正在从已保存断点继续" : "", task.error = "", task.filename = "", task.complete = !1, task.archiveStatus = null, task.finishedAt = 0, task.artifact = null, task.requestResumeCount = 0, task.challengeResumeCount = 0, task.resumeAvailable = resumeFromCheckpoint, task.controller = new AbortController(), this.#tasks.set(topicId, task), this.#trimTasks(), this.#options.notify?.(resumeFromCheckpoint ? `已从断点继续后台下载:${title}` : `已加入后台下载:${title}`), this.#scheduleRender(), this.#tail = this.#tail.catch(() => {
	    }).then(() => this.#run(task)), this.#taskSnapshot(task);
	  }
	  destroy() {
	    this.scope.destroy();
	  }
	  async #run(task) {
	    const controller = task.controller;
	    if (!(!controller || controller.signal.aborted))
	      try {
	        for (; ; )
	          try {
	            const artifact = await this.#options.worker(
	              task.topicId,
	              task.title,
	              controller.signal,
	              (progress) => {
	                controller.signal.aborted || task.controller !== controller || (task.phase = progress.phase, progress.completed !== void 0 && (task.completed = Math.max(0, Math.floor(progress.completed))), progress.total !== void 0 && (task.total = Math.max(0, Math.floor(progress.total))), task.detail = String(progress.detail ?? ""), this.#scheduleRender());
	              },
	              task.selection
	            );
	            if (controller.signal.throwIfAborted(), task.controller !== controller) return;
	            task.artifact = artifact, task.filename = artifact.filename, task.completed = artifact.postCount, task.total = artifact.expectedPostCount, task.complete = artifact.complete, task.archiveStatus = normalizedArchiveStatus(artifact.archiveStatus), task.phase = "ready", task.finishedAt = this.#now(), task.detail = "", task.resumeAvailable = !1, task.requestResumeCount = 0, task.challengeResumeCount = 0;
	            try {
	              await this.#backupArtifact(task, artifact), task.detail = "已保存到 Reader 下载历史 · 点击下载按钮可保存 HTML 到本地", this.#options.notify?.(
	                `Topic #${task.topicId} 已保存到下载历史`
	              );
	            } catch (error) {
	              task.detail = `HTML 已生成,Reader 缓存备份失败 · ${String(
	                error?.message ?? error
	              )}`, this.#options.notify?.(
	                `Topic #${task.topicId} HTML 已生成,但 Reader 本地备份失败`
	              );
	            }
	            break;
	          } catch (error) {
	            if (task.controller !== controller) return;
	            if (controller.signal.aborted) throw error;
	            const resume = this.#options.requestResume?.(error) ?? null;
	            if (!resume) throw error;
	            const waitMs = resume.waitMs;
	            if (task.resumeAvailable = !0, resume.kind === "cloudflare-challenge") {
	              if (task.challengeResumeCount >= DOWNLOAD_CHALLENGE_AUTO_RESUME_LIMIT) throw error;
	              task.challengeResumeCount += 1;
	            } else {
	              if (task.requestResumeCount >= DOWNLOAD_REQUEST_AUTO_RESUME_LIMIT) throw error;
	              task.requestResumeCount += 1;
	            }
	            task.phase = resume.kind === "cloudflare-challenge" ? "waiting-challenge" : "waiting-rate-limit", task.error = "";
	            const checkpoint = task.total > 0 ? `,已保存 ${Math.min(task.completed, task.total)}/${task.total} 楼断点` : ",已保存当前断点";
	            if (resume.kind === "cloudflare-challenge")
	              task.detail = `Cloudflare 验证未完成${checkpoint} · 等待 Cloudflare 验证通过后自动续传`, this.#options.notify?.(
	                `Topic #${task.topicId} 已暂停下载并保存断点,等待 Cloudflare 验证通过`
	              );
	            else {
	              const waitLabel = waitMs >= 1e3 ? `${Math.ceil(waitMs / 1e3)} 秒` : waitMs > 0 ? `${waitMs} 毫秒` : "立即", resumeLabel = waitMs > 0 ? `${waitLabel}后` : waitLabel;
	              task.detail = `遇到 HTTP 429${checkpoint} · ${resumeLabel}自动续传(${task.requestResumeCount}/${DOWNLOAD_REQUEST_AUTO_RESUME_LIMIT})`, this.#options.notify?.(
	                `Topic #${task.topicId} 遇到 429,已保存断点,${resumeLabel}自动续传`
	              );
	            }
	            if (this.#scheduleRender(), await resume.wait(controller.signal), controller.signal.throwIfAborted(), task.controller !== controller) return;
	            task.phase = "queued", task.detail = task.total > 0 ? `正在从 ${Math.min(task.completed, task.total)}/${task.total} 楼断点继续` : "正在从已保存断点继续", this.#scheduleRender();
	          }
	      } catch (error) {
	        if (task.controller !== controller) return;
	        if (controller.signal.aborted)
	          task.phase = "cancelled", task.error = "";
	        else {
	          task.phase = "error";
	          const resumable = this.#options.requestResume?.(error) ?? null;
	          resumable && task.resumeAvailable ? resumable.kind === "cloudflare-challenge" ? task.error = task.challengeResumeCount >= DOWNLOAD_CHALLENGE_AUTO_RESUME_LIMIT ? "Cloudflare 验证后再次触发 · 已停止自动续传,断点已保存,可稍后继续下载" : "Cloudflare 验证未完成 · 断点已保存,可继续下载" : task.error = task.requestResumeCount >= DOWNLOAD_REQUEST_AUTO_RESUME_LIMIT ? "HTTP 429 连续续传已达上限 · 断点已保存,可稍后继续下载" : "HTTP 429 · 断点已保存,可继续下载" : task.error = String(
	            error?.message ?? error
	          ), this.#options.notify?.(
	            `Topic #${task.topicId} 下载失败:${task.error}`
	          );
	        }
	        task.finishedAt = this.#now();
	      } finally {
	        task.controller === controller && (task.controller = null), this.#scheduleRender();
	      }
	  }
	  #click(event) {
	    const target = (0, import_event_target.eventElement)(event)?.closest(
	      "[data-topic-download-action],.ldp-topic-download-current"
	    ) ?? null;
	    if (!target) return;
	    if (target === this.#downloadCurrent) {
	      this.enqueueCurrent(!0);
	      return;
	    }
	    const topicId = (0, import_identifiers.discourseTopicId)(Number(target.dataset.topicId)), task = this.#tasks.get(topicId);
	    if (!task) return;
	    const action = target.dataset.topicDownloadAction;
	    if (action === "cancel") {
	      task.controller?.abort(new DOMException("用户取消 Topic 下载", "AbortError")), task.phase = "cancelled", task.finishedAt = this.#now(), this.#scheduleRender();
	      return;
	    }
	    if (action === "retry") {
	      this.enqueue(task.topicId, task.title, task.selection);
	      return;
	    }
	    if (action === "remove") {
	      this.#confirmAndRemove(task);
	      return;
	    }
	    if (action === "save") {
	      this.#ensureArtifact(task).then(() => this.#saveArtifact(task)).then(() => this.#options.notify?.(
	        `Topic #${task.topicId} 已触发浏览器下载`
	      )).catch((error) => {
	        this.#options.notify?.(
	          `Topic #${task.topicId} HTML 保存失败:${String(error)}`
	        );
	      });
	      return;
	    }
	    action === "view" && this.#ensureArtifact(task).then(() => this.#view(task)).catch((error) => {
	      this.#options.notify?.(
	        `Topic #${task.topicId} 离线查看失败:${String(error)}`
	      );
	    });
	  }
	  async #saveArtifact(task) {
	    if (!task.artifact) return Promise.resolve();
	    await Promise.resolve(this.#options.downloads.save(
	      new Blob([task.artifact.html], { type: "text/html;charset=utf-8" }),
	      task.artifact.filename
	    )), task.localDownloadRequestedAt = this.#now();
	    try {
	      await this.#backupArtifact(task, task.artifact);
	    } catch (error) {
	      this.#options.notify?.(
	        `Topic #${task.topicId} 已触发下载,但本地下载状态记录失败:${String(error)}`
	      );
	    }
	    this.#scheduleRender();
	  }
	  async #confirmAndRemove(task) {
	    if (!this.#removing.has(task.topicId)) {
	      this.#removing.add(task.topicId);
	      try {
	        const store = this.#options.artifacts, hasCachedHtml = !!await store?.read(task.topicId), choice = await this.#options.confirmRemoval?.(
	          Object.freeze({
	            topicId: task.topicId,
	            title: task.title,
	            filename: task.filename,
	            hasCachedHtml,
	            localDownloadRequestedAt: task.localDownloadRequestedAt
	          }),
	          this.#details
	        ) ?? "cancel";
	        if (choice === "cancel" || this.#tasks.get(task.topicId) !== task) return;
	        choice === "remove-record-and-cache" ? await store?.remove(task.topicId) : await store?.remove(task.topicId, { preserveHtml: !0 }), task.controller?.abort(
	          new DOMException("下载记录已移除", "AbortError")
	        ), this.#tasks.delete(task.topicId), this.#selectedHistoryTopics.delete(task.topicId), this.#options.notify?.(
	          choice === "remove-record-and-cache" ? `Topic #${task.topicId} 下载记录与 Reader 缓存 HTML 已删除` : `Topic #${task.topicId} 下载记录已移除,Reader 缓存 HTML 已保留`
	        ), this.#scheduleRender();
	      } catch (error) {
	        this.#options.notify?.(
	          `Topic #${task.topicId} 下载记录移除失败:${String(error)}`
	        );
	      } finally {
	        this.#removing.delete(task.topicId);
	      }
	    }
	  }
	  async #confirmAndRemoveSelected() {
	    const tasks = [...this.#selectedHistoryTopics].map((topicId) => this.#tasks.get(topicId) ?? null).filter((task) => !!task);
	    if (!(!tasks.length || tasks.some((task) => this.#removing.has(task.topicId)))) {
	      for (const task of tasks) this.#removing.add(task.topicId);
	      try {
	        const store = this.#options.artifacts, contexts = await Promise.all(tasks.map(async (task) => Object.freeze({
	          topicId: task.topicId,
	          title: task.title,
	          filename: task.filename,
	          hasCachedHtml: !!await store?.read(task.topicId),
	          localDownloadRequestedAt: task.localDownloadRequestedAt
	        }))), choice = await this.#options.confirmBulkRemoval?.(
	          Object.freeze(contexts),
	          this.#details
	        ) ?? "cancel";
	        if (choice === "cancel") return;
	        const activeTasks = tasks.filter((task) => this.#tasks.get(task.topicId) === task);
	        for (const task of activeTasks)
	          choice === "remove-record-and-cache" ? await store?.remove(task.topicId) : await store?.remove(task.topicId, { preserveHtml: !0 }), task.controller?.abort(
	            new DOMException("下载记录已批量移除", "AbortError")
	          ), this.#tasks.delete(task.topicId), this.#selectedHistoryTopics.delete(task.topicId);
	        this.#options.notify?.(
	          choice === "remove-record-and-cache" ? `已移除 ${activeTasks.length} 条下载记录及其 Reader 缓存 HTML` : `已移除 ${activeTasks.length} 条下载记录,Reader 缓存 HTML 已保留`
	        ), this.#scheduleRender();
	      } catch (error) {
	        this.#options.notify?.(`Topic 下载记录批量移除失败:${String(error)}`);
	      } finally {
	        for (const task of tasks) this.#removing.delete(task.topicId);
	      }
	    }
	  }
	  async #view(task) {
	    if (!task.artifact) return;
	    if (this.#options.viewHtml) {
	      await this.#options.viewHtml(
	        task.artifact.html,
	        task.title,
	        task.topicId
	      );
	      return;
	    }
	    const view = this.#options.document.defaultView, urlApi = view?.URL ?? globalThis.URL;
	    if (!view || typeof urlApi.createObjectURL != "function")
	      throw new Error("当前浏览器不支持本地 HTML 新标签查看");
	    const objectUrl = urlApi.createObjectURL(new Blob(
	      [(0, import_reader_topic_offline_document.prepareReaderTopicOfflineBlobHtml)(
	        task.artifact.html,
	        this.#options.document
	      )],
	      { type: "text/html;charset=utf-8" }
	    )), retainedObjectUrl = Object.freeze({
	      urlApi,
	      value: objectUrl
	    });
	    this.#viewObjectUrls.add(retainedObjectUrl);
	    const popup = view.open(objectUrl, "_blank");
	    if (!popup)
	      throw urlApi.revokeObjectURL(objectUrl), this.#viewObjectUrls.delete(retainedObjectUrl), new Error("浏览器阻止了离线 Topic 新标签页");
	    popup.opener = null;
	    const hydrate = () => {
	      try {
	        this.#options.hydrateHtmlWindow?.(popup);
	      } catch (error) {
	        this.#options.notify?.(
	          `Topic #${task.topicId} 离线正文水合失败:${String(error)}`
	        );
	      }
	    };
	    typeof popup.addEventListener == "function" ? popup.addEventListener(
	      "load",
	      () => {
	        hydrate();
	      },
	      { once: !0 }
	    ) : hydrate();
	  }
	  #render() {
	    this.#renderFrame = 0, this.syncCurrent();
	    const tasks = [...this.#tasks.values()].sort((left, right) => right.createdAt - left.createdAt), query = this.#historySearch.value.trim().toLocaleLowerCase("zh-CN"), filteredTasks = query ? tasks.filter((task) => [
	      String(task.topicId),
	      task.title,
	      task.filename,
	      selectionLabel(task.selection),
	      phaseLabel(task),
	      task.archiveStatus === null ? "" : `${task.archiveStatus} 版本`
	    ].join(" ").toLocaleLowerCase("zh-CN").includes(query)) : tasks, pageCount = Math.max(
	      1,
	      Math.ceil(filteredTasks.length / DOWNLOAD_HISTORY_PAGE_SIZE)
	    );
	    this.#historyPage = Math.min(this.#historyPage, pageCount - 1);
	    const pageStart = this.#historyPage * DOWNLOAD_HISTORY_PAGE_SIZE, pageTasks = filteredTasks.slice(
	      pageStart,
	      pageStart + DOWNLOAD_HISTORY_PAGE_SIZE
	    );
	    this.#visibleHistoryTopicIds = Object.freeze(pageTasks.map((task) => task.topicId)), this.#options.floating && (this.#details.hidden = !this.#managerVisible);
	    const renderKey = JSON.stringify({
	      current: this.#downloadCurrent.dataset.topicId,
	      open: this.#managerOpen,
	      query,
	      page: this.#historyPage,
	      batch: this.#historyBatchMode,
	      selected: [...this.#selectedHistoryTopics].sort((left, right) => Number(left) - Number(right)),
	      tasks: tasks.map((task) => [
	        task.topicId,
	        task.phase,
	        task.completed,
	        task.total,
	        task.detail,
	        task.error,
	        task.filename,
	        task.archiveStatus,
	        task.selection.mode,
	        task.selection.expression,
	        task.localDownloadRequestedAt
	      ])
	    });
	    if (renderKey === this.#renderKey) return;
	    this.#renderKey = renderKey;
	    const activeCount = tasks.filter((task) => !["ready", "error", "cancelled"].includes(task.phase)).length;
	    this.#summaryCount.textContent = activeCount ? `${activeCount} 进行中` : tasks.length ? String(tasks.length) : "", this.#historyCount.textContent = query ? `${filteredTasks.length} / ${tasks.length} 条` : `${tasks.length} 条`, this.#historyBatchToggle.setAttribute(
	      "aria-pressed",
	      String(this.#historyBatchMode)
	    ), this.#historyBatchToggle.setAttribute(
	      "aria-label",
	      this.#historyBatchMode ? "退出批量管理" : "进入批量管理"
	    ), this.#historyBatchToggle.querySelector("span").textContent = this.#historyBatchMode ? "完成" : "批量管理", this.#historyBatchBar.hidden = !this.#historyBatchMode;
	    const allPageSelected = pageTasks.length > 0 && pageTasks.every((task) => this.#selectedHistoryTopics.has(task.topicId));
	    if (this.#historySelectPage.disabled = pageTasks.length === 0, this.#historySelectPage.setAttribute(
	      "aria-label",
	      allPageSelected ? "取消选择当前页" : "选择当前页"
	    ), this.#historySelectPage.querySelector("span").textContent = allPageSelected ? "取消本页" : "全选本页", this.#historySelectionCount.textContent = `已选 ${this.#selectedHistoryTopics.size} 条`, this.#historyRemoveSelected.disabled = this.#selectedHistoryTopics.size === 0, this.#list.replaceChildren(...pageTasks.map((task) => this.#row(task))), !pageTasks.length) {
	      const empty = (0, import_html_element.htmlElement)(this.#options.document, "p", "ldp-topic-download-empty");
	      empty.textContent = tasks.length ? "没有匹配的下载记录。" : "还没有下载任务。", this.#list.append(empty);
	    }
	    this.#historyPagination.hidden = filteredTasks.length <= DOWNLOAD_HISTORY_PAGE_SIZE, this.#historyPageLabel.textContent = `第 ${this.#historyPage + 1} / ${pageCount} 页`, this.#historyPagePrevious.disabled = this.#historyPage === 0, this.#historyPageNext.disabled = this.#historyPage >= pageCount - 1, this.#emit();
	  }
	  async #restoreArtifacts(reconcile = !1) {
	    const store = this.#options.artifacts;
	    if (!(!store || this.scope.destroyed))
	      try {
	        const entries = await store.list(), storedTopicIds = new Set(entries.map((entry) => (0, import_identifiers.discourseTopicId)(entry.topicId)));
	        if (reconcile)
	          for (const [topicId, task] of this.#tasks)
	            storedTopicIds.has(topicId) || task.phase !== "ready" || (this.#tasks.delete(topicId), this.#selectedHistoryTopics.delete(topicId));
	        for (const entry of entries) {
	          const topicId = (0, import_identifiers.discourseTopicId)(entry.topicId);
	          if (this.scope.destroyed) continue;
	          const current = this.#tasks.get(topicId);
	          current && current.phase !== "ready" && current.phase !== "error" && current.phase !== "cancelled" || this.#tasks.set(topicId, {
	            topicId,
	            title: entry.title,
	            selection: restoredSelection(entry),
	            phase: "ready",
	            completed: entry.postCount,
	            total: entry.expectedPostCount,
	            detail: "已从 Reader 本地备份恢复",
	            error: "",
	            filename: entry.filename,
	            complete: entry.complete,
	            archiveStatus: normalizedArchiveStatus(entry.archiveStatus),
	            createdAt: entry.createdAt,
	            finishedAt: entry.finishedAt,
	            localDownloadRequestedAt: Math.max(0, Number(entry.localDownloadRequestedAt) || 0),
	            artifact: null,
	            controller: null,
	            requestResumeCount: 0,
	            challengeResumeCount: 0,
	            resumeAvailable: !1
	          });
	        }
	        this.#trimTasks(), this.#scheduleRender();
	      } catch (error) {
	        this.#options.notify?.(`Topic 下载历史读取失败:${String(error)}`);
	      }
	  }
	  async #ensureArtifact(task) {
	    if (task.artifact) return;
	    const cached = await this.#options.artifacts?.read(task.topicId) ?? null;
	    if (!cached) throw new Error("Reader 本地 HTML 备份已不可用");
	    task.artifact = Object.freeze({
	      html: cached.html,
	      filename: cached.filename,
	      postCount: cached.postCount,
	      expectedPostCount: cached.expectedPostCount,
	      complete: cached.complete,
	      archiveStatus: normalizedArchiveStatus(cached.archiveStatus)
	    }), task.archiveStatus = normalizedArchiveStatus(cached.archiveStatus);
	  }
	  #backupArtifact(task, artifact) {
	    const store = this.#options.artifacts;
	    if (!store) return Promise.resolve();
	    const record = Object.freeze({
	      topicId: task.topicId,
	      title: task.title,
	      selectionMode: task.selection.mode,
	      selectionExpression: task.selection.expression,
	      ...artifact,
	      archiveStatus: task.archiveStatus,
	      createdAt: task.createdAt,
	      finishedAt: task.finishedAt,
	      localDownloadRequestedAt: task.localDownloadRequestedAt
	    });
	    return store.write(record);
	  }
	  #row(task) {
	    const document = this.#options.document, row = (0, import_html_element.htmlElement)(document, "article", "ldp-topic-download-task");
	    if (row.classList.add(`is-${task.phase}`), row.dataset.topicId = String(task.topicId), task.archiveStatus !== null && (row.dataset.archiveStatus = String(task.archiveStatus)), this.#historyBatchMode) {
	      row.classList.add("is-batch");
	      const selectionLabelNode = (0, import_html_element.htmlElement)(
	        document,
	        "label",
	        "ldp-topic-download-task-selection"
	      ), selectionInput = document.createElement("input");
	      selectionInput.type = "checkbox", selectionInput.checked = this.#selectedHistoryTopics.has(task.topicId), selectionInput.dataset.topicDownloadSelect = "", selectionInput.dataset.topicId = String(task.topicId), selectionInput.setAttribute(
	        "aria-label",
	        `选择 Topic #${task.topicId}:${task.title}`
	      ), selectionLabelNode.append(selectionInput), row.append(selectionLabelNode);
	    }
	    const copy = (0, import_html_element.htmlElement)(document, "span", "ldp-topic-download-task-copy"), title = (0, import_html_element.htmlElement)(document, "strong");
	    title.textContent = task.title;
	    const state = (0, import_html_element.htmlElement)(document, "small");
	    if (state.textContent = task.archiveStatus === null ? phaseLabel(task) : `${task.archiveStatus} 版本 · ${phaseLabel(task)}`, copy.append(title, state), ["loading-posts", "loading-replies", "waiting-rate-limit", "waiting-challenge"].includes(task.phase) && task.total > 0) {
	      const progress = (0, import_html_element.htmlElement)(document, "progress");
	      progress.max = task.total, progress.value = Math.min(task.total, task.completed), progress.setAttribute("aria-label", phaseLabel(task)), copy.append(progress);
	    }
	    const actions = (0, import_html_element.htmlElement)(document, "span", "ldp-topic-download-task-actions"), addAction = (action, label, iconName) => {
	      const actionButton = button(
	        document,
	        `ldp-topic-download-${action}`,
	        label,
	        iconName
	      );
	      actionButton.dataset.topicDownloadAction = action, actionButton.dataset.topicId = String(task.topicId), actions.append(actionButton);
	    };
	    return task.phase === "ready" ? (addAction("view", "查看离线 Topic", "external-link"), addAction("save", "下载 HTML 到本地", "download"), addAction("remove", "移除下载记录", "x")) : task.phase === "error" || task.phase === "cancelled" ? (addAction(
	      "retry",
	      task.resumeAvailable ? "继续下载" : "重试下载",
	      "rotate-ccw"
	    ), addAction("remove", "移除下载记录", "x")) : addAction("cancel", "取消后台下载", "x"), row.append(copy, actions), row;
	  }
	  #taskSnapshot(task) {
	    return Object.freeze({
	      topicId: task.topicId,
	      title: task.title,
	      selection: task.selection,
	      phase: task.phase,
	      completed: task.completed,
	      total: task.total,
	      detail: task.detail,
	      error: task.error,
	      filename: task.filename,
	      complete: task.complete,
	      archiveStatus: task.archiveStatus,
	      createdAt: task.createdAt,
	      finishedAt: task.finishedAt,
	      localDownloadRequestedAt: task.localDownloadRequestedAt
	    });
	  }
	  #scheduleRender() {
	    if (this.scope.destroyed || this.#renderFrame) return;
	    let completed = !1;
	    const frame = this.#requestFrame(() => {
	      completed = !0, this.#render();
	    });
	    completed || (this.#renderFrame = frame);
	  }
	  #trimTasks() {
	    const disposable = [...this.#tasks.values()].filter((task) => ["error", "cancelled"].includes(task.phase)).sort((left, right) => right.createdAt - left.createdAt);
	    for (const task of disposable.slice(20))
	      this.#tasks.delete(task.topicId), this.#selectedHistoryTopics.delete(task.topicId);
	  }
	  #emit() {
	    for (const error of this.changes.emit(this.snapshot()))
	      this.#options.notify?.(`Topic 下载管理更新失败:${String(error)}`);
	  }
	}
}, "62b74758e846074d345e19770e0162308c50698704cc6b1c8dc965f7247f90c3");

/* Source: lite/src/sync/reader-webdav-category-ports.ts */
runtime.register("src/sync/reader-webdav-category-ports.js", function(module, exports, require) {
	var reader_webdav_category_ports_exports = {};
	__export(reader_webdav_category_ports_exports, {
	  createReaderWebDavActivityHistoryCategoryPort: () => createReaderWebDavActivityHistoryCategoryPort,
	  createReaderWebDavCategoryPorts: () => createReaderWebDavCategoryPorts,
	  createReaderWebDavNotificationHistoryCategoryPort: () => createReaderWebDavNotificationHistoryCategoryPort,
	  createReaderWebDavTranslationCacheCategoryPort: () => createReaderWebDavTranslationCacheCategoryPort,
	  createReaderWebDavTranslationCategoryPort: () => createReaderWebDavTranslationCategoryPort,
	  mergeReaderWebDavConnectHistoryValues: () => mergeReaderWebDavConnectHistoryValues,
	  mergeReaderWebDavHistoryValues: () => mergeReaderWebDavHistoryValues,
	  readerWebDavActivityHistoryRecordMatchesSchema: () => readerWebDavActivityHistoryRecordMatchesSchema,
	  readerWebDavBookmarkRecordMatchesSchema: () => readerWebDavBookmarkRecordMatchesSchema,
	  readerWebDavConnectHistoryRecordMatchesSchema: () => readerWebDavConnectHistoryRecordMatchesSchema,
	  readerWebDavCustomSiteRecordMatchesSchema: () => readerWebDavCustomSiteRecordMatchesSchema,
	  readerWebDavHistoryRecordMatchesSchema: () => readerWebDavHistoryRecordMatchesSchema,
	  readerWebDavNotificationHistoryRecordMatchesSchema: () => readerWebDavNotificationHistoryRecordMatchesSchema,
	  readerWebDavPreferenceRecordMatchesSchema: () => readerWebDavPreferenceRecordMatchesSchema,
	  readerWebDavQueueRecordMatchesSchema: () => readerWebDavQueueRecordMatchesSchema,
	  readerWebDavTopicContextRecordMatchesSchema: () => readerWebDavTopicContextRecordMatchesSchema,
	  readerWebDavTranslationCacheRecordMatchesSchema: () => readerWebDavTranslationCacheRecordMatchesSchema,
	  readerWebDavTranslationRemoteValueMatchesSchema: () => readerWebDavTranslationRemoteValueMatchesSchema
	});
	module.exports = __toCommonJS(reader_webdav_category_ports_exports);
	var import_identifiers = require("../discourse/identifiers.js"), import_reader_bookmark_model = require("../bookmark/reader-bookmark-model.js"), import_reader_history_repository = require("../history/reader-history-repository.js"), import_reader_custom_site_repository = require("../site/reader-custom-site-repository.js"), import_reader_notification_model = require("../notification/reader-notification-model.js"), import_reader_translation_config = require("../translation/reader-translation-config.js"), import_reader_webdav_model = require("./reader-webdav-model.js"), import_reader_webdav_secret_codec = require("./reader-webdav-secret-codec.js"), import_reader_webdav_offline_topic_port = require("./reader-webdav-offline-topic-port.js"), import_reader_webdav_history_cache_port = require("./reader-webdav-history-cache-port.js");
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function localRecord(id, value) {
	  return Object.freeze({ id, value });
	}
	function readerWebDavPreferenceRecordMatchesSchema(preferences, id, value, normalize, records = []) {
	  if (!Object.hasOwn(preferences, id)) return !1;
	  try {
	    const normalized = normalize({
	      ...preferences,
	      ...Object.fromEntries(records.map((entry) => [entry.id, entry.value])),
	      [id]: value
	    });
	    return Object.hasOwn(normalized, id) && (0, import_reader_webdav_model.readerWebDavFingerprint)(normalized[id]) === (0, import_reader_webdav_model.readerWebDavFingerprint)(value);
	  } catch {
	    return !1;
	  }
	}
	function readerWebDavTopicContextRecordMatchesSchema(id, value) {
	  const source = record(value);
	  return source ? id === "geometry" ? exactKeys(source, ["left", "top", "width", "height"]) && finiteNumber(source.left) && finiteNumber(source.top) && finiteNumber(source.width) && Number(source.width) > 0 && finiteNumber(source.height) && Number(source.height) > 0 : id.startsWith("view:") && id.length > 5 && exactKeys(source, [
	    "at",
	    "number",
	    "scrollTop",
	    "scrollLeft",
	    "offset"
	  ]) && finiteNonNegativeNumber(source.at) && positiveSafeInteger(source.number) && finiteNonNegativeNumber(source.scrollTop) && finiteNonNegativeNumber(source.scrollLeft) && finiteNumber(source.offset) : !1;
	}
	function readerWebDavCustomSiteRecordMatchesSchema(id, value) {
	  if (typeof value != "string" || value !== id) return !1;
	  const normalized = (0, import_reader_custom_site_repository.normalizeReaderCustomSiteHost)(value);
	  return !!(normalized && normalized === value && !(0, import_reader_custom_site_repository.readerBuiltinDiscourseHost)(value));
	}
	function categoryPort(value) {
	  return Object.freeze(value);
	}
	function exactKeys(value, keys) {
	  const expected = new Set(keys);
	  return Object.keys(value).length === expected.size && Object.keys(value).every((key) => expected.has(key));
	}
	function finiteNumber(value) {
	  return typeof value == "number" && Number.isFinite(value);
	}
	function finiteNonNegativeNumber(value) {
	  return finiteNumber(value) && value >= 0;
	}
	function positiveSafeInteger(value) {
	  return typeof value == "number" && Number.isSafeInteger(value) && value > 0;
	}
	function canonicalRecordFieldsMatch(value, normalized, required, optional = []) {
	  const source = record(value), canonical = record(normalized);
	  if (!source || !canonical) return !1;
	  const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
	  return required.some((key) => !Object.hasOwn(source, key)) || Object.keys(source).some((key) => !allowed.has(key)) ? !1 : Object.entries(source).every(([key, item]) => Object.hasOwn(canonical, key) && (0, import_reader_webdav_model.readerWebDavFingerprint)(item) === (0, import_reader_webdav_model.readerWebDavFingerprint)(canonical[key]));
	}
	function number(value, fallback = 0) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) ? numeric : fallback;
	}
	function text(value) {
	  return String(value ?? "").trim();
	}
	function historyValue(value) {
	  return (0, import_reader_history_repository.normalizeReaderHistoryEntry)(value);
	}
	function readerWebDavHistoryRecordMatchesSchema(id, value) {
	  const normalized = historyValue(value);
	  return normalized !== null && String(normalized.topicId) === id && canonicalRecordFieldsMatch(value, normalized, [
	    "topicId",
	    "title",
	    "postsCount",
	    "avatarTemplate",
	    "ownerUsername",
	    "postNumber",
	    "readPostNumbers",
	    "firstViewedAt",
	    "viewedAt"
	  ], [
	    "topicSubtitle",
	    "categoryId",
	    "categoryName",
	    "tags",
	    "viewport",
	    "archiveStatus",
	    "archivePostNumber"
	  ]);
	}
	function mergeReaderWebDavHistoryValues(local, remote) {
	  const left = historyValue(local), right = historyValue(remote);
	  if (!left) return right;
	  if (!right) return left;
	  const recent = left.viewedAt >= right.viewedAt ? left : right, older = recent === left ? right : left, archived = recent.archiveStatus !== null ? recent : older;
	  return Object.freeze({
	    ...recent,
	    postsCount: Math.max(left.postsCount, right.postsCount),
	    // 历史字段是观察快照;升级归一化会为旧记录补空值,空占位不能
	    // 抹掉另一设备已经观察到的分类、标签或精确阅读锚点。
	    topicSubtitle: recent.topicSubtitle || older.topicSubtitle,
	    categoryId: recent.categoryId ?? older.categoryId,
	    categoryName: recent.categoryName || older.categoryName,
	    tags: recent.tags.length ? recent.tags : older.tags,
	    viewport: recent.viewport ?? older.viewport,
	    readPostNumbers: Object.freeze([.../* @__PURE__ */ new Set([
	      ...left.readPostNumbers,
	      ...right.readPostNumbers
	    ])].sort((a, b) => a - b)),
	    archiveStatus: archived.archiveStatus,
	    archivePostNumber: archived.archivePostNumber,
	    firstViewedAt: Math.min(left.firstViewedAt, right.firstViewedAt),
	    viewedAt: Math.max(left.viewedAt, right.viewedAt)
	  });
	}
	function queueValue(value) {
	  const source = record(value), topicId = (0, import_identifiers.tryDiscourseTopicId)(source?.topicId);
	  return !source || !topicId ? null : Object.freeze({
	    topicId,
	    title: text(source.title) || `帖子 #${topicId}`,
	    href: text(source.href) || `/t/${topicId}`,
	    avatarTemplate: text(source.avatarTemplate),
	    avatarSource: text(source.avatarSource),
	    ownerUsername: text(source.ownerUsername),
	    postNumber: (0, import_identifiers.tryDiscoursePostNumber)(source.postNumber),
	    addedAt: Math.max(1, number(source.addedAt, 1)),
	    pinned: source.pinned === !0
	  });
	}
	function readerWebDavQueueRecordMatchesSchema(id, value) {
	  const normalized = queueValue(value);
	  return normalized !== null && String(normalized.topicId) === id && canonicalRecordFieldsMatch(value, normalized, [
	    "topicId",
	    "title",
	    "href",
	    "avatarTemplate",
	    "avatarSource",
	    "ownerUsername",
	    "postNumber",
	    "addedAt",
	    "pinned"
	  ]);
	}
	function mergeQueue(local, remote) {
	  const left = queueValue(local), right = queueValue(remote);
	  return left ? right ? Object.freeze({
	    ...right,
	    ...left,
	    title: left.title || right.title,
	    href: left.href || right.href,
	    avatarTemplate: left.avatarTemplate || right.avatarTemplate,
	    avatarSource: left.avatarSource || right.avatarSource,
	    ownerUsername: left.ownerUsername || right.ownerUsername,
	    postNumber: left.postNumber ?? right.postNumber,
	    addedAt: Math.min(left.addedAt, right.addedAt),
	    pinned: left.pinned || right.pinned
	  }) : left : right;
	}
	function bookmarkTaxonomyValue(source) {
	  const rawCategoryId = Number(source.categoryId), categoryId = Number.isSafeInteger(rawCategoryId) && rawCategoryId > 0 ? rawCategoryId : null, categoryName = text(source.categoryName), tags = Object.freeze([...new Map(
	    (Array.isArray(source.tags) ? source.tags : []).map((value) => text(value)).filter(Boolean).map((value) => [value.toLocaleLowerCase("zh-CN"), value])
	  ).values()]);
	  return Object.freeze({ categoryId, categoryName, tags });
	}
	function bookmarkValue(value) {
	  const source = record(value), tab = source?.tab, topicId = (0, import_identifiers.tryDiscourseTopicId)(source?.topicId), postNumber = (0, import_identifiers.tryDiscoursePostNumber)(source?.postNumber), identity = text(source?.identity);
	  if (!source || tab !== "Topic" && tab !== "Post" || !topicId || !postNumber || !identity) return null;
	  const rawBookmarkId = Number(source.bookmarkId), bookmarkId = Number.isSafeInteger(rawBookmarkId) && rawBookmarkId > 0 ? rawBookmarkId : null, postId = tab === "Post" ? (0, import_identifiers.tryDiscoursePostId)(source.postId) : null, title = text(source.title) || `帖子 #${topicId}`, authorUsername = text(source.authorUsername), name = text(source.name), taxonomy = bookmarkTaxonomyValue(source);
	  return Object.freeze({
	    identity,
	    tab,
	    bookmarkId,
	    topicId,
	    postId,
	    postNumber,
	    title,
	    authorUsername,
	    avatarTemplate: text(source.avatarTemplate),
	    createdAt: text(source.createdAt),
	    name,
	    highestPostNumber: Math.max(0, Math.floor(number(
	      source.highestPostNumber
	    ))),
	    reaction: "",
	    excerpt: "",
	    ...taxonomy,
	    searchText: [
	      title,
	      name,
	      authorUsername,
	      `@${authorUsername}`,
	      tab === "Post" ? `楼层 ${postNumber}` : "帖子",
	      taxonomy.categoryName,
	      ...taxonomy.tags
	    ].filter(Boolean).join(" ").toLocaleLowerCase()
	  });
	}
	function bookmarkRemoteValue(value) {
	  return Object.freeze({
	    identity: value.identity,
	    tab: value.tab,
	    bookmarkId: value.bookmarkId,
	    topicId: value.topicId,
	    postId: value.postId,
	    postNumber: value.postNumber,
	    title: value.title,
	    authorUsername: value.authorUsername,
	    avatarTemplate: value.avatarTemplate,
	    createdAt: value.createdAt,
	    name: value.name,
	    highestPostNumber: value.highestPostNumber,
	    categoryId: value.categoryId,
	    categoryName: value.categoryName,
	    tags: value.tags
	  });
	}
	function readerWebDavBookmarkRecordMatchesSchema(id, value) {
	  const normalized = bookmarkValue(value), remote = normalized ? bookmarkRemoteValue(normalized) : null;
	  return normalized !== null && normalized.identity === id && canonicalRecordFieldsMatch(value, remote, [
	    "identity",
	    "tab",
	    "bookmarkId",
	    "topicId",
	    "postId",
	    "postNumber",
	    "title",
	    "authorUsername",
	    "avatarTemplate",
	    "createdAt",
	    "name",
	    "highestPostNumber"
	  ], ["categoryId", "categoryName", "tags"]);
	}
	function mergeBookmark(local, remote) {
	  const left = bookmarkValue(local), right = bookmarkValue(remote);
	  if (!left) return remote;
	  if (!right) return local;
	  const leftAt = Date.parse(left.createdAt) || 0, rightAt = Date.parse(right.createdAt) || 0, recent = leftAt >= rightAt ? left : right, older = recent === left ? right : left;
	  return bookmarkRemoteValue(Object.freeze({
	    ...older,
	    ...recent,
	    categoryId: recent.categoryId ?? older.categoryId,
	    categoryName: recent.categoryName || older.categoryName,
	    tags: recent.tags.length ? recent.tags : older.tags
	  }));
	}
	const ACTIVITY_TABS = Object.freeze(["Reaction", "Boost", "Reply"]);
	function activityHistoryValue(value) {
	  const source = record(value), tab = source?.tab, topicId = (0, import_identifiers.tryDiscourseTopicId)(source?.topicId), postNumber = (0, import_identifiers.tryDiscoursePostNumber)(source?.postNumber), identity = text(source?.identity);
	  if (!source || !ACTIVITY_TABS.includes(tab) || !topicId || !postNumber || !identity) return null;
	  const postId = (0, import_identifiers.tryDiscoursePostId)(source.postId), title = text(source.title) || `帖子 #${topicId}`, authorUsername = text(source.authorUsername), reaction = tab === "Reaction" ? text(source.reaction) : "", excerpt = text(source.excerpt).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(), createdAt = Number.isFinite(Date.parse(text(source.createdAt))) ? text(source.createdAt) : "", taxonomy = bookmarkTaxonomyValue(source);
	  return Object.freeze({
	    identity,
	    tab,
	    bookmarkId: null,
	    topicId,
	    postId,
	    postNumber,
	    title,
	    authorUsername,
	    avatarTemplate: text(source.avatarTemplate),
	    createdAt,
	    name: text(source.name),
	    highestPostNumber: Math.max(0, Math.floor(number(
	      source.highestPostNumber
	    ))),
	    reaction,
	    excerpt,
	    ...taxonomy,
	    searchText: [
	      title,
	      authorUsername,
	      `@${authorUsername}`,
	      reaction,
	      excerpt,
	      tab === "Reaction" ? `表情回应 回应 楼层 ${postNumber}` : tab === "Boost" ? `Boost 楼层 ${postNumber}` : `回复 楼层 ${postNumber}`,
	      taxonomy.categoryName,
	      ...taxonomy.tags
	    ].filter(Boolean).join(" ").toLocaleLowerCase()
	  });
	}
	function activityHistoryRemoteValue(value) {
	  return Object.freeze({
	    identity: value.identity,
	    tab: value.tab,
	    topicId: value.topicId,
	    postId: value.postId,
	    postNumber: value.postNumber,
	    title: value.title,
	    authorUsername: value.authorUsername,
	    avatarTemplate: value.avatarTemplate,
	    createdAt: value.createdAt,
	    reaction: value.reaction,
	    excerpt: value.excerpt,
	    categoryId: value.categoryId,
	    categoryName: value.categoryName,
	    tags: value.tags
	  });
	}
	function readerWebDavActivityHistoryRecordMatchesSchema(id, value) {
	  const normalized = activityHistoryValue(value);
	  return normalized !== null && normalized.identity === id && canonicalRecordFieldsMatch(
	    value,
	    activityHistoryRemoteValue(normalized),
	    [
	      "identity",
	      "tab",
	      "topicId",
	      "postId",
	      "postNumber",
	      "title",
	      "authorUsername",
	      "avatarTemplate",
	      "createdAt",
	      "reaction",
	      "excerpt",
	      "categoryId",
	      "categoryName",
	      "tags"
	    ]
	  );
	}
	function mergeActivityHistory(local, remote) {
	  const left = activityHistoryValue(local), right = activityHistoryValue(remote);
	  if (!left) return remote;
	  if (!right) return local;
	  const leftAt = Date.parse(left.createdAt) || 0, rightAt = Date.parse(right.createdAt) || 0, recent = leftAt > rightAt ? left : rightAt > leftAt ? right : (0, import_reader_webdav_model.readerWebDavFingerprint)(activityHistoryRemoteValue(left)) >= (0, import_reader_webdav_model.readerWebDavFingerprint)(activityHistoryRemoteValue(right)) ? left : right, older = recent === left ? right : left;
	  return activityHistoryRemoteValue(Object.freeze({
	    ...older,
	    ...recent,
	    title: recent.title || older.title,
	    authorUsername: recent.authorUsername || older.authorUsername,
	    avatarTemplate: recent.avatarTemplate || older.avatarTemplate,
	    excerpt: recent.excerpt || older.excerpt,
	    reaction: recent.reaction || older.reaction,
	    categoryId: recent.categoryId ?? older.categoryId,
	    categoryName: recent.categoryName || older.categoryName,
	    tags: recent.tags.length ? recent.tags : older.tags
	  }));
	}
	function notificationHistoryValue(value) {
	  const source = record(value), group = text(source?.group), identity = text(source?.identity);
	  if (!source || !identity || !import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.includes(group)) return null;
	  const descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group), actor = text(source.actor), summary = text(source.summary), excerpt = text(source.excerpt).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(), createdAt = Number.isFinite(Date.parse(text(source.createdAt))) ? text(source.createdAt) : (/* @__PURE__ */ new Date(0)).toISOString(), topicId = (0, import_identifiers.tryDiscourseTopicId)(record(source.target)?.topicId), postNumber = (0, import_identifiers.tryDiscoursePostNumber)(record(source.target)?.postNumber), target = topicId && postNumber ? Object.freeze({ topicId, postNumber }) : null, rawCount = Number(source.aggregateCount), aggregateCount = Number.isSafeInteger(rawCount) && rawCount > 1 ? rawCount : null, typeLabel = text(source.typeLabel) || descriptor.label, rawCategoryId = Number(source.categoryId), categoryId = Number.isSafeInteger(rawCategoryId) && rawCategoryId > 0 ? rawCategoryId : null, categoryName = text(source.categoryName), tags = Object.freeze([...new Map(
	    (Array.isArray(source.tags) ? source.tags : []).map((value2) => text(value2)).filter(Boolean).map((value2) => [value2.toLocaleLowerCase("zh-CN"), value2])
	  ).values()]);
	  return Object.freeze({
	    identity,
	    group,
	    source: descriptor.source,
	    sourceNotificationId: null,
	    notificationTypeId: null,
	    highPriority: source.highPriority === !0,
	    typeName: text(source.typeName),
	    typeLabel,
	    aggregateCount,
	    icon: text(source.icon) || descriptor.icon,
	    actor,
	    avatarFallback: text(source.avatarFallback) || actor.slice(0, 1).toLocaleUpperCase() || "?",
	    avatarTemplate: text(source.avatarTemplate),
	    summary,
	    excerpt,
	    stateLabel: "",
	    createdAt,
	    read: null,
	    href: text(source.href),
	    target,
	    categoryId,
	    categoryName,
	    tags,
	    searchText: (0, import_reader_notification_model.notificationSearchText)([
	      actor,
	      summary,
	      excerpt,
	      typeLabel,
	      target?.topicId,
	      target?.postNumber,
	      categoryName,
	      ...tags
	    ])
	  });
	}
	function notificationHistoryRemoteValue(value) {
	  return Object.freeze({
	    identity: value.identity,
	    group: value.group,
	    highPriority: value.highPriority,
	    typeName: value.typeName,
	    typeLabel: value.typeLabel,
	    aggregateCount: value.aggregateCount,
	    icon: value.icon,
	    actor: value.actor,
	    avatarFallback: value.avatarFallback,
	    avatarTemplate: value.avatarTemplate,
	    summary: value.summary,
	    excerpt: value.excerpt,
	    createdAt: value.createdAt,
	    href: value.href,
	    target: value.target,
	    categoryId: value.categoryId,
	    categoryName: value.categoryName,
	    tags: value.tags
	  });
	}
	function readerWebDavNotificationHistoryRecordMatchesSchema(id, value) {
	  const normalized = notificationHistoryValue(value);
	  return normalized !== null && normalized.identity === id && canonicalRecordFieldsMatch(
	    value,
	    notificationHistoryRemoteValue(normalized),
	    [
	      "identity",
	      "group",
	      "highPriority",
	      "typeName",
	      "typeLabel",
	      "aggregateCount",
	      "icon",
	      "actor",
	      "avatarFallback",
	      "avatarTemplate",
	      "summary",
	      "excerpt",
	      "createdAt",
	      "href",
	      "target",
	      "categoryId",
	      "categoryName",
	      "tags"
	    ]
	  );
	}
	function mergeNotificationHistory(local, remote) {
	  const left = notificationHistoryValue(local), right = notificationHistoryValue(remote);
	  if (!left) return remote;
	  if (!right) return local;
	  const leftAt = Date.parse(left.createdAt) || 0, rightAt = Date.parse(right.createdAt) || 0, recent = leftAt > rightAt ? left : rightAt > leftAt ? right : (0, import_reader_webdav_model.readerWebDavFingerprint)(notificationHistoryRemoteValue(left)) >= (0, import_reader_webdav_model.readerWebDavFingerprint)(notificationHistoryRemoteValue(right)) ? left : right, older = recent === left ? right : left;
	  return notificationHistoryRemoteValue(Object.freeze({
	    ...older,
	    ...recent,
	    actor: recent.actor || older.actor,
	    avatarTemplate: recent.avatarTemplate || older.avatarTemplate,
	    summary: recent.summary || older.summary,
	    excerpt: recent.excerpt || older.excerpt,
	    href: recent.href || older.href,
	    target: recent.target ?? older.target,
	    categoryId: recent.categoryId ?? older.categoryId,
	    categoryName: recent.categoryName || older.categoryName,
	    tags: recent.tags.length ? recent.tags : older.tags
	  }));
	}
	function mergeTopicContext(local, remote) {
	  const left = record(local), right = record(remote);
	  return left ? right ? number(left.at) >= number(right.at) ? local : remote : local : remote;
	}
	function connectMetricSample(value) {
	  const source = record(value);
	  if (!source) return null;
	  const sample = {
	    first: Number(source.first),
	    last: Number(source.last),
	    firstObservedAt: Number(source.firstObservedAt),
	    lastObservedAt: Number(source.lastObservedAt)
	  };
	  return Object.values(sample).every(Number.isFinite) ? Object.freeze(sample) : null;
	}
	function mergeConnectMetricSample(local, remote) {
	  const left = connectMetricSample(local), right = connectMetricSample(remote);
	  if (!left) return right;
	  if (!right) return left;
	  const tieWinner = (0, import_reader_webdav_model.readerWebDavFingerprint)(left) >= (0, import_reader_webdav_model.readerWebDavFingerprint)(right) ? left : right, first = left.firstObservedAt < right.firstObservedAt ? left : right.firstObservedAt < left.firstObservedAt ? right : tieWinner, last = left.lastObservedAt > right.lastObservedAt ? left : right.lastObservedAt > left.lastObservedAt ? right : tieWinner;
	  return Object.freeze({
	    first: first.first,
	    last: last.last,
	    firstObservedAt: first.firstObservedAt,
	    lastObservedAt: last.lastObservedAt
	  });
	}
	function mergeReaderWebDavConnectHistoryValues(local, remote) {
	  const left = record(local), right = record(remote);
	  if (!left) return remote;
	  if (!right) return local;
	  const leftDays = record(left.days) ?? {}, rightDays = record(right.days) ?? {}, days = /* @__PURE__ */ Object.create(null);
	  for (const day of /* @__PURE__ */ new Set([...Object.keys(leftDays), ...Object.keys(rightDays)])) {
	    if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
	    const leftMetrics = record(leftDays[day]) ?? {}, rightMetrics = record(rightDays[day]) ?? {}, metrics = /* @__PURE__ */ Object.create(null);
	    for (const key of /* @__PURE__ */ new Set([
	      ...Object.keys(leftMetrics),
	      ...Object.keys(rightMetrics)
	    ])) {
	      const sample = mergeConnectMetricSample(
	        leftMetrics[key],
	        rightMetrics[key]
	      );
	      sample && (metrics[key] = sample);
	    }
	    Object.keys(metrics).length && (days[day] = Object.freeze(metrics));
	  }
	  const confirmedReads = /* @__PURE__ */ Object.create(null), leftReads = record(left.confirmedReads) ?? {}, rightReads = record(right.confirmedReads) ?? {};
	  for (const fingerprint of /* @__PURE__ */ new Set([
	    ...Object.keys(leftReads),
	    ...Object.keys(rightReads)
	  ])) {
	    if (!/^\d+:\d+$/.test(fingerprint)) continue;
	    const candidates = [leftReads[fingerprint], rightReads[fingerprint]].map(Number).filter(Number.isFinite);
	    candidates.length && (confirmedReads[fingerprint] = Math.min(...candidates));
	  }
	  const starts = [left.readTrackingStartedAt, right.readTrackingStartedAt].filter(finiteNonNegativeNumber);
	  return Object.freeze({
	    version: 1,
	    days: Object.freeze(days),
	    readTrackingStartedAt: starts.length ? Math.min(...starts) : null,
	    confirmedReads: Object.freeze(confirmedReads)
	  });
	}
	function readerWebDavConnectHistoryRecordMatchesSchema(id, value) {
	  const source = record(value), days = record(source?.days), confirmedReads = record(source?.confirmedReads);
	  if (id !== "current" || !source || source.version !== 1 || !days || !confirmedReads || !exactKeys(source, [
	    "version",
	    "days",
	    "readTrackingStartedAt",
	    "confirmedReads"
	  ]) || !(source.readTrackingStartedAt === null || finiteNonNegativeNumber(source.readTrackingStartedAt))) return !1;
	  for (const [day, rawMetrics] of Object.entries(days)) {
	    const metrics = record(rawMetrics);
	    if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !metrics) return !1;
	    for (const [key, rawSample] of Object.entries(metrics)) {
	      const sample = record(rawSample);
	      if (!key || !sample || !exactKeys(sample, [
	        "first",
	        "last",
	        "firstObservedAt",
	        "lastObservedAt"
	      ]) || !finiteNumber(sample.first) || !finiteNumber(sample.last) || !finiteNonNegativeNumber(sample.firstObservedAt) || !finiteNonNegativeNumber(sample.lastObservedAt) || sample.firstObservedAt > sample.lastObservedAt) return !1;
	    }
	  }
	  return Object.entries(confirmedReads).every(([fingerprint, confirmedAt]) => /^\d+:\d+$/.test(fingerprint) && finiteNonNegativeNumber(confirmedAt));
	}
	const TRANSLATION_SECTION_CACHE_ID_PREFIX = "reader-translation-section?", TRANSLATION_CACHE_RECORD_ID = "sections", TRANSLATION_CACHE_MAX_SECTIONS = 240, TRANSLATION_CACHE_MAX_PLAINTEXT_BYTES = 720 * 1024;
	function translationCacheEntry(value) {
	  const source = record(value), id = text(source?.id), translation = String(source?.translation ?? "").trim(), storedAt = number(source?.storedAt, -1);
	  return !id.startsWith(TRANSLATION_SECTION_CACHE_ID_PREFIX) || id.length > 240 || !translation || storedAt < 0 ? null : Object.freeze({ id, translation, storedAt });
	}
	function translationCachePayload(value) {
	  const source = record(value), candidates = (Array.isArray(source?.sections) ? source.sections : []).map(translationCacheEntry).filter((entry) => entry !== null).sort((left, right) => right.storedAt - left.storedAt || left.id.localeCompare(right.id)), unique = /* @__PURE__ */ new Map();
	  for (const entry of candidates) {
	    const current = unique.get(entry.id);
	    (!current || entry.storedAt > current.storedAt) && unique.set(entry.id, entry);
	  }
	  const sections = [], encoder = new TextEncoder();
	  let payloadBytes = encoder.encode('{"version":1,"sections":[]}').byteLength;
	  for (const entry of unique.values()) {
	    if (sections.length >= TRANSLATION_CACHE_MAX_SECTIONS) break;
	    const entryBytes = encoder.encode(JSON.stringify(entry)).byteLength, nextBytes = payloadBytes + entryBytes + (sections.length ? 1 : 0);
	    nextBytes > TRANSLATION_CACHE_MAX_PLAINTEXT_BYTES || (sections.push(entry), payloadBytes = nextBytes);
	  }
	  return Object.freeze({
	    version: 1,
	    sections: Object.freeze(sections)
	  });
	}
	function mergeTranslationCache(local, remote) {
	  return translationCachePayload({
	    sections: [
	      ...translationCachePayload(local).sections,
	      ...translationCachePayload(remote).sections
	    ]
	  });
	}
	function readerWebDavTranslationCacheRecordMatchesSchema(id, value) {
	  const source = record(value);
	  return id === TRANSLATION_CACHE_RECORD_ID && source?.version === 1 && Array.isArray(source.sections) && (0, import_reader_webdav_model.readerWebDavFingerprint)(source) === (0, import_reader_webdav_model.readerWebDavFingerprint)(translationCachePayload(source));
	}
	function translationRemoteProfileMatchesSchema(value, version) {
	  const source = record(value);
	  if (!source) return !1;
	  const keys = [
	    "baseUrl",
	    "model",
	    "prompt",
	    "temperature",
	    "reasoningEffort",
	    "requestsPerMinute",
	    "tokensPerMinute",
	    "animation",
	    ...version >= 4 ? ["models"] : [],
	    ...version >= 5 ? ["modelCatalog"] : []
	  ];
	  return !(!exactKeys(source, keys) || typeof source.baseUrl != "string" || (0, import_reader_translation_config.normalizeReaderTranslationBaseUrl)(source.baseUrl) !== source.baseUrl || typeof source.model != "string" || source.model.trim() !== source.model || source.model.length > 160 || typeof source.prompt != "string" || source.prompt.trim() !== source.prompt || !source.prompt || source.prompt.length > 4e3 || !finiteNumber(source.temperature) || source.temperature < 0 || source.temperature > 1 || typeof source.reasoningEffort != "string" || source.reasoningEffort.trim() !== source.reasoningEffort || source.reasoningEffort.length > 64 || /[\u0000-\u001f\u007f]/.test(source.reasoningEffort) || !Number.isSafeInteger(source.requestsPerMinute) || Number(source.requestsPerMinute) < 0 || Number(source.requestsPerMinute) > 1e4 || !Number.isSafeInteger(source.tokensPerMinute) || Number(source.tokensPerMinute) < 0 || Number(source.tokensPerMinute) > 1e8 || typeof source.animation != "string" || (0, import_reader_translation_config.normalizeReaderTranslationAnimation)(source.animation) !== source.animation || version >= 4 && (!Array.isArray(source.models) || source.models.some((item) => typeof item != "string" || !item || item.trim() !== item)) || version >= 5 && (!Array.isArray(source.modelCatalog) || source.modelCatalog.some((item) => {
	    const normalized = (0, import_reader_translation_config.normalizeReaderAiModelCatalogEntry)(item);
	    return !normalized || (0, import_reader_webdav_model.readerWebDavFingerprint)(normalized) !== (0, import_reader_webdav_model.readerWebDavFingerprint)(item);
	  })));
	}
	function readerWebDavTranslationRemoteValueMatchesSchema(value) {
	  const source = record(value);
	  if (!source || ![3, 4, 5].includes(source.version)) return !1;
	  const version = source.version, keys = [
	    "version",
	    "activeBaseUrl",
	    "profiles",
	    "encryptedApiKeys",
	    ...version >= 5 || Object.hasOwn(source, "animation") ? ["animation"] : []
	  ];
	  return exactKeys(source, keys) && typeof source.activeBaseUrl == "string" && (0, import_reader_translation_config.normalizeReaderTranslationBaseUrl)(source.activeBaseUrl) === source.activeBaseUrl && Array.isArray(source.profiles) && source.profiles.length > 0 && source.profiles.every((profile) => translationRemoteProfileMatchesSchema(profile, version)) && (source.encryptedApiKeys === "" || (0, import_reader_webdav_secret_codec.readerWebDavEncryptedSecretMatchesSchema)(source.encryptedApiKeys)) && (!Object.hasOwn(source, "animation") || typeof source.animation == "string" && (0, import_reader_translation_config.normalizeReaderTranslationAnimation)(source.animation) === source.animation);
	}
	function encryptedTranslationKeyAssociatedData(context, recordId, baseUrls) {
	  return `awesome-linuxdo-reader-lite-webdav|translation-key|${context.scopeId}|${recordId}|${JSON.stringify(baseUrls)}|v2`;
	}
	async function encodeTranslationConfigRecords(records, context) {
	  const entries = await Promise.all(Object.entries(records).map(
	    async ([id, item]) => {
	      if (item.deleted) return [id, item];
	      const config = (0, import_reader_translation_config.normalizeReaderTranslationConfig)(item.value), baseUrls = config.profiles.map((profile) => profile.baseUrl), apiKeys = config.profiles.map((profile) => profile.apiKey), profiles = config.profiles.map((profile) => Object.freeze({
	        baseUrl: profile.baseUrl,
	        models: profile.models,
	        modelCatalog: profile.modelCatalog,
	        model: profile.model,
	        prompt: profile.prompt,
	        temperature: profile.temperature,
	        reasoningEffort: profile.reasoningEffort,
	        requestsPerMinute: profile.requestsPerMinute,
	        tokensPerMinute: profile.tokensPerMinute,
	        animation: profile.animation
	      })), value = Object.freeze({
	        version: 5,
	        activeBaseUrl: config.activeBaseUrl,
	        animation: config.animation,
	        profiles: Object.freeze(profiles),
	        encryptedApiKeys: apiKeys.some(Boolean) ? await (0, import_reader_webdav_secret_codec.encryptReaderWebDavSecret)(
	          apiKeys,
	          context.secret,
	          encryptedTranslationKeyAssociatedData(context, id, baseUrls)
	        ) : ""
	      });
	      return [id, Object.freeze({ ...item, value })];
	    }
	  ));
	  return Object.freeze(Object.fromEntries(entries));
	}
	async function decodeTranslationConfigRecords(records, context) {
	  const entries = await Promise.all(Object.entries(records).map(
	    async ([id, item]) => {
	      if (item.deleted) return [id, item];
	      const source = record(item.value);
	      if (!source || !readerWebDavTranslationRemoteValueMatchesSchema(source))
	        throw new Error("WebDAV 翻译服务集合格式无效");
	      const version = source.version, rawProfiles = source.profiles, baseUrls = rawProfiles.map((rawProfile) => String(record(rawProfile).baseUrl)), decryptedKeys = source.encryptedApiKeys ? await (0, import_reader_webdav_secret_codec.decryptReaderWebDavSecret)(
	        source.encryptedApiKeys,
	        context.secret,
	        encryptedTranslationKeyAssociatedData(context, id, baseUrls)
	      ) : [];
	      if (!Array.isArray(decryptedKeys) || source.encryptedApiKeys !== "" && decryptedKeys.length !== rawProfiles.length)
	        throw new Error("WebDAV 翻译 API Key 集合格式无效");
	      const profiles = [];
	      for (const [index, rawProfile] of rawProfiles.entries()) {
	        const profile = record(rawProfile), baseUrl = baseUrls[index];
	        if (!profile || !baseUrl)
	          throw new Error("WebDAV 翻译服务项格式无效");
	        profiles.push({
	          baseUrl,
	          apiKey: String(decryptedKeys[index] ?? ""),
	          models: version >= 4 ? profile.models : [],
	          modelCatalog: version >= 5 ? profile.modelCatalog : [],
	          model: profile.model,
	          prompt: profile.prompt,
	          temperature: profile.temperature,
	          reasoningEffort: profile.reasoningEffort,
	          requestsPerMinute: profile.requestsPerMinute,
	          tokensPerMinute: profile.tokensPerMinute,
	          animation: profile.animation
	        });
	      }
	      const value = (0, import_reader_translation_config.normalizeReaderTranslationConfig)({
	        profiles,
	        activeBaseUrl: source.activeBaseUrl,
	        ...Object.hasOwn(source, "animation") ? { animation: source.animation } : {}
	      });
	      if (value.profiles.length !== rawProfiles.length || value.activeBaseUrl !== source.activeBaseUrl || Object.hasOwn(source, "animation") && value.animation !== source.animation || rawProfiles.some((rawProfile, index) => {
	        const profile = record(rawProfile), normalized = value.profiles[index];
	        if (!normalized) return !0;
	        const canonical = Object.freeze({
	          baseUrl: normalized.baseUrl,
	          ...version >= 4 ? { models: normalized.models } : {},
	          ...version >= 5 ? { modelCatalog: normalized.modelCatalog } : {},
	          model: normalized.model,
	          prompt: normalized.prompt,
	          temperature: normalized.temperature,
	          reasoningEffort: normalized.reasoningEffort,
	          requestsPerMinute: normalized.requestsPerMinute,
	          tokensPerMinute: normalized.tokensPerMinute,
	          animation: normalized.animation
	        });
	        return (0, import_reader_webdav_model.readerWebDavFingerprint)(profile) !== (0, import_reader_webdav_model.readerWebDavFingerprint)(canonical);
	      })) throw new Error("WebDAV 翻译服务集合字段类型无效");
	      return [id, Object.freeze({ ...item, value })];
	    }
	  ));
	  return Object.freeze(Object.fromEntries(entries));
	}
	function createReaderWebDavTranslationCategoryPort(repository) {
	  return categoryPort({
	    category: "translation",
	    initialStrategy: "remote",
	    validateRecord: (id) => id === "current",
	    capture: async () => [localRecord(
	      "current",
	      (await repository.load()).config
	    )],
	    mergeValues: (local) => local,
	    apply: (records) => repository.saveConfig((0, import_reader_translation_config.normalizeReaderTranslationConfig)(
	      records.find((entry) => entry.id === "current")?.value ?? (0, import_reader_translation_config.createReaderTranslationDefaultConfig)()
	    )),
	    decodeRemoteRecords: (records, context) => decodeTranslationConfigRecords(records, context),
	    encodeRemoteRecords: (records, context) => encodeTranslationConfigRecords(records, context)
	  });
	}
	function createReaderWebDavTranslationCacheCategoryPort(options) {
	  return categoryPort({
	    category: "translation-cache",
	    initialStrategy: "merge",
	    validateRecord: readerWebDavTranslationCacheRecordMatchesSchema,
	    capture: async () => {
	      const entries = await options.responses.entries({
	        kinds: [options.cache.kind],
	        tags: options.cache.tags
	      });
	      return [localRecord(
	        TRANSLATION_CACHE_RECORD_ID,
	        translationCachePayload({
	          sections: entries.map((entry) => ({
	            id: entry.id,
	            translation: typeof entry.value == "string" ? entry.value : "",
	            storedAt: entry.storedAt
	          }))
	        })
	      )];
	    },
	    mergeValues: mergeTranslationCache,
	    apply: async (records) => {
	      const payload = translationCachePayload(records.find((entry) => entry.id === TRANSLATION_CACHE_RECORD_ID)?.value);
	      await Promise.all(payload.sections.map((entry) => options.responses.restore({
	        id: entry.id,
	        kind: options.cache.kind,
	        tags: options.cache.tags,
	        freshForMs: options.cache.freshForMs,
	        retainForMs: options.cache.retainForMs,
	        persist: options.cache.persist
	      }, entry.translation, entry.storedAt)));
	    }
	  });
	}
	function createReaderWebDavNotificationHistoryCategoryPort(notifications) {
	  return (0, import_reader_webdav_history_cache_port.createReaderWebDavHistoryCacheCategoryPort)({
	    category: "notification-history",
	    validateRecord: readerWebDavNotificationHistoryRecordMatchesSchema,
	    capture: () => notifications.syncHistoryRecords().filter((entry) => import_reader_notification_model.READER_NOTIFICATION_AGGREGATE_GROUP_ORDER.includes(entry.group)).map((entry) => localRecord(entry.identity, notificationHistoryRemoteValue(entry))),
	    mergeValues: mergeNotificationHistory,
	    apply: (records) => notifications.applySyncedHistoryRecords(
	      (0, import_reader_notification_model.sortReaderNotifications)(records.map((entry) => notificationHistoryValue(entry.value)).filter((entry) => entry !== null))
	    )
	  });
	}
	function createReaderWebDavActivityHistoryCategoryPort(activity) {
	  return (0, import_reader_webdav_history_cache_port.createReaderWebDavHistoryCacheCategoryPort)({
	    category: "activity-history",
	    validateRecord: readerWebDavActivityHistoryRecordMatchesSchema,
	    capture: () => activity.activitySyncRecords().filter((entry) => ACTIVITY_TABS.includes(entry.tab)).map((entry) => localRecord(entry.identity, activityHistoryRemoteValue(entry))),
	    mergeValues: mergeActivityHistory,
	    apply: (records) => activity.applySyncedActivityRecords(
	      (0, import_reader_bookmark_model.sortReaderBookmarkRecords)(records.map((entry) => activityHistoryValue(entry.value)).filter((entry) => entry !== null))
	    )
	  });
	}
	function createReaderWebDavCategoryPorts(options) {
	  const ports = [
	    categoryPort({
	      category: "history",
	      initialStrategy: "merge",
	      validateRecord: readerWebDavHistoryRecordMatchesSchema,
	      /* 岁月史书依赖本机正文,不能同步到没有对应正文的另一设备。 */
	      capture: () => options.history.snapshot.entries.map((entry) => localRecord(String(entry.topicId), entry)),
	      mergeValues: mergeReaderWebDavHistoryValues,
	      apply: (records) => {
	        options.history.replaceExternal(records.map((entry) => historyValue(entry.value)).filter((entry) => entry !== null).sort((left, right) => right.viewedAt - left.viewedAt));
	      }
	    }),
	    categoryPort({
	      category: "preferences",
	      initialStrategy: "remote",
	      validateRecord: (id, value, records) => options.preferences.validate(id, value, records),
	      capture: () => Object.entries(options.preferences.read()).map(
	        ([id, value]) => localRecord(id, value)
	      ),
	      mergeValues: (local) => local,
	      apply: (records) => options.preferences.update(Object.fromEntries(
	        records.map((entry) => [entry.id, entry.value])
	      ))
	    }),
	    categoryPort({
	      category: "topic-context",
	      initialStrategy: "merge",
	      validateRecord: readerWebDavTopicContextRecordMatchesSchema,
	      capture: () => {
	        const snapshot = options.topicContext.snapshot;
	        return Object.freeze([
	          ...snapshot.fullPageGeometry ? [localRecord("geometry", snapshot.fullPageGeometry)] : [],
	          ...Object.entries(snapshot.views).map(([id, value]) => localRecord(`view:${id}`, value))
	        ]);
	      },
	      mergeValues: mergeTopicContext,
	      apply: (records) => options.topicContext.replaceExternal({
	        fullPageGeometry: records.find((entry) => entry.id === "geometry")?.value ?? null,
	        views: Object.fromEntries(records.filter((entry) => entry.id.startsWith("view:")).map((entry) => [entry.id.slice(5), entry.value]))
	      })
	    }),
	    categoryPort({
	      category: "custom-sites",
	      initialStrategy: "merge",
	      validateRecord: readerWebDavCustomSiteRecordMatchesSchema,
	      capture: async () => (await options.customSites.load()).map((host) => localRecord(host, host)),
	      mergeValues: (local) => local,
	      apply: (records) => options.customSites.replaceExternal(
	        records.map((entry) => entry.value)
	      )
	    })
	  ];
	  return options.queue && ports.push(categoryPort({
	    category: "queue",
	    initialStrategy: "merge",
	    validateRecord: readerWebDavQueueRecordMatchesSchema,
	    capture: () => options.queue.syncEntries().map((entry) => localRecord(String(entry.topicId), entry)),
	    mergeValues: mergeQueue,
	    apply: (records) => options.queue.replaceExternal(records.map((entry) => queueValue(entry.value)).filter((entry) => entry !== null))
	  })), options.bookmarks && (ports.push(categoryPort({
	    category: "bookmarks",
	    initialStrategy: "merge",
	    validateRecord: readerWebDavBookmarkRecordMatchesSchema,
	    capture: async () => (await options.bookmarks.syncBookmarkRecords()).map((entry) => localRecord(entry.identity, bookmarkRemoteValue(entry))),
	    mergeValues: mergeBookmark,
	    apply: (records) => options.bookmarks.applySyncedBookmarkRecords(
	      records.map((entry) => bookmarkValue(entry.value)).filter((entry) => entry !== null)
	    )
	  })), ports.push(createReaderWebDavActivityHistoryCategoryPort(options.bookmarks))), options.notifications && ports.push(createReaderWebDavNotificationHistoryCategoryPort(
	    options.notifications
	  )), options.connectHistory && ports.push(categoryPort({
	    category: "connect-history",
	    initialStrategy: "merge",
	    validateRecord: readerWebDavConnectHistoryRecordMatchesSchema,
	    capture: () => [localRecord(
	      "current",
	      options.connectHistory.syncValue()
	    )],
	    mergeValues: mergeReaderWebDavConnectHistoryValues,
	    apply: (records) => options.connectHistory.replaceExternal(
	      records.find((entry) => entry.id === "current")?.value
	    )
	  })), options.translation && ports.push(createReaderWebDavTranslationCategoryPort(options.translation)), options.translationCache && ports.push(createReaderWebDavTranslationCacheCategoryPort(
	    options.translationCache
	  )), options.offlineTopics && ports.push((0, import_reader_webdav_offline_topic_port.createReaderWebDavOfflineTopicCategoryPort)(
	    options.offlineTopics
	  )), Object.freeze(ports);
	}
}, "6feff4948f5e5b1ec90d525ea5d3461735fce9f7393cecdb8c60881b33a2f23d");

/* Source: lite/src/sync/reader-webdav-client.ts */
runtime.register("src/sync/reader-webdav-client.js", function(module, exports, require) {
	var reader_webdav_client_exports = {};
	__export(reader_webdav_client_exports, {
	  ReaderWebDavClient: () => ReaderWebDavClient,
	  ReaderWebDavError: () => ReaderWebDavError
	});
	module.exports = __toCommonJS(reader_webdav_client_exports);
	var import_reader_webdav_model = require("./reader-webdav-model.js");
	class ReaderWebDavError extends Error {
	  code;
	  status;
	  constructor(code, message, status = 0) {
	    super(message), this.name = "ReaderWebDavError", this.code = code, this.status = status;
	  }
	}
	function headerValue(headers, name) {
	  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
	  return String(headers ?? "").match(
	    new RegExp(`^${escaped}:\\s*(.+)$`, "im")
	  )?.[1]?.trim() ?? "";
	}
	function encodedPath(path) {
	  return path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
	}
	function targetUrl(config, remotePath = config.remotePath) {
	  const path = (0, import_reader_webdav_model.normalizeReaderWebDavRemotePath)(remotePath);
	  if (!path) throw new ReaderWebDavError(
	    "unexpected",
	    "WebDAV 远端路径无效"
	  );
	  return new URL(encodedPath(path), config.endpoint);
	}
	function statusError(status, operation) {
	  return status === 401 ? new ReaderWebDavError(
	    "auth",
	    "WebDAV 认证失败,请检查用户名和应用密码",
	    status
	  ) : status === 403 ? new ReaderWebDavError(
	    "forbidden",
	    `WebDAV 没有${operation}权限`,
	    status
	  ) : status === 404 ? new ReaderWebDavError(
	    "not-found",
	    "WebDAV 目标不存在",
	    status
	  ) : status === 409 || status === 412 ? new ReaderWebDavError(
	    "conflict",
	    "WebDAV 远端文件版本已变化,请重新读取后重试",
	    status
	  ) : status >= 500 ? new ReaderWebDavError(
	    "server",
	    `WebDAV 服务暂时不可用(HTTP ${status})`,
	    status
	  ) : new ReaderWebDavError(
	    "unexpected",
	    `WebDAV ${operation}失败(HTTP ${status || 0})`,
	    status
	  );
	}
	class ReaderWebDavClient {
	  #request;
	  #timeoutMs;
	  #maxDocumentBytes;
	  constructor(options) {
	    this.#request = options.request, this.#timeoutMs = Math.max(
	      3e3,
	      Math.min(6e4, Math.round(options.timeoutMs ?? 15e3))
	    ), this.#maxDocumentBytes = Math.max(
	      16384,
	      Math.min(8 * 1024 * 1024, Math.round(
	        options.maxDocumentBytes ?? 2 * 1024 * 1024
	      ))
	    );
	  }
	  async test(config, signal) {
	    const response = await this.#execute(
	      config,
	      "PROPFIND",
	      new URL(config.endpoint),
	      { Depth: "0" },
	      signal
	    );
	    if (response.status !== 200 && response.status !== 207)
	      throw statusError(response.status, "连接检测");
	  }
	  async read(config, signal) {
	    const response = await this.#execute(
	      config,
	      "GET",
	      targetUrl(config),
	      {
	        Accept: "application/json",
	        "Cache-Control": "no-cache",
	        Pragma: "no-cache"
	      },
	      signal
	    );
	    if (response.status === 404 || response.status === 409) return null;
	    if (response.status < 200 || response.status >= 300)
	      throw statusError(response.status, "读取");
	    const text = String(response.responseText ?? "");
	    if (new TextEncoder().encode(text).byteLength > this.#maxDocumentBytes)
	      throw new ReaderWebDavError(
	        "unexpected",
	        "WebDAV 同步文件超过 2 MiB 安全上限"
	      );
	    const etag = headerValue(response.responseHeaders, "ETag");
	    if (!etag) throw new ReaderWebDavError(
	      "unexpected",
	      "WebDAV 读取成功但服务器未返回 ETag,无法安全同步"
	    );
	    return Object.freeze({ text, etag });
	  }
	  async write(config, text, etag, signal) {
	    if (new TextEncoder().encode(text).byteLength > this.#maxDocumentBytes)
	      throw new ReaderWebDavError(
	        "unexpected",
	        "WebDAV 同步文件超过 2 MiB 安全上限"
	      );
	    etag || await this.#ensureCollections(config, config.remotePath, signal);
	    const response = await this.#execute(
	      config,
	      "PUT",
	      targetUrl(config),
	      {
	        "Content-Type": "application/json; charset=utf-8",
	        ...etag ? { "If-Match": etag } : { "If-None-Match": "*" }
	      },
	      signal,
	      text
	    );
	    if (![200, 201, 204].includes(response.status))
	      throw statusError(response.status, "写入");
	    return headerValue(response.responseHeaders, "ETag");
	  }
	  /**
	   * 读取主同步文件之外的独立对象。离线 Topic HTML 与历史清单不受主
	   * sync.json 的 2 MiB 安全上限约束;需要内容完整性校验的对象由上层负责。
	   */
	  async readObject(config, remotePath, signal) {
	    const response = await this.#execute(
	      config,
	      "GET",
	      targetUrl(config, remotePath),
	      {
	        Accept: "text/html, application/json;q=0.9, */*;q=0.1",
	        "Cache-Control": "no-cache",
	        Pragma: "no-cache"
	      },
	      signal
	    );
	    if (response.status === 404 || response.status === 409) return null;
	    if (response.status < 200 || response.status >= 300)
	      throw statusError(response.status, "读取独立对象");
	    const etag = headerValue(response.responseHeaders, "ETag");
	    if (!etag) throw new ReaderWebDavError(
	      "unexpected",
	      "WebDAV 独立对象读取成功但服务器未返回 ETag"
	    );
	    return Object.freeze({
	      text: String(response.responseText ?? ""),
	      etag
	    });
	  }
	  /** 条件写入独立对象;内容对象用 null ETag 只允许首次创建。 */
	  async writeObject(config, remotePath, text, etag, contentType, signal) {
	    etag || await this.#ensureCollections(config, remotePath, signal);
	    const response = await this.#execute(
	      config,
	      "PUT",
	      targetUrl(config, remotePath),
	      {
	        "Content-Type": contentType,
	        ...etag ? { "If-Match": etag } : { "If-None-Match": "*" }
	      },
	      signal,
	      text
	    );
	    if (![200, 201, 204].includes(response.status))
	      throw statusError(response.status, "写入独立对象");
	    return headerValue(response.responseHeaders, "ETag");
	  }
	  async #ensureCollections(config, remotePath, signal) {
	    const normalized = (0, import_reader_webdav_model.normalizeReaderWebDavRemotePath)(remotePath);
	    if (!normalized) throw new ReaderWebDavError(
	      "unexpected",
	      "WebDAV 独立对象路径无效"
	    );
	    const segments = normalized.split("/").slice(0, -1);
	    let relative = "";
	    for (const segment of segments) {
	      relative += `${encodeURIComponent(segment)}/`;
	      const response = await this.#execute(
	        config,
	        "MKCOL",
	        new URL(relative, config.endpoint),
	        {},
	        signal
	      );
	      if (![200, 201, 204, 405].includes(response.status))
	        throw statusError(response.status, "创建同步目录");
	    }
	  }
	  #execute(config, method, url, headers, signal, data) {
	    return signal.aborted ? Promise.reject(signal.reason) : new Promise((resolve, reject) => {
	      let settled = !1, handle;
	      const cleanup = () => signal.removeEventListener("abort", abort), fail = (cause) => {
	        settled || (settled = !0, cleanup(), reject(cause));
	      }, abort = () => {
	        if (!settled) {
	          settled = !0, cleanup();
	          try {
	            handle?.abort?.();
	          } finally {
	            reject(signal.reason);
	          }
	        }
	      };
	      signal.addEventListener("abort", abort, { once: !0 });
	      try {
	        handle = this.#request({
	          method,
	          url: url.href,
	          headers,
	          user: config.username,
	          password: config.password,
	          timeout: this.#timeoutMs,
	          responseType: "text",
	          ...data === void 0 ? {} : { data },
	          onload: (response) => {
	            settled || (settled = !0, cleanup(), resolve(response));
	          },
	          onerror: () => fail(new ReaderWebDavError(
	            "network",
	            "WebDAV 网络连接失败"
	          )),
	          ontimeout: () => fail(new ReaderWebDavError(
	            "timeout",
	            "WebDAV 请求超时"
	          )),
	          onabort: () => {
	            signal.aborted ? abort() : fail(new ReaderWebDavError(
	              "network",
	              "WebDAV 请求已取消"
	            ));
	          }
	        });
	      } catch (cause) {
	        settled = !0, cleanup(), reject(cause);
	      }
	    });
	  }
	}
}, "347e396fab1aded6277faa4fc4e24214c7d0374edc82c20843ef99b5b43e4314");

/* Source: lite/src/sync/reader-webdav-config-repository.ts */
runtime.register("src/sync/reader-webdav-config-repository.js", function(module, exports, require) {
	var reader_webdav_config_repository_exports = {};
	__export(reader_webdav_config_repository_exports, {
	  READER_WEBDAV_CONFIG_STORAGE_KEY: () => READER_WEBDAV_CONFIG_STORAGE_KEY,
	  ReaderWebDavConfigRepository: () => ReaderWebDavConfigRepository
	});
	module.exports = __toCommonJS(reader_webdav_config_repository_exports);
	var import_signal = require("../kernel/signal.js"), import_reader_webdav_model = require("./reader-webdav-model.js");
	const READER_WEBDAV_CONFIG_STORAGE_KEY = "awesome-linuxdo-reader:webdav:v2";
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function normalizedBaselines(value) {
	  const scopes = record(value), result = /* @__PURE__ */ Object.create(null);
	  for (const [scopeId, rawBaseline] of Object.entries(scopes ?? {})) {
	    if (!scopeId || scopeId.length > 240) continue;
	    const source = record(rawBaseline), baseline = {};
	    for (const category of import_reader_webdav_model.READER_WEBDAV_CATEGORIES) {
	      const rawRecords = record(source?.[category]);
	      rawRecords && (baseline[category] = Object.freeze(Object.fromEntries(
	        Object.entries(rawRecords).filter(([id, state]) => !!id && typeof state == "string").map(([id, state]) => [id, state])
	      )));
	    }
	    result[scopeId] = Object.freeze(baseline);
	  }
	  return Object.freeze(result);
	}
	function normalizedStatus(value) {
	  const source = record(value), kind = ["idle", "syncing", "success", "error"].includes(String(source?.kind)) ? source.kind : "idle";
	  return Object.freeze({
	    kind: kind === "syncing" ? "idle" : kind,
	    message: String(source?.message ?? ""),
	    at: Math.max(0, Number(source?.at) || 0)
	  });
	}
	function sameConfig(left, right) {
	  return left.endpoint === right.endpoint && left.username === right.username && left.password === right.password && left.remotePath === right.remotePath && left.autoSyncEnabled === right.autoSyncEnabled && left.autoSyncIntervalMinutes === right.autoSyncIntervalMinutes && import_reader_webdav_model.READER_WEBDAV_CATEGORIES.every((category) => left.categories[category] === right.categories[category]);
	}
	function sameBaseline(left, right) {
	  return left ? import_reader_webdav_model.READER_WEBDAV_CATEGORIES.every((category) => {
	    const leftRecords = left[category], rightRecords = right[category];
	    if (leftRecords === rightRecords) return !0;
	    if (!leftRecords || !rightRecords) return !1;
	    const leftIds = Object.keys(leftRecords), rightIds = Object.keys(rightRecords);
	    return leftIds.length === rightIds.length && leftIds.every((id) => leftRecords[id] === rightRecords[id]);
	  }) : !1;
	}
	function defaultWriterId() {
	  const random = globalThis.crypto?.randomUUID?.();
	  return random ? `device:${random}` : `device:${Date.now()}`;
	}
	class ReaderWebDavConfigRepository {
	  changes = new import_signal.Signal();
	  #storage;
	  #storageKey;
	  #createWriterId;
	  #snapshot = Object.freeze({
	    loaded: !1,
	    config: (0, import_reader_webdav_model.createReaderWebDavDefaultConfig)(),
	    writerId: "",
	    baselines: Object.freeze({}),
	    status: Object.freeze({ kind: "idle", message: "", at: 0 })
	  });
	  #loadPromise = null;
	  #writeTail = Promise.resolve();
	  constructor(options) {
	    this.#storage = options.storage, this.#storageKey = options.storageKey ?? READER_WEBDAV_CONFIG_STORAGE_KEY, this.#createWriterId = options.createWriterId ?? defaultWriterId;
	  }
	  get snapshot() {
	    return this.#snapshot;
	  }
	  get storageKey() {
	    return this.#storageKey;
	  }
	  async load() {
	    if (this.#snapshot.loaded) return this.#snapshot;
	    if (this.#loadPromise) return this.#loadPromise;
	    this.#loadPromise = (async () => {
	      const source = record(await this.#storage.getValue(this.#storageKey)), snapshot = Object.freeze({
	        loaded: !0,
	        config: (0, import_reader_webdav_model.normalizeReaderWebDavConfig)(source?.config),
	        writerId: String(source?.writerId ?? "").trim() || this.#createWriterId(),
	        baselines: normalizedBaselines(source?.baselines),
	        status: normalizedStatus(source?.status)
	      });
	      return this.#commit(() => snapshot);
	    })();
	    try {
	      return await this.#loadPromise;
	    } finally {
	      this.#loadPromise = null;
	    }
	  }
	  reloadExternal() {
	    const transaction = this.#writeTail.then(async () => {
	      const source = record(await this.#storage.getValue(this.#storageKey)), snapshot = Object.freeze({
	        loaded: !0,
	        config: (0, import_reader_webdav_model.normalizeReaderWebDavConfig)(source?.config),
	        writerId: String(source?.writerId ?? "").trim() || this.#createWriterId(),
	        baselines: normalizedBaselines(source?.baselines),
	        status: normalizedStatus(source?.status)
	      });
	      return this.#snapshot = snapshot, this.changes.emit(snapshot), snapshot;
	    });
	    return this.#writeTail = transaction.then(
	      () => {
	      },
	      () => {
	      }
	    ), transaction;
	  }
	  async saveConfig(value) {
	    await this.load();
	    const config = (0, import_reader_webdav_model.normalizeReaderWebDavConfig)(value);
	    return this.#commit((snapshot) => sameConfig(snapshot.config, config) ? snapshot : Object.freeze({
	      ...snapshot,
	      config,
	      status: Object.freeze({
	        kind: "idle",
	        message: "WebDAV 设置已更新,尚未使用当前配置同步。",
	        at: 0
	      })
	    }));
	  }
	  async saveBaseline(scopeId, baseline) {
	    return await this.load(), this.#commit((snapshot) => sameBaseline(snapshot.baselines[scopeId], baseline) ? snapshot : Object.freeze({
	      ...snapshot,
	      baselines: Object.freeze({
	        ...snapshot.baselines,
	        [scopeId]: baseline
	      })
	    }));
	  }
	  /**
	   * 本机缓存清理只解除对应类别的三方合并基线,不触碰远端记录。
	   *
	   * 同一份本机数据可能先后连接多个 WebDAV 目标,因此必须从所有目标 scope
	   * 移除类别基线;下一次同步会按首次合并策略恢复仍存在的远端内容,而不会把
	   * 本机缓存缺失误判成用户主动删除。
	   */
	  async forgetBaselineCategories(categories) {
	    await this.load();
	    const forgotten = new Set(categories);
	    return forgotten.size ? this.#commit((snapshot) => {
	      let changed = !1;
	      const baselines = {};
	      for (const [scopeId, baseline] of Object.entries(snapshot.baselines)) {
	        let scopeChanged = !1;
	        const next = {};
	        for (const category of import_reader_webdav_model.READER_WEBDAV_CATEGORIES) {
	          const records = baseline[category];
	          if (records !== void 0) {
	            if (forgotten.has(category)) {
	              changed = !0, scopeChanged = !0;
	              continue;
	            }
	            next[category] = records;
	          }
	        }
	        baselines[scopeId] = scopeChanged ? Object.freeze(next) : baseline;
	      }
	      return changed ? Object.freeze({
	        ...snapshot,
	        baselines: Object.freeze(baselines)
	      }) : snapshot;
	    }) : this.#snapshot;
	  }
	  async saveStatus(status) {
	    return await this.load(), this.#commit((snapshot) => Object.freeze({
	      ...snapshot,
	      status: Object.freeze({ ...status })
	    }));
	  }
	  #commit(update) {
	    const transaction = this.#writeTail.then(async () => {
	      const snapshot = update(this.#snapshot);
	      return snapshot === this.#snapshot || (await this.#storage.setValue(this.#storageKey, {
	        version: 2,
	        config: snapshot.config,
	        writerId: snapshot.writerId,
	        baselines: snapshot.baselines,
	        status: snapshot.status
	      }), this.#snapshot = snapshot, this.changes.emit(snapshot)), snapshot;
	    });
	    return this.#writeTail = transaction.then(
	      () => {
	      },
	      () => {
	      }
	    ), transaction;
	  }
	}
}, "839ea66a04fb71f7166bdeafe0fc480acff86e5ba7aea7bf196244b6280c79c1");

/* Source: lite/src/sync/reader-webdav-coordinator.ts */
runtime.register("src/sync/reader-webdav-coordinator.js", function(module, exports, require) {
	var reader_webdav_coordinator_exports = {};
	__export(reader_webdav_coordinator_exports, {
	  ReaderWebDavAutoSync: () => ReaderWebDavAutoSync,
	  ReaderWebDavCoordinator: () => ReaderWebDavCoordinator
	});
	module.exports = __toCommonJS(reader_webdav_coordinator_exports);
	var import_lifecycle = require("../kernel/lifecycle.js"), import_coordinated_request_client = require("../network/coordinated-request-client.js"), import_reader_webdav_client = require("./reader-webdav-client.js"), import_reader_webdav_model = require("./reader-webdav-model.js");
	const CONFLICT_RETRY_DELAYS_MS = Object.freeze([250, 750]);
	function errorMessage(cause) {
	  return cause instanceof Error && cause.message.trim() ? cause.message : "WebDAV 同步失败";
	}
	function localFingerprint(records) {
	  return (0, import_reader_webdav_model.readerWebDavFingerprint)([...records].map((entry) => ({ id: entry.id, value: entry.value })).sort((left, right) => left.id.localeCompare(right.id)));
	}
	function baselineStorageScopeId(config, runtimeScopeId) {
	  return `target:${(0, import_reader_webdav_model.readerWebDavFingerprint)({
	    endpoint: config.endpoint,
	    username: config.username,
	    remotePath: config.remotePath,
	    runtimeScopeId
	  })}`;
	}
	function withScope(document, scopeId, scope, writerId, now) {
	  return Object.freeze({
	    ...document,
	    updatedAt: now,
	    writerId,
	    scopes: Object.freeze({
	      ...document.scopes,
	      [scopeId]: scope
	    })
	  });
	}
	class ReaderWebDavCoordinator {
	  #client;
	  #repository;
	  #categories;
	  #hostname;
	  #username;
	  #now;
	  #retryDelay;
	  #active = null;
	  #activeConfig = null;
	  #localCacheMutationBarrier = null;
	  constructor(options) {
	    this.#client = options.client, this.#repository = options.repository, this.#categories = new Map(options.categories.map((port) => [
	      port.category,
	      port
	    ])), this.#hostname = options.hostname, this.#username = options.username, this.#now = options.now ?? Date.now, this.#retryDelay = options.retryDelay ?? import_coordinated_request_client.abortableDelay;
	  }
	  async testConnection(signal = new AbortController().signal) {
	    const snapshot = await this.#repository.load(), issues = (0, import_reader_webdav_model.validateReaderWebDavConfig)(snapshot.config, {
	      requireCredentials: !0
	    });
	    if (issues.length) throw new Error(issues[0]);
	    await this.#client.test(snapshot.config, signal);
	  }
	  /**
	   * 在本机缓存清理期间暂停同步,并先解除对应类别的本地三方合并基线。
	   * 已经开始的同步会先完整结束;清理者释放 barrier 后,后续同步才可读取新本地状态。
	   */
	  async acquireLocalCacheClear(categories) {
	    for (; this.#localCacheMutationBarrier; )
	      await this.#localCacheMutationBarrier;
	    let resolveBarrier = () => {
	    };
	    const barrier = new Promise((resolve) => {
	      resolveBarrier = resolve;
	    });
	    this.#localCacheMutationBarrier = barrier;
	    let released = !1;
	    const release = () => {
	      released || (released = !0, this.#localCacheMutationBarrier === barrier && (this.#localCacheMutationBarrier = null), resolveBarrier());
	    };
	    try {
	      return this.#active && await this.#active, await this.#repository.forgetBaselineCategories(categories), release;
	    } catch (cause) {
	      throw release(), cause;
	    }
	  }
	  syncNow(signal = new AbortController().signal) {
	    if (this.#localCacheMutationBarrier)
	      return this.#localCacheMutationBarrier.then(() => {
	        if (signal.aborted) throw signal.reason;
	        return this.syncNow(signal);
	      });
	    if (this.#active)
	      return this.#activeConfig === this.#repository.snapshot.config ? this.#active : this.#active.catch(() => {
	      }).then(() => {
	        if (signal.aborted) throw signal.reason;
	        return this.syncNow(signal);
	      });
	    this.#activeConfig = this.#repository.snapshot.config;
	    const active = this.#synchronize(signal).finally(() => {
	      this.#active === active && (this.#active = null, this.#activeConfig = null);
	    });
	    return this.#active = active, active;
	  }
	  async #synchronize(signal) {
	    const startedAt = this.#now();
	    let synchronizedConfig = null;
	    await this.#repository.saveStatus(Object.freeze({
	      kind: "syncing",
	      message: "正在读取并合并 WebDAV 数据…",
	      at: startedAt
	    }));
	    try {
	      const snapshot = await this.#repository.load();
	      synchronizedConfig = snapshot.config;
	      const issues = (0, import_reader_webdav_model.validateReaderWebDavConfig)(snapshot.config, {
	        requireCredentials: !0
	      });
	      if (issues.length) throw new Error(issues[0]);
	      const scopeId = (0, import_reader_webdav_model.readerWebDavRuntimeScopeId)(
	        this.#hostname(),
	        this.#username()
	      ), baselineScopeId = baselineStorageScopeId(
	        snapshot.config,
	        scopeId
	      ), requested = import_reader_webdav_model.READER_WEBDAV_CATEGORIES.filter(
	        (category) => snapshot.config.categories[category]
	      ), unavailable = requested.filter(
	        (category) => !this.#categories.has(category)
	      );
	      if (unavailable.length)
	        throw new Error(
	          `所选同步内容当前不可用:${unavailable.map(
	            (category) => import_reader_webdav_model.READER_WEBDAV_CATEGORY_LABELS[category]
	          ).join("、")}`
	        );
	      const selected = requested.map(
	        (category) => this.#categories.get(category)
	      );
	      if (!selected.length) throw new Error("所选同步内容当前不可用");
	      const regularSelected = selected.filter((port) => !port.synchronizeStandalone), standaloneSelected = selected.filter((port) => !!port.synchronizeStandalone), transformContext = Object.freeze({
	        secret: snapshot.config.password,
	        scopeId
	      });
	      let outcome = null, nextBaseline = snapshot.baselines[baselineScopeId] ?? Object.freeze({}), applyRecords = [];
	      regularSelected.length || (outcome = Object.freeze({
	        uploaded: 0,
	        imported: 0,
	        deleted: 0,
	        conflicts: 0,
	        categories: selected.length,
	        remoteCreated: !1,
	        at: this.#now()
	      }));
	      for (let attempt = 0; regularSelected.length && attempt < 3; attempt += 1) {
	        if (signal.aborted) throw signal.reason;
	        const remoteFile = await this.#client.read(snapshot.config, signal), document = remoteFile ? (0, import_reader_webdav_model.normalizeReaderWebDavDocument)(JSON.parse(remoteFile.text)) : (0, import_reader_webdav_model.createReaderWebDavDocument)(snapshot.writerId, this.#now()), storedRemoteScope = document.scopes[scopeId], remoteScope = storedRemoteScope ?? Object.freeze({
	          categories: Object.freeze({})
	        }), categories = { ...remoteScope.categories }, attemptBaseline = storedRemoteScope ? nextBaseline : Object.freeze({}), baseline = { ...attemptBaseline }, pendingApply = [];
	        let uploaded = 0, imported = 0, deleted = 0, conflicts = 0, changed = remoteFile === null && regularSelected.length > 0;
	        for (const port of regularSelected) {
	          const local = await port.capture();
	          if (port.validateRecord) {
	            for (const item of local)
	              if (!port.validateRecord(item.id, item.value, local))
	                throw new Error(
	                  `本机 WebDAV ${port.category} 记录 ${item.id} 身份不一致`
	                );
	          }
	          const remoteCategory = remoteScope.categories[port.category], remoteRecords = remoteCategory?.records ?? {}, decodedRemoteRecords = port.decodeRemoteRecords ? await port.decodeRemoteRecords(
	            remoteRecords,
	            transformContext
	          ) : remoteRecords, activeRemoteRecords = Object.freeze(
	            Object.entries(decodedRemoteRecords).filter(([, item]) => !item.deleted).map(([id, item]) => Object.freeze({
	              id,
	              value: item.value
	            }))
	          );
	          if (port.validateRecord) {
	            for (const item of activeRemoteRecords)
	              if (!port.validateRecord(
	                item.id,
	                item.value,
	                activeRemoteRecords
	              ))
	                throw new Error(
	                  `远端 WebDAV ${port.category} 记录 ${item.id} 身份不一致`
	                );
	          }
	          const reconciled = (0, import_reader_webdav_model.reconcileReaderWebDavRecords)({
	            local,
	            remote: decodedRemoteRecords,
	            ...remoteCategory === void 0 || attemptBaseline[port.category] === void 0 ? {} : { baseline: attemptBaseline[port.category] },
	            writerId: snapshot.writerId,
	            now: this.#now(),
	            initialStrategy: port.initialStrategy,
	            mergeValues: port.mergeValues
	          }), encodedRecords = reconciled.changed && port.encodeRemoteRecords ? await port.encodeRemoteRecords(
	            reconciled.records,
	            transformContext
	          ) : reconciled.changed ? reconciled.records : remoteRecords;
	          categories[port.category] = Object.freeze({
	            records: encodedRecords
	          }), baseline[port.category] = reconciled.baseline, pendingApply.push(Object.freeze({
	            port,
	            records: reconciled.active,
	            captured: local
	          })), changed ||= reconciled.changed, uploaded += reconciled.uploaded, imported += reconciled.imported, deleted += reconciled.deleted, conflicts += reconciled.conflicts;
	        }
	        const nextDocument = withScope(
	          document,
	          scopeId,
	          Object.freeze({ categories: Object.freeze(categories) }),
	          snapshot.writerId,
	          this.#now()
	        );
	        try {
	          changed && await this.#client.write(
	            snapshot.config,
	            JSON.stringify(nextDocument),
	            remoteFile?.etag ?? null,
	            signal
	          );
	          let localChangedDuringSync = !1;
	          for (const item of pendingApply) {
	            const current = await item.port.capture();
	            if (localFingerprint(current) !== localFingerprint(item.captured)) {
	              localChangedDuringSync = !0;
	              break;
	            }
	          }
	          if (localChangedDuringSync) {
	            if (attempt < 2) continue;
	            throw new Error(
	              "同步期间本地数据持续变化,已保留本机内容,请稍后重试"
	            );
	          }
	          nextBaseline = Object.freeze(baseline), applyRecords = Object.freeze(pendingApply), outcome = Object.freeze({
	            uploaded,
	            imported,
	            deleted,
	            conflicts,
	            categories: selected.length,
	            remoteCreated: remoteFile === null && changed,
	            at: this.#now()
	          });
	          break;
	        } catch (cause) {
	          if (cause instanceof import_reader_webdav_client.ReaderWebDavError && cause.code === "conflict") {
	            if (attempt < CONFLICT_RETRY_DELAYS_MS.length) {
	              await this.#retryDelay(
	                CONFLICT_RETRY_DELAYS_MS[attempt],
	                signal
	              );
	              continue;
	            }
	            throw new import_reader_webdav_client.ReaderWebDavError(
	              "conflict",
	              "WebDAV 远端版本持续变化,已保留本机数据;请稍后重试,并检查其他标签页或设备是否正在同步",
	              cause.status
	            );
	          }
	          throw cause;
	        }
	      }
	      if (!outcome) throw new Error("WebDAV 文件持续冲突,请稍后重试");
	      for (const item of applyRecords)
	        localFingerprint(item.records) !== localFingerprint(item.captured) && await item.port.apply(item.records);
	      regularSelected.length && await this.#repository.saveBaseline(baselineScopeId, nextBaseline);
	      let aggregate = outcome;
	      for (const port of standaloneSelected) {
	        const standalone = await port.synchronizeStandalone({
	          client: this.#client,
	          config: snapshot.config,
	          signal,
	          scopeId,
	          writerId: snapshot.writerId,
	          ...nextBaseline[port.category] === void 0 ? {} : { baseline: nextBaseline[port.category] },
	          now: this.#now,
	          retryDelay: this.#retryDelay
	        });
	        nextBaseline = Object.freeze({
	          ...nextBaseline,
	          [port.category]: standalone.baseline
	        }), await this.#repository.saveBaseline(baselineScopeId, nextBaseline), aggregate = Object.freeze({
	          ...aggregate,
	          uploaded: aggregate.uploaded + standalone.uploaded,
	          imported: aggregate.imported + standalone.imported,
	          deleted: aggregate.deleted + standalone.deleted,
	          conflicts: aggregate.conflicts + standalone.conflicts,
	          remoteCreated: aggregate.remoteCreated || standalone.remoteCreated,
	          at: this.#now()
	        });
	      }
	      const message = `同步完成:上传 ${aggregate.uploaded},下载 ${aggregate.imported},删除 ${aggregate.deleted},冲突 ${aggregate.conflicts}`;
	      return this.#repository.snapshot.config === synchronizedConfig && await this.#repository.saveStatus(Object.freeze({
	        kind: "success",
	        message,
	        at: aggregate.at
	      })), aggregate;
	    } catch (cause) {
	      throw (synchronizedConfig === null || this.#repository.snapshot.config === synchronizedConfig) && await this.#repository.saveStatus(Object.freeze({
	        kind: "error",
	        message: errorMessage(cause),
	        at: this.#now()
	      })), cause;
	    }
	  }
	}
	class ReaderWebDavAutoSync {
	  scope;
	  #repository;
	  #coordinator;
	  #visibilityState;
	  #startupDelayMs;
	  #schedule;
	  #cancel;
	  #handle = null;
	  #signature = "";
	  #first = !0;
	  constructor(options) {
	    this.#repository = options.repository, this.#coordinator = options.coordinator, this.#visibilityState = options.visibilityState, this.#startupDelayMs = Math.max(1e3, options.startupDelayMs ?? 3e4), this.#schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs)), this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle)), this.scope = import_lifecycle.LifecycleScope.ownedBy(options.parentScope), this.#repository.changes.subscribe(() => this.#refresh(), this.scope), this.scope.add(() => this.#clear()), this.#repository.load().then(() => this.#refresh()).catch(() => {
	    });
	  }
	  #refresh() {
	    if (this.scope.destroyed) return;
	    const config = this.#repository.snapshot.config, signature = `${config.autoSyncEnabled}:${config.autoSyncIntervalMinutes}`;
	    signature !== this.#signature && (this.#signature = signature, this.#clear(), config.autoSyncEnabled && (this.#arm(this.#first ? this.#startupDelayMs : config.autoSyncIntervalMinutes * 6e4), this.#first = !1));
	  }
	  #arm(delayMs) {
	    this.#handle = this.#schedule(() => {
	      this.#handle = null, this.#run();
	    }, delayMs);
	  }
	  async #run() {
	    if (!(this.scope.destroyed || !this.#repository.snapshot.config.autoSyncEnabled)) {
	      if (this.#visibilityState() === "visible")
	        try {
	          await this.#coordinator.syncNow();
	        } catch {
	        }
	      !this.scope.destroyed && this.#repository.snapshot.config.autoSyncEnabled && this.#arm(
	        this.#repository.snapshot.config.autoSyncIntervalMinutes * 6e4
	      );
	    }
	  }
	  #clear() {
	    this.#handle !== null && (this.#cancel(this.#handle), this.#handle = null);
	  }
	}
}, "804a922d868dbe22d7f7fe07f6e06d164aab77a5a6ef730e78a14da0831b1d8c");

/* Source: lite/src/sync/reader-webdav-history-cache-port.ts */
runtime.register("src/sync/reader-webdav-history-cache-port.js", function(module, exports, require) {
	var reader_webdav_history_cache_port_exports = {};
	__export(reader_webdav_history_cache_port_exports, {
	  createReaderWebDavHistoryCacheCategoryPort: () => createReaderWebDavHistoryCacheCategoryPort
	});
	module.exports = __toCommonJS(reader_webdav_history_cache_port_exports);
	var import_reader_webdav_client = require("./reader-webdav-client.js"), import_reader_webdav_model = require("./reader-webdav-model.js");
	const HISTORY_CACHE_MANIFEST_FORMAT = "awesome-linuxdo-reader-lite-history-cache", HISTORY_CACHE_MANIFEST_VERSION = 1, CONFLICT_RETRY_DELAYS_MS = Object.freeze([250, 750]);
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function normalizedRecordId(value) {
	  const source = String(value ?? "").trim();
	  return !source || source.length > 240 || /[\u0000-\u001f]/.test(source) ? "" : source;
	}
	function manifestPath(remotePath, scopeId, category) {
	  const normalized = (0, import_reader_webdav_model.normalizeReaderWebDavRemotePath)(remotePath);
	  if (!normalized) throw new Error("WebDAV 远端路径无效");
	  const directory = normalized.split("/").slice(0, -1).join("/"), scope = (0, import_reader_webdav_model.readerWebDavFingerprint)(scopeId);
	  return `${directory}/history-cache/${scope}/${category}.json`;
	}
	function normalizeManifest(value, category) {
	  const source = record(value);
	  if (source?.format !== HISTORY_CACHE_MANIFEST_FORMAT || source.schemaVersion !== HISTORY_CACHE_MANIFEST_VERSION || source.category !== category) throw new Error(`WebDAV ${category} 清单格式或版本不受支持`);
	  const rawRecords = record(source.records);
	  if (!rawRecords) throw new Error(`WebDAV ${category} 清单缺少 records`);
	  if (typeof source.updatedAt != "number" || !Number.isFinite(source.updatedAt) || source.updatedAt < 0 || typeof source.writerId != "string") throw new Error(`WebDAV ${category} 清单字段类型无效`);
	  const records = /* @__PURE__ */ Object.create(null);
	  for (const [rawId, rawValue] of Object.entries(rawRecords)) {
	    const id = normalizedRecordId(rawId);
	    if (!id || id !== rawId)
	      throw new Error(`WebDAV ${category} 记录 ID 无效`);
	    records[id] = (0, import_reader_webdav_model.normalizeReaderWebDavRemoteRecord)(rawValue);
	  }
	  return Object.freeze({
	    format: HISTORY_CACHE_MANIFEST_FORMAT,
	    schemaVersion: HISTORY_CACHE_MANIFEST_VERSION,
	    category,
	    updatedAt: source.updatedAt,
	    writerId: source.writerId,
	    records: Object.freeze(records)
	  });
	}
	function recordsFingerprint(records) {
	  return (0, import_reader_webdav_model.readerWebDavFingerprint)([...records].map((entry) => ({ id: entry.id, value: entry.value })).sort((left, right) => left.id.localeCompare(right.id)));
	}
	async function synchronizeHistoryCache(options, context) {
	  const path = manifestPath(
	    context.config.remotePath,
	    context.scopeId,
	    options.category
	  );
	  for (let attempt = 0; attempt < 3; attempt += 1) {
	    if (context.signal.aborted) throw context.signal.reason;
	    const local = Object.freeze([...await options.capture()]), localState = recordsFingerprint(local), remoteFile = await context.client.readObject(
	      context.config,
	      path,
	      context.signal
	    ), remote = remoteFile ? normalizeManifest(JSON.parse(remoteFile.text), options.category) : Object.freeze({
	      format: HISTORY_CACHE_MANIFEST_FORMAT,
	      schemaVersion: HISTORY_CACHE_MANIFEST_VERSION,
	      category: options.category,
	      updatedAt: 0,
	      writerId: "",
	      records: Object.freeze({})
	    });
	    for (const [id, item] of Object.entries(remote.records))
	      if (!item.deleted && !options.validateRecord(id, item.value))
	        throw new Error(
	          `WebDAV ${options.category} 记录 ${id} 身份不一致或格式无效`
	        );
	    for (const item of local)
	      if (!options.validateRecord(item.id, item.value))
	        throw new Error(`本机 WebDAV ${options.category} 记录 ${item.id} 格式无效`);
	    const reconciled = (0, import_reader_webdav_model.reconcileReaderWebDavRecords)({
	      local,
	      remote: remote.records,
	      writerId: context.writerId,
	      now: context.now(),
	      initialStrategy: "merge",
	      mergeValues: options.mergeValues
	    });
	    try {
	      if (reconciled.changed) {
	        const manifest = Object.freeze({
	          format: HISTORY_CACHE_MANIFEST_FORMAT,
	          schemaVersion: HISTORY_CACHE_MANIFEST_VERSION,
	          category: options.category,
	          updatedAt: context.now(),
	          writerId: context.writerId,
	          records: reconciled.records
	        });
	        await context.client.writeObject(
	          context.config,
	          path,
	          JSON.stringify(manifest),
	          remoteFile?.etag ?? null,
	          "application/json; charset=utf-8",
	          context.signal
	        );
	      }
	      const current = await options.capture();
	      if (recordsFingerprint(current) !== localState) {
	        if (attempt < 2) continue;
	        throw new Error(
	          "WebDAV 历史同步期间本地缓存持续变化,已保留本机内容,请稍后重试"
	        );
	      }
	      return recordsFingerprint(reconciled.active) !== localState && await options.apply(reconciled.active), Object.freeze({
	        // 该类别不传播删除,也不依赖三方基线。避免把每条历史指纹
	        // 再复制到 WebDAV 配置存储,历史增长不会拖大配置本身。
	        baseline: Object.freeze({}),
	        uploaded: reconciled.uploaded,
	        imported: reconciled.imported,
	        deleted: 0,
	        conflicts: reconciled.conflicts,
	        remoteCreated: remoteFile === null && reconciled.changed
	      });
	    } catch (cause) {
	      if (!(cause instanceof import_reader_webdav_client.ReaderWebDavError) || cause.code !== "conflict")
	        throw cause;
	      if (attempt < CONFLICT_RETRY_DELAYS_MS.length) {
	        await context.retryDelay(
	          CONFLICT_RETRY_DELAYS_MS[attempt],
	          context.signal
	        );
	        continue;
	      }
	      throw new import_reader_webdav_client.ReaderWebDavError(
	        "conflict",
	        `WebDAV ${options.category} 清单持续变化,已保留本机历史;请稍后重试,并检查其他标签页或设备是否正在同步`,
	        cause.status
	      );
	    }
	  }
	  throw new Error(`WebDAV ${options.category} 清单持续冲突,请稍后重试`);
	}
	function createReaderWebDavHistoryCacheCategoryPort(options) {
	  return Object.freeze({
	    category: options.category,
	    initialStrategy: "merge",
	    capture: options.capture,
	    mergeValues: options.mergeValues,
	    apply: options.apply,
	    synchronizeStandalone: (context) => synchronizeHistoryCache(options, context)
	  });
	}
}, "b626c922282b3df80ec672c0dcfb2f4ce4de6c36a0e5cad31fb6bc11a4e4b274");

/* Source: lite/src/sync/reader-webdav-model.ts */
runtime.register("src/sync/reader-webdav-model.js", function(module, exports, require) {
	var reader_webdav_model_exports = {};
	__export(reader_webdav_model_exports, {
	  READER_WEBDAV_CATEGORIES: () => READER_WEBDAV_CATEGORIES,
	  READER_WEBDAV_CATEGORY_LABELS: () => READER_WEBDAV_CATEGORY_LABELS,
	  READER_WEBDAV_DEFAULT_REMOTE_PATH: () => READER_WEBDAV_DEFAULT_REMOTE_PATH,
	  READER_WEBDAV_FORMAT: () => READER_WEBDAV_FORMAT,
	  READER_WEBDAV_SCHEMA_VERSION: () => READER_WEBDAV_SCHEMA_VERSION,
	  createReaderWebDavCategorySelection: () => createReaderWebDavCategorySelection,
	  createReaderWebDavDefaultConfig: () => createReaderWebDavDefaultConfig,
	  createReaderWebDavDocument: () => createReaderWebDavDocument,
	  normalizeReaderWebDavConfig: () => normalizeReaderWebDavConfig,
	  normalizeReaderWebDavDocument: () => normalizeReaderWebDavDocument,
	  normalizeReaderWebDavRemotePath: () => normalizeReaderWebDavRemotePath,
	  normalizeReaderWebDavRemoteRecord: () => normalizeReaderWebDavRemoteRecord,
	  readerWebDavFingerprint: () => readerWebDavFingerprint,
	  readerWebDavRuntimeScopeId: () => readerWebDavRuntimeScopeId,
	  reconcileReaderWebDavRecords: () => reconcileReaderWebDavRecords,
	  validateReaderWebDavConfig: () => validateReaderWebDavConfig
	});
	module.exports = __toCommonJS(reader_webdav_model_exports);
	const READER_WEBDAV_FORMAT = "awesome-linuxdo-reader-lite-webdav", READER_WEBDAV_SCHEMA_VERSION = 2, READER_WEBDAV_DEFAULT_REMOTE_PATH = "ALR-Lite/v2/sync.json", READER_WEBDAV_CATEGORIES = Object.freeze([
	  "history",
	  "bookmarks",
	  "notification-history",
	  "activity-history",
	  "preferences",
	  "queue",
	  "topic-context",
	  "custom-sites",
	  "connect-history",
	  "translation",
	  "translation-cache",
	  "offline-topics"
	]), READER_WEBDAV_CATEGORY_LABELS = Object.freeze({
	  history: "浏览历史",
	  bookmarks: "收藏记录",
	  "notification-history": "通知历史缓存",
	  "activity-history": "回复、Boost 与表情回应历史",
	  preferences: "设置配置",
	  queue: "阅读队列",
	  "topic-context": "阅读位置与窗口状态",
	  "custom-sites": "自定义适用站点",
	  "connect-history": "Connect 本机观察历史",
	  translation: "AI 服务集合(Key 加密)",
	  "translation-cache": "已翻译 Section 缓存",
	  "offline-topics": "离线 Topic 下载(HTML 正文)"
	}), AUTO_SYNC_INTERVALS = /* @__PURE__ */ new Set([15, 30, 60, 180, 360]), MISSING_STATE = "missing", DELETED_STATE = "deleted";
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function timestamp(value, label) {
	  if (typeof value != "number" || !Number.isFinite(value) || value < 0)
	    throw new Error(`${label} 必须是非负有限数值`);
	  return value;
	}
	function canonical(value) {
	  return value === null || typeof value != "object" ? JSON.stringify(value) ?? "null" : Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
	}
	function readerWebDavFingerprint(value) {
	  const source = canonical(value);
	  let left = 2166136261, right = 2654435769;
	  for (let index = 0; index < source.length; index += 1) {
	    const code = source.charCodeAt(index);
	    left = Math.imul(left ^ code, 16777619) >>> 0, right = Math.imul(right ^ code, 2246822507) >>> 0;
	  }
	  return `${left.toString(16).padStart(8, "0")}${right.toString(16).padStart(8, "0")}`;
	}
	function valueState(value) {
	  return `value:${readerWebDavFingerprint(value)}`;
	}
	function remoteState(value) {
	  return value ? value.deleted ? DELETED_STATE : valueState(value.value) : MISSING_STATE;
	}
	function normalizedRecordId(value) {
	  const source = String(value ?? "").trim();
	  return !source || source.length > 240 || /[\u0000-\u001f]/.test(source) ? "" : source;
	}
	function normalizeReaderWebDavRemotePath(value) {
	  const segments = String(value ?? "").trim().replace(/^\/+|\/+$/g, "").split("/").map((segment) => segment.trim());
	  return segments.length < 2 || segments.some((segment) => !segment || segment === "." || segment === ".." || segment.length > 80) ? "" : segments.join("/");
	}
	function normalizedEndpoint(value) {
	  try {
	    const url = new URL(String(value ?? "").trim());
	    return url.protocol !== "https:" || url.username || url.password || url.search || url.hash ? "" : (url.pathname = `${url.pathname.replace(/\/+$/g, "")}/`, url.href);
	  } catch {
	    return "";
	  }
	}
	function createReaderWebDavCategorySelection(value = null) {
	  const source = record(value);
	  return Object.freeze(Object.fromEntries(READER_WEBDAV_CATEGORIES.map(
	    (category) => [category, source ? source[category] === !0 : ["history", "bookmarks", "queue"].includes(category)]
	  )));
	}
	function createReaderWebDavDefaultConfig() {
	  return Object.freeze({
	    endpoint: "https://dav.jianguoyun.com/dav/",
	    username: "",
	    password: "",
	    remotePath: READER_WEBDAV_DEFAULT_REMOTE_PATH,
	    categories: createReaderWebDavCategorySelection(),
	    autoSyncEnabled: !1,
	    autoSyncIntervalMinutes: 60
	  });
	}
	function normalizeReaderWebDavConfig(value) {
	  const source = record(value);
	  if (!source) return createReaderWebDavDefaultConfig();
	  const interval = Number(source?.autoSyncIntervalMinutes);
	  return Object.freeze({
	    endpoint: normalizedEndpoint(source?.endpoint),
	    username: String(source?.username ?? "").trim(),
	    password: String(source?.password ?? ""),
	    remotePath: normalizeReaderWebDavRemotePath(source.remotePath),
	    categories: createReaderWebDavCategorySelection(source?.categories),
	    autoSyncEnabled: source?.autoSyncEnabled === !0,
	    autoSyncIntervalMinutes: AUTO_SYNC_INTERVALS.has(interval) ? interval : 60
	  });
	}
	function validateReaderWebDavConfig(value, options = {}) {
	  const issues = [];
	  return normalizedEndpoint(value.endpoint) || issues.push("WebDAV 地址必须是 HTTPS"), normalizeReaderWebDavRemotePath(value.remotePath) || issues.push("远端路径必须包含目录和文件名"), options.requireCredentials !== !1 && (value.username.trim() || issues.push("请填写 WebDAV 用户名"), value.password || issues.push("请填写 WebDAV 应用密码")), READER_WEBDAV_CATEGORIES.some((category) => value.categories[category]) || issues.push("至少选择一种同步内容"), Object.freeze(issues);
	}
	function normalizeReaderWebDavRemoteRecord(value) {
	  const source = record(value);
	  if (!source) throw new Error("WebDAV 远端记录必须是对象");
	  if (typeof source.deleted != "boolean")
	    throw new Error("WebDAV 远端记录 deleted 必须是布尔值");
	  if (typeof source.writerId != "string")
	    throw new Error("WebDAV 远端记录 writerId 必须是字符串");
	  const deleted = source.deleted;
	  if (!deleted && !Object.hasOwn(source, "value"))
	    throw new Error("WebDAV 活跃远端记录缺少 value");
	  return Object.freeze({
	    changedAt: timestamp(source.changedAt, "WebDAV 远端记录 changedAt"),
	    writerId: source.writerId,
	    deleted,
	    ...deleted ? {} : { value: source.value }
	  });
	}
	function normalizeRemoteCategory(value) {
	  const source = record(value);
	  if (!source) throw new Error("WebDAV 远端分类必须是对象");
	  const rawRecords = record(source?.records);
	  if (!rawRecords) throw new Error("WebDAV 远端分类缺少 records");
	  const records = /* @__PURE__ */ Object.create(null);
	  for (const [rawId, rawValue] of Object.entries(rawRecords)) {
	    const id = normalizedRecordId(rawId);
	    if (!id || id !== rawId) throw new Error("WebDAV 远端记录 ID 无效");
	    records[id] = normalizeReaderWebDavRemoteRecord(rawValue);
	  }
	  return Object.freeze({ records: Object.freeze(records) });
	}
	function createReaderWebDavDocument(writerId, now = Date.now()) {
	  return Object.freeze({
	    format: READER_WEBDAV_FORMAT,
	    schemaVersion: 2,
	    updatedAt: now,
	    writerId,
	    scopes: Object.freeze({})
	  });
	}
	function normalizeReaderWebDavDocument(value) {
	  const source = record(value);
	  if (source?.format !== READER_WEBDAV_FORMAT || source.schemaVersion !== 2) throw new Error("远端同步文件格式或版本不受支持");
	  const rawScopes = record(source.scopes);
	  if (!rawScopes) throw new Error("远端同步文件缺少 scopes");
	  const scopes = /* @__PURE__ */ Object.create(null);
	  for (const [rawScopeId, rawScope] of Object.entries(rawScopes)) {
	    const scopeId = normalizedRecordId(rawScopeId), scopeSource = record(rawScope), rawCategories = record(scopeSource?.categories);
	    if (!scopeId || scopeId !== rawScopeId || !rawCategories)
	      throw new Error("WebDAV 远端 scope 格式无效");
	    const categories = /* @__PURE__ */ Object.create(null);
	    for (const [rawCategory, rawCategoryValue] of Object.entries(
	      rawCategories
	    )) {
	      const category = normalizedRecordId(rawCategory);
	      if (!category || category !== rawCategory)
	        throw new Error("WebDAV 远端分类 ID 无效");
	      categories[category] = normalizeRemoteCategory(rawCategoryValue);
	    }
	    scopes[scopeId] = Object.freeze({
	      categories: Object.freeze(categories)
	    });
	  }
	  if (typeof source.writerId != "string")
	    throw new Error("WebDAV 远端 writerId 必须是字符串");
	  return Object.freeze({
	    format: READER_WEBDAV_FORMAT,
	    schemaVersion: 2,
	    updatedAt: timestamp(source.updatedAt, "WebDAV 远端 updatedAt"),
	    writerId: source.writerId,
	    scopes: Object.freeze(scopes)
	  });
	}
	function localRecords(value) {
	  const result = /* @__PURE__ */ new Map();
	  for (const item of value) {
	    const id = normalizedRecordId(item.id);
	    id && result.set(id, Object.freeze({ id, value: item.value }));
	  }
	  return result;
	}
	function reconcileReaderWebDavRecords(options) {
	  const local = localRecords(options.local), next = Object.assign(
	    /* @__PURE__ */ Object.create(null),
	    options.remote
	  ), ids = /* @__PURE__ */ new Set([
	    ...local.keys(),
	    ...Object.keys(options.remote),
	    ...Object.keys(options.baseline ?? {})
	  ]);
	  let uploaded = 0, imported = 0, deleted = 0, conflicts = 0;
	  for (const id of ids) {
	    const localItem = local.get(id), remoteItem = options.remote[id], baselineState = options.baseline?.[id], localState = localItem ? valueState(localItem.value) : baselineState === DELETED_STATE ? DELETED_STATE : MISSING_STATE, currentRemoteState = remoteState(remoteItem);
	    let chosen, mergedValue;
	    if (baselineState === void 0 ? remoteItem ? !localItem || remoteItem.deleted || options.initialStrategy === "remote" ? chosen = "remote" : (chosen = "merged", mergedValue = options.mergeValues(localItem.value, remoteItem.value)) : chosen = "local" : !remoteItem && localItem ? (chosen = "local", conflicts += 1) : localState !== baselineState ? currentRemoteState !== baselineState ? localState === currentRemoteState ? chosen = "remote" : localItem && remoteItem && !remoteItem.deleted ? (chosen = "merged", mergedValue = options.mergeValues(localItem.value, remoteItem.value), conflicts += 1) : (chosen = "remote", conflicts += 1) : chosen = "local" : chosen = "remote", chosen === "remote") {
	      currentRemoteState !== localState && (imported += 1), !remoteItem && baselineState !== void 0 && (next[id] = Object.freeze({
	        changedAt: options.now,
	        writerId: options.writerId,
	        deleted: !0
	      }), deleted += 1);
	      continue;
	    }
	    const value = chosen === "merged" ? mergedValue : localItem?.value;
	    if (value === void 0 && !localItem) {
	      next[id] = Object.freeze({
	        changedAt: options.now,
	        writerId: options.writerId,
	        deleted: !0
	      }), deleted += 1, uploaded += 1;
	      continue;
	    }
	    remoteState(remoteItem) !== valueState(value) && (next[id] = Object.freeze({
	      changedAt: options.now,
	      writerId: options.writerId,
	      deleted: !1,
	      value
	    }), uploaded += 1), chosen === "merged" && valueState(value) !== localState && (imported += 1);
	  }
	  const active = Object.freeze(Object.entries(next).filter(([, item]) => !item.deleted).map(([id, item]) => Object.freeze({ id, value: item.value })).sort((left, right) => left.id.localeCompare(right.id))), baseline = Object.freeze(Object.fromEntries(Object.entries(next).map(
	    ([id, item]) => [id, remoteState(item)]
	  )));
	  return Object.freeze({
	    records: Object.freeze(next),
	    active,
	    baseline,
	    changed: canonical(next) !== canonical(options.remote),
	    uploaded,
	    imported,
	    deleted,
	    conflicts
	  });
	}
	function readerWebDavRuntimeScopeId(hostname, username) {
	  const host = String(hostname ?? "").trim().toLowerCase(), user = String(username ?? "").trim().toLowerCase();
	  if (!host) throw new Error("当前站点身份不可用");
	  if (!user) throw new Error("当前登录账号尚未就绪,请稍后重试");
	  return `site:${host}|account:${user}`;
	}
}, "a812872096a68697e30587bb0fe712b783e666a73f0bafa267eca4ad8326d461");

/* Source: lite/src/sync/reader-webdav-offline-topic-port.ts */
runtime.register("src/sync/reader-webdav-offline-topic-port.js", function(module, exports, require) {
	var reader_webdav_offline_topic_port_exports = {};
	__export(reader_webdav_offline_topic_port_exports, {
	  createReaderWebDavOfflineTopicCategoryPort: () => createReaderWebDavOfflineTopicCategoryPort
	});
	module.exports = __toCommonJS(reader_webdav_offline_topic_port_exports);
	var import_reader_webdav_client = require("./reader-webdav-client.js"), import_reader_webdav_model = require("./reader-webdav-model.js");
	const OFFLINE_TOPIC_MANIFEST_FORMAT = "awesome-linuxdo-reader-lite-offline-topics", OFFLINE_TOPIC_MANIFEST_VERSION = 1, CONFLICT_RETRY_DELAYS_MS = Object.freeze([250, 750]);
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function timestamp(value) {
	  const numeric = Number(value);
	  return Number.isFinite(numeric) && numeric >= 0 ? numeric : 0;
	}
	function topicId(value) {
	  const numeric = Number(value);
	  return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : null;
	}
	function selectionMode(value) {
	  return value === "op" || value === "custom" ? value : "all";
	}
	function archiveStatus(value) {
	  const status = Number(value);
	  return status === 403 || status === 404 || status === 410 ? status : null;
	}
	function metadata(value) {
	  const source = record(value), id = topicId(source?.topicId);
	  if (!source || !id) return null;
	  const mode = selectionMode(source.selectionMode);
	  return Object.freeze({
	    topicId: id,
	    title: String(source.title || `Topic #${id}`),
	    selectionMode: mode,
	    selectionExpression: mode === "custom" ? String(source.selectionExpression ?? "") : "",
	    filename: String(
	      source.filename || `topic-${id}-lite-offline.html`
	    ),
	    postCount: Math.max(0, Math.floor(Number(source.postCount) || 0)),
	    expectedPostCount: Math.max(
	      0,
	      Math.floor(Number(source.expectedPostCount) || 0)
	    ),
	    complete: source.complete === !0,
	    archiveStatus: archiveStatus(source.archiveStatus),
	    createdAt: timestamp(source.createdAt),
	    finishedAt: timestamp(source.finishedAt),
	    localDownloadRequestedAt: timestamp(source.localDownloadRequestedAt)
	  });
	}
	function artifact(value) {
	  const source = record(value), valueMetadata = metadata(source), html = typeof source?.html == "string" ? source.html : "";
	  return !valueMetadata || !html ? null : Object.freeze({ ...valueMetadata, html });
	}
	function localRecord(value) {
	  return Object.freeze({ id: String(value.topicId), value });
	}
	function manifestDirectory(remotePath, scopeId) {
	  const normalized = (0, import_reader_webdav_model.normalizeReaderWebDavRemotePath)(remotePath);
	  if (!normalized) throw new Error("WebDAV 远端路径无效");
	  const directory = normalized.split("/").slice(0, -1).join("/"), scope = (0, import_reader_webdav_model.readerWebDavFingerprint)(scopeId);
	  return `${directory}/offline-topics/${scope}`;
	}
	function manifestPath(remotePath, scopeId) {
	  return `${manifestDirectory(remotePath, scopeId)}/manifest.json`;
	}
	function objectPath(remotePath, scopeId, artifactTopicId, sha2562) {
	  return `${manifestDirectory(remotePath, scopeId)}/objects/${artifactTopicId}-${sha2562}.html`;
	}
	async function sha256(value) {
	  const subtle = globalThis.crypto?.subtle;
	  if (!subtle) throw new Error("当前环境不支持离线 HTML SHA-256 校验");
	  const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
	  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
	}
	function byteLength(value) {
	  return new TextEncoder().encode(value).byteLength;
	}
	function normalizeManifest(value) {
	  const source = record(value);
	  if (source?.format !== OFFLINE_TOPIC_MANIFEST_FORMAT || source.schemaVersion !== OFFLINE_TOPIC_MANIFEST_VERSION) throw new Error("WebDAV 离线 Topic 清单格式或版本不受支持");
	  const rawRecords = record(source.records);
	  if (!rawRecords) throw new Error("WebDAV 离线 Topic 清单缺少 records");
	  if (typeof source.updatedAt != "number" || !Number.isFinite(source.updatedAt) || source.updatedAt < 0 || typeof source.writerId != "string") throw new Error("WebDAV 离线 Topic 清单字段类型无效");
	  const records = {};
	  for (const [id, rawValue] of Object.entries(rawRecords)) {
	    if (!topicId(id) || String(topicId(id)) !== id)
	      throw new Error("WebDAV 离线 Topic 清单记录 ID 无效");
	    records[id] = (0, import_reader_webdav_model.normalizeReaderWebDavRemoteRecord)(rawValue);
	  }
	  return Object.freeze({
	    format: OFFLINE_TOPIC_MANIFEST_FORMAT,
	    schemaVersion: OFFLINE_TOPIC_MANIFEST_VERSION,
	    updatedAt: source.updatedAt,
	    writerId: source.writerId,
	    records: Object.freeze(records)
	  });
	}
	function remoteValue(value, context) {
	  const source = record(value), valueMetadata = metadata(source), object = record(source?.object), digest = String(object?.sha256 ?? ""), bytes = Number(object?.bytes), path = (0, import_reader_webdav_model.normalizeReaderWebDavRemotePath)(object?.path);
	  if (source?.version !== 1 || !valueMetadata || !/^[a-f0-9]{64}$/.test(digest) || !Number.isSafeInteger(bytes) || bytes < 1 || !path || path !== objectPath(
	    context.config.remotePath,
	    context.scopeId,
	    valueMetadata.topicId,
	    digest
	  )) return null;
	  const normalized = Object.freeze({
	    ...valueMetadata,
	    version: 1,
	    object: Object.freeze({ path, sha256: digest, bytes })
	  }), { archiveStatus: _archiveStatus, ...legacyNormalized } = normalized, canonical = Object.hasOwn(source, "archiveStatus") ? normalized : Object.freeze(legacyNormalized);
	  return (0, import_reader_webdav_model.readerWebDavFingerprint)(source) === (0, import_reader_webdav_model.readerWebDavFingerprint)(canonical) ? normalized : null;
	}
	async function captureArtifacts(store) {
	  const result = [];
	  for (const entry of await store.list()) {
	    const value = await store.read(entry.topicId);
	    value && result.push(localRecord(value));
	  }
	  return Object.freeze(result.sort((left, right) => Number(left.id) - Number(right.id)));
	}
	function recordsFingerprint(records) {
	  return (0, import_reader_webdav_model.readerWebDavFingerprint)([...records].sort((left, right) => left.id.localeCompare(right.id)));
	}
	async function decodeRemoteRecords(records, local, context) {
	  const localById = new Map(local.map((entry) => [entry.id, artifact(entry.value)])), entries = [];
	  for (const [id, item] of Object.entries(records)) {
	    if (item.deleted) {
	      entries.push([id, item]);
	      continue;
	    }
	    const reference = remoteValue(item.value, context);
	    if (!reference)
	      throw new Error(`WebDAV 离线 Topic #${id} 清单记录无效`);
	    if (String(reference.topicId) !== id)
	      throw new Error(`WebDAV 离线 Topic #${id} 清单记录身份不一致`);
	    const localArtifact = localById.get(id);
	    let html = "";
	    if (localArtifact && byteLength(localArtifact.html) === reference.object.bytes && await sha256(localArtifact.html) === reference.object.sha256)
	      html = localArtifact.html;
	    else {
	      const object = await context.client.readObject(
	        context.config,
	        reference.object.path,
	        context.signal
	      );
	      if (!object) throw new Error(
	        `WebDAV 离线 Topic #${id} HTML 对象不存在`
	      );
	      if (html = object.text, byteLength(html) !== reference.object.bytes || await sha256(html) !== reference.object.sha256) throw new Error(
	        `WebDAV 离线 Topic #${id} HTML 完整性校验失败`
	      );
	    }
	    const value = artifact({ ...reference, html });
	    if (!value) throw new Error(`WebDAV 离线 Topic #${id} 正文无效`);
	    entries.push([id, Object.freeze({ ...item, value })]);
	  }
	  return Object.freeze(Object.fromEntries(entries));
	}
	async function ensureObject(value, path, digest, bytes, context) {
	  try {
	    await context.client.writeObject(
	      context.config,
	      path,
	      value.html,
	      null,
	      "text/html; charset=utf-8",
	      context.signal
	    );
	    return;
	  } catch (cause) {
	    if (!(cause instanceof import_reader_webdav_client.ReaderWebDavError) || cause.code !== "conflict")
	      throw cause;
	  }
	  const existing = await context.client.readObject(
	    context.config,
	    path,
	    context.signal
	  );
	  if (!existing || byteLength(existing.text) !== bytes || await sha256(existing.text) !== digest) throw new Error(`WebDAV 离线 Topic #${value.topicId} 已有对象校验失败`);
	}
	async function encodeRemoteRecords(records, previous, context) {
	  const entries = [];
	  for (const [id, item] of Object.entries(records)) {
	    if (item.deleted) {
	      entries.push([id, item]);
	      continue;
	    }
	    const value = artifact(item.value);
	    if (!value) throw new Error(`本机离线 Topic #${id} 正文无效`);
	    const digest = await sha256(value.html), bytes = byteLength(value.html), path = objectPath(
	      context.config.remotePath,
	      context.scopeId,
	      value.topicId,
	      digest
	    ), previousValue = previous[id]?.deleted ? null : remoteValue(previous[id]?.value, context);
	    (previousValue?.object.path !== path || previousValue.object.sha256 !== digest || previousValue.object.bytes !== bytes) && await ensureObject(value, path, digest, bytes, context);
	    const { html: _html, ...valueMetadata } = value;
	    entries.push([id, Object.freeze({
	      ...item,
	      value: Object.freeze({
	        ...valueMetadata,
	        version: 1,
	        object: Object.freeze({ path, sha256: digest, bytes })
	      })
	    })]);
	  }
	  return Object.freeze(Object.fromEntries(entries));
	}
	function mergeArtifacts(local, remote) {
	  const left = artifact(local), right = artifact(remote);
	  return left ? right ? left.finishedAt !== right.finishedAt ? left.finishedAt > right.finishedAt ? left : right : left.complete !== right.complete ? left.complete ? left : right : left.postCount !== right.postCount ? left.postCount > right.postCount ? left : right : (0, import_reader_webdav_model.readerWebDavFingerprint)(left) >= (0, import_reader_webdav_model.readerWebDavFingerprint)(right) ? left : right : local : remote;
	}
	async function applyArtifacts(store, records) {
	  const incoming = /* @__PURE__ */ new Map();
	  for (const entry of records) {
	    const value = artifact(entry.value);
	    value && incoming.set(value.topicId, value);
	  }
	  for (const [id, value] of incoming) {
	    const current = await store.read(id);
	    current && (0, import_reader_webdav_model.readerWebDavFingerprint)(current) === (0, import_reader_webdav_model.readerWebDavFingerprint)(value) || await store.write(value);
	  }
	  for (const entry of await store.list())
	    incoming.has(entry.topicId) || await store.remove(entry.topicId);
	}
	async function synchronizeStandalone(store, context) {
	  const path = manifestPath(context.config.remotePath, context.scopeId);
	  for (let attempt = 0; attempt < 3; attempt += 1) {
	    context.signal.throwIfAborted();
	    const captured = await captureArtifacts(store), capturedFingerprint = recordsFingerprint(captured), remoteFile = await context.client.readObject(
	      context.config,
	      path,
	      context.signal
	    ), manifest = remoteFile ? normalizeManifest(JSON.parse(remoteFile.text)) : Object.freeze({
	      format: OFFLINE_TOPIC_MANIFEST_FORMAT,
	      schemaVersion: OFFLINE_TOPIC_MANIFEST_VERSION,
	      updatedAt: 0,
	      writerId: "",
	      records: Object.freeze({})
	    }), decoded = await decodeRemoteRecords(
	      manifest.records,
	      captured,
	      context
	    ), reconciled = (0, import_reader_webdav_model.reconcileReaderWebDavRecords)({
	      local: captured,
	      remote: decoded,
	      ...context.baseline === void 0 ? {} : { baseline: context.baseline },
	      writerId: context.writerId,
	      now: context.now(),
	      initialStrategy: "merge",
	      mergeValues: mergeArtifacts
	    }), encoded = reconciled.changed ? await encodeRemoteRecords(
	      reconciled.records,
	      manifest.records,
	      context
	    ) : manifest.records;
	    try {
	      reconciled.changed && await context.client.writeObject(
	        context.config,
	        path,
	        JSON.stringify(Object.freeze({
	          format: OFFLINE_TOPIC_MANIFEST_FORMAT,
	          schemaVersion: OFFLINE_TOPIC_MANIFEST_VERSION,
	          updatedAt: context.now(),
	          writerId: context.writerId,
	          records: encoded
	        })),
	        remoteFile?.etag ?? null,
	        "application/json; charset=utf-8",
	        context.signal
	      );
	      const current = await captureArtifacts(store);
	      if (recordsFingerprint(current) !== capturedFingerprint) {
	        if (attempt < 2) continue;
	        throw new Error(
	          "离线 Topic 同步期间本地下载持续变化,已保留本机内容,请稍后重试"
	        );
	      }
	      return await applyArtifacts(store, reconciled.active), Object.freeze({
	        baseline: reconciled.baseline,
	        uploaded: reconciled.uploaded,
	        imported: reconciled.imported,
	        deleted: reconciled.deleted,
	        conflicts: reconciled.conflicts,
	        remoteCreated: remoteFile === null && reconciled.changed
	      });
	    } catch (cause) {
	      if (cause instanceof import_reader_webdav_client.ReaderWebDavError && cause.code === "conflict") {
	        if (attempt < CONFLICT_RETRY_DELAYS_MS.length) {
	          await context.retryDelay(
	            CONFLICT_RETRY_DELAYS_MS[attempt],
	            context.signal
	          );
	          continue;
	        }
	        throw new import_reader_webdav_client.ReaderWebDavError(
	          "conflict",
	          "WebDAV 离线 Topic 清单持续变化,已保留本机下载;请稍后重试,并检查其他标签页或设备是否正在同步",
	          cause.status
	        );
	      }
	      throw cause;
	    }
	  }
	  throw new Error("WebDAV 离线 Topic 清单持续冲突,请稍后重试");
	}
	function createReaderWebDavOfflineTopicCategoryPort(store) {
	  return Object.freeze({
	    category: "offline-topics",
	    initialStrategy: "merge",
	    capture: () => [],
	    mergeValues: (local) => local,
	    apply: () => {
	    },
	    synchronizeStandalone: (context) => synchronizeStandalone(store, context)
	  });
	}
}, "3780054a84e84f3719406b93c4357e2faa560b5c17e1a0dd3d113feb34543a34");

/* Source: lite/src/sync/reader-webdav-secret-codec.ts */
runtime.register("src/sync/reader-webdav-secret-codec.js", function(module, exports, require) {
	var reader_webdav_secret_codec_exports = {};
	__export(reader_webdav_secret_codec_exports, {
	  decryptReaderWebDavSecret: () => decryptReaderWebDavSecret,
	  encryptReaderWebDavSecret: () => encryptReaderWebDavSecret,
	  readerWebDavEncryptedSecretMatchesSchema: () => readerWebDavEncryptedSecretMatchesSchema
	});
	module.exports = __toCommonJS(reader_webdav_secret_codec_exports);
	const READER_WEBDAV_SECRET_FORMAT = "awesome-linuxdo-reader-lite-aes-gcm";
	function record(value) {
	  return value !== null && typeof value == "object" && !Array.isArray(value) ? value : null;
	}
	function cryptoPort() {
	  const crypto = globalThis.crypto;
	  if (!crypto?.subtle || !crypto.getRandomValues)
	    throw new Error("浏览器缺少 Web Crypto,无法加密 WebDAV 翻译设置");
	  return crypto;
	}
	function base64Url(bytes) {
	  let binary = "";
	  for (const byte of bytes) binary += String.fromCharCode(byte);
	  return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
	}
	function fromBase64Url(value, maximum) {
	  if (typeof value != "string")
	    throw new Error("WebDAV 加密载荷类型无效");
	  const source = value;
	  if (!source || !/^[A-Za-z0-9_-]+$/u.test(source) || source.length > Math.ceil(maximum * 4 / 3) + 4)
	    throw new Error("WebDAV 加密载荷长度无效");
	  const base64 = source.replaceAll("-", "+").replaceAll("_", "/"), padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="), binary = atob(padded);
	  if (binary.length > maximum) throw new Error("WebDAV 加密载荷超过安全上限");
	  const bytes = new Uint8Array(binary.length);
	  for (let index = 0; index < binary.length; index += 1)
	    bytes[index] = binary.charCodeAt(index);
	  return bytes;
	}
	function readerWebDavEncryptedSecretMatchesSchema(value) {
	  const source = record(value);
	  if (!source || Object.keys(source).length !== 8 || ![
	    "format",
	    "version",
	    "kdf",
	    "iterations",
	    "salt",
	    "cipher",
	    "iv",
	    "ciphertext"
	  ].every((key) => Object.hasOwn(source, key)) || source.format !== READER_WEBDAV_SECRET_FORMAT || source.version !== 1 || source.kdf !== "PBKDF2-SHA-256" || source.cipher !== "AES-256-GCM" || typeof source.iterations != "number" || !Number.isSafeInteger(source.iterations) || source.iterations < 1e5 || source.iterations > 1e6) return !1;
	  try {
	    const salt = fromBase64Url(source.salt, 16), iv = fromBase64Url(source.iv, 12), ciphertext = fromBase64Url(
	      source.ciphertext,
	      1048576
	    );
	    return salt.length === 16 && iv.length === 12 && ciphertext.length >= 16 && base64Url(salt) === source.salt && base64Url(iv) === source.iv && base64Url(ciphertext) === source.ciphertext;
	  } catch {
	    return !1;
	  }
	}
	async function encryptionKey(secret, salt, iterations, usage) {
	  if (!secret) throw new Error("WebDAV 应用密码为空,无法加密翻译设置");
	  const crypto = cryptoPort(), material = await crypto.subtle.importKey(
	    "raw",
	    new TextEncoder().encode(secret),
	    "PBKDF2",
	    !1,
	    ["deriveKey"]
	  );
	  return crypto.subtle.deriveKey(
	    {
	      name: "PBKDF2",
	      hash: "SHA-256",
	      salt,
	      iterations
	    },
	    material,
	    { name: "AES-GCM", length: 256 },
	    !1,
	    [usage]
	  );
	}
	async function encryptReaderWebDavSecret(value, secret, associatedData) {
	  const crypto = cryptoPort(), salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12)), key = await encryptionKey(secret, salt, 21e4, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt(
	    {
	      name: "AES-GCM",
	      iv,
	      additionalData: new TextEncoder().encode(associatedData)
	    },
	    key,
	    new TextEncoder().encode(JSON.stringify(value))
	  ));
	  if (ciphertext.length > 1048576)
	    throw new Error("翻译 API Key 加密后超过 WebDAV 安全上限");
	  return Object.freeze({
	    format: READER_WEBDAV_SECRET_FORMAT,
	    version: 1,
	    kdf: "PBKDF2-SHA-256",
	    iterations: 21e4,
	    salt: base64Url(salt),
	    cipher: "AES-256-GCM",
	    iv: base64Url(iv),
	    ciphertext: base64Url(ciphertext)
	  });
	}
	async function decryptReaderWebDavSecret(value, secret, associatedData) {
	  try {
	    if (!readerWebDavEncryptedSecretMatchesSchema(value))
	      throw new Error("unsupported envelope");
	    const source = value, iterations = source.iterations, salt = fromBase64Url(source.salt, 16), iv = fromBase64Url(source.iv, 12);
	    if (salt.length !== 16 || iv.length !== 12)
	      throw new Error("invalid nonce");
	    const ciphertext = fromBase64Url(
	      source.ciphertext,
	      1048576
	    ), key = await encryptionKey(secret, salt, iterations, "decrypt"), plaintext = await cryptoPort().subtle.decrypt(
	      {
	        name: "AES-GCM",
	        iv,
	        additionalData: new TextEncoder().encode(associatedData)
	      },
	      key,
	      ciphertext
	    );
	    return JSON.parse(new TextDecoder().decode(plaintext));
	  } catch {
	    throw new Error(
	      "WebDAV 翻译 API Key 解密失败;请确认应用密码与加密时一致"
	    );
	  }
	}
}, "e6fb2aa401af8addb0db5eca34d0e7fa22c3b8a25d4df2f1592660cd818a670a");

	runtime.markLibrary("main-lite-platform");
})();