feat: 完善视频任务与 KOC 资源库

This commit is contained in:
巫凤萍
2026-08-15 03:53:09 +08:00
parent ad3dbdcc86
commit f37d05dd88
66 changed files with 6633 additions and 558 deletions

View File

@@ -23,6 +23,30 @@ test("builds the KOC LOOP product shell", async () => {
await access(new URL("../.next/static", import.meta.url));
});
test("keeps distribution filters as always-visible fuzzy search fields", async () => {
const [adminApp, globalCss] = await Promise.all([
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
readFile(new URL("../app/globals.css", import.meta.url), "utf8"),
]);
assert.match(globalCss, /\.distribution-task-search\s*\{[^}]*flex-direction:\s*row/s);
assert.match(globalCss, /\.distribution-task-search\s*\{[^}]*margin:\s*0/s);
assert.match(globalCss, /\.distribution-task-search\s*>\s*span\s*\{[^}]*flex:\s*0\s+0\s+18px/s);
assert.match(globalCss, /\.distribution-task-filter-combobox\.brand\s*\{[^}]*flex-basis:\s*176px/s);
assert.match(globalCss, /\.distribution-task-filter-menu\s*\{[^}]*position:\s*absolute[^}]*z-index:\s*60/s);
assert.match(globalCss, /\.distribution-task-filter-input\s*\{[^}]*display:\s*flex[^}]*margin:\s*0/s);
assert.match(adminApp, /aria-expanded=\{openTaskFilter === filter\.key\}/);
assert.match(adminApp, /role="combobox"/);
assert.match(adminApp, /role="listbox"/);
assert.match(adminApp, /placeholder=\{`搜索\$\{filter\.label\}`\}/);
assert.match(adminApp, /setTaskFilterValue\(filter\.key, event\.target\.value\)/);
assert.doesNotMatch(adminApp, /distribution-task-filter-trigger/);
assert.doesNotMatch(adminApp, /distribution-task-filter-menu-search/);
assert.doesNotMatch(adminApp, /taskFilterQuery/);
assert.doesNotMatch(adminApp, /distribution-task-option-panel/);
assert.doesNotMatch(adminApp, /<select value=\{contentTypeFilter\}/);
});
test("stacks user management and securely removes departed accounts", async () => {
const [usersPage, usersRoute, globalCss] = await Promise.all([
readFile(new URL("../app/users-page.tsx", import.meta.url), "utf8"),
@@ -137,8 +161,8 @@ test("issues external task links and supports one-to-one note submissions", asyn
assert.match(partnerRoute, /claimantIdentifier\.canonical/);
assert.match(partnerRoute, /legacyPartnerId/);
assert.match(partnerRoute, /微信号或手机号/);
assert.match(partnerRoute, /extractXhsPublishUrl/);
assert.match(partnerRoute, /小红书长链或短链/);
assert.match(partnerRoute, /extractPublishUrl/);
assert.match(partnerRoute, /task\.platform/);
assert.match(partnerRoute, /请填写发布链接/);
assert.doesNotMatch(partnerRoute, /请填写发布账号昵称和发布链接/);
assert.match(partnerRoute, /没有找到领取记录/);
@@ -147,11 +171,16 @@ test("issues external task links and supports one-to-one note submissions", asyn
assert.match(partnerRoute, /enrichDistributionAccount/);
assert.match(partnerRoute, /runInBackground\(enrichment/);
assert.match(accountEnrichment, /profile_url = excluded\.profile_url/);
assert.match(accountEnrichment, /resolveXhsAccountProfileFromMcp/);
assert.match(accountEnrichment, /resolveXhsProfileDetailsFromMcp/);
assert.match(accountEnrichment, /resolveAccountProfileFromMcp/);
assert.match(accountEnrichment, /resolveProfileDetailsFromMcp/);
assert.match(accountEnrichment, /isVerifiedXhsProfileUrl/);
assert.match(accountEnrichment, /endsWith\("\.xiaohongshu\.com"\)/);
assert.match(accountEnrichment, /DELETE FROM accounts/);
assert.match(accountEnrichment, /WHERE platform = \? AND platform_uid = \?/);
assert.match(accountEnrichment, /existingAccount\?\.id \|\| canonicalAccountId/);
assert.match(accountEnrichment, /cl\.claimant_name AS claimant_contact/);
assert.match(accountEnrichment, /current_contact = CASE/);
assert.match(accountEnrichment, /!row\.resolved_account_id/);
assert.match(accountEnrichment, /a\.followers/);
assert.match(accountEnrichment, /followers = CASE/);
assert.match(accountEnrichment, /Number\(row\.followers \?\? 0\) === 0/);
@@ -171,6 +200,9 @@ test("issues external task links and supports one-to-one note submissions", asyn
assert.match(imageRoute, /creator-center\//);
assert.match(imageRoute, /imageKind === "publish"/);
assert.match(imageRoute, /imageKind === "creator"/);
assert.match(imageRoute, /Content-Type", "video\/mp4"/);
assert.match(imageRoute, /video-\$\{imageIndex\}\.mp4/);
assert.match(imageRoute, /downloadRequested \? "attachment" : "inline"/);
assert.match(imageRoute, /cl\.claim_token/);
assert.match(imageUploadRoute, /isAdminRequest/);
assert.match(cors, /KOC_PORTAL_URL/);
@@ -178,6 +210,17 @@ test("issues external task links and supports one-to-one note submissions", asyn
assert.match(cors, /X-KOC-Upload-Kind/);
assert.match(cors, /Access-Control-Allow-Origin/);
assert.match(adminApp, /hasCreatorMetrics/);
assert.match(adminApp, /function PlatformBadge/);
assert.match(adminApp, /function resourceProfileLink/);
assert.match(adminApp, /搜索主页/);
assert.match(adminApp, /latest_publish_url/);
assert.match(adminApp, /通过作品查看主页/);
assert.match(adminApp, /platform-logo/);
assert.match(adminApp, /按任务名称模糊搜索/);
assert.match(adminApp, /全部品牌\/项目/);
assert.match(adminApp, /全部内容类型/);
assert.match(adminApp, /全部平台/);
assert.match(adminApp, /task-scope-subline/);
assert.match(adminApp, /待KOC填写数据/);
assert.match(adminApp, /AdminImageLightbox/);
assert.match(adminApp, /CreatorScreenshotPreview/);
@@ -195,6 +238,43 @@ test("issues external task links and supports one-to-one note submissions", asyn
assert.match(migration, /claim_token/);
});
test("exports and imports claim-bound Excel backfill workbooks", async () => {
const [route, parser, workbook, imageNormalizer, nginx] = await Promise.all([
readFile(new URL("../app/api/partner-batch-workbook/route.ts", import.meta.url), "utf8"),
readFile(new URL("../lib/partner-batch-workbook.ts", import.meta.url), "utf8"),
readFile(new URL("../lib/recovery-workbook.ts", import.meta.url), "utf8"),
readFile(new URL("../lib/workbook-image.ts", import.meta.url), "utf8"),
readFile(new URL("../deploy/nginx/koc-loop.conf", import.meta.url), "utf8"),
]);
assert.match(route, /批量回填/);
assert.match(route, /publish-evidence/);
assert.match(route, /creator-center/);
assert.match(route, /extractPublishUrl/);
assert.match(route, /findAccess/);
assert.match(route, /isDifferentFromStoredImage/);
assert.match(route, /rowIndex \+ 1/);
assert.match(route, /buildPartnerBatchWorkbookColumns/);
assert.match(route, /resolvePartnerWorkbookOrigin/);
assert.match(route, /download: "1"/);
assert.match(route, /columns\.sourceImageStartColumn \+ index/);
assert.match(parser, /_系统笔记ID/);
assert.match(parser, /笔记截图/);
assert.match(parser, /数据分析截图(单篇笔记数据分析截图)/);
assert.match(parser, /parseImages/);
assert.match(workbook, /hiddenColumns/);
assert.match(workbook, /offsetX/);
assert.doesNotMatch(workbook, /value \|\| "见图"/);
assert.match(imageNormalizer, /\.rotate\(\)/);
assert.match(imageNormalizer, /\.png\(/);
assert.match(nginx, /client_max_body_size 85m/);
assert.equal((nginx.match(/proxy_set_header Host \$http_host;/g) ?? []).length, 2);
assert.equal(
(nginx.match(/proxy_set_header X-Forwarded-Host \$http_host;/g) ?? []).length,
2,
);
});
test("supports task collection schedules and latest public metrics", async () => {
const [
adminApp,
@@ -206,6 +286,7 @@ test("supports task collection schedules and latest public metrics", async () =>
migration,
accountMigration,
compose,
mcpClient,
] = await Promise.all([
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
readFile(new URL("../app/api/action/route.ts", import.meta.url), "utf8"),
@@ -216,6 +297,7 @@ test("supports task collection schedules and latest public metrics", async () =>
readFile(new URL("../mysql/0001_init.sql", import.meta.url), "utf8"),
readFile(new URL("../mysql/0001_init.sql", import.meta.url), "utf8"),
readFile(new URL("../docker-compose.self-hosted.yml", import.meta.url), "utf8"),
readFile(new URL("../lib/mcp-collection-client.ts", import.meta.url), "utf8"),
]);
for (const label of [
@@ -238,12 +320,19 @@ test("supports task collection schedules and latest public metrics", async () =>
assert.match(adminApp, /sortWithNullsLast/);
assert.match(adminApp, /内容 \/ 发布账号/);
assert.match(adminApp, /recovery-title-link/);
assert.match(adminApp, /打开小红书笔记/);
assert.match(adminApp, /打开\$\{selectedTask\.platform\}作品/);
assert.match(adminApp, /target="_blank"/);
assert.match(adminApp, /noopener noreferrer/);
assert.match(adminApp, /const noteUrl = xhsPublishUrl\(item\.publish_url\)/);
assert.match(adminApp, /const noteUrl = publicPublishUrl\(item\.publish_url\)/);
assert.match(adminApp, /noteUrl \? \(/);
assert.match(adminApp, /updateDistributionPublishUrl/);
assert.match(adminApp, /填写链接/);
assert.match(adminApp, /更新链接/);
assert.match(adminApp, /hostname === "xhslink\.cn"/);
assert.match(actionRoute, /update_distribution_publish_url/);
assert.match(actionRoute, /extractPublishUrl/);
assert.match(actionRoute, /DELETE FROM collection_runs WHERE distribution_id = \?/);
assert.match(actionRoute, /enrichDistributionAccount/);
assert.match(actionRoute, /save_collection_schedule/);
assert.match(actionRoute, /collect_now/);
assert.match(actionRoute, /backfill_account_profiles/);
@@ -253,7 +342,11 @@ test("supports task collection schedules and latest public metrics", async () =>
assert.match(actionRoute, /createCollectionRunTasks/);
assert.match(bootstrapRoute, /runDueScheduledCollections/);
assert.match(collectionService, /INSERT OR IGNORE INTO collection_runs/);
assert.match(collectionService, /collectXhsMetricsFromMcp/);
assert.match(
collectionService,
/run\.status === "success" && source !== "manual"/,
);
assert.match(collectionService, /collectMetricsFromMcp/);
assert.doesNotMatch(collectionService, /hashText/);
assert.match(collectionService, /runScheduledCollections/);
assert.match(collectionService, /runDueScheduledCollections/);
@@ -272,6 +365,10 @@ test("supports task collection schedules and latest public metrics", async () =>
assert.match(migration, /collection_runs_distribution_date_idx/);
assert.match(accountMigration, /public_account_id/);
assert.match(compose, /ENABLE_SCHEDULER/);
assert.match(mcpClient, /"fetch_content_detail"/);
assert.match(mcpClient, /"parse_xhs_user_summary"/);
assert.doesNotMatch(mcpClient, /"parse_xhs_note"/);
assert.doesNotMatch(mcpClient, /"collect_xhs_wen_note_detail"/);
});
test("supports fixed screenshot collection tasks without publish metrics", async () => {
@@ -314,7 +411,7 @@ test("exports complete task recovery data to Excel with embedded images", async
assert.match(adminApp, /导出全部数据/);
assert.match(adminApp, /\/api\/recovery-export/);
assert.match(adminApp, /图片和截图已嵌入表格/);
assert.match(exportRoute, /小红书昵称/);
assert.match(exportRoute, /`\$\{task\.platform\}昵称`/);
assert.match(exportRoute, /曝光量-实际第7天/);
assert.match(exportRoute, /阅读量-实际第7天/);
assert.match(exportRoute, /publish_screenshot_key/);
@@ -323,8 +420,9 @@ test("exports complete task recovery data to Excel with embedded images", async
assert.match(exportRoute, /isAdminRequest/);
assert.match(exportRoute, /consumeMcpExportToken/);
assert.match(workbook, /xl\/drawings\/drawing1\.xml/);
assert.match(workbook, /twoCellAnchor editAs="twoCell"/);
assert.match(workbook, /xl\/media\/image/);
assert.match(workbook, /oneCellAnchor/);
assert.match(workbook, /relationships\/image/);
});
test("provides simple username-password login and three server-enforced roles", async () => {
@@ -393,6 +491,7 @@ test("filters and exports the current KOC resource result set", async () => {
]);
assert.match(adminApp, /搜索账号名称 \/ 账号ID/);
assert.match(adminApp, /当前联系人/);
assert.match(adminApp, /搜索IP地区/);
assert.match(adminApp, /搜索合作来源/);
assert.match(adminApp, /ipLocation\.includes\(ipKeyword\)/);
@@ -401,6 +500,7 @@ test("filters and exports the current KOC resource result set", async () => {
assert.match(adminApp, /filteredAccounts\.map\(\(item\) => item\.account\.id\)/);
assert.match(exportRoute, /小红书号\/抖音号/);
assert.match(exportRoute, /历史合作来源/);
assert.match(exportRoute, /当前联系人/);
assert.match(exportRoute, /合作社资源 · 不可直联/);
assert.match(exportRoute, /isManagerRequest/);
assert.match(exportRoute, /consumeMcpExportToken/);
@@ -408,21 +508,60 @@ test("filters and exports the current KOC resource result set", async () => {
});
test("imports existing KOC resources through a validated spreadsheet preview", async () => {
const [adminApp, importRoute, resourceParser, accountMigration] = await Promise.all([
const [adminApp, globalCss, importRoute, resourceParser, accountMigration, profileMigration, contactMigration] = await Promise.all([
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
readFile(new URL("../app/globals.css", import.meta.url), "utf8"),
readFile(new URL("../app/api/resources-import/route.ts", import.meta.url), "utf8"),
readFile(new URL("../lib/resource-import.ts", import.meta.url), "utf8"),
readFile(new URL("../mysql/0002_resource_import.sql", import.meta.url), "utf8"),
readFile(new URL("../mysql/0006_account_profile_tags.sql", import.meta.url), "utf8"),
readFile(new URL("../mysql/0007_account_current_contact.sql", import.meta.url), "utf8"),
]);
assert.match(adminApp, /下载导入模板/);
assert.match(adminApp, /校验并预览/);
assert.match(adminApp, /确认导入/);
assert.match(importRoute, /isManagerRequest/);
assert.match(importRoute, /mode !== "commit"/);
assert.match(resourceParser, /RESOURCE_IMPORT_MAX_ROWS = 100/);
assert.match(resourceParser, /当前自动解析仅支持小红书账号主页/);
assert.match(importRoute, /resolveXhsProfileDetailsFromMcp/);
assert.match(resourceParser, /RESOURCE_IMPORT_MAX_ROWS = 10_000/);
assert.match(resourceParser, /RESOURCE_IMPORT_MAX_BYTES = 20 \* 1024 \* 1024/);
assert.match(adminApp, /单次最多 10,000 个账号,文件不超过 20MB/);
assert.match(importRoute, /RESOURCE_IMPORT_DB_BATCH_SIZE = 100/);
assert.match(importRoute, /bulk resource profile enrichment/);
assert.match(adminApp, /异常数据将自动跳过,不会导入/);
assert.match(
adminApp,
/summary\.create \+ importPreview\.summary\.update === 0 \|\| importWorking/,
);
assert.doesNotMatch(
adminApp,
/disabled=\{importPreview\.summary\.error > 0 \|\| importWorking\}/,
);
assert.match(importRoute, /const importableRows = analyzed\.filter/);
assert.match(importRoute, /跳过 \$\{summary\.error\} 条异常数据/);
assert.match(importRoute, /previewAnalyzedRows\(analyzed\)/);
assert.match(resourceParser, /当前自动解析仅支持小红书或抖音账号主页/);
assert.match(importRoute, /resolveProfileDetailsFromMcp/);
assert.match(accountMigration, /cooperation_source/);
assert.match(profileMigration, /ADD COLUMN gender/);
assert.match(profileMigration, /ADD COLUMN bio/);
assert.match(profileMigration, /ADD COLUMN tags/);
assert.match(contactMigration, /ADD COLUMN current_contact/);
assert.match(adminApp, /gender-icon male/);
assert.match(adminApp, /gender-icon female/);
assert.match(adminApp, /resource-profile-avatar/);
assert.match(adminApp, /resource-account-number/);
assert.match(adminApp, /resource-platform-line/);
assert.match(adminApp, /resource-latest/);
assert.match(adminApp, /合作来源/);
assert.match(adminApp, /待打标/);
assert.doesNotMatch(adminApp, /className="verified-dot"/);
assert.match(globalCss, /\.resource-tags\s*\{[^}]*margin-bottom:\s*auto/s);
assert.match(globalCss, /\.resource-card-foot\s*\{[^}]*margin-top:\s*10px/s);
assert.match(adminApp, /resource-tags/);
assert.match(adminApp, /最多 5 个/);
assert.match(resourceParser, /gender: \["性别"\]/);
assert.match(resourceParser, /bio: \["简介"/);
assert.match(resourceParser, /tags: \["标签"/);
});
test("supports anonymous partner delegation without creating a second data flow", async () => {