548 lines
18 KiB
TypeScript
548 lines
18 KiB
TypeScript
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;
|
||
}
|