diff --git a/dist-release/background/index.js b/dist-release/background/index.js index 89db7fb..df638d0 100644 --- a/dist-release/background/index.js +++ b/dist-release/background/index.js @@ -26,9 +26,10 @@ } // src/background/auth/state.ts - function createLoggedOutAuthState(config) { + function createLoggedOutAuthState(config, lastError) { return { isAuthenticated: false, + lastError: lastError ?? null, resource: config?.apiResource ?? null }; } @@ -64,6 +65,14 @@ if (!isAuthenticated) { return createLoggedOutAuthState(config); } + try { + await options.authClient.getAccessToken(config.apiResource); + } catch (error) { + return createLoggedOutAuthState( + config, + error instanceof Error ? error.message : String(error) + ); + } const claims = await options.authClient.getIdTokenClaims(); return createLoggedInAuthState(claims, config); }, diff --git a/dist-release/content/index.js b/dist-release/content/index.js index b826c8b..194a655 100644 --- a/dist-release/content/index.js +++ b/dist-release/content/index.js @@ -117,39 +117,42 @@ ]; var BACKEND_METRIC_COLUMNS = [ { - header: "\u770B\u540E\u641C\u7387", + header: "\u79D2\u601Dapi-\u770B\u540E\u641C\u7387", readValue: (record) => record.backendMetrics?.afterViewSearchRate ?? "" }, { - header: "\u770B\u540E\u641C\u6570", + header: "\u79D2\u601Dapi-\u770B\u540E\u641C\u6570", readValue: (record) => record.backendMetrics?.afterViewSearchCount ?? "" }, { - header: "\u65B0\u589EA3\u6570", + header: "\u79D2\u601Dapi-\u65B0\u589EA3\u6570", readValue: (record) => record.backendMetrics?.a3IncreaseCount ?? "" }, { - header: "\u65B0\u589EA3\u7387", + header: "\u79D2\u601Dapi-\u65B0\u589EA3\u7387", readValue: (record) => record.backendMetrics?.newA3Rate ?? "" }, { - header: "CPA3", + header: "\u79D2\u601Dapi-CPA3", readValue: (record) => record.backendMetrics?.cpa3 ?? "" }, { - header: "cp_search", + header: "\u79D2\u601Dapi-cp_search", readValue: (record) => record.backendMetrics?.cpSearch ?? "" } ]; function buildMarketCsv(records) { - const baseColumns = buildBaseColumns(records); - const csvColumns = [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS]; + const csvColumns = buildMarketCsvColumns(records); 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 buildMarketCsvColumns(records) { + const baseColumns = buildBaseColumns(records); + return [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS]; + } function buildBaseColumns(records) { const orderedHeaders = []; const seenHeaders = /* @__PURE__ */ new Set(); @@ -172,6 +175,758 @@ })); } + // src/content/market/audience-profile-csv.ts + var PROFILE_LAYOUTS = [ + { includeGender: true, kind: "audience", label: "\u89C2\u4F17\u753B\u50CF" }, + { includeGender: true, kind: "fans", label: "\u7C89\u4E1D\u753B\u50CF" }, + { includeGender: false, kind: "longtimeFans", label: "\u94C1\u7C89\u753B\u50CF" } + ]; + var GENDER_LABELS = ["\u7537\u6027", "\u5973\u6027"]; + var AGE_LABELS = ["18-23", "24-30", "31-40", "41-50", "50+"]; + var CITY_TIER_LABELS = [ + "\u4E00\u7EBF\u57CE\u5E02", + "\u4E8C\u7EBF\u57CE\u5E02", + "\u4E09\u7EBF\u57CE\u5E02", + "\u56DB\u7EBF\u57CE\u5E02", + "\u4E94\u7EBF\u57CE\u5E02" + ]; + var CROWD_LABELS = [ + "\u7CBE\u81F4\u5988\u5988", + "\u90FD\u5E02\u94F6\u53D1", + "\u65B0\u9510\u767D\u9886", + "\u8D44\u6DF1\u4E2D\u4EA7", + "\u90FD\u5E02\u84DD\u9886", + "Z\u4E16\u4EE3", + "\u5C0F\u9547\u4E2D\u8001\u5E74", + "\u5C0F\u9547\u9752\u5E74" + ]; + var BUSINESS_VIDEO_LAYOUTS = [ + { key: "personalVideo", label: "\u4E2A\u4EBA\u89C6\u9891" }, + { key: "xingtuVideo", label: "\u661F\u56FE\u89C6\u9891" } + ]; + var BUSINESS_VIDEO_METRIC_LAYOUTS = [ + { key: "medianPlay", label: "\u64AD\u653E\u91CF\u4E2D\u4F4D\u6570" }, + { key: "finishRate", label: "\u5B8C\u64AD\u7387" }, + { key: "interactionRate", label: "\u4E92\u52A8\u7387" }, + { key: "publishedItems", label: "\u53D1\u5E03\u4F5C\u54C1" }, + { key: "averageDuration", label: "\u5E73\u5747\u65F6\u957F" }, + { key: "averageLike", label: "\u5E73\u5747\u70B9\u8D5E" }, + { key: "averageComment", label: "\u5E73\u5747\u8BC4\u8BBA" }, + { key: "averageShare", label: "\u5E73\u5747\u8F6C\u53D1" } + ]; + var BUSINESS_VIDEO_SECTION_LABEL = "\u5185\u5BB9\u6570\u636E"; + var BUSINESS_ESTIMATE_SECTION_LABEL = "\u6548\u679C\u9884\u4F30"; + var BUSINESS_ESTIMATE_LAYOUTS = [ + { key: "oneToTwenty", label: "1-20s\u89C6\u9891" }, + { key: "twentyToSixty", label: "20-60s\u89C6\u9891" }, + { key: "overSixty", label: "60s\u4EE5\u4E0A\u89C6\u9891" } + ]; + var BUSINESS_ESTIMATE_METRIC_LAYOUTS = [ + { key: "expectedCpm", label: "\u9884\u671FCPM" }, + { key: "expectedCpe", label: "\u9884\u671FCPE" }, + { key: "expectedPlay", label: "\u9884\u671F\u64AD\u653E\u91CF" }, + { key: "hotRate", label: "\u7206\u6587\u7387" } + ]; + function buildAudienceProfileCsv(rows) { + const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record)); + const csvColumns = [ + ...marketColumns.map(toMarketColumn), + ...buildBusinessAbilityColumns(), + ...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)) + ]; + const headerLine = csvColumns.map((column) => column.header).join(","); + const rowLines = rows.map( + (row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",") + ); + return [headerLine, ...rowLines].join("\n"); + } + function buildBusinessAbilityColumns() { + return [ + ...BUSINESS_VIDEO_LAYOUTS.flatMap( + (videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({ + header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`, + readValue: (row) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key) + })) + ), + ...BUSINESS_ESTIMATE_LAYOUTS.flatMap( + (durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({ + header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`, + readValue: (row) => readBusinessEstimateValue(row, durationLayout.key, metricLayout.key) + })) + ) + ]; + } + function readBusinessVideoValue(row, videoKey, metricKey) { + const businessAbility = row.businessAbility; + if (!businessAbility || businessAbility.status !== "success") { + return ""; + } + return businessAbility.videos[videoKey]?.[metricKey] ?? ""; + } + function readBusinessEstimateValue(row, durationKey, metricKey) { + const businessAbility = row.businessAbility; + if (!businessAbility || businessAbility.status !== "success") { + return ""; + } + return businessAbility.estimates[durationKey]?.[metricKey] ?? ""; + } + function toMarketColumn(column) { + return { + header: column.header, + readValue: (row) => column.readValue(row.record) + }; + } + function buildProfileColumns(layout) { + const columns = []; + if (layout.includeGender) { + columns.push( + ...buildFixedDistributionColumns( + layout.label, + layout.kind, + "gender", + GENDER_LABELS + ) + ); + } + columns.push( + ...buildFixedDistributionColumns(layout.label, layout.kind, "age", AGE_LABELS), + ...buildFixedDistributionColumns( + layout.label, + layout.kind, + "cityTier", + CITY_TIER_LABELS + ), + ...buildFixedDistributionColumns(layout.label, layout.kind, "crowd", CROWD_LABELS) + ); + return columns; + } + function buildFixedDistributionColumns(prefix, kind, key, labels) { + return labels.map((label) => ({ + header: `${prefix}-${label}\u5360\u6BD4`, + readValue: (row) => readDistributionValue(row.profiles[kind], key, label) + })); + } + function readDistributionValue(profile, key, label) { + if (profile.status !== "success") { + return ""; + } + return readProfileDistributionItems(profile, key).find( + (candidate) => candidate.label === label + )?.value ?? "0%"; + } + function readProfileDistributionItems(profile, key) { + return profile.status === "success" ? profile[key] ?? [] : []; + } + + // src/content/market/audience-profile-client.ts + var SECTION_BY_DISPLAY = [ + [/性别/, "gender"], + [/年龄/, "age"], + [/省份|全国省份/, "province"], + [/城市分布|地域/, "cityTop"], + [/城市等级/, "cityTier"], + [/兴趣/, "interest"], + [/八大人群/, "crowd"] + ]; + var GENDER_LABELS2 = { + female: "\u5973\u6027", + male: "\u7537\u6027" + }; + var AGE_ORDER = ["18-23", "24-30", "31-40", "41-50", "50+"]; + var CITY_TIER_ORDER = ["\u4E00\u7EBF", "\u65B0\u4E00\u7EBF", "\u4E8C\u7EBF", "\u4E09\u7EBF", "\u56DB\u7EBF", "\u4E94\u7EBF"]; + var AUDIENCE_PROFILE_TARGETS = { + audience: { linkType: 5, source: "audienceDistribution" }, + fans: { authorType: 1, source: "fansDistribution" }, + longtimeFans: { authorType: 5, source: "fansDistribution" } + }; + function createAudienceProfileClient(options = {}) { + const baseUrl = options.baseUrl ?? resolveBaseUrl(); + const fetchImpl = options.fetchImpl ?? defaultFetch; + const timeoutMs = options.timeoutMs ?? 8e3; + return { + async loadAudienceProfile(record, target) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetchImpl( + buildAudienceProfileUrl(record.authorId, baseUrl, target), + { + credentials: "include", + method: "GET", + signal: controller.signal + } + ); + if (!response.ok) { + return { + failureReason: "request-failed", + status: "failed" + }; + } + return mapAudienceProfileResponse(await response.json()); + } catch (error) { + return { + failureReason: error instanceof Error && error.name === "AbortError" ? "timeout" : "request-failed", + status: "failed" + }; + } finally { + clearTimeout(timeoutId); + } + } + }; + } + function buildAudienceProfileUrl(authorId, baseUrl, target) { + const url = new URL( + target.source === "audienceDistribution" ? "/gw/api/data_sp/author_audience_distribution" : "/gw/api/data_sp/get_author_fans_distribution", + baseUrl + ); + url.searchParams.set("o_author_id", authorId); + url.searchParams.set("platform_source", "1"); + if (target.source === "audienceDistribution") { + url.searchParams.set("platform_channel", "1"); + url.searchParams.set("link_type", String(target.linkType)); + } else { + url.searchParams.set("author_type", String(target.authorType)); + } + return url.toString(); + } + function mapAudienceProfileResponse(payload) { + if (!isRecord(payload) || !Array.isArray(payload.distributions)) { + return { + failureReason: "bad-response", + status: "failed" + }; + } + const profile = { + status: "success" + }; + payload.distributions.forEach((section) => { + if (!isRecord(section)) { + return; + } + const display = readString(section.type_display); + const sectionName = resolveSection(display); + if (!sectionName || !Array.isArray(section.distribution_list)) { + return; + } + profile[sectionName] = normalizeDistributionItems( + section.distribution_list, + sectionName + ); + }); + if (Object.keys(profile).length === 1) { + return { + failureReason: "missing-profile", + status: "failed" + }; + } + return profile; + } + function normalizeDistributionItems(rawItems, sectionName) { + const parsedItems = rawItems.map((item) => { + if (!isRecord(item)) { + return null; + } + const key = readString(item.distribution_key); + const value = readNumber(item.distribution_value); + if (!key || value === null) { + return null; + } + return { + label: normalizeLabel(key, sectionName), + rawLabel: key, + value + }; + }).filter( + (item) => Boolean(item) + ); + const total = parsedItems.reduce((sum, item) => sum + item.value, 0); + if (total <= 0) { + return []; + } + return parsedItems.sort((left, right) => compareDistributionItems(left, right, sectionName)).map((item) => ({ + label: item.label, + value: formatPercent(item.value / total) + })); + } + function compareDistributionItems(left, right, sectionName) { + if (sectionName === "age") { + return orderIndex(AGE_ORDER, left.rawLabel) - orderIndex(AGE_ORDER, right.rawLabel); + } + if (sectionName === "cityTier") { + return orderIndex(CITY_TIER_ORDER, left.rawLabel) - orderIndex(CITY_TIER_ORDER, right.rawLabel); + } + return right.value - left.value; + } + function orderIndex(order, value) { + const index = order.indexOf(value); + return index === -1 ? order.length : index; + } + function normalizeLabel(label, sectionName) { + if (sectionName === "gender") { + return GENDER_LABELS2[label] ?? label; + } + if (sectionName === "cityTier" && !label.endsWith("\u57CE\u5E02")) { + return `${label}\u57CE\u5E02`; + } + return label; + } + function resolveSection(display) { + if (!display) { + return null; + } + return SECTION_BY_DISPLAY.find(([pattern]) => pattern.test(display))?.[1] ?? null; + } + function formatPercent(value) { + const percent = Math.round(value * 1e3) / 10; + return `${Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(1)}%`; + } + function readString(value) { + return typeof value === "string" && value.trim() ? value.trim() : null; + } + function readNumber(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim()) { + const numericValue = Number(value); + return Number.isFinite(numericValue) ? numericValue : null; + } + return 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 isRecord(value) { + return typeof value === "object" && value !== null; + } + + // src/content/market/author-base-client.ts + function createAuthorBaseClient(options = {}) { + const baseUrl = options.baseUrl ?? resolveBaseUrl2(); + const fetchImpl = options.fetchImpl ?? defaultFetch2; + const timeoutMs = options.timeoutMs ?? 8e3; + return { + async loadAuthorBaseInfo(authorId) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetchImpl( + buildAuthorBaseInfoUrl(authorId, baseUrl), + { + credentials: "include", + method: "GET", + signal: controller.signal + } + ); + if (!response.ok) { + return buildFailedRecord(authorId, "request-failed"); + } + return mapAuthorBaseInfoResponse(authorId, await response.json()); + } catch (error) { + return buildFailedRecord( + authorId, + error instanceof Error && error.name === "AbortError" ? "timeout" : "request-failed" + ); + } finally { + clearTimeout(timeoutId); + } + } + }; + } + function buildAuthorBaseInfoUrl(authorId, baseUrl) { + const url = new URL("/gw/api/author/get_author_base_info", baseUrl); + url.searchParams.set("o_author_id", authorId); + url.searchParams.set("platform_source", "1"); + url.searchParams.set("platform_channel", "1"); + url.searchParams.set("recommend", "true"); + url.searchParams.set("need_sec_uid", "true"); + url.searchParams.set("need_linkage_info", "true"); + return url.toString(); + } + function mapAuthorBaseInfoResponse(authorId, payload) { + if (!isRecord2(payload)) { + return buildFailedRecord(authorId, "bad-response"); + } + const authorName = readString2(payload.nick_name); + if (!authorName) { + return buildFailedRecord(authorId, "missing-rate"); + } + return { + authorId, + authorName, + status: "success" + }; + } + function buildFailedRecord(authorId, failureReason) { + return { + authorId, + authorName: "", + failureReason, + status: "failed" + }; + } + function readString2(value) { + return typeof value === "string" && value.trim() ? value.trim() : null; + } + function resolveBaseUrl2() { + if (typeof location !== "undefined" && location.origin) { + return location.origin; + } + return "https://xingtu.cn"; + } + async function defaultFetch2(input, init) { + return fetch(input, init); + } + function isRecord2(value) { + return typeof value === "object" && value !== null; + } + + // src/content/market/author-id-input.ts + var AUTHOR_ID_PATTERN = /^\d{16,20}$/; + function parseAuthorIds(input) { + const ids = []; + const duplicates = []; + const invalidTokens = []; + const seen = /* @__PURE__ */ new Set(); + input.split(/[\s,,;;]+/).map((token) => token.trim()).filter(Boolean).forEach((token) => { + if (!/^\d+$/.test(token) || !AUTHOR_ID_PATTERN.test(token)) { + invalidTokens.push(token); + return; + } + if (seen.has(token)) { + duplicates.push(token); + return; + } + seen.add(token); + ids.push(token); + }); + return { + duplicates, + ids, + invalidTokens + }; + } + + // src/content/market/business-ability-client.ts + var VIDEO_TYPES = { + personalVideo: 1, + xingtuVideo: 2 + }; + function createBusinessAbilityClient(options = {}) { + const baseUrl = options.baseUrl ?? resolveBaseUrl3(); + const fetchImpl = options.fetchImpl ?? defaultFetch3; + const timeoutMs = options.timeoutMs ?? 8e3; + return { + async loadBusinessAbility(record) { + const personalVideo = await loadJson( + buildBusinessAbilityVideoUrl(record.authorId, baseUrl, VIDEO_TYPES.personalVideo) + ); + const xingtuVideo = await loadJson( + buildBusinessAbilityVideoUrl(record.authorId, baseUrl, VIDEO_TYPES.xingtuVideo) + ); + const estimates = await loadJson( + buildBusinessAbilityEstimateUrl(record.authorId, baseUrl) + ); + if (!personalVideo.ok || !xingtuVideo.ok || !estimates.ok) { + return { + failureReason: personalVideo.failureReason ?? xingtuVideo.failureReason ?? estimates.failureReason, + status: "failed" + }; + } + return { + estimates: mapBusinessAbilityEstimateResponse(estimates.payload), + status: "success", + videos: { + personalVideo: mapBusinessAbilityVideoResponse(personalVideo.payload), + xingtuVideo: mapBusinessAbilityVideoResponse(xingtuVideo.payload) + } + }; + } + }; + async function loadJson(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 { failureReason: "request-failed", ok: false }; + } + return { ok: true, payload: await response.json() }; + } catch (error) { + return { + failureReason: error instanceof Error && error.name === "AbortError" ? "timeout" : "request-failed", + ok: false + }; + } finally { + clearTimeout(timeoutId); + } + } + } + function buildBusinessAbilityVideoUrl(authorId, baseUrl, videoType) { + const url = new URL("/gw/api/data_sp/get_author_spread_info", baseUrl); + url.searchParams.set("o_author_id", authorId); + url.searchParams.set("platform_source", "1"); + url.searchParams.set("platform_channel", "1"); + url.searchParams.set("type", String(videoType)); + url.searchParams.set("flow_type", "0"); + url.searchParams.set("only_assign", "true"); + url.searchParams.set("range", "2"); + return url.toString(); + } + function buildBusinessAbilityEstimateUrl(authorId, baseUrl) { + const url = new URL( + "/gw/api/aggregator/get_author_commerce_spread_info", + baseUrl + ); + url.searchParams.set("o_author_id", authorId); + return url.toString(); + } + function mapBusinessAbilityVideoResponse(payload) { + const data = getPayloadData(payload); + return { + averageComment: formatWan(readNumber2(data?.comment_avg)), + averageDuration: formatDuration(readNumber2(data?.avg_duration)), + averageLike: formatWan(readNumber2(data?.like_avg)), + averageShare: formatWan(readNumber2(data?.share_avg)), + finishRate: formatBasisPointRate(readNestedNumber(data, "play_over_rate", "value")), + interactionRate: formatBasisPointRate( + readNestedNumber(data, "interact_rate", "value") + ), + medianPlay: formatWan(readNumber2(data?.play_mid)), + publishedItems: formatPublishedItems(readNumber2(data?.item_num)) + }; + } + function mapBusinessAbilityEstimateResponse(payload) { + const data = getPayloadData(payload); + const expectedPlay = formatWan(readNumber2(data?.vv)); + const hotRate = formatDecimalRate(readNumber2(data?.platform_hot_rate)); + return { + oneToTwenty: { + expectedCpe: formatDecimal(readNumber2(data?.cpe_1_20), 1), + expectedCpm: formatFixedDecimal(readNumber2(data?.cpm_1_20), 1), + expectedPlay, + hotRate + }, + overSixty: { + expectedCpe: formatDecimal(readNumber2(data?.cpe_60), 1), + expectedCpm: formatFixedDecimal(readNumber2(data?.cpm_60), 1), + expectedPlay, + hotRate + }, + twentyToSixty: { + expectedCpe: formatDecimal(readNumber2(data?.cpe_20_60), 1), + expectedCpm: formatFixedDecimal(readNumber2(data?.cpm_20_60), 1), + expectedPlay, + hotRate + } + }; + } + function formatPublishedItems(value) { + if (value === null) { + return ""; + } + return value > 0 && value < 5 ? "<5" : formatDecimal(value, 0); + } + function formatDuration(value) { + if (value === null) { + return ""; + } + return `${formatDecimal(value / 100, 0)}s`; + } + function formatBasisPointRate(value) { + if (value === null) { + return ""; + } + return `${formatDecimal(value / 100, 1)}%`; + } + function formatDecimalRate(value) { + if (value === null) { + return "\u7F3A\u5931"; + } + return `${formatDecimal(value * 100, 0)}%`; + } + function formatWan(value) { + if (value === null) { + return ""; + } + if (Math.abs(value) >= 1e4) { + return `${formatDecimal(value / 1e4, 1)}w`; + } + return formatDecimal(value, 0); + } + function formatDecimal(value, digits) { + if (value === null || !Number.isFinite(value)) { + return ""; + } + const fixed = value.toFixed(digits); + return fixed.replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1"); + } + function formatFixedDecimal(value, digits) { + if (value === null || !Number.isFinite(value)) { + return ""; + } + return value.toFixed(digits); + } + function readNestedNumber(data, objectKey, valueKey) { + const objectValue = data?.[objectKey]; + if (!isRecord3(objectValue)) { + return null; + } + return readNumber2(objectValue[valueKey]); + } + function readNumber2(value) { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim()) { + const numericValue = Number(value); + return Number.isFinite(numericValue) ? numericValue : null; + } + return null; + } + function getPayloadData(payload) { + if (!isRecord3(payload)) { + return null; + } + return isRecord3(payload.data) ? payload.data : payload; + } + function resolveBaseUrl3() { + if (typeof location !== "undefined" && location.origin) { + return location.origin; + } + return "https://xingtu.cn"; + } + async function defaultFetch3(input, init) { + return fetch(input, init); + } + function isRecord3(value) { + return typeof value === "object" && value !== null; + } + + // src/content/market/author-id-dialog.ts + function promptForAuthorIds(document2) { + return new Promise((resolve) => { + const overlay = document2.createElement("div"); + overlay.dataset.authorIdDialog = "overlay"; + applyOverlayStyles(overlay); + const dialog = document2.createElement("section"); + applyDialogStyles(dialog); + const title = document2.createElement("h2"); + title.textContent = "\u6309\u661F\u56FEID\u5BFC\u51FA\u753B\u50CFCSV"; + applyTitleStyles(title); + const textarea = document2.createElement("textarea"); + textarea.dataset.authorIdDialogInput = "textarea"; + textarea.placeholder = "\u6BCF\u884C\u4E00\u4E2A\u661F\u56FEID\uFF0C\u4E5F\u652F\u6301\u9017\u53F7\u3001\u7A7A\u683C\u5206\u9694"; + applyTextareaStyles(textarea); + const hint = document2.createElement("p"); + hint.textContent = "\u7C98\u8D34\u5BA2\u6237\u63D0\u4F9B\u7684\u8FBE\u4EBA\u661F\u56FEID\uFF0C\u786E\u8BA4\u540E\u5C06\u6279\u91CF\u5BFC\u51FA\u753B\u50CF\u548C\u5546\u4E1A\u80FD\u529B\u6570\u636E\u3002"; + applyHintStyles(hint); + const actions = document2.createElement("div"); + applyActionsStyles(actions); + const cancelButton = document2.createElement("button"); + cancelButton.type = "button"; + cancelButton.textContent = "\u53D6\u6D88"; + applySecondaryButtonStyles(cancelButton); + const confirmButton = document2.createElement("button"); + confirmButton.type = "button"; + confirmButton.textContent = "\u5F00\u59CB\u5BFC\u51FA"; + applyPrimaryButtonStyles(confirmButton); + actions.append(cancelButton, confirmButton); + dialog.append(title, hint, textarea, actions); + overlay.append(dialog); + document2.body.appendChild(overlay); + const close = (value) => { + overlay.remove(); + resolve(value); + }; + cancelButton.addEventListener("click", () => close(null)); + confirmButton.addEventListener("click", () => close(textarea.value)); + overlay.addEventListener("click", (event) => { + if (event.target === overlay) { + close(null); + } + }); + textarea.focus(); + }); + } + function applyOverlayStyles(overlay) { + overlay.style.position = "fixed"; + overlay.style.inset = "0"; + overlay.style.zIndex = "2147483647"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.background = "rgba(15, 23, 42, 0.38)"; + } + function applyDialogStyles(dialog) { + dialog.style.width = "520px"; + dialog.style.maxWidth = "calc(100vw - 32px)"; + dialog.style.background = "#ffffff"; + dialog.style.borderRadius = "8px"; + dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)"; + dialog.style.padding = "20px"; + dialog.style.boxSizing = "border-box"; + } + function applyTitleStyles(title) { + title.style.margin = "0 0 8px"; + title.style.fontSize = "18px"; + title.style.fontWeight = "700"; + title.style.color = "#1f2329"; + } + function applyHintStyles(hint) { + hint.style.margin = "0 0 12px"; + hint.style.fontSize = "13px"; + hint.style.lineHeight = "20px"; + hint.style.color = "#64748b"; + } + function applyTextareaStyles(textarea) { + textarea.style.width = "100%"; + textarea.style.height = "220px"; + textarea.style.resize = "vertical"; + textarea.style.border = "1px solid #d0d7de"; + textarea.style.borderRadius = "6px"; + textarea.style.padding = "10px"; + textarea.style.boxSizing = "border-box"; + textarea.style.fontSize = "13px"; + textarea.style.lineHeight = "20px"; + textarea.style.fontFamily = "ui-monospace, SFMono-Regular, Menlo, monospace"; + textarea.style.color = "#1f2329"; + } + function applyActionsStyles(actions) { + actions.style.display = "flex"; + actions.style.justifyContent = "flex-end"; + actions.style.columnGap = "8px"; + actions.style.marginTop = "14px"; + } + function applyPrimaryButtonStyles(button) { + button.style.height = "32px"; + button.style.padding = "0 15px"; + button.style.border = "1px solid #7f1d2d"; + button.style.borderRadius = "8px"; + button.style.background = "#7f1d2d"; + button.style.color = "#ffffff"; + button.style.fontWeight = "600"; + } + function applySecondaryButtonStyles(button) { + button.style.height = "32px"; + button.style.padding = "0 15px"; + button.style.border = "1px solid #d0d7de"; + button.style.borderRadius = "8px"; + button.style.background = "#ffffff"; + button.style.color = "#1f2329"; + button.style.fontWeight = "600"; + } + // src/content/market/batch-name-dialog.ts var DIALOG_STYLE_ID = "sces-batch-name-dialog-style"; var activeDialogs = /* @__PURE__ */ new WeakMap(); @@ -188,13 +943,13 @@ dialogRoot.setAttribute("role", "dialog"); dialogRoot.setAttribute("aria-modal", "true"); dialogRoot.setAttribute("aria-labelledby", "sces-batch-name-title"); - applyOverlayStyles(dialogRoot); + applyOverlayStyles2(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); + applyTitleStyles2(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); @@ -213,12 +968,12 @@ cancelButton.type = "button"; cancelButton.dataset.pluginBatchNameCancel = "button"; cancelButton.textContent = "\u53D6\u6D88"; - applySecondaryButtonStyles(cancelButton); + applySecondaryButtonStyles2(cancelButton); const confirmButton = document2.createElement("button"); confirmButton.type = "button"; confirmButton.dataset.pluginBatchNameConfirm = "button"; confirmButton.textContent = "\u786E\u8BA4\u63D0\u4EA4"; - applyPrimaryButtonStyles(confirmButton); + applyPrimaryButtonStyles2(confirmButton); buttonRow.append(cancelButton, confirmButton); dialogPanel.append(title, description, input, errorText, buttonRow); dialogRoot.appendChild(dialogPanel); @@ -300,7 +1055,7 @@ `; document2.head.appendChild(style); } - function applyOverlayStyles(root) { + function applyOverlayStyles2(root) { root.style.position = "fixed"; root.style.inset = "0"; root.style.background = "rgba(15, 23, 42, 0.38)"; @@ -319,7 +1074,7 @@ panel.style.padding = "24px"; panel.style.boxSizing = "border-box"; } - function applyTitleStyles(title) { + function applyTitleStyles2(title) { title.style.margin = "0"; title.style.color = "#4c0519"; title.style.fontSize = "20px"; @@ -358,7 +1113,7 @@ buttonRow.style.gap = "10px"; buttonRow.style.marginTop = "18px"; } - function applySecondaryButtonStyles(button) { + function applySecondaryButtonStyles2(button) { button.style.height = "36px"; button.style.padding = "0 16px"; button.style.border = "1px solid #d7dde6"; @@ -368,7 +1123,7 @@ button.style.fontWeight = "600"; button.style.cursor = "pointer"; } - function applyPrimaryButtonStyles(button) { + function applyPrimaryButtonStyles2(button) { button.style.height = "36px"; button.style.padding = "0 16px"; button.style.border = "1px solid #7f1d2d"; @@ -441,9 +1196,9 @@ 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")) ?? "", - coreUserId: readString(readMarketFieldValue(row, attributeDatas, "core_user_id")) ?? void 0, + authorId: readString3(readMarketFieldValue(row, attributeDatas, "star_id")) ?? readString3(readMarketFieldValue(row, attributeDatas, "id")) ?? "", + authorName: readString3(readMarketFieldValue(row, attributeDatas, "nickname")) ?? readString3(readMarketFieldValue(row, attributeDatas, "nick_name")) ?? "", + coreUserId: readString3(readMarketFieldValue(row, attributeDatas, "core_user_id")) ?? void 0, exportFields: buildMarketExportFieldFallbacks(row, attributeDatas), hasDirectRatesSource: true, location: readMarketLocation(row, attributeDatas), @@ -465,7 +1220,7 @@ 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( + records: marketList.map((row) => isRecord4(row) ? mapMarketListRow(row) : null).filter( (row) => row !== null && Boolean(row.authorId || row.authorName) ), totalCount: readKnownNumberDeep(container, TOTAL_COUNT_KEYS) ?? void 0, @@ -473,7 +1228,7 @@ }; } function readKnownPaginationNumber(value, kind) { - if (!isRecord(value)) { + if (!isRecord4(value)) { return null; } return readKnownNumberDeep(value, kind === "page" ? PAGE_NUMBER_KEYS : PAGE_SIZE_KEYS); @@ -482,7 +1237,7 @@ const queue = [value]; while (queue.length > 0) { const current = queue.shift(); - if (!isRecord(current)) { + if (!isRecord4(current)) { continue; } if (readMarketListArray(current)) { @@ -506,21 +1261,21 @@ return null; } function unwrapVueRef(value) { - if (isRecord(value) && "value" in value) { + if (isRecord4(value) && "value" in value) { return value.value; } return value; } - function isRecord(value) { + function isRecord4(value) { return typeof value === "object" && value !== null; } function readMarketAttributeDatas(record) { - return isRecord(record.attribute_datas) ? record.attribute_datas : {}; + return isRecord4(record.attribute_datas) ? record.attribute_datas : {}; } function readMarketFieldValue(record, attributeDatas, field) { return record[field] ?? attributeDatas[field]; } - function readString(value) { + function readString3(value) { return typeof value === "string" ? value : null; } function normalizeMarketListRate(value) { @@ -583,11 +1338,11 @@ 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 nickname = readString3(readMarketFieldValue(record, attributeDatas, "nickname")) ?? readString3(readMarketFieldValue(record, attributeDatas, "nick_name")) ?? ""; const parts = [ nickname, readMarketGenderLabel(readMarketFieldValue(record, attributeDatas, "gender")), - readString(readMarketFieldValue(record, attributeDatas, "city")) ?? "" + readString3(readMarketFieldValue(record, attributeDatas, "city")) ?? "" ].filter((value) => Boolean(value)); return parts.length > 0 ? parts.join(" ") : void 0; } @@ -616,7 +1371,7 @@ return `${themes.slice(0, 2).join(" ")} ${themes.length - 2}+`; } function readMarketLocation(record, attributeDatas) { - return readString(readMarketFieldValue(record, attributeDatas, "city")) ?? void 0; + return readString3(readMarketFieldValue(record, attributeDatas, "city")) ?? void 0; } function readMarketPrice21To60s(record, attributeDatas) { return formatCurrencyValue( @@ -626,10 +1381,10 @@ function readMarketRepresentativeVideo(record, attributeDatas) { const items = readArrayLike(readMarketFieldValue(record, attributeDatas, "items")); for (const item of items) { - if (!isRecord(item)) { + if (!isRecord4(item)) { continue; } - const title = readString(item.title); + const title = readString3(item.title); if (hasTextValue(title)) { return normalizeExportCellText(title); } @@ -637,7 +1392,7 @@ return void 0; } function readMarketGenderLabel(value) { - const rawValue = typeof value === "number" ? String(value) : readString(value); + const rawValue = typeof value === "number" ? String(value) : readString3(value); if (rawValue === "1") { return "\u7537"; } @@ -734,13 +1489,13 @@ return []; } function readRecordLike(value) { - if (isRecord(value)) { + if (isRecord4(value)) { return value; } if (typeof value === "string") { try { const parsedValue = JSON.parse(value); - return isRecord(parsedValue) ? parsedValue : null; + return isRecord4(parsedValue) ? parsedValue : null; } catch { return null; } @@ -757,7 +1512,7 @@ return void 0; } function readKnownNumberDeep(value, keys) { - if (!isRecord(value)) { + if (!isRecord4(value)) { return null; } const directValue = readKnownNumber(value, keys); @@ -1695,7 +2450,7 @@ if (!Array.isArray(marketList)) { continue; } - return marketList.map((row) => isRecord2(row) ? mapMarketListRow(row) : null).filter((row) => row !== null); + return marketList.map((row) => isRecord5(row) ? mapMarketListRow(row) : null).filter((row) => row !== null); } } return []; @@ -1708,10 +2463,10 @@ const setupStates = []; while (queue.length > 0) { const current = queue.shift(); - if (!isRecord2(current)) { + if (!isRecord5(current)) { continue; } - if (isRecord2(current._setupState)) { + if (isRecord5(current._setupState)) { setupStates.push(current._setupState); } const children = Array.isArray(current.$children) ? current.$children : []; @@ -1732,18 +2487,18 @@ return []; } return parsedRows.map((row) => { - const record = isRecord2(row) ? row : {}; - const singleVideoAfterSearchRate = readString2( + const record = isRecord5(row) ? row : {}; + const singleVideoAfterSearchRate = readString4( record.singleVideoAfterSearchRate ); return { - authorId: readString2(record.authorId) ?? "", - authorName: readString2(record.authorName) ?? "", - coreUserId: readString2(record.coreUserId) ?? void 0, + authorId: readString4(record.authorId) ?? "", + authorName: readString4(record.authorName) ?? "", + coreUserId: readString4(record.coreUserId) ?? void 0, exportFields: readSerializedExportFields(record), hasDirectRatesSource: Boolean(singleVideoAfterSearchRate), - location: readString2(record.location) ?? void 0, - price21To60s: readString2(record.price21To60s) ?? void 0, + location: readString4(record.location) ?? void 0, + price21To60s: readString4(record.price21To60s) ?? void 0, rates: singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : void 0 @@ -1754,15 +2509,15 @@ } } function unwrapVueRef2(value) { - if (isRecord2(value) && "value" in value) { + if (isRecord5(value) && "value" in value) { return value.value; } return value; } - function isRecord2(value) { + function isRecord5(value) { return typeof value === "object" && value !== null; } - function readString2(value) { + function readString4(value) { return typeof value === "string" ? value : null; } function normalizeExportCellText2(value) { @@ -2029,7 +2784,7 @@ })}`; } function readSerializedExportFields(record) { - if (!isRecord2(record.exportFields)) { + if (!isRecord5(record.exportFields)) { return void 0; } const entries = Object.entries(record.exportFields).flatMap( @@ -2170,8 +2925,8 @@ // src/content/market/api-client.ts function createMarketApiClient(options = {}) { - const baseUrl = options.baseUrl ?? resolveBaseUrl(); - const fetchImpl = options.fetchImpl ?? defaultFetch; + const baseUrl = options.baseUrl ?? resolveBaseUrl4(); + const fetchImpl = options.fetchImpl ?? defaultFetch4; const timeoutMs = options.timeoutMs ?? 8e3; return { async loadAuthorAseInfo(authorId) { @@ -2232,7 +2987,7 @@ return url.toString(); } function mapAuthorAseInfoResponse(payload) { - const data = getPayloadData(payload); + const data = getPayloadData2(payload); if (!data) { return { success: false, @@ -2259,28 +3014,28 @@ } }; } - function getPayloadData(payload) { - if (!isRecord3(payload)) { + function getPayloadData2(payload) { + if (!isRecord6(payload)) { return null; } - return isRecord3(payload.data) ? payload.data : payload; + return isRecord6(payload.data) ? payload.data : payload; } function readNormalizedRate(value) { return typeof value === "string" ? normalizeRateDisplay(value) : null; } - function resolveBaseUrl() { + function resolveBaseUrl4() { if (typeof location !== "undefined" && location.origin) { return location.origin; } return "https://xingtu.cn"; } - async function defaultFetch(input, init) { + async function defaultFetch4(input, init) { return fetch(input, init); } function isAbortError(error) { return error instanceof Error && error.name === "AbortError"; } - function isRecord3(value) { + function isRecord6(value) { return typeof value === "object" && value !== null; } @@ -2498,8 +3253,13 @@ "[data-plugin-toolbar='root']" ); if (existingRoot) { - ensureToolbarMounted(existingRoot, document2); - return readToolbarDom(existingRoot); + if (existingRoot.querySelector( + '[data-plugin-export-audience-profile-by-id="button"]' + )) { + ensureToolbarMounted(existingRoot, document2); + return readToolbarDom(existingRoot); + } + existingRoot.remove(); } const root = document2.createElement("section"); root.dataset.pluginToolbar = "root"; @@ -2523,6 +3283,14 @@ exportButton.type = "button"; exportButton.dataset.pluginExport = "button"; exportButton.textContent = "\u5BFC\u51FACSV"; + const audienceProfileExportButton = document2.createElement("button"); + audienceProfileExportButton.type = "button"; + audienceProfileExportButton.dataset.pluginExportAudienceProfile = "button"; + audienceProfileExportButton.textContent = "\u5BFC\u51FA\u753B\u50CFCSV"; + const audienceProfileByIdExportButton = document2.createElement("button"); + audienceProfileByIdExportButton.type = "button"; + audienceProfileByIdExportButton.dataset.pluginExportAudienceProfileById = "button"; + audienceProfileByIdExportButton.textContent = "\u6309ID\u5BFC\u51FA\u753B\u50CFCSV"; const batchSubmitButton = document2.createElement("button"); batchSubmitButton.type = "button"; batchSubmitButton.dataset.pluginBatchSubmit = "button"; @@ -2534,11 +3302,15 @@ exportRangeSelect, exportCustomPagesInput, exportButton, + audienceProfileExportButton, + audienceProfileByIdExportButton, batchSubmitButton, exportStatusText ); document2.body.appendChild(root); applyNativeControlStyles(document2, { + audienceProfileExportButton, + audienceProfileByIdExportButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -2548,12 +3320,20 @@ exportButton.addEventListener("click", () => { void handlers.onExport(); }); + audienceProfileExportButton.addEventListener("click", () => { + void handlers.onExportAudienceProfile(); + }); + audienceProfileByIdExportButton.addEventListener("click", () => { + void handlers.onExportAudienceProfileByIds(); + }); batchSubmitButton.addEventListener("click", () => { void handlers.onSubmitBatch(); }); exportRangeSelect.addEventListener("change", () => { syncCustomPagesInputVisibility({ batchSubmitButton, + audienceProfileByIdExportButton, + audienceProfileExportButton, exportButton, exportCustomPagesInput, exportRangeSelect, @@ -2562,6 +3342,8 @@ }); }); const toolbarDom = { + audienceProfileExportButton, + audienceProfileByIdExportButton, batchSubmitButton, exportButton, exportCustomPagesInput, @@ -2580,6 +3362,12 @@ } function readToolbarDom(root) { const toolbarDom = { + audienceProfileByIdExportButton: root.querySelector( + '[data-plugin-export-audience-profile-by-id="button"]' + ), + audienceProfileExportButton: root.querySelector( + '[data-plugin-export-audience-profile="button"]' + ), batchSubmitButton: root.querySelector( '[data-plugin-batch-submit="button"]' ), @@ -2649,6 +3437,8 @@ function setToolbarBusyState(toolbar, isBusy) { [ toolbar.batchSubmitButton, + toolbar.audienceProfileByIdExportButton, + toolbar.audienceProfileExportButton, toolbar.exportButton, toolbar.exportRangeSelect, toolbar.exportCustomPagesInput @@ -2773,10 +3563,17 @@ const nativeButton = primaryButton ?? findNativeActionButton(document2, "\u81EA\u5B9A\u4E49\u6307\u6807") ?? findNativeActionButton(document2, "\u5BFC\u51FA"); if (nativeButton) { controls.exportButton.className = nativeButton.className; + controls.audienceProfileExportButton.className = nativeButton.className; + controls.audienceProfileByIdExportButton.className = nativeButton.className; controls.batchSubmitButton.className = nativeButton.className; } - [controls.exportButton, controls.batchSubmitButton].forEach((button) => { - applyPrimaryButtonStyles2(button); + [ + controls.exportButton, + controls.audienceProfileExportButton, + controls.audienceProfileByIdExportButton, + controls.batchSubmitButton + ].forEach((button) => { + applyPrimaryButtonStyles3(button); button.style.whiteSpace = "nowrap"; }); [controls.exportRangeSelect, controls.exportCustomPagesInput].forEach((element) => { @@ -2791,7 +3588,7 @@ controls.exportRangeSelect.style.minWidth = "104px"; controls.exportCustomPagesInput.style.width = "72px"; } - function applyPrimaryButtonStyles2(button) { + function applyPrimaryButtonStyles3(button) { button.style.backgroundColor = "#7f1d2d"; button.style.border = "1px solid #7f1d2d"; button.style.borderRadius = "8px"; @@ -2817,12 +3614,16 @@ style.id = PLUGIN_ACTION_BUTTON_STYLE_ID; style.textContent = ` [data-plugin-export="button"]:hover:not(:disabled), + [data-plugin-export-audience-profile="button"]:hover:not(:disabled), + [data-plugin-export-audience-profile-by-id="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-export-audience-profile="button"]:active:not(:disabled), + [data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled), [data-plugin-batch-submit="button"]:active:not(:disabled) { background-color: #58111f !important; border-color: #58111f !important; @@ -2830,12 +3631,16 @@ } [data-plugin-export="button"]:focus-visible, + [data-plugin-export-audience-profile="button"]:focus-visible, + [data-plugin-export-audience-profile-by-id="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-export-audience-profile="button"]:disabled, + [data-plugin-export-audience-profile-by-id="button"]:disabled, [data-plugin-batch-submit="button"]:disabled { background-color: #c89ca4 !important; border-color: #c89ca4 !important; @@ -2945,11 +3750,11 @@ const queue = Object.values(setupState); while (queue.length > 0) { const current = unwrapVueRef3(queue.shift()); - if (!isRecord4(current)) { + if (!isRecord7(current)) { continue; } const reqParams = unwrapVueRef3(current.reqParams); - if (isRecord4(reqParams)) { + if (isRecord7(reqParams)) { return reqParams; } Object.values(current).forEach((value) => { @@ -2965,12 +3770,12 @@ return MARKET_SEARCH_ENDPOINT_PATH; } function unwrapVueRef3(value) { - if (isRecord4(value) && "value" in value) { + if (isRecord7(value) && "value" in value) { return value.value; } return value; } - function isRecord4(value) { + function isRecord7(value) { return typeof value === "object" && value !== null; } @@ -2984,7 +3789,7 @@ "page_num" ]; function createSilentExportController(options) { - const fetchImpl = options.fetchImpl ?? defaultFetch2; + const fetchImpl = options.fetchImpl ?? defaultFetch5; return { async exportRecords(target) { const snapshot = readMarketListRequestSnapshot(options.document); @@ -3117,7 +3922,7 @@ } try { const parsedJson = JSON.parse(trimmedBody); - if (!replacePageNumberInValue(parsedJson, pageNumber) && isRecord5(parsedJson)) { + if (!replacePageNumberInValue(parsedJson, pageNumber) && isRecord8(parsedJson)) { parsedJson.page = pageNumber; } return JSON.stringify(parsedJson); @@ -3144,7 +3949,7 @@ } try { const parsedBody = JSON.parse(trimmedBody); - return isRecord5(parsedBody) ? parsedBody : null; + return isRecord8(parsedBody) ? parsedBody : null; } catch { const searchParams = new URLSearchParams(trimmedBody); const payload = {}; @@ -3155,7 +3960,7 @@ } } function replacePageNumberInValue(value, pageNumber) { - if (!isRecord5(value)) { + if (!isRecord8(value)) { return false; } let replaced = false; @@ -3204,10 +4009,10 @@ const parsedValue = Number(value); return Number.isFinite(parsedValue) ? parsedValue : null; } - async function defaultFetch2(input, init) { + async function defaultFetch5(input, init) { return fetch(input, init); } - function isRecord5(value) { + function isRecord8(value) { return typeof value === "object" && value !== null; } function mergeMarketRecord2(existingRecord, incomingRecord) { @@ -3432,15 +4237,31 @@ // src/content/market/index.ts function createMarketController(options) { const marketApiClient = createMarketApiClient(); + const audienceProfileClient = createAudienceProfileClient(); + const authorBaseClient = createAuthorBaseClient(); + const businessAbilityClient = createBusinessAbilityClient(); 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 buildAudienceCsv = options.buildAudienceProfileCsv ?? buildAudienceProfileCsv; + const loadAudienceProfile = options.loadAudienceProfile ?? audienceProfileClient.loadAudienceProfile; + const loadAuthorBaseInfo = options.loadAuthorBaseInfo ?? authorBaseClient.loadAuthorBaseInfo; + const loadBusinessAbility = options.loadBusinessAbility ?? businessAbilityClient.loadBusinessAbility; const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage)); const mutationObserverFactory = options.mutationObserverFactory ?? ((callback) => new MutationObserver(callback)); const promptBatchName = options.promptBatchName ?? (() => promptForBatchName(options.document)); + const promptAuthorIds = options.promptAuthorIds ?? (() => promptForAuthorIds(options.document)); const submitBatch = options.submitBatch ?? ((payload) => readBatchSubmitAck(sendRuntimeMessage, payload)); + const audienceProfileTargets = [ + { kind: "audience", target: AUDIENCE_PROFILE_TARGETS.audience }, + { kind: "fans", target: AUDIENCE_PROFILE_TARGETS.fans }, + { + kind: "longtimeFans", + target: AUDIENCE_PROFILE_TARGETS.longtimeFans + } + ]; let activeProgressLabel = "\u5BFC\u51FA\u4E2D"; let shouldShowDetailedProgress = true; const exportRangeController = createExportRangeController({ @@ -3513,6 +4334,107 @@ setToolbarBusyState(toolbar, false); } }, + onExportAudienceProfile: async () => { + syncSelectionStateFromDom(); + if (selectedAuthorIds.size === 0) { + setToolbarExportStatus(toolbar, "\u8BF7\u5148\u52FE\u9009\u9700\u8981\u5BFC\u51FA\u753B\u50CF\u7684\u8FBE\u4EBA"); + return; + } + const exportTarget = readToolbarExportTarget(toolbar); + if (!exportTarget.target) { + setToolbarExportStatus(toolbar, exportTarget.error ?? "\u5BFC\u51FA\u914D\u7F6E\u65E0\u6548"); + return; + } + setToolbarBusyState(toolbar, true); + try { + const selectedRecords = filterRecordsBySelectionStrict( + await exportRecords(exportTarget.target, "\u753B\u50CF\u5BFC\u51FA\u4E2D", { + showDetailedProgress: false + }) + ); + if (selectedRecords.length === 0) { + setToolbarExportStatus(toolbar, "\u5F53\u524D\u5BFC\u51FA\u8303\u56F4\u5185\u6CA1\u6709\u9009\u4E2D\u7684\u8FBE\u4EBA"); + return; + } + const rows = []; + for (let index = 0; index < selectedRecords.length; index += 1) { + const record = selectedRecords[index]; + setToolbarExportStatus( + toolbar, + `\u753B\u50CF\u5BFC\u51FA\u4E2D ${index + 1}/${selectedRecords.length}...` + ); + const [profiles, businessAbility] = await Promise.all([ + loadAudienceProfileSet(record), + loadBusinessAbilitySafe(record) + ]); + rows.push({ + businessAbility, + profiles, + record + }); + } + if (rows.every( + (row) => Object.values(row.profiles).every((profile) => profile.status === "failed") + )) { + setToolbarExportStatus(toolbar, "\u753B\u50CF\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"); + return; + } + options.onCsvReady?.(buildAudienceCsv(rows), buildAudienceProfileFilename()); + setToolbarExportStatus(toolbar, ""); + } catch (error) { + setToolbarExportStatus( + toolbar, + error instanceof Error ? error.message : "\u753B\u50CF\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" + ); + } finally { + setToolbarBusyState(toolbar, false); + } + }, + onExportAudienceProfileByIds: async () => { + const input = await promptAuthorIds(); + if (input === null) { + return; + } + const parsed = parseAuthorIds(input); + if (parsed.ids.length === 0) { + setToolbarExportStatus(toolbar, "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u8FBE\u4EBA\u661F\u56FEID"); + return; + } + setToolbarBusyState(toolbar, true); + try { + setToolbarExportStatus( + toolbar, + `\u8BC6\u522B ${parsed.ids.length + parsed.duplicates.length + parsed.invalidTokens.length} \u4E2A\uFF0C\u53BB\u91CD\u540E ${parsed.ids.length} \u4E2A\uFF0C\u975E\u6CD5 ${parsed.invalidTokens.length} \u4E2A` + ); + const backendMetricsByAuthorId = await loadBackendMetricsMap(parsed.ids); + const rows = []; + for (let index = 0; index < parsed.ids.length; index += 1) { + const authorId = parsed.ids[index]; + setToolbarExportStatus( + toolbar, + `\u6309ID\u753B\u50CF\u5BFC\u51FA\u4E2D ${index + 1}/${parsed.ids.length}...` + ); + rows.push( + await loadAudienceProfileRowById( + authorId, + backendMetricsByAuthorId.get(authorId) + ) + ); + } + options.onCsvReady?.( + buildAudienceCsv(rows), + buildAudienceProfileFilename(/* @__PURE__ */ new Date(), "\u6309ID\u5BFC\u51FA") + ); + setToolbarExportStatus(toolbar, ""); + } catch (error) { + setToolbarExportStatus( + toolbar, + error instanceof Error ? error.message : "\u6309ID\u5BFC\u51FA\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" + ); + } finally { + setToolbarBusyState(toolbar, false); + } + }, onSubmitBatch: async () => { syncSelectionStateFromDom(); const exportTarget = readToolbarExportTarget(toolbar); @@ -3827,6 +4749,137 @@ ); return selectedRecords.length > 0 ? selectedRecords : records; } + function filterRecordsBySelectionStrict(records) { + if (selectedAuthorIds.size === 0) { + return []; + } + return records.filter((record) => selectedAuthorIds.has(record.authorId)); + } + async function loadAudienceProfileSet(record) { + const profiles = {}; + for (const { kind, target } of audienceProfileTargets) { + try { + profiles[kind] = await loadAudienceProfile(record, target); + } catch (error) { + profiles[kind] = { + failureReason: error instanceof Error ? error.message : "request-failed", + status: "failed" + }; + } + } + return profiles; + } + async function loadBusinessAbilitySafe(record) { + try { + return await loadBusinessAbility(record); + } catch (error) { + return { + failureReason: error instanceof Error ? error.message : "request-failed", + status: "failed" + }; + } + } + async function loadAudienceProfileRowById(authorId, backendMetrics) { + const [baseRecord, metricsResult] = await Promise.all([ + loadAuthorBaseInfoSafe(authorId), + loadAuthorMetricsSafe(authorId) + ]); + const recordForRequests = { + ...baseRecord, + authorName: baseRecord.authorName || authorId, + ...metricsResult.success ? { rates: metricsResult.rates } : {}, + ...backendMetrics ? { backendMetrics, backendMetricsStatus: "success" } : {} + }; + const [profiles, businessAbility] = await Promise.all([ + loadAudienceProfileSet(recordForRequests), + loadBusinessAbilitySafe(recordForRequests) + ]); + const failureReasons = collectAudienceProfileRowFailures( + baseRecord, + profiles, + businessAbility + ); + const rowStatus = failureReasons.length === 0 ? "\u6210\u529F" : hasAudienceProfileRowSuccess(baseRecord, profiles, businessAbility) ? "\u90E8\u5206\u6210\u529F" : "\u5931\u8D25"; + const authorName = baseRecord.authorName || ""; + return { + businessAbility, + profiles, + record: { + ...recordForRequests, + exportFields: { + \u8FBE\u4EBAID: authorId, + \u8FBE\u4EBA\u540D\u79F0: authorName, + \u5BFC\u51FA\u72B6\u6001: rowStatus, + \u5931\u8D25\u539F\u56E0: failureReasons.join("; ") + } + } + }; + } + async function loadAuthorBaseInfoSafe(authorId) { + try { + return await loadAuthorBaseInfo(authorId); + } catch (error) { + return { + authorId, + authorName: "", + failureReason: error instanceof Error ? "request-failed" : "request-failed", + status: "failed" + }; + } + } + async function loadAuthorMetricsSafe(authorId) { + try { + return await loadAuthorMetrics(authorId); + } catch { + return { + reason: "request-failed", + success: false + }; + } + } + async function loadBackendMetricsMap(authorIds) { + const metricsMap = /* @__PURE__ */ new Map(); + if (!searchBackendMetrics || authorIds.length === 0) { + return metricsMap; + } + try { + const rows = await searchBackendMetrics(authorIds); + rows.forEach((row) => { + const { starId, ...backendMetrics } = row; + metricsMap.set(starId, backendMetrics); + }); + } catch { + return metricsMap; + } + return metricsMap; + } + function collectAudienceProfileRowFailures(baseRecord, profiles, businessAbility) { + const failures = []; + if (baseRecord.status === "failed") { + failures.push(`\u57FA\u7840\u4FE1\u606F:${baseRecord.failureReason ?? "request-failed"}`); + } + Object.entries(profiles).forEach(([kind, profile]) => { + if (profile.status === "failed") { + failures.push(`${readAudienceProfileKindLabel(kind)}:${profile.failureReason ?? "request-failed"}`); + } + }); + if (businessAbility.status === "failed") { + failures.push(`\u5546\u4E1A\u80FD\u529B:${businessAbility.failureReason ?? "request-failed"}`); + } + return failures; + } + function hasAudienceProfileRowSuccess(baseRecord, profiles, businessAbility) { + return baseRecord.status === "success" || businessAbility.status === "success" || Object.values(profiles).some((profile) => profile.status === "success"); + } + function readAudienceProfileKindLabel(kind) { + if (kind === "audience") { + return "\u89C2\u4F17\u753B\u50CF"; + } + if (kind === "fans") { + return "\u7C89\u4E1D\u753B\u50CF"; + } + return "\u94C1\u7C89\u753B\u50CF"; + } async function prepareCurrentPageForExport() { await runSyncCycle(); await harvestCurrentPageForExport(); @@ -4321,9 +5374,18 @@ globalThis.chrome?.runtime?.sendMessage ); } + function buildAudienceProfileFilename(date = /* @__PURE__ */ new Date(), label) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + const labelPart = label ? `_${label}` : ""; + return `\u8FBE\u4EBA\u8FDE\u63A5\u7528\u6237\u753B\u50CF${labelPart}_${year}${month}${day}_${hour}${minute}.csv`; + } // src/content/market/auth-gate.ts - function renderMarketAuthGate(document2, currentWindow) { + function renderMarketAuthGate(document2, currentWindow, message = "\u8BF7\u5148\u767B\u5F55\u63D2\u4EF6") { const existingGate = document2.querySelector( '[data-market-auth-gate="root"]' ); @@ -4333,10 +5395,14 @@ 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

`; + const title = root.querySelector("strong"); + if (title) { + title.textContent = message; + } 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"); }); @@ -4358,7 +5424,11 @@ const authState = await readAuthState2(sendAuthMessage); if (!authState?.isAuthenticated) { await waitForBodyReady(currentDocument, currentWindow); - renderMarketAuthGate(currentDocument, currentWindow); + renderMarketAuthGate( + currentDocument, + currentWindow, + isExpiredAuthState(authState) ? "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" : void 0 + ); return { ready: Promise.resolve() }; @@ -4366,11 +5436,15 @@ await waitForBodyReady(currentDocument, currentWindow); return controllerFactory({ document: currentDocument, - onCsvReady: (csv) => { + onCsvReady: (csv, filename) => { + if (filename) { + downloadCsv(currentDocument, currentWindow, csv, filename); + return; + } if (requestCsvDownload(csv)) { return; } - downloadCsv(currentDocument, currentWindow, csv); + downloadCsv(currentDocument, currentWindow, csv, filename); }, window: currentWindow }); @@ -4403,14 +5477,14 @@ }); } bootstrapContentScript(); - function requestCsvDownload(csv) { + function requestCsvDownload(csv, filename) { const runtime = globalThis.chrome?.runtime; if (!runtime?.id || typeof runtime.sendMessage !== "function") { return false; } runtime.sendMessage({ csv, - filename: `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`, + filename: filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`, type: DOWNLOAD_MARKET_CSV_MESSAGE }); return true; @@ -4439,14 +5513,14 @@ currentWindow.setTimeout(handleReady, 0); }); } - function downloadCsv(document2, window2, csv) { + function downloadCsv(document2, window2, csv, filename) { 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`; + link.download = filename ?? `star-chart-search-enhancer-${formatTimestampForFilename()}.csv`; document2.body.appendChild(link); link.click(); link.remove(); @@ -4455,6 +5529,10 @@ function formatTimestampForFilename() { return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); } + function isExpiredAuthState(authState) { + const lastError = authState?.lastError; + return typeof lastError === "string" && (/token/i.test(lastError) || lastError.includes("\u8FC7\u671F")); + } function installMarketPageBridge(document2) { if (document2.documentElement.querySelector( '[data-sces-market-bridge="script"]' diff --git a/docs/【超简单版】插件安装使用指南.md b/docs/【超简单版】插件安装使用指南.md index 1dd707b..e94a4e1 100644 --- a/docs/【超简单版】插件安装使用指南.md +++ b/docs/【超简单版】插件安装使用指南.md @@ -37,9 +37,9 @@ 4. 点击左上角出现的 **"加载已解压的扩展程序"** -5. 选择刚才解压出来的文件夹里的 **`dist-release`** 文件夹 +5. 选择刚才解压出来的插件文件夹 - ⚠️ **重要**:必须选择 `dist-release` 这个子文件夹,不要选外层文件夹 + ⚠️ **重要**:如果文件夹里能看到 `manifest.json`、`content`、`background`、`popup` 这些文件和文件夹,说明选对了。 6. 看到绿色的插件卡片出现,就装好了! @@ -72,14 +72,43 @@ https://xingtu.cn/ad/creator/market ## 📝 主要功能 -### 1️⃣ 导出 Excel 表格 +### 1️⃣ 导出基础数据(基础CSV) - 勾选你想导出的达人(不勾就选全部) - 选择范围:当前页 / 前5页 / 全部 - 点击 **"导出CSV"** - 文件自动下载到电脑的"下载"文件夹 -### 2️⃣ 提交批次 +### 2️⃣ 导出画像数据(画像CSV) + +当你需要导出达人的画像、内容数据、效果预估时使用: + +- 先勾选你想导出画像的达人 +- 选择范围:当前页 / 前5页 / 全部 +- 点击 **"导出画像CSV"** +- 等待下载完成 +- 文件自动下载到电脑的"下载"文件夹 + +⚠️ **重要**:画像导出必须先勾选达人,因为它会额外请求达人详情页数据,不能默认导出全部。 + +**画像CSV包含**: +- 观众画像、粉丝画像、铁粉画像 +- 内容数据:个人视频/星图视频的播放量中位数、完播率、互动率、发布作品、平均时长、平均点赞、平均评论、平均转发 +- 效果预估:不同视频时长的预期CPM、预期CPE、预期播放量、爆文率 +- 秒思api数据:看后搜率、看后搜数、新增A3数、新增A3率、CPA3、cp_search + +### 3️⃣ 按ID导出画像数据 + +当你想批量查询特定达人ID的画像数据时使用: + +- 点击 **"按ID导出画像CSV"** +- 在弹出的对话框中输入达人ID(每行一个) +- 点击确认 +- 等待下载完成 + +**适用场景**:已知一批达人星图ID,需要批量导出这些达人的画像CSV。 + +### 4️⃣ 提交批次 - 勾选你想提交的达人 - 点击 **"提交批次"** @@ -100,7 +129,7 @@ https://xingtu.cn/ad/creator/market ⚠️ **如果重新加载后还是旧版本**: - 先点击插件卡片的 **"移除"** 删除旧版本 - 然后重新点击 **"加载已解压的扩展程序"** -- 再次选择 `dist-release` 文件夹 +- 再次选择新解压出来的插件文件夹 --- @@ -136,4 +165,4 @@ A: 重新解压压缩包,然后到 `chrome://extensions` 点"重新加载" 2. 页面截图 3. 扩展 ID(从 chrome://extensions 里看) -**记住正确的 ID:`**pkjopdibdnomhogjheclhnknmejccffg**` +**记住正确的 ID:`pkjopdibdnomhogjheclhnknmejccffg`** diff --git a/release/star-chart-search-enhancer-chrome-web-store.zip b/release/star-chart-search-enhancer-chrome-web-store.zip deleted file mode 100644 index 6397d89..0000000 Binary files a/release/star-chart-search-enhancer-chrome-web-store.zip and /dev/null differ diff --git a/release/star-chart-search-enhancer-internal.zip b/release/star-chart-search-enhancer-internal.zip index f4df8ec..ede2c1f 100644 Binary files a/release/star-chart-search-enhancer-internal.zip and b/release/star-chart-search-enhancer-internal.zip differ diff --git a/src/content/market/audience-profile-csv.ts b/src/content/market/audience-profile-csv.ts index 11ffa35..f0b2a58 100644 --- a/src/content/market/audience-profile-csv.ts +++ b/src/content/market/audience-profile-csv.ts @@ -68,6 +68,9 @@ const BUSINESS_VIDEO_METRIC_LAYOUTS: Array<{ { key: "averageShare", label: "平均转发" } ]; +const BUSINESS_VIDEO_SECTION_LABEL = "内容数据"; +const BUSINESS_ESTIMATE_SECTION_LABEL = "效果预估"; + const BUSINESS_ESTIMATE_LAYOUTS: Array<{ key: BusinessAbilityDurationKind; label: string; @@ -108,14 +111,14 @@ function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] { return [ ...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({ - header: `商业能力-${videoLayout.label}-${metricLayout.label}`, + header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`, readValue: (row: AudienceProfileExportRow) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key) })) ), ...BUSINESS_ESTIMATE_LAYOUTS.flatMap((durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({ - header: `商业能力-${durationLayout.label}-${metricLayout.label}`, + header: `${BUSINESS_ESTIMATE_SECTION_LABEL}-${durationLayout.label}-${metricLayout.label}`, readValue: (row: AudienceProfileExportRow) => readBusinessEstimateValue(row, durationLayout.key, metricLayout.key) })) diff --git a/src/content/market/csv-exporter.ts b/src/content/market/csv-exporter.ts index 5d0b324..471355f 100644 --- a/src/content/market/csv-exporter.ts +++ b/src/content/market/csv-exporter.ts @@ -45,31 +45,31 @@ const RATE_COLUMNS: CsvColumn[] = [ const BACKEND_METRIC_COLUMNS: CsvColumn[] = [ { - header: "看后搜率", + header: "秒思api-看后搜率", readValue: (record: MarketRecord) => record.backendMetrics?.afterViewSearchRate ?? "" }, { - header: "看后搜数", + header: "秒思api-看后搜数", readValue: (record: MarketRecord) => record.backendMetrics?.afterViewSearchCount ?? "" }, { - header: "新增A3数", + header: "秒思api-新增A3数", readValue: (record: MarketRecord) => record.backendMetrics?.a3IncreaseCount ?? "" }, { - header: "新增A3率", + header: "秒思api-新增A3率", readValue: (record: MarketRecord) => record.backendMetrics?.newA3Rate ?? "" }, { - header: "CPA3", + header: "秒思api-CPA3", readValue: (record: MarketRecord) => record.backendMetrics?.cpa3 ?? "" }, { - header: "cp_search", + header: "秒思api-cp_search", readValue: (record: MarketRecord) => record.backendMetrics?.cpSearch ?? "" } ]; diff --git a/tests/audience-profile-csv.test.ts b/tests/audience-profile-csv.test.ts index 10472f3..a42c370 100644 --- a/tests/audience-profile-csv.test.ts +++ b/tests/audience-profile-csv.test.ts @@ -91,11 +91,13 @@ describe("audience-profile-csv", () => { expect(headerLine).toContain("达人信息,连接用户数"); expect(headerLine).not.toContain("抓取状态"); expect(headerLine).not.toContain("失败原因"); - expect(headerLine).toContain("商业能力-个人视频-播放量中位数"); - expect(headerLine).toContain("商业能力-星图视频-平均转发"); - expect(headerLine).toContain("商业能力-1-20s视频-预期CPM"); - expect(headerLine).toContain("商业能力-20-60s视频-爆文率"); - expect(headerLine).toContain("商业能力-60s以上视频-预期播放量"); + expect(headerLine).toContain("内容数据-个人视频-播放量中位数"); + expect(headerLine).toContain("内容数据-星图视频-平均转发"); + expect(headerLine).toContain("效果预估-1-20s视频-预期CPM"); + expect(headerLine).toContain("效果预估-20-60s视频-爆文率"); + expect(headerLine).toContain("效果预估-60s以上视频-预期播放量"); + expect(headerLine).not.toContain("商业能力-个人视频-播放量中位数"); + expect(headerLine).not.toContain("商业能力-20-60s视频-预期CPM"); expect(headerLine).toContain("观众画像-男性占比"); expect(headerLine).toContain("粉丝画像-女性占比"); expect(headerLine).not.toContain("铁粉画像-男性占比"); @@ -110,10 +112,10 @@ describe("audience-profile-csv", () => { expect(headerLine).not.toContain("兴趣TOP"); expect(rowLine).toContain("71.7%"); expect(rowLine).toContain("60%"); - expect(readCsvValue(csv, "商业能力-个人视频-播放量中位数")).toBe("3738.4w"); - expect(readCsvValue(csv, "商业能力-星图视频-平均转发")).toBe("68.4w"); - expect(readCsvValue(csv, "商业能力-1-20s视频-预期CPM")).toBe("120.0"); - expect(readCsvValue(csv, "商业能力-20-60s视频-爆文率")).toBe("缺失"); + expect(readCsvValue(csv, "内容数据-个人视频-播放量中位数")).toBe("3738.4w"); + expect(readCsvValue(csv, "内容数据-星图视频-平均转发")).toBe("68.4w"); + expect(readCsvValue(csv, "效果预估-1-20s视频-预期CPM")).toBe("120.0"); + expect(readCsvValue(csv, "效果预估-20-60s视频-爆文率")).toBe("缺失"); }); test("leaves distribution cells empty when profile loading fails", () => { diff --git a/tests/csv-exporter.test.ts b/tests/csv-exporter.test.ts index 47cde00..d9c1fb2 100644 --- a/tests/csv-exporter.test.ts +++ b/tests/csv-exporter.test.ts @@ -16,12 +16,12 @@ describe("csv-exporter", () => { "21-60s报价", "单视频看后搜率", "个人视频看后搜率", - "看后搜率", - "看后搜数", - "新增A3数", - "新增A3率", - "CPA3", - "cp_search" + "秒思api-看后搜率", + "秒思api-看后搜数", + "秒思api-新增A3数", + "秒思api-新增A3率", + "秒思api-CPA3", + "秒思api-cp_search" ].join(",") ); }); @@ -61,12 +61,12 @@ describe("csv-exporter", () => { "21-60s报价", "单视频看后搜率", "个人视频看后搜率", - "看后搜率", - "看后搜数", - "新增A3数", - "新增A3率", - "CPA3", - "cp_search" + "秒思api-看后搜率", + "秒思api-看后搜数", + "秒思api-新增A3数", + "秒思api-新增A3率", + "秒思api-CPA3", + "秒思api-cp_search" ].join(",") ); expect(rowLine).toBe( @@ -96,12 +96,12 @@ describe("csv-exporter", () => { "粉丝数", "单视频看后搜率", "个人视频看后搜率", - "看后搜率", - "看后搜数", - "新增A3数", - "新增A3率", - "CPA3", - "cp_search" + "秒思api-看后搜率", + "秒思api-看后搜数", + "秒思api-新增A3数", + "秒思api-新增A3率", + "秒思api-CPA3", + "秒思api-cp_search" ].join(",") ); expect(rowLine).toBe("Alice,100w,,,,,,,,");