docs: add distribution build and simplified user guide
- Include dist-release/ and release/ for direct colleague use - Add beginner-friendly installation guide - Update .gitignore to track distribution builds
This commit is contained in:
parent
3992d4c325
commit
09c106e020
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,8 +2,8 @@
|
||||
.old-reference/
|
||||
.local/
|
||||
dist/
|
||||
dist-release/
|
||||
release/
|
||||
# dist-release/
|
||||
# release/
|
||||
node_modules/
|
||||
|
||||
# Local debug captures
|
||||
|
||||
BIN
dist-release/assets/icons/icon-128.png
Normal file
BIN
dist-release/assets/icons/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
dist-release/assets/icons/icon-16.png
Normal file
BIN
dist-release/assets/icons/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 777 B |
BIN
dist-release/assets/icons/icon-32.png
Normal file
BIN
dist-release/assets/icons/icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
dist-release/assets/icons/icon-48.png
Normal file
BIN
dist-release/assets/icons/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
13
dist-release/assets/icons/icon-source.svg
Normal file
13
dist-release/assets/icons/icon-source.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="16" y1="12" x2="114" y2="116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9F1239"/>
|
||||
<stop offset="1" stop-color="#4C0519"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="8" width="112" height="112" rx="28" fill="url(#bg)"/>
|
||||
<path d="M34 80L50 62L64 70L83 46" stroke="#FFF7ED" stroke-linecap="round" stroke-linejoin="round" stroke-width="10"/>
|
||||
<circle cx="86" cy="44" r="8" fill="#FFF7ED"/>
|
||||
<path d="M79 80C79 71.7157 85.7157 65 94 65C102.284 65 109 71.7157 109 80C109 88.2843 102.284 95 94 95C85.7157 95 79 88.2843 79 80Z" stroke="#FFF7ED" stroke-width="8"/>
|
||||
<path d="M104 91L114 101" stroke="#FFF7ED" stroke-linecap="round" stroke-width="8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
3335
dist-release/background/index.js
Normal file
3335
dist-release/background/index.js
Normal file
File diff suppressed because it is too large
Load Diff
4446
dist-release/content/index.js
Normal file
4446
dist-release/content/index.js
Normal file
File diff suppressed because it is too large
Load Diff
583
dist-release/content/market-page-bridge.js
Normal file
583
dist-release/content/market-page-bridge.js
Normal file
@ -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;
|
||||
}
|
||||
})();
|
||||
58
dist-release/manifest.json
Normal file
58
dist-release/manifest.json
Normal file
@ -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/*"
|
||||
]
|
||||
}
|
||||
12
dist-release/popup/index.html
Normal file
12
dist-release/popup/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Star Chart Search Enhancer</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="app"></main>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
219
dist-release/popup/index.js
Normal file
219
dist-release/popup/index.js
Normal file
@ -0,0 +1,219 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/popup/view.ts
|
||||
function renderLoggedOut(root, error) {
|
||||
root.innerHTML = `
|
||||
<section data-popup-state="logged-out">
|
||||
<h1>Star Chart Search Enhancer</h1>
|
||||
<p>\u767B\u5F55\u540E\u624D\u80FD\u4F7F\u7528\u661F\u56FE\u589E\u5F3A\u529F\u80FD</p>
|
||||
${error ? `<p data-popup-error="true">${error}</p>` : ""}
|
||||
<button type="button" data-popup-sign-in="button">\u767B\u5F55 Logto</button>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
function renderLoggedIn(root, authState) {
|
||||
const userInfo = authState.userInfo;
|
||||
root.innerHTML = `
|
||||
<section data-popup-state="logged-in">
|
||||
<h1>Star Chart Search Enhancer</h1>
|
||||
<p>\u5DF2\u767B\u5F55</p>
|
||||
<p>${userInfo?.name ?? userInfo?.username ?? "\u672A\u77E5\u7528\u6237"}</p>
|
||||
<p>${userInfo?.email ?? ""}</p>
|
||||
<button type="button" data-popup-sign-out="button">\u9000\u51FA\u767B\u5F55</button>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
function renderDevPanel(root, authState) {
|
||||
const panel = root.ownerDocument.createElement("section");
|
||||
panel.dataset.popupDevPanel = "root";
|
||||
panel.innerHTML = `
|
||||
<h2>dev auth panel</h2>
|
||||
<p>resource: ${authState.resource ?? ""}</p>
|
||||
<p>scopes: ${(authState.scopes ?? []).join(", ")}</p>
|
||||
<p>token: ${authState.tokenAvailable ? "available" : "missing"}</p>
|
||||
<p>expires: ${authState.accessTokenExpiresAt ?? "unknown"}</p>
|
||||
<p>error: ${authState.lastError ?? ""}</p>
|
||||
<button type="button" data-popup-test-protected-api="button">\u6D4B\u8BD5\u53D7\u4FDD\u62A4\u63A5\u53E3</button>
|
||||
<pre data-popup-protected-api-result="output"></pre>
|
||||
`;
|
||||
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();
|
||||
}
|
||||
})();
|
||||
132
docs/【超简单版】插件安装使用指南.md
Normal file
132
docs/【超简单版】插件安装使用指南.md
Normal file
@ -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**`
|
||||
BIN
release/star-chart-search-enhancer-chrome-web-store.zip
Normal file
BIN
release/star-chart-search-enhancer-chrome-web-store.zip
Normal file
Binary file not shown.
BIN
release/star-chart-search-enhancer-internal.zip
Normal file
BIN
release/star-chart-search-enhancer-internal.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user