feat: 完善视频任务与 KOC 资源库
This commit is contained in:
547
lib/partner-batch-workbook.ts
Normal file
547
lib/partner-batch-workbook.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
import path from "node:path";
|
||||
import { strFromU8, unzipSync } from "fflate";
|
||||
|
||||
export const PARTNER_BATCH_HEADERS = [
|
||||
"序号(不能改)",
|
||||
"标题",
|
||||
"笔记内容(正文+话题)",
|
||||
"图片",
|
||||
"发布链接",
|
||||
"笔记截图",
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
"_系统笔记ID",
|
||||
"_原笔记截图",
|
||||
"_原数据分析截图",
|
||||
] as const;
|
||||
|
||||
export const PARTNER_BATCH_VISIBLE_COLUMN_COUNT = 7;
|
||||
export const PARTNER_BATCH_MAX_BYTES = 80_000_000;
|
||||
|
||||
export type PartnerBatchWorkbookColumns = {
|
||||
headers: string[];
|
||||
columnWidths: number[];
|
||||
sourceImageStartColumn: number;
|
||||
sourceImageCount: number;
|
||||
sourceVideoStartColumn: number;
|
||||
sourceVideoCount: number;
|
||||
publishUrlColumn: number;
|
||||
publishScreenshotColumn: number;
|
||||
creatorScreenshotColumn: number;
|
||||
systemColumn: number;
|
||||
};
|
||||
|
||||
function nonNegativeInteger(value: number) {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
||||
}
|
||||
|
||||
export function buildPartnerBatchWorkbookColumns(input: {
|
||||
contentFormat: "image_text" | "video";
|
||||
maxSourceImages: number;
|
||||
maxSourceVideos: number;
|
||||
}): PartnerBatchWorkbookColumns {
|
||||
const sourceImageCount =
|
||||
input.contentFormat === "video"
|
||||
? 0
|
||||
: Math.max(1, nonNegativeInteger(input.maxSourceImages));
|
||||
const sourceVideoCount =
|
||||
input.contentFormat === "video"
|
||||
? Math.max(1, nonNegativeInteger(input.maxSourceVideos))
|
||||
: 0;
|
||||
const sourceImageStartColumn = 3;
|
||||
const sourceVideoStartColumn = sourceImageStartColumn + sourceImageCount;
|
||||
const publishUrlColumn = sourceVideoStartColumn + sourceVideoCount;
|
||||
const publishScreenshotColumn = publishUrlColumn + 1;
|
||||
const creatorScreenshotColumn = publishScreenshotColumn + 1;
|
||||
const systemColumn = creatorScreenshotColumn + 1;
|
||||
return {
|
||||
headers: [
|
||||
"序号(不能改)",
|
||||
"标题",
|
||||
"笔记内容(正文+话题)",
|
||||
...Array.from(
|
||||
{ length: sourceImageCount },
|
||||
(_, index) => `图片${index + 1}`,
|
||||
),
|
||||
...Array.from(
|
||||
{ length: sourceVideoCount },
|
||||
(_, index) => `视频${index + 1}`,
|
||||
),
|
||||
"发布链接",
|
||||
"笔记截图",
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
"_系统笔记ID",
|
||||
"_原笔记截图",
|
||||
"_原数据分析截图",
|
||||
],
|
||||
columnWidths: [
|
||||
14,
|
||||
30,
|
||||
62,
|
||||
...Array.from({ length: sourceImageCount }, () => 24),
|
||||
...Array.from({ length: sourceVideoCount }, () => 20),
|
||||
45,
|
||||
28,
|
||||
32,
|
||||
22,
|
||||
22,
|
||||
22,
|
||||
],
|
||||
sourceImageStartColumn,
|
||||
sourceImageCount,
|
||||
sourceVideoStartColumn,
|
||||
sourceVideoCount,
|
||||
publishUrlColumn,
|
||||
publishScreenshotColumn,
|
||||
creatorScreenshotColumn,
|
||||
systemColumn,
|
||||
};
|
||||
}
|
||||
|
||||
function firstForwardedValue(value: string | null) {
|
||||
return value?.split(",")[0]?.trim() ?? "";
|
||||
}
|
||||
|
||||
function httpOrigin(value: string) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "http:" || url.protocol === "https:"
|
||||
? url.origin
|
||||
: "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePartnerWorkbookOrigin(
|
||||
request: Request,
|
||||
configuredOrigin = "",
|
||||
) {
|
||||
const requestUrl = new URL(request.url);
|
||||
const host =
|
||||
firstForwardedValue(request.headers.get("x-forwarded-host")) ||
|
||||
firstForwardedValue(request.headers.get("host"));
|
||||
const forwardedProtocol = firstForwardedValue(
|
||||
request.headers.get("x-forwarded-proto"),
|
||||
).toLowerCase();
|
||||
const protocol = ["http", "https"].includes(forwardedProtocol)
|
||||
? forwardedProtocol
|
||||
: requestUrl.protocol.replace(":", "");
|
||||
const proxyOrigin = host ? httpOrigin(`${protocol}://${host}`) : "";
|
||||
return (
|
||||
httpOrigin(configuredOrigin) ||
|
||||
proxyOrigin ||
|
||||
httpOrigin(requestUrl.origin) ||
|
||||
requestUrl.origin
|
||||
);
|
||||
}
|
||||
|
||||
export type PartnerBatchImage = {
|
||||
bytes: Uint8Array;
|
||||
contentType: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
export type PartnerBatchImportRow = {
|
||||
spreadsheetRow: number;
|
||||
sequence: string;
|
||||
title: string;
|
||||
publishUrl: string;
|
||||
distributionId: string;
|
||||
originalPublishScreenshotKey: string;
|
||||
originalCreatorScreenshotKey: string;
|
||||
publishScreenshot: PartnerBatchImage | null;
|
||||
creatorScreenshot: PartnerBatchImage | null;
|
||||
};
|
||||
|
||||
function decodeXml(value: string) {
|
||||
return value
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, code) =>
|
||||
String.fromCodePoint(Number.parseInt(code, 16)),
|
||||
);
|
||||
}
|
||||
|
||||
function xmlAttribute(value: string) {
|
||||
return decodeXml(value);
|
||||
}
|
||||
|
||||
function textNodes(xml: string) {
|
||||
return [...xml.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)]
|
||||
.map((match) => decodeXml(match[1]))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function columnIndex(reference: string) {
|
||||
const letters = reference.match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? "";
|
||||
let result = 0;
|
||||
for (const letter of letters) result = result * 26 + letter.charCodeAt(0) - 64;
|
||||
return Math.max(0, result - 1);
|
||||
}
|
||||
|
||||
function parseWorksheet(xml: string, sharedStrings: string[]) {
|
||||
const rows: string[][] = [];
|
||||
for (const rowMatch of xml.matchAll(/<row\b([^>]*)>([\s\S]*?)<\/row>/g)) {
|
||||
const rowNumber = Number(
|
||||
rowMatch[1].match(/\br="(\d+)"/)?.[1] ?? rows.length + 1,
|
||||
);
|
||||
const values: string[] = [];
|
||||
for (const cellMatch of rowMatch[2].matchAll(
|
||||
/<c\b([^>]*)>([\s\S]*?)<\/c>/g,
|
||||
)) {
|
||||
const attributes = cellMatch[1];
|
||||
const body = cellMatch[2];
|
||||
const reference = attributes.match(/\br="([A-Z]+\d+)"/i)?.[1] ?? "A1";
|
||||
const type = attributes.match(/\bt="([^"]+)"/)?.[1] ?? "";
|
||||
const rawValue = body.match(/<v>([\s\S]*?)<\/v>/)?.[1] ?? "";
|
||||
let value = "";
|
||||
if (type === "s") value = sharedStrings[Number(rawValue)] ?? "";
|
||||
else if (type === "inlineStr") value = textNodes(body);
|
||||
else if (type === "b") value = rawValue === "1" ? "TRUE" : "FALSE";
|
||||
else value = decodeXml(rawValue);
|
||||
values[columnIndex(reference)] = value.trim();
|
||||
}
|
||||
while (rows.length < rowNumber - 1) rows.push([]);
|
||||
rows[rowNumber - 1] = values;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function normalizeHeader(value: string) {
|
||||
return value.replace(/[\s_\-()()]/g, "").toLocaleLowerCase("zh-CN");
|
||||
}
|
||||
|
||||
function isSourceImageHeader(value: string) {
|
||||
return /^(?:图片|发布配图)\d*$/.test(normalizeHeader(value));
|
||||
}
|
||||
|
||||
function headerAliases(header: (typeof PARTNER_BATCH_HEADERS)[number]) {
|
||||
const aliases: Record<string, string[]> = {
|
||||
"序号(不能改)": ["序号(不能改)", "序号"],
|
||||
标题: ["标题"],
|
||||
"笔记内容(正文+话题)": ["笔记内容(正文+话题)", "笔记内容"],
|
||||
图片: ["图片", "发布配图"],
|
||||
发布链接: ["发布链接"],
|
||||
笔记截图: ["笔记截图", "发布截图"],
|
||||
"数据分析截图(单篇笔记数据分析截图)": [
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
"数据分析截图",
|
||||
"创作者中心截图",
|
||||
],
|
||||
_系统笔记ID: ["_系统笔记ID", "系统笔记ID"],
|
||||
_原笔记截图: ["_原笔记截图"],
|
||||
_原数据分析截图: ["_原数据分析截图"],
|
||||
};
|
||||
return aliases[header] ?? [header];
|
||||
}
|
||||
|
||||
function findHeader(rows: string[][]) {
|
||||
for (let rowIndex = 0; rowIndex < Math.min(rows.length, 8); rowIndex += 1) {
|
||||
const mapping = new Map<(typeof PARTNER_BATCH_HEADERS)[number], number>();
|
||||
rows[rowIndex].forEach((value, column) => {
|
||||
for (const header of PARTNER_BATCH_HEADERS) {
|
||||
if (header === "图片" && isSourceImageHeader(value)) {
|
||||
if (!mapping.has(header)) mapping.set(header, column);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
headerAliases(header).some(
|
||||
(alias) => normalizeHeader(alias) === normalizeHeader(value),
|
||||
)
|
||||
) {
|
||||
mapping.set(header, column);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (
|
||||
mapping.has("序号(不能改)") &&
|
||||
mapping.has("标题") &&
|
||||
mapping.has("发布链接") &&
|
||||
mapping.has("笔记截图") &&
|
||||
mapping.has("数据分析截图(单篇笔记数据分析截图)") &&
|
||||
mapping.has("_系统笔记ID")
|
||||
) {
|
||||
return { rowIndex, mapping };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function relationshipMap(xml: string) {
|
||||
const relationships = new Map<string, string>();
|
||||
for (const match of xml.matchAll(/<Relationship\b([^>]*)\/?\s*>/g)) {
|
||||
const attributes = match[1];
|
||||
const id = attributes.match(/\bId="([^"]+)"/)?.[1] ?? "";
|
||||
const target = attributes.match(/\bTarget="([^"]+)"/)?.[1] ?? "";
|
||||
if (id && target) relationships.set(id, xmlAttribute(target));
|
||||
}
|
||||
return relationships;
|
||||
}
|
||||
|
||||
function contentType(bytes: Uint8Array, fileName: string) {
|
||||
if (bytes[0] === 0x89 && bytes[1] === 0x50) return "image/png";
|
||||
if (bytes[0] === 0xff && bytes[1] === 0xd8) return "image/jpeg";
|
||||
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
|
||||
return "image/gif";
|
||||
}
|
||||
if (String.fromCharCode(...bytes.slice(8, 12)) === "WEBP") return "image/webp";
|
||||
const extension = path.extname(fileName).toLowerCase();
|
||||
return extension === ".png"
|
||||
? "image/png"
|
||||
: extension === ".gif"
|
||||
? "image/gif"
|
||||
: extension === ".webp"
|
||||
? "image/webp"
|
||||
: "image/jpeg";
|
||||
}
|
||||
|
||||
function resolveZipPath(base: string, target: string) {
|
||||
return path.posix.normalize(path.posix.join(path.posix.dirname(base), target));
|
||||
}
|
||||
|
||||
function parseDrawingImages(entries: Record<string, Uint8Array>) {
|
||||
const images = new Map<string, PartnerBatchImage>();
|
||||
const sheetRelationshipsXml = entries["xl/worksheets/_rels/sheet1.xml.rels"]
|
||||
? strFromU8(entries["xl/worksheets/_rels/sheet1.xml.rels"])
|
||||
: "";
|
||||
const sheetRelationships = relationshipMap(sheetRelationshipsXml);
|
||||
const sheetXml = entries["xl/worksheets/sheet1.xml"]
|
||||
? strFromU8(entries["xl/worksheets/sheet1.xml"])
|
||||
: "";
|
||||
const drawingId = sheetXml.match(/<drawing\b[^>]*r:id="([^"]+)"/)?.[1] ?? "";
|
||||
const drawingTarget = sheetRelationships.get(drawingId);
|
||||
if (!drawingTarget) return images;
|
||||
const drawingPath = resolveZipPath("xl/worksheets/sheet1.xml", drawingTarget);
|
||||
const drawingXml = entries[drawingPath] ? strFromU8(entries[drawingPath]) : "";
|
||||
const drawingRelationshipsPath = path.posix.join(
|
||||
path.posix.dirname(drawingPath),
|
||||
"_rels",
|
||||
`${path.posix.basename(drawingPath)}.rels`,
|
||||
);
|
||||
const drawingRelationships = relationshipMap(
|
||||
entries[drawingRelationshipsPath]
|
||||
? strFromU8(entries[drawingRelationshipsPath])
|
||||
: "",
|
||||
);
|
||||
for (const anchor of drawingXml.matchAll(
|
||||
/<xdr:(?:oneCellAnchor|twoCellAnchor)\b[^>]*>[\s\S]*?<xdr:from>([\s\S]*?)<\/xdr:from>[\s\S]*?<a:blip\b[^>]*r:embed="([^"]+)"[\s\S]*?<\/xdr:(?:oneCellAnchor|twoCellAnchor)>/g,
|
||||
)) {
|
||||
const column = Number(anchor[1].match(/<xdr:col>(\d+)<\/xdr:col>/)?.[1]);
|
||||
const row = Number(anchor[1].match(/<xdr:row>(\d+)<\/xdr:row>/)?.[1]);
|
||||
const mediaTarget = drawingRelationships.get(anchor[2]);
|
||||
if (!Number.isInteger(column) || !Number.isInteger(row) || !mediaTarget) continue;
|
||||
const mediaPath = resolveZipPath(drawingPath, mediaTarget);
|
||||
const bytes = entries[mediaPath];
|
||||
if (!bytes) continue;
|
||||
images.set(`${row + 1}:${column}`, {
|
||||
bytes,
|
||||
contentType: contentType(bytes, mediaPath),
|
||||
fileName: path.posix.basename(mediaPath),
|
||||
});
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function parseRichValueImages(entries: Record<string, Uint8Array>) {
|
||||
const images = new Map<string, PartnerBatchImage>();
|
||||
const worksheetXml = entries["xl/worksheets/sheet1.xml"]
|
||||
? strFromU8(entries["xl/worksheets/sheet1.xml"])
|
||||
: "";
|
||||
const metadataXml = entries["xl/metadata.xml"]
|
||||
? strFromU8(entries["xl/metadata.xml"])
|
||||
: "";
|
||||
const richValueXml = entries["xl/richData/rdrichvalue.xml"]
|
||||
? strFromU8(entries["xl/richData/rdrichvalue.xml"])
|
||||
: "";
|
||||
const richValueRelXml = entries["xl/richData/richValueRel.xml"]
|
||||
? strFromU8(entries["xl/richData/richValueRel.xml"])
|
||||
: "";
|
||||
const richValueRelRelationships = relationshipMap(
|
||||
entries["xl/richData/_rels/richValueRel.xml.rels"]
|
||||
? strFromU8(entries["xl/richData/_rels/richValueRel.xml.rels"])
|
||||
: "",
|
||||
);
|
||||
if (
|
||||
!worksheetXml ||
|
||||
!metadataXml ||
|
||||
!richValueXml ||
|
||||
!richValueRelXml ||
|
||||
!richValueRelRelationships.size
|
||||
) {
|
||||
return images;
|
||||
}
|
||||
|
||||
const valueMetadataXml =
|
||||
metadataXml.match(/<valueMetadata\b[^>]*>([\s\S]*?)<\/valueMetadata>/)?.[1] ??
|
||||
"";
|
||||
const metadataToRichValue = [
|
||||
...valueMetadataXml.matchAll(/<bk\b[^>]*>[\s\S]*?<rc\b[^>]*\bv="(\d+)"[^>]*\/>[\s\S]*?<\/bk>/g),
|
||||
].map((match) => Number(match[1]));
|
||||
const richValueToRelationship = [
|
||||
...richValueXml.matchAll(/<rv\b[^>]*>([\s\S]*?)<\/rv>/g),
|
||||
].map((match) => Number(match[1].match(/<v>(\d+)<\/v>/)?.[1] ?? -1));
|
||||
const relationshipIds = [
|
||||
...richValueRelXml.matchAll(/<rel\b[^>]*\br:id="([^"]+)"[^>]*\/>/g),
|
||||
].map((match) => match[1]);
|
||||
|
||||
for (const cell of worksheetXml.matchAll(
|
||||
/<c\b([^>]*)>[\s\S]*?<\/c>/g,
|
||||
)) {
|
||||
const reference = cell[1].match(/\br="([A-Z]+\d+)"/i)?.[1] ?? "";
|
||||
const metadataIndex = Number(cell[1].match(/\bvm="(\d+)"/)?.[1] ?? 0);
|
||||
const row = Number(reference.match(/\d+$/)?.[0] ?? 0);
|
||||
const column = columnIndex(reference);
|
||||
if (!reference || !metadataIndex || !row) continue;
|
||||
const richValueIndex = metadataToRichValue[metadataIndex - 1];
|
||||
const relationshipIndex = richValueToRelationship[richValueIndex];
|
||||
const relationshipId = relationshipIds[relationshipIndex];
|
||||
const mediaTarget = richValueRelRelationships.get(relationshipId);
|
||||
if (!mediaTarget) continue;
|
||||
const mediaPath = resolveZipPath(
|
||||
"xl/richData/richValueRel.xml",
|
||||
mediaTarget,
|
||||
);
|
||||
const bytes = entries[mediaPath];
|
||||
if (!bytes) continue;
|
||||
images.set(`${row}:${column}`, {
|
||||
bytes,
|
||||
contentType: contentType(bytes, mediaPath),
|
||||
fileName: path.posix.basename(mediaPath),
|
||||
});
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function parseWpsCellImages(entries: Record<string, Uint8Array>) {
|
||||
const images = new Map<string, PartnerBatchImage>();
|
||||
const worksheetXml = entries["xl/worksheets/sheet1.xml"]
|
||||
? strFromU8(entries["xl/worksheets/sheet1.xml"])
|
||||
: "";
|
||||
const cellImagesXml = entries["xl/cellimages.xml"]
|
||||
? strFromU8(entries["xl/cellimages.xml"])
|
||||
: "";
|
||||
const relationships = relationshipMap(
|
||||
entries["xl/_rels/cellimages.xml.rels"]
|
||||
? strFromU8(entries["xl/_rels/cellimages.xml.rels"])
|
||||
: "",
|
||||
);
|
||||
if (!worksheetXml || !cellImagesXml || !relationships.size) return images;
|
||||
|
||||
const imageIdToRelationship = new Map<string, string>();
|
||||
for (const match of cellImagesXml.matchAll(
|
||||
/<(?:etc:)?cellImage\b[^>]*>([\s\S]*?)<\/(?:etc:)?cellImage>/g,
|
||||
)) {
|
||||
const imageId = match[1].match(
|
||||
/<(?:xdr:)?cNvPr\b[^>]*\bname="([^"]+)"/,
|
||||
)?.[1];
|
||||
const relationshipId = match[1].match(
|
||||
/<(?:a:)?blip\b[^>]*\br:embed="([^"]+)"/,
|
||||
)?.[1];
|
||||
if (imageId && relationshipId) {
|
||||
imageIdToRelationship.set(imageId, relationshipId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const cell of worksheetXml.matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
|
||||
const reference = cell[1].match(/\br="([A-Z]+\d+)"/i)?.[1] ?? "";
|
||||
const imageId = decodeXml(cell[2]).match(/DISPIMG\("([^"]+)"/i)?.[1];
|
||||
if (!reference || !imageId) continue;
|
||||
const relationshipId = imageIdToRelationship.get(imageId);
|
||||
const mediaTarget = relationshipId
|
||||
? relationships.get(relationshipId)
|
||||
: undefined;
|
||||
if (!mediaTarget) continue;
|
||||
const mediaPath = resolveZipPath("xl/cellimages.xml", mediaTarget);
|
||||
const bytes = entries[mediaPath];
|
||||
if (!bytes) continue;
|
||||
const row = Number(reference.match(/\d+$/)?.[0] ?? 0);
|
||||
if (!row) continue;
|
||||
images.set(`${row}:${columnIndex(reference)}`, {
|
||||
bytes,
|
||||
contentType: contentType(bytes, mediaPath),
|
||||
fileName: path.posix.basename(mediaPath),
|
||||
});
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function parseImages(entries: Record<string, Uint8Array>) {
|
||||
const images = parseDrawingImages(entries);
|
||||
for (const [cell, image] of parseRichValueImages(entries)) {
|
||||
images.set(cell, image);
|
||||
}
|
||||
for (const [cell, image] of parseWpsCellImages(entries)) {
|
||||
images.set(cell, image);
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
function valueAt(
|
||||
row: string[],
|
||||
mapping: Map<(typeof PARTNER_BATCH_HEADERS)[number], number>,
|
||||
header: (typeof PARTNER_BATCH_HEADERS)[number],
|
||||
) {
|
||||
const column = mapping.get(header);
|
||||
return column === undefined ? "" : String(row[column] ?? "").trim();
|
||||
}
|
||||
|
||||
export function parsePartnerBatchWorkbook(input: ArrayBuffer | Uint8Array) {
|
||||
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||
if (bytes.byteLength > PARTNER_BATCH_MAX_BYTES) {
|
||||
throw new Error("批量回填表不能超过80MB");
|
||||
}
|
||||
const entries = unzipSync(bytes);
|
||||
const worksheetBytes = entries["xl/worksheets/sheet1.xml"];
|
||||
if (!worksheetBytes) throw new Error("Excel 中没有可读取的批量回填工作表");
|
||||
const sharedXml = entries["xl/sharedStrings.xml"]
|
||||
? strFromU8(entries["xl/sharedStrings.xml"])
|
||||
: "";
|
||||
const sharedStrings = [
|
||||
...sharedXml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g),
|
||||
].map((match) => textNodes(match[1]));
|
||||
const rows = parseWorksheet(strFromU8(worksheetBytes), sharedStrings);
|
||||
const header = findHeader(rows);
|
||||
if (!header) {
|
||||
throw new Error("表格结构不正确,请使用本领取页面导出的批量回填表");
|
||||
}
|
||||
const images = parseImages(entries);
|
||||
const publishScreenshotColumn = header.mapping.get("笔记截图")!;
|
||||
const creatorScreenshotColumn = header.mapping.get(
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
)!;
|
||||
const result: PartnerBatchImportRow[] = [];
|
||||
for (let index = header.rowIndex + 1; index < rows.length; index += 1) {
|
||||
const row = rows[index];
|
||||
const distributionId = valueAt(row, header.mapping, "_系统笔记ID");
|
||||
if (!distributionId && !row.some((value) => String(value ?? "").trim())) continue;
|
||||
result.push({
|
||||
spreadsheetRow: index + 1,
|
||||
sequence: valueAt(row, header.mapping, "序号(不能改)"),
|
||||
title: valueAt(row, header.mapping, "标题"),
|
||||
publishUrl: valueAt(row, header.mapping, "发布链接"),
|
||||
distributionId,
|
||||
originalPublishScreenshotKey: valueAt(
|
||||
row,
|
||||
header.mapping,
|
||||
"_原笔记截图",
|
||||
),
|
||||
originalCreatorScreenshotKey: valueAt(
|
||||
row,
|
||||
header.mapping,
|
||||
"_原数据分析截图",
|
||||
),
|
||||
publishScreenshot:
|
||||
images.get(`${index + 1}:${publishScreenshotColumn}`) ?? null,
|
||||
creatorScreenshot:
|
||||
images.get(`${index + 1}:${creatorScreenshotColumn}`) ?? null,
|
||||
});
|
||||
}
|
||||
if (result.length === 0) throw new Error("表格中没有可回填的笔记");
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user