Awesome LinuxDo Reader Lite Platform Library

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

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/591595/1905253/Awesome%20LinuxDo%20Reader%20Lite%20Platform%20Library.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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.6
// @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.6 - 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.6",
			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.6") {
		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_reader_chronicle_repository = require("../history/reader-chronicle-repository.js"), import_lifecycle = require("../kernel/lifecycle.js"), import_reader_settings_dom = require("../settings/reader-settings-dom.js");
	const CATEGORIES = Object.freeze([
	  {
	    id: "history",
	    title: "浏览历史与岁月史书",
	    help: "勾选“浏览历史与岁月史书”后再点下方清理,会删除阅读器保存的主题、最近阅读楼层、查看时间和岁月史书删除/不可用记录;不会删除浏览器本身的访问历史,也不会因收到失效信号自动触发清理。",
	    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) => (0, import_reader_chronicle_repository.readerChronicleRecord)(entry) !== null);
	    return withApplicationCacheDetail(
	      `${topicIds.size} 个主题 · ${history.length - chronicle.length} 条浏览记录 · ${chronicle.length} 条失效记录 · ${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);
	  }
	}
}, "f78298bd3cb1be0e04c52c89d01440aa85c7e485ea4b0bf2990b46172e0bba93");

/* 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),
	      ...manifest.recordVersion === void 0 ? {} : { recordVersion: manifest.recordVersion },
	      ...manifest.sourceTotalHint === void 0 ? {} : { sourceTotalHint: manifest.sourceTotalHint },
	      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 requestedRecordVersion = options.recordVersion === void 0 ? previousManifest?.recordVersion : positiveSafeInteger(options.recordVersion);
	    if (requestedRecordVersion === null)
	      throw new RangeError("集合投影 recordVersion 必须是正安全整数");
	    const requestedSourceTotalHint = options.sourceTotalHint === void 0 ? previousManifest?.sourceTotalHint : safeInteger(options.sourceTotalHint);
	    if (requestedSourceTotalHint === null)
	      throw new RangeError("集合投影 sourceTotalHint 必须是非负安全整数");
	    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)
	        ),
	        ...requestedRecordVersion === void 0 ? {} : { recordVersion: requestedRecordVersion },
	        ...requestedSourceTotalHint === void 0 ? {} : { sourceTotalHint: requestedSourceTotalHint },
	        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), sourceTotalHint = source.sourceTotalHint === void 0 ? void 0 : safeInteger(source.sourceTotalHint), recordVersion = source.recordVersion === void 0 ? void 0 : positiveSafeInteger(source.recordVersion);
	    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 || recordVersion === null || sourceTotalHint === 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),
	      ...recordVersion === void 0 ? {} : { recordVersion },
	      ...sourceTotalHint === void 0 ? {} : { sourceTotalHint },
	      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 } : {}
	    });
	  }
	}
}, "35a404a1e681cc90f584c248acfde2e77c19985f209743c97e0a2ab29d312d69");

/* 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.deleted_at !== null && incomingRecord.deleted_at !== void 0 || incomingRecord.deletedAt !== null && incomingRecord.deletedAt !== void 0 || 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();
	    });
	  }
	}
}, "4191d5f0aa579070c51658fd34a3cf1a3282be259d952a99d24efbb012f1fae4");

/* 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.#session && 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 已销毁");
	  }
	}
}, "15a15d4ab9fbacbed02df246d9066f3d29e78cbcf408dc595536d1564fb0aa00");

/* 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 nativeNotificationTitleLabel(host, data) {
	  const key = String(data.title ?? "").trim();
	  if (!key) return "";
	  try {
	    const module2 = (0, import_value_record.objectRecord)(host.lookupModule("discourse-i18n")), i18n = (0, import_value_record.objectRecord)(module2?.default ?? module2?.I18n ?? module2), translate = i18n?.t;
	    if (typeof translate != "function") return "";
	    const label = nativePresentationText(translate.call(i18n, key));
	    return label && label !== key && !label.startsWith("[missing ") ? label : "";
	  } catch {
	    return "";
	  }
	}
	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.valueRecord)(
	      notificationModule?.default ?? notificationModule
	    ), 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 = Object.freeze({
	        ...nativeNotificationData(notification),
	        ...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), linkTitle = nativePresentationText(director.linkTitle), translatedTitle = nativeNotificationTitleLabel(
	          this.#host,
	          modelData
	        );
	        return Object.freeze({
	          actor: modelData.display_username ?? modelData.username ?? nativeModelValue(model, "username"),
	          typeName: resolvedType,
	          typeLabel: (linkTitle && linkTitle !== resolvedType ? linkTitle : "") || translatedTitle || 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"],
	        ["composer:created-post", "replies"],
	        ["post:created", "replies"]
	      ]), attached = [], pending = /* @__PURE__ */ new Set();
	      let publishScheduled = !1, active = !0;
	      const publish = () => {
	        if (publishScheduled = !1, !active) return;
	        const sources = [...pending];
	        pending.clear();
	        for (const source of sources) listener(source);
	      };
	      for (const [name, source] of subscriptions) {
	        const handler = () => {
	          pending.add(source), !publishScheduled && (publishScheduled = !0, Promise.resolve().then(publish));
	        };
	        try {
	          on.call(appEvents, name, context, handler), attached.push(Object.freeze({ name, handler }));
	        } catch {
	        }
	      }
	      return () => {
	        if (active) {
	          active = !1, pending.clear();
	          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;
	  }
	}
}, "53f3dcccd3da5ae08525cb2bf7c72d015613508e86c7938810afd882594b7135");

/* 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"
	]), COMPOSER_TOP_LAYER_FALLBACK_CLASS = "ldp-composer-top-layer-fallback", 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;
	  #managed = !1;
	  #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.#managed = !0, this.#activate(), !0) : (this.#deactivate(), !1);
	  }
	  sync() {
	    if (this.#destroyed) return;
	    const candidate = this.#document.querySelector("#reply-control"), managed = this.#managed;
	    if (candidate !== this.#composer && (this.#bindComposer(candidate), this.#managed = managed), !!this.#managed) {
	      if (!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 (!this.#managed || !composer || !this.#composerAvailable()) return !1;
	    try {
	      const style = this.#window.getComputedStyle?.(composer);
	      if (style?.display === "none" || style?.visibility === "hidden")
	        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.#managed = !1, 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.#pageRoot.classList.add(COMPOSER_TOP_LAYER_FALLBACK_CLASS), this.#releaseFloatingTopLayers();
	      return;
	    }
	    this.#pageRoot.classList.remove(COMPOSER_TOP_LAYER_FALLBACK_CLASS), 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.#pageRoot.classList.remove(COMPOSER_TOP_LAYER_FALLBACK_CLASS), 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;
	    }
	  }
	}
}, "3512a7c5d26fb9ad519f866ba8816c86db4e4929df4ee374daa361483e280fce");

/* 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,
	  readerChronicleHttpStatus: () => readerChronicleHttpStatus,
	  readerChronicleRecord: () => readerChronicleRecord,
	  readerChronicleRequestTarget: () => readerChronicleRequestTarget,
	  readerChronicleStatus: () => readerChronicleStatus
	});
	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 readerChronicleStatus(value) {
	  if (value === "deleted") return "deleted";
	  const numeric = Number(value);
	  return numeric === 403 || numeric === 404 || numeric === 410 ? numeric : null;
	}
	function readerChronicleHttpStatus(value) {
	  const status = readerChronicleStatus(value);
	  return status === 403 || status === 404 || status === 410 ? status : 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}`,
	    input.status === "deleted" ? "已删除 deleted" : String(input.status),
	    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), status = readerChronicleStatus(source?.status);
	  if (!source || !targetKind || topicId === null || status === null || 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,
	    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("岁月史书记录类型无效");
	  const status = readerChronicleStatus(input.status ?? 404);
	  if (status === null) throw new Error("岁月史书只接受删除或 403/404/410 信号");
	  if (input.bodyCached !== !0)
	    throw new Error("岁月史书只接受本机仍有可定位内容的失效记录");
	  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("回复失效记录必须包含楼层或 post.id");
	  if (targetKind === "boost" && boostId === null)
	    throw new Error("Boost 失效记录必须包含 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,
	    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 }));
	  }
	}
}, "287aeafba012502a3483ab02d2fa9cb842c8a1fa6ed58eafe5e0769a70d634ba");

/* 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) {
	  const status = record.status === "deleted" ? "已删除" : String(record.status);
	  return record.kind === "topic" ? `主题 · ${status}` : record.kind === "reply" ? `回复 #${record.postNumber ?? "?"} · ${status}` : `Boost #${record.boostId ?? "?"} · ${status}`;
	}
	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: "岁月史书失效记录搜集栏",
	      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 = "搜索主题、定位、删除或状态", 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 ? "没有匹配的失效记录。" : "还没有收到可定位的 Topic、回复或 Boost 删除/不可用信号。"
	    )), 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",
	      "",
	      "岁月史书只保留本机仍有可定位内容的删除或 403/404/410 记录。"
	    )), 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("该失效记录暂时无法打开");
	    });
	  }
	  #showMore() {
	    const total = this.#filteredRecords().length;
	    this.#visibleLimit >= total || (this.#visibleLimit = Math.min(total, this.#visibleLimit + CHRONICLE_BATCH_SIZE), this.#render());
	  }
	}
}, "58cdf35a9d5b64f5650eac7ee404580eaee1501b70eb5abb1ebe4e5c517ad692");

/* 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, channel = null, stopped = !1;
	  const onBroadcastMessage = (event) => {
	    const message = event.data;
	    message?.schemaVersion === 1 && message.type === "updated" && check();
	  }, stop = () => {
	    stopped || (stopped = !0, timer !== null && cancel(timer), timer = null, options.storageEvents?.removeEventListener("storage", onStorage), channel?.removeEventListener("message", onBroadcastMessage), channel?.close(), channel = null);
	  }, 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();
	  };
	  options.storageEvents?.addEventListener("storage", onStorage);
	  try {
	    channel = options.broadcastChannelFactory?.(
	      READER_REQUEST_PERMIT_CHANNEL
	    ) ?? null, channel?.addEventListener("message", onBroadcastMessage);
	  } catch (error) {
	    onError(error), channel = null;
	  }
	  return 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();
	  #stateChangeListeners = /* @__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.#stateChangeListeners.clear(), this.#wake(), this.#removeOwnedState();
	    });
	  }
	  subscribeStateChanges(listener) {
	    return this.#assertOpen(), this.#stateChangeListeners.add(listener), () => this.#stateChangeListeners.delete(listener);
	  }
	  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, popupLoadRevision = 0, popupLoadExpeditePromise = Promise.resolve();
	    const onPopupLoad = () => {
	      popupLoadRevision += 1, nextVerifyAt = this.#now(), popupLoadExpeditePromise = popupLoadExpeditePromise.then(() => 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 loadRevisionBeforeProbe = popupLoadRevision;
	          if (await popupLoadExpeditePromise, await this.#challengePassesProbe(signal)) {
	            await this.#completeChallengeLease();
	            try {
	              popup.close?.();
	            } catch {
	            }
	            return this.#challengeWindow = null, !0;
	          }
	          if (popupLoadRevision !== loadRevisionBeforeProbe) {
	            await popupLoadExpeditePromise, verifyDelayMs = this.#challengeVerifyIntervalMs, nextVerifyAt = this.#now();
	            continue;
	          }
	          verifyDelayMs = Math.min(maxVerifyDelayMs, verifyDelayMs * 2), nextVerifyAt = this.#now() + verifyDelayMs;
	        }
	        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.#notifyStateChange(), 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);
	    }
	  }
	  #notifyStateChange() {
	    for (const listener of [...this.#stateChangeListeners])
	      try {
	        listener();
	      } 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()), message?.schemaVersion === 1 && message.sourceId !== this.#sourceId && message.type === "updated" && this.#notifyStateChange(), this.#wake();
	  };
	  #onStorage = (event) => {
	    const key = event.key;
	    (key === null || key === READER_REQUEST_PERMIT_STORAGE_KEY) && (this.#notifyStateChange(), this.#wake());
	  };
	}
}, "e4a67daacd8aebd6449c3382c4ee9fb74750bbd7a8e4929b7c2eae87a9dd7696");

/* 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: input.parallelHead ? "topic-batch" : "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);
	  }
	}
}, "ef7d95ec50f5eb0f2179c0448de3331733c936854c1a419b64445060d675f868");

/* 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), requestGroup = group.key === "other" ? (0, import_reader_notification_model.readerNotificationGroup)("all") : group, page = nonNegativePage(pageValue), variant = notificationPageVariant(requestGroup), payload = await cachedNotificationPage.call(this.#gateway, {
	      authScope: this.authScope,
	      group: requestGroup.key,
	      page,
	      ...variant ? { variant } : {},
	      cache: notificationPageCacheSettings(requestGroup.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), requestGroup = group.key === "other" ? (0, import_reader_notification_model.readerNotificationGroup)("all") : group, page = nonNegativePage(pageValue), variant = notificationPageVariant(requestGroup);
	    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(
	      requestGroup.key,
	      page,
	      this.#native.username(),
	      previousCursor
	    );
	    assertNotificationDescriptor(descriptor);
	    const payload = await this.#gateway.loadNotificationPage({
	      authScope: this.authScope,
	      group: requestGroup.key,
	      page,
	      ...options.refresh ? { parallelHead: !0 } : {},
	      ...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(requestGroup.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.key === "other" ? (0, import_reader_notification_model.readerNotificationTypeBelongsToOther)(record.typeName) : !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 = group.key === "other" ? page * group.pageSize + records.length + (hasNext ? group.pageSize : 0) : 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,
	      ...group.key === "other" ? {
	        sourceTotal: serverTotal > 0 ? serverTotal : page * group.pageSize + rawEntries.length + (hasNext ? group.pageSize : 0)
	      } : {},
	      hasNext,
	      nextCursor
	    });
	  }
	}
}, "16087e694cc4a313caf77972cbe30e4930bda605b4d862b94c12edfb8b344b8b");

/* 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, DEFAULT_HEAD_CATCH_UP_MAX_PAGES = 20, 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"
	]), OTHER_NOTIFICATION_RECORD_VERSION = 2;
	function notificationProjectionCheckpointNeedsRepair(group, snapshot) {
	  const descriptor = (0, import_reader_notification_model.readerNotificationGroup)(group);
	  if (group === "other" && snapshot.recordVersion !== OTHER_NOTIFICATION_RECORD_VERSION && snapshot.records.some((record) => record.group === "other" && record.typeName.trim() !== "" && record.typeLabel.trim() === record.typeName.trim())) return !0;
	  if (group === "other" && snapshot.complete && snapshot.sourceTotalHint !== void 0 && snapshot.sourceOffset !== void 0)
	    return snapshot.sourceOffset < snapshot.sourceTotalHint;
	  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,
	      sourceTotalHint: 0,
	      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();
	  #backgroundHeadRefreshes = /* @__PURE__ */ new Map();
	  #backgroundRefreshingGroups = /* @__PURE__ */ new Set();
	  #backgroundRefreshFailedGroups = /* @__PURE__ */ new Set();
	  #backgroundHeadRefreshEpoch = 0;
	  #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.#backgroundHeadRefreshEpoch += 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.#backgroundHeadRefreshes.clear(), this.#backgroundRefreshingGroups.clear(), this.#backgroundRefreshFailedGroups.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,
	      backgroundRefreshingGroups: Object.freeze(
	        [...this.#backgroundRefreshingGroups]
	      ),
	      backgroundRefreshFailedGroups: Object.freeze(
	        [...this.#backgroundRefreshFailedGroups]
	      ),
	      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);
	      const records = new Map(
	        this.#historyRecords.get(group) ?? []
	      );
	      for (const record of snapshot.records)
	        records.set(record.identity, record);
	      this.#historyRecords.set(group, records), state.sourceTotalHint = Math.max(
	        0,
	        Math.floor(Number(snapshot.sourceTotalHint) || 0)
	      ), 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,
	              state.sourceTotalHint,
	              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()) {
	        const historyPage = entry.historyPage ?? entry.page;
	        historyPage.group === group && entry.advancesHistory !== !1 && this.#indexHistoryPage(historyPage);
	      }
	    }
	    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.sourceTotalHint = 0, state.complete = !1, state.retryAt = null, state.error = null;
	  }
	  clearCache() {
	    this.scope.destroyed || (this.#loadEpoch += 1, this.#navigationEpoch += 1, this.#backgroundWarmEpoch += 1, this.#backgroundHeadRefreshEpoch += 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.#backgroundHeadRefreshes.clear(), this.#backgroundRefreshingGroups.clear(), this.#backgroundRefreshFailedGroups.clear(), 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);
	    let loadedMissingPage = !1;
	    if (cached && this.#renderFromCache(), cached || (this.#loading = !0, this.#emit(), this.#projection && await this.#restoreSelectedProjection(this.#navigationEpoch), loadedMissingPage = !this.#pages.has(key), await this.#runSelectedRequest(() => this.#showSelectedPage())), !loadedMissingPage) {
	      await this.refresh();
	      return;
	    }
	    this.#schedulePoll();
	  }
	  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() {
	    if (!this.scope.destroyed) {
	      this.#liveRefresh !== null && this.#cancel(this.#liveRefresh), this.#liveRefresh = null, this.#nativeChangeEpoch += 1, this.#nativeChangePending = !0, this.#unreadCount = this.#native.unreadCount();
	      try {
	        await this.#runSelectedRequest(() => this.#refreshAfterNativeChange(!0));
	      } finally {
	        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;
	    }
	    if (this.#group === "all" && this.#page > 0) {
	      const page = this.#indexedAggregatePage(this.#page);
	      this.#cachePage(page, this.#now(), !1, !1), this.#applyPage(page), this.#emit();
	      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
	        });
	      }
	      if (page === 0 && options.refresh) {
	        const pages = await this.#loadHeadPagesUntilKnown(group, options);
	        for (const loaded of pages.slice(1))
	          this.#cachePage(loaded, this.#now(), !0, !1);
	        return pages.length > 1 && this.#queueTopicTaxonomyEnrichment(pages.slice(1), options), pages[0];
	      }
	      return this.#requests.load(group, page, options);
	    }
	    return this.#loadAggregatePage(page, options);
	  }
	  #knownIdentitiesForGroup(group) {
	    const identities = /* @__PURE__ */ new Set();
	    for (const record of this.#projectionRecords.get(group)?.values() ?? [])
	      identities.add(record.identity);
	    for (const record of this.#historyRecords.get(group)?.values() ?? [])
	      identities.add(record.identity);
	    const prefix = `${group}:`;
	    for (const [key, entry] of this.#pages)
	      if (key.startsWith(prefix))
	        for (const record of entry.page.records) identities.add(record.identity);
	    return identities;
	  }
	  async #loadHeadPagesUntilKnown(group, options) {
	    const knownIdentities = this.#knownIdentitiesForGroup(group), pages = [];
	    for (let page = 0; ; page += 1) {
	      const sparseComplement = group === "other", loaded = await this.#requests.load(
	        group,
	        page,
	        sparseComplement && page > 0 && options.refresh ? { ...options, refresh: !1 } : options
	      );
	      pages.push(loaded);
	      const reachedKnownIdentity = loaded.records.some((record) => knownIdentities.has(record.identity));
	      if (!loaded.hasNext || !sparseComplement && loaded.records.length === 0 || knownIdentities.size === 0 || reachedKnownIdentity || page + 1 >= DEFAULT_HEAD_CATCH_UP_MAX_PAGES) break;
	    }
	    return Object.freeze(pages);
	  }
	  #hasGroupHeadFallback(group) {
	    if (this.#pages.has(pageKey(group, 0)) || (this.#historyRecords.get(group)?.size ?? 0) > 0 || (this.#projectionRecords.get(group)?.size ?? 0) > 0) return !0;
	    const state = this.#historyGroups.get(group);
	    return !!(state && (state.pages.size > 0 || state.nextPage > 0 || state.terminalPage !== null || state.complete));
	  }
	  #queueBackgroundHeadRefresh(group, options) {
	    if (this.scope.destroyed || this.#backgroundHeadRefreshes.has(group)) return;
	    const epoch = this.#backgroundHeadRefreshEpoch;
	    this.#backgroundRefreshFailedGroups.delete(group), this.#backgroundRefreshingGroups.add(group), this.#emit();
	    let flight;
	    flight = (async () => {
	      try {
	        const pages = await this.#loadHeadPagesUntilKnown(group, {
	          ...options,
	          refresh: !0,
	          background: !0
	        });
	        if (this.scope.destroyed || epoch !== this.#backgroundHeadRefreshEpoch) return;
	        for (const loaded of pages)
	          this.#cachePage(loaded, this.#now(), !0, !1);
	        this.#queueTopicTaxonomyEnrichment(pages, {
	          ...options,
	          background: !0
	        }), this.#lastAuthoritativeAt.set(pageKey(group, 0), this.#now()), this.#raiseUnreadCountForCachedRecords(), this.#backgroundRefreshFailedGroups.delete(group), this.#open ? this.#renderFromCache() : this.#emit();
	      } catch (cause) {
	        if (this.scope.destroyed || epoch !== this.#backgroundHeadRefreshEpoch) return;
	        this.#backgroundRefreshFailedGroups.add(group);
	        const pollBackoffMs = readerNotificationPollBackoffMs(cause);
	        pollBackoffMs > 0 && (this.#pollNotBefore = Math.max(
	          this.#pollNotBefore,
	          this.#now() + pollBackoffMs
	        )), this.#onError(cause);
	      } finally {
	        if (this.#backgroundHeadRefreshes.get(group) !== flight) return;
	        this.#backgroundHeadRefreshes.delete(group), this.#backgroundRefreshingGroups.delete(group), !this.scope.destroyed && epoch === this.#backgroundHeadRefreshEpoch && (this.#open ? this.#renderFromCache() : this.#emit());
	      }
	    })(), this.#backgroundHeadRefreshes.set(group, flight);
	  }
	  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) => {
	        if (options.refresh)
	          try {
	            const pages2 = await this.#loadHeadPagesUntilKnown(
	              groupKey,
	              options
	            );
	            return Object.freeze({ pages: pages2, error: null });
	          } catch (cause) {
	            return Object.freeze({
	              pages: Object.freeze([]),
	              error: cause
	            });
	          }
	        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.#now(), !0, !options.refresh);
	    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) => {
	        if (options.refresh) {
	          if (groupKey === "reactions" && this.#hasGroupHeadFallback(groupKey))
	            return this.#queueBackgroundHeadRefresh(groupKey, options), Object.freeze({
	              pages: Object.freeze([]),
	              error: null
	            });
	          try {
	            const pages2 = await this.#loadHeadPagesUntilKnown(
	              groupKey,
	              options
	            );
	            return Object.freeze({ pages: pages2, error: null });
	          } catch (cause) {
	            return Object.freeze({
	              pages: Object.freeze([]),
	              error: cause
	            });
	          }
	        }
	        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.#now(), !0, !options.refresh);
	      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)))
	    );
	  }
	  #indexedOtherRecords() {
	    return (0, import_reader_notification_model.sortReaderNotifications)([
	      ...this.#historyRecords.get("other")?.values() ?? []
	    ]);
	  }
	  #indexedOtherPage(page, indexed = this.#indexedOtherRecords()) {
	    const descriptor = (0, import_reader_notification_model.readerNotificationGroup)("other"), start = page * descriptor.pageSize, end = start + descriptor.pageSize;
	    return Object.freeze({
	      group: "other",
	      page,
	      records: Object.freeze(indexed.slice(start, end)),
	      total: indexed.length,
	      hasNext: end < indexed.length,
	      nextCursor: null
	    });
	  }
	  #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(), other = this.#indexedOtherRecords();
	    for (const [key, entry] of [...this.#pages])
	      key.startsWith("other:") && this.#pages.set(key, Object.freeze({
	        page: this.#indexedOtherPage(entry.page.page, other),
	        loadedAt: entry.loadedAt,
	        ...entry.advancesHistory === void 0 ? {} : { advancesHistory: entry.advancesHistory },
	        ...entry.historyPage === void 0 ? {} : { historyPage: entry.historyPage }
	      }));
	    if (other.length && !this.#pages.has(pageKey("other", 0)) && this.#pages.set(pageKey("other", 0), Object.freeze({
	      page: this.#indexedOtherPage(0, other),
	      loadedAt: this.#now()
	    })), this.#group === "other" && !this.#hasLocalFilters()) {
	      const pageSize2 = (0, import_reader_notification_model.readerNotificationGroup)("other").pageSize, totalPages2 = Math.max(1, Math.ceil(other.length / pageSize2));
	      this.#page >= totalPages2 && (this.#page = totalPages2 - 1);
	      const page2 = this.#indexedOtherPage(this.#page, other), existing = this.#pages.get(pageKey("other", this.#page));
	      this.#pages.set(pageKey("other", this.#page), Object.freeze({
	        page: page2,
	        loadedAt: this.#now(),
	        ...existing?.advancesHistory === void 0 ? {} : { advancesHistory: existing.advancesHistory },
	        ...existing?.historyPage === void 0 ? {} : { historyPage: existing.historyPage }
	      })), this.#applyPage(page2);
	    }
	    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, this.#now(), !0, !refresh), 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 (page.sourceTotal !== void 0 && (state.sourceTotalHint = Math.max(
	      0,
	      Math.floor(Number(page.sourceTotal) || 0)
	    )), state.estimatedPages = Math.max(
	      state.estimatedPages,
	      page.page + 1 + (page.hasNext ? 1 : 0),
	      Math.ceil(Math.max(page.total, state.sourceTotalHint) / group.pageSize)
	    ), page.hasNext && state.terminalPage === page.page ? state.terminalPage = null : 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, advanceHistory = !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 }), historyState = this.#historyGroups.get(inherited.group), group = (0, import_reader_notification_model.readerNotificationGroup)(inherited.group), reopensSparseCheckpoint = inherited.group === "other" && inherited.page === 0 && (inherited.sourceTotal ?? 0) > (historyState?.nextPage ?? 0) * group.pageSize && historyState?.complete === !0;
	    historyState && reopensSparseCheckpoint && (historyState.complete = !1, historyState.terminalPage = null, historyState.retryAt = null, historyState.error = null, this.#historyStatus = "idle", this.#historyCurrentGroup = null, this.#historyError = null, this.#historyRetryAt = null, this.#projectionCheckpointReplacements.add(inherited.group));
	    const hasHistoryCheckpoint = historyState !== void 0 && (historyState.pages.size > 0 || historyState.nextPage > 0 || historyState.terminalPage !== null || historyState.complete || (this.#historyRecords.get(inherited.group)?.size ?? 0) > 0), advancesHistory = advanceHistory || reopensSparseCheckpoint || !hasHistoryCheckpoint;
	    if (this.#pages.delete(key), this.#pages.set(key, Object.freeze({
	      page: inherited,
	      loadedAt,
	      advancesHistory,
	      ...advancesHistory ? { historyPage: inherited } : {}
	    })), advancesHistory)
	      this.#indexHistoryPage(inherited);
	    else {
	      const historyRecords = this.#historyRecords.get(inherited.group) ?? /* @__PURE__ */ new Map();
	      for (const record of inherited.records)
	        historyRecords.set(record.identity, record);
	      this.#historyRecords.set(inherited.group, historyRecords);
	    }
	    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 ? state.complete : 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 record of this.#historyRecords.get(group)?.values() ?? [])
	        byIdentity.set(record.identity, record);
	      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
	    ), sourceTotalHint = cachedPages.reduce(
	      (total, page) => Math.max(total, page.sourceTotal ?? 0),
	      state?.sourceTotalHint ?? 0
	    ), replaceCheckpoint = this.#projectionCheckpointReplacements.has(group);
	    return this.#projection.write(group, committedRecords, {
	      mergeStored: !exactReplacement,
	      totalHint,
	      ...group === "other" && complete ? { recordVersion: OTHER_NOTIFICATION_RECORD_VERSION } : {},
	      ...sourceTotalHint > 0 ? { sourceTotalHint } : {},
	      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) {
	    const remember = (partition, values) => {
	      const indexed = 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(force = !1) {
	    if (this.scope.destroyed) return;
	    if (this.#hasLocalFilters() && !force) {
	      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 (selectedPage > 0) {
	        await this.#refreshAllHeadAfterNativeChange();
	        return;
	      }
	      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,
	        !1
	      ), !valid())) return;
	      const inboxKey = pageKey("inbox", 0);
	      if (this.#native.username().trim() && !this.#pages.has(inboxKey)) {
	        const inboxPages = await this.#loadHeadPagesUntilKnown("inbox", {
	          refresh: !0,
	          background: !0
	        });
	        if (!valid()) return;
	        for (const inbox of inboxPages)
	          this.#cachePage(inbox, this.#now(), !0, !1);
	        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, refresh = !0) {
	    try {
	      const nativePage = await this.#requests.load("all", 0, {
	        ...refresh ? { 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;
	      this.#refreshExpandedNativeAssociation(
	        changeEpoch,
	        selectedGroup,
	        selectedPage
	      );
	    } 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);
	    }
	  }
	  async #refreshExpandedNativeAssociation(changeEpoch, selectedGroup, selectedPage) {
	    try {
	      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 backoffMs = this.#pollNotBefore - this.#now(), delayMs = backoffMs > 0 ? backoffMs : this.#pollIntervalMs();
	    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.refresh().catch(this.#onError);
	      }
	    }, delayMs));
	  }
	  #pollIntervalMs() {
	    const source = (0, import_reader_notification_model.readerNotificationGroup)(this.#group).source;
	    return this.#group === "all" || source === "user-actions" || source === "boosts-received" || source === "reactions-received" ? this.#syntheticPollIntervalMs : this.#nativePollIntervalMs;
	  }
	  #activityRecoveryDue(key) {
	    const observedAt = this.#lastAuthoritativeAt.get(key) ?? this.#pages.get(key)?.loadedAt;
	    if (observedAt === void 0) return !1;
	    const recoveryMs = this.#pollIntervalMs();
	    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);
	  }
	}
}, "1eca7bcb2e6bde5fa4d74a32fbeea49abd517cc4a94ed8bd7412c68e687b8564");

/* 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,
	  readerNotificationTypeBelongsToOther: () => readerNotificationTypeBelongsToOther,
	  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]
	  }),
	  other: group({
	    key: "other",
	    mode: "notifications",
	    source: "notifications",
	    label: "其他",
	    icon: "list-checks",
	    pageSize: 24
	  }),
	  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",
	  "other",
	  "inbox",
	  "sent",
	  "newMessages",
	  "unreadMessages",
	  "archive",
	  "botMessages"
	]), READER_NOTIFICATION_PANEL_GROUP_ORDER = Object.freeze([
	  "all",
	  "replies",
	  "boosts",
	  "reactionLikes",
	  "mentions",
	  "edits",
	  "links",
	  "other",
	  "inbox",
	  "sent",
	  "newMessages",
	  "unreadMessages",
	  "archive",
	  "botMessages"
	]), READER_NOTIFICATION_AGGREGATE_GROUP_ORDER = Object.freeze([
	  "replies",
	  "likes",
	  "mentions",
	  "edits",
	  "links",
	  "boosts",
	  "reactions",
	  "other"
	]), READER_NOTIFICATION_OTHER_EXCLUDED_TYPE_NAMES = Object.freeze(/* @__PURE__ */ new Set([
	  ...Object.values(READER_NOTIFICATION_GROUPS).filter((candidate) => candidate.mode === "notifications" && candidate.key !== "other").flatMap((candidate) => candidate.typeNames),
	  // 私信已有独立模式;原生通知只用于已读身份关联,不能再混入“其他”。
	  "private_message",
	  "invited_to_private_message",
	  "group_message_summary"
	]));
	function readerNotificationTypeBelongsToOther(value) {
	  const typeName = String(value ?? "").trim().toLocaleLowerCase("en-US");
	  return !READER_NOTIFICATION_OTHER_EXCLUDED_TYPE_NAMES.has(typeName);
	}
	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.other;
	}
	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({
	  custom: "sparkles",
	  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",
	  following_created_topic: "followed-topic",
	  post_approved: "post-approved",
	  topic_reminder: "calendar-clock"
	});
	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, typeName = notificationText(source.typeName), storedTypeLabel = notificationText(source.typeLabel), typeLabel = notificationSource === "notifications" ? nativeNotificationTypeLabel(typeName, {
	    typeName,
	    typeLabel: storedTypeLabel
	  }, Object.freeze({})) : storedTypeLabel;
	  return recordResult({
	    identity,
	    group: group2,
	    source: notificationSource,
	    sourceNotificationId: positiveId(source.sourceNotificationId),
	    notificationTypeId: positiveId(source.notificationTypeId),
	    highPriority: source.highPriority === !0,
	    typeName,
	    typeLabel,
	    aggregateCount: aggregateCount(source.aggregateCount),
	    icon: notificationSource === "notifications" ? nativeNotificationTypeIcon(
	      typeName,
	      typeLabel,
	      notificationText(source.icon) || "bell"
	    ) : 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}` : ""}` : "";
	}
	const NATIVE_OTHER_TYPE_LABELS = Object.freeze({
	  custom: "自定义通知",
	  following_created_topic: "您关注的人新话题",
	  post_approved: "已批准帖子",
	  topic_reminder: "话题提醒"
	}), NATIVE_CUSTOM_TITLE_LABELS = Object.freeze({
	  "solved.notification.title": "您的帖子被标记为解决方案",
	  "solved.notification.topic_solved_title": "话题已解决"
	});
	function nativeNotificationTypeLabel(typeName, presented, data) {
	  const presentedLabel = notificationText(presented.typeLabel);
	  if (typeName === "custom") {
	    const customLabel = NATIVE_CUSTOM_TITLE_LABELS[String(data.title ?? "")];
	    if (customLabel) return customLabel;
	    if (presentedLabel === "您的帖子已被标记为解决方案")
	      return "您的帖子被标记为解决方案";
	  }
	  const canonicalLabel = NATIVE_OTHER_TYPE_LABELS[typeName];
	  return canonicalLabel && typeName !== "custom" ? canonicalLabel : presentedLabel && presentedLabel !== typeName ? presentedLabel : (NATIVE_OTHER_TYPE_LABELS[typeName] ?? presentedLabel) || typeName || "通知";
	}
	function nativeNotificationTypeIcon(typeName, typeLabel, fallback = "bell") {
	  return typeName === "custom" && /(?:解决方案|已解决|话题解决)/.test(typeLabel) ? "solution-badge" : TYPE_ICONS[typeName] ?? fallback;
	}
	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 ?? "通知"
	  ), typeLabel = nativeNotificationTypeLabel(typeName, presented, data), 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,
	    aggregateCount: count,
	    icon: nativeNotificationTypeIcon(typeName, typeLabel, 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)));
	}
}, "c7ae36f309ef972277e3343b361efec404729b7e38c7939105b9c8c4fb30c5aa");

/* 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;
	  #refreshHeaderAction;
	  #scrollWindow;
	  #recordNodes = new import_reader_collection_floating_window.ReaderCollectionNodeCache();
	  #relativeTimer = null;
	  #refreshVisualReset = null;
	  #refreshVisualState = "idle";
	  #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.#refreshHeaderAction = this.#document.createElement("button"), this.#refreshHeaderAction.type = "button", this.#refreshHeaderAction.className = "ldp-notification-refresh", this.#refreshHeaderAction.title = "更新通知", this.#refreshHeaderAction.setAttribute("aria-label", "更新通知"), this.#refreshHeaderAction.replaceChildren(
	      (0, import_reader_icon.renderReaderIcon)(
	        this.#document,
	        "rotate-ccw",
	        this.#renderIcon
	      )
	    ), this.#markAllHeaderActions.append(
	      this.#refreshHeaderAction,
	      this.#elements.markAll
	    ), this.#surface.attachHeaderActions({
	      root: this.#markAllHeaderActions,
	      buttons: [this.#refreshHeaderAction, 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.#refreshVisualReset !== null && (this.#cancel(this.#refreshVisualReset), this.#refreshVisualReset = null), 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.#refreshHeaderAction, "click", () => {
	      this.#refreshVisualState !== "running" && (this.#setRefreshVisualState("running"), this.#controller.refresh().then(() => {
	        if (!this.scope.destroyed) {
	          if (this.#controller.snapshot.error) {
	            this.#setRefreshVisualState("error", 3e3), this.#notify("通知更新失败,请稍后重试");
	            return;
	          }
	          this.#setRefreshVisualState("success", 1800), this.#notify("通知已更新");
	        }
	      }).catch((cause) => {
	        this.scope.destroyed || (this.#setRefreshVisualState("error", 3e3), 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.#syncRefreshHeaderAction(snapshot), 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} 条`);
	  }
	  #setRefreshVisualState(state, resetAfterMs = 0) {
	    this.#refreshVisualReset !== null && (this.#cancel(this.#refreshVisualReset), this.#refreshVisualReset = null), this.#refreshVisualState = state, this.#syncRefreshHeaderAction(this.#controller.snapshot), this.#syncWindowStatus(this.#controller.snapshot), resetAfterMs > 0 && (this.#refreshVisualReset = this.#schedule(() => {
	      this.#refreshVisualReset = null, !this.scope.destroyed && (this.#refreshVisualState = "idle", this.#syncRefreshHeaderAction(this.#controller.snapshot), this.#syncWindowStatus(this.#controller.snapshot));
	    }, resetAfterMs));
	  }
	  #syncRefreshHeaderAction(snapshot) {
	    const busy = snapshot.loading || snapshot.refreshing || this.#refreshVisualState === "running", backgroundReaction = snapshot.backgroundRefreshingGroups.includes(
	      "reactions"
	    ), backgroundReactionFailed = snapshot.backgroundRefreshFailedGroups.includes("reactions"), state = busy ? "running" : backgroundReactionFailed ? "error" : this.#refreshVisualState, label = state === "running" ? "正在更新通知" : backgroundReaction ? "主要通知已更新,回应后台更新中" : state === "success" ? "通知更新完成" : state === "error" ? backgroundReactionFailed ? "回应后台更新失败,点击重试" : "通知更新失败,点击重试" : "更新通知", icon = state === "running" ? "loader" : backgroundReaction ? "rotate-ccw" : state === "success" ? "check-square" : state === "error" ? "x" : "rotate-ccw";
	    this.#refreshHeaderAction.disabled = busy, this.#refreshHeaderAction.dataset.ldpRequestBusy = busy ? "1" : "0", this.#refreshHeaderAction.dataset.refreshState = state, this.#refreshHeaderAction.classList.toggle("is-refreshing", busy), this.#refreshHeaderAction.setAttribute("aria-busy", String(busy)), this.#refreshHeaderAction.setAttribute("aria-label", label), this.#refreshHeaderAction.title = label, this.#refreshHeaderAction.replaceChildren((0, import_reader_icon.renderReaderIcon)(
	      this.#document,
	      icon,
	      this.#renderIcon
	    ));
	  }
	  #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} 条` : "", refreshStatus = snapshot.refreshing || this.#refreshVisualState === "running" ? "正在更新通知" : this.#refreshVisualState === "success" ? "刚刚更新" : this.#refreshVisualState === "error" ? "更新失败" : "", backgroundRefreshStatus = snapshot.backgroundRefreshingGroups.includes("reactions") ? "后台更新回应" : snapshot.backgroundRefreshFailedGroups.includes("reactions") ? "回应后台更新失败" : "";
	    if (this.#surface.frame.meta.textContent = [
	      snapshot.unreadCount > 0 ? `未读 ${snapshot.unreadCount}` : "",
	      totalStatus,
	      cacheStatus,
	      complete ? "历史已到底" : "",
	      refreshStatus,
	      backgroundRefreshStatus
	    ].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.notificationType = record.typeName, 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.dataset.notificationType = record.typeName, typeIcon.dataset.notificationIcon = record.icon, record.group === "other" && (typeIcon.title = record.typeLabel.trim() || record.typeName.trim() || "通知"), 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");
	    titleText.className = "ldp-notification-title-text";
	    const displayTitle = recordDisplayTitle(record, archiveMarker), specificType = record.group === "other" ? record.typeLabel.trim() || record.typeName.trim() || "通知" : "";
	    if (titleText.textContent = specificType ? `【${specificType}】${displayTitle}` : displayTitle, 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);
	  }
	}
}, "a8ee1081f593a7a1b4a4a12c1d1995dcc0542b4ee8c904523eb04c4fd0c373d0");

/* 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");
})();