feat: 完善视频任务与 KOC 资源库
This commit is contained in:
327
tests/partner-batch-workbook.test.mjs
Normal file
327
tests/partner-batch-workbook.test.mjs
Normal file
@@ -0,0 +1,327 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
|
||||
import { buildRecoveryWorkbook } from "../lib/recovery-workbook.ts";
|
||||
import {
|
||||
PARTNER_BATCH_HEADERS,
|
||||
buildPartnerBatchWorkbookColumns,
|
||||
parsePartnerBatchWorkbook,
|
||||
resolvePartnerWorkbookOrigin,
|
||||
} from "../lib/partner-batch-workbook.ts";
|
||||
import { hasMp4FileSignature } from "../lib/video-file.ts";
|
||||
|
||||
const png = Uint8Array.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
]);
|
||||
|
||||
test("round-trips hidden assignment IDs and embedded backfill screenshots", () => {
|
||||
const workbook = buildRecoveryWorkbook({
|
||||
sheetName: "批量回填",
|
||||
headers: [...PARTNER_BATCH_HEADERS],
|
||||
columnWidths: [10, 20, 40, 30, 40, 24, 28, 20, 20, 20],
|
||||
hiddenColumns: [7, 8, 9],
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
1,
|
||||
"测试笔记",
|
||||
"正文 #话题",
|
||||
"",
|
||||
"https://www.xiaohongshu.com/explore/1234567890abcdef",
|
||||
"",
|
||||
"",
|
||||
"distribution-1",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
images: [
|
||||
{
|
||||
column: 3,
|
||||
image: { bytes: png, contentType: "image/png", description: "原图" },
|
||||
},
|
||||
{
|
||||
column: 5,
|
||||
image: { bytes: png, contentType: "image/png", description: "笔记截图" },
|
||||
},
|
||||
{
|
||||
column: 6,
|
||||
image: { bytes: png, contentType: "image/png", description: "数据截图" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const rows = parsePartnerBatchWorkbook(workbook);
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].distributionId, "distribution-1");
|
||||
assert.equal(rows[0].title, "测试笔记");
|
||||
assert.match(rows[0].publishUrl, /xiaohongshu\.com/);
|
||||
assert.equal(rows[0].publishScreenshot?.contentType, "image/png");
|
||||
assert.equal(rows[0].creatorScreenshot?.contentType, "image/png");
|
||||
|
||||
const entries = unzipSync(workbook);
|
||||
const worksheet = strFromU8(entries["xl/worksheets/sheet1.xml"]);
|
||||
assert.match(worksheet, /序号(不能改)/);
|
||||
assert.doesNotMatch(worksheet, /张原图(见图)|已回填(见图)|请插入笔记截图/);
|
||||
assert.match(worksheet, /<drawing r:id="rId1"\/>/);
|
||||
assert.doesNotMatch(worksheet, /#VALUE!/);
|
||||
const drawing = strFromU8(entries["xl/drawings/drawing1.xml"]);
|
||||
assert.equal((drawing.match(/<xdr:twoCellAnchor editAs="twoCell">/g) ?? []).length, 3);
|
||||
assert.match(drawing, /<xdr:col>3<\/xdr:col>/);
|
||||
assert.match(drawing, /<xdr:col>5<\/xdr:col>/);
|
||||
assert.match(drawing, /<xdr:col>6<\/xdr:col>/);
|
||||
assert.match(worksheet, /min="8" max="8"[^>]*hidden="1"/);
|
||||
});
|
||||
|
||||
test("accepts the legacy sequence header for previously exported workbooks", () => {
|
||||
const legacyHeaders = [...PARTNER_BATCH_HEADERS];
|
||||
legacyHeaders[0] = "序号";
|
||||
const workbook = buildRecoveryWorkbook({
|
||||
sheetName: "批量回填",
|
||||
headers: legacyHeaders,
|
||||
columnWidths: [10, 20, 40, 30, 40, 24, 28, 20, 20, 20],
|
||||
hiddenColumns: [7, 8, 9],
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
1,
|
||||
"旧模板笔记",
|
||||
"正文",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"distribution-legacy",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
images: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const rows = parsePartnerBatchWorkbook(workbook);
|
||||
assert.equal(rows[0].sequence, "1");
|
||||
assert.equal(rows[0].distributionId, "distribution-legacy");
|
||||
});
|
||||
|
||||
test("imports dynamic source image columns without confusing screenshot columns", () => {
|
||||
const headers = [
|
||||
"序号(不能改)",
|
||||
"标题",
|
||||
"笔记内容(正文+话题)",
|
||||
"图片1",
|
||||
"图片2",
|
||||
"发布链接",
|
||||
"笔记截图",
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
"_系统笔记ID",
|
||||
"_原笔记截图",
|
||||
"_原数据分析截图",
|
||||
];
|
||||
const workbook = buildRecoveryWorkbook({
|
||||
sheetName: "批量回填",
|
||||
headers,
|
||||
columnWidths: headers.map(() => 20),
|
||||
hiddenColumns: [8, 9, 10],
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
1,
|
||||
"多图笔记",
|
||||
"正文",
|
||||
"",
|
||||
"",
|
||||
"https://www.xiaohongshu.com/explore/dynamic",
|
||||
"",
|
||||
"",
|
||||
"distribution-dynamic",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
images: [
|
||||
{
|
||||
column: 3,
|
||||
image: { bytes: png, contentType: "image/png", description: "原图1" },
|
||||
},
|
||||
{
|
||||
column: 4,
|
||||
image: { bytes: png, contentType: "image/png", description: "原图2" },
|
||||
},
|
||||
{
|
||||
column: 6,
|
||||
image: { bytes: png, contentType: "image/png", description: "笔记截图" },
|
||||
},
|
||||
{
|
||||
column: 7,
|
||||
image: { bytes: png, contentType: "image/png", description: "数据截图" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const rows = parsePartnerBatchWorkbook(workbook);
|
||||
assert.equal(rows[0].distributionId, "distribution-dynamic");
|
||||
assert.equal(rows[0].publishScreenshot?.contentType, "image/png");
|
||||
assert.equal(rows[0].creatorScreenshot?.contentType, "image/png");
|
||||
});
|
||||
|
||||
test("imports screenshots saved by WPS as DISPIMG cell images", () => {
|
||||
const sourceImage = Uint8Array.from([...png, 1]);
|
||||
const publishScreenshot = Uint8Array.from([...png, 2]);
|
||||
const creatorScreenshot = Uint8Array.from([...png, 3]);
|
||||
const worksheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>
|
||||
<row r="1">
|
||||
<c r="A1" t="inlineStr"><is><t>序号(不能改)</t></is></c>
|
||||
<c r="B1" t="inlineStr"><is><t>标题</t></is></c>
|
||||
<c r="C1" t="inlineStr"><is><t>笔记内容(正文+话题)</t></is></c>
|
||||
<c r="D1" t="inlineStr"><is><t>图片1</t></is></c>
|
||||
<c r="E1" t="inlineStr"><is><t>发布链接</t></is></c>
|
||||
<c r="F1" t="inlineStr"><is><t>笔记截图</t></is></c>
|
||||
<c r="G1" t="inlineStr"><is><t>数据分析截图(单篇笔记数据分析截图)</t></is></c>
|
||||
<c r="H1" t="inlineStr"><is><t>_系统笔记ID</t></is></c>
|
||||
<c r="I1" t="inlineStr"><is><t>_原笔记截图</t></is></c>
|
||||
<c r="J1" t="inlineStr"><is><t>_原数据分析截图</t></is></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2"><v>1</v></c>
|
||||
<c r="B2" t="inlineStr"><is><t>WPS 笔记</t></is></c>
|
||||
<c r="D2" t="str"><f>_xlfn.DISPIMG("SOURCE",1)</f><v>=DISPIMG("SOURCE",1)</v></c>
|
||||
<c r="E2" t="inlineStr"><is><t>https://www.xiaohongshu.com/explore/wps</t></is></c>
|
||||
<c r="F2" t="str"><f>_xlfn.DISPIMG("PUBLISH",1)</f><v>=DISPIMG("PUBLISH",1)</v></c>
|
||||
<c r="G2" t="str"><f>_xlfn.DISPIMG("CREATOR",1)</f><v>=DISPIMG("CREATOR",1)</v></c>
|
||||
<c r="H2" t="inlineStr"><is><t>distribution-wps</t></is></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>`;
|
||||
const cellImages = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="SOURCE"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage>
|
||||
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="2" name="PUBLISH"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId2"/></xdr:blipFill></xdr:pic></etc:cellImage>
|
||||
<etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="3" name="CREATOR"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId3"/></xdr:blipFill></xdr:pic></etc:cellImage>
|
||||
</etc:cellImages>`;
|
||||
const relationships = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/source.png"/>
|
||||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/publish.png"/>
|
||||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/creator.png"/>
|
||||
</Relationships>`;
|
||||
const workbook = zipSync({
|
||||
"xl/worksheets/sheet1.xml": strToU8(worksheet),
|
||||
"xl/cellimages.xml": strToU8(cellImages),
|
||||
"xl/_rels/cellimages.xml.rels": strToU8(relationships),
|
||||
"xl/media/source.png": sourceImage,
|
||||
"xl/media/publish.png": publishScreenshot,
|
||||
"xl/media/creator.png": creatorScreenshot,
|
||||
});
|
||||
|
||||
const rows = parsePartnerBatchWorkbook(workbook);
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].distributionId, "distribution-wps");
|
||||
assert.deepEqual(rows[0].publishScreenshot?.bytes, publishScreenshot);
|
||||
assert.deepEqual(rows[0].creatorScreenshot?.bytes, creatorScreenshot);
|
||||
});
|
||||
|
||||
test("builds video-task workbooks with video columns and no source image columns", () => {
|
||||
const columns = buildPartnerBatchWorkbookColumns({
|
||||
contentFormat: "video",
|
||||
maxSourceImages: 3,
|
||||
maxSourceVideos: 2,
|
||||
});
|
||||
assert.deepEqual(columns.headers.slice(0, 8), [
|
||||
"序号(不能改)",
|
||||
"标题",
|
||||
"笔记内容(正文+话题)",
|
||||
"视频1",
|
||||
"视频2",
|
||||
"发布链接",
|
||||
"笔记截图",
|
||||
"数据分析截图(单篇笔记数据分析截图)",
|
||||
]);
|
||||
assert.equal(columns.headers.some((header) => /^图片\d+$/.test(header)), false);
|
||||
|
||||
const workbook = buildRecoveryWorkbook({
|
||||
sheetName: "视频批量回填",
|
||||
headers: columns.headers,
|
||||
columnWidths: columns.columnWidths,
|
||||
hiddenColumns: [
|
||||
columns.systemColumn,
|
||||
columns.systemColumn + 1,
|
||||
columns.systemColumn + 2,
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
cells: [
|
||||
1,
|
||||
"视频笔记",
|
||||
"视频正文 #测试",
|
||||
"下载视频1",
|
||||
"下载视频2",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"distribution-video",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
images: [],
|
||||
hyperlinks: [
|
||||
{
|
||||
column: columns.sourceVideoStartColumn,
|
||||
url: "https://koc.example.com/api/partner-image?kind=video&download=1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const entries = unzipSync(workbook);
|
||||
const worksheet = strFromU8(entries["xl/worksheets/sheet1.xml"]);
|
||||
const relationships = strFromU8(
|
||||
entries["xl/worksheets/_rels/sheet1.xml.rels"],
|
||||
);
|
||||
assert.match(worksheet, /视频1/);
|
||||
assert.doesNotMatch(worksheet, /图片1/);
|
||||
assert.match(relationships, /https:\/\/koc\.example\.com\/api\/partner-image/);
|
||||
assert.match(relationships, /download=1/);
|
||||
});
|
||||
|
||||
test("uses the configured public origin before proxy or container addresses", () => {
|
||||
const request = new Request("http://app:3000/api/partner-batch-workbook", {
|
||||
headers: {
|
||||
host: "app:3000",
|
||||
"x-forwarded-host": "internal-proxy:8080",
|
||||
"x-forwarded-proto": "https",
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
resolvePartnerWorkbookOrigin(request, "https://koc.example.com/koc/"),
|
||||
"https://koc.example.com",
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves a forwarded non-standard port when no origin is configured", () => {
|
||||
const request = new Request("http://app:3000/api/partner-batch-workbook", {
|
||||
headers: {
|
||||
host: "app:3000",
|
||||
"x-forwarded-host": "localhost:8080",
|
||||
"x-forwarded-proto": "http",
|
||||
},
|
||||
});
|
||||
assert.equal(resolvePartnerWorkbookOrigin(request), "http://localhost:8080");
|
||||
});
|
||||
|
||||
test("recognizes MP4 bytes instead of trusting a response content type", () => {
|
||||
const mp4Header = Uint8Array.from([
|
||||
0x00, 0x00, 0x00, 0x18,
|
||||
0x66, 0x74, 0x79, 0x70,
|
||||
0x69, 0x73, 0x6f, 0x6d,
|
||||
0x00, 0x00, 0x02, 0x00,
|
||||
0x69, 0x73, 0x6f, 0x6d,
|
||||
0x6d, 0x70, 0x34, 0x32,
|
||||
]);
|
||||
assert.equal(hasMp4FileSignature(mp4Header), true);
|
||||
assert.equal(hasMp4FileSignature(new TextEncoder().encode("not a video")), false);
|
||||
});
|
||||
Reference in New Issue
Block a user