公开数据补交闭环(F1): - distributions 新增 public_metrics_screenshot_key(mysql/0022 迁移) - KOC 门户:链接已回填但采集失败时展示补交数据截图入口(kind=public-metrics) - 运营端:回收队列"核对补录"支持公开互动数据(赞/藏/评),按补交截图补录后采集状态置成功 - 企微催办:采集失败纳入待办(公开数据待补交) - task-result-image 支持 kind=publish 输出发布截图 门户文案(F2): - 自动采集数据明确"由系统每天自动采集,无需你填写",仅识别失败时才手动填写曝光/阅读 任务中心分发详情: - 任务中心/数据回收的分发表行可点击,弹窗查看详情 - 详情按任务类型适配:发布阶段/数据回收(自动采集)/创作者数据(第7天)/截图回收四区块三段式布局
268 lines
9.2 KiB
TypeScript
268 lines
9.2 KiB
TypeScript
import {
|
||
ensureSchema,
|
||
getRawDb,
|
||
getUploadBucket,
|
||
uid,
|
||
} from "../../../lib/mvp-db";
|
||
import {
|
||
partnerOptions,
|
||
withPartnerCors,
|
||
} from "../../../lib/partner-cors";
|
||
import {
|
||
MAX_RESULT_SCREENSHOTS,
|
||
parseResultScreenshotKeys,
|
||
serializeResultScreenshotKeys,
|
||
} from "../../../lib/result-screenshots";
|
||
import { creatorScreenshotMcpUrl } from "../../../lib/creator-screenshot-access";
|
||
import {
|
||
extractCreatorMetricsFromMcp,
|
||
resolveCollectionMcpConfig,
|
||
type CollectionMcpBindings,
|
||
} from "../../../lib/mcp-collection-client";
|
||
import { getRuntimeEnv } from "../../../lib/runtime-env";
|
||
|
||
async function readUpload(request: Request) {
|
||
const contentType = request.headers.get("content-type") ?? "";
|
||
if (contentType.startsWith("multipart/form-data")) {
|
||
const form = await request.formData();
|
||
const file = form.get("file");
|
||
if (!(file instanceof File)) return null;
|
||
return {
|
||
taskToken: String(form.get("taskToken") ?? "").trim(),
|
||
claimToken: String(form.get("claimToken") ?? "").trim(),
|
||
delegationToken: String(form.get("delegationToken") ?? "").trim(),
|
||
distributionId: String(form.get("distributionId") ?? "").trim(),
|
||
uploadKind: String(form.get("uploadKind") ?? "publish").trim(),
|
||
fileName: file.name,
|
||
fileType: file.type,
|
||
fileBytes: await file.arrayBuffer(),
|
||
};
|
||
}
|
||
const encodedName = request.headers.get("x-koc-file-name") ?? "screenshot.jpg";
|
||
let fileName = "screenshot.jpg";
|
||
try {
|
||
fileName = decodeURIComponent(encodedName);
|
||
} catch {
|
||
fileName = "screenshot.jpg";
|
||
}
|
||
return {
|
||
taskToken: String(request.headers.get("x-koc-task") ?? "").trim(),
|
||
claimToken: String(request.headers.get("x-koc-claim") ?? "").trim(),
|
||
delegationToken: String(
|
||
request.headers.get("x-koc-delegation") ?? "",
|
||
).trim(),
|
||
distributionId: String(
|
||
request.headers.get("x-koc-distribution") ?? "",
|
||
).trim(),
|
||
uploadKind: String(
|
||
request.headers.get("x-koc-upload-kind") ?? "publish",
|
||
).trim(),
|
||
fileName,
|
||
fileType: contentType.split(";")[0].trim(),
|
||
fileBytes: await request.arrayBuffer(),
|
||
};
|
||
}
|
||
|
||
async function handlePost(request: Request) {
|
||
try {
|
||
await ensureSchema();
|
||
const upload = await readUpload(request);
|
||
if (
|
||
!upload ||
|
||
(!upload.claimToken && !upload.delegationToken) ||
|
||
(!upload.delegationToken && !upload.taskToken) ||
|
||
!upload.distributionId ||
|
||
upload.fileBytes.byteLength === 0
|
||
) {
|
||
return Response.json({ error: "请选择需要上传的截图" }, { status: 400 });
|
||
}
|
||
if (
|
||
!upload.fileType.startsWith("image/") ||
|
||
upload.fileBytes.byteLength > 8_000_000
|
||
) {
|
||
return Response.json(
|
||
{ error: "仅支持8MB以内的图片" },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
const isCreatorCenter = upload.uploadKind === "creator-center";
|
||
const isTaskResult = upload.uploadKind === "task-result";
|
||
const isPublicMetrics = upload.uploadKind === "public-metrics";
|
||
if (
|
||
!["publish", "creator-center", "task-result", "public-metrics"].includes(
|
||
upload.uploadKind,
|
||
)
|
||
) {
|
||
return Response.json({ error: "不支持的截图类型" }, { status: 400 });
|
||
}
|
||
const assignment = upload.delegationToken
|
||
? await getRawDb()
|
||
.prepare(
|
||
`SELECT d.id, d.publish_url, d.result_screenshot_key, t.recovery_enabled
|
||
FROM distributions d
|
||
JOIN delegation_bundles b ON b.id = d.delegation_bundle_id
|
||
JOIN tasks t ON t.id = d.task_id
|
||
WHERE d.id = ?
|
||
AND b.share_token = ?
|
||
AND b.status = 'active'`,
|
||
)
|
||
.bind(upload.distributionId, upload.delegationToken)
|
||
.first<{ id: string; publish_url: string | null; result_screenshot_key: string | null; recovery_enabled: number }>()
|
||
: await getRawDb()
|
||
.prepare(
|
||
`SELECT d.id, d.publish_url, d.result_screenshot_key, t.recovery_enabled
|
||
FROM distributions d
|
||
JOIN claims c ON c.id = d.claim_id
|
||
JOIN tasks t ON t.id = d.task_id
|
||
WHERE d.id = ?
|
||
AND c.claim_token = ?
|
||
AND t.share_token = ?
|
||
AND c.task_id = t.id`,
|
||
)
|
||
.bind(upload.distributionId, upload.claimToken, upload.taskToken)
|
||
.first<{ id: string; publish_url: string | null; result_screenshot_key: string | null; recovery_enabled: number }>();
|
||
if (!assignment) {
|
||
return Response.json({ error: "笔记与领取凭证不匹配" }, { status: 403 });
|
||
}
|
||
if ((isCreatorCenter || isPublicMetrics) && !assignment.publish_url) {
|
||
return Response.json(
|
||
{ error: "请先回填这篇笔记的发布信息" },
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
if (isTaskResult && Number(assignment.recovery_enabled) !== 1) {
|
||
return Response.json({ error: "该任务未开通截图回收" }, { status: 400 });
|
||
}
|
||
const existingResultKeys = isTaskResult
|
||
? parseResultScreenshotKeys(assignment.result_screenshot_key)
|
||
: [];
|
||
if (isTaskResult && existingResultKeys.length >= MAX_RESULT_SCREENSHOTS) {
|
||
return Response.json(
|
||
{ error: `每份任务最多上传${MAX_RESULT_SCREENSHOTS}张截图` },
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
const extension =
|
||
upload.fileName.split(".").at(-1)?.replace(/[^a-zA-Z0-9]/g, "") || "jpg";
|
||
const key = isTaskResult
|
||
? `task-results/${upload.distributionId}/${uid("shot")}.${extension}`
|
||
: isCreatorCenter
|
||
? `creator-center/${upload.distributionId}/${uid("shot")}.${extension}`
|
||
: isPublicMetrics
|
||
? `public-metrics/${upload.distributionId}/${uid("shot")}.${extension}`
|
||
: `publish-evidence/${upload.distributionId}/${uid("shot")}.${extension}`;
|
||
await getUploadBucket().put(key, upload.fileBytes, {
|
||
httpMetadata: { contentType: upload.fileType },
|
||
});
|
||
if (isTaskResult) {
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
result_screenshot_key = ?,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(
|
||
serializeResultScreenshotKeys([...existingResultKeys, key]),
|
||
upload.distributionId,
|
||
)
|
||
.run();
|
||
} else if (isPublicMetrics) {
|
||
// 公开数据自动采集失败时的兜底凭证:运营据此核对补录互动数据
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
public_metrics_screenshot_key = ?,
|
||
collection_status_description = 'KOC已补交数据截图,待运营核对',
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(key, upload.distributionId)
|
||
.run();
|
||
} else if (isCreatorCenter) {
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
screenshot_key = ?,
|
||
ocr_status = CASE
|
||
WHEN exposure IS NOT NULL AND views IS NOT NULL THEN ocr_status
|
||
ELSE 'processing'
|
||
END,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(key, upload.distributionId)
|
||
.run();
|
||
try {
|
||
const metrics = await extractCreatorMetricsFromMcp(
|
||
creatorScreenshotMcpUrl(request, upload.distributionId, key),
|
||
resolveCollectionMcpConfig(
|
||
getRuntimeEnv() as unknown as CollectionMcpBindings,
|
||
),
|
||
);
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET exposure = ?, views = ?,
|
||
ocr_status = 'success', updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(metrics.exposure, metrics.views, upload.distributionId)
|
||
.run();
|
||
return Response.json({
|
||
uploaded: true,
|
||
exposure: metrics.exposure,
|
||
views: metrics.views,
|
||
ocrStatus: "success",
|
||
kind: "creator-center",
|
||
});
|
||
} catch (error) {
|
||
console.error("[KOC LOOP] creator screenshot OCR failed", {
|
||
distributionId: upload.distributionId,
|
||
detail: error instanceof Error ? error.message : String(error),
|
||
});
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET ocr_status = 'failed', updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||
)
|
||
.bind(upload.distributionId)
|
||
.run();
|
||
}
|
||
} else {
|
||
await getRawDb()
|
||
.prepare(
|
||
`UPDATE distributions SET
|
||
publish_screenshot_key = ?,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?`,
|
||
)
|
||
.bind(key, upload.distributionId)
|
||
.run();
|
||
}
|
||
return Response.json({
|
||
uploaded: true,
|
||
screenshotCount: isTaskResult ? existingResultKeys.length + 1 : undefined,
|
||
kind: isTaskResult
|
||
? "task-result"
|
||
: isCreatorCenter
|
||
? "creator-center"
|
||
: isPublicMetrics
|
||
? "public-metrics"
|
||
: "publish",
|
||
ocrStatus: isCreatorCenter ? "failed" : undefined,
|
||
});
|
||
} catch (error) {
|
||
return Response.json(
|
||
{ error: error instanceof Error ? error.message : "上传失败" },
|
||
{ status: 500 },
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function POST(request: Request) {
|
||
return withPartnerCors(request, await handlePost(request));
|
||
}
|
||
|
||
export async function OPTIONS(request: Request) {
|
||
return partnerOptions(request);
|
||
}
|