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, //); assert.doesNotMatch(worksheet, /#VALUE!/); const drawing = strFromU8(entries["xl/drawings/drawing1.xml"]); assert.equal((drawing.match(//g) ?? []).length, 3); assert.match(drawing, /3<\/xdr:col>/); assert.match(drawing, /5<\/xdr:col>/); assert.match(drawing, /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 = ` 序号(不能改) 标题 笔记内容(正文+话题) 图片1 发布链接 笔记截图 数据分析截图(单篇笔记数据分析截图) _系统笔记ID _原笔记截图 _原数据分析截图 1 WPS 笔记 _xlfn.DISPIMG("SOURCE",1)=DISPIMG("SOURCE",1) https://www.xiaohongshu.com/explore/wps _xlfn.DISPIMG("PUBLISH",1)=DISPIMG("PUBLISH",1) _xlfn.DISPIMG("CREATOR",1)=DISPIMG("CREATOR",1) distribution-wps `; const cellImages = ` `; const 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); });