feat: ship self-hosted KOC LOOP workflows

This commit is contained in:
巫凤萍
2026-08-11 23:07:26 +08:00
parent 1f1887c860
commit ddad4b7659
88 changed files with 6056 additions and 8938 deletions

View File

@@ -1,5 +1,7 @@
import { env } from "cloudflare:workers";
import { getRequestExecutionContext } from "vinext/shims/request-context";
import { getRuntimeEnv } from "../../../lib/runtime-env";
import { runInBackground } from "../../../lib/background";
const env = getRuntimeEnv();
import { backfillAccountProfiles } from "../../../lib/account-enrichment-service";
import {
ensureSchema,
@@ -24,9 +26,10 @@ import {
readFeishuSource,
type FeishuBindings,
} from "../../../lib/feishu-client";
import { createDistributionTask } from "../../../lib/task-service";
export const runtime = "edge";
import {
createDistributionTask,
createScreenshotTask,
} from "../../../lib/task-service";
type ActionBody = {
action?: string;
@@ -79,6 +82,16 @@ export async function POST(request: Request) {
},
env as unknown as FeishuBindings,
);
} else if (body.action === "create_screenshot_task") {
await createScreenshotTask({
name: String(body.name ?? "").trim(),
brand: String(body.brand ?? "").trim(),
dueAt: String(body.dueAt ?? "").trim(),
keyword: String(body.keyword ?? "").trim(),
instructions: String(body.instructions ?? "").trim(),
quantity: numberValue(body.quantity),
exampleImageKey: String(body.exampleImageKey ?? "").trim(),
});
} else if (body.action === "claim") {
const partnerId = String(body.partnerId ?? "");
const taskId = String(body.taskId ?? "");
@@ -88,9 +101,9 @@ export async function POST(request: Request) {
`SELECT id FROM contents
WHERE task_id = ? AND status = 'available'
ORDER BY created_at, id
LIMIT ?`,
LIMIT ${quantity}`,
)
.bind(taskId, quantity)
.bind(taskId)
.all<{ id: string }>();
if (available.results.length === 0) {
return Response.json({ error: "当前没有可领取内容" }, { status: 409 });
@@ -146,12 +159,18 @@ export async function POST(request: Request) {
);
}
const task = await db
.prepare("SELECT id FROM tasks WHERE id = ?")
.prepare("SELECT id, task_type FROM tasks WHERE id = ?")
.bind(taskId)
.first<{ id: string }>();
.first<{ id: string; task_type?: string | null }>();
if (!task) {
return Response.json({ error: "任务不存在" }, { status: 404 });
}
if (task.task_type === "screenshot_collect") {
return Response.json(
{ error: "截图回收任务不需要设置数据采集计划" },
{ status: 400 },
);
}
await db.batch([
db
.prepare(
@@ -193,12 +212,7 @@ export async function POST(request: Request) {
"catchup",
taskId,
).catch(() => undefined);
const executionContext = getRequestExecutionContext();
if (executionContext) {
executionContext.waitUntil(catchup);
} else {
await catchup;
}
runInBackground(catchup, "collection catchup");
} else if (body.action === "run_due_collections") {
await runDueScheduledCollections(
db,
@@ -224,6 +238,24 @@ export async function POST(request: Request) {
if (body.action === "collect" && day === null) {
return Response.json({ error: "采集周期无效" }, { status: 400 });
}
const distribution = await db
.prepare(
`SELECT t.task_type
FROM distributions d
JOIN tasks t ON t.id = d.task_id
WHERE d.id = ?`,
)
.bind(distributionId)
.first<{ task_type?: string | null }>();
if (!distribution) {
return Response.json({ error: "笔记记录不存在" }, { status: 404 });
}
if (distribution.task_type === "screenshot_collect") {
return Response.json(
{ error: "截图回收任务不支持公开数据采集" },
{ status: 400 },
);
}
await collectDistributionMetrics(
db,
distributionId,
@@ -239,6 +271,19 @@ export async function POST(request: Request) {
if (!taskId) {
return Response.json({ error: "请选择需要补采的任务" }, { status: 400 });
}
const task = await db
.prepare("SELECT task_type FROM tasks WHERE id = ?")
.bind(taskId)
.first<{ task_type?: string | null }>();
if (!task) {
return Response.json({ error: "任务不存在" }, { status: 404 });
}
if (task.task_type === "screenshot_collect") {
return Response.json(
{ error: "截图回收任务没有需要补采的公开数据" },
{ status: 400 },
);
}
await retryFailedCollections(
db,
taskId,
@@ -254,12 +299,7 @@ export async function POST(request: Request) {
),
Math.max(1, Math.min(25, numberValue(body.limit, 10))),
).catch(() => undefined);
const executionContext = getRequestExecutionContext();
if (executionContext) {
executionContext.waitUntil(backfill);
} else {
await backfill;
}
runInBackground(backfill, "account profile backfill");
} else if (body.action === "set_public_account_ids") {
const items = Array.isArray(body.items) ? body.items.slice(0, 50) : [];
const normalized = items