diff --git a/.gitignore b/.gitignore index 65ceac2..b625a63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ .old-reference/ .local/ dist/ -dist-release/ -release/ +# dist-release/ +# release/ node_modules/ # Local debug captures diff --git a/dist-release/assets/icons/icon-128.png b/dist-release/assets/icons/icon-128.png new file mode 100644 index 0000000..8cc212d Binary files /dev/null and b/dist-release/assets/icons/icon-128.png differ diff --git a/dist-release/assets/icons/icon-16.png b/dist-release/assets/icons/icon-16.png new file mode 100644 index 0000000..b2d62c6 Binary files /dev/null and b/dist-release/assets/icons/icon-16.png differ diff --git a/dist-release/assets/icons/icon-32.png b/dist-release/assets/icons/icon-32.png new file mode 100644 index 0000000..c87d772 Binary files /dev/null and b/dist-release/assets/icons/icon-32.png differ diff --git a/dist-release/assets/icons/icon-48.png b/dist-release/assets/icons/icon-48.png new file mode 100644 index 0000000..ed5f38f Binary files /dev/null and b/dist-release/assets/icons/icon-48.png differ diff --git a/dist-release/assets/icons/icon-source.svg b/dist-release/assets/icons/icon-source.svg new file mode 100644 index 0000000..4c81685 --- /dev/null +++ b/dist-release/assets/icons/icon-source.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/dist-release/background/index.js b/dist-release/background/index.js new file mode 100644 index 0000000..89db7fb --- /dev/null +++ b/dist-release/background/index.js @@ -0,0 +1,3335 @@ +"use strict"; +(() => { + // src/shared/auth-config.ts + var defaultAuthConfig = { + apiResource: "https://talent-search.intelligrow.cn", + appId: "i4jkllbvih0554r4n0fd3", + enableDevAuthPanel: false, + logtoEndpoint: "https://login-api.intelligrow.cn", + scopes: ["openid", "profile", "offline_access", "talent-search:read"] + }; + function readAuthConfig(overrides = {}) { + const nextConfig = { + ...defaultAuthConfig, + ...overrides + }; + if (!nextConfig.logtoEndpoint.trim()) { + throw new Error("auth config logtoEndpoint is required"); + } + if (!nextConfig.appId.trim()) { + throw new Error("auth config appId is required"); + } + if (!nextConfig.apiResource.trim()) { + throw new Error("auth config apiResource is required"); + } + return nextConfig; + } + + // src/background/auth/state.ts + function createLoggedOutAuthState(config) { + return { + isAuthenticated: false, + resource: config?.apiResource ?? null + }; + } + function createLoggedInAuthState(claims, config) { + return { + accessTokenExpiresAt: null, + isAuthenticated: true, + resource: config?.apiResource ?? null, + scopes: config?.scopes ?? [], + tokenAvailable: true, + userInfo: { + email: readStringClaim(claims, "email"), + name: readStringClaim(claims, "name"), + sub: readStringClaim(claims, "sub"), + username: readStringClaim(claims, "username") + } + }; + } + function readStringClaim(claims, key) { + const value = claims?.[key]; + return typeof value === "string" ? value : void 0; + } + + // src/background/auth/controller.ts + function createAuthController(options) { + const config = options.config ?? readAuthConfig(); + return { + async getAccessToken() { + return options.authClient.getAccessToken(config.apiResource); + }, + async getAuthState() { + const isAuthenticated = await options.authClient.isAuthenticated(); + if (!isAuthenticated) { + return createLoggedOutAuthState(config); + } + const claims = await options.authClient.getIdTokenClaims(); + return createLoggedInAuthState(claims, config); + }, + async signIn() { + await options.authClient.signIn(); + }, + async signOut() { + await options.authClient.signOut(); + } + }; + } + + // node_modules/map-obj/index.js + var isObject = (value) => typeof value === "object" && value !== null; + var isObjectCustom = (value) => isObject(value) && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date); + var mapObjectSkip = /* @__PURE__ */ Symbol("mapObjectSkip"); + var _mapObject = (object, mapper, options, isSeen = /* @__PURE__ */ new WeakMap()) => { + options = { + deep: false, + target: {}, + ...options + }; + if (isSeen.has(object)) { + return isSeen.get(object); + } + isSeen.set(object, options.target); + const { target } = options; + delete options.target; + const mapArray = (array) => array.map((element) => isObjectCustom(element) ? _mapObject(element, mapper, options, isSeen) : element); + if (Array.isArray(object)) { + return mapArray(object); + } + for (const [key, value] of Object.entries(object)) { + const mapResult = mapper(key, value, object); + if (mapResult === mapObjectSkip) { + continue; + } + let [newKey, newValue, { shouldRecurse = true } = {}] = mapResult; + if (newKey === "__proto__") { + continue; + } + if (options.deep && shouldRecurse && isObjectCustom(newValue)) { + newValue = Array.isArray(newValue) ? mapArray(newValue) : _mapObject(newValue, mapper, options, isSeen); + } + target[newKey] = newValue; + } + return target; + }; + function mapObject(object, mapper, options) { + if (!isObject(object)) { + throw new TypeError(`Expected an object, got \`${object}\` (${typeof object})`); + } + return _mapObject(object, mapper, options); + } + + // node_modules/camelcase/index.js + var UPPERCASE = /[\p{Lu}]/u; + var LOWERCASE = /[\p{Ll}]/u; + var LEADING_CAPITAL = /^[\p{Lu}](?![\p{Lu}])/gu; + var IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u; + var SEPARATORS = /[_.\- ]+/; + var LEADING_SEPARATORS = new RegExp("^" + SEPARATORS.source); + var SEPARATORS_AND_IDENTIFIER = new RegExp(SEPARATORS.source + IDENTIFIER.source, "gu"); + var NUMBERS_AND_IDENTIFIER = new RegExp("\\d+" + IDENTIFIER.source, "gu"); + var preserveCamelCase = (string, toLowerCase, toUpperCase, preserveConsecutiveUppercase2) => { + let isLastCharLower = false; + let isLastCharUpper = false; + let isLastLastCharUpper = false; + let isLastLastCharPreserved = false; + for (let index = 0; index < string.length; index++) { + const character = string[index]; + isLastLastCharPreserved = index > 2 ? string[index - 3] === "-" : true; + if (isLastCharLower && UPPERCASE.test(character)) { + string = string.slice(0, index) + "-" + string.slice(index); + isLastCharLower = false; + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = true; + index++; + } else if (isLastCharUpper && isLastLastCharUpper && LOWERCASE.test(character) && (!isLastLastCharPreserved || preserveConsecutiveUppercase2)) { + string = string.slice(0, index - 1) + "-" + string.slice(index - 1); + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = false; + isLastCharLower = true; + } else { + isLastCharLower = toLowerCase(character) === character && toUpperCase(character) !== character; + isLastLastCharUpper = isLastCharUpper; + isLastCharUpper = toUpperCase(character) === character && toLowerCase(character) !== character; + } + } + return string; + }; + var preserveConsecutiveUppercase = (input, toLowerCase) => { + LEADING_CAPITAL.lastIndex = 0; + return input.replaceAll(LEADING_CAPITAL, (match) => toLowerCase(match)); + }; + var postProcess = (input, toUpperCase) => { + SEPARATORS_AND_IDENTIFIER.lastIndex = 0; + NUMBERS_AND_IDENTIFIER.lastIndex = 0; + return input.replaceAll(NUMBERS_AND_IDENTIFIER, (match, pattern, offset) => ["_", "-"].includes(input.charAt(offset + match.length)) ? match : toUpperCase(match)).replaceAll(SEPARATORS_AND_IDENTIFIER, (_, identifier) => toUpperCase(identifier)); + }; + function camelCase(input, options) { + if (!(typeof input === "string" || Array.isArray(input))) { + throw new TypeError("Expected the input to be `string | string[]`"); + } + options = { + pascalCase: false, + preserveConsecutiveUppercase: false, + ...options + }; + if (Array.isArray(input)) { + input = input.map((x) => x.trim()).filter((x) => x.length).join("-"); + } else { + input = input.trim(); + } + if (input.length === 0) { + return ""; + } + const toLowerCase = options.locale === false ? (string) => string.toLowerCase() : (string) => string.toLocaleLowerCase(options.locale); + const toUpperCase = options.locale === false ? (string) => string.toUpperCase() : (string) => string.toLocaleUpperCase(options.locale); + if (input.length === 1) { + if (SEPARATORS.test(input)) { + return ""; + } + return options.pascalCase ? toUpperCase(input) : toLowerCase(input); + } + const hasUpperCase = input !== toLowerCase(input); + if (hasUpperCase) { + input = preserveCamelCase(input, toLowerCase, toUpperCase, options.preserveConsecutiveUppercase); + } + input = input.replace(LEADING_SEPARATORS, ""); + input = options.preserveConsecutiveUppercase ? preserveConsecutiveUppercase(input, toLowerCase) : toLowerCase(input); + if (options.pascalCase) { + input = toUpperCase(input.charAt(0)) + input.slice(1); + } + return postProcess(input, toUpperCase); + } + + // node_modules/quick-lru/index.js + var QuickLRU = class extends Map { + constructor(options = {}) { + super(); + if (!(options.maxSize && options.maxSize > 0)) { + throw new TypeError("`maxSize` must be a number greater than 0"); + } + if (typeof options.maxAge === "number" && options.maxAge === 0) { + throw new TypeError("`maxAge` must be a number greater than 0"); + } + this.maxSize = options.maxSize; + this.maxAge = options.maxAge || Number.POSITIVE_INFINITY; + this.onEviction = options.onEviction; + this.cache = /* @__PURE__ */ new Map(); + this.oldCache = /* @__PURE__ */ new Map(); + this._size = 0; + } + // TODO: Use private class methods when targeting Node.js 16. + _emitEvictions(cache2) { + if (typeof this.onEviction !== "function") { + return; + } + for (const [key, item] of cache2) { + this.onEviction(key, item.value); + } + } + _deleteIfExpired(key, item) { + if (typeof item.expiry === "number" && item.expiry <= Date.now()) { + if (typeof this.onEviction === "function") { + this.onEviction(key, item.value); + } + return this.delete(key); + } + return false; + } + _getOrDeleteIfExpired(key, item) { + const deleted = this._deleteIfExpired(key, item); + if (deleted === false) { + return item.value; + } + } + _getItemValue(key, item) { + return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value; + } + _peek(key, cache2) { + const item = cache2.get(key); + return this._getItemValue(key, item); + } + _set(key, value) { + this.cache.set(key, value); + this._size++; + if (this._size >= this.maxSize) { + this._size = 0; + this._emitEvictions(this.oldCache); + this.oldCache = this.cache; + this.cache = /* @__PURE__ */ new Map(); + } + } + _moveToRecent(key, item) { + this.oldCache.delete(key); + this._set(key, item); + } + *_entriesAscending() { + for (const item of this.oldCache) { + const [key, value] = item; + if (!this.cache.has(key)) { + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield item; + } + } + } + for (const item of this.cache) { + const [key, value] = item; + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield item; + } + } + } + get(key) { + if (this.cache.has(key)) { + const item = this.cache.get(key); + return this._getItemValue(key, item); + } + if (this.oldCache.has(key)) { + const item = this.oldCache.get(key); + if (this._deleteIfExpired(key, item) === false) { + this._moveToRecent(key, item); + return item.value; + } + } + } + set(key, value, { maxAge = this.maxAge } = {}) { + const expiry = typeof maxAge === "number" && maxAge !== Number.POSITIVE_INFINITY ? Date.now() + maxAge : void 0; + if (this.cache.has(key)) { + this.cache.set(key, { + value, + expiry + }); + } else { + this._set(key, { value, expiry }); + } + return this; + } + has(key) { + if (this.cache.has(key)) { + return !this._deleteIfExpired(key, this.cache.get(key)); + } + if (this.oldCache.has(key)) { + return !this._deleteIfExpired(key, this.oldCache.get(key)); + } + return false; + } + peek(key) { + if (this.cache.has(key)) { + return this._peek(key, this.cache); + } + if (this.oldCache.has(key)) { + return this._peek(key, this.oldCache); + } + } + delete(key) { + const deleted = this.cache.delete(key); + if (deleted) { + this._size--; + } + return this.oldCache.delete(key) || deleted; + } + clear() { + this.cache.clear(); + this.oldCache.clear(); + this._size = 0; + } + resize(newSize) { + if (!(newSize && newSize > 0)) { + throw new TypeError("`maxSize` must be a number greater than 0"); + } + const items = [...this._entriesAscending()]; + const removeCount = items.length - newSize; + if (removeCount < 0) { + this.cache = new Map(items); + this.oldCache = /* @__PURE__ */ new Map(); + this._size = items.length; + } else { + if (removeCount > 0) { + this._emitEvictions(items.slice(0, removeCount)); + } + this.oldCache = new Map(items.slice(removeCount)); + this.cache = /* @__PURE__ */ new Map(); + this._size = 0; + } + this.maxSize = newSize; + } + *keys() { + for (const [key] of this) { + yield key; + } + } + *values() { + for (const [, value] of this) { + yield value; + } + } + *[Symbol.iterator]() { + for (const item of this.cache) { + const [key, value] = item; + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield [key, value.value]; + } + } + for (const item of this.oldCache) { + const [key, value] = item; + if (!this.cache.has(key)) { + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield [key, value.value]; + } + } + } + } + *entriesDescending() { + let items = [...this.cache]; + for (let i = items.length - 1; i >= 0; --i) { + const item = items[i]; + const [key, value] = item; + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield [key, value.value]; + } + } + items = [...this.oldCache]; + for (let i = items.length - 1; i >= 0; --i) { + const item = items[i]; + const [key, value] = item; + if (!this.cache.has(key)) { + const deleted = this._deleteIfExpired(key, value); + if (deleted === false) { + yield [key, value.value]; + } + } + } + } + *entriesAscending() { + for (const [key, value] of this._entriesAscending()) { + yield [key, value.value]; + } + } + get size() { + if (!this._size) { + return this.oldCache.size; + } + let oldCacheSize = 0; + for (const key of this.oldCache.keys()) { + if (!this.cache.has(key)) { + oldCacheSize++; + } + } + return Math.min(this._size + oldCacheSize, this.maxSize); + } + entries() { + return this.entriesAscending(); + } + forEach(callbackFunction, thisArgument = this) { + for (const [key, value] of this.entriesAscending()) { + callbackFunction.call(thisArgument, value, key, this); + } + } + get [Symbol.toStringTag]() { + return JSON.stringify([...this.entriesAscending()]); + } + }; + + // node_modules/camelcase-keys/index.js + var has = (array, key) => array.some((element) => { + if (typeof element === "string") { + return element === key; + } + element.lastIndex = 0; + return element.test(key); + }); + var cache = new QuickLRU({ maxSize: 1e5 }); + var isObject2 = (value) => typeof value === "object" && value !== null && !(value instanceof RegExp) && !(value instanceof Error) && !(value instanceof Date); + var transform = (input, options = {}) => { + if (!isObject2(input)) { + return input; + } + const { + exclude, + pascalCase = false, + stopPaths, + deep = false, + preserveConsecutiveUppercase: preserveConsecutiveUppercase2 = false + } = options; + const stopPathsSet = new Set(stopPaths); + const makeMapper = (parentPath) => (key, value) => { + if (deep && isObject2(value)) { + const path = parentPath === void 0 ? key : `${parentPath}.${key}`; + if (!stopPathsSet.has(path)) { + value = mapObject(value, makeMapper(path)); + } + } + if (!(exclude && has(exclude, key))) { + const cacheKey = pascalCase ? `${key}_` : key; + if (cache.has(cacheKey)) { + key = cache.get(cacheKey); + } else { + const returnValue = camelCase(key, { pascalCase, locale: false, preserveConsecutiveUppercase: preserveConsecutiveUppercase2 }); + if (key.length < 100) { + cache.set(cacheKey, returnValue); + } + key = returnValue; + } + } + return [key, value]; + }; + return mapObject(input, makeMapper(void 0)); + }; + function camelcaseKeys(input, options) { + if (Array.isArray(input)) { + return Object.keys(input).map((key) => transform(input[key], options)); + } + return transform(input, options); + } + + // node_modules/@logto/js/lib/consts/openid.js + var ReservedScope; + (function(ReservedScope2) { + ReservedScope2["OpenId"] = "openid"; + ReservedScope2["OfflineAccess"] = "offline_access"; + })(ReservedScope || (ReservedScope = {})); + var ReservedResource; + (function(ReservedResource2) { + ReservedResource2["Organization"] = "urn:logto:resource:organizations"; + })(ReservedResource || (ReservedResource = {})); + var UserScope; + (function(UserScope2) { + UserScope2["Profile"] = "profile"; + UserScope2["Email"] = "email"; + UserScope2["Phone"] = "phone"; + UserScope2["Address"] = "address"; + UserScope2["CustomData"] = "custom_data"; + UserScope2["Identities"] = "identities"; + UserScope2["Roles"] = "roles"; + UserScope2["Organizations"] = "urn:logto:scope:organizations"; + UserScope2["OrganizationRoles"] = "urn:logto:scope:organization_roles"; + UserScope2["Sessions"] = "urn:logto:scope:sessions"; + })(UserScope || (UserScope = {})); + var idTokenClaims = Object.freeze({ + [UserScope.Profile]: ["name", "picture", "username"], + [UserScope.Email]: ["email", "email_verified"], + [UserScope.Phone]: ["phone_number", "phone_number_verified"], + [UserScope.Address]: [], + [UserScope.Roles]: ["roles"], + [UserScope.Organizations]: ["organizations"], + [UserScope.OrganizationRoles]: ["organization_roles"], + [UserScope.CustomData]: [], + [UserScope.Identities]: [], + [UserScope.Sessions]: [] + }); + var userinfoClaims = Object.freeze({ + [UserScope.Profile]: [], + [UserScope.Email]: [], + [UserScope.Phone]: [], + [UserScope.Address]: [], + [UserScope.Roles]: [], + [UserScope.Organizations]: [], + [UserScope.OrganizationRoles]: [], + [UserScope.CustomData]: ["custom_data"], + [UserScope.Identities]: ["identities"], + [UserScope.Sessions]: [] + }); + var userClaims = Object.freeze( + // Hard to infer type directly, use `as` for a workaround. + // eslint-disable-next-line no-restricted-syntax + Object.fromEntries(Object.values(UserScope).map((current) => [ + current, + [...idTokenClaims[current], ...userinfoClaims[current]] + ])) + ); + + // node_modules/@logto/js/lib/consts/index.js + var ContentType = { + formUrlEncoded: { "Content-Type": "application/x-www-form-urlencoded" } + }; + var TokenGrantType; + (function(TokenGrantType2) { + TokenGrantType2["AuthorizationCode"] = "authorization_code"; + TokenGrantType2["RefreshToken"] = "refresh_token"; + })(TokenGrantType || (TokenGrantType = {})); + var QueryKey; + (function(QueryKey2) { + QueryKey2["ClientId"] = "client_id"; + QueryKey2["Code"] = "code"; + QueryKey2["CodeChallenge"] = "code_challenge"; + QueryKey2["CodeChallengeMethod"] = "code_challenge_method"; + QueryKey2["CodeVerifier"] = "code_verifier"; + QueryKey2["Error"] = "error"; + QueryKey2["ErrorDescription"] = "error_description"; + QueryKey2["GrantType"] = "grant_type"; + QueryKey2["IdToken"] = "id_token"; + QueryKey2["IdTokenHint"] = "id_token_hint"; + QueryKey2["LoginHint"] = "login_hint"; + QueryKey2["PostLogoutRedirectUri"] = "post_logout_redirect_uri"; + QueryKey2["Prompt"] = "prompt"; + QueryKey2["RedirectUri"] = "redirect_uri"; + QueryKey2["RefreshToken"] = "refresh_token"; + QueryKey2["Resource"] = "resource"; + QueryKey2["ResponseType"] = "response_type"; + QueryKey2["Scope"] = "scope"; + QueryKey2["State"] = "state"; + QueryKey2["Token"] = "token"; + QueryKey2["InteractionMode"] = "interaction_mode"; + QueryKey2["OrganizationId"] = "organization_id"; + QueryKey2["FirstScreen"] = "first_screen"; + QueryKey2["Identifier"] = "identifier"; + QueryKey2["DirectSignIn"] = "direct_sign_in"; + QueryKey2["OneTimeToken"] = "one_time_token"; + })(QueryKey || (QueryKey = {})); + var Prompt; + (function(Prompt2) { + Prompt2["None"] = "none"; + Prompt2["Consent"] = "consent"; + Prompt2["Login"] = "login"; + })(Prompt || (Prompt = {})); + + // node_modules/@logto/js/lib/core/fetch-token.js + var fetchTokenByAuthorizationCode = async ({ clientId, tokenEndpoint, redirectUri, codeVerifier, code, resource }, requester) => { + const parameters = new URLSearchParams(); + parameters.append(QueryKey.ClientId, clientId); + parameters.append(QueryKey.Code, code); + parameters.append(QueryKey.CodeVerifier, codeVerifier); + parameters.append(QueryKey.RedirectUri, redirectUri); + parameters.append(QueryKey.GrantType, TokenGrantType.AuthorizationCode); + if (resource) { + parameters.append(QueryKey.Resource, resource); + } + const snakeCaseCodeTokenResponse = await requester(tokenEndpoint, { + method: "POST", + headers: ContentType.formUrlEncoded, + body: parameters.toString() + }); + return camelcaseKeys(snakeCaseCodeTokenResponse); + }; + var fetchTokenByRefreshToken = async (params, requester) => { + const { clientId, tokenEndpoint, refreshToken, resource, organizationId, scopes } = params; + const parameters = new URLSearchParams(); + parameters.append(QueryKey.ClientId, clientId); + parameters.append(QueryKey.RefreshToken, refreshToken); + parameters.append(QueryKey.GrantType, TokenGrantType.RefreshToken); + if (resource) { + parameters.append(QueryKey.Resource, resource); + } + if (organizationId) { + parameters.append(QueryKey.OrganizationId, organizationId); + } + if (scopes?.length) { + parameters.append(QueryKey.Scope, scopes.join(" ")); + } + const snakeCaseRefreshTokenTokenResponse = await requester(tokenEndpoint, { + method: "POST", + headers: ContentType.formUrlEncoded, + body: parameters.toString() + }); + return camelcaseKeys(snakeCaseRefreshTokenTokenResponse); + }; + + // node_modules/@logto/js/lib/core/oidc-config.js + var discoveryPath = "/oidc/.well-known/openid-configuration"; + var fetchOidcConfig = async (endpoint, requester) => camelcaseKeys(await requester(endpoint)); + + // node_modules/@logto/js/lib/core/revoke.js + var revoke = async (revocationEndpoint, clientId, token, requester) => requester(revocationEndpoint, { + method: "POST", + headers: ContentType.formUrlEncoded, + body: new URLSearchParams({ + [QueryKey.ClientId]: clientId, + [QueryKey.Token]: token + }).toString() + }); + + // node_modules/@logto/js/lib/utils/scopes.js + var withReservedScopes = (originalScopes) => { + const reservedScopes = Object.values(ReservedScope); + const uniqueScopes = /* @__PURE__ */ new Set([...reservedScopes, UserScope.Profile, ...originalScopes ?? []]); + return Array.from(uniqueScopes).join(" "); + }; + + // node_modules/@logto/js/lib/core/sign-in.js + var codeChallengeMethod = "S256"; + var responseType = "code"; + var buildPrompt = (prompt) => { + if (Array.isArray(prompt)) { + return prompt.join(" "); + } + return prompt ?? Prompt.Consent; + }; + var generateSignInUri = ({ authorizationEndpoint, clientId, redirectUri, codeChallenge, state, scopes, resources, prompt, firstScreen, identifiers: identifier, interactionMode, loginHint, directSignIn, oneTimeToken, extraParams, includeReservedScopes = true }) => { + const urlSearchParameters = new URLSearchParams({ + [QueryKey.ClientId]: clientId, + [QueryKey.RedirectUri]: redirectUri, + [QueryKey.CodeChallenge]: codeChallenge, + [QueryKey.CodeChallengeMethod]: codeChallengeMethod, + [QueryKey.State]: state, + [QueryKey.ResponseType]: responseType, + [QueryKey.Prompt]: buildPrompt(prompt) + }); + const computedScopes = includeReservedScopes ? withReservedScopes(scopes) : scopes?.join(" "); + if (computedScopes) { + urlSearchParameters.append(QueryKey.Scope, computedScopes); + } + if (loginHint) { + urlSearchParameters.append(QueryKey.LoginHint, loginHint); + } + if (directSignIn) { + urlSearchParameters.append(QueryKey.DirectSignIn, `${directSignIn.method}:${directSignIn.target}`); + } + for (const resource of resources ?? []) { + urlSearchParameters.append(QueryKey.Resource, resource); + } + if (firstScreen) { + urlSearchParameters.append(QueryKey.FirstScreen, firstScreen); + } else if (interactionMode) { + urlSearchParameters.append(QueryKey.InteractionMode, interactionMode); + } + if (identifier && identifier.length > 0) { + urlSearchParameters.append(QueryKey.Identifier, identifier.join(" ")); + } + if (oneTimeToken) { + urlSearchParameters.append(QueryKey.OneTimeToken, oneTimeToken); + } + if (extraParams) { + for (const [key, value] of Object.entries(extraParams)) { + urlSearchParameters.append(key, value); + } + } + return `${authorizationEndpoint}?${urlSearchParameters.toString()}`; + }; + + // node_modules/@logto/js/lib/core/sign-out.js + var generateSignOutUri = ({ endSessionEndpoint, clientId, postLogoutRedirectUri }) => { + const urlSearchParameters = new URLSearchParams({ [QueryKey.ClientId]: clientId }); + if (postLogoutRedirectUri) { + urlSearchParameters.append(QueryKey.PostLogoutRedirectUri, postLogoutRedirectUri); + } + return `${endSessionEndpoint}?${urlSearchParameters.toString()}`; + }; + + // node_modules/@logto/js/lib/core/user-info.js + var fetchUserInfo = async (userInfoEndpoint, accessToken, requester) => requester(userInfoEndpoint, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + + // node_modules/@silverhand/essentials/lib/utilities/array.js + var deduplicate = (array) => [...new Set(array)]; + + // node_modules/@silverhand/essentials/lib/utilities/assertions.js + var notFalsy = (value) => Boolean(value); + + // node_modules/@silverhand/essentials/lib/utilities/conditional.js + var conditional = (exp) => notFalsy(exp) ? exp : void 0; + var conditionalString = (exp) => notFalsy(exp) ? String(exp) : ""; + + // node_modules/@silverhand/essentials/lib/utilities/function.js + var isPromise = (value) => value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function"; + var trySafe = (exec, onError) => { + try { + const unwrapped = typeof exec === "function" ? exec() : exec; + return isPromise(unwrapped) ? ( + // eslint-disable-next-line promise/prefer-await-to-then + unwrapped.catch((error) => { + onError?.(error); + }) + ) : unwrapped; + } catch (error) { + onError?.(error); + } + }; + + // node_modules/@silverhand/essentials/lib/utilities/string.js + var replaceNonUrlSafeCharacters = (base64String) => base64String.replaceAll("+", "-").replaceAll("/", "_").replaceAll(/=+$/g, ""); + var restoreNonUrlSafeCharacters = (base64String) => base64String.replaceAll("-", "+").replaceAll("_", "/"); + var urlSafeBase64 = { + isSafe: (input) => /^[\w-]*$/.test(input), + encode: (rawString) => { + const encodedString = btoa(unescape(encodeURIComponent(rawString))); + return replaceNonUrlSafeCharacters(encodedString); + }, + decode: (encodedString) => { + const nonUrlSafeEncodedString = restoreNonUrlSafeCharacters(encodedString); + return decodeURIComponent(escape(atob(nonUrlSafeEncodedString))); + }, + replaceNonUrlSafeCharacters, + restoreNonUrlSafeCharacters + }; + + // node_modules/@silverhand/essentials/lib/utilities/url.js + var joinPath = (...segments) => { + const result = []; + for (const segment of segments.join("/").split("/")) { + if (!segment || segment === ".") { + continue; + } + if ([...segment].every((char) => char === ".")) { + result.pop(); + continue; + } + result.push(segment); + } + return "/" + result.join("/"); + }; + var appendPath = (baseUrl, ...pathnames) => new URL(joinPath(baseUrl.pathname, ...pathnames), baseUrl); + + // node_modules/@logto/js/lib/utils/arbitrary-object.js + var isArbitraryObject = (data) => typeof data === "object" && data !== null; + + // node_modules/@logto/js/lib/utils/errors.js + var logtoErrorCodes = Object.freeze({ + "id_token.invalid_iat": "Invalid issued at time in the ID token", + "id_token.invalid_token": "Invalid ID token", + "callback_uri_verification.redirect_uri_mismatched": "The callback URI mismatches the redirect URI.", + "callback_uri_verification.error_found": "Error found in the callback URI", + "callback_uri_verification.missing_state": "Missing state in the callback URI", + "callback_uri_verification.state_mismatched": "State mismatched in the callback URI", + "callback_uri_verification.missing_code": "Missing code in the callback URI", + crypto_subtle_unavailable: "Crypto.subtle is unavailable in insecure contexts (non-HTTPS).", + unexpected_response_error: "Unexpected response error from the server." + }); + var LogtoError = class extends Error { + constructor(code, data) { + super(logtoErrorCodes[code]); + this.code = code; + this.data = data; + this.name = "LogtoError"; + } + }; + var isLogtoRequestErrorJson = (data) => { + if (!isArbitraryObject(data)) { + return false; + } + return typeof data.code === "string" && typeof data.message === "string"; + }; + var LogtoRequestError = class extends Error { + constructor(code, message2, cause) { + super(message2); + this.code = code; + this.cause = cause; + this.name = "LogtoRequestError"; + } + }; + var OidcError = class { + constructor(error, errorDescription) { + this.error = error; + this.errorDescription = errorDescription; + this.name = "OidcError"; + } + }; + + // node_modules/@logto/js/lib/utils/callback-uri.js + var parseUriParameters = (uri) => { + const [, queryString = ""] = uri.split("?"); + return new URLSearchParams(queryString); + }; + var verifyAndParseCodeFromCallbackUri = (callbackUri, redirectUri, state) => { + if (!callbackUri.startsWith(redirectUri)) { + throw new LogtoError("callback_uri_verification.redirect_uri_mismatched"); + } + const uriParameters = parseUriParameters(callbackUri); + const error = conditional(uriParameters.get(QueryKey.Error)); + const errorDescription = conditional(uriParameters.get(QueryKey.ErrorDescription)); + if (error) { + throw new LogtoError("callback_uri_verification.error_found", new OidcError(error, errorDescription)); + } + const stateFromCallbackUri = uriParameters.get(QueryKey.State); + if (!stateFromCallbackUri) { + throw new LogtoError("callback_uri_verification.missing_state"); + } + if (stateFromCallbackUri !== state) { + throw new LogtoError("callback_uri_verification.state_mismatched"); + } + const code = uriParameters.get(QueryKey.Code); + if (!code) { + throw new LogtoError("callback_uri_verification.missing_code"); + } + return code; + }; + + // node_modules/@logto/js/lib/utils/id-token.js + function assertIdTokenClaims(data) { + if (!isArbitraryObject(data)) { + throw new TypeError("IdToken is expected to be an object"); + } + for (const key of ["iss", "sub", "aud"]) { + if (typeof data[key] !== "string") { + throw new TypeError(`At path: IdToken.${key}: expected a string`); + } + } + for (const key of ["exp", "iat"]) { + if (typeof data[key] !== "number") { + throw new TypeError(`At path: IdToken.${key}: expected a number`); + } + } + for (const key of ["at_hash", "name", "username", "picture", "email", "phone_number"]) { + if (data[key] === void 0) { + continue; + } + if (typeof data[key] !== "string" && data[key] !== null) { + throw new TypeError(`At path: IdToken.${key}: expected null or a string`); + } + } + for (const key of ["email_verified", "phone_number_verified"]) { + if (data[key] === void 0) { + continue; + } + if (typeof data[key] !== "boolean") { + throw new TypeError(`At path: IdToken.${key}: expected a boolean`); + } + } + } + var decodeIdToken = (token) => { + const { 1: encodedPayload } = token.split("."); + if (!encodedPayload) { + throw new LogtoError("id_token.invalid_token"); + } + const json = urlSafeBase64.decode(encodedPayload); + const idTokenClaims2 = JSON.parse(json); + assertIdTokenClaims(idTokenClaims2); + return idTokenClaims2; + }; + + // node_modules/@logto/js/lib/utils/access-token.js + function assertAccessTokenClaims(data) { + if (!isArbitraryObject(data)) { + throw new TypeError("AccessToken is expected to be an object"); + } + for (const key of ["jti", "iss", "sub", "aud", "client_id", "scope"]) { + if (data[key] === void 0) { + continue; + } + if (typeof data[key] !== "string" && data[key] !== null) { + throw new TypeError(`At path: AccessToken.${key}: expected null or a string`); + } + } + for (const key of ["exp", "iat"]) { + if (data[key] === void 0) { + continue; + } + if (typeof data[key] !== "number" && data[key] !== null) { + throw new TypeError(`At path: AccessToken.${key}: expected null or a number`); + } + } + } + var decodeAccessToken = (accessToken) => { + const { 1: encodedPayload } = accessToken.split("."); + if (!encodedPayload) { + return {}; + } + const json = urlSafeBase64.decode(encodedPayload); + const accessTokenClaims = JSON.parse(json); + assertAccessTokenClaims(accessTokenClaims); + return accessTokenClaims; + }; + + // node_modules/jose/dist/browser/runtime/webcrypto.js + var webcrypto_default = crypto; + var isCryptoKey = (key) => key instanceof CryptoKey; + + // node_modules/jose/dist/browser/lib/buffer_utils.js + var encoder = new TextEncoder(); + var decoder = new TextDecoder(); + var MAX_INT32 = 2 ** 32; + function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0); + const buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) { + buf.set(buffer, i); + i += buffer.length; + } + return buf; + } + + // node_modules/jose/dist/browser/runtime/base64url.js + var decodeBase64 = (encoded) => { + const binary = atob(encoded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + }; + var decode = (input) => { + let encoded = input; + if (encoded instanceof Uint8Array) { + encoded = decoder.decode(encoded); + } + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""); + try { + return decodeBase64(encoded); + } catch { + throw new TypeError("The input to be decoded is not correctly encoded."); + } + }; + + // node_modules/jose/dist/browser/util/errors.js + var JOSEError = class extends Error { + constructor(message2, options) { + super(message2, options); + this.code = "ERR_JOSE_GENERIC"; + this.name = this.constructor.name; + Error.captureStackTrace?.(this, this.constructor); + } + }; + JOSEError.code = "ERR_JOSE_GENERIC"; + var JWTClaimValidationFailed = class extends JOSEError { + constructor(message2, payload, claim = "unspecified", reason = "unspecified") { + super(message2, { cause: { claim, reason, payload } }); + this.code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + this.claim = claim; + this.reason = reason; + this.payload = payload; + } + }; + JWTClaimValidationFailed.code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + var JWTExpired = class extends JOSEError { + constructor(message2, payload, claim = "unspecified", reason = "unspecified") { + super(message2, { cause: { claim, reason, payload } }); + this.code = "ERR_JWT_EXPIRED"; + this.claim = claim; + this.reason = reason; + this.payload = payload; + } + }; + JWTExpired.code = "ERR_JWT_EXPIRED"; + var JOSEAlgNotAllowed = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JOSE_ALG_NOT_ALLOWED"; + } + }; + JOSEAlgNotAllowed.code = "ERR_JOSE_ALG_NOT_ALLOWED"; + var JOSENotSupported = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JOSE_NOT_SUPPORTED"; + } + }; + JOSENotSupported.code = "ERR_JOSE_NOT_SUPPORTED"; + var JWEDecryptionFailed = class extends JOSEError { + constructor(message2 = "decryption operation failed", options) { + super(message2, options); + this.code = "ERR_JWE_DECRYPTION_FAILED"; + } + }; + JWEDecryptionFailed.code = "ERR_JWE_DECRYPTION_FAILED"; + var JWEInvalid = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JWE_INVALID"; + } + }; + JWEInvalid.code = "ERR_JWE_INVALID"; + var JWSInvalid = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JWS_INVALID"; + } + }; + JWSInvalid.code = "ERR_JWS_INVALID"; + var JWTInvalid = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JWT_INVALID"; + } + }; + JWTInvalid.code = "ERR_JWT_INVALID"; + var JWKInvalid = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JWK_INVALID"; + } + }; + JWKInvalid.code = "ERR_JWK_INVALID"; + var JWKSInvalid = class extends JOSEError { + constructor() { + super(...arguments); + this.code = "ERR_JWKS_INVALID"; + } + }; + JWKSInvalid.code = "ERR_JWKS_INVALID"; + var JWKSNoMatchingKey = class extends JOSEError { + constructor(message2 = "no applicable key found in the JSON Web Key Set", options) { + super(message2, options); + this.code = "ERR_JWKS_NO_MATCHING_KEY"; + } + }; + JWKSNoMatchingKey.code = "ERR_JWKS_NO_MATCHING_KEY"; + var JWKSMultipleMatchingKeys = class extends JOSEError { + constructor(message2 = "multiple matching keys found in the JSON Web Key Set", options) { + super(message2, options); + this.code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + } + }; + JWKSMultipleMatchingKeys.code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + var JWKSTimeout = class extends JOSEError { + constructor(message2 = "request timed out", options) { + super(message2, options); + this.code = "ERR_JWKS_TIMEOUT"; + } + }; + JWKSTimeout.code = "ERR_JWKS_TIMEOUT"; + var JWSSignatureVerificationFailed = class extends JOSEError { + constructor(message2 = "signature verification failed", options) { + super(message2, options); + this.code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + } + }; + JWSSignatureVerificationFailed.code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + + // node_modules/jose/dist/browser/lib/crypto_key.js + function unusable(name, prop = "algorithm.name") { + return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); + } + function isAlgorithm(algorithm, name) { + return algorithm.name === name; + } + function getHashLength(hash) { + return parseInt(hash.name.slice(4), 10); + } + function getNamedCurve(alg) { + switch (alg) { + case "ES256": + return "P-256"; + case "ES384": + return "P-384"; + case "ES512": + return "P-521"; + default: + throw new Error("unreachable"); + } + } + function checkUsage(key, usages) { + if (usages.length && !usages.some((expected) => key.usages.includes(expected))) { + let msg = "CryptoKey does not support this operation, its usages must include "; + if (usages.length > 2) { + const last = usages.pop(); + msg += `one of ${usages.join(", ")}, or ${last}.`; + } else if (usages.length === 2) { + msg += `one of ${usages[0]} or ${usages[1]}.`; + } else { + msg += `${usages[0]}.`; + } + throw new TypeError(msg); + } + } + function checkSigCryptoKey(key, alg, ...usages) { + switch (alg) { + case "HS256": + case "HS384": + case "HS512": { + if (!isAlgorithm(key.algorithm, "HMAC")) + throw unusable("HMAC"); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, "algorithm.hash"); + break; + } + case "RS256": + case "RS384": + case "RS512": { + if (!isAlgorithm(key.algorithm, "RSASSA-PKCS1-v1_5")) + throw unusable("RSASSA-PKCS1-v1_5"); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, "algorithm.hash"); + break; + } + case "PS256": + case "PS384": + case "PS512": { + if (!isAlgorithm(key.algorithm, "RSA-PSS")) + throw unusable("RSA-PSS"); + const expected = parseInt(alg.slice(2), 10); + const actual = getHashLength(key.algorithm.hash); + if (actual !== expected) + throw unusable(`SHA-${expected}`, "algorithm.hash"); + break; + } + case "EdDSA": { + if (key.algorithm.name !== "Ed25519" && key.algorithm.name !== "Ed448") { + throw unusable("Ed25519 or Ed448"); + } + break; + } + case "Ed25519": { + if (!isAlgorithm(key.algorithm, "Ed25519")) + throw unusable("Ed25519"); + break; + } + case "ES256": + case "ES384": + case "ES512": { + if (!isAlgorithm(key.algorithm, "ECDSA")) + throw unusable("ECDSA"); + const expected = getNamedCurve(alg); + const actual = key.algorithm.namedCurve; + if (actual !== expected) + throw unusable(expected, "algorithm.namedCurve"); + break; + } + default: + throw new TypeError("CryptoKey does not support this operation"); + } + checkUsage(key, usages); + } + + // node_modules/jose/dist/browser/lib/invalid_key_input.js + function message(msg, actual, ...types2) { + types2 = types2.filter(Boolean); + if (types2.length > 2) { + const last = types2.pop(); + msg += `one of type ${types2.join(", ")}, or ${last}.`; + } else if (types2.length === 2) { + msg += `one of type ${types2[0]} or ${types2[1]}.`; + } else { + msg += `of type ${types2[0]}.`; + } + if (actual == null) { + msg += ` Received ${actual}`; + } else if (typeof actual === "function" && actual.name) { + msg += ` Received function ${actual.name}`; + } else if (typeof actual === "object" && actual != null) { + if (actual.constructor?.name) { + msg += ` Received an instance of ${actual.constructor.name}`; + } + } + return msg; + } + var invalid_key_input_default = (actual, ...types2) => { + return message("Key must be ", actual, ...types2); + }; + function withAlg(alg, actual, ...types2) { + return message(`Key for the ${alg} algorithm must be `, actual, ...types2); + } + + // node_modules/jose/dist/browser/runtime/is_key_like.js + var is_key_like_default = (key) => { + if (isCryptoKey(key)) { + return true; + } + return key?.[Symbol.toStringTag] === "KeyObject"; + }; + var types = ["CryptoKey"]; + + // node_modules/jose/dist/browser/lib/is_disjoint.js + var isDisjoint = (...headers) => { + const sources = headers.filter(Boolean); + if (sources.length === 0 || sources.length === 1) { + return true; + } + let acc; + for (const header of sources) { + const parameters = Object.keys(header); + if (!acc || acc.size === 0) { + acc = new Set(parameters); + continue; + } + for (const parameter of parameters) { + if (acc.has(parameter)) { + return false; + } + acc.add(parameter); + } + } + return true; + }; + var is_disjoint_default = isDisjoint; + + // node_modules/jose/dist/browser/lib/is_object.js + function isObjectLike(value) { + return typeof value === "object" && value !== null; + } + function isObject4(input) { + if (!isObjectLike(input) || Object.prototype.toString.call(input) !== "[object Object]") { + return false; + } + if (Object.getPrototypeOf(input) === null) { + return true; + } + let proto = input; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + return Object.getPrototypeOf(input) === proto; + } + + // node_modules/jose/dist/browser/runtime/check_key_length.js + var check_key_length_default = (alg, key) => { + if (alg.startsWith("RS") || alg.startsWith("PS")) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength !== "number" || modulusLength < 2048) { + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); + } + } + }; + + // node_modules/jose/dist/browser/lib/is_jwk.js + function isJWK(key) { + return isObject4(key) && typeof key.kty === "string"; + } + function isPrivateJWK(key) { + return key.kty !== "oct" && typeof key.d === "string"; + } + function isPublicJWK(key) { + return key.kty !== "oct" && typeof key.d === "undefined"; + } + function isSecretJWK(key) { + return isJWK(key) && key.kty === "oct" && typeof key.k === "string"; + } + + // node_modules/jose/dist/browser/runtime/jwk_to_key.js + function subtleMapping(jwk) { + let algorithm; + let keyUsages; + switch (jwk.kty) { + case "RSA": { + switch (jwk.alg) { + case "PS256": + case "PS384": + case "PS512": + algorithm = { name: "RSA-PSS", hash: `SHA-${jwk.alg.slice(-3)}` }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "RS256": + case "RS384": + case "RS512": + algorithm = { name: "RSASSA-PKCS1-v1_5", hash: `SHA-${jwk.alg.slice(-3)}` }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "RSA-OAEP": + case "RSA-OAEP-256": + case "RSA-OAEP-384": + case "RSA-OAEP-512": + algorithm = { + name: "RSA-OAEP", + hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}` + }; + keyUsages = jwk.d ? ["decrypt", "unwrapKey"] : ["encrypt", "wrapKey"]; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + case "EC": { + switch (jwk.alg) { + case "ES256": + algorithm = { name: "ECDSA", namedCurve: "P-256" }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "ES384": + algorithm = { name: "ECDSA", namedCurve: "P-384" }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "ES512": + algorithm = { name: "ECDSA", namedCurve: "P-521" }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "ECDH-ES": + case "ECDH-ES+A128KW": + case "ECDH-ES+A192KW": + case "ECDH-ES+A256KW": + algorithm = { name: "ECDH", namedCurve: jwk.crv }; + keyUsages = jwk.d ? ["deriveBits"] : []; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + case "OKP": { + switch (jwk.alg) { + case "Ed25519": + algorithm = { name: "Ed25519" }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "EdDSA": + algorithm = { name: jwk.crv }; + keyUsages = jwk.d ? ["sign"] : ["verify"]; + break; + case "ECDH-ES": + case "ECDH-ES+A128KW": + case "ECDH-ES+A192KW": + case "ECDH-ES+A256KW": + algorithm = { name: jwk.crv }; + keyUsages = jwk.d ? ["deriveBits"] : []; + break; + default: + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + } + break; + } + default: + throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value'); + } + return { algorithm, keyUsages }; + } + var parse = async (jwk) => { + if (!jwk.alg) { + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + } + const { algorithm, keyUsages } = subtleMapping(jwk); + const rest = [ + algorithm, + jwk.ext ?? false, + jwk.key_ops ?? keyUsages + ]; + const keyData = { ...jwk }; + delete keyData.alg; + delete keyData.use; + return webcrypto_default.subtle.importKey("jwk", keyData, ...rest); + }; + var jwk_to_key_default = parse; + + // node_modules/jose/dist/browser/runtime/normalize_key.js + var exportKeyValue = (k) => decode(k); + var privCache; + var pubCache; + var isKeyObject = (key) => { + return key?.[Symbol.toStringTag] === "KeyObject"; + }; + var importAndCache = async (cache2, key, jwk, alg, freeze = false) => { + let cached = cache2.get(key); + if (cached?.[alg]) { + return cached[alg]; + } + const cryptoKey = await jwk_to_key_default({ ...jwk, alg }); + if (freeze) + Object.freeze(key); + if (!cached) { + cache2.set(key, { [alg]: cryptoKey }); + } else { + cached[alg] = cryptoKey; + } + return cryptoKey; + }; + var normalizePublicKey = (key, alg) => { + if (isKeyObject(key)) { + let jwk = key.export({ format: "jwk" }); + delete jwk.d; + delete jwk.dp; + delete jwk.dq; + delete jwk.p; + delete jwk.q; + delete jwk.qi; + if (jwk.k) { + return exportKeyValue(jwk.k); + } + pubCache || (pubCache = /* @__PURE__ */ new WeakMap()); + return importAndCache(pubCache, key, jwk, alg); + } + if (isJWK(key)) { + if (key.k) + return decode(key.k); + pubCache || (pubCache = /* @__PURE__ */ new WeakMap()); + const cryptoKey = importAndCache(pubCache, key, key, alg, true); + return cryptoKey; + } + return key; + }; + var normalizePrivateKey = (key, alg) => { + if (isKeyObject(key)) { + let jwk = key.export({ format: "jwk" }); + if (jwk.k) { + return exportKeyValue(jwk.k); + } + privCache || (privCache = /* @__PURE__ */ new WeakMap()); + return importAndCache(privCache, key, jwk, alg); + } + if (isJWK(key)) { + if (key.k) + return decode(key.k); + privCache || (privCache = /* @__PURE__ */ new WeakMap()); + const cryptoKey = importAndCache(privCache, key, key, alg, true); + return cryptoKey; + } + return key; + }; + var normalize_key_default = { normalizePublicKey, normalizePrivateKey }; + + // node_modules/jose/dist/browser/key/import.js + async function importJWK(jwk, alg) { + if (!isObject4(jwk)) { + throw new TypeError("JWK must be an object"); + } + alg || (alg = jwk.alg); + switch (jwk.kty) { + case "oct": + if (typeof jwk.k !== "string" || !jwk.k) { + throw new TypeError('missing "k" (Key Value) Parameter value'); + } + return decode(jwk.k); + case "RSA": + if ("oth" in jwk && jwk.oth !== void 0) { + throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported'); + } + case "EC": + case "OKP": + return jwk_to_key_default({ ...jwk, alg }); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } + } + + // node_modules/jose/dist/browser/lib/check_key_type.js + var tag = (key) => key?.[Symbol.toStringTag]; + var jwkMatchesOp = (alg, key, usage) => { + if (key.use !== void 0 && key.use !== "sig") { + throw new TypeError("Invalid key for this operation, when present its use must be sig"); + } + if (key.key_ops !== void 0 && key.key_ops.includes?.(usage) !== true) { + throw new TypeError(`Invalid key for this operation, when present its key_ops must include ${usage}`); + } + if (key.alg !== void 0 && key.alg !== alg) { + throw new TypeError(`Invalid key for this operation, when present its alg must be ${alg}`); + } + return true; + }; + var symmetricTypeCheck = (alg, key, usage, allowJwk) => { + if (key instanceof Uint8Array) + return; + if (allowJwk && isJWK(key)) { + if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage)) + return; + throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`); + } + if (!is_key_like_default(key)) { + throw new TypeError(withAlg(alg, key, ...types, "Uint8Array", allowJwk ? "JSON Web Key" : null)); + } + if (key.type !== "secret") { + throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`); + } + }; + var asymmetricTypeCheck = (alg, key, usage, allowJwk) => { + if (allowJwk && isJWK(key)) { + switch (usage) { + case "sign": + if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage)) + return; + throw new TypeError(`JSON Web Key for this operation be a private JWK`); + case "verify": + if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage)) + return; + throw new TypeError(`JSON Web Key for this operation be a public JWK`); + } + } + if (!is_key_like_default(key)) { + throw new TypeError(withAlg(alg, key, ...types, allowJwk ? "JSON Web Key" : null)); + } + if (key.type === "secret") { + throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`); + } + if (usage === "sign" && key.type === "public") { + throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`); + } + if (usage === "decrypt" && key.type === "public") { + throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`); + } + if (key.algorithm && usage === "verify" && key.type === "private") { + throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`); + } + if (key.algorithm && usage === "encrypt" && key.type === "private") { + throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`); + } + }; + function checkKeyType(allowJwk, alg, key, usage) { + const symmetric = alg.startsWith("HS") || alg === "dir" || alg.startsWith("PBES2") || /^A\d{3}(?:GCM)?KW$/.test(alg); + if (symmetric) { + symmetricTypeCheck(alg, key, usage, allowJwk); + } else { + asymmetricTypeCheck(alg, key, usage, allowJwk); + } + } + var check_key_type_default = checkKeyType.bind(void 0, false); + var checkKeyTypeWithJwk = checkKeyType.bind(void 0, true); + + // node_modules/jose/dist/browser/lib/validate_crit.js + function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) { + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + } + if (!protectedHeader || protectedHeader.crit === void 0) { + return /* @__PURE__ */ new Set(); + } + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== "string" || input.length === 0)) { + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + } + let recognized; + if (recognizedOption !== void 0) { + recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]); + } else { + recognized = recognizedDefault; + } + for (const parameter of protectedHeader.crit) { + if (!recognized.has(parameter)) { + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + } + if (joseHeader[parameter] === void 0) { + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + } + if (recognized.get(parameter) && protectedHeader[parameter] === void 0) { + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + } + return new Set(protectedHeader.crit); + } + var validate_crit_default = validateCrit; + + // node_modules/jose/dist/browser/lib/validate_algorithms.js + var validateAlgorithms = (option, algorithms) => { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) { + throw new TypeError(`"${option}" option must be an array of strings`); + } + if (!algorithms) { + return void 0; + } + return new Set(algorithms); + }; + var validate_algorithms_default = validateAlgorithms; + + // node_modules/jose/dist/browser/runtime/subtle_dsa.js + function subtleDsa(alg, algorithm) { + const hash = `SHA-${alg.slice(-3)}`; + switch (alg) { + case "HS256": + case "HS384": + case "HS512": + return { hash, name: "HMAC" }; + case "PS256": + case "PS384": + case "PS512": + return { hash, name: "RSA-PSS", saltLength: alg.slice(-3) >> 3 }; + case "RS256": + case "RS384": + case "RS512": + return { hash, name: "RSASSA-PKCS1-v1_5" }; + case "ES256": + case "ES384": + case "ES512": + return { hash, name: "ECDSA", namedCurve: algorithm.namedCurve }; + case "Ed25519": + return { name: "Ed25519" }; + case "EdDSA": + return { name: algorithm.name }; + default: + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + } + } + + // node_modules/jose/dist/browser/runtime/get_sign_verify_key.js + async function getCryptoKey(alg, key, usage) { + if (usage === "sign") { + key = await normalize_key_default.normalizePrivateKey(key, alg); + } + if (usage === "verify") { + key = await normalize_key_default.normalizePublicKey(key, alg); + } + if (isCryptoKey(key)) { + checkSigCryptoKey(key, alg, usage); + return key; + } + if (key instanceof Uint8Array) { + if (!alg.startsWith("HS")) { + throw new TypeError(invalid_key_input_default(key, ...types)); + } + return webcrypto_default.subtle.importKey("raw", key, { hash: `SHA-${alg.slice(-3)}`, name: "HMAC" }, false, [usage]); + } + throw new TypeError(invalid_key_input_default(key, ...types, "Uint8Array", "JSON Web Key")); + } + + // node_modules/jose/dist/browser/runtime/verify.js + var verify = async (alg, key, signature, data) => { + const cryptoKey = await getCryptoKey(alg, key, "verify"); + check_key_length_default(alg, cryptoKey); + const algorithm = subtleDsa(alg, cryptoKey.algorithm); + try { + return await webcrypto_default.subtle.verify(algorithm, cryptoKey, signature, data); + } catch { + return false; + } + }; + var verify_default = verify; + + // node_modules/jose/dist/browser/jws/flattened/verify.js + async function flattenedVerify(jws, key, options) { + if (!isObject4(jws)) { + throw new JWSInvalid("Flattened JWS must be an object"); + } + if (jws.protected === void 0 && jws.header === void 0) { + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + } + if (jws.protected !== void 0 && typeof jws.protected !== "string") { + throw new JWSInvalid("JWS Protected Header incorrect type"); + } + if (jws.payload === void 0) { + throw new JWSInvalid("JWS Payload missing"); + } + if (typeof jws.signature !== "string") { + throw new JWSInvalid("JWS Signature missing or incorrect type"); + } + if (jws.header !== void 0 && !isObject4(jws.header)) { + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + } + let parsedProt = {}; + if (jws.protected) { + try { + const protectedHeader = decode(jws.protected); + parsedProt = JSON.parse(decoder.decode(protectedHeader)); + } catch { + throw new JWSInvalid("JWS Protected Header is invalid"); + } + } + if (!is_disjoint_default(parsedProt, jws.header)) { + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + } + const joseHeader = { + ...parsedProt, + ...jws.header + }; + const extensions = validate_crit_default(JWSInvalid, /* @__PURE__ */ new Map([["b64", true]]), options?.crit, parsedProt, joseHeader); + let b64 = true; + if (extensions.has("b64")) { + b64 = parsedProt.b64; + if (typeof b64 !== "boolean") { + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + } + } + const { alg } = joseHeader; + if (typeof alg !== "string" || !alg) { + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + } + const algorithms = options && validate_algorithms_default("algorithms", options.algorithms); + if (algorithms && !algorithms.has(alg)) { + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + } + if (b64) { + if (typeof jws.payload !== "string") { + throw new JWSInvalid("JWS Payload must be a string"); + } + } else if (typeof jws.payload !== "string" && !(jws.payload instanceof Uint8Array)) { + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + } + let resolvedKey = false; + if (typeof key === "function") { + key = await key(parsedProt, jws); + resolvedKey = true; + checkKeyTypeWithJwk(alg, key, "verify"); + if (isJWK(key)) { + key = await importJWK(key, alg); + } + } else { + checkKeyTypeWithJwk(alg, key, "verify"); + } + const data = concat(encoder.encode(jws.protected ?? ""), encoder.encode("."), typeof jws.payload === "string" ? encoder.encode(jws.payload) : jws.payload); + let signature; + try { + signature = decode(jws.signature); + } catch { + throw new JWSInvalid("Failed to base64url decode the signature"); + } + const verified = await verify_default(alg, key, signature, data); + if (!verified) { + throw new JWSSignatureVerificationFailed(); + } + let payload; + if (b64) { + try { + payload = decode(jws.payload); + } catch { + throw new JWSInvalid("Failed to base64url decode the payload"); + } + } else if (typeof jws.payload === "string") { + payload = encoder.encode(jws.payload); + } else { + payload = jws.payload; + } + const result = { payload }; + if (jws.protected !== void 0) { + result.protectedHeader = parsedProt; + } + if (jws.header !== void 0) { + result.unprotectedHeader = jws.header; + } + if (resolvedKey) { + return { ...result, key }; + } + return result; + } + + // node_modules/jose/dist/browser/jws/compact/verify.js + async function compactVerify(jws, key, options) { + if (jws instanceof Uint8Array) { + jws = decoder.decode(jws); + } + if (typeof jws !== "string") { + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + } + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) { + throw new JWSInvalid("Invalid Compact JWS"); + } + const verified = await flattenedVerify({ payload, protected: protectedHeader, signature }, key, options); + const result = { payload: verified.payload, protectedHeader: verified.protectedHeader }; + if (typeof key === "function") { + return { ...result, key: verified.key }; + } + return result; + } + + // node_modules/jose/dist/browser/lib/epoch.js + var epoch_default = (date) => Math.floor(date.getTime() / 1e3); + + // node_modules/jose/dist/browser/lib/secs.js + var minute = 60; + var hour = minute * 60; + var day = hour * 24; + var week = day * 7; + var year = day * 365.25; + var REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i; + var secs_default = (str) => { + const matched = REGEX.exec(str); + if (!matched || matched[4] && matched[1]) { + throw new TypeError("Invalid time period format"); + } + const value = parseFloat(matched[2]); + const unit = matched[3].toLowerCase(); + let numericDate; + switch (unit) { + case "sec": + case "secs": + case "second": + case "seconds": + case "s": + numericDate = Math.round(value); + break; + case "minute": + case "minutes": + case "min": + case "mins": + case "m": + numericDate = Math.round(value * minute); + break; + case "hour": + case "hours": + case "hr": + case "hrs": + case "h": + numericDate = Math.round(value * hour); + break; + case "day": + case "days": + case "d": + numericDate = Math.round(value * day); + break; + case "week": + case "weeks": + case "w": + numericDate = Math.round(value * week); + break; + default: + numericDate = Math.round(value * year); + break; + } + if (matched[1] === "-" || matched[4] === "ago") { + return -numericDate; + } + return numericDate; + }; + + // node_modules/jose/dist/browser/lib/jwt_claims_set.js + var normalizeTyp = (value) => value.toLowerCase().replace(/^application\//, ""); + var checkAudiencePresence = (audPayload, audOption) => { + if (typeof audPayload === "string") { + return audOption.includes(audPayload); + } + if (Array.isArray(audPayload)) { + return audOption.some(Set.prototype.has.bind(new Set(audPayload))); + } + return false; + }; + var jwt_claims_set_default = (protectedHeader, encodedPayload, options = {}) => { + let payload; + try { + payload = JSON.parse(decoder.decode(encodedPayload)); + } catch { + } + if (!isObject4(payload)) { + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + } + const { typ } = options; + if (typ && (typeof protectedHeader.typ !== "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) { + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", "check_failed"); + } + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options; + const presenceCheck = [...requiredClaims]; + if (maxTokenAge !== void 0) + presenceCheck.push("iat"); + if (audience !== void 0) + presenceCheck.push("aud"); + if (subject !== void 0) + presenceCheck.push("sub"); + if (issuer !== void 0) + presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) { + if (!(claim in payload)) { + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + } + } + if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) { + throw new JWTClaimValidationFailed('unexpected "iss" claim value', payload, "iss", "check_failed"); + } + if (subject && payload.sub !== subject) { + throw new JWTClaimValidationFailed('unexpected "sub" claim value', payload, "sub", "check_failed"); + } + if (audience && !checkAudiencePresence(payload.aud, typeof audience === "string" ? [audience] : audience)) { + throw new JWTClaimValidationFailed('unexpected "aud" claim value', payload, "aud", "check_failed"); + } + let tolerance; + switch (typeof options.clockTolerance) { + case "string": + tolerance = secs_default(options.clockTolerance); + break; + case "number": + tolerance = options.clockTolerance; + break; + case "undefined": + tolerance = 0; + break; + default: + throw new TypeError("Invalid clockTolerance option type"); + } + const { currentDate } = options; + const now = epoch_default(currentDate || /* @__PURE__ */ new Date()); + if ((payload.iat !== void 0 || maxTokenAge) && typeof payload.iat !== "number") { + throw new JWTClaimValidationFailed('"iat" claim must be a number', payload, "iat", "invalid"); + } + if (payload.nbf !== void 0) { + if (typeof payload.nbf !== "number") { + throw new JWTClaimValidationFailed('"nbf" claim must be a number', payload, "nbf", "invalid"); + } + if (payload.nbf > now + tolerance) { + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", "check_failed"); + } + } + if (payload.exp !== void 0) { + if (typeof payload.exp !== "number") { + throw new JWTClaimValidationFailed('"exp" claim must be a number', payload, "exp", "invalid"); + } + if (payload.exp <= now - tolerance) { + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", "check_failed"); + } + } + if (maxTokenAge) { + const age = now - payload.iat; + const max = typeof maxTokenAge === "number" ? maxTokenAge : secs_default(maxTokenAge); + if (age - tolerance > max) { + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", "check_failed"); + } + if (age < 0 - tolerance) { + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", "check_failed"); + } + } + return payload; + }; + + // node_modules/jose/dist/browser/jwt/verify.js + async function jwtVerify(jwt, key, options) { + const verified = await compactVerify(jwt, key, options); + if (verified.protectedHeader.crit?.includes("b64") && verified.protectedHeader.b64 === false) { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + } + const payload = jwt_claims_set_default(verified.protectedHeader, verified.payload, options); + const result = { payload, protectedHeader: verified.protectedHeader }; + if (typeof key === "function") { + return { ...result, key: verified.key }; + } + return result; + } + + // node_modules/jose/dist/browser/jwks/local.js + function getKtyFromAlg(alg) { + switch (typeof alg === "string" && alg.slice(0, 2)) { + case "RS": + case "PS": + return "RSA"; + case "ES": + return "EC"; + case "Ed": + return "OKP"; + default: + throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); + } + } + function isJWKSLike(jwks) { + return jwks && typeof jwks === "object" && Array.isArray(jwks.keys) && jwks.keys.every(isJWKLike); + } + function isJWKLike(key) { + return isObject4(key); + } + function clone(obj) { + if (typeof structuredClone === "function") { + return structuredClone(obj); + } + return JSON.parse(JSON.stringify(obj)); + } + var LocalJWKSet = class { + constructor(jwks) { + this._cached = /* @__PURE__ */ new WeakMap(); + if (!isJWKSLike(jwks)) { + throw new JWKSInvalid("JSON Web Key Set malformed"); + } + this._jwks = clone(jwks); + } + async getKey(protectedHeader, token) { + const { alg, kid } = { ...protectedHeader, ...token?.header }; + const kty = getKtyFromAlg(alg); + const candidates = this._jwks.keys.filter((jwk2) => { + let candidate = kty === jwk2.kty; + if (candidate && typeof kid === "string") { + candidate = kid === jwk2.kid; + } + if (candidate && typeof jwk2.alg === "string") { + candidate = alg === jwk2.alg; + } + if (candidate && typeof jwk2.use === "string") { + candidate = jwk2.use === "sig"; + } + if (candidate && Array.isArray(jwk2.key_ops)) { + candidate = jwk2.key_ops.includes("verify"); + } + if (candidate) { + switch (alg) { + case "ES256": + candidate = jwk2.crv === "P-256"; + break; + case "ES256K": + candidate = jwk2.crv === "secp256k1"; + break; + case "ES384": + candidate = jwk2.crv === "P-384"; + break; + case "ES512": + candidate = jwk2.crv === "P-521"; + break; + case "Ed25519": + candidate = jwk2.crv === "Ed25519"; + break; + case "EdDSA": + candidate = jwk2.crv === "Ed25519" || jwk2.crv === "Ed448"; + break; + } + } + return candidate; + }); + const { 0: jwk, length } = candidates; + if (length === 0) { + throw new JWKSNoMatchingKey(); + } + if (length !== 1) { + const error = new JWKSMultipleMatchingKeys(); + const { _cached } = this; + error[Symbol.asyncIterator] = async function* () { + for (const jwk2 of candidates) { + try { + yield await importWithAlgCache(_cached, jwk2, alg); + } catch { + } + } + }; + throw error; + } + return importWithAlgCache(this._cached, jwk, alg); + } + }; + async function importWithAlgCache(cache2, jwk, alg) { + const cached = cache2.get(jwk) || cache2.set(jwk, {}).get(jwk); + if (cached[alg] === void 0) { + const key = await importJWK({ ...jwk, ext: true }, alg); + if (key instanceof Uint8Array || key.type !== "public") { + throw new JWKSInvalid("JSON Web Key Set members must be public keys"); + } + cached[alg] = key; + } + return cached[alg]; + } + function createLocalJWKSet(jwks) { + const set = new LocalJWKSet(jwks); + const localJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); + Object.defineProperties(localJWKSet, { + jwks: { + value: () => clone(set._jwks), + enumerable: true, + configurable: false, + writable: false + } + }); + return localJWKSet; + } + + // node_modules/jose/dist/browser/runtime/fetch_jwks.js + var fetchJwks = async (url, timeout, options) => { + let controller; + let id; + let timedOut = false; + if (typeof AbortController === "function") { + controller = new AbortController(); + id = setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeout); + } + const response = await fetch(url.href, { + signal: controller ? controller.signal : void 0, + redirect: "manual", + headers: options.headers + }).catch((err) => { + if (timedOut) + throw new JWKSTimeout(); + throw err; + }); + if (id !== void 0) + clearTimeout(id); + if (response.status !== 200) { + throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); + } + try { + return await response.json(); + } catch { + throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); + } + }; + var fetch_jwks_default = fetchJwks; + + // node_modules/jose/dist/browser/jwks/remote.js + function isCloudflareWorkers() { + return typeof WebSocketPair !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel"; + } + var USER_AGENT; + if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) { + const NAME = "jose"; + const VERSION = "v5.10.0"; + USER_AGENT = `${NAME}/${VERSION}`; + } + var jwksCache = /* @__PURE__ */ Symbol(); + function isFreshJwksCache(input, cacheMaxAge) { + if (typeof input !== "object" || input === null) { + return false; + } + if (!("uat" in input) || typeof input.uat !== "number" || Date.now() - input.uat >= cacheMaxAge) { + return false; + } + if (!("jwks" in input) || !isObject4(input.jwks) || !Array.isArray(input.jwks.keys) || !Array.prototype.every.call(input.jwks.keys, isObject4)) { + return false; + } + return true; + } + var RemoteJWKSet = class { + constructor(url, options) { + if (!(url instanceof URL)) { + throw new TypeError("url must be an instance of URL"); + } + this._url = new URL(url.href); + this._options = { agent: options?.agent, headers: options?.headers }; + this._timeoutDuration = typeof options?.timeoutDuration === "number" ? options?.timeoutDuration : 5e3; + this._cooldownDuration = typeof options?.cooldownDuration === "number" ? options?.cooldownDuration : 3e4; + this._cacheMaxAge = typeof options?.cacheMaxAge === "number" ? options?.cacheMaxAge : 6e5; + if (options?.[jwksCache] !== void 0) { + this._cache = options?.[jwksCache]; + if (isFreshJwksCache(options?.[jwksCache], this._cacheMaxAge)) { + this._jwksTimestamp = this._cache.uat; + this._local = createLocalJWKSet(this._cache.jwks); + } + } + } + coolingDown() { + return typeof this._jwksTimestamp === "number" ? Date.now() < this._jwksTimestamp + this._cooldownDuration : false; + } + fresh() { + return typeof this._jwksTimestamp === "number" ? Date.now() < this._jwksTimestamp + this._cacheMaxAge : false; + } + async getKey(protectedHeader, token) { + if (!this._local || !this.fresh()) { + await this.reload(); + } + try { + return await this._local(protectedHeader, token); + } catch (err) { + if (err instanceof JWKSNoMatchingKey) { + if (this.coolingDown() === false) { + await this.reload(); + return this._local(protectedHeader, token); + } + } + throw err; + } + } + async reload() { + if (this._pendingFetch && isCloudflareWorkers()) { + this._pendingFetch = void 0; + } + const headers = new Headers(this._options.headers); + if (USER_AGENT && !headers.has("User-Agent")) { + headers.set("User-Agent", USER_AGENT); + this._options.headers = Object.fromEntries(headers.entries()); + } + this._pendingFetch || (this._pendingFetch = fetch_jwks_default(this._url, this._timeoutDuration, this._options).then((json) => { + this._local = createLocalJWKSet(json); + if (this._cache) { + this._cache.uat = Date.now(); + this._cache.jwks = json; + } + this._jwksTimestamp = Date.now(); + this._pendingFetch = void 0; + }).catch((err) => { + this._pendingFetch = void 0; + throw err; + })); + await this._pendingFetch; + } + }; + function createRemoteJWKSet(url, options) { + const set = new RemoteJWKSet(url, options); + const remoteJWKSet = async (protectedHeader, token) => set.getKey(protectedHeader, token); + Object.defineProperties(remoteJWKSet, { + coolingDown: { + get: () => set.coolingDown(), + enumerable: true, + configurable: false + }, + fresh: { + get: () => set.fresh(), + enumerable: true, + configurable: false + }, + reload: { + value: () => set.reload(), + enumerable: true, + configurable: false, + writable: false + }, + reloading: { + get: () => !!set._pendingFetch, + enumerable: true, + configurable: false + }, + jwks: { + value: () => set._local?.jwks(), + enumerable: true, + configurable: false, + writable: false + } + }); + return remoteJWKSet; + } + + // node_modules/@logto/client/lib/adapter/defaults.js + var defaultClockTolerance = 300; + var verifyIdToken = async (idToken, clientId, issuer, jwks, clockTolerance = defaultClockTolerance) => { + const result = await jwtVerify(idToken, jwks, { audience: clientId, issuer, clockTolerance }); + if (Math.abs((result.payload.iat ?? 0) - Date.now() / 1e3) > clockTolerance) { + throw new LogtoError("id_token.invalid_iat"); + } + }; + var DefaultJwtVerifier = class { + constructor(client, clockTolerance = defaultClockTolerance) { + this.client = client; + this.clockTolerance = clockTolerance; + } + async verifyIdToken(idToken) { + const { appId } = this.client.logtoConfig; + const { issuer, jwksUri } = await this.client.getOidcConfig(); + this.getJwtVerifyGetKey ||= createRemoteJWKSet(new URL(jwksUri)); + await verifyIdToken(idToken, appId, issuer, this.getJwtVerifyGetKey, this.clockTolerance); + } + }; + + // node_modules/@logto/client/lib/adapter/types.js + var PersistKey; + (function(PersistKey2) { + PersistKey2["IdToken"] = "idToken"; + PersistKey2["RefreshToken"] = "refreshToken"; + PersistKey2["AccessToken"] = "accessToken"; + PersistKey2["SignInSession"] = "signInSession"; + })(PersistKey || (PersistKey = {})); + var CacheKey; + (function(CacheKey2) { + CacheKey2["OpenidConfig"] = "openidConfiguration"; + CacheKey2["Jwks"] = "jwks"; + })(CacheKey || (CacheKey = {})); + + // node_modules/@logto/client/lib/adapter/index.js + var ClientAdapterInstance = class { + /* END OF IMPLEMENTATION */ + constructor(adapter) { + Object.assign(this, adapter); + } + async setStorageItem(key, value) { + if (!value) { + await this.storage.removeItem(key); + return; + } + await this.storage.setItem(key, value); + } + /** + * Try to get the string value from the cache and parse as JSON. + * Return the parsed value if it is an object, return `undefined` otherwise. + * + * @param key The cache key to get value from. + */ + async getCachedObject(key) { + const cached = await trySafe(async () => { + const data = await this.unstable_cache?.getItem(key); + return conditional(data && JSON.parse(data)); + }); + if (cached && typeof cached === "object") { + return cached; + } + } + /** + * Try to get the value from the cache first, if it doesn't exist in cache, + * run the getter function and store the result into cache. + * + * @param key The cache key to get value from. + */ + async getWithCache(key, getter) { + const cached = await this.getCachedObject(key); + if (cached) { + return cached; + } + const result = await getter(); + await this.unstable_cache?.setItem(key, JSON.stringify(result)); + return result; + } + }; + + // node_modules/@logto/client/lib/errors.js + var logtoClientErrorCodes = Object.freeze({ + "sign_in_session.invalid": "Invalid sign-in session.", + "sign_in_session.not_found": "Sign-in session not found.", + not_authenticated: "Not authenticated.", + fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.", + user_cancelled: "The user cancelled the action.", + missing_scope_organizations: `The \`${UserScope.Organizations}\` scope is required` + }); + var LogtoClientError = class extends Error { + constructor(code, data) { + super(logtoClientErrorCodes[code]); + this.name = "LogtoClientError"; + this.code = code; + this.data = data; + } + }; + + // node_modules/@logto/client/lib/types/index.js + var normalizeLogtoConfig = (config) => { + const { prompt = Prompt.Consent, scopes = [], resources, ...rest } = config; + const includeReservedScopes = config.includeReservedScopes ?? true; + return { + ...rest, + prompt, + scopes: includeReservedScopes ? withReservedScopes(scopes).split(" ") : scopes, + resources: scopes.includes(UserScope.Organizations) ? deduplicate([...resources ?? [], ReservedResource.Organization]) : resources, + includeReservedScopes + }; + }; + var isLogtoSignInSessionItem = (data) => { + if (!isArbitraryObject(data)) { + return false; + } + return ["redirectUri", "codeVerifier", "state"].every((key) => typeof data[key] === "string"); + }; + var isLogtoAccessTokenMap = (data) => { + if (!isArbitraryObject(data)) { + return false; + } + return Object.values(data).every((value) => { + if (!isArbitraryObject(value)) { + return false; + } + return typeof value.token === "string" && typeof value.scope === "string" && typeof value.expiresAt === "number"; + }); + }; + + // node_modules/@logto/client/lib/utils/index.js + var buildAccessTokenKey = (resource = "", organizationId, scopes = []) => `${scopes.slice().sort().join(" ")}@${resource}${conditionalString(organizationId && `#${organizationId}`)}`; + var getDiscoveryEndpoint = (endpoint) => appendPath(new URL(endpoint), discoveryPath).toString(); + + // node_modules/@logto/client/lib/utils/memoize.js + function memoize(run) { + const promiseCache = /* @__PURE__ */ new Map(); + const memoized = async function(...args) { + const promiseKey = JSON.stringify(args); + const cachedPromise = promiseCache.get(promiseKey); + if (cachedPromise) { + return cachedPromise; + } + const promise = (async () => { + try { + return await run.apply(this, args); + } finally { + promiseCache.delete(promiseKey); + } + })(); + promiseCache.set(promiseKey, promise); + return promise; + }; + return memoized; + } + + // node_modules/@logto/client/lib/utils/once.js + function once2(function_) { + let called = false; + let result; + return function(...args) { + if (!called) { + called = true; + result = function_.apply(this, args); + } + return result; + }; + } + + // node_modules/@logto/client/lib/client.js + var StandardLogtoClient = class { + get jwtVerifier() { + return this.jwtVerifierInstance; + } + constructor(logtoConfig, adapter, buildJwtVerifier) { + this.getOidcConfig = once2(this.#getOidcConfig); + this.getAccessToken = memoize(this.#getAccessToken); + this.getOrganizationToken = memoize(this.#getOrganizationToken); + this.clearAccessToken = memoize(this.#clearAccessToken); + this.clearAllTokens = memoize(this.#clearAllTokens); + this.handleSignInCallback = memoize(this.#handleSignInCallback); + this.accessTokenMap = /* @__PURE__ */ new Map(); + this.logtoConfig = normalizeLogtoConfig(logtoConfig); + this.adapter = new ClientAdapterInstance(adapter); + this.jwtVerifierInstance = buildJwtVerifier(this); + void this.loadAccessTokenMap(); + } + /** + * Set the JWT verifier for the client. + * @param buildJwtVerifier The JWT verifier instance or a function that returns the JWT verifier instance. + */ + setJwtVerifier(buildJwtVerifier) { + this.jwtVerifierInstance = typeof buildJwtVerifier === "function" ? buildJwtVerifier(this) : buildJwtVerifier; + } + /** + * Check if the user is authenticated by checking if the ID token exists. + */ + async isAuthenticated() { + return Boolean(await this.getIdToken()); + } + /** + * Get the Refresh Token from the storage. + */ + async getRefreshToken() { + return this.adapter.storage.getItem("refreshToken"); + } + /** + * Get the ID Token from the storage. If you want to get the ID Token claims, + * use {@link getIdTokenClaims} instead. + */ + async getIdToken() { + return this.adapter.storage.getItem("idToken"); + } + /** + * Get the ID Token claims. + */ + async getIdTokenClaims() { + const idToken = await this.getIdToken(); + if (!idToken) { + throw new LogtoClientError("not_authenticated", "ID token not found"); + } + return decodeIdToken(idToken); + } + /** + * Get the access token claims for the specified resource. + * + * @param resource The resource that the access token is granted for. If not + * specified, the access token will be used for OpenID Connect or the default + * resource, as specified in the Logto Console. + */ + async getAccessTokenClaims(resource) { + const accessToken = await this.getAccessToken(resource); + return decodeAccessToken(accessToken); + } + /** + * Get the organization token claims for the specified organization. + * + * @param organizationId The ID of the organization that the access token is granted for. + */ + async getOrganizationTokenClaims(organizationId) { + const accessToken = await this.getOrganizationToken(organizationId); + return decodeAccessToken(accessToken); + } + /** + * Get the user information from the Userinfo Endpoint. + * + * Note the Userinfo Endpoint will return more claims than the ID Token. See + * {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information} + * for more information. + * + * @returns The user information. + * @throws LogtoClientError if the user is not authenticated. + */ + async fetchUserInfo() { + const { userinfoEndpoint } = await this.getOidcConfig(); + const accessToken = await this.getAccessToken(); + if (!accessToken) { + throw new LogtoClientError("fetch_user_info_failed"); + } + return fetchUserInfo(userinfoEndpoint, accessToken, this.adapter.requester); + } + async signIn(options, mode, hint) { + const { redirectUri: redirectUriUrl, postRedirectUri: postRedirectUriUrl, firstScreen, identifiers, interactionMode, loginHint, directSignIn, extraParams, prompt, clearTokens } = typeof options === "string" || options instanceof URL ? { + redirectUri: options, + postRedirectUri: void 0, + firstScreen: void 0, + identifiers: void 0, + interactionMode: mode, + loginHint: hint, + directSignIn: void 0, + extraParams: void 0, + prompt: void 0, + clearTokens: true + } : options; + const redirectUri = redirectUriUrl.toString(); + const postRedirectUri = postRedirectUriUrl?.toString(); + const { appId: clientId, prompt: promptViaConfig, resources, scopes, includeReservedScopes } = this.logtoConfig; + const { authorizationEndpoint } = await this.getOidcConfig(); + const [codeVerifier, state] = await Promise.all([ + this.adapter.generateCodeVerifier(), + this.adapter.generateState() + ]); + const codeChallenge = await this.adapter.generateCodeChallenge(codeVerifier); + const signInUri = generateSignInUri({ + authorizationEndpoint, + clientId, + redirectUri: redirectUri.toString(), + codeChallenge, + state, + scopes, + resources, + prompt: prompt ?? promptViaConfig, + firstScreen, + identifiers, + interactionMode, + loginHint, + directSignIn, + extraParams, + includeReservedScopes + }); + await Promise.all([ + this.setSignInSession({ redirectUri, postRedirectUri, codeVerifier, state }), + clearTokens === false ? void 0 : this.clearAllTokens() + ]); + await this.adapter.navigate(signInUri, { redirectUri, for: "sign-in" }); + } + /** + * Check if the user is redirected from the sign-in page by checking if the + * current URL matches the redirect URI in the sign-in session. + * + * If there's no sign-in session, it will return `false`. + * + * @param url The current URL. + */ + async isSignInRedirected(url) { + const signInSession = await this.getSignInSession(); + if (!signInSession) { + return false; + } + const { redirectUri } = signInSession; + const { origin, pathname } = new URL(url); + return `${origin}${pathname}` === redirectUri; + } + /** + * Start the sign-out flow with the specified redirect URI. The URI must be + * registered in the Logto Console. + * + * It will also revoke all the tokens and clean up the storage. + * + * The user will be redirected that URI after the sign-out flow is completed. + * If the `postLogoutRedirectUri` is not specified, the user will be redirected + * to a default page. + */ + async signOut(postLogoutRedirectUri) { + const { appId: clientId } = this.logtoConfig; + const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig(); + const refreshToken = await this.getRefreshToken(); + if (refreshToken) { + try { + await revoke(revocationEndpoint, clientId, refreshToken, this.adapter.requester); + } catch { + } + } + const url = generateSignOutUri({ + endSessionEndpoint, + postLogoutRedirectUri, + clientId + }); + await this.clearAllTokens(); + await this.adapter.navigate(url, { redirectUri: postLogoutRedirectUri, for: "sign-out" }); + } + async getSignInSession() { + const jsonItem = await this.adapter.storage.getItem("signInSession"); + if (!jsonItem) { + return null; + } + const item = JSON.parse(jsonItem); + if (!isLogtoSignInSessionItem(item)) { + throw new LogtoClientError("sign_in_session.invalid"); + } + return item; + } + async setSignInSession(value) { + return this.adapter.setStorageItem(PersistKey.SignInSession, value && JSON.stringify(value)); + } + async setIdToken(value) { + return this.adapter.setStorageItem(PersistKey.IdToken, value); + } + async setRefreshToken(value) { + return this.adapter.setStorageItem(PersistKey.RefreshToken, value); + } + async getAccessTokenByRefreshToken(resource, organizationId) { + const currentRefreshToken = await this.getRefreshToken(); + if (!currentRefreshToken) { + throw new LogtoClientError("not_authenticated", "Refresh token not found"); + } + const accessTokenKey = buildAccessTokenKey(resource, organizationId); + const { appId: clientId } = this.logtoConfig; + const { tokenEndpoint } = await this.getOidcConfig(); + const requestedAt = Math.round(Date.now() / 1e3); + const { accessToken, refreshToken, idToken, scope, expiresIn } = await fetchTokenByRefreshToken({ + clientId, + tokenEndpoint, + refreshToken: currentRefreshToken, + resource, + organizationId + }, this.adapter.requester); + this.accessTokenMap.set(accessTokenKey, { + token: accessToken, + scope, + /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property + * in the token claims. It is utilized by the client to determine if the cached access token + * has expired and when a new access token should be requested. + */ + expiresAt: requestedAt + expiresIn + }); + await this.saveAccessTokenMap(); + if (refreshToken) { + await this.setRefreshToken(refreshToken); + } + if (idToken) { + await this.jwtVerifier.verifyIdToken(idToken); + await this.setIdToken(idToken); + } + return accessToken; + } + async saveAccessTokenMap() { + const data = {}; + for (const [key, accessToken] of this.accessTokenMap.entries()) { + data[key] = accessToken; + } + await this.adapter.storage.setItem("accessToken", JSON.stringify(data)); + } + async loadAccessTokenMap() { + const raw = await this.adapter.storage.getItem("accessToken"); + if (!raw) { + return; + } + try { + const json = JSON.parse(raw); + if (!isLogtoAccessTokenMap(json)) { + return; + } + this.accessTokenMap.clear(); + for (const [key, accessToken] of Object.entries(json)) { + this.accessTokenMap.set(key, accessToken); + } + } catch (error) { + console.warn(error); + } + } + async #getOidcConfig() { + return this.adapter.getWithCache(CacheKey.OpenidConfig, async () => { + return fetchOidcConfig(getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester); + }); + } + async #getAccessToken(resource, organizationId) { + if (!await this.isAuthenticated()) { + throw new LogtoClientError("not_authenticated"); + } + const accessTokenKey = buildAccessTokenKey(resource, organizationId); + const accessToken = this.accessTokenMap.get(accessTokenKey); + if (accessToken && accessToken.expiresAt > Date.now() / 1e3) { + return accessToken.token; + } + if (accessToken) { + this.accessTokenMap.delete(accessTokenKey); + } + return this.getAccessTokenByRefreshToken(resource, organizationId); + } + async #getOrganizationToken(organizationId) { + if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) { + throw new LogtoClientError("missing_scope_organizations"); + } + return this.getAccessToken(void 0, organizationId); + } + async #clearAccessToken() { + this.accessTokenMap.clear(); + await this.adapter.storage.removeItem("accessToken"); + } + async #clearAllTokens() { + await Promise.all([this.setRefreshToken(null), this.setIdToken(null), this.clearAccessToken()]); + } + async #handleSignInCallback(callbackUri) { + const signInSession = await this.getSignInSession(); + if (!signInSession) { + throw new LogtoClientError("sign_in_session.not_found"); + } + const { redirectUri, postRedirectUri, state, codeVerifier } = signInSession; + const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state); + const accessTokenKey = buildAccessTokenKey(); + const { appId: clientId } = this.logtoConfig; + const { tokenEndpoint } = await this.getOidcConfig(); + const requestedAt = Math.round(Date.now() / 1e3); + const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({ + clientId, + tokenEndpoint, + redirectUri, + codeVerifier, + code + }, this.adapter.requester); + await this.jwtVerifier.verifyIdToken(idToken); + await this.setRefreshToken(refreshToken ?? null); + await this.setIdToken(idToken); + this.accessTokenMap.set(accessTokenKey, { + token: accessToken, + scope, + /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property + * in the token claims. It is utilized by the client to determine if the cached access token + * has expired and when a new access token should be requested. + */ + expiresAt: requestedAt + expiresIn + }); + await this.saveAccessTokenMap(); + await this.setSignInSession(null); + if (postRedirectUri) { + await this.adapter.navigate(postRedirectUri, { for: "post-sign-in" }); + } + } + }; + + // node_modules/@logto/client/lib/utils/requester.js + var createRequester = (fetchFunction) => { + return async (...args) => { + const response = await fetchFunction(...args); + if (!response.ok) { + const cloned = response.clone(); + const responseJson = await response.json(); + console.error(`Logto requester error: [status=${response.status}]`, responseJson); + if (!isLogtoRequestErrorJson(responseJson)) { + throw new LogtoError("unexpected_response_error", responseJson); + } + const { code, message: message2 } = responseJson; + throw new LogtoRequestError(code, message2, cloned); + } + return response.json(); + }; + }; + + // node_modules/@logto/client/lib/index.js + var LogtoClient = class extends StandardLogtoClient { + constructor(logtoConfig, adapter, buildJwtVerifier) { + super(logtoConfig, adapter, buildJwtVerifier ?? ((client) => new DefaultJwtVerifier(client))); + } + }; + + // node_modules/js-base64/base64.mjs + var _hasBuffer = typeof Buffer === "function"; + var _TD = typeof TextDecoder === "function" ? new TextDecoder() : void 0; + var _TE = typeof TextEncoder === "function" ? new TextEncoder() : void 0; + var b64ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var b64chs = Array.prototype.slice.call(b64ch); + var b64tab = ((a) => { + let tab = {}; + a.forEach((c, i) => tab[c] = i); + return tab; + })(b64chs); + var _fromCC = String.fromCharCode.bind(String); + var _U8Afrom = typeof Uint8Array.from === "function" ? Uint8Array.from.bind(Uint8Array) : (it) => new Uint8Array(Array.prototype.slice.call(it, 0)); + var _mkUriSafe = (src) => src.replace(/=/g, "").replace(/[+\/]/g, (m0) => m0 == "+" ? "-" : "_"); + var btoaPolyfill = (bin) => { + let u32, c0, c1, c2, asc = ""; + const pad = bin.length % 3; + for (let i = 0; i < bin.length; ) { + if ((c0 = bin.charCodeAt(i++)) > 255 || (c1 = bin.charCodeAt(i++)) > 255 || (c2 = bin.charCodeAt(i++)) > 255) + throw new TypeError("invalid character found"); + u32 = c0 << 16 | c1 << 8 | c2; + asc += b64chs[u32 >> 18 & 63] + b64chs[u32 >> 12 & 63] + b64chs[u32 >> 6 & 63] + b64chs[u32 & 63]; + } + return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc; + }; + var _btoa = typeof btoa === "function" ? (bin) => btoa(bin) : _hasBuffer ? (bin) => Buffer.from(bin, "binary").toString("base64") : btoaPolyfill; + var _fromUint8Array = _hasBuffer ? (u8a) => Buffer.from(u8a).toString("base64") : (u8a) => { + const maxargs = 4096; + let strs = []; + for (let i = 0, l = u8a.length; i < l; i += maxargs) { + strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs))); + } + return _btoa(strs.join("")); + }; + var fromUint8Array = (u8a, urlsafe = false) => urlsafe ? _mkUriSafe(_fromUint8Array(u8a)) : _fromUint8Array(u8a); + + // node_modules/@logto/browser/lib/utils/generators.js + var generateRandomString = (length = 64) => fromUint8Array(crypto.getRandomValues(new Uint8Array(length)), true); + var generateState = () => generateRandomString(); + var generateCodeVerifier = () => generateRandomString(); + var generateCodeChallenge = async (codeVerifier) => { + if (crypto.subtle === void 0) { + throw new LogtoError("crypto_subtle_unavailable"); + } + const encodedCodeVerifier = new TextEncoder().encode(codeVerifier); + const codeChallenge = new Uint8Array(await crypto.subtle.digest("SHA-256", encodedCodeVerifier)); + return fromUint8Array(codeChallenge, true); + }; + + // node_modules/@logto/chrome-extension/lib/storage.js + var keyPrefix = `logto`; + var ChromeExtensionStorage = class { + constructor(appId) { + this.appId = appId; + } + getKey(item) { + if (item === void 0) { + return `${keyPrefix}:${this.appId}`; + } + return `${keyPrefix}:${this.appId}:${item}`; + } + // eslint-disable-next-line @typescript-eslint/ban-types + async getItem(key) { + const storageKey = this.getKey(key); + const object = await chrome.storage.local.get(storageKey); + return object[storageKey] ? String(object[storageKey]) : null; + } + async setItem(key, value) { + await chrome.storage.local.set({ [this.getKey(key)]: value }); + } + async removeItem(key) { + await chrome.storage.local.remove(this.getKey(key)); + } + }; + + // node_modules/@logto/chrome-extension/lib/index.js + var LogtoClient2 = class extends LogtoClient { + /** + * @param config The configuration object for the client. + */ + constructor(config) { + const requester = createRequester(fetch); + const navigate = async (url, params) => { + switch (params.for) { + case "sign-in": { + const responseUrl = await chrome.identity.launchWebAuthFlow({ url, interactive: true }); + if (!responseUrl) { + throw new LogtoClientError("user_cancelled"); + } + await this.handleSignInCallback(responseUrl); + break; + } + case "sign-out": { + await chrome.identity.launchWebAuthFlow({ + url, + interactive: false, + abortOnLoadForNonInteractive: false, + timeoutMsForNonInteractive: 1e4 + }); + break; + } + default: { + throw new Error(`Unsupported navigation for ${params.for}`); + } + } + }; + super(config, { + requester, + navigate, + storage: new ChromeExtensionStorage(config.appId), + generateCodeChallenge, + generateCodeVerifier, + generateState + }); + } + }; + + // src/background/auth/client.ts + function createLogtoAuthClient() { + const config = readAuthConfig(); + const client = new LogtoClient2({ + appId: config.appId, + endpoint: config.logtoEndpoint, + resources: [config.apiResource], + scopes: config.scopes + }); + return { + getAccessToken(resource) { + return client.getAccessToken(resource); + }, + getIdTokenClaims() { + return client.getIdTokenClaims(); + }, + isAuthenticated() { + return client.isAuthenticated(); + }, + signIn() { + return client.signIn(readChromeIdentity().getRedirectURL("/callback")); + }, + signOut() { + return client.signOut(readChromeIdentity().getRedirectURL()); + } + }; + } + function readChromeIdentity() { + const identity = globalThis.chrome?.identity; + if (typeof identity?.getRedirectURL !== "function") { + throw new Error("chrome.identity.getRedirectURL is unavailable"); + } + return { + getRedirectURL: identity.getRedirectURL.bind(identity) + }; + } + + // src/shared/auth-messages.ts + var authRequestTypes = /* @__PURE__ */ new Set([ + "auth:get-state", + "auth:sign-in", + "auth:sign-out", + "auth:get-access-token" + ]); + function isAuthRequestMessage(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + return typeof candidate.type === "string" && authRequestTypes.has(candidate.type); + } + function isAuthResponseMessage(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + if (candidate.ok === false) { + return candidate.type === "auth:error" && typeof candidate.error === "string"; + } + if (candidate.ok !== true || typeof candidate.type !== "string") { + return false; + } + if (candidate.type === "auth:ack") { + return true; + } + if (candidate.type === "auth:token") { + return Boolean( + candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string" + ); + } + if (candidate.type === "auth:state") { + return Boolean( + candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean" + ); + } + return false; + } + + // src/shared/batch-submit-config.ts + var DEFAULT_BATCH_SUBMIT_BASE_URL = "http://192.168.31.21:8083"; + + // src/shared/batch-submit-client.ts + function createBatchSubmitClient(options) { + const baseUrl = options.baseUrl ?? DEFAULT_BATCH_SUBMIT_BASE_URL; + const fetchImpl = options.fetchImpl ?? fetch; + const getAccessToken = options.getAccessToken ?? (() => readAccessToken(options.sendMessage)); + return { + async submitBatch(payload) { + const token = await getAccessToken(); + const response = await fetchImpl( + buildBatchSubmitUrl(baseUrl), + { + body: JSON.stringify(payload), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json" + }, + method: "POST" + } + ); + if (response.status === 401 || response.status === 403) { + throw new Error("batch submit unauthorized"); + } + if (!response.ok) { + throw new Error(`batch submit failed: ${response.status}`); + } + return readBatchSubmitResponse(await response.json()); + } + }; + } + function buildBatchSubmitUrl(baseUrl) { + return new URL("/api/v1/batch-status/batches", baseUrl).toString(); + } + async function readAccessToken(sendMessage) { + const response = await sendMessage({ type: "auth:get-access-token" }); + if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim()) { + throw new Error("batch submit token unavailable"); + } + return response.value.accessToken; + } + function readBatchSubmitResponse(payload) { + if (!isRecord(payload)) { + throw new Error("batch submit response is invalid"); + } + if (payload.success !== true) { + const message2 = typeof payload.msg === "string" && payload.msg.trim() ? payload.msg : "batch submit failed"; + throw new Error(message2); + } + return "data" in payload ? payload.data : payload; + } + function isRecord(value) { + return typeof value === "object" && value !== null; + } + + // src/shared/backend-metrics-config.ts + var DEFAULT_BACKEND_METRICS_BASE_URL = "https://talent-search.intelligrow.cn"; + + // src/shared/backend-metrics-client.ts + function createBackendMetricsClient(options) { + const baseUrl = options.baseUrl ?? DEFAULT_BACKEND_METRICS_BASE_URL; + const fetchImpl = options.fetchImpl ?? defaultFetch; + return { + async searchByStarIds(starIds) { + const response = await fetchImpl(buildBackendMetricsSearchUrl(baseUrl), { + body: JSON.stringify(buildBackendMetricsSearchRequestBody(starIds)), + headers: { + Authorization: `Bearer ${await options.getAccessToken()}`, + "Content-Type": "application/json" + }, + method: "POST" + }); + if (!response.ok) { + throw new Error("backend metrics request failed"); + } + return mapBackendMetricsSearchResponse(await response.json()); + } + }; + } + function buildBackendMetricsSearchUrl(baseUrl) { + return new URL("/api/v1/history/talents/search", baseUrl).toString(); + } + function buildBackendMetricsSearchRequestBody(starIds) { + return { + page: 1, + size: Math.max(20, starIds.length), + type: "star_id", + values: starIds + }; + } + function mapBackendMetricsSearchResponse(payload) { + const rows = readResponseRows(payload); + if (!rows) { + throw new Error("backend metrics response is invalid"); + } + return rows.flatMap((row) => { + if (!isRecord2(row) || typeof row.star_id !== "string") { + return []; + } + return [ + { + a3IncreaseCount: formatDecimalValue( + readAverageA3IncreaseCount(row) + ), + afterViewSearchCount: formatDecimalValue(row.avg_after_view_search_cnt), + afterViewSearchRate: formatRateValue(row.avg_after_view_search_rate), + cpSearch: formatDecimalValue(row.cp_search), + cpa3: formatDecimalValue(readCpa3Value(row)), + newA3Rate: formatRateValue(row.avg_new_a3_rate), + starId: row.star_id + } + ]; + }); + } + function readAverageA3IncreaseCount(row) { + const directAverage = readFiniteNumber(row.avg_a3_increase_cnt); + if (directAverage !== null) { + return directAverage; + } + const totalNewA3 = readTotalNewA3Value(row); + const videoCount = readFiniteNumber(row.video_count) ?? readNestedVideoCount(row.videos); + if (totalNewA3 === null || videoCount === null || videoCount <= 0) { + return null; + } + return totalNewA3 / videoCount; + } + function readCpa3Value(row) { + const directCpa3 = readFiniteNumber(row.cpa3); + if (directCpa3 !== null) { + return directCpa3; + } + const totalCost = readFiniteNumber(row.total_estimated_video_cost); + const totalNewA3 = readTotalNewA3Value(row); + if (totalCost === null || totalNewA3 === null || totalNewA3 <= 0) { + return null; + } + return totalCost / totalNewA3; + } + function readTotalNewA3Value(row) { + const derivedFromTotals = deriveTotalNewA3FromTotals(row); + if (derivedFromTotals !== null) { + return derivedFromTotals; + } + return deriveTotalNewA3FromVideos(row.videos); + } + function deriveTotalNewA3FromTotals(row) { + const totalPlayCount = readFiniteNumber(row.total_play_cnt); + const averageNewA3Rate = readFiniteNumber(row.avg_new_a3_rate); + if (totalPlayCount === null || averageNewA3Rate === null) { + return null; + } + return totalPlayCount * averageNewA3Rate; + } + function deriveTotalNewA3FromVideos(value) { + if (!Array.isArray(value)) { + return null; + } + let total = 0; + let hasFiniteValue = false; + value.forEach((video) => { + if (!isRecord2(video)) { + return; + } + const newA3 = readFiniteNumber(video.new_a3); + if (newA3 === null) { + return; + } + hasFiniteValue = true; + total += newA3; + }); + return hasFiniteValue ? total : null; + } + function readNestedVideoCount(value) { + return Array.isArray(value) ? value.length : null; + } + function readResponseRows(payload) { + if (!isRecord2(payload) || payload.success !== true) { + return null; + } + const topLevelData = isRecord2(payload.data) ? payload.data : null; + return Array.isArray(topLevelData?.data) ? topLevelData.data : null; + } + function formatRateValue(value) { + const number = typeof value === "number" ? value : Number(value); + if (Number.isFinite(number)) { + const percentage = number * 100; + const formatted = new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + minimumFractionDigits: percentage % 1 === 0 ? 0 : 2 + }).format(percentage); + return `${formatted}%`; + } + return ""; + } + function formatDecimalValue(value) { + const number = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(number)) { + return ""; + } + return new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + }).format(number); + } + async function defaultFetch(input, init) { + return fetch(input, init); + } + function isRecord2(value) { + return typeof value === "object" && value !== null; + } + function readFiniteNumber(value) { + const number = typeof value === "number" ? value : Number(value); + return Number.isFinite(number) ? number : null; + } + + // src/shared/backend-metrics-messages.ts + function isBackendMetricsSearchRequestMessage(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + return candidate.type === "backend-metrics:search" && Boolean( + candidate.value && typeof candidate.value === "object" && Array.isArray(candidate.value.starIds) && candidate.value.starIds.every( + (starId) => typeof starId === "string" + ) + ); + } + + // src/background/index.ts + function registerBackgroundMessageHandler(chromeLike = readChromeLike(), dependencies = {}) { + let authController = dependencies.authController; + let searchBackendMetrics = dependencies.searchBackendMetrics; + let submitBatch = dependencies.submitBatch; + chromeLike.runtime?.onMessage?.addListener((message2, _sender, sendResponse) => { + if (isDownloadMarketCsvMessage(message2)) { + void triggerCsvDownload(chromeLike, message2).then(() => { + sendResponse({ ok: true }); + }).catch((error) => { + sendResponse({ + error: error instanceof Error ? error.message : String(error), + ok: false + }); + }); + return true; + } + if (isBatchSubmitMessage(message2)) { + authController ??= createAuthController({ + authClient: createLogtoAuthClient() + }); + submitBatch ??= createBatchSubmitClient({ + baseUrl: DEFAULT_BATCH_SUBMIT_BASE_URL, + getAccessToken: () => authController.getAccessToken(), + sendMessage: () => Promise.reject(new Error("background batch submit does not use sendMessage")) + }).submitBatch; + void submitBatch(message2.payload).then((value) => { + sendResponse({ + ok: true, + type: "batch:ack", + value + }); + }).catch((error) => { + sendResponse({ + error: error instanceof Error ? error.message : String(error), + ok: false, + type: "batch:error" + }); + }); + return true; + } + if (isBackendMetricsSearchRequestMessage(message2)) { + authController ??= createAuthController({ + authClient: createLogtoAuthClient() + }); + searchBackendMetrics ??= createBackendMetricsClient({ + baseUrl: DEFAULT_BACKEND_METRICS_BASE_URL, + getAccessToken: () => authController.getAccessToken() + }).searchByStarIds; + void searchBackendMetrics(message2.value.starIds).then((rows) => { + sendResponse({ + ok: true, + type: "backend-metrics:result", + value: { + rows + } + }); + }).catch((error) => { + sendResponse({ + error: error instanceof Error ? error.message : String(error), + ok: false, + type: "backend-metrics:error" + }); + }); + return true; + } + if (!isAuthRequestMessage(message2)) { + return; + } + authController ??= createAuthController({ + authClient: createLogtoAuthClient() + }); + void handleAuthMessage(authController, message2).then((response) => { + sendResponse(response); + }).catch((error) => { + sendResponse({ + error: error instanceof Error ? error.message : String(error), + ok: false, + type: "auth:error" + }); + }); + return true; + }); + } + async function handleAuthMessage(authController, message2) { + if (message2.type === "auth:get-state") { + return { + ok: true, + type: "auth:state", + value: await authController.getAuthState() + }; + } + if (message2.type === "auth:get-access-token") { + return { + ok: true, + type: "auth:token", + value: { + accessToken: await authController.getAccessToken() + } + }; + } + if (message2.type === "auth:sign-in") { + await authController.signIn(); + return { + ok: true, + type: "auth:ack" + }; + } + await authController.signOut(); + return { + ok: true, + type: "auth:ack" + }; + } + function readChromeLike() { + return globalThis.chrome ?? {}; + } + async function triggerCsvDownload(chromeLike, message2) { + if (!chromeLike.downloads?.download) { + throw new Error("chrome.downloads.download is unavailable"); + } + const csvUrl = `data:text/csv;charset=utf-8,${encodeURIComponent(`\uFEFF${message2.csv}`)}`; + await Promise.resolve( + chromeLike.downloads.download({ + filename: message2.filename, + saveAs: false, + url: csvUrl + }) + ); + } + function isDownloadMarketCsvMessage(message2) { + if (!message2 || typeof message2 !== "object") { + return false; + } + const candidate = message2; + return candidate.type === "download-market-csv" && typeof candidate.csv === "string" && typeof candidate.filename === "string"; + } + function isBatchSubmitMessage(message2) { + if (!message2 || typeof message2 !== "object") { + return false; + } + const candidate = message2; + return candidate.type === "batch:submit" && "payload" in candidate; + } + registerBackgroundMessageHandler(); +})(); +/*! Bundled license information: + +@silverhand/essentials/lib/utilities/assertions.js: + (*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + *) +*/ diff --git a/dist-release/content/index.js b/dist-release/content/index.js new file mode 100644 index 0000000..db7cf3a --- /dev/null +++ b/dist-release/content/index.js @@ -0,0 +1,4446 @@ +"use strict"; +(() => { + // src/shared/rate-normalizer.ts + function normalizeRateDisplay(value) { + const trimmedValue = value.trim(); + const rangeMatch = trimmedValue.match( + /^([0-9]+(?:\.[0-9]+)?)\s*%?\s*-\s*([0-9]+(?:\.[0-9]+)?)\s*%$/ + ); + if (rangeMatch) { + const [, lowerBound, upperBound] = rangeMatch; + return `${lowerBound}% - ${upperBound}%`; + } + return trimmedValue.replace(/\s+/g, ""); + } + function normalizeFractionRateDisplay(value) { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) { + return null; + } + const percentageValue = numericValue * 100; + return `${trimTrailingZeros(percentageValue.toFixed(6))}%`; + } + function parseRateLowerBound(value) { + const comparableRate = toComparableRate(value); + return comparableRate?.numeric ?? null; + } + function compareRateValues(leftValue, rightValue) { + const leftComparable = toComparableRate(leftValue); + const rightComparable = toComparableRate(rightValue); + if (!leftComparable && !rightComparable) { + return 0; + } + if (!leftComparable) { + return 1; + } + if (!rightComparable) { + return -1; + } + if (leftComparable.numeric !== rightComparable.numeric) { + return leftComparable.numeric - rightComparable.numeric; + } + if (leftComparable.isLessThan === rightComparable.isLessThan) { + return 0; + } + return leftComparable.isLessThan ? -1 : 1; + } + function toComparableRate(value) { + if (!value) { + return null; + } + const normalizedValue = normalizeRateDisplay(value); + const lessThanMatch = normalizedValue.match(/^<\s*([0-9]+(?:\.[0-9]+)?)%$/); + if (lessThanMatch) { + return { + isLessThan: true, + numeric: Number(lessThanMatch[1]) + }; + } + const rangeMatch = normalizedValue.match( + /^([0-9]+(?:\.[0-9]+)?)%\s*-\s*([0-9]+(?:\.[0-9]+)?)%$/ + ); + if (rangeMatch) { + return { + isLessThan: false, + numeric: Number(rangeMatch[1]) + }; + } + const exactMatch = normalizedValue.match(/^([0-9]+(?:\.[0-9]+)?)%$/); + if (exactMatch) { + return { + isLessThan: false, + numeric: Number(exactMatch[1]) + }; + } + return null; + } + function trimTrailingZeros(value) { + return value.replace(/\.?0+$/, ""); + } + + // src/shared/csv.ts + function escapeCsvCell(value) { + if (/[",\n]/.test(value)) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + } + + // src/content/market/csv-exporter.ts + var FALLBACK_BASE_COLUMNS = [ + { + header: "\u8FBE\u4EBAID", + readValue: (record) => record.authorId + }, + { + header: "\u8FBE\u4EBA\u540D\u79F0", + readValue: (record) => record.authorName + }, + { + header: "\u5730\u533A", + readValue: (record) => record.location ?? "" + }, + { + header: "21-60s\u62A5\u4EF7", + readValue: (record) => record.price21To60s ?? "" + } + ]; + var RATE_COLUMNS = [ + { + header: "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387", + readValue: (record) => record.rates?.singleVideoAfterSearchRate ? normalizeRateDisplay(record.rates.singleVideoAfterSearchRate) : "" + }, + { + header: "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387", + readValue: (record) => record.rates?.personalVideoAfterSearchRate ? normalizeRateDisplay(record.rates.personalVideoAfterSearchRate) : "" + } + ]; + var BACKEND_METRIC_COLUMNS = [ + { + header: "\u770B\u540E\u641C\u7387", + readValue: (record) => record.backendMetrics?.afterViewSearchRate ?? "" + }, + { + header: "\u770B\u540E\u641C\u6570", + readValue: (record) => record.backendMetrics?.afterViewSearchCount ?? "" + }, + { + header: "\u65B0\u589EA3\u6570", + readValue: (record) => record.backendMetrics?.a3IncreaseCount ?? "" + }, + { + header: "\u65B0\u589EA3\u7387", + readValue: (record) => record.backendMetrics?.newA3Rate ?? "" + }, + { + header: "CPA3", + readValue: (record) => record.backendMetrics?.cpa3 ?? "" + }, + { + header: "cp_search", + readValue: (record) => record.backendMetrics?.cpSearch ?? "" + } + ]; + function buildMarketCsv(records) { + const baseColumns = buildBaseColumns(records); + const csvColumns = [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS]; + const headerLine = csvColumns.map((column) => column.header).join(","); + const rowLines = records.map( + (record) => csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",") + ); + return [headerLine, ...rowLines].join("\n"); + } + function buildBaseColumns(records) { + const orderedHeaders = []; + const seenHeaders = /* @__PURE__ */ new Set(); + const excludedHeaders = /* @__PURE__ */ new Set(["\u4EE3\u8868\u89C6\u9891"]); + records.forEach((record) => { + Object.keys(record.exportFields ?? {}).forEach((header) => { + if (seenHeaders.has(header) || excludedHeaders.has(header)) { + return; + } + seenHeaders.add(header); + orderedHeaders.push(header); + }); + }); + if (orderedHeaders.length === 0) { + return FALLBACK_BASE_COLUMNS; + } + return orderedHeaders.map((header) => ({ + header, + readValue: (record) => record.exportFields?.[header] ?? "" + })); + } + + // src/content/market/batch-name-dialog.ts + var DIALOG_STYLE_ID = "sces-batch-name-dialog-style"; + var activeDialogs = /* @__PURE__ */ new WeakMap(); + function promptForBatchName(document2) { + const existingDialog = activeDialogs.get(document2); + if (existingDialog) { + existingDialog.input.focus(); + existingDialog.input.select(); + return existingDialog.promise; + } + ensureDialogStyles(document2); + const dialogRoot = document2.createElement("div"); + dialogRoot.dataset.pluginBatchNameDialog = "root"; + dialogRoot.setAttribute("role", "dialog"); + dialogRoot.setAttribute("aria-modal", "true"); + dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title"); + applyOverlayStyles(dialogRoot); + const dialogPanel = document2.createElement("div"); + applyPanelStyles(dialogPanel); + const title = document2.createElement("h2"); + title.id = "sces-batch-name-title"; + title.textContent = "\u63D0\u4EA4\u6279\u6B21"; + applyTitleStyles(title); + const description = document2.createElement("p"); + description.textContent = "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0\uFF0C\u4FBF\u4E8E\u540E\u7EED\u5728\u7CFB\u7EDF\u4E2D\u8BC6\u522B\u548C\u8FFD\u8E2A\u3002"; + applyDescriptionStyles(description); + const input = document2.createElement("input"); + input.type = "text"; + input.dataset.pluginBatchNameInput = "input"; + input.placeholder = "\u4F8B\u5982\uFF1A618\u8FBE\u4EBA\u7B5B\u9009\u7B2C\u4E00\u6279"; + input.maxLength = 60; + applyInputStyles(input); + const errorText = document2.createElement("p"); + errorText.dataset.pluginBatchNameError = "text"; + applyErrorStyles(errorText); + const buttonRow = document2.createElement("div"); + applyButtonRowStyles(buttonRow); + const cancelButton = document2.createElement("button"); + cancelButton.type = "button"; + cancelButton.dataset.pluginBatchNameCancel = "button"; + cancelButton.textContent = "\u53D6\u6D88"; + applySecondaryButtonStyles(cancelButton); + const confirmButton = document2.createElement("button"); + confirmButton.type = "button"; + confirmButton.dataset.pluginBatchNameConfirm = "button"; + confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4"; + applyPrimaryButtonStyles(confirmButton); + buttonRow.append(cancelButton, confirmButton); + dialogPanel.append(title, description, input, errorText, buttonRow); + dialogRoot.appendChild(dialogPanel); + document2.body.appendChild(dialogRoot); + const dialogPromise = new Promise((resolve) => { + const closeDialog = (value) => { + activeDialogs.delete(document2); + dialogRoot.remove(); + document2.removeEventListener("keydown", handleDocumentKeydown, true); + resolve(value); + }; + const submitValue = () => { + const value = input.value.trim(); + if (!value) { + errorText.textContent = "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0"; + input.setAttribute("aria-invalid", "true"); + input.focus(); + return; + } + closeDialog(value); + }; + const handleDocumentKeydown = (event) => { + if (event.key === "Escape") { + event.preventDefault(); + closeDialog(null); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + submitValue(); + } + }; + input.addEventListener("input", () => { + if (!input.value.trim()) { + return; + } + errorText.textContent = ""; + input.removeAttribute("aria-invalid"); + }); + cancelButton.addEventListener("click", () => { + closeDialog(null); + }); + confirmButton.addEventListener("click", () => { + submitValue(); + }); + dialogRoot.addEventListener("click", (event) => { + if (event.target === dialogRoot) { + closeDialog(null); + } + }); + document2.addEventListener("keydown", handleDocumentKeydown, true); + }); + activeDialogs.set(document2, { + input, + promise: dialogPromise + }); + input.focus(); + return dialogPromise; + } + function ensureDialogStyles(document2) { + if (document2.getElementById(DIALOG_STYLE_ID)) { + return; + } + const style = document2.createElement("style"); + style.id = DIALOG_STYLE_ID; + style.textContent = ` + [data-plugin-batch-name-dialog="root"] { + animation: sces-batch-name-fade-in 0.16s ease; + } + + @keyframes sces-batch-name-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + `; + document2.head.appendChild(style); + } + function applyOverlayStyles(root) { + root.style.position = "fixed"; + root.style.inset = "0"; + root.style.background = "rgba(15, 23, 42, 0.38)"; + root.style.display = "flex"; + root.style.alignItems = "center"; + root.style.justifyContent = "center"; + root.style.padding = "24px"; + root.style.zIndex = "2147483647"; + } + function applyPanelStyles(panel) { + panel.style.width = "min(420px, calc(100vw - 32px))"; + panel.style.background = "#fffaf9"; + panel.style.border = "1px solid rgba(127, 29, 45, 0.14)"; + panel.style.borderRadius = "18px"; + panel.style.boxShadow = "0 28px 70px rgba(15, 23, 42, 0.22)"; + panel.style.padding = "24px"; + panel.style.boxSizing = "border-box"; + } + function applyTitleStyles(title) { + title.style.margin = "0"; + title.style.color = "#4c0519"; + title.style.fontSize = "20px"; + title.style.fontWeight = "700"; + title.style.lineHeight = "28px"; + } + function applyDescriptionStyles(description) { + description.style.margin = "10px 0 0"; + description.style.color = "#64748b"; + description.style.fontSize = "13px"; + description.style.lineHeight = "20px"; + } + function applyInputStyles(input) { + input.style.width = "100%"; + input.style.height = "42px"; + input.style.marginTop = "18px"; + input.style.padding = "0 14px"; + input.style.boxSizing = "border-box"; + input.style.border = "1px solid #d8c1c6"; + input.style.borderRadius = "12px"; + input.style.background = "#ffffff"; + input.style.color = "#1f2937"; + input.style.fontSize = "14px"; + input.style.outline = "none"; + } + function applyErrorStyles(errorText) { + errorText.style.minHeight = "20px"; + errorText.style.margin = "8px 0 0"; + errorText.style.color = "#b91c1c"; + errorText.style.fontSize = "12px"; + errorText.style.lineHeight = "18px"; + } + function applyButtonRowStyles(buttonRow) { + buttonRow.style.display = "flex"; + buttonRow.style.justifyContent = "flex-end"; + buttonRow.style.gap = "10px"; + buttonRow.style.marginTop = "18px"; + } + function applySecondaryButtonStyles(button) { + button.style.height = "36px"; + button.style.padding = "0 16px"; + button.style.border = "1px solid #d7dde6"; + button.style.borderRadius = "10px"; + button.style.background = "#ffffff"; + button.style.color = "#334155"; + button.style.fontWeight = "600"; + button.style.cursor = "pointer"; + } + function applyPrimaryButtonStyles(button) { + button.style.height = "36px"; + button.style.padding = "0 16px"; + button.style.border = "1px solid #7f1d2d"; + button.style.borderRadius = "10px"; + button.style.background = "#7f1d2d"; + button.style.color = "#ffffff"; + button.style.fontWeight = "600"; + button.style.cursor = "pointer"; + } + + // src/content/market/batch-payload.ts + function createBatchPayload(options) { + const logtoUserId = options.authState.userInfo?.sub?.trim(); + if (!logtoUserId) { + throw new Error("batch submit user id unavailable"); + } + const resource = options.authState.resource?.trim(); + if (!resource) { + throw new Error("batch submit resource unavailable"); + } + const batchName = options.batchName.trim(); + if (!batchName) { + throw new Error("batch submit batch name is required"); + } + return { + authors: options.records.map((record) => ({ + authorId: record.authorId, + authorName: record.authorName + })), + batchName, + createdAt: options.createdAt, + creatorName: options.authState.userInfo?.name ?? options.authState.userInfo?.username ?? logtoUserId, + logtoUserId, + resource + }; + } + + // src/content/market/market-list-row.ts + var PAGE_NUMBER_KEYS = [ + "currentPage", + "page", + "pageNo", + "pageNum", + "page_no", + "page_num" + ]; + var PAGE_SIZE_KEYS = [ + "limit", + "pageSize", + "page_size", + "size" + ]; + var TOTAL_COUNT_KEYS = [ + "total", + "totalCount", + "total_count" + ]; + var TOTAL_PAGE_KEYS = [ + "pageCount", + "page_count", + "totalPage", + "totalPages", + "total_page", + "total_pages" + ]; + function mapMarketListRow(row) { + const attributeDatas = readMarketAttributeDatas(row); + const singleVideoAfterSearchRate = normalizeMarketListRate( + readMarketFieldValue(row, attributeDatas, "avg_search_after_view_rate_30d") + ); + return { + authorId: readString(readMarketFieldValue(row, attributeDatas, "star_id")) ?? readString(readMarketFieldValue(row, attributeDatas, "id")) ?? "", + authorName: readString(readMarketFieldValue(row, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(row, attributeDatas, "nick_name")) ?? "", + exportFields: buildMarketExportFieldFallbacks(row, attributeDatas), + hasDirectRatesSource: true, + location: readMarketLocation(row, attributeDatas), + price21To60s: readMarketPrice21To60s(row, attributeDatas), + rates: singleVideoAfterSearchRate ? { + singleVideoAfterSearchRate + } : void 0 + }; + } + function parseMarketListResponse(payload) { + const container = findMarketListContainer(payload); + if (!container) { + return null; + } + const marketList = readMarketListArray(container); + if (!marketList) { + return null; + } + return { + currentPage: readKnownNumberDeep(container, PAGE_NUMBER_KEYS) ?? void 0, + pageSize: readKnownNumberDeep(container, PAGE_SIZE_KEYS) ?? void 0, + records: marketList.map((row) => isRecord(row) ? mapMarketListRow(row) : null).filter( + (row) => row !== null && Boolean(row.authorId || row.authorName) + ), + totalCount: readKnownNumberDeep(container, TOTAL_COUNT_KEYS) ?? void 0, + totalPages: readKnownNumberDeep(container, TOTAL_PAGE_KEYS) ?? void 0 + }; + } + function readKnownPaginationNumber(value, kind) { + if (!isRecord(value)) { + return null; + } + return readKnownNumberDeep(value, kind === "page" ? PAGE_NUMBER_KEYS : PAGE_SIZE_KEYS); + } + function findMarketListContainer(value) { + const queue = [value]; + while (queue.length > 0) { + const current = queue.shift(); + if (!isRecord(current)) { + continue; + } + if (readMarketListArray(current)) { + return current; + } + Object.values(current).forEach((entry) => { + queue.push(unwrapVueRef(entry)); + }); + } + return null; + } + function readMarketListArray(record) { + const marketList = unwrapVueRef(record.marketList); + if (Array.isArray(marketList)) { + return marketList; + } + const authors = unwrapVueRef(record.authors); + if (Array.isArray(authors)) { + return authors; + } + return null; + } + function unwrapVueRef(value) { + if (isRecord(value) && "value" in value) { + return value.value; + } + return value; + } + function isRecord(value) { + return typeof value === "object" && value !== null; + } + function readMarketAttributeDatas(record) { + return isRecord(record.attribute_datas) ? record.attribute_datas : {}; + } + function readMarketFieldValue(record, attributeDatas, field) { + return record[field] ?? attributeDatas[field]; + } + function readString(value) { + return typeof value === "string" ? value : null; + } + function normalizeMarketListRate(value) { + if (typeof value === "number") { + return normalizeFractionRateDisplay(String(value)); + } + return typeof value === "string" ? normalizeFractionRateDisplay(value) : null; + } + function normalizeExportCellText(value) { + return value?.replace(/\s+/g, " ").trim() ?? ""; + } + function buildMarketExportFieldFallbacks(record, attributeDatas) { + const exportFields = {}; + const authorInfo = buildMarketAuthorInfo(record, attributeDatas); + const authorType = buildMarketAuthorType(record, attributeDatas); + const contentTheme = buildMarketContentTheme(record, attributeDatas); + const connectedUsers = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "link_link_cnt_by_industry")) + ); + const followerCount = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "follower")) + ); + const expectedCpm = formatDecimalDisplay( + readNumericValue(readMarketFieldValue(record, attributeDatas, "prospective_20_60_cpm")) + ); + const expectedPlayCount = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "expected_play_num")) + ); + const interactionRate = formatFractionPercent( + readNumericValue(readMarketFieldValue(record, attributeDatas, "interact_rate_within_30d")) + ); + const finishRate = formatFractionPercent( + readNumericValue(readMarketFieldValue(record, attributeDatas, "play_over_rate_within_30d")) + ); + const burstRate = readBurstRateDisplay( + readNumericValue(readMarketFieldValue(record, attributeDatas, "burst_text_rate")) + ); + const price21To60s = readMarketPrice21To60s(record, attributeDatas); + const representativeVideo = readMarketRepresentativeVideo(record, attributeDatas); + assignExportField(exportFields, "\u8FBE\u4EBA\u4FE1\u606F", authorInfo); + assignExportField(exportFields, "\u4EE3\u8868\u89C6\u9891", representativeVideo); + assignExportField(exportFields, "\u8FBE\u4EBA\u7C7B\u578B", authorType); + assignExportField(exportFields, "\u5185\u5BB9\u4E3B\u9898", contentTheme); + assignExportField(exportFields, "\u8FDE\u63A5\u7528\u6237\u6570", connectedUsers); + assignExportField(exportFields, "\u7C89\u4E1D\u6570", followerCount); + assignExportField(exportFields, "\u9884\u671FCPM", expectedCpm); + assignExportField(exportFields, "\u9884\u671F\u64AD\u653E\u91CF", expectedPlayCount); + assignExportField(exportFields, "\u4E92\u52A8\u7387", interactionRate); + assignExportField(exportFields, "\u5B8C\u64AD\u7387", finishRate); + assignExportField(exportFields, "\u7206\u6587\u7387", burstRate); + assignExportField(exportFields, "21-60s\u62A5\u4EF7", price21To60s); + return Object.keys(exportFields).length > 0 ? exportFields : void 0; + } + function assignExportField(exportFields, key, value) { + if (hasTextValue(value)) { + exportFields[key] = value; + } + } + function hasTextValue(value) { + return Boolean(value && value.trim().length > 0); + } + function buildMarketAuthorInfo(record, attributeDatas) { + const nickname = readString(readMarketFieldValue(record, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(record, attributeDatas, "nick_name")) ?? ""; + const parts = [ + nickname, + readMarketGenderLabel(readMarketFieldValue(record, attributeDatas, "gender")), + readString(readMarketFieldValue(record, attributeDatas, "city")) ?? "" + ].filter((value) => Boolean(value)); + return parts.length > 0 ? parts.join(" ") : void 0; + } + function buildMarketAuthorType(record, attributeDatas) { + const tagsRelation = readRecordLike( + readMarketFieldValue(record, attributeDatas, "tags_relation") + ); + if (tagsRelation) { + const primaryTag = Object.keys(tagsRelation)[0]; + if (hasTextValue(primaryTag)) { + return primaryTag; + } + } + return void 0; + } + function buildMarketContentTheme(record, attributeDatas) { + const themes = readStringArray( + readMarketFieldValue(record, attributeDatas, "content_theme_labels_180d") + ); + if (themes.length === 0) { + return void 0; + } + if (themes.length <= 2) { + return themes.join(" "); + } + return `${themes.slice(0, 2).join(" ")} ${themes.length - 2}+`; + } + function readMarketLocation(record, attributeDatas) { + return readString(readMarketFieldValue(record, attributeDatas, "city")) ?? void 0; + } + function readMarketPrice21To60s(record, attributeDatas) { + return formatCurrencyValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "price_20_60")) + ); + } + function readMarketRepresentativeVideo(record, attributeDatas) { + const items = readArrayLike(readMarketFieldValue(record, attributeDatas, "items")); + for (const item of items) { + if (!isRecord(item)) { + continue; + } + const title = readString(item.title); + if (hasTextValue(title)) { + return normalizeExportCellText(title); + } + } + return void 0; + } + function readMarketGenderLabel(value) { + const rawValue = typeof value === "number" ? String(value) : readString(value); + if (rawValue === "1") { + return "\u7537"; + } + if (rawValue === "2") { + return "\u5973"; + } + return void 0; + } + function readBurstRateDisplay(value) { + if (value === null) { + return void 0; + } + if (value <= 0) { + return "-"; + } + return formatFractionPercent(value); + } + function formatCurrencyValue(value) { + if (value === null) { + return void 0; + } + return `\xA5${value.toLocaleString("en-US", { + maximumFractionDigits: 0 + })}`; + } + function formatWanValue(value) { + if (value === null) { + return void 0; + } + return `${formatDecimalWithGrouping(value / 1e4)}w`; + } + function formatFractionPercent(value) { + if (value === null) { + return void 0; + } + return `${formatDecimalDisplay(value * 100)}%`; + } + function formatDecimalDisplay(value) { + if (value === null) { + return void 0; + } + return value.toLocaleString("en-US", { + maximumFractionDigits: 1, + minimumFractionDigits: 0, + useGrouping: false + }); + } + function formatDecimalWithGrouping(value) { + return value.toLocaleString("en-US", { + maximumFractionDigits: 1, + minimumFractionDigits: 0 + }); + } + function readNumericValue(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string") { + const trimmedValue = value.trim(); + if (!trimmedValue) { + return null; + } + const parsedValue = Number(trimmedValue); + return Number.isFinite(parsedValue) ? parsedValue : null; + } + return null; + } + function readStringArray(value) { + if (Array.isArray(value)) { + return value.filter((item) => typeof item === "string"); + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return Array.isArray(parsedValue) ? parsedValue.filter((item) => typeof item === "string") : []; + } catch { + return []; + } + } + return []; + } + function readArrayLike(value) { + if (Array.isArray(value)) { + return value; + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return Array.isArray(parsedValue) ? parsedValue : []; + } catch { + return []; + } + } + return []; + } + function readRecordLike(value) { + if (isRecord(value)) { + return value; + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return isRecord(parsedValue) ? parsedValue : null; + } catch { + return null; + } + } + return null; + } + function readKnownNumber(record, keys) { + for (const key of keys) { + const value = readNumericValue(record[key]); + if (value !== null) { + return value; + } + } + return void 0; + } + function readKnownNumberDeep(value, keys) { + if (!isRecord(value)) { + return null; + } + const directValue = readKnownNumber(value, keys); + if (typeof directValue === "number") { + return directValue; + } + for (const nestedValue of Object.values(value)) { + const candidate = readKnownNumberDeep(unwrapVueRef(nestedValue), keys); + if (typeof candidate === "number") { + return candidate; + } + } + return null; + } + + // src/content/market/dom-sync.ts + var BACKEND_COLUMN_KEY = "backendMetrics"; + var SELECTION_COLUMN_KEY = "selection"; + var SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate"; + var PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate"; + var ACTION_HEADER_TEXT = "\u64CD\u4F5C"; + var AUTHOR_HEADER_TEXT = "\u8FBE\u4EBA\u4FE1\u606F"; + var BACKEND_HEADER_TEXT = "\u79D2\u63A2\u6307\u6807"; + var MARKET_SCROLL_HINT_TEXT = "\u6A2A\u5411\u6EDA\u52A8\u53EF\u67E5\u770B\u770B\u540E\u641C\u7387\u3001\u79D2\u63A2\u6307\u6807"; + var MARKET_SCROLLBAR_STYLE_ID = "sces-market-scrollbar-style"; + var UNAVAILABLE_RATE_TEXT = "\u6682\u65E0\u6765\u6E90"; + var UNAVAILABLE_BACKEND_METRICS_TEXT = "\u6682\u65E0\u6570\u636E"; + var SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows"; + var SORTABLE_RATE_FIELDS = [SINGLE_COLUMN_KEY, PERSONAL_COLUMN_KEY]; + var BACKEND_METRIC_COLUMNS2 = [ + { + field: "afterViewSearchRate", + label: "\u770B\u540E\u641C\u7387" + }, + { + field: "afterViewSearchCount", + label: "\u770B\u540E\u641C\u6570" + }, + { + field: "a3IncreaseCount", + label: "\u65B0\u589EA3\u6570" + }, + { + field: "newA3Rate", + label: "\u65B0\u589EA3\u7387" + }, + { + field: "cpa3", + label: "CPA3" + }, + { + field: "cpSearch", + label: "cp_search" + } + ]; + var SORTABLE_MARKET_FIELDS = [ + ...SORTABLE_RATE_FIELDS, + ...BACKEND_METRIC_COLUMNS2.map((column) => column.field) + ]; + function syncMarketTable(root) { + return syncSyntheticMarketTable(root) ?? syncDivGridMarketTable(root); + } + function readMarketPageSignature(root) { + const document2 = getOwnerDocument(root); + const explicitPageIndex = document2?.documentElement.getAttribute("data-test-page-index") ?? ""; + const activePageIndex = document2?.querySelector(".el-pagination .number.active, .xt-pagination .number.active")?.textContent?.trim() ?? ""; + const authorIds = readRawAuthorIds(root).join("|"); + return `${explicitPageIndex || activePageIndex}::${authorIds}`; + } + function findNextPageControl(root) { + const document2 = getOwnerDocument(root); + if (!document2) { + return null; + } + const explicitControl = document2.querySelector('[data-testid="next-page"]'); + if (explicitControl instanceof document2.defaultView.HTMLElement) { + return explicitControl; + } + const paginationNextControl = document2.querySelector( + ".el-pagination .btn-next, .xt-pagination .btn-next" + ); + if (paginationNextControl instanceof document2.defaultView.HTMLElement) { + return paginationNextControl; + } + const candidates = Array.from( + document2.querySelectorAll("button, a, [role='button']") + ).filter( + (element) => element instanceof document2.defaultView.HTMLElement + ); + return candidates.find( + (element) => /下一页|next/i.test(normalizeExportCellText2(element.textContent)) + ) ?? null; + } + function isPageControlDisabled(control) { + if (!control) { + return true; + } + if (control instanceof HTMLButtonElement) { + return control.disabled; + } + return control.getAttribute("aria-disabled") === "true"; + } + function renderMarketRowState(rowDom, record) { + renderBackendMetricsCells(rowDom.backendMetricsCells, record); + if (record.status === "success" && record.rates) { + rowDom.singleCell.textContent = readRateCellText( + record.rates.singleVideoAfterSearchRate + ); + rowDom.personalCell.textContent = readRateCellText( + record.rates.personalVideoAfterSearchRate + ); + return; + } + if (record.status === "loading") { + rowDom.singleCell.textContent = "\u52A0\u8F7D\u4E2D..."; + rowDom.personalCell.textContent = "\u52A0\u8F7D\u4E2D..."; + return; + } + if (record.status === "failed") { + rowDom.singleCell.textContent = "\u52A0\u8F7D\u5931\u8D25"; + rowDom.personalCell.textContent = "\u52A0\u8F7D\u5931\u8D25"; + return; + } + rowDom.singleCell.textContent = ""; + rowDom.personalCell.textContent = ""; + } + function applyRowVisibility(table, visibleAuthorIds) { + table.rows.forEach((rowDom) => { + const isVisible = visibleAuthorIds.has(rowDom.authorId); + rowDom.visibilityTargets.forEach((target) => { + target.hidden = !isVisible; + }); + }); + } + function applyRowOrder(table, orderedAuthorIds) { + const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom])); + const orderByAuthorId = new Map( + orderedAuthorIds.map((authorId, index) => [authorId, index]) + ); + orderedAuthorIds.forEach((authorId) => { + const rowDom = rowById.get(authorId); + if (!rowDom) { + return; + } + rowDom.orderTargets.forEach(({ container, mode, node }) => { + const visualOrder = orderByAuthorId.get(authorId) ?? orderedAuthorIds.length; + if (mode === "css") { + container.dataset.marketOrderMode = "css"; + container.style.display = "flex"; + container.style.flexDirection = "column"; + node.style.order = String(visualOrder); + return; + } + container.dataset.marketOrderMode = "dom"; + container.appendChild(node); + }); + }); + } + function syncPluginSortHeaders(root, options) { + SORTABLE_MARKET_FIELDS.forEach((field) => { + const cell = root.querySelector( + `[data-market-header-cell="${field}"]` + ); + if (!cell) { + return; + } + syncSortableHeaderCell(cell, { + direction: options.activeSort?.field === field ? options.activeSort.direction : "none", + field, + onToggleSort: options.onToggleSort + }); + }); + } + function syncMarketSelectionState(table, selectedAuthorIds) { + table.rows.forEach((rowDom) => { + rowDom.selectionCheckbox.dataset.marketSelectionAuthorId = rowDom.authorId; + rowDom.selectionCheckbox.checked = selectedAuthorIds.has(rowDom.authorId); + }); + if (!table.headerSelectionCheckbox) { + return; + } + const visibleRows = table.rows.filter( + (rowDom) => rowDom.visibilityTargets.some((target) => !target.hidden) + ); + const scopedRows = visibleRows.length > 0 ? visibleRows : table.rows; + const selectedCount = scopedRows.filter( + (rowDom) => selectedAuthorIds.has(rowDom.authorId) + ).length; + table.headerSelectionCheckbox.indeterminate = selectedCount > 0 && selectedCount < scopedRows.length; + table.headerSelectionCheckbox.checked = scopedRows.length > 0 && selectedCount === scopedRows.length; + table.headerSelectionCheckbox.disabled = scopedRows.length === 0; + } + function syncSyntheticMarketTable(root) { + const header = root.querySelector("[data-market-header]"); + const body = root.querySelector("[data-market-body]"); + if (!header || !body) { + return null; + } + const selectionHeader = ensureSyntheticHeaderCell(header, SELECTION_COLUMN_KEY, ""); + const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeader); + ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387"); + ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387"); + BACKEND_METRIC_COLUMNS2.forEach(({ field, label }) => { + ensureSyntheticHeaderCell(header, field, label); + }); + const headerLabelsByField = readSyntheticHeaderLabels(header); + const rows = Array.from(body.querySelectorAll("[data-market-row]")).map( + (rowElement) => { + const row = rowElement; + const selectionCell = ensureSyntheticRowCell(row, SELECTION_COLUMN_KEY); + const selectionCheckbox = ensureSelectionRowControl(selectionCell); + const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY); + const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY); + const backendMetricsCells = Object.fromEntries( + BACKEND_METRIC_COLUMNS2.map(({ field }) => [field, ensureSyntheticRowCell(row, field)]) + ); + const authorId = row.dataset.authorId ?? ""; + selectionCheckbox.dataset.marketSelectionAuthorId = authorId; + return { + authorId, + authorName: row.querySelector('[data-market-field="authorName"]')?.textContent?.trim() ?? "", + backendMetricsCells, + exportFields: readSyntheticExportFields(row, headerLabelsByField), + hasDirectRatesSource: false, + location: row.querySelector('[data-market-field="location"]')?.textContent?.trim() ?? "", + orderTargets: [ + { + container: body, + mode: "dom", + node: row + } + ], + personalCell, + price21To60s: row.querySelector('[data-market-field="price21To60s"]')?.textContent?.trim() ?? "", + rates: void 0, + row, + selectionCheckbox, + singleCell, + visibilityTargets: [row] + }; + } + ); + return { + headerSelectionCheckbox, + rows + }; + } + function syncDivGridMarketTable(root) { + const document2 = getOwnerDocument(root); + if (!document2) { + return null; + } + for (const marketRoot of document2.querySelectorAll(".base-author-list")) { + if (!(marketRoot instanceof document2.defaultView.HTMLElement)) { + continue; + } + const syncedTable = syncDivGridRoot(marketRoot); + if (syncedTable) { + return syncedTable; + } + } + return null; + } + function readRawAuthorIds(root) { + const document2 = getOwnerDocument(root); + const syntheticAuthorIds = readSyntheticAuthorIds(root); + if (syntheticAuthorIds && syntheticAuthorIds.length > 0) { + return syntheticAuthorIds; + } + const divGridAuthorIds = readDivGridAuthorIds(root); + if (divGridAuthorIds && divGridAuthorIds.length > 0) { + return divGridAuthorIds; + } + if (!document2) { + return []; + } + return readSerializedMarketRows(document2).map((row) => row.authorId).filter((authorId) => Boolean(authorId)); + } + function readSyntheticAuthorIds(root) { + const body = root.querySelector("[data-market-body]"); + if (!body) { + return null; + } + return Array.from(body.querySelectorAll("[data-market-row]")).map( + (row) => row instanceof HTMLElement ? row.dataset.authorId ?? "" : "" + ).filter((authorId) => Boolean(authorId)); + } + function readDivGridAuthorIds(root) { + const document2 = getOwnerDocument(root); + if (!document2) { + return null; + } + const marketRoot = document2.querySelector(".base-author-list"); + if (!(marketRoot instanceof document2.defaultView.HTMLElement)) { + return null; + } + const bodySection = Array.from(marketRoot.querySelectorAll(".section-wrapper")).find( + (section) => section instanceof document2.defaultView.HTMLElement && !section.classList.contains("sticky-header") + ); + const authorSection = bodySection ? Array.from(bodySection.children).find( + (child) => child instanceof document2.defaultView.HTMLElement && child.querySelector(".content-column .content-cell") + ) ?? null : null; + const authorColumn = authorSection ? getNativeAuthorColumn(authorSection) : null; + if (!authorColumn) { + return null; + } + return getDirectContentCells(authorColumn).map((cell) => extractAuthorId(cell)).filter((authorId) => Boolean(authorId)); + } + function syncDivGridRoot(root) { + const headerSection = root.querySelector( + ".section-wrapper.sticky-header" + ); + const bodySection = Array.from(root.querySelectorAll(".section-wrapper")).find( + (section) => section instanceof root.ownerDocument.defaultView.HTMLElement && !section.classList.contains("sticky-header") + ); + if (!headerSection || !bodySection) { + return null; + } + const authorHeader = findCellByText(getDirectHeaderCells(headerSection), AUTHOR_HEADER_TEXT); + const actionHeader = findCellByText(getDirectHeaderCells(headerSection), ACTION_HEADER_TEXT); + if (!authorHeader || !actionHeader) { + return null; + } + const rightHeaderSection = actionHeader.parentElement; + if (!(rightHeaderSection instanceof root.ownerDocument.defaultView.HTMLElement)) { + return null; + } + const middleHeaderSection = findPreviousNativeSection(rightHeaderSection) ?? rightHeaderSection; + const authorSection = getIndexedChild( + bodySection, + getDirectChildIndex(headerSection, authorHeader) + ); + const authorHeaderSection = getIndexedChild( + headerSection, + getDirectChildIndex(headerSection, authorHeader) + ); + const rightSection = getIndexedChild( + bodySection, + getDirectChildIndex(headerSection, actionHeader) + ); + if (!authorSection || !authorHeaderSection || !rightSection) { + return null; + } + const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection; + const pluginHeaderSection = ensurePluginSection(headerSection, rightHeaderSection, { + testId: "plugin-header", + type: "header" + }); + const pluginBodySection = ensurePluginSection(bodySection, rightSection, { + testId: "plugin-section", + type: "body" + }); + const authorColumn = getNativeAuthorColumn(authorSection); + const actionColumn = getActionColumn(rightSection); + if (!authorColumn || !actionColumn) { + return null; + } + const rowCount = getDirectContentCells(authorColumn).length; + const selectionHeaderCell = ensureDivHeaderCell( + authorHeaderSection, + authorHeader, + SELECTION_COLUMN_KEY, + "" + ); + const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeaderCell); + const selectionColumn = ensureDivBodyColumn( + authorSection, + authorColumn, + SELECTION_COLUMN_KEY, + rowCount + ); + const headerTemplateCell = getDirectHeaderCells(middleHeaderSection).at(-1) ?? findPreviousHeaderCell(actionHeader) ?? actionHeader; + const bodyTemplateColumn = getDirectContentColumns(middleBodySection).at(-1) ?? findPreviousColumn(actionColumn) ?? actionColumn; + ensureDivHeaderCell( + pluginHeaderSection, + headerTemplateCell, + SINGLE_COLUMN_KEY, + "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387" + ); + ensureDivHeaderCell( + pluginHeaderSection, + headerTemplateCell, + PERSONAL_COLUMN_KEY, + "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387" + ); + const singleColumn = ensureDivBodyColumn( + pluginBodySection, + bodyTemplateColumn, + SINGLE_COLUMN_KEY, + rowCount + ); + const personalColumn = ensureDivBodyColumn( + pluginBodySection, + bodyTemplateColumn, + PERSONAL_COLUMN_KEY, + rowCount + ); + const backendMetricColumns = Object.fromEntries( + BACKEND_METRIC_COLUMNS2.map(({ field, label }) => { + ensureDivHeaderCell(pluginHeaderSection, headerTemplateCell, field, label); + return [ + field, + ensureDivBodyColumn( + pluginBodySection, + bodyTemplateColumn, + field, + rowCount + ) + ]; + }) + ); + syncContainerWidth(pluginHeaderSection); + syncContainerWidth(pluginBodySection); + syncContainerWidth(authorHeaderSection); + syncContainerWidth(authorSection); + ensureVisibleHorizontalScroll(headerSection); + ensureVisibleHorizontalScroll(bodySection); + ensureScrollHint(root, headerSection); + const allBodyColumns = Array.from(bodySection.children).flatMap( + (section) => section instanceof root.ownerDocument.defaultView.HTMLElement ? getDirectContentColumns(section) : [] + ); + const allHeaderCells = Array.from(headerSection.children).flatMap( + (section) => section instanceof root.ownerDocument.defaultView.HTMLElement ? getDirectHeaderCells(section) : [] + ); + const authorCells = getDirectContentCells(authorColumn); + const selectionCells = getDirectContentCells(selectionColumn); + const singleCells = getDirectContentCells(singleColumn); + const personalCells = getDirectContentCells(personalColumn); + const backendMetricCellsByField = Object.fromEntries( + BACKEND_METRIC_COLUMNS2.map(({ field }) => [ + field, + getDirectContentCells(backendMetricColumns[field]) + ]) + ); + const priceColumn = findPreviousColumn(actionColumn); + const priceCells = priceColumn ? getDirectContentCells(priceColumn) : []; + const remainingVueMarketRows = [...readVueMarketRows(root)]; + const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)]; + const rows = authorCells.flatMap((authorCell, index) => { + const selectionCell = selectionCells[index] ?? null; + const singleCell = singleCells[index] ?? null; + const personalCell = personalCells[index] ?? null; + const backendMetricsCells = Object.fromEntries( + BACKEND_METRIC_COLUMNS2.map(({ field }) => [ + field, + backendMetricCellsByField[field][index] ?? null + ]) + ); + if (!selectionCell || !singleCell || !personalCell || Object.values(backendMetricsCells).some((cell) => cell === null)) { + return []; + } + const selectionCheckbox = ensureSelectionRowControl(selectionCell); + const alignedRowCells = allBodyColumns.map( + (column) => getDirectContentCells(column)[index] ?? null + ); + const rowCells = alignedRowCells.filter( + (cell) => cell !== null + ); + const directAuthorId = extractAuthorId(authorCell) || ""; + const directAuthorName = extractAuthorName(authorCell) || ""; + const vueMarketRow = takeMatchedMarketDataRow( + remainingVueMarketRows, + directAuthorId, + directAuthorName + ); + const serializedMarketRow = takeMatchedMarketDataRow( + remainingSerializedMarketRows, + directAuthorId, + directAuthorName + ); + const fallbackMarketRow = mergeMarketDataRows(serializedMarketRow, vueMarketRow); + const exportFields = mergeExportFieldMaps( + readExportFieldsForDivGridRow(allHeaderCells, alignedRowCells), + fallbackMarketRow?.exportFields + ); + const authorId = directAuthorId || fallbackMarketRow?.authorId || ""; + const authorName = directAuthorName || fallbackMarketRow?.authorName || ""; + const price21To60s = mergeNonEmptyString( + readDivGridPriceDisplay(priceCells[index]?.textContent), + fallbackMarketRow?.price21To60s + ); + selectionCheckbox.dataset.marketSelectionAuthorId = authorId; + return [ + { + authorId, + authorName, + backendMetricsCells, + exportFields, + hasDirectRatesSource: fallbackMarketRow?.hasDirectRatesSource ?? false, + location: fallbackMarketRow?.location, + orderTargets: rowCells.map((cell) => { + const container = cell.parentElement; + if (!(container instanceof root.ownerDocument.defaultView.HTMLElement)) { + return null; + } + return { + container, + mode: "css", + node: cell + }; + }).filter((target) => target !== null), + personalCell, + price21To60s, + rates: fallbackMarketRow?.rates, + row: authorCell, + selectionCheckbox, + singleCell, + visibilityTargets: rowCells + } + ]; + }); + return { + headerSelectionCheckbox, + rows + }; + } + function ensureSyntheticHeaderCell(header, field, label) { + const existingCell = header.querySelector( + `[data-market-header-cell="${field}"]` + ); + if (existingCell) { + existingCell.textContent = label; + return existingCell; + } + const nextCell = header.ownerDocument.createElement("div"); + nextCell.dataset.marketHeaderCell = field; + nextCell.textContent = label; + if (field === SELECTION_COLUMN_KEY) { + header.insertBefore(nextCell, header.firstChild); + } else { + header.appendChild(nextCell); + } + return nextCell; + } + function ensureSyntheticRowCell(row, field) { + const existingCell = row.querySelector( + `[data-market-row-cell="${field}"]` + ); + if (existingCell) { + return existingCell; + } + const nextCell = row.ownerDocument.createElement(field === BACKEND_COLUMN_KEY ? "div" : "span"); + nextCell.dataset.marketRowCell = field; + if (field === SELECTION_COLUMN_KEY) { + row.insertBefore(nextCell, row.firstChild); + } else { + row.appendChild(nextCell); + } + return nextCell; + } + function ensureDivHeaderCell(container, templateCell, field, label) { + const existingCell = container.querySelector( + `[data-market-header-cell="${field}"]` + ); + if (existingCell) { + existingCell.textContent = label; + applyPluginHeaderCellStyles(existingCell); + return existingCell; + } + const nextCell = cloneElementShallow(templateCell); + nextCell.dataset.marketHeaderCell = field; + nextCell.textContent = label; + applyColumnWidth(nextCell, field); + applyPluginHeaderCellStyles(nextCell); + if (field === SELECTION_COLUMN_KEY) { + container.insertBefore(nextCell, templateCell); + } else { + container.appendChild(nextCell); + } + return nextCell; + } + function ensureDivBodyColumn(container, templateColumn, field, rowCount) { + const existingColumn = container.querySelector( + `[data-market-column-group="${field}"]` + ); + if (existingColumn) { + syncDivColumnCells(existingColumn, templateColumn, field, rowCount); + return existingColumn; + } + const nextColumn = cloneElementShallow(templateColumn); + nextColumn.dataset.marketColumnGroup = field; + applyColumnWidth(nextColumn, field); + syncDivColumnCells(nextColumn, templateColumn, field, rowCount); + if (field === SELECTION_COLUMN_KEY) { + container.insertBefore(nextColumn, templateColumn); + } else { + container.appendChild(nextColumn); + } + return nextColumn; + } + function syncDivColumnCells(column, templateColumn, field, rowCount) { + const currentCells = getDirectContentCells(column); + while (currentCells.length > rowCount) { + currentCells.pop()?.remove(); + } + const templateCells = getDirectContentCells(templateColumn); + for (let index = 0; index < rowCount; index += 1) { + const existingCell = getDirectContentCells(column)[index] ?? null; + if (existingCell) { + existingCell.dataset.marketRowCell = field; + applyPluginContentCellStyles(existingCell); + continue; + } + const templateCell = templateCells[index] ?? templateCells[templateCells.length - 1] ?? null; + const nextCell = field === SELECTION_COLUMN_KEY ? templateCell ? createSelectionContentCell(templateCell) : createBareContentCell(column.ownerDocument) : templateCell ? cloneElementShallow(templateCell) : createBareContentCell(column.ownerDocument); + nextCell.dataset.marketRowCell = field; + applyColumnWidth(nextCell, field); + applyPluginContentCellStyles(nextCell); + nextCell.textContent = ""; + column.appendChild(nextCell); + } + } + function applyPluginHeaderCellStyles(cell) { + cell.style.display = "flex"; + cell.style.alignItems = "center"; + cell.style.justifyContent = "normal"; + cell.style.cursor = "pointer"; + cell.style.whiteSpace = "nowrap"; + } + function applyPluginContentCellStyles(cell) { + cell.style.display = "flex"; + cell.style.alignItems = "center"; + cell.style.justifyContent = "normal"; + cell.style.paddingTop = "12px"; + cell.style.paddingBottom = "12px"; + cell.style.boxSizing = "border-box"; + cell.style.whiteSpace = "nowrap"; + } + function ensureSelectionHeaderControl(cell) { + cell.textContent = ""; + cell.style.gap = "6px"; + cell.style.justifyContent = "center"; + const checkbox = ensureSelectionCheckbox(cell, "header"); + const label = cell.querySelector( + '[data-market-selection-label="header"]' + ); + if (label) { + label.textContent = "\u5168\u9009"; + return checkbox; + } + const nextLabel = cell.ownerDocument.createElement("span"); + nextLabel.dataset.marketSelectionLabel = "header"; + nextLabel.textContent = "\u5168\u9009"; + nextLabel.style.fontSize = "12px"; + cell.appendChild(nextLabel); + return checkbox; + } + function ensureSelectionRowControl(cell) { + cell.textContent = ""; + cell.style.justifyContent = "center"; + return ensureSelectionCheckbox(cell, "row"); + } + function ensureSelectionCheckbox(container, kind) { + const existingCheckbox = container.querySelector( + `[data-market-selection-checkbox="${kind}"]` + ); + if (existingCheckbox) { + existingCheckbox.type = "checkbox"; + return existingCheckbox; + } + const checkbox = container.ownerDocument.createElement("input"); + checkbox.type = "checkbox"; + checkbox.dataset.marketSelectionCheckbox = kind; + checkbox.style.cursor = "pointer"; + container.appendChild(checkbox); + return checkbox; + } + function getOwnerDocument(root) { + if ("ownerDocument" in root && root.ownerDocument) { + return root.ownerDocument; + } + return "nodeType" in root && root.nodeType === 9 ? root : null; + } + function readSyntheticHeaderLabels(header) { + return Array.from(header.querySelectorAll("[data-market-header-cell]")).reduce((labels, cell) => { + if (!(cell instanceof header.ownerDocument.defaultView.HTMLElement)) { + return labels; + } + const field = cell.dataset.marketHeaderCell; + if (!field) { + return labels; + } + labels[field] = normalizeExportCellText2(cell.textContent); + return labels; + }, {}); + } + function readSyntheticExportFields(row, headerLabelsByField) { + const exportFields = {}; + for (const cell of row.querySelectorAll("[data-market-field]")) { + if (!(cell instanceof row.ownerDocument.defaultView.HTMLElement)) { + continue; + } + const field = cell.dataset.marketField; + const headerLabel = field ? headerLabelsByField[field] : ""; + if (!shouldExportColumn(headerLabel)) { + continue; + } + exportFields[headerLabel] = normalizeExportCellText2(cell.textContent); + } + return exportFields; + } + function readExportFieldsForDivGridRow(headerCells, rowCells) { + const exportFields = {}; + rowCells.forEach((cell, index) => { + const headerLabel = normalizeExportCellText2(headerCells[index]?.textContent); + if (!shouldExportColumn(headerLabel)) { + return; + } + exportFields[headerLabel] = headerLabel === "21-60s\u62A5\u4EF7" ? readDivGridPriceDisplay(cell?.textContent) ?? "" : normalizeExportCellText2(cell?.textContent); + }); + return exportFields; + } + function findPreviousHeaderCell(cell) { + let current = cell.previousElementSibling; + while (current) { + if (current instanceof cell.ownerDocument.defaultView.HTMLElement && current.classList.contains("header-cell")) { + return current; + } + current = current.previousElementSibling; + } + return null; + } + function findPreviousColumn(column) { + let current = column.previousElementSibling; + while (current) { + if (current instanceof column.ownerDocument.defaultView.HTMLElement && current.classList.contains("content-column")) { + return current; + } + current = current.previousElementSibling; + } + return null; + } + function ensurePluginSection(rootSection, referenceSection, options) { + const existingSection = rootSection.querySelector( + `[data-market-plugin-section="${options.type}"]` + ); + if (existingSection) { + existingSection.dataset.testid = options.testId; + existingSection.setAttribute("data-testid", options.testId); + return existingSection; + } + const templateSection = findPreviousSection(referenceSection) ?? referenceSection; + const nextSection = cloneElementShallow(templateSection); + nextSection.dataset.marketPluginSection = options.type; + nextSection.dataset.testid = options.testId; + nextSection.setAttribute("data-testid", options.testId); + resetStickySectionStyles(nextSection); + rootSection.insertBefore(nextSection, referenceSection); + return nextSection; + } + function ensureVisibleHorizontalScroll(section) { + ensureVisibleScrollbarStyles(section.ownerDocument); + section.classList.remove("hide-scrollbar"); + section.dataset.marketScrollbar = "visible"; + section.style.overflowX = "auto"; + section.style.scrollbarWidth = "thin"; + section.style.scrollbarColor = "rgba(148, 163, 184, 0.95) rgba(226, 232, 240, 0.9)"; + } + function ensureVisibleScrollbarStyles(document2) { + if (document2.getElementById(MARKET_SCROLLBAR_STYLE_ID)) { + return; + } + const style = document2.createElement("style"); + style.id = MARKET_SCROLLBAR_STYLE_ID; + style.textContent = ` + [data-market-scrollbar="visible"]::-webkit-scrollbar { + display: block !important; + height: 10px !important; + } + + [data-market-scrollbar="visible"]::-webkit-scrollbar-track { + background: rgba(226, 232, 240, 0.9) !important; + border-radius: 999px; + } + + [data-market-scrollbar="visible"]::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.95) !important; + border: 2px solid rgba(226, 232, 240, 0.9); + border-radius: 999px; + } + `; + document2.head.appendChild(style); + } + function ensureScrollHint(root, headerSection) { + const existingHint = root.querySelector( + '[data-testid="market-scroll-hint"]' + ); + if (existingHint) { + existingHint.textContent = MARKET_SCROLL_HINT_TEXT; + return; + } + const hint = root.ownerDocument.createElement("div"); + hint.dataset.testid = "market-scroll-hint"; + hint.setAttribute("data-testid", "market-scroll-hint"); + hint.textContent = MARKET_SCROLL_HINT_TEXT; + hint.style.color = "#64748b"; + hint.style.display = "flex"; + hint.style.fontSize = "12px"; + hint.style.justifyContent = "flex-end"; + hint.style.lineHeight = "18px"; + hint.style.padding = "0 12px 8px"; + root.insertBefore(hint, headerSection); + } + function findPreviousSection(section) { + let current = section.previousElementSibling; + while (current) { + if (current instanceof section.ownerDocument.defaultView.HTMLElement) { + return current; + } + current = current.previousElementSibling; + } + return null; + } + function findPreviousNativeSection(section) { + let current = section.previousElementSibling; + while (current) { + if (current instanceof section.ownerDocument.defaultView.HTMLElement && !current.hasAttribute("data-market-plugin-section")) { + return current; + } + current = current.previousElementSibling; + } + return null; + } + function resetStickySectionStyles(section) { + section.style.position = ""; + section.style.left = ""; + section.style.right = ""; + section.style.zIndex = ""; + section.style.width = ""; + section.style.minWidth = ""; + } + function getActionColumn(bodySection) { + const columns = getDirectContentColumns(bodySection); + return columns[columns.length - 1] ?? null; + } + function getNativeAuthorColumn(authorSection) { + return getDirectContentColumns(authorSection).find( + (column) => !column.dataset.marketColumnGroup && getDirectContentCells(column).some( + (cell) => cell.querySelector("a") || cell.querySelector(".author-nickname") || Boolean(cell.dataset.authorId) + ) + ) ?? null; + } + function getDirectHeaderCells(section) { + return Array.from(section.querySelectorAll(".header-cell")).filter( + (cell) => cell instanceof section.ownerDocument.defaultView.HTMLElement + ); + } + function getDirectContentColumns(section) { + return Array.from(section.children).filter( + (child) => child instanceof section.ownerDocument.defaultView.HTMLElement && child.classList.contains("content-column") + ); + } + function getDirectContentCells(column) { + return Array.from(column.children).filter( + (child) => child instanceof column.ownerDocument.defaultView.HTMLElement && child.classList.contains("content-cell") + ); + } + function getDirectChildIndex(root, descendant) { + const directChild = Array.from(root.children).find((child) => child.contains(descendant)); + return directChild ? Array.from(root.children).indexOf(directChild) : -1; + } + function getIndexedChild(root, index) { + if (index < 0) { + return null; + } + const child = root.children[index] ?? null; + return child instanceof root.ownerDocument.defaultView.HTMLElement ? child : null; + } + function findCellByText(cells, text) { + return cells.find((cell) => cell.textContent?.trim() === text) ?? null; + } + function cloneElementShallow(reference) { + const clone = reference.ownerDocument.createElement(reference.tagName); + Array.from(reference.attributes).forEach((attribute) => { + clone.setAttribute(attribute.name, attribute.value); + }); + return clone; + } + function createBareContentCell(document2) { + const cell = document2.createElement("div"); + cell.className = "content-cell"; + return cell; + } + function createSelectionContentCell(templateCell) { + const cell = cloneElementShallow(templateCell); + cell.removeAttribute("data-testid"); + cell.removeAttribute("data-author-id"); + return cell; + } + function extractAuthorId(authorCell) { + const explicitAuthorId = authorCell.dataset.authorId; + if (explicitAuthorId) { + return explicitAuthorId; + } + const linkedAuthorId = Array.from(authorCell.querySelectorAll("a")).map((link) => extractAuthorIdFromHref(link.href)).find((value) => Boolean(value)); + if (linkedAuthorId) { + return linkedAuthorId; + } + const fallbackAuthorId = authorCell.querySelector("[data-author-id]")?.getAttribute("data-author-id"); + return fallbackAuthorId ?? ""; + } + function extractAuthorName(authorCell) { + return authorCell.querySelector(".author-nickname")?.textContent?.trim() ?? authorCell.textContent?.trim() ?? ""; + } + function extractAuthorIdFromHref(href) { + const match = href.match(/\/author-homepage\/[^/]+\/(\d+)/); + return match?.[1] ?? null; + } + function readVueMarketRows(marketRoot) { + const vueRoot = marketRoot.__vue__; + const setupStates = collectVueSetupStates(vueRoot); + for (const setupState of setupStates) { + for (const value of Object.values(setupState)) { + const candidate = unwrapVueRef2(value); + if (!candidate || typeof candidate !== "object") { + continue; + } + const marketList = unwrapVueRef2( + candidate.marketList + ); + if (!Array.isArray(marketList)) { + continue; + } + return marketList.map((row) => isRecord2(row) ? mapMarketListRow(row) : null).filter((row) => row !== null); + } + } + return []; + } + function collectVueSetupStates(vueRoot) { + if (!vueRoot) { + return []; + } + const queue = [vueRoot]; + const setupStates = []; + while (queue.length > 0) { + const current = queue.shift(); + if (!isRecord2(current)) { + continue; + } + if (isRecord2(current._setupState)) { + setupStates.push(current._setupState); + } + const children = Array.isArray(current.$children) ? current.$children : []; + queue.push(...children); + } + return setupStates; + } + function readSerializedMarketRows(document2) { + const serializedRows = document2.documentElement.getAttribute( + SERIALIZED_MARKET_ROWS_ATTRIBUTE + ); + if (!serializedRows) { + return []; + } + try { + const parsedRows = JSON.parse(serializedRows); + if (!Array.isArray(parsedRows)) { + return []; + } + return parsedRows.map((row) => { + const record = isRecord2(row) ? row : {}; + const singleVideoAfterSearchRate = readString2( + record.singleVideoAfterSearchRate + ); + return { + authorId: readString2(record.authorId) ?? "", + authorName: readString2(record.authorName) ?? "", + exportFields: readSerializedExportFields(record), + hasDirectRatesSource: Boolean(singleVideoAfterSearchRate), + location: readString2(record.location) ?? void 0, + price21To60s: readString2(record.price21To60s) ?? void 0, + rates: singleVideoAfterSearchRate ? { + singleVideoAfterSearchRate + } : void 0 + }; + }).filter((row) => Boolean(row.authorId || row.authorName)); + } catch { + return []; + } + } + function unwrapVueRef2(value) { + if (isRecord2(value) && "value" in value) { + return value.value; + } + return value; + } + function isRecord2(value) { + return typeof value === "object" && value !== null; + } + function readString2(value) { + return typeof value === "string" ? value : null; + } + function normalizeExportCellText2(value) { + return value?.replace(/\s+/g, " ").trim() ?? ""; + } + function readDivGridPriceDisplay(value) { + const normalizedValue = normalizeExportCellText2(value); + if (!normalizedValue) { + return void 0; + } + const match = normalizedValue.match(/^¥?\s*([\d,]+(?:\.\d+)?)$/); + if (!match) { + return void 0; + } + const numericValue = Number(match[1].replace(/,/g, "")); + if (!Number.isFinite(numericValue)) { + return void 0; + } + return formatCurrencyValue2(numericValue); + } + function shouldExportColumn(label) { + const excludedBackendLabels = new Set(BACKEND_METRIC_COLUMNS2.map((column) => column.label)); + return Boolean( + label && label !== ACTION_HEADER_TEXT && label !== BACKEND_HEADER_TEXT && !excludedBackendLabels.has(label) && label !== "\u5355\u89C6\u9891\u770B\u540E\u641C\u7387" && label !== "\u4E2A\u4EBA\u89C6\u9891\u770B\u540E\u641C\u7387" + ); + } + function syncSortableHeaderCell(cell, options) { + const label = readSortableHeaderLabel(cell); + const sorterRoot = ensureHeaderSorterRoot(cell); + const text = ensureHeaderSorterText(sorterRoot); + const icon = ensureHeaderSorterIcon(sorterRoot); + const upTriangle = ensureHeaderTriangle(icon, "up"); + const downTriangle = ensureHeaderTriangle(icon, "down"); + text.textContent = label; + cell.dataset.marketSortField = options.field; + cell.dataset.marketSortDirection = options.direction; + cell.setAttribute("role", "button"); + cell.tabIndex = 0; + cell.onclick = () => { + options.onToggleSort(options.field); + }; + cell.onkeydown = (event) => { + if (event.key !== "Enter" && event.key !== " ") { + return; + } + event.preventDefault(); + options.onToggleSort(options.field); + }; + syncTriangleStyles(upTriangle, { + active: options.direction === "asc", + direction: "up" + }); + syncTriangleStyles(downTriangle, { + active: options.direction === "desc", + direction: "down" + }); + } + function readSortableHeaderLabel(cell) { + return cell.dataset.marketHeaderLabel ?? normalizeExportCellText2(cell.textContent) ?? ""; + } + function ensureHeaderSorterRoot(cell) { + const existingRoot = cell.querySelector( + '[data-market-sorter="root"]' + ); + if (existingRoot) { + return existingRoot; + } + cell.dataset.marketHeaderLabel = normalizeExportCellText2(cell.textContent); + cell.replaceChildren(); + const root = cell.ownerDocument.createElement("span"); + root.dataset.marketSorter = "root"; + root.style.alignItems = "center"; + root.style.display = "inline-flex"; + root.style.gap = "4px"; + root.style.maxWidth = "100%"; + cell.appendChild(root); + return root; + } + function ensureHeaderSorterText(sorterRoot) { + const existingText = sorterRoot.querySelector( + '[data-market-sorter="text"]' + ); + if (existingText) { + return existingText; + } + const text = sorterRoot.ownerDocument.createElement("span"); + text.dataset.marketSorter = "text"; + text.style.display = "inline-block"; + text.style.lineHeight = "20px"; + text.style.whiteSpace = "nowrap"; + sorterRoot.appendChild(text); + return text; + } + function ensureHeaderSorterIcon(sorterRoot) { + const existingIcon = sorterRoot.querySelector( + '[data-market-sorter="icon"]' + ); + if (existingIcon) { + return existingIcon; + } + const icon = sorterRoot.ownerDocument.createElement("span"); + icon.dataset.marketSorter = "icon"; + icon.style.display = "inline-flex"; + icon.style.flexDirection = "column"; + icon.style.gap = "2px"; + icon.style.justifyContent = "center"; + icon.style.minWidth = "8px"; + sorterRoot.appendChild(icon); + return icon; + } + function ensureHeaderTriangle(iconRoot, direction) { + const existingTriangle = iconRoot.querySelector( + `[data-market-sorter-triangle="${direction}"]` + ); + if (existingTriangle) { + return existingTriangle; + } + const triangle = iconRoot.ownerDocument.createElement("span"); + triangle.dataset.marketSorterTriangle = direction; + triangle.style.display = "block"; + triangle.style.height = "0"; + triangle.style.width = "0"; + triangle.style.borderLeft = "4px solid transparent"; + triangle.style.borderRight = "4px solid transparent"; + iconRoot.appendChild(triangle); + return triangle; + } + function syncTriangleStyles(triangle, options) { + const activeColor = "#1f2329"; + const inactiveColor = "#c9cdd4"; + if (options.direction === "up") { + triangle.style.borderBottom = `5px solid ${options.active ? activeColor : inactiveColor}`; + triangle.style.borderTop = "0 solid transparent"; + } else { + triangle.style.borderTop = `5px solid ${options.active ? activeColor : inactiveColor}`; + triangle.style.borderBottom = "0 solid transparent"; + } + } + function readRateCellText(value) { + return value ? normalizeRateDisplay(value) : UNAVAILABLE_RATE_TEXT; + } + function applyColumnWidth(element, field) { + if (field === SELECTION_COLUMN_KEY) { + element.style.minWidth = "56px"; + element.style.width = "56px"; + } + if (field === BACKEND_COLUMN_KEY) { + element.style.minWidth = "240px"; + element.style.width = "240px"; + } + if (field === SINGLE_COLUMN_KEY || field === PERSONAL_COLUMN_KEY) { + element.style.minWidth = "160px"; + element.style.width = "160px"; + } + if (BACKEND_METRIC_COLUMNS2.some((column) => column.field === field)) { + element.style.minWidth = "120px"; + element.style.width = "120px"; + } + } + function syncContainerWidth(container) { + if (!(container instanceof HTMLElement)) { + return; + } + const directChildren = Array.from(container.children).filter( + (child) => child instanceof HTMLElement + ); + const totalWidth = directChildren.reduce((sum, child) => { + return sum + readElementWidth(child); + }, 0); + if (totalWidth <= 0) { + return; + } + container.style.width = `${totalWidth}px`; + container.style.minWidth = `${totalWidth}px`; + } + function readElementWidth(element) { + const styleWidth = Number.parseFloat(element.style.width || ""); + if (Number.isFinite(styleWidth) && styleWidth > 0) { + return styleWidth; + } + const minWidth = Number.parseFloat(element.style.minWidth || ""); + if (Number.isFinite(minWidth) && minWidth > 0) { + return minWidth; + } + return 0; + } + function mergeMarketDataRows(baseRow, preferredRow) { + if (!baseRow && !preferredRow) { + return null; + } + if (!baseRow) { + return preferredRow; + } + if (!preferredRow) { + return baseRow; + } + return { + authorId: preferredRow.authorId || baseRow.authorId, + authorName: preferredRow.authorName || baseRow.authorName, + exportFields: mergeExportFieldMaps(baseRow.exportFields, preferredRow.exportFields), + hasDirectRatesSource: preferredRow.hasDirectRatesSource || baseRow.hasDirectRatesSource, + location: mergeNonEmptyString(baseRow.location, preferredRow.location), + price21To60s: mergeNonEmptyString( + baseRow.price21To60s, + preferredRow.price21To60s + ), + rates: mergeRates(baseRow.rates, preferredRow.rates) + }; + } + function takeMatchedMarketDataRow(remainingRows, authorId, authorName) { + if (remainingRows.length === 0) { + return null; + } + const matchedIndex = remainingRows.findIndex((row) => { + if (authorId && row.authorId === authorId) { + return true; + } + if (authorName && row.authorName === authorName) { + return true; + } + return false; + }); + if (matchedIndex >= 0) { + return remainingRows.splice(matchedIndex, 1)[0] ?? null; + } + if (!authorId && !authorName) { + return remainingRows.shift() ?? null; + } + return null; + } + function mergeExportFieldMaps(current, fallback) { + if (!current && !fallback) { + return void 0; + } + const nextFields = { + ...current ?? {} + }; + Object.entries(fallback ?? {}).forEach(([key, value]) => { + if (!hasTextValue2(nextFields[key]) && hasTextValue2(value)) { + nextFields[key] = value; + } + }); + return nextFields; + } + function mergeRates(current, fallback) { + if (!current && !fallback) { + return void 0; + } + return { + singleVideoAfterSearchRate: current?.singleVideoAfterSearchRate ?? fallback?.singleVideoAfterSearchRate, + personalVideoAfterSearchRate: current?.personalVideoAfterSearchRate ?? fallback?.personalVideoAfterSearchRate + }; + } + function mergeNonEmptyString(current, fallback) { + return hasTextValue2(current) ? current : fallback; + } + function formatCurrencyValue2(value) { + if (value === null) { + return void 0; + } + return `\xA5${value.toLocaleString("en-US", { + maximumFractionDigits: 0 + })}`; + } + function readSerializedExportFields(record) { + if (!isRecord2(record.exportFields)) { + return void 0; + } + const entries = Object.entries(record.exportFields).flatMap( + ([key, value]) => typeof value === "string" ? [[key, value]] : [] + ); + return entries.length > 0 ? Object.fromEntries(entries) : void 0; + } + function hasTextValue2(value) { + return typeof value === "string" && value.trim().length > 0; + } + function renderBackendMetricsCells(cells, record) { + if (record.backendMetricsStatus === "loading" || record.status === "loading" && !record.backendMetricsStatus) { + fillBackendMetricCells(cells, "\u52A0\u8F7D\u4E2D..."); + return; + } + if (record.backendMetricsStatus === "failed") { + fillBackendMetricCells(cells, "\u52A0\u8F7D\u5931\u8D25"); + return; + } + if (record.backendMetricsStatus === "missing") { + fillBackendMetricCells(cells, UNAVAILABLE_BACKEND_METRICS_TEXT); + return; + } + if (record.backendMetricsStatus !== "success" || !record.backendMetrics) { + fillBackendMetricCells(cells, ""); + return; + } + BACKEND_METRIC_COLUMNS2.forEach(({ field }) => { + cells[field].textContent = record.backendMetrics?.[field] ?? ""; + }); + } + function fillBackendMetricCells(cells, value) { + BACKEND_METRIC_COLUMNS2.forEach(({ field }) => { + cells[field].textContent = value; + }); + } + + // src/content/market/filter-sort-controller.ts + function applyFilterAndSort(records, options = {}) { + const filteredRecords = records.filter( + (record) => matchesFilters(record, options.filters) + ); + if (!options.sort) { + return filteredRecords; + } + return [...filteredRecords].sort( + (leftRecord, rightRecord) => compareRecords(leftRecord, rightRecord, options.sort) + ); + } + function matchesFilters(record, filters) { + if (!filters) { + return true; + } + return meetsThreshold( + record.rates?.singleVideoAfterSearchRate, + filters.singleVideoAfterSearchRateMin + ) && meetsThreshold( + record.rates?.personalVideoAfterSearchRate, + filters.personalVideoAfterSearchRateMin + ); + } + function meetsThreshold(rateValue, minValue) { + if (minValue == null) { + return true; + } + const lowerBound = parseRateLowerBound(rateValue ?? null); + return lowerBound != null && lowerBound >= minValue; + } + function compareRecords(leftRecord, rightRecord, sort) { + if (isRateSortField(sort.field)) { + return compareRateSortRecords(leftRecord, rightRecord, sort); + } + return compareBackendMetricRecords(leftRecord, rightRecord, sort); + } + function compareRateSortRecords(leftRecord, rightRecord, sort) { + const field = sort.field; + const leftValue = leftRecord.rates?.[field]; + const rightValue = rightRecord.rates?.[field]; + const leftLowerBound = parseRateLowerBound(leftValue ?? null); + const rightLowerBound = parseRateLowerBound(rightValue ?? null); + if (leftLowerBound == null && rightLowerBound == null) { + return compareRecordIdentity(leftRecord, rightRecord); + } + if (leftLowerBound == null) { + return 1; + } + if (rightLowerBound == null) { + return -1; + } + if (leftLowerBound !== rightLowerBound) { + return sort.direction === "asc" ? leftLowerBound - rightLowerBound : rightLowerBound - leftLowerBound; + } + const tieBreak = compareRateValues(leftValue, rightValue); + if (tieBreak !== 0) { + return sort.direction === "asc" ? tieBreak : -tieBreak; + } + return compareRecordIdentity(leftRecord, rightRecord); + } + function compareBackendMetricRecords(leftRecord, rightRecord, sort) { + const field = sort.field; + const leftValue = parseBackendMetricValue(leftRecord.backendMetrics?.[field]); + const rightValue = parseBackendMetricValue(rightRecord.backendMetrics?.[field]); + if (leftValue == null && rightValue == null) { + return compareRecordIdentity(leftRecord, rightRecord); + } + if (leftValue == null) { + return 1; + } + if (rightValue == null) { + return -1; + } + if (leftValue !== rightValue) { + return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue; + } + return compareRecordIdentity(leftRecord, rightRecord); + } + function parseBackendMetricValue(value) { + if (!value) { + return null; + } + const normalizedValue = value.replace(/,/g, "").replace(/%/g, "").trim(); + if (!normalizedValue) { + return null; + } + const numericValue = Number(normalizedValue); + return Number.isFinite(numericValue) ? numericValue : null; + } + function isRateSortField(field) { + return field === "singleVideoAfterSearchRate" || field === "personalVideoAfterSearchRate"; + } + function compareRecordIdentity(leftRecord, rightRecord) { + const authorIdCompare = leftRecord.authorId.localeCompare(rightRecord.authorId); + if (authorIdCompare !== 0) { + return authorIdCompare; + } + return leftRecord.authorName.localeCompare(rightRecord.authorName); + } + + // src/content/market/api-client.ts + function createMarketApiClient(options = {}) { + const baseUrl = options.baseUrl ?? resolveBaseUrl(); + const fetchImpl = options.fetchImpl ?? defaultFetch; + const timeoutMs = options.timeoutMs ?? 8e3; + return { + async loadAuthorAseInfo(authorId) { + const primaryResult = await loadAuthorMetricsFromUrl( + buildAuthorCommerceSeedBaseInfoUrl(authorId, baseUrl) + ); + if (primaryResult.success || primaryResult.reason === "timeout") { + return primaryResult; + } + return loadAuthorMetricsFromUrl(buildAuthorAseInfoUrl(authorId, baseUrl)); + } + }; + async function loadAuthorMetricsFromUrl(url) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetchImpl(url, { + credentials: "include", + method: "GET", + signal: controller.signal + }); + if (!response.ok) { + return { + success: false, + reason: "request-failed" + }; + } + return mapAuthorAseInfoResponse(await response.json()); + } catch (error) { + if (isAbortError(error) || controller.signal.aborted) { + return { + success: false, + reason: "timeout" + }; + } + return { + success: false, + reason: "request-failed" + }; + } finally { + clearTimeout(timeoutId); + } + } + } + function buildAuthorAseInfoUrl(authorId, baseUrl) { + const url = new URL("/gw/api/aggregator/get_author_ase_info", baseUrl); + url.searchParams.set("author_id", authorId); + url.searchParams.set("range", "30"); + return url.toString(); + } + function buildAuthorCommerceSeedBaseInfoUrl(authorId, baseUrl) { + const url = new URL( + "/gw/api/aggregator/get_author_commerce_seed_base_info", + baseUrl + ); + url.searchParams.set("o_author_id", authorId); + url.searchParams.set("range", "90"); + return url.toString(); + } + function mapAuthorAseInfoResponse(payload) { + const data = getPayloadData(payload); + if (!data) { + return { + success: false, + reason: "bad-response" + }; + } + const singleVideoAfterSearchRate = readNormalizedRate( + data.avg_search_after_view_rate + ); + const personalVideoAfterSearchRate = readNormalizedRate( + data.personal_avg_search_after_view_rate + ); + if (!singleVideoAfterSearchRate && !personalVideoAfterSearchRate) { + return { + success: false, + reason: "missing-rate" + }; + } + return { + success: true, + rates: { + ...singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : {}, + ...personalVideoAfterSearchRate ? { personalVideoAfterSearchRate } : {} + } + }; + } + function getPayloadData(payload) { + if (!isRecord3(payload)) { + return null; + } + return isRecord3(payload.data) ? payload.data : payload; + } + function readNormalizedRate(value) { + return typeof value === "string" ? normalizeRateDisplay(value) : null; + } + function resolveBaseUrl() { + if (typeof location !== "undefined" && location.origin) { + return location.origin; + } + return "https://xingtu.cn"; + } + async function defaultFetch(input, init) { + return fetch(input, init); + } + function isAbortError(error) { + return error instanceof Error && error.name === "AbortError"; + } + function isRecord3(value) { + return typeof value === "object" && value !== null; + } + + // src/content/market/export-range-controller.ts + function createExportRangeController(options) { + return { + async exportRecords(target) { + const mergedRecords = /* @__PURE__ */ new Map(); + let currentPage = 0; + let expectedMinimumRowCount; + while (true) { + currentPage += 1; + options.onProgress?.({ + currentPage, + totalPages: target.mode === "count" ? target.pageCount : void 0 + }); + const currentPageRecords = await preparePageRecords(expectedMinimumRowCount); + if (!currentPageRecords) { + throw new Error(`\u7B2C ${currentPage} \u9875\u52A0\u8F7D\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`); + } + currentPageRecords.forEach((record) => { + const existingRecord = mergedRecords.get(record.authorId); + mergedRecords.set(record.authorId, mergeMarketRecord(existingRecord, record)); + }); + expectedMinimumRowCount = Math.max( + expectedMinimumRowCount ?? 0, + currentPageRecords.length + ); + if (target.mode === "count" && currentPage >= target.pageCount) { + break; + } + const previousSignature = readMarketPageSignature(options.document); + const nextPageControl = findNextPageControl(options.document); + if (!nextPageControl || isPageControlDisabled(nextPageControl)) { + break; + } + nextPageControl.click(); + const pageChanged = await waitForPageChange(previousSignature); + if (!pageChanged) { + throw new Error(`\u7B2C ${currentPage + 1} \u9875\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`); + } + } + return Array.from(mergedRecords.values()); + } + }; + async function preparePageRecords(expectedMinimumRowCount) { + for (let attempt = 0; attempt < 4; attempt += 1) { + const currentPageReady = await waitForCurrentPageReady(); + if (!currentPageReady) { + return null; + } + await options.prepareCurrentPageForExport(); + const currentPageRecords = options.readCurrentPageRecords(); + if (currentPageRecords.length > 0 && (typeof expectedMinimumRowCount !== "number" || expectedMinimumRowCount <= 0 || isCurrentPageTerminal() || currentPageRecords.length >= expectedMinimumRowCount)) { + return currentPageRecords; + } + } + return null; + } + async function waitForPageChange(previousSignature) { + const previousPageState = parsePageSignature(previousSignature); + for (let attempt = 0; attempt < 60; attempt += 1) { + await new Promise((resolve) => { + options.window.setTimeout(resolve, 50); + }); + await Promise.resolve(); + const nextSignature = readMarketPageSignature(options.document); + const nextPageState = parsePageSignature(nextSignature); + if (hasLoadedNextPage(previousPageState, nextPageState)) { + return true; + } + } + return false; + } + async function waitForCurrentPageReady() { + let stableAttemptCount = 0; + let lastReadyFingerprint = ""; + for (let attempt = 0; attempt < 80; attempt += 1) { + await new Promise((resolve) => { + options.window.setTimeout(resolve, 150); + }); + await Promise.resolve(); + const pageState = readCurrentPageState(); + if (!pageState.authorIds || pageState.rowCount <= 0) { + stableAttemptCount = 0; + lastReadyFingerprint = ""; + continue; + } + const readyFingerprint = [ + pageState.pageToken, + pageState.authorIds, + String(pageState.rowCount), + pageState.isTerminalPage ? "terminal" : "paged" + ].join("::"); + if (readyFingerprint === lastReadyFingerprint) { + stableAttemptCount += 1; + } else { + lastReadyFingerprint = readyFingerprint; + stableAttemptCount = 1; + } + if (stableAttemptCount >= 6) { + return true; + } + } + return false; + } + function readCurrentPageState() { + const pageSignature = parsePageSignature(readMarketPageSignature(options.document)); + const nextPageControl = findNextPageControl(options.document); + return { + authorIds: pageSignature.authorIds, + isTerminalPage: isPageControlDisabled(nextPageControl), + pageToken: pageSignature.pageToken, + rowCount: options.readCurrentPageRowCount() + }; + } + function isCurrentPageTerminal() { + return isPageControlDisabled(findNextPageControl(options.document)); + } + } + function parsePageSignature(signature) { + const separatorIndex = signature.indexOf("::"); + if (separatorIndex < 0) { + return { + authorIds: "", + pageToken: signature.trim() + }; + } + return { + authorIds: signature.slice(separatorIndex + 2).trim(), + pageToken: signature.slice(0, separatorIndex).trim() + }; + } + function hasLoadedNextPage(previousPageState, nextPageState) { + if (!nextPageState.authorIds) { + return false; + } + if (nextPageState.pageToken || previousPageState.pageToken) { + return nextPageState.pageToken !== previousPageState.pageToken; + } + return nextPageState.authorIds !== previousPageState.authorIds; + } + function mergeMarketRecord(existingRecord, incomingRecord) { + if (!existingRecord) { + return { + ...incomingRecord, + exportFields: mergeFieldMap(void 0, incomingRecord.exportFields), + rates: mergeFieldMap(void 0, incomingRecord.rates) + }; + } + return { + ...existingRecord, + ...incomingRecord, + authorName: mergeStringValue(existingRecord.authorName, incomingRecord.authorName) ?? "", + exportFields: mergeFieldMap( + existingRecord.exportFields, + incomingRecord.exportFields + ), + failureReason: incomingRecord.failureReason ?? existingRecord.failureReason, + hasDirectRatesSource: existingRecord.hasDirectRatesSource || incomingRecord.hasDirectRatesSource, + location: mergeStringValue(existingRecord.location, incomingRecord.location), + price21To60s: mergeStringValue( + existingRecord.price21To60s, + incomingRecord.price21To60s + ), + rates: mergeFieldMap(existingRecord.rates, incomingRecord.rates), + status: mergeStatus(existingRecord.status, incomingRecord.status) + }; + } + function mergeFieldMap(current, incoming) { + if (!current && !incoming) { + return void 0; + } + const merged = { + ...current ?? {} + }; + Object.entries(incoming ?? {}).forEach(([key, value]) => { + const currentValue = merged[key]; + if (hasTextValue3(value) || !hasTextValue3(currentValue)) { + merged[key] = value; + } + }); + return merged; + } + function mergeStatus(current, incoming) { + const priority = { + failed: 1, + idle: 0, + loading: 2, + missing: -1, + success: 3 + }; + return priority[incoming] >= priority[current] ? incoming : current; + } + function mergeStringValue(current, incoming) { + if (hasTextValue3(incoming) || !hasTextValue3(current)) { + return incoming ?? current; + } + return current; + } + function hasTextValue3(value) { + return typeof value === "string" && value.trim().length > 0; + } + + // src/content/market/plugin-toolbar.ts + var PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style"; + function isPluginToolbarMounted(root, document2) { + const actionRow = findNativeActionRow(document2); + return Boolean(actionRow && root.parentElement === actionRow && !root.hidden); + } + function ensurePluginToolbar(document2, handlers) { + ensurePluginActionButtonTheme(document2); + const existingRoot = document2.querySelector( + "[data-plugin-toolbar='root']" + ); + if (existingRoot) { + ensureToolbarMounted(existingRoot, document2); + return readToolbarDom(existingRoot); + } + const root = document2.createElement("section"); + root.dataset.pluginToolbar = "root"; + applyToolbarRootStyles(root); + const exportRangeSelect = document2.createElement("select"); + exportRangeSelect.dataset.pluginExportRange = "select"; + appendOption(exportRangeSelect, "current", "\u5F53\u524D\u9875"); + appendOption(exportRangeSelect, "first-5", "\u524D5\u9875"); + appendOption(exportRangeSelect, "first-10", "\u524D10\u9875"); + appendOption(exportRangeSelect, "all", "\u5168\u90E8"); + appendOption(exportRangeSelect, "custom", "\u81EA\u5B9A\u4E49"); + exportRangeSelect.value = "first-5"; + const exportCustomPagesInput = document2.createElement("input"); + exportCustomPagesInput.type = "number"; + exportCustomPagesInput.min = "1"; + exportCustomPagesInput.step = "1"; + exportCustomPagesInput.hidden = true; + exportCustomPagesInput.placeholder = "\u9875\u6570"; + exportCustomPagesInput.dataset.pluginExportCustomPages = "input"; + const exportButton = document2.createElement("button"); + exportButton.type = "button"; + exportButton.dataset.pluginExport = "button"; + exportButton.textContent = "\u5BFC\u51FACSV"; + const batchSubmitButton = document2.createElement("button"); + batchSubmitButton.type = "button"; + batchSubmitButton.dataset.pluginBatchSubmit = "button"; + batchSubmitButton.textContent = "\u63D0\u4EA4\u6279\u6B21"; + const exportStatusText = document2.createElement("span"); + exportStatusText.dataset.pluginExportStatus = "text"; + applyStatusStyles(exportStatusText); + root.append( + exportRangeSelect, + exportCustomPagesInput, + exportButton, + batchSubmitButton, + exportStatusText + ); + document2.body.appendChild(root); + applyNativeControlStyles(document2, { + batchSubmitButton, + exportButton, + exportCustomPagesInput, + exportRangeSelect + }); + ensureToolbarMounted(root, document2); + exportButton.addEventListener("click", () => { + void handlers.onExport(); + }); + batchSubmitButton.addEventListener("click", () => { + void handlers.onSubmitBatch(); + }); + exportRangeSelect.addEventListener("change", () => { + syncCustomPagesInputVisibility({ + batchSubmitButton, + exportButton, + exportCustomPagesInput, + exportRangeSelect, + exportStatusText, + root + }); + }); + const toolbarDom = { + batchSubmitButton, + exportButton, + exportCustomPagesInput, + exportRangeSelect, + exportStatusText, + root + }; + syncCustomPagesInputVisibility(toolbarDom); + return toolbarDom; + } + function appendOption(select, value, label) { + const option = select.ownerDocument.createElement("option"); + option.value = value; + option.textContent = label; + select.appendChild(option); + } + function readToolbarDom(root) { + const toolbarDom = { + batchSubmitButton: root.querySelector( + '[data-plugin-batch-submit="button"]' + ), + exportButton: root.querySelector( + '[data-plugin-export="button"]' + ), + exportCustomPagesInput: root.querySelector( + '[data-plugin-export-custom-pages="input"]' + ), + exportRangeSelect: root.querySelector( + '[data-plugin-export-range="select"]' + ), + exportStatusText: root.querySelector( + '[data-plugin-export-status="text"]' + ), + root + }; + syncCustomPagesInputVisibility(toolbarDom); + return toolbarDom; + } + function readToolbarExportTarget(toolbar) { + const scope = toolbar.exportRangeSelect.value; + if (scope === "all") { + return { + target: { + mode: "all" + } + }; + } + if (scope === "current") { + return { + target: { + mode: "count", + pageCount: 1 + } + }; + } + if (scope === "first-5") { + return { + target: { + mode: "count", + pageCount: 5 + } + }; + } + if (scope === "first-10") { + return { + target: { + mode: "count", + pageCount: 10 + } + }; + } + const pageCount = Number(toolbar.exportCustomPagesInput.value); + if (!Number.isInteger(pageCount) || pageCount < 1) { + return { + error: "\u8BF7\u8F93\u5165\u6709\u6548\u9875\u6570" + }; + } + return { + target: { + mode: "count", + pageCount + } + }; + } + function setToolbarBusyState(toolbar, isBusy) { + [ + toolbar.batchSubmitButton, + toolbar.exportButton, + toolbar.exportRangeSelect, + toolbar.exportCustomPagesInput + ].forEach((element) => { + element.disabled = isBusy; + }); + } + function setToolbarExportStatus(toolbar, text) { + toolbar.exportStatusText.textContent = text; + } + function syncCustomPagesInputVisibility(toolbar) { + toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom"; + } + function ensureToolbarMounted(root, document2) { + const actionRow = findNativeActionRow(document2); + if (!actionRow) { + root.hidden = true; + return; + } + const customizeButton = findNativeActionButton(actionRow, "\u81EA\u5B9A\u4E49\u6307\u6807"); + const insertionAnchor = customizeButton ? findDirectChildAnchor(actionRow, customizeButton) : null; + if (insertionAnchor) { + actionRow.insertBefore(root, insertionAnchor); + } else if (root.parentElement !== actionRow) { + actionRow.prepend(root); + } + root.hidden = false; + } + function findNativeActionRow(document2) { + const customizeButton = findNativeActionButton(document2, "\u81EA\u5B9A\u4E49\u6307\u6807"); + const exportButton = findNativeActionButton(document2, "\u5BFC\u51FA"); + const header = findHeaderContainer(customizeButton, exportButton); + const sharedActionRow = customizeButton && exportButton ? findSmallestSharedActionRow(customizeButton, exportButton, header) : null; + if (sharedActionRow) { + return sharedActionRow; + } + const scope = header ?? document2; + const candidates = Array.from( + scope.querySelectorAll(".xt-space.xt-space--medium, .search-content--header") + ).filter( + (element) => element instanceof document2.defaultView.HTMLElement + ); + const rankedCandidates = candidates.filter( + (candidate) => isNativeActionRowCandidate(candidate, customizeButton, exportButton) + ).sort((left, right) => { + const depthDelta = getDepthWithinAncestor(right, header) - getDepthWithinAncestor(left, header); + if (depthDelta !== 0) { + return depthDelta; + } + return normalizeText(left.textContent).length - normalizeText(right.textContent).length; + }); + return rankedCandidates[0] ?? null; + } + function findHeaderContainer(customizeButton, exportButton) { + return customizeButton?.closest(".search-content--header") ?? exportButton?.closest(".search-content--header"); + } + function findSmallestSharedActionRow(customizeButton, exportButton, boundary) { + const exportAncestors = new Set(collectAncestorChain(exportButton, boundary)); + for (const candidate of collectAncestorChain(customizeButton, boundary)) { + if (exportAncestors.has(candidate) && isNativeActionRowCandidate(candidate, customizeButton, exportButton)) { + return candidate; + } + } + return null; + } + function collectAncestorChain(element, boundary) { + const ancestors = []; + let current = element.parentElement; + while (current) { + ancestors.push(current); + if (current === boundary) { + break; + } + current = current.parentElement; + } + return ancestors; + } + function isNativeActionRowCandidate(candidate, customizeButton, exportButton) { + if (customizeButton && !candidate.contains(customizeButton)) { + return false; + } + if (exportButton && !candidate.contains(exportButton)) { + return false; + } + const directChildLabels = Array.from(candidate.children).flatMap((child) => { + const buttons = []; + if (child instanceof candidate.ownerDocument.defaultView.HTMLButtonElement) { + buttons.push(child); + } + buttons.push(...Array.from(child.querySelectorAll("button"))); + return buttons; + }).map((button) => normalizeText(button.textContent)); + return directChildLabels.includes("\u5BFC\u51FA") && (directChildLabels.includes("\u81EA\u5B9A\u4E49\u6307\u6807") || Boolean(customizeButton)); + } + function getDepthWithinAncestor(element, boundary) { + let depth = 0; + let current = element.parentElement; + while (current && current !== boundary) { + depth += 1; + current = current.parentElement; + } + return depth; + } + function findNativeActionButton(root, text) { + const document2 = root instanceof Document ? root : root.ownerDocument; + if (!document2) { + return null; + } + const candidates = Array.from(root.querySelectorAll("button")).filter( + (element) => element instanceof document2.defaultView.HTMLElement + ); + return candidates.find((element) => normalizeText(element.textContent) === text) ?? null; + } + function applyToolbarRootStyles(root) { + root.style.display = "inline-flex"; + root.style.alignItems = "center"; + root.style.columnGap = "8px"; + root.style.flexWrap = "wrap"; + } + function applyNativeControlStyles(document2, controls) { + const primaryButton = findButtonContainingText(document2, "\u53D1\u5E03\u4EFB\u52A1") ?? findButtonContainingText(document2, "+\u53D1\u5E03\u4EFB\u52A1"); + const nativeButton = primaryButton ?? findNativeActionButton(document2, "\u81EA\u5B9A\u4E49\u6307\u6807") ?? findNativeActionButton(document2, "\u5BFC\u51FA"); + if (nativeButton) { + controls.exportButton.className = nativeButton.className; + controls.batchSubmitButton.className = nativeButton.className; + } + [controls.exportButton, controls.batchSubmitButton].forEach((button) => { + applyPrimaryButtonStyles2(button); + button.style.whiteSpace = "nowrap"; + }); + [controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => { + element.style.height = "32px"; + element.style.border = "1px solid #d0d7de"; + element.style.borderRadius = "6px"; + element.style.padding = "0 10px"; + element.style.background = "#fff"; + element.style.color = "#1f2329"; + element.style.boxSizing = "border-box"; + }); + controls.exportRangeSelect.style.minWidth = "104px"; + controls.exportCustomPagesInput.style.width = "72px"; + } + function applyPrimaryButtonStyles2(button) { + button.style.backgroundColor = "#7f1d2d"; + button.style.border = "1px solid #7f1d2d"; + button.style.borderRadius = "8px"; + button.style.color = "#ffffff"; + button.style.height = "32px"; + button.style.padding = "0 15px"; + button.style.boxSizing = "border-box"; + button.style.fontWeight = "600"; + button.style.transition = "background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease"; + } + function applyStatusStyles(statusText) { + statusText.style.color = "#64748b"; + statusText.style.fontSize = "12px"; + statusText.style.lineHeight = "20px"; + statusText.style.marginLeft = "4px"; + statusText.style.whiteSpace = "nowrap"; + } + function ensurePluginActionButtonTheme(document2) { + if (document2.getElementById(PLUGIN_ACTION_BUTTON_STYLE_ID)) { + return; + } + const style = document2.createElement("style"); + style.id = PLUGIN_ACTION_BUTTON_STYLE_ID; + style.textContent = ` + [data-plugin-export="button"]:hover:not(:disabled), + [data-plugin-batch-submit="button"]:hover:not(:disabled) { + background-color: #6d1627 !important; + border-color: #6d1627 !important; + } + + [data-plugin-export="button"]:active:not(:disabled), + [data-plugin-batch-submit="button"]:active:not(:disabled) { + background-color: #58111f !important; + border-color: #58111f !important; + transform: translateY(1px); + } + + [data-plugin-export="button"]:focus-visible, + [data-plugin-batch-submit="button"]:focus-visible { + outline: none !important; + box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important; + } + + [data-plugin-export="button"]:disabled, + [data-plugin-batch-submit="button"]:disabled { + background-color: #c89ca4 !important; + border-color: #c89ca4 !important; + color: rgba(255, 255, 255, 0.95) !important; + cursor: not-allowed !important; + opacity: 1 !important; + transform: none !important; + box-shadow: none !important; + } + `; + document2.head.appendChild(style); + } + function normalizeText(value) { + return value?.replace(/\s+/g, " ").trim() ?? ""; + } + function findButtonContainingText(root, text) { + const document2 = root instanceof Document ? root : root.ownerDocument; + if (!document2) { + return null; + } + const candidates = Array.from(root.querySelectorAll("button")).filter( + (element) => element instanceof document2.defaultView.HTMLElement + ); + return candidates.find((element) => normalizeText(element.textContent).includes(text)) ?? null; + } + function findDirectChildAnchor(ancestor, descendant) { + let current = descendant; + let previous = null; + while (current && current !== ancestor) { + previous = current; + current = current.parentElement; + } + return current === ancestor ? previous : null; + } + + // src/content/market/market-list-request-snapshot.ts + var MARKET_REQUEST_SNAPSHOT_ATTRIBUTE = "data-sces-market-request-snapshot"; + var MARKET_SEARCH_ENDPOINT_PATH = "/gw/api/gsearch/search_for_author_square"; + function readMarketListRequestSnapshot(document2) { + const serializedSnapshot = document2.documentElement.getAttribute( + MARKET_REQUEST_SNAPSHOT_ATTRIBUTE + ); + if (!serializedSnapshot) { + return readMarketListRequestSnapshotFromPageState(document2); + } + try { + const parsedSnapshot = normalizeMarketListRequestSnapshot( + JSON.parse(serializedSnapshot) + ); + if (!parsedSnapshot) { + return readMarketListRequestSnapshotFromPageState(document2); + } + return parsedSnapshot; + } catch { + return readMarketListRequestSnapshotFromPageState(document2); + } + } + function isMarketListRequestSnapshot(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + return typeof candidate.method === "string" && typeof candidate.url === "string" && (!("body" in candidate) || typeof candidate.body === "string") && (!("headers" in candidate) || isStringRecord(candidate.headers)); + } + function normalizeMarketListRequestSnapshot(value) { + if (!value || typeof value !== "object") { + return null; + } + const candidate = value; + const normalizedSnapshot = { + body: typeof candidate.body === "string" ? candidate.body : void 0, + method: typeof candidate.method === "string" ? candidate.method : void 0, + url: typeof candidate.url === "string" ? candidate.url : void 0 + }; + if (candidate.headers && typeof candidate.headers === "object") { + normalizedSnapshot.headers = Object.fromEntries( + Object.entries(candidate.headers).filter( + ([, entry]) => ["string", "number", "boolean"].includes(typeof entry) + ).map(([key, entry]) => [key, String(entry)]) + ); + } + return isMarketListRequestSnapshot(normalizedSnapshot) ? normalizedSnapshot : null; + } + function isStringRecord(value) { + if (!value || typeof value !== "object") { + return false; + } + return Object.values(value).every((entry) => typeof entry === "string"); + } + function readMarketListRequestSnapshotFromPageState(document2) { + const reqParams = findMarketReqParams(document2); + if (!reqParams) { + return null; + } + return { + body: JSON.stringify(reqParams), + method: "POST", + url: buildMarketSearchUrl(document2) + }; + } + function findMarketReqParams(document2) { + const marketRoot = document2.querySelector(".base-author-list"); + const setupState = marketRoot?.__vue__?._setupState; + if (!setupState) { + return null; + } + const queue = Object.values(setupState); + while (queue.length > 0) { + const current = unwrapVueRef3(queue.shift()); + if (!isRecord4(current)) { + continue; + } + const reqParams = unwrapVueRef3(current.reqParams); + if (isRecord4(reqParams)) { + return reqParams; + } + Object.values(current).forEach((value) => { + queue.push(value); + }); + } + return null; + } + function buildMarketSearchUrl(document2) { + if (document2.location?.origin && document2.location.origin !== "null" && document2.location.origin !== "about:blank") { + return document2.location.origin.includes("xingtu.cn") ? MARKET_SEARCH_ENDPOINT_PATH : new URL(MARKET_SEARCH_ENDPOINT_PATH, document2.location.origin).toString(); + } + return MARKET_SEARCH_ENDPOINT_PATH; + } + function unwrapVueRef3(value) { + if (isRecord4(value) && "value" in value) { + return value.value; + } + return value; + } + function isRecord4(value) { + return typeof value === "object" && value !== null; + } + + // src/content/market/silent-export-controller.ts + var PAGE_NUMBER_KEYS2 = [ + "currentPage", + "page", + "pageNo", + "pageNum", + "page_no", + "page_num" + ]; + function createSilentExportController(options) { + const fetchImpl = options.fetchImpl ?? defaultFetch2; + return { + async exportRecords(target) { + const snapshot = readMarketListRequestSnapshot(options.document); + if (!snapshot) { + return null; + } + const baseRequest = createPagedRequest(snapshot); + if (!baseRequest) { + return null; + } + const mergedRecords = /* @__PURE__ */ new Map(); + const maxPageCount = target.mode === "count" ? target.pageCount : 200; + let totalPagesHint; + for (let offset = 0; offset < maxPageCount; offset += 1) { + const pageNumber = baseRequest.initialPage + offset; + options.onProgress?.({ + currentPage: offset + 1, + totalPages: target.mode === "count" ? target.pageCount : totalPagesHint + }); + const payload = await fetchPagePayload(fetchImpl, baseRequest, pageNumber); + const parsedResponse = parseMarketListResponse(payload); + if (!parsedResponse) { + return null; + } + totalPagesHint = parsedResponse.totalPages ?? totalPagesHint; + if (parsedResponse.records.length === 0) { + break; + } + parsedResponse.records.forEach((record) => { + const existingRecord = mergedRecords.get(record.authorId); + mergedRecords.set(record.authorId, mergeMarketRecord2(existingRecord, record)); + }); + if (target.mode === "count" && offset + 1 >= target.pageCount) { + break; + } + if (target.mode === "all") { + if (typeof parsedResponse.totalPages === "number" && pageNumber >= parsedResponse.totalPages) { + break; + } + if (typeof parsedResponse.pageSize === "number" && parsedResponse.records.length < parsedResponse.pageSize) { + break; + } + } + } + return Array.from(mergedRecords.values()); + } + }; + } + function createPagedRequest(snapshot) { + const bodyPage = readPageFromBody(snapshot.body); + if (bodyPage !== null) { + return { + initialPage: bodyPage, + pageSource: "body", + snapshot + }; + } + const urlPage = readPageFromUrl(snapshot.url); + if (urlPage !== null) { + return { + initialPage: urlPage, + pageSource: "url", + snapshot + }; + } + return { + initialPage: 1, + pageSource: "none", + snapshot + }; + } + async function fetchPagePayload(fetchImpl, request, pageNumber) { + const nextUrl = request.pageSource === "url" ? mutateUrlPage(request.snapshot.url, pageNumber) : request.snapshot.url; + const nextBody = mutateBodyPage(request.snapshot.body, pageNumber); + const response = await fetchImpl(nextUrl, { + body: nextBody, + credentials: "include", + headers: filterReplayHeaders(request.snapshot.headers, nextBody), + method: request.snapshot.method + }); + if (!response.ok) { + throw new Error("\u9759\u9ED8\u5BFC\u51FA\u8BF7\u6C42\u5931\u8D25"); + } + return response.json(); + } + function readPageFromUrl(url) { + try { + const parsedUrl = new URL(url); + for (const key of PAGE_NUMBER_KEYS2) { + const value = readNumericString(parsedUrl.searchParams.get(key)); + if (value !== null) { + return value; + } + } + } catch { + return null; + } + return null; + } + function mutateUrlPage(url, pageNumber) { + try { + const parsedUrl = new URL(url); + for (const key of PAGE_NUMBER_KEYS2) { + if (!parsedUrl.searchParams.has(key)) { + continue; + } + parsedUrl.searchParams.set(key, String(pageNumber)); + return parsedUrl.toString(); + } + parsedUrl.searchParams.set("page", String(pageNumber)); + return parsedUrl.toString(); + } catch { + return url; + } + } + function readPageFromBody(body) { + const parsedBody = parseBody(body); + if (!parsedBody) { + return null; + } + return readKnownPaginationNumber(parsedBody, "page"); + } + function mutateBodyPage(body, pageNumber) { + if (!body) { + return body; + } + const trimmedBody = body.trim(); + if (!trimmedBody) { + return body; + } + try { + const parsedJson = JSON.parse(trimmedBody); + if (!replacePageNumberInValue(parsedJson, pageNumber) && isRecord5(parsedJson)) { + parsedJson.page = pageNumber; + } + return JSON.stringify(parsedJson); + } catch { + const searchParams = new URLSearchParams(trimmedBody); + for (const key of PAGE_NUMBER_KEYS2) { + if (!searchParams.has(key)) { + continue; + } + searchParams.set(key, String(pageNumber)); + return searchParams.toString(); + } + searchParams.set("page", String(pageNumber)); + return searchParams.toString(); + } + } + function parseBody(body) { + if (!body) { + return null; + } + const trimmedBody = body.trim(); + if (!trimmedBody) { + return null; + } + try { + const parsedBody = JSON.parse(trimmedBody); + return isRecord5(parsedBody) ? parsedBody : null; + } catch { + const searchParams = new URLSearchParams(trimmedBody); + const payload = {}; + searchParams.forEach((value, key) => { + payload[key] = value; + }); + return payload; + } + } + function replacePageNumberInValue(value, pageNumber) { + if (!isRecord5(value)) { + return false; + } + let replaced = false; + PAGE_NUMBER_KEYS2.forEach((key) => { + if (!(key in value)) { + return; + } + value[key] = pageNumber; + replaced = true; + }); + if (replaced) { + return true; + } + return Object.values(value).some((entry) => replacePageNumberInValue(entry, pageNumber)); + } + function filterReplayHeaders(headers, body) { + const filteredHeaders = Object.fromEntries( + Object.entries(headers ?? {}).filter(([key]) => { + const normalizedKey = key.toLowerCase(); + return normalizedKey !== "content-length" && normalizedKey !== "host"; + }) + ); + if (body) { + if (!hasHeader(filteredHeaders, "accept")) { + filteredHeaders.Accept = "application/json, text/plain, */*"; + } + if (!hasHeader(filteredHeaders, "content-type")) { + filteredHeaders["Content-Type"] = "application/json"; + } + if (!hasHeader(filteredHeaders, "x-login-source")) { + filteredHeaders["x-login-source"] = "1"; + } + if (!hasHeader(filteredHeaders, "agw-js-conv")) { + filteredHeaders["Agw-Js-Conv"] = "str"; + } + } + return Object.keys(filteredHeaders).length > 0 ? filteredHeaders : void 0; + } + function hasHeader(headers, key) { + return Object.keys(headers).some((headerKey) => headerKey.toLowerCase() === key); + } + function readNumericString(value) { + if (!value) { + return null; + } + const parsedValue = Number(value); + return Number.isFinite(parsedValue) ? parsedValue : null; + } + async function defaultFetch2(input, init) { + return fetch(input, init); + } + function isRecord5(value) { + return typeof value === "object" && value !== null; + } + function mergeMarketRecord2(existingRecord, incomingRecord) { + if (!existingRecord) { + return { + ...incomingRecord, + exportFields: mergeFieldMap2(void 0, incomingRecord.exportFields), + rates: mergeFieldMap2(void 0, incomingRecord.rates), + status: incomingRecord.status ?? "idle" + }; + } + return { + ...existingRecord, + ...incomingRecord, + authorName: mergeStringValue2(existingRecord.authorName, incomingRecord.authorName) ?? "", + exportFields: mergeFieldMap2( + existingRecord.exportFields, + incomingRecord.exportFields + ), + failureReason: incomingRecord.failureReason ?? existingRecord.failureReason, + hasDirectRatesSource: existingRecord.hasDirectRatesSource || incomingRecord.hasDirectRatesSource, + location: mergeStringValue2(existingRecord.location, incomingRecord.location), + price21To60s: mergeStringValue2( + existingRecord.price21To60s, + incomingRecord.price21To60s + ), + rates: mergeFieldMap2(existingRecord.rates, incomingRecord.rates), + status: incomingRecord.status ?? existingRecord.status + }; + } + function mergeFieldMap2(current, incoming) { + if (!current && !incoming) { + return void 0; + } + const merged = { + ...current ?? {} + }; + Object.entries(incoming ?? {}).forEach(([key, value]) => { + const currentValue = merged[key]; + if (hasTextValue4(value) || !hasTextValue4(currentValue)) { + merged[key] = value; + } + }); + return merged; + } + function mergeStringValue2(current, incoming) { + return hasTextValue4(incoming) ? incoming : current; + } + function hasTextValue4(value) { + return Boolean(value && value.trim().length > 0); + } + + // src/content/market/result-store.ts + function createMarketResultStore() { + const records = /* @__PURE__ */ new Map(); + return { + getRecord(authorId) { + return records.get(authorId) ?? null; + }, + listRecords() { + return Array.from(records.values()); + }, + setAuthorFailed(authorId, reason) { + const existingRecord = ensureRecord(authorId); + existingRecord.status = "failed"; + existingRecord.failureReason = reason; + }, + setAuthorLoading(authorId) { + const existingRecord = ensureRecord(authorId); + existingRecord.status = "loading"; + delete existingRecord.failureReason; + }, + setBackendMetricsFailed(authorId) { + const existingRecord = ensureRecord(authorId); + existingRecord.backendMetricsStatus = "failed"; + }, + setBackendMetricsLoading(authorId) { + const existingRecord = ensureRecord(authorId); + existingRecord.backendMetricsStatus = "loading"; + }, + setBackendMetricsMissing(authorId) { + const existingRecord = ensureRecord(authorId); + existingRecord.backendMetricsStatus = "missing"; + }, + setBackendMetricsSuccess(authorId, backendMetrics) { + const existingRecord = ensureRecord(authorId); + existingRecord.backendMetricsStatus = "success"; + existingRecord.backendMetrics = { + ...existingRecord.backendMetrics, + ...backendMetrics + }; + }, + setAuthorSuccess(authorId, rates) { + const existingRecord = ensureRecord(authorId); + existingRecord.status = "success"; + existingRecord.rates = { + ...existingRecord.rates, + ...rates + }; + delete existingRecord.failureReason; + }, + upsertMarketRow(row) { + const existingRecord = records.get(row.authorId); + if (existingRecord) { + existingRecord.authorName = mergeStringValue3(existingRecord.authorName, row.authorName) ?? existingRecord.authorName; + existingRecord.location = mergeStringValue3( + existingRecord.location, + row.location + ); + existingRecord.price21To60s = mergeStringValue3( + existingRecord.price21To60s, + row.price21To60s + ); + existingRecord.exportFields = mergeFieldMap3( + existingRecord.exportFields, + row.exportFields + ); + existingRecord.backendMetrics = mergeFieldMap3( + existingRecord.backendMetrics, + row.backendMetrics + ); + existingRecord.hasDirectRatesSource = existingRecord.hasDirectRatesSource || row.hasDirectRatesSource; + existingRecord.rates = mergeFieldMap3(existingRecord.rates, row.rates); + return existingRecord; + } + const nextRecord = { + ...row, + backendMetricsStatus: "idle", + status: "idle" + }; + records.set(row.authorId, nextRecord); + return nextRecord; + } + }; + function ensureRecord(authorId) { + const existingRecord = records.get(authorId); + if (existingRecord) { + return existingRecord; + } + const nextRecord = { + authorId, + authorName: authorId, + backendMetricsStatus: "idle", + status: "idle" + }; + records.set(authorId, nextRecord); + return nextRecord; + } + } + function mergeFieldMap3(current, incoming) { + if (!current && !incoming) { + return void 0; + } + const merged = { + ...current ?? {} + }; + Object.entries(incoming ?? {}).forEach(([key, value]) => { + const currentValue = merged[key]; + if (!hasTextValue5(currentValue)) { + merged[key] = value; + } + }); + return merged; + } + function mergeStringValue3(current, incoming) { + if (!hasTextValue5(current)) { + return incoming ?? current; + } + return current; + } + function hasTextValue5(value) { + return typeof value === "string" && value.trim().length > 0; + } + + // src/shared/auth-messages.ts + function isAuthResponseMessage(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + if (candidate.ok === false) { + return candidate.type === "auth:error" && typeof candidate.error === "string"; + } + if (candidate.ok !== true || typeof candidate.type !== "string") { + return false; + } + if (candidate.type === "auth:ack") { + return true; + } + if (candidate.type === "auth:token") { + return Boolean( + candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string" + ); + } + if (candidate.type === "auth:state") { + return Boolean( + candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean" + ); + } + return false; + } + + // src/shared/backend-metrics-messages.ts + function isBackendMetricsResponseMessage(value) { + if (!value || typeof value !== "object") { + return false; + } + const candidate = value; + if (candidate.ok === false) { + return candidate.type === "backend-metrics:error" && typeof candidate.error === "string"; + } + return Boolean( + candidate.ok === true && candidate.type === "backend-metrics:result" && candidate.value && typeof candidate.value === "object" && Array.isArray(candidate.value.rows) + ); + } + + // src/content/market/index.ts + function createMarketController(options) { + const marketApiClient = createMarketApiClient(); + const sendRuntimeMessage = createRuntimeMessageSender(); + const resultStore = options.resultStore ?? createMarketResultStore(); + const loadAuthorMetrics = options.loadAuthorMetrics ?? marketApiClient.loadAuthorAseInfo; + const searchBackendMetrics = options.searchBackendMetrics ?? (hasRuntimeMessageSender() ? (starIds) => readBackendMetrics(sendRuntimeMessage, starIds) : null); + const buildCsv = options.buildCsv ?? buildMarketCsv; + const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage)); + const mutationObserverFactory = options.mutationObserverFactory ?? ((callback) => new MutationObserver(callback)); + const promptBatchName = options.promptBatchName ?? (() => promptForBatchName(options.document)); + const submitBatch = options.submitBatch ?? ((payload) => readBatchSubmitAck(sendRuntimeMessage, payload)); + let activeProgressLabel = "\u5BFC\u51FA\u4E2D"; + let shouldShowDetailedProgress = true; + const exportRangeController = createExportRangeController({ + document: options.document, + onProgress: ({ currentPage, totalPages }) => { + updateToolbarProgress(currentPage, totalPages); + }, + prepareCurrentPageForExport, + readCurrentPageRecords: () => getVisibleOrderedRecords(), + readCurrentPageRowCount: () => countCurrentPageRows(options.document), + window: options.window + }); + const silentExportController = createSilentExportController({ + document: options.document, + onProgress: ({ currentPage, totalPages }) => { + updateToolbarProgress(currentPage, totalPages); + } + }); + let activeSort; + let isDisposed = false; + let isSyncRunning = false; + let isSyncScheduled = false; + let lastKnownPageSignature = ""; + let needsResync = false; + let scheduledSyncTimeoutId = null; + const selectedAuthorIds = /* @__PURE__ */ new Set(); + let toolbar; + const observer = mutationObserverFactory(() => { + if (isDisposed) { + return; + } + let nextPageSignature = lastKnownPageSignature; + try { + nextPageSignature = readMarketPageSignature(options.document); + } catch { + return; + } + const toolbarNeedsRemount = !toolbar || !isPluginToolbarMounted(toolbar.root, options.document); + const selectionControlsMissing = !options.document.querySelector('[data-market-selection-checkbox="row"]') || !options.document.querySelector('[data-market-selection-checkbox="header"]'); + if (nextPageSignature === lastKnownPageSignature && !toolbarNeedsRemount && !selectionControlsMissing) { + return; + } + scheduleSync(); + }); + const observationRoot = options.document.body ?? options.document.documentElement; + startObserving(); + const toolbarHandlers = { + onExport: async () => { + syncSelectionStateFromDom(); + const exportTarget = readToolbarExportTarget(toolbar); + if (!exportTarget.target) { + setToolbarExportStatus(toolbar, exportTarget.error ?? "\u5BFC\u51FA\u914D\u7F6E\u65E0\u6548"); + return; + } + setToolbarBusyState(toolbar, true); + try { + const records = filterRecordsBySelection( + await exportRecords(exportTarget.target, "\u5BFC\u51FA\u4E2D", { + showDetailedProgress: selectedAuthorIds.size === 0 + }) + ); + options.onCsvReady?.(buildCsv(records)); + setToolbarExportStatus(toolbar, ""); + } catch (error) { + setToolbarExportStatus( + toolbar, + error instanceof Error ? error.message : "\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" + ); + } finally { + setToolbarBusyState(toolbar, false); + } + }, + onSubmitBatch: async () => { + syncSelectionStateFromDom(); + const exportTarget = readToolbarExportTarget(toolbar); + if (!exportTarget.target) { + setToolbarExportStatus(toolbar, exportTarget.error ?? "\u5BFC\u51FA\u914D\u7F6E\u65E0\u6548"); + return; + } + const batchName = await promptBatchName(); + if (batchName === null) { + return; + } + if (!batchName.trim()) { + setToolbarExportStatus(toolbar, "\u8BF7\u8F93\u5165\u6279\u6B21\u540D\u79F0"); + return; + } + setToolbarBusyState(toolbar, true); + try { + const hasSelectedAuthors = selectedAuthorIds.size > 0; + const records = filterRecordsBySelection( + await exportRecords( + exportTarget.target, + hasSelectedAuthors ? "\u63D0\u4EA4\u5DF2\u9009\u8FBE\u4EBA\u4E2D" : "\u63D0\u4EA4\u4E2D", + { + showDetailedProgress: !hasSelectedAuthors + } + ) + ); + const authState = await getAuthState(); + if (!authState.isAuthenticated) { + throw new Error("\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6"); + } + const payload = createBatchPayload({ + authState, + batchName, + createdAt: (/* @__PURE__ */ new Date()).toISOString(), + records + }); + await submitBatch(payload); + setToolbarExportStatus(toolbar, "\u6279\u6B21\u63D0\u4EA4\u6210\u529F"); + } catch (error) { + setToolbarExportStatus( + toolbar, + error instanceof Error ? error.message : "\u6279\u6B21\u63D0\u4EA4\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" + ); + } finally { + setToolbarBusyState(toolbar, false); + } + } + }; + toolbar = ensurePluginToolbar(options.document, toolbarHandlers); + const ready = runSyncCycle(); + return { + dispose() { + isDisposed = true; + observer.disconnect(); + if (scheduledSyncTimeoutId !== null) { + options.window.clearTimeout(scheduledSyncTimeoutId); + scheduledSyncTimeoutId = null; + } + }, + ready + }; + async function hydrateCurrentPage() { + const table = syncMarketTable(options.document); + if (!table) { + return; + } + const pageRows = []; + for (const rowDom of table.rows) { + const rowSnapshot = readRowSnapshot(rowDom); + if (!rowSnapshot.authorId || !hasTextValue6(rowSnapshot.authorName)) { + continue; + } + pageRows.push({ + rowDom, + rowSnapshot + }); + resultStore.upsertMarketRow(rowSnapshot); + } + const pendingRateRows = []; + const rowsNeedingBackendMetrics = []; + pageRows.forEach(({ rowDom, rowSnapshot }) => { + if (rowSnapshot.hasDirectRatesSource) { + resultStore.setAuthorSuccess(rowSnapshot.authorId, rowSnapshot.rates ?? {}); + } + const existingRecord = resultStore.getRecord(rowSnapshot.authorId); + const needsRateFetch = !hasSettledRateState(existingRecord) && !hasCompleteRates(existingRecord?.rates); + const needsBackendMetrics = Boolean(searchBackendMetrics) && !hasSettledBackendMetricsState(existingRecord); + if (needsRateFetch) { + resultStore.setAuthorLoading(rowSnapshot.authorId); + pendingRateRows.push({ + rowDom, + rowSnapshot + }); + } + if (needsBackendMetrics) { + resultStore.setBackendMetricsLoading(rowSnapshot.authorId); + rowsNeedingBackendMetrics.push({ + rowDom, + rowSnapshot + }); + } + if (needsRateFetch || needsBackendMetrics) { + renderMarketRowState(rowDom, { + ...existingRecord ?? { + authorId: rowSnapshot.authorId, + authorName: rowSnapshot.authorName, + status: "idle" + }, + ...rowSnapshot, + backendMetricsStatus: needsBackendMetrics ? "loading" : existingRecord?.backendMetricsStatus, + rates: existingRecord?.rates, + status: needsRateFetch || needsBackendMetrics ? "loading" : existingRecord?.status ?? "idle" + }); + return; + } + if (existingRecord) { + renderMarketRowState(rowDom, existingRecord); + } + }); + await Promise.all([ + hydrateRatesForRows(pendingRateRows), + hydrateBackendMetricsForPage(rowsNeedingBackendMetrics) + ]); + pageRows.forEach(({ rowDom, rowSnapshot }) => { + const record = resultStore.getRecord(rowSnapshot.authorId); + if (!record) { + return; + } + renderMarketRowState(rowDom, record); + }); + } + async function hydrateRatesForRows(pageRows) { + if (pageRows.length === 0) { + return; + } + await Promise.all( + pageRows.map(async ({ rowSnapshot }) => { + const metricsResult = await loadAuthorMetrics(rowSnapshot.authorId); + if (metricsResult.success) { + resultStore.setAuthorSuccess(rowSnapshot.authorId, metricsResult.rates); + return; + } + resultStore.setAuthorFailed(rowSnapshot.authorId, metricsResult.reason); + }) + ); + } + async function hydrateBackendMetricsForPage(pageRows) { + if (!searchBackendMetrics || pageRows.length === 0) { + return; + } + try { + const rows = await searchBackendMetrics( + pageRows.map(({ rowSnapshot }) => rowSnapshot.authorId) + ); + const rowMap = new Map(rows.map((row) => [row.starId, row])); + pageRows.forEach(({ rowSnapshot }) => { + const backendMetrics = rowMap.get(rowSnapshot.authorId); + if (backendMetrics) { + resultStore.setBackendMetricsSuccess(rowSnapshot.authorId, backendMetrics); + } else { + resultStore.setBackendMetricsMissing(rowSnapshot.authorId); + } + }); + } catch { + pageRows.forEach(({ rowSnapshot }) => { + resultStore.setBackendMetricsFailed(rowSnapshot.authorId); + }); + } + } + function applyCurrentView() { + runWithoutMutationSync(() => { + toolbar = ensurePluginToolbar(options.document, toolbarHandlers); + const table = syncMarketTable(options.document); + if (!table) { + return; + } + syncPluginSortHeaders(options.document, { + activeSort, + onToggleSort: toggleSortFromHeader + }); + const records = getVisibleOrderedRecords(table); + applyRowVisibility(table, new Set(records.map((record) => record.authorId))); + applyRowOrder(table, records.map((record) => record.authorId)); + bindSelectionControls(table); + syncMarketSelectionState(table, selectedAuthorIds); + lastKnownPageSignature = readMarketPageSignature(options.document); + }); + } + function bindSelectionControls(table) { + if (!table) { + return; + } + table.rows.forEach((rowDom) => { + rowDom.selectionCheckbox.dataset.marketSelectionAuthorId = rowDom.authorId; + if (rowDom.selectionCheckbox.dataset.marketSelectionBound === "true") { + return; + } + rowDom.selectionCheckbox.dataset.marketSelectionBound = "true"; + rowDom.selectionCheckbox.addEventListener("change", () => { + if (rowDom.selectionCheckbox.checked) { + selectedAuthorIds.add(rowDom.authorId); + } else { + selectedAuthorIds.delete(rowDom.authorId); + } + refreshSelectionControls(); + }); + }); + if (!table.headerSelectionCheckbox) { + return; + } + if (table.headerSelectionCheckbox.dataset.marketSelectionBound === "true") { + return; + } + table.headerSelectionCheckbox.dataset.marketSelectionBound = "true"; + table.headerSelectionCheckbox.addEventListener("change", () => { + const currentTable = syncMarketTable(options.document); + if (!currentTable) { + return; + } + const visibleRows = currentTable.rows.filter( + (rowDom) => rowDom.visibilityTargets.some((target) => !target.hidden) + ); + const scopedRows = visibleRows.length > 0 ? visibleRows : currentTable.rows; + if (table.headerSelectionCheckbox?.checked) { + scopedRows.forEach((rowDom) => { + selectedAuthorIds.add(rowDom.authorId); + }); + } else { + scopedRows.forEach((rowDom) => { + selectedAuthorIds.delete(rowDom.authorId); + }); + } + refreshSelectionControls(); + }); + } + function refreshSelectionControls() { + const table = syncMarketTable(options.document); + if (!table) { + return; + } + bindSelectionControls(table); + syncMarketSelectionState(table, selectedAuthorIds); + } + function syncSelectionStateFromDom() { + const rowSelectionCheckboxes = Array.from( + options.document.querySelectorAll('[data-market-selection-checkbox="row"]') + ).filter( + (element) => element instanceof HTMLInputElement + ); + if (rowSelectionCheckboxes.length === 0) { + return; + } + rowSelectionCheckboxes.forEach((checkbox) => { + const authorId = checkbox.dataset.marketSelectionAuthorId?.trim(); + if (!authorId) { + return; + } + if (checkbox.checked) { + selectedAuthorIds.add(authorId); + } else { + selectedAuthorIds.delete(authorId); + } + }); + refreshSelectionControls(); + } + function toggleSortFromHeader(field) { + activeSort = getNextSortState(activeSort, field); + applyCurrentView(); + } + function getVisibleOrderedRecords(table = syncMarketTable(options.document)) { + const currentPageRecords = readCurrentPageRecords(table); + return applyFilterAndSort(currentPageRecords, { + sort: activeSort + }); + } + async function exportRecords(target, inProgressLabel = "\u5BFC\u51FA\u4E2D", progressOptions = {}) { + activeProgressLabel = inProgressLabel; + shouldShowDetailedProgress = progressOptions.showDetailedProgress ?? true; + setToolbarExportStatus(toolbar, `${inProgressLabel}...`); + if (target.mode === "count" && target.pageCount <= 1) { + await prepareCurrentPageForExport(); + return getVisibleOrderedRecords(); + } + const silentExportRecords = await silentExportController.exportRecords(target); + if (silentExportRecords) { + return hydrateExportRecords( + silentExportRecords.map((record) => ({ + ...record, + status: record.status ?? "idle" + })) + ); + } + return exportRangeController.exportRecords(target); + } + function updateToolbarProgress(currentPage, totalPages) { + if (!shouldShowDetailedProgress) { + setToolbarExportStatus(toolbar, `${activeProgressLabel}...`); + return; + } + setToolbarExportStatus( + toolbar, + totalPages ? `${activeProgressLabel} ${currentPage}/${totalPages} \u9875...` : `${activeProgressLabel} \u7B2C${currentPage}\u9875...` + ); + } + function filterRecordsBySelection(records) { + if (selectedAuthorIds.size === 0) { + return records; + } + const selectedRecords = records.filter( + (record) => selectedAuthorIds.has(record.authorId) + ); + return selectedRecords.length > 0 ? selectedRecords : records; + } + async function prepareCurrentPageForExport() { + await runSyncCycle(); + await harvestCurrentPageForExport(); + await runSyncCycle(); + } + async function hydrateExportRecords(records) { + for (const record of records) { + resultStore.upsertMarketRow(record); + const existingRecord = resultStore.getRecord(record.authorId); + if (existingRecord?.status === "success" && existingRecord.rates) { + continue; + } + if (record.hasDirectRatesSource) { + const directRates = record.rates ?? {}; + const hasAllRates = Boolean(directRates.singleVideoAfterSearchRate) && Boolean(directRates.personalVideoAfterSearchRate); + resultStore.setAuthorSuccess(record.authorId, directRates); + if (hasAllRates) { + continue; + } + } else { + resultStore.setAuthorLoading(record.authorId); + } + const metricsResult = await loadAuthorMetrics(record.authorId); + if (metricsResult.success) { + resultStore.setAuthorSuccess(record.authorId, metricsResult.rates); + } else { + resultStore.setAuthorFailed(record.authorId, metricsResult.reason); + } + } + if (searchBackendMetrics) { + const backendTargetRecords = records.filter((record) => { + const existingRecord = resultStore.getRecord(record.authorId); + return !(existingRecord?.backendMetricsStatus === "success" || existingRecord?.backendMetricsStatus === "missing"); + }); + if (backendTargetRecords.length > 0) { + backendTargetRecords.forEach((record) => { + resultStore.setBackendMetricsLoading(record.authorId); + }); + try { + const backendRows = await searchBackendMetrics( + backendTargetRecords.map((record) => record.authorId) + ); + const backendRowMap = new Map(backendRows.map((row) => [row.starId, row])); + backendTargetRecords.forEach((record) => { + const backendMetrics = backendRowMap.get(record.authorId); + if (backendMetrics) { + resultStore.setBackendMetricsSuccess(record.authorId, backendMetrics); + } else { + resultStore.setBackendMetricsMissing(record.authorId); + } + }); + } catch { + backendTargetRecords.forEach((record) => { + resultStore.setBackendMetricsFailed(record.authorId); + }); + } + } + } + return records.map((record) => toMarketRecord(record)); + } + async function harvestCurrentPageForExport() { + let hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled(); + if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0) { + return; + } + const table = syncMarketTable(options.document); + const scrollContainer = findCurrentPageScrollContainer(table); + if (!scrollContainer) { + return; + } + const originalScrollTop = scrollContainer.scrollTop; + const maxScrollTop = Math.max( + 0, + scrollContainer.scrollHeight - scrollContainer.clientHeight + ); + if (maxScrollTop <= 0) { + return; + } + const step = Math.max(scrollContainer.clientHeight, 240); + for (let nextScrollTop = Math.min(originalScrollTop + step, maxScrollTop); nextScrollTop > originalScrollTop && nextScrollTop <= maxScrollTop; nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)) { + setScrollTop(scrollContainer, nextScrollTop); + hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled(); + if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0) { + break; + } + if (nextScrollTop === maxScrollTop) { + break; + } + } + if (scrollContainer.scrollTop !== originalScrollTop) { + setScrollTop(scrollContainer, originalScrollTop); + } + } + function readCurrentPageRecords(table) { + if (!table) { + return []; + } + return table.rows.map((rowDom) => { + const rowSnapshot = readRowSnapshot(rowDom); + if (!rowSnapshot.authorId || !hasTextValue6(rowSnapshot.authorName)) { + return null; + } + return toMarketRecord(rowSnapshot); + }).filter((record) => record !== null); + } + function toMarketRecord(rowSnapshot) { + const existingRecord = resultStore.getRecord(rowSnapshot.authorId); + const authorName = mergeStringValue4(existingRecord?.authorName, rowSnapshot.authorName) ?? ""; + const location2 = mergeStringValue4(existingRecord?.location, rowSnapshot.location); + const price21To60s = mergeStringValue4( + existingRecord?.price21To60s, + rowSnapshot.price21To60s + ); + return { + ...existingRecord, + ...rowSnapshot, + authorName, + backendMetrics: mergeFieldMap4( + existingRecord?.backendMetrics, + rowSnapshot.backendMetrics + ), + backendMetricsStatus: existingRecord?.backendMetricsStatus ?? "idle", + exportFields: withExportFieldFallbacks( + mergeFieldMap4(existingRecord?.exportFields, rowSnapshot.exportFields), + { + authorName, + location: location2, + price21To60s + } + ), + location: location2, + price21To60s, + rates: mergeFieldMap4(existingRecord?.rates, rowSnapshot.rates), + status: existingRecord?.status ?? "idle" + }; + } + function collectCurrentPageSnapshots() { + readCurrentPageRows(options.document).forEach((rowSnapshot) => { + resultStore.upsertMarketRow(rowSnapshot); + }); + } + function findCurrentPageScrollContainer(table) { + if (!table) { + return null; + } + const candidateScores = /* @__PURE__ */ new Map(); + const candidateRoots = table.rows.map((rowDom) => rowDom.row).filter((row) => row instanceof options.window.HTMLElement); + for (const rootElement of candidateRoots) { + let currentElement = rootElement.parentElement; + let depth = 0; + while (currentElement) { + if (isScrollableContainer(currentElement)) { + const scrollRange = currentElement.scrollHeight - currentElement.clientHeight; + const existingScore = candidateScores.get(currentElement); + if (!existingScore || depth < existingScore.depth) { + candidateScores.set(currentElement, { + depth, + scrollRange + }); + } + } + depth += 1; + currentElement = currentElement.parentElement; + } + } + const rankedCandidates = Array.from(candidateScores.entries()).sort((left, right) => { + const [, leftScore] = left; + const [, rightScore] = right; + if (rightScore.scrollRange !== leftScore.scrollRange) { + return rightScore.scrollRange - leftScore.scrollRange; + } + return leftScore.depth - rightScore.depth; + }); + return rankedCandidates[0]?.[0] ?? null; + } + function isScrollableContainer(element) { + const computedStyle = options.window.getComputedStyle(element); + return /auto|scroll|overlay/.test(computedStyle.overflowY) && element.scrollHeight > element.clientHeight; + } + async function waitForDomSettled() { + await new Promise((resolve) => { + options.window.setTimeout(resolve, 0); + }); + await Promise.resolve(); + } + async function collectCurrentPageSnapshotsUntilSettled() { + let previousFingerprint = ""; + let stablePassCount = 0; + let fingerprintStableSince = 0; + let lastSnapshot = { + blankExportFieldCount: 0, + fingerprint: "", + missingDefaultFieldCount: 0 + }; + for (let attempt = 0; attempt < 16; attempt += 1) { + await waitForDomSettled(); + if (attempt > 0) { + await new Promise((resolve) => { + options.window.setTimeout( + resolve, + previousFingerprint.includes("|missing:0") ? 25 : 50 + ); + }); + await Promise.resolve(); + } + collectCurrentPageSnapshots(); + const hydrationSnapshot = readVisibleRowHydrationSnapshot(); + lastSnapshot = hydrationSnapshot; + if (!hydrationSnapshot.fingerprint) { + stablePassCount = 0; + previousFingerprint = ""; + continue; + } + if (hydrationSnapshot.fingerprint === previousFingerprint) { + stablePassCount += 1; + } else { + previousFingerprint = hydrationSnapshot.fingerprint; + stablePassCount = 1; + fingerprintStableSince = options.window.Date.now(); + } + const stableForMs = options.window.Date.now() - fingerprintStableSince; + if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount === 0 && stablePassCount >= 2) { + return hydrationSnapshot; + } + if (hydrationSnapshot.missingDefaultFieldCount === 0 && hydrationSnapshot.blankExportFieldCount > 0 && stablePassCount >= 2 && stableForMs >= 500) { + return hydrationSnapshot; + } + } + return lastSnapshot; + } + function readVisibleRowHydrationSnapshot() { + const table = syncMarketTable(options.document); + if (!table || table.rows.length === 0) { + return { + blankExportFieldCount: 0, + fingerprint: "", + missingDefaultFieldCount: 0 + }; + } + const parts = table.rows.map((rowDom) => { + const rowSnapshot = readRowSnapshot(rowDom); + const populatedFieldCount = Object.values(rowSnapshot.exportFields ?? {}).filter( + (value) => typeof value === "string" && value.trim().length > 0 + ).length; + const blankExportFieldCount = Object.values(rowSnapshot.exportFields ?? {}).filter( + (value) => typeof value !== "string" || value.trim().length === 0 + ).length; + const hasAuthorField = hasTextValue6(rowSnapshot.exportFields?.["\u8FBE\u4EBA\u4FE1\u606F"]); + const hasRepresentativeVideo = hasTextValue6( + rowSnapshot.exportFields?.["\u4EE3\u8868\u89C6\u9891"] + ); + const hasPriceField = hasTextValue6(rowSnapshot.price21To60s) || hasTextValue6(rowSnapshot.exportFields?.["21-60s\u62A5\u4EF7"]); + const missingDefaultFieldCount = Number(!hasAuthorField) + Number(!hasRepresentativeVideo) + Number(!hasPriceField); + return [ + rowSnapshot.authorId, + populatedFieldCount, + `blank:${blankExportFieldCount}`, + hasAuthorField ? "author" : "no-author", + hasRepresentativeVideo ? "video" : "no-video", + hasPriceField ? "price" : "no-price", + `missing:${missingDefaultFieldCount}` + ].join(":"); + }); + return { + blankExportFieldCount: parts.reduce((count, part) => { + const match = part.match(/:blank:(\d+):/); + return count + Number(match?.[1] ?? 0); + }, 0), + fingerprint: parts.join("|"), + missingDefaultFieldCount: parts.reduce((count, part) => { + const match = part.match(/missing:(\d+)$/); + return count + Number(match?.[1] ?? 0); + }, 0) + }; + } + function scheduleSync() { + if (isDisposed) { + return; + } + if (isSyncRunning) { + needsResync = true; + return; + } + if (isSyncScheduled) { + return; + } + isSyncScheduled = true; + scheduledSyncTimeoutId = options.window.setTimeout(() => { + scheduledSyncTimeoutId = null; + isSyncScheduled = false; + if (isDisposed) { + return; + } + void runSyncCycle(); + }, 0); + } + function runWithoutMutationSync(callback) { + if (isDisposed) { + return; + } + observer.disconnect(); + try { + callback(); + } finally { + startObserving(); + } + } + function startObserving() { + if (isDisposed || !observationRoot) { + return; + } + observer.observe(observationRoot, { + childList: true, + subtree: true + }); + } + async function runSyncCycle() { + if (isDisposed) { + return; + } + if (isSyncRunning) { + needsResync = true; + return; + } + isSyncRunning = true; + try { + toolbar = ensurePluginToolbar(options.document, toolbarHandlers); + await hydrateCurrentPage(); + applyCurrentView(); + lastKnownPageSignature = readMarketPageSignature(options.document); + } finally { + isSyncRunning = false; + if (isDisposed) { + return; + } + if (needsResync) { + needsResync = false; + scheduleSync(); + } + } + } + } + function setScrollTop(element, top) { + element.scrollTop = top; + element.dispatchEvent(new Event("scroll")); + } + function readCurrentPageRows(document2) { + const table = syncMarketTable(document2); + if (!table) { + return []; + } + return table.rows.map((rowDom) => readRowSnapshot(rowDom)).filter( + (row) => Boolean(row.authorId) && hasTextValue6(row.authorName) + ); + } + function countCurrentPageRows(document2) { + const table = syncMarketTable(document2); + if (!table) { + return 0; + } + return table.rows.filter((rowDom) => Boolean(rowDom.authorId)).length; + } + function readRowSnapshot(rowDom) { + return { + authorId: rowDom.authorId, + authorName: rowDom.authorName, + exportFields: rowDom.exportFields, + hasDirectRatesSource: rowDom.hasDirectRatesSource, + location: rowDom.location, + price21To60s: rowDom.price21To60s, + rates: rowDom.rates + }; + } + function getNextSortState(currentSort, field) { + if (!currentSort || currentSort.field !== field) { + return { + direction: "desc", + field + }; + } + if (currentSort.direction === "desc") { + return { + direction: "asc", + field + }; + } + return void 0; + } + function hasCompleteRates(rates) { + return Boolean( + rates?.singleVideoAfterSearchRate && rates?.personalVideoAfterSearchRate + ); + } + function hasSettledRateState(record) { + if (!record) { + return false; + } + return record.status === "failed" || hasCompleteRates(record.rates); + } + function hasSettledBackendMetricsState(record) { + if (!record) { + return false; + } + return record.backendMetricsStatus === "success" || record.backendMetricsStatus === "missing" || record.backendMetricsStatus === "failed"; + } + function mergeFieldMap4(current, incoming) { + if (!current && !incoming) { + return void 0; + } + const merged = { + ...current ?? {} + }; + Object.entries(incoming ?? {}).forEach(([key, value]) => { + const currentValue = merged[key]; + if (hasTextValue6(value) || !hasTextValue6(currentValue)) { + merged[key] = value; + } + }); + return merged; + } + function createRuntimeMessageSender() { + return (message) => Promise.resolve( + globalThis.chrome?.runtime?.sendMessage?.(message) + ); + } + async function readAuthState(sendMessage) { + const response = await sendMessage({ type: "auth:get-state" }); + if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") { + throw new Error("\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6"); + } + return response.value; + } + async function readBatchSubmitAck(sendMessage, payload) { + const response = await sendMessage({ + payload, + type: "batch:submit" + }); + if (response && typeof response === "object" && response.ok === true) { + return response.value; + } + if (response && typeof response === "object" && response.ok === false && typeof response.error === "string") { + throw new Error(response.error); + } + throw new Error("\u6279\u6B21\u63D0\u4EA4\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"); + } + async function readBackendMetrics(sendMessage, starIds) { + const response = await sendMessage({ + type: "backend-metrics:search", + value: { + starIds + } + }); + if (isBackendMetricsResponseMessage(response) && response.ok && response.type === "backend-metrics:result") { + return response.value.rows; + } + throw new Error("\u540E\u7AEF\u6307\u6807\u52A0\u8F7D\u5931\u8D25"); + } + function mergeStringValue4(current, incoming) { + if (hasTextValue6(incoming) || !hasTextValue6(current)) { + return incoming ?? current; + } + return current; + } + function withExportFieldFallbacks(exportFields, fallbackValues) { + if (!exportFields) { + return void 0; + } + const nextExportFields = { + ...exportFields + }; + if ("\u8FBE\u4EBA\u4FE1\u606F" in nextExportFields && !hasTextValue6(nextExportFields["\u8FBE\u4EBA\u4FE1\u606F"]) && hasTextValue6(fallbackValues.authorName)) { + nextExportFields["\u8FBE\u4EBA\u4FE1\u606F"] = fallbackValues.authorName; + } + if ("\u5730\u533A" in nextExportFields && !hasTextValue6(nextExportFields["\u5730\u533A"]) && hasTextValue6(fallbackValues.location)) { + nextExportFields["\u5730\u533A"] = fallbackValues.location; + } + if ("21-60s\u62A5\u4EF7" in nextExportFields && !hasTextValue6(nextExportFields["21-60s\u62A5\u4EF7"]) && hasTextValue6(fallbackValues.price21To60s)) { + nextExportFields["21-60s\u62A5\u4EF7"] = fallbackValues.price21To60s; + } + return nextExportFields; + } + function hasTextValue6(value) { + return typeof value === "string" && value.trim().length > 0; + } + function hasRuntimeMessageSender() { + return Boolean( + globalThis.chrome?.runtime?.sendMessage + ); + } + + // src/content/market/auth-gate.ts + function renderMarketAuthGate(document2, currentWindow) { + const existingGate = document2.querySelector( + '[data-market-auth-gate="root"]' + ); + if (existingGate) { + return existingGate; + } + const root = document2.createElement("section"); + root.dataset.marketAuthGate = "root"; + root.innerHTML = ` + \u8BF7\u5148\u767B\u5F55\u63D2\u4EF6 +

\u6253\u5F00\u6269\u5C55\u5F39\u7A97\u5B8C\u6210\u767B\u5F55\u540E\u5237\u65B0\u672C\u9875

+ + `; + root.querySelector('[data-market-auth-help="button"]')?.addEventListener("click", () => { + currentWindow.alert("\u8BF7\u70B9\u51FB\u6D4F\u89C8\u5668\u5DE5\u5177\u680F\u4E2D\u7684\u6269\u5C55\u56FE\u6807\u5B8C\u6210\u767B\u5F55"); + }); + document2.body.prepend(root); + return root; + } + + // src/content/index.ts + var DOWNLOAD_MARKET_CSV_MESSAGE = "download-market-csv"; + async function bootContentScript(options = {}) { + const currentWindow = options.window ?? window; + const currentDocument = options.document ?? document; + const controllerFactory = options.createMarketController ?? createMarketController; + const sendAuthMessage = options.sendAuthMessage ?? createRuntimeMessageSender2(); + if (!isMarketPage(currentWindow.location.href)) { + return null; + } + installMarketPageBridge(currentDocument); + const authState = await readAuthState2(sendAuthMessage); + if (!authState?.isAuthenticated) { + await waitForBodyReady(currentDocument, currentWindow); + renderMarketAuthGate(currentDocument, currentWindow); + return { + ready: Promise.resolve() + }; + } + await waitForBodyReady(currentDocument, currentWindow); + return controllerFactory({ + document: currentDocument, + onCsvReady: (csv) => { + if (requestCsvDownload(csv)) { + return; + } + downloadCsv(currentDocument, currentWindow, csv); + }, + window: currentWindow + }); + } + async function readAuthState2(sendMessage) { + const response = await sendMessage({ type: "auth:get-state" }); + if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") { + return null; + } + return response.value; + } + function isMarketPage(url) { + const parsedUrl = new URL(url); + const isXingtuHost = parsedUrl.hostname === "xingtu.cn" || parsedUrl.hostname.endsWith(".xingtu.cn"); + return isXingtuHost && parsedUrl.pathname.startsWith("/ad/creator/market"); + } + function bootstrapContentScript() { + const runtime = globalThis.chrome?.runtime; + if (!runtime || typeof window === "undefined" || typeof document === "undefined") { + return; + } + const marker = "__starChartSearchEnhancerContentController"; + const scopedWindow = window; + if (scopedWindow[marker]) { + return; + } + scopedWindow[marker] = true; + void bootContentScript().then((controller) => { + scopedWindow[marker] = controller; + }); + } + bootstrapContentScript(); + function requestCsvDownload(csv) { + const runtime = globalThis.chrome?.runtime; + if (!runtime?.id || typeof runtime.sendMessage !== "function") { + return false; + } + runtime.sendMessage({ + csv, + filename: `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`, + type: DOWNLOAD_MARKET_CSV_MESSAGE + }); + return true; + } + function createRuntimeMessageSender2() { + return async (message) => { + const runtime = globalThis.chrome?.runtime; + if (typeof runtime?.sendMessage !== "function") { + return null; + } + return runtime.sendMessage(message); + }; + } + async function waitForBodyReady(document2, currentWindow) { + if (document2.body) { + return; + } + await new Promise((resolve) => { + const handleReady = () => { + if (document2.body) { + document2.removeEventListener("DOMContentLoaded", handleReady); + resolve(); + } + }; + document2.addEventListener("DOMContentLoaded", handleReady); + currentWindow.setTimeout(handleReady, 0); + }); + } + function downloadCsv(document2, window2, csv) { + const blob = new Blob(["\uFEFF", csv], { + type: "text/csv;charset=utf-8" + }); + const objectUrl = window2.URL.createObjectURL(blob); + const link = document2.createElement("a"); + link.href = objectUrl; + link.download = `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`; + document2.body.appendChild(link); + link.click(); + link.remove(); + window2.URL.revokeObjectURL(objectUrl); + } + function formatTimestampForFilename() { + return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); + } + function installMarketPageBridge(document2) { + if (document2.documentElement.querySelector( + '[data-sces-market-bridge="script"]' + )) { + return; + } + const script = document2.createElement("script"); + script.dataset.scesMarketBridge = "script"; + const runtime = globalThis.chrome?.runtime; + const bridgeUrl = runtime?.getURL?.("content/market-page-bridge.js"); + if (bridgeUrl) { + script.src = bridgeUrl; + } else { + script.textContent = ""; + } + (document2.head ?? document2.documentElement).appendChild(script); + } +})(); diff --git a/dist-release/content/market-page-bridge.js b/dist-release/content/market-page-bridge.js new file mode 100644 index 0000000..f09d29c --- /dev/null +++ b/dist-release/content/market-page-bridge.js @@ -0,0 +1,583 @@ +"use strict"; +(() => { + // src/shared/rate-normalizer.ts + function normalizeFractionRateDisplay(value) { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) { + return null; + } + const percentageValue = numericValue * 100; + return `${trimTrailingZeros(percentageValue.toFixed(6))}%`; + } + function trimTrailingZeros(value) { + return value.replace(/\.?0+$/, ""); + } + + // src/content/market/market-list-request-snapshot.ts + var MARKET_REQUEST_SNAPSHOT_ATTRIBUTE = "data-sces-market-request-snapshot"; + function writeMarketListRequestSnapshot(document2, snapshot) { + document2.documentElement.setAttribute( + MARKET_REQUEST_SNAPSHOT_ATTRIBUTE, + JSON.stringify(snapshot) + ); + } + + // src/content/market/market-list-row.ts + var PAGE_NUMBER_KEYS = [ + "currentPage", + "page", + "pageNo", + "pageNum", + "page_no", + "page_num" + ]; + var PAGE_SIZE_KEYS = [ + "limit", + "pageSize", + "page_size", + "size" + ]; + var TOTAL_COUNT_KEYS = [ + "total", + "totalCount", + "total_count" + ]; + var TOTAL_PAGE_KEYS = [ + "pageCount", + "page_count", + "totalPage", + "totalPages", + "total_page", + "total_pages" + ]; + function mapMarketListRow(row) { + const attributeDatas = readMarketAttributeDatas(row); + const singleVideoAfterSearchRate = normalizeMarketListRate( + readMarketFieldValue(row, attributeDatas, "avg_search_after_view_rate_30d") + ); + return { + authorId: readString(readMarketFieldValue(row, attributeDatas, "star_id")) ?? readString(readMarketFieldValue(row, attributeDatas, "id")) ?? "", + authorName: readString(readMarketFieldValue(row, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(row, attributeDatas, "nick_name")) ?? "", + exportFields: buildMarketExportFieldFallbacks(row, attributeDatas), + hasDirectRatesSource: true, + location: readMarketLocation(row, attributeDatas), + price21To60s: readMarketPrice21To60s(row, attributeDatas), + rates: singleVideoAfterSearchRate ? { + singleVideoAfterSearchRate + } : void 0 + }; + } + function parseMarketListResponse(payload) { + const container = findMarketListContainer(payload); + if (!container) { + return null; + } + const marketList = readMarketListArray(container); + if (!marketList) { + return null; + } + return { + currentPage: readKnownNumberDeep(container, PAGE_NUMBER_KEYS) ?? void 0, + pageSize: readKnownNumberDeep(container, PAGE_SIZE_KEYS) ?? void 0, + records: marketList.map((row) => isRecord(row) ? mapMarketListRow(row) : null).filter( + (row) => row !== null && Boolean(row.authorId || row.authorName) + ), + totalCount: readKnownNumberDeep(container, TOTAL_COUNT_KEYS) ?? void 0, + totalPages: readKnownNumberDeep(container, TOTAL_PAGE_KEYS) ?? void 0 + }; + } + function findMarketListContainer(value) { + const queue = [value]; + while (queue.length > 0) { + const current = queue.shift(); + if (!isRecord(current)) { + continue; + } + if (readMarketListArray(current)) { + return current; + } + Object.values(current).forEach((entry) => { + queue.push(unwrapVueRef(entry)); + }); + } + return null; + } + function readMarketListArray(record) { + const marketList = unwrapVueRef(record.marketList); + if (Array.isArray(marketList)) { + return marketList; + } + const authors = unwrapVueRef(record.authors); + if (Array.isArray(authors)) { + return authors; + } + return null; + } + function unwrapVueRef(value) { + if (isRecord(value) && "value" in value) { + return value.value; + } + return value; + } + function isRecord(value) { + return typeof value === "object" && value !== null; + } + function readMarketAttributeDatas(record) { + return isRecord(record.attribute_datas) ? record.attribute_datas : {}; + } + function readMarketFieldValue(record, attributeDatas, field) { + return record[field] ?? attributeDatas[field]; + } + function readString(value) { + return typeof value === "string" ? value : null; + } + function normalizeMarketListRate(value) { + if (typeof value === "number") { + return normalizeFractionRateDisplay(String(value)); + } + return typeof value === "string" ? normalizeFractionRateDisplay(value) : null; + } + function normalizeExportCellText(value) { + return value?.replace(/\s+/g, " ").trim() ?? ""; + } + function buildMarketExportFieldFallbacks(record, attributeDatas) { + const exportFields = {}; + const authorInfo = buildMarketAuthorInfo(record, attributeDatas); + const authorType = buildMarketAuthorType(record, attributeDatas); + const contentTheme = buildMarketContentTheme(record, attributeDatas); + const connectedUsers = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "link_link_cnt_by_industry")) + ); + const followerCount = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "follower")) + ); + const expectedCpm = formatDecimalDisplay( + readNumericValue(readMarketFieldValue(record, attributeDatas, "prospective_20_60_cpm")) + ); + const expectedPlayCount = formatWanValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "expected_play_num")) + ); + const interactionRate = formatFractionPercent( + readNumericValue(readMarketFieldValue(record, attributeDatas, "interact_rate_within_30d")) + ); + const finishRate = formatFractionPercent( + readNumericValue(readMarketFieldValue(record, attributeDatas, "play_over_rate_within_30d")) + ); + const burstRate = readBurstRateDisplay( + readNumericValue(readMarketFieldValue(record, attributeDatas, "burst_text_rate")) + ); + const price21To60s = readMarketPrice21To60s(record, attributeDatas); + const representativeVideo = readMarketRepresentativeVideo(record, attributeDatas); + assignExportField(exportFields, "\u8FBE\u4EBA\u4FE1\u606F", authorInfo); + assignExportField(exportFields, "\u4EE3\u8868\u89C6\u9891", representativeVideo); + assignExportField(exportFields, "\u8FBE\u4EBA\u7C7B\u578B", authorType); + assignExportField(exportFields, "\u5185\u5BB9\u4E3B\u9898", contentTheme); + assignExportField(exportFields, "\u8FDE\u63A5\u7528\u6237\u6570", connectedUsers); + assignExportField(exportFields, "\u7C89\u4E1D\u6570", followerCount); + assignExportField(exportFields, "\u9884\u671FCPM", expectedCpm); + assignExportField(exportFields, "\u9884\u671F\u64AD\u653E\u91CF", expectedPlayCount); + assignExportField(exportFields, "\u4E92\u52A8\u7387", interactionRate); + assignExportField(exportFields, "\u5B8C\u64AD\u7387", finishRate); + assignExportField(exportFields, "\u7206\u6587\u7387", burstRate); + assignExportField(exportFields, "21-60s\u62A5\u4EF7", price21To60s); + return Object.keys(exportFields).length > 0 ? exportFields : void 0; + } + function assignExportField(exportFields, key, value) { + if (hasTextValue(value)) { + exportFields[key] = value; + } + } + function hasTextValue(value) { + return Boolean(value && value.trim().length > 0); + } + function buildMarketAuthorInfo(record, attributeDatas) { + const nickname = readString(readMarketFieldValue(record, attributeDatas, "nickname")) ?? readString(readMarketFieldValue(record, attributeDatas, "nick_name")) ?? ""; + const parts = [ + nickname, + readMarketGenderLabel(readMarketFieldValue(record, attributeDatas, "gender")), + readString(readMarketFieldValue(record, attributeDatas, "city")) ?? "" + ].filter((value) => Boolean(value)); + return parts.length > 0 ? parts.join(" ") : void 0; + } + function buildMarketAuthorType(record, attributeDatas) { + const tagsRelation = readRecordLike( + readMarketFieldValue(record, attributeDatas, "tags_relation") + ); + if (tagsRelation) { + const primaryTag = Object.keys(tagsRelation)[0]; + if (hasTextValue(primaryTag)) { + return primaryTag; + } + } + return void 0; + } + function buildMarketContentTheme(record, attributeDatas) { + const themes = readStringArray( + readMarketFieldValue(record, attributeDatas, "content_theme_labels_180d") + ); + if (themes.length === 0) { + return void 0; + } + if (themes.length <= 2) { + return themes.join(" "); + } + return `${themes.slice(0, 2).join(" ")} ${themes.length - 2}+`; + } + function readMarketLocation(record, attributeDatas) { + return readString(readMarketFieldValue(record, attributeDatas, "city")) ?? void 0; + } + function readMarketPrice21To60s(record, attributeDatas) { + return formatCurrencyValue( + readNumericValue(readMarketFieldValue(record, attributeDatas, "price_20_60")) + ); + } + function readMarketRepresentativeVideo(record, attributeDatas) { + const items = readArrayLike(readMarketFieldValue(record, attributeDatas, "items")); + for (const item of items) { + if (!isRecord(item)) { + continue; + } + const title = readString(item.title); + if (hasTextValue(title)) { + return normalizeExportCellText(title); + } + } + return void 0; + } + function readMarketGenderLabel(value) { + const rawValue = typeof value === "number" ? String(value) : readString(value); + if (rawValue === "1") { + return "\u7537"; + } + if (rawValue === "2") { + return "\u5973"; + } + return void 0; + } + function readBurstRateDisplay(value) { + if (value === null) { + return void 0; + } + if (value <= 0) { + return "-"; + } + return formatFractionPercent(value); + } + function formatCurrencyValue(value) { + if (value === null) { + return void 0; + } + return `\xA5${value.toLocaleString("en-US", { + maximumFractionDigits: 0 + })}`; + } + function formatWanValue(value) { + if (value === null) { + return void 0; + } + return `${formatDecimalWithGrouping(value / 1e4)}w`; + } + function formatFractionPercent(value) { + if (value === null) { + return void 0; + } + return `${formatDecimalDisplay(value * 100)}%`; + } + function formatDecimalDisplay(value) { + if (value === null) { + return void 0; + } + return value.toLocaleString("en-US", { + maximumFractionDigits: 1, + minimumFractionDigits: 0, + useGrouping: false + }); + } + function formatDecimalWithGrouping(value) { + return value.toLocaleString("en-US", { + maximumFractionDigits: 1, + minimumFractionDigits: 0 + }); + } + function readNumericValue(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string") { + const trimmedValue = value.trim(); + if (!trimmedValue) { + return null; + } + const parsedValue = Number(trimmedValue); + return Number.isFinite(parsedValue) ? parsedValue : null; + } + return null; + } + function readStringArray(value) { + if (Array.isArray(value)) { + return value.filter((item) => typeof item === "string"); + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return Array.isArray(parsedValue) ? parsedValue.filter((item) => typeof item === "string") : []; + } catch { + return []; + } + } + return []; + } + function readArrayLike(value) { + if (Array.isArray(value)) { + return value; + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return Array.isArray(parsedValue) ? parsedValue : []; + } catch { + return []; + } + } + return []; + } + function readRecordLike(value) { + if (isRecord(value)) { + return value; + } + if (typeof value === "string") { + try { + const parsedValue = JSON.parse(value); + return isRecord(parsedValue) ? parsedValue : null; + } catch { + return null; + } + } + return null; + } + function readKnownNumber(record, keys) { + for (const key of keys) { + const value = readNumericValue(record[key]); + if (value !== null) { + return value; + } + } + return void 0; + } + function readKnownNumberDeep(value, keys) { + if (!isRecord(value)) { + return null; + } + const directValue = readKnownNumber(value, keys); + if (typeof directValue === "number") { + return directValue; + } + for (const nestedValue of Object.values(value)) { + const candidate = readKnownNumberDeep(unwrapVueRef(nestedValue), keys); + if (typeof candidate === "number") { + return candidate; + } + } + return null; + } + + // src/content/market/page-bridge.ts + var BRIDGE_MARKER = "__SCES_MARKET_PAGE_BRIDGE_INSTALLED__"; + var MARKET_SEARCH_REQUEST_PATH = "/gw/api/gsearch/search_for_author_square"; + var SERIALIZED_MARKET_ROWS_ATTRIBUTE = "data-sces-market-rows"; + installMarketPageBridge(); + function installMarketPageBridge() { + if (window[BRIDGE_MARKER]) { + syncSerializedMarketRows(); + return; + } + window[BRIDGE_MARKER] = true; + installMarketRequestSnapshotBridge(); + syncSerializedMarketRows(); + const observer = new MutationObserver(() => { + syncSerializedMarketRows(); + }); + observer.observe(document.documentElement, { + childList: true, + subtree: true + }); + window.setInterval(() => { + syncSerializedMarketRows(); + }, 1e3); + } + function installMarketRequestSnapshotBridge() { + installFetchSnapshotBridge(); + installXmlHttpRequestSnapshotBridge(); + } + function syncSerializedMarketRows() { + if (typeof document === "undefined") { + return; + } + const nextSerializedRows = JSON.stringify(readSerializedMarketRows()); + if (document.documentElement.getAttribute(SERIALIZED_MARKET_ROWS_ATTRIBUTE) !== nextSerializedRows) { + document.documentElement.setAttribute( + SERIALIZED_MARKET_ROWS_ATTRIBUTE, + nextSerializedRows + ); + } + } + function installFetchSnapshotBridge() { + if (typeof window.fetch !== "function") { + return; + } + const originalFetch = window.fetch.bind(window); + window.fetch = async (input, init) => { + const requestSnapshot = readFetchSnapshot(input, init); + const response = await originalFetch(input, init); + if (requestSnapshot) { + const clonedResponse = response.clone(); + void captureMarketSnapshotFromResponse( + requestSnapshot, + () => clonedResponse.json() + ); + } + return response; + }; + } + function installXmlHttpRequestSnapshotBridge() { + const OriginalXmlHttpRequest = window.XMLHttpRequest; + if (!OriginalXmlHttpRequest) { + return; + } + const originalOpen = OriginalXmlHttpRequest.prototype.open; + const originalSend = OriginalXmlHttpRequest.prototype.send; + const originalSetRequestHeader = OriginalXmlHttpRequest.prototype.setRequestHeader; + OriginalXmlHttpRequest.prototype.open = function(method, url, ...rest) { + this.__scesMarketSnapshot = { + headers: {}, + method, + url: String(url) + }; + return originalOpen.call(this, method, url, ...rest); + }; + OriginalXmlHttpRequest.prototype.setRequestHeader = function(name, value) { + this.__scesMarketSnapshot?.headers && (this.__scesMarketSnapshot.headers[name] = value); + return originalSetRequestHeader.call(this, name, value); + }; + OriginalXmlHttpRequest.prototype.send = function(body) { + const snapshotState = this.__scesMarketSnapshot; + if (snapshotState) { + snapshotState.body = typeof body === "string" ? body : void 0; + this.addEventListener("load", () => { + if (this.status < 200 || this.status >= 300 || typeof this.responseText !== "string") { + return; + } + void captureMarketSnapshotFromResponse( + snapshotState, + async () => JSON.parse(this.responseText) + ); + }); + } + return originalSend.call(this, body); + }; + } + async function captureMarketSnapshotFromResponse(snapshot, readPayload) { + if (!isMarketSearchRequest(snapshot.url)) { + return; + } + try { + const payload = await readPayload(); + if (!parseMarketListResponse(payload)) { + return; + } + writeMarketListRequestSnapshot(document, { + body: snapshot.body, + headers: snapshot.headers, + method: snapshot.method, + url: snapshot.url + }); + } catch { + } + } + function readSerializedMarketRows() { + const marketList = readMarketList(); + return marketList.map((row) => { + const attributeDatas = isRecord2(row.attribute_datas) ? row.attribute_datas : {}; + const singleVideoAfterSearchRate = readNormalizedFractionRate( + attributeDatas.avg_search_after_view_rate_30d + ); + return { + authorId: readString2(row.star_id) ?? readString2(attributeDatas.id) ?? "", + authorName: readString2(attributeDatas.nickname) ?? readString2(row.nick_name) ?? "", + singleVideoAfterSearchRate + }; + }).filter((row) => Boolean(row.authorId || row.authorName)); + } + function readFetchSnapshot(input, init) { + const request = input instanceof Request ? input : null; + const method = init?.method ?? request?.method ?? "GET"; + const url = request?.url ?? String(input); + const body = typeof init?.body === "string" ? init.body : typeof request?.bodyUsed === "boolean" && request.bodyUsed ? void 0 : void 0; + const headers = serializeHeaders(init?.headers ?? request?.headers); + return { + body, + headers, + method, + url + }; + } + function serializeHeaders(headers) { + if (!headers) { + return void 0; + } + if (headers instanceof Headers) { + return Object.fromEntries(headers.entries()); + } + if (Array.isArray(headers)) { + return Object.fromEntries(headers); + } + return Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key, String(value)]) + ); + } + function readMarketList() { + if (typeof document === "undefined") { + return []; + } + const marketRoot = document.querySelector(".base-author-list"); + const setupState = marketRoot?.__vue__?._setupState; + if (!setupState) { + return []; + } + for (const value of Object.values(setupState)) { + const candidate = unwrapVueRef2(value); + if (Array.isArray(candidate) && looksLikeMarketList(candidate)) { + return candidate; + } + if (!isRecord2(candidate) || !Array.isArray(candidate.marketList)) { + continue; + } + if (looksLikeMarketList(candidate.marketList)) { + return candidate.marketList; + } + } + return []; + } + function isMarketSearchRequest(url) { + return url === MARKET_SEARCH_REQUEST_PATH || url.startsWith(`${MARKET_SEARCH_REQUEST_PATH}?`) || url.includes(`${MARKET_SEARCH_REQUEST_PATH}?`) || url.endsWith(MARKET_SEARCH_REQUEST_PATH); + } + function looksLikeMarketList(value) { + const firstRow = value[0]; + return isRecord2(firstRow) && ("star_id" in firstRow || "attribute_datas" in firstRow); + } + function unwrapVueRef2(value) { + if (isRecord2(value) && "value" in value) { + return value.value; + } + return value; + } + function isRecord2(value) { + return typeof value === "object" && value !== null; + } + function readString2(value) { + return typeof value === "string" ? value : null; + } + function readNormalizedFractionRate(value) { + return typeof value === "string" ? normalizeFractionRateDisplay(value) ?? void 0 : void 0; + } +})(); diff --git a/dist-release/manifest.json b/dist-release/manifest.json new file mode 100644 index 0000000..9df1c42 --- /dev/null +++ b/dist-release/manifest.json @@ -0,0 +1,58 @@ +{ + "action": { + "default_icon": { + "16": "assets/icons/icon-16.png", + "32": "assets/icons/icon-32.png" + }, + "default_popup": "popup/index.html" + }, + "background": { + "service_worker": "background/index.js" + }, + "content_scripts": [ + { + "js": [ + "content/index.js" + ], + "matches": [ + "https://xingtu.cn/ad/creator/market*", + "https://*.xingtu.cn/ad/creator/market*" + ], + "run_at": "document_start" + } + ], + "description": "Bootstraps the Xingtu creator market content script.", + "icons": { + "16": "assets/icons/icon-16.png", + "32": "assets/icons/icon-32.png", + "48": "assets/icons/icon-48.png", + "128": "assets/icons/icon-128.png" + }, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0CaZJxcX97TbRXCR08L10t9EZFV31+wPnUgDf21j2f0qYaWdblzWXfVkeU9jGb2Hr2Etpp7F/XuBa6pcipUXkzMMBkJ42KOkciAwbuzTBoAtGB8o9aoWigtax+gGfSz+T3BjqxKBJtXqeqbIAKCDIlxRKIrY+KcY1Z+mD5BKcBHKsUDQPlHsrjc1g0wIBD5doz9LoOk1Wso6gK5cSeOp9lw5YHcu4TImR4yqxGiL6pZwnpciuX/g7qjWBZXn5gf0YBlDsBDDTt5upbP3NguUKgO2qA9M77LyeUwXl3aqbIxYi/VwsQ2t5w9PGWtnOUQQDWUcEg/9dfTb89esZXKATwIDAQAB", + "manifest_version": 3, + "name": "Star Chart Search Enhancer", + "permissions": [ + "downloads", + "identity", + "storage" + ], + "version": "0.2.0421.2", + "web_accessible_resources": [ + { + "matches": [ + "https://xingtu.cn/*", + "https://*.xingtu.cn/*" + ], + "resources": [ + "content/market-page-bridge.js" + ] + } + ], + "host_permissions": [ + "https://xingtu.cn/ad/creator/market*", + "https://*.xingtu.cn/ad/creator/market*", + "https://login-api.intelligrow.cn/*", + "https://talent-search.intelligrow.cn/*", + "http://192.168.31.21:8083/*" + ] +} diff --git a/dist-release/popup/index.html b/dist-release/popup/index.html new file mode 100644 index 0000000..6e17f31 --- /dev/null +++ b/dist-release/popup/index.html @@ -0,0 +1,12 @@ + + + + + + Star Chart Search Enhancer + + +
+ + + diff --git a/dist-release/popup/index.js b/dist-release/popup/index.js new file mode 100644 index 0000000..2003be9 --- /dev/null +++ b/dist-release/popup/index.js @@ -0,0 +1,219 @@ +"use strict"; +(() => { + // src/popup/view.ts + function renderLoggedOut(root, error) { + root.innerHTML = ` +
+

Star Chart Search Enhancer

+

\u767B\u5F55\u540E\u624D\u80FD\u4F7F\u7528\u661F\u56FE\u589E\u5F3A\u529F\u80FD

+ ${error ? `

${error}

` : ""} + +
+ `; + } + function renderLoggedIn(root, authState) { + const userInfo = authState.userInfo; + root.innerHTML = ` +
+

Star Chart Search Enhancer

+

\u5DF2\u767B\u5F55

+

${userInfo?.name ?? userInfo?.username ?? "\u672A\u77E5\u7528\u6237"}

+

${userInfo?.email ?? ""}

+ +
+ `; + } + function renderDevPanel(root, authState) { + const panel = root.ownerDocument.createElement("section"); + panel.dataset.popupDevPanel = "root"; + panel.innerHTML = ` +

dev auth panel

+

resource: ${authState.resource ?? ""}

+

scopes: ${(authState.scopes ?? []).join(", ")}

+

token: ${authState.tokenAvailable ? "available" : "missing"}

+

expires: ${authState.accessTokenExpiresAt ?? "unknown"}

+

error: ${authState.lastError ?? ""}

+ +

+  `;
+    root.appendChild(panel);
+  }
+  function setProtectedApiResult(root, value) {
+    const output = root.querySelector(
+      '[data-popup-protected-api-result="output"]'
+    );
+    if (!output) {
+      return;
+    }
+    output.textContent = value;
+  }
+
+  // src/shared/auth-config.ts
+  var defaultAuthConfig = {
+    apiResource: "https://talent-search.intelligrow.cn",
+    appId: "i4jkllbvih0554r4n0fd3",
+    enableDevAuthPanel: false,
+    logtoEndpoint: "https://login-api.intelligrow.cn",
+    scopes: ["openid", "profile", "offline_access", "talent-search:read"]
+  };
+  function readAuthConfig(overrides = {}) {
+    const nextConfig = {
+      ...defaultAuthConfig,
+      ...overrides
+    };
+    if (!nextConfig.logtoEndpoint.trim()) {
+      throw new Error("auth config logtoEndpoint is required");
+    }
+    if (!nextConfig.appId.trim()) {
+      throw new Error("auth config appId is required");
+    }
+    if (!nextConfig.apiResource.trim()) {
+      throw new Error("auth config apiResource is required");
+    }
+    return nextConfig;
+  }
+
+  // src/shared/auth-messages.ts
+  function isAuthResponseMessage(value) {
+    if (!value || typeof value !== "object") {
+      return false;
+    }
+    const candidate = value;
+    if (candidate.ok === false) {
+      return candidate.type === "auth:error" && typeof candidate.error === "string";
+    }
+    if (candidate.ok !== true || typeof candidate.type !== "string") {
+      return false;
+    }
+    if (candidate.type === "auth:ack") {
+      return true;
+    }
+    if (candidate.type === "auth:token") {
+      return Boolean(
+        candidate.value && typeof candidate.value === "object" && typeof candidate.value.accessToken === "string"
+      );
+    }
+    if (candidate.type === "auth:state") {
+      return Boolean(
+        candidate.value && typeof candidate.value === "object" && typeof candidate.value.isAuthenticated === "boolean"
+      );
+    }
+    return false;
+  }
+
+  // src/shared/protected-api-client.ts
+  function createProtectedApiClient(options) {
+    const fetchImpl = options.fetchImpl ?? fetch;
+    return {
+      async loadProtectedMockData() {
+        const token = await readAccessToken(options.sendMessage);
+        const response = await fetchImpl(
+          new URL("/api/mock/protected", options.baseUrl).toString(),
+          {
+            headers: {
+              Authorization: `Bearer ${token}`
+            },
+            method: "GET"
+          }
+        );
+        if (response.status === 401 || response.status === 403) {
+          throw new Error("protected api unauthorized");
+        }
+        if (!response.ok) {
+          throw new Error(`protected api request failed: ${response.status}`);
+        }
+        return response.json();
+      }
+    };
+  }
+  async function readAccessToken(sendMessage) {
+    const response = await sendMessage({ type: "auth:get-access-token" });
+    if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:token" || !response.value.accessToken.trim()) {
+      throw new Error("protected api token unavailable");
+    }
+    return response.value.accessToken;
+  }
+
+  // src/popup/index.ts
+  async function bootPopup(options = {}) {
+    const currentDocument = options.document ?? document;
+    const popupConfig = readAuthConfig(options.config);
+    const root = currentDocument.querySelector("#app");
+    const HTMLElementCtor = currentDocument.defaultView?.HTMLElement;
+    if (!root || HTMLElementCtor && !(root instanceof HTMLElementCtor)) {
+      throw new Error("popup root #app is required");
+    }
+    const sendMessage = options.sendMessage ?? ((message) => Promise.resolve(
+      globalThis.chrome?.runtime?.sendMessage?.(message)
+    ));
+    const fetchProtectedApi = options.fetchProtectedApi ?? createProtectedApiClient({
+      baseUrl: "http://127.0.0.1:4319",
+      sendMessage
+    }).loadProtectedMockData;
+    await renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi);
+  }
+  async function renderCurrentAuthState(root, popupConfig, sendMessage, fetchProtectedApi) {
+    const response = await sendMessage({ type: "auth:get-state" });
+    if (!isAuthResponseMessage(response) || !response.ok || response.type !== "auth:state") {
+      renderLoggedOut(root, "\u8BA4\u8BC1\u72B6\u6001\u8BFB\u53D6\u5931\u8D25");
+      return;
+    }
+    if (!response.value.isAuthenticated) {
+      renderLoggedOut(root, response.value.lastError);
+      root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
+        void runAuthAction(root, popupConfig, sendMessage, {
+          actionMessage: { type: "auth:sign-in" },
+          fetchProtectedApi
+        });
+      });
+      return;
+    }
+    renderLoggedIn(root, response.value);
+    root.querySelector('[data-popup-sign-out="button"]')?.addEventListener("click", () => {
+      void runAuthAction(root, popupConfig, sendMessage, {
+        actionMessage: { type: "auth:sign-out" },
+        fetchProtectedApi
+      });
+    });
+    if (popupConfig.enableDevAuthPanel) {
+      renderDevPanel(root, response.value);
+      root.querySelector('[data-popup-test-protected-api="button"]')?.addEventListener("click", () => {
+        void runProtectedApiProbe(root, fetchProtectedApi);
+      });
+    }
+  }
+  async function runAuthAction(root, popupConfig, sendMessage, options) {
+    const response = await sendMessage(options.actionMessage);
+    if (isActionError(response)) {
+      renderLoggedOut(root, response.error);
+      root.querySelector('[data-popup-sign-in="button"]')?.addEventListener("click", () => {
+        void runAuthAction(root, popupConfig, sendMessage, options);
+      });
+      return;
+    }
+    await renderCurrentAuthState(
+      root,
+      popupConfig,
+      sendMessage,
+      options.fetchProtectedApi
+    );
+  }
+  function isActionError(response) {
+    return isAuthResponseMessage(response) && !response.ok && response.type === "auth:error";
+  }
+  async function runProtectedApiProbe(root, fetchProtectedApi) {
+    setProtectedApiResult(root, "\u8BF7\u6C42\u4E2D...");
+    try {
+      const result = await fetchProtectedApi();
+      setProtectedApiResult(root, JSON.stringify(result, null, 2));
+    } catch (error) {
+      setProtectedApiResult(
+        root,
+        error instanceof Error ? error.message : String(error)
+      );
+    }
+  }
+  if (typeof document !== "undefined") {
+    void bootPopup();
+  }
+})();
diff --git a/docs/【超简单版】插件安装使用指南.md b/docs/【超简单版】插件安装使用指南.md
new file mode 100644
index 0000000..52634a3
--- /dev/null
+++ b/docs/【超简单版】插件安装使用指南.md
@@ -0,0 +1,132 @@
+# 🌟 星图增强插件 - 超简单使用指南
+
+> 适合:完全没用过插件的新手 | 阅读时间:3分钟
+
+---
+
+## 📦 第一步:拿到压缩包
+
+你会从同事那里收到一个文件:
+
+**`star-chart-search-enhancer-internal.zip`**
+
+把它保存在桌面上,不要删掉。
+
+---
+
+## 📂 第二步:解压(右键就行)
+
+1. 在桌面上找到这个压缩包
+2. **右键** → 选择"解压到当前文件夹"(或"Extract Here")
+3. 会多出一个文件夹,名字类似 `star-chart-search-enhancer-internal`
+
+⚠️ **重要**:这个文件夹要一直放在桌面,不要删、不要改名
+
+---
+
+## 🔧 第三步:安装到 Chrome(只需做一次)
+
+1. 打开 **Chrome 浏览器**
+2. 在地址栏输入:
+   ```
+   chrome://extensions
+   ```
+   然后按回车
+
+3. 右上角找到 **"开发者模式"** → 打开开关(点一下变蓝色)
+
+4. 点击左上角出现的 **"加载已解压的扩展程序"**
+
+5. 选择刚才解压出来的那个文件夹
+
+6. 看到绿色的插件卡片出现,就装好了!
+
+✅ **检查**:点击"详情",确认 ID 是 `pkjopdibdnomhogjheclhnknmejccffg`
+
+---
+
+## 🔑 第四步:登录(只用登录一次)
+
+1. 点击 Chrome 右上角的 **拼图图标** 🧩
+2. 找到 **Star Chart Search Enhancer**
+3. 点击 **图钉** 📌 把它固定到工具栏
+4. 点击插件图标,然后点 **"登录"**
+5. 按提示完成公司账号登录
+
+---
+
+## 🚀 第五步:开始使用
+
+### 打开星图页面
+
+访问:
+```
+https://xingtu.cn/ad/creator/market
+```
+
+等待页面加载,你会看到页面上多了一排新按钮。
+
+---
+
+## 📝 主要功能
+
+### 1️⃣ 导出 Excel 表格
+
+- 勾选你想导出的达人(不勾就选全部)
+- 选择范围:当前页 / 前5页 / 全部
+- 点击 **"导出CSV"**
+- 文件自动下载到电脑的"下载"文件夹
+
+### 2️⃣ 提交批次
+
+- 勾选你想提交的达人
+- 点击 **"提交批次"**
+- 输入批次名称(例如:`5月母婴达人第一批`)
+- 点击确认
+
+---
+
+## 🔄 如何更新插件
+
+收到新版本压缩包时:
+
+1. 删掉桌面上的旧文件夹
+2. 解压新的压缩包
+3. 打开 `chrome://extensions`
+4. 找到插件卡片,点击 **"重新加载"** 🔄
+
+---
+
+## ❓ 常见问题
+
+### Q: 页面没有多出按钮?
+A: 先点击插件图标确认已登录,然后刷新页面(按 F5)
+
+### Q: 提示登录失败?
+A: 关闭弹窗再试一次,或检查网络连接
+
+### Q: 导出没反应?
+A: 检查浏览器的下载列表,文件可能已经下好了
+
+### Q: 不小心把文件夹删了?
+A: 重新解压压缩包,然后到 `chrome://extensions` 点"重新加载"
+
+---
+
+## ✅ 每日使用 checklist
+
+- [ ] 打开 Chrome,确认插件图标在
+- [ ] 点击图标,确认显示"已登录"
+- [ ] 打开星图页面
+- [ ] 正常使用导出/提交功能
+
+---
+
+## 🆘 还是不行?
+
+把下面信息发给同事:
+1. 你在哪一步卡住了
+2. 页面截图
+3. 扩展 ID(从 chrome://extensions 里看)
+
+**记住正确的 ID:`**pkjopdibdnomhogjheclhnknmejccffg**`
diff --git a/release/star-chart-search-enhancer-chrome-web-store.zip b/release/star-chart-search-enhancer-chrome-web-store.zip
new file mode 100644
index 0000000..6397d89
Binary files /dev/null and b/release/star-chart-search-enhancer-chrome-web-store.zip differ
diff --git a/release/star-chart-search-enhancer-internal.zip b/release/star-chart-search-enhancer-internal.zip
new file mode 100644
index 0000000..f4df8ec
Binary files /dev/null and b/release/star-chart-search-enhancer-internal.zip differ