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,26 +1,80 @@
import { env } from "cloudflare:workers";
import { type DatabaseClient, getDatabase } from "./database";
import { getObjectStore } from "./object-store";
import { getRuntimeEnv, isEnabled } from "./runtime-env";
import feishuSnapshot from "./feishu-source-snapshot.json";
type D1ResultRow = Record<string, unknown>;
export function getRawDb(): D1Database {
const database = (env as unknown as { DB?: D1Database }).DB;
if (!database) {
throw new Error("数据库尚未连接");
}
return database;
export function getRawDb(): DatabaseClient {
return getDatabase();
}
export function getUploadBucket(): R2Bucket {
const bucket = (env as unknown as { UPLOADS?: R2Bucket }).UPLOADS;
if (!bucket) {
throw new Error("文件存储尚未连接");
}
return bucket;
export function getUploadBucket() {
return getObjectStore();
}
export async function ensureSchema(database?: D1Database) {
export async function ensureSchema(database?: DatabaseClient) {
const db = database ?? getRawDb();
{
await db.prepare("SELECT id FROM tasks LIMIT 1").all();
const tasksWithoutShare = await db
.prepare("SELECT id FROM tasks WHERE share_token IS NULL OR share_token = ''")
.all<{ id: string }>();
for (const task of tasksWithoutShare.results) {
await db
.prepare("UPDATE tasks SET share_token = ? WHERE id = ?")
.bind(crypto.randomUUID().replaceAll("-", ""), task.id)
.run();
}
await db
.prepare(
`UPDATE distributions
SET latest_likes = COALESCE(d7_likes, d5_likes, d2_likes),
latest_comments = COALESCE(d7_comments, d5_comments, d2_comments),
latest_collects = COALESCE(d7_collects, d5_collects, d2_collects),
collection_status = 'success',
collection_status_description = '历史采集数据已迁移',
collection_updated_at = updated_at,
last_collection_day = CASE
WHEN d7_likes IS NOT NULL THEN 7
WHEN d5_likes IS NOT NULL THEN 5
WHEN d2_likes IS NOT NULL THEN 2
ELSE NULL
END
WHERE latest_likes IS NULL
AND COALESCE(d7_likes, d5_likes, d2_likes) IS NOT NULL`,
)
.run();
const tasksNeedingImages = await db
.prepare(
`SELECT DISTINCT t.id
FROM tasks t
JOIN contents c ON c.task_id = t.id
WHERE t.source_sheet_id = ?
AND (c.image_assets IS NULL OR c.image_assets = '' OR c.image_assets = '[]')`,
)
.bind(feishuSnapshot.sheetId)
.all<{ id: string }>();
for (const task of tasksNeedingImages.results) {
const updates = feishuSnapshot.rows
.filter((row) => row.images.length > 0)
.map((row) =>
db
.prepare(
`UPDATE contents SET image_assets = ?
WHERE task_id = ? AND source_row = ?
AND (image_assets IS NULL OR image_assets = '' OR image_assets = '[]')`,
)
.bind(JSON.stringify(row.images), task.id, row.sourceRow),
);
if (updates.length > 0) await db.batch(updates);
}
}
return;
const statements = [
`CREATE TABLE IF NOT EXISTS partners (
id TEXT PRIMARY KEY,
@@ -39,6 +93,7 @@ export async function ensureSchema(database?: D1Database) {
claimed_quantity INTEGER NOT NULL DEFAULT 0,
due_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
task_type TEXT NOT NULL DEFAULT 'content_publish',
source_url TEXT NOT NULL DEFAULT '',
source_sheet_id TEXT NOT NULL DEFAULT '',
source_sheet_name TEXT NOT NULL DEFAULT '',
@@ -71,6 +126,7 @@ export async function ensureSchema(database?: D1Database) {
followers INTEGER NOT NULL DEFAULT 0,
post_count INTEGER NOT NULL DEFAULT 0,
avg_views INTEGER NOT NULL DEFAULT 0,
cooperation_source TEXT NOT NULL DEFAULT '',
first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
@@ -109,6 +165,8 @@ export async function ensureSchema(database?: D1Database) {
publish_url TEXT,
publish_time TEXT,
publish_screenshot_key TEXT,
result_screenshot_key TEXT,
result_submitted_at TEXT,
status TEXT NOT NULL DEFAULT 'claimed',
claimed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
screenshot_key TEXT,
@@ -192,6 +250,11 @@ export async function ensureSchema(database?: D1Database) {
};
await ensureColumn("tasks", "source_url", "source_url TEXT NOT NULL DEFAULT ''");
await ensureColumn(
"tasks",
"task_type",
"task_type TEXT NOT NULL DEFAULT 'content_publish'",
);
await ensureColumn(
"tasks",
"source_sheet_id",
@@ -241,6 +304,16 @@ export async function ensureSchema(database?: D1Database) {
"publish_screenshot_key",
"publish_screenshot_key TEXT",
);
await ensureColumn(
"distributions",
"result_screenshot_key",
"result_screenshot_key TEXT",
);
await ensureColumn(
"distributions",
"result_submitted_at",
"result_submitted_at TEXT",
);
await ensureColumn(
"distributions",
"latest_likes",
@@ -399,6 +472,7 @@ export async function ensureSchema(database?: D1Database) {
}
export async function seedIfEmpty() {
if (!isEnabled(getRuntimeEnv().SEED_DEMO_DATA)) return;
const db = getRawDb();
const row = await db.prepare("SELECT COUNT(*) AS count FROM tasks").first<{
count: number;
@@ -733,6 +807,7 @@ export async function getDashboardData() {
a.platform AS account_platform,
t.name AS task_name,
t.brand AS task_brand,
t.task_type AS task_type,
t.due_at AS due_at
FROM distributions d
JOIN contents c ON c.id = d.content_id
@@ -749,8 +824,7 @@ export async function getDashboardData() {
tasks: tasksResult.results as D1ResultRow[],
accounts: accountsResult.results as D1ResultRow[],
distributions: distributionsResult.results as D1ResultRow[],
portal_url:
(env as unknown as { KOC_PORTAL_URL?: string }).KOC_PORTAL_URL ?? "",
portal_url: getRuntimeEnv().KOC_PORTAL_URL ?? "",
};
}