feat: ship self-hosted KOC LOOP workflows
This commit is contained in:
12
db/index.ts
12
db/index.ts
@@ -1,13 +1,7 @@
|
||||
import { env } from "cloudflare:workers";
|
||||
import { drizzle } from "drizzle-orm/d1";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import { getPool } from "../lib/database";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export function getDb() {
|
||||
if (!env.DB) {
|
||||
throw new Error(
|
||||
"Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database."
|
||||
);
|
||||
}
|
||||
|
||||
return drizzle(env.DB, { schema });
|
||||
return drizzle({ client: getPool(), schema, mode: "default" });
|
||||
}
|
||||
|
||||
330
db/schema.ts
330
db/schema.ts
@@ -1,91 +1,104 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
datetime,
|
||||
index,
|
||||
integer,
|
||||
sqliteTable,
|
||||
int,
|
||||
mysqlTable,
|
||||
text,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
varchar,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
|
||||
export const partners = sqliteTable("partners", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
wecomName: text("wecom_name").notNull(),
|
||||
owner: text("owner").notNull().default("运营组"),
|
||||
claimedTotal: integer("claimed_total").notNull().default(0),
|
||||
completedTotal: integer("completed_total").notNull().default(0),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
const timestamp = (name: string) =>
|
||||
datetime(name, { mode: "string", fsp: 3 })
|
||||
.notNull()
|
||||
.default(sql`CURRENT_TIMESTAMP(3)`);
|
||||
|
||||
export const partners = mysqlTable("partners", {
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
wecomName: varchar("wecom_name", { length: 255 }).notNull(),
|
||||
owner: varchar("owner", { length: 255 }).notNull().default("运营组"),
|
||||
claimedTotal: int("claimed_total").notNull().default(0),
|
||||
completedTotal: int("completed_total").notNull().default(0),
|
||||
createdAt: timestamp("created_at"),
|
||||
});
|
||||
|
||||
export const tasks = sqliteTable(
|
||||
export const tasks = mysqlTable(
|
||||
"tasks",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
brand: text("brand").notNull(),
|
||||
quantity: integer("quantity").notNull(),
|
||||
claimedQuantity: integer("claimed_quantity").notNull().default(0),
|
||||
dueAt: text("due_at").notNull(),
|
||||
status: text("status").notNull().default("active"),
|
||||
sourceUrl: text("source_url").notNull().default(""),
|
||||
sourceSheetId: text("source_sheet_id").notNull().default(""),
|
||||
sourceSheetName: text("source_sheet_name").notNull().default(""),
|
||||
sourceSyncedAt: text("source_synced_at"),
|
||||
shareToken: text("share_token"),
|
||||
collectionStartDate: text("collection_start_date"),
|
||||
collectionDays: text("collection_days").notNull().default("[]"),
|
||||
collectionScheduleUpdatedAt: text("collection_schedule_updated_at"),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
brand: varchar("brand", { length: 255 }).notNull(),
|
||||
quantity: int("quantity").notNull(),
|
||||
claimedQuantity: int("claimed_quantity").notNull().default(0),
|
||||
dueAt: varchar("due_at", { length: 32 }).notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("active"),
|
||||
taskType: varchar("task_type", { length: 32 })
|
||||
.notNull()
|
||||
.default("content_publish"),
|
||||
sourceUrl: text("source_url").notNull(),
|
||||
sourceSheetId: varchar("source_sheet_id", { length: 255 }).notNull().default(""),
|
||||
sourceSheetName: varchar("source_sheet_name", { length: 255 }).notNull().default(""),
|
||||
sourceSyncedAt: datetime("source_synced_at", { mode: "string", fsp: 3 }),
|
||||
shareToken: varchar("share_token", { length: 128 }),
|
||||
collectionStartDate: varchar("collection_start_date", { length: 32 }),
|
||||
collectionDays: text("collection_days").notNull(),
|
||||
collectionScheduleUpdatedAt: datetime("collection_schedule_updated_at", {
|
||||
mode: "string",
|
||||
fsp: 3,
|
||||
}),
|
||||
createdAt: timestamp("created_at"),
|
||||
},
|
||||
(table) => [uniqueIndex("tasks_share_token_idx").on(table.shareToken)],
|
||||
);
|
||||
|
||||
export const contents = sqliteTable("contents", {
|
||||
id: text("id").primaryKey(),
|
||||
taskId: text("task_id").notNull(),
|
||||
export const contents = mysqlTable("contents", {
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
taskId: varchar("task_id", { length: 64 }).notNull(),
|
||||
title: text("title").notNull(),
|
||||
body: text("body").notNull().default(""),
|
||||
imageAssets: text("image_assets").notNull().default("[]"),
|
||||
status: text("status").notNull().default("available"),
|
||||
source: text("source").notNull().default("飞书内容表"),
|
||||
sourceRow: integer("source_row"),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
body: text("body").notNull(),
|
||||
imageAssets: text("image_assets").notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("available"),
|
||||
source: varchar("source", { length: 255 }).notNull().default("飞书内容表"),
|
||||
sourceRow: int("source_row"),
|
||||
createdAt: timestamp("created_at"),
|
||||
});
|
||||
|
||||
export const accounts = sqliteTable(
|
||||
export const accounts = mysqlTable(
|
||||
"accounts",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
platform: text("platform").notNull().default("小红书"),
|
||||
platformUid: text("platform_uid").notNull(),
|
||||
publicAccountId: text("public_account_id").notNull().default(""),
|
||||
nickname: text("nickname").notNull(),
|
||||
profileUrl: text("profile_url").notNull().default(""),
|
||||
ipLocation: text("ip_location").notNull().default("待识别"),
|
||||
followers: integer("followers").notNull().default(0),
|
||||
postCount: integer("post_count").notNull().default(0),
|
||||
avgViews: integer("avg_views").notNull().default(0),
|
||||
firstSeenAt: text("first_seen_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
lastSeenAt: text("last_seen_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
platform: varchar("platform", { length: 32 }).notNull().default("小红书"),
|
||||
platformUid: varchar("platform_uid", { length: 255 }).notNull(),
|
||||
publicAccountId: varchar("public_account_id", { length: 255 }).notNull().default(""),
|
||||
nickname: varchar("nickname", { length: 255 }).notNull(),
|
||||
profileUrl: text("profile_url").notNull(),
|
||||
ipLocation: varchar("ip_location", { length: 255 }).notNull().default("待识别"),
|
||||
followers: int("followers").notNull().default(0),
|
||||
postCount: int("post_count").notNull().default(0),
|
||||
avgViews: int("avg_views").notNull().default(0),
|
||||
cooperationSource: varchar("cooperation_source", { length: 500 })
|
||||
.notNull()
|
||||
.default(""),
|
||||
firstSeenAt: timestamp("first_seen_at"),
|
||||
lastSeenAt: timestamp("last_seen_at"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("accounts_platform_uid_idx").on(
|
||||
table.platform,
|
||||
table.platformUid,
|
||||
),
|
||||
uniqueIndex("accounts_platform_uid_idx").on(table.platform, table.platformUid),
|
||||
],
|
||||
);
|
||||
|
||||
export const claims = sqliteTable(
|
||||
export const claims = mysqlTable(
|
||||
"claims",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
taskId: text("task_id").notNull(),
|
||||
partnerId: text("partner_id").notNull(),
|
||||
claimantName: text("claimant_name").notNull(),
|
||||
claimToken: text("claim_token").notNull(),
|
||||
quantity: integer("quantity").notNull(),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
taskId: varchar("task_id", { length: 64 }).notNull(),
|
||||
partnerId: varchar("partner_id", { length: 64 }).notNull(),
|
||||
claimantName: varchar("claimant_name", { length: 255 }).notNull(),
|
||||
claimToken: varchar("claim_token", { length: 128 }).notNull(),
|
||||
quantity: int("quantity").notNull(),
|
||||
createdAt: timestamp("created_at"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("claims_claim_token_idx").on(table.claimToken),
|
||||
@@ -97,123 +110,116 @@ export const claims = sqliteTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const delegationBundles = sqliteTable(
|
||||
export const delegationBundles = mysqlTable(
|
||||
"delegation_bundles",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
taskId: text("task_id").notNull(),
|
||||
claimId: text("claim_id").notNull(),
|
||||
partnerId: text("partner_id").notNull(),
|
||||
label: text("label").notNull(),
|
||||
shareToken: text("share_token").notNull(),
|
||||
quantity: integer("quantity").notNull(),
|
||||
status: text("status").notNull().default("active"),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
revokedAt: text("revoked_at"),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
taskId: varchar("task_id", { length: 64 }).notNull(),
|
||||
claimId: varchar("claim_id", { length: 64 }).notNull(),
|
||||
partnerId: varchar("partner_id", { length: 64 }).notNull(),
|
||||
label: varchar("label", { length: 255 }).notNull(),
|
||||
shareToken: varchar("share_token", { length: 128 }).notNull(),
|
||||
quantity: int("quantity").notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("active"),
|
||||
createdAt: timestamp("created_at"),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
revokedAt: datetime("revoked_at", { mode: "string", fsp: 3 }),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("delegation_bundles_share_token_idx").on(table.shareToken),
|
||||
index("delegation_bundles_claim_created_idx").on(
|
||||
table.claimId,
|
||||
table.createdAt,
|
||||
),
|
||||
index("delegation_bundles_claim_created_idx").on(table.claimId, table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const distributions = sqliteTable("distributions", {
|
||||
id: text("id").primaryKey(),
|
||||
taskId: text("task_id").notNull(),
|
||||
contentId: text("content_id").notNull(),
|
||||
partnerId: text("partner_id").notNull(),
|
||||
claimId: text("claim_id"),
|
||||
delegationBundleId: text("delegation_bundle_id"),
|
||||
accountId: text("account_id"),
|
||||
export const distributions = mysqlTable("distributions", {
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
taskId: varchar("task_id", { length: 64 }).notNull(),
|
||||
contentId: varchar("content_id", { length: 64 }).notNull(),
|
||||
partnerId: varchar("partner_id", { length: 64 }).notNull(),
|
||||
claimId: varchar("claim_id", { length: 64 }),
|
||||
delegationBundleId: varchar("delegation_bundle_id", { length: 64 }),
|
||||
accountId: varchar("account_id", { length: 64 }),
|
||||
publishUrl: text("publish_url"),
|
||||
publishTime: text("publish_time"),
|
||||
publishScreenshotKey: text("publish_screenshot_key"),
|
||||
status: text("status").notNull().default("claimed"),
|
||||
claimedAt: text("claimed_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
screenshotKey: text("screenshot_key"),
|
||||
ocrStatus: text("ocr_status").notNull().default("none"),
|
||||
exposure: integer("exposure"),
|
||||
views: integer("views"),
|
||||
d2Likes: integer("d2_likes"),
|
||||
d2Comments: integer("d2_comments"),
|
||||
d2Collects: integer("d2_collects"),
|
||||
d5Likes: integer("d5_likes"),
|
||||
d5Comments: integer("d5_comments"),
|
||||
d5Collects: integer("d5_collects"),
|
||||
d7Likes: integer("d7_likes"),
|
||||
d7Comments: integer("d7_comments"),
|
||||
d7Collects: integer("d7_collects"),
|
||||
latestLikes: integer("latest_likes"),
|
||||
latestComments: integer("latest_comments"),
|
||||
latestCollects: integer("latest_collects"),
|
||||
collectionStatus: text("collection_status").notNull().default("pending"),
|
||||
publishTime: datetime("publish_time", { mode: "string", fsp: 3 }),
|
||||
publishScreenshotKey: varchar("publish_screenshot_key", { length: 512 }),
|
||||
resultScreenshotKey: text("result_screenshot_key"),
|
||||
resultSubmittedAt: datetime("result_submitted_at", { mode: "string", fsp: 3 }),
|
||||
status: varchar("status", { length: 32 }).notNull().default("claimed"),
|
||||
claimedAt: timestamp("claimed_at"),
|
||||
screenshotKey: varchar("screenshot_key", { length: 512 }),
|
||||
ocrStatus: varchar("ocr_status", { length: 32 }).notNull().default("none"),
|
||||
exposure: int("exposure"),
|
||||
views: int("views"),
|
||||
d2Likes: int("d2_likes"),
|
||||
d2Comments: int("d2_comments"),
|
||||
d2Collects: int("d2_collects"),
|
||||
d5Likes: int("d5_likes"),
|
||||
d5Comments: int("d5_comments"),
|
||||
d5Collects: int("d5_collects"),
|
||||
d7Likes: int("d7_likes"),
|
||||
d7Comments: int("d7_comments"),
|
||||
d7Collects: int("d7_collects"),
|
||||
latestLikes: int("latest_likes"),
|
||||
latestComments: int("latest_comments"),
|
||||
latestCollects: int("latest_collects"),
|
||||
collectionStatus: varchar("collection_status", { length: 32 })
|
||||
.notNull()
|
||||
.default("pending"),
|
||||
collectionStatusDescription: text("collection_status_description"),
|
||||
collectionUpdatedAt: text("collection_updated_at"),
|
||||
lastCollectionDay: integer("last_collection_day"),
|
||||
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
collectionUpdatedAt: datetime("collection_updated_at", { mode: "string", fsp: 3 }),
|
||||
lastCollectionDay: int("last_collection_day"),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
});
|
||||
|
||||
export const collectionRuns = sqliteTable(
|
||||
export const collectionRuns = mysqlTable(
|
||||
"collection_runs",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
taskId: text("task_id").notNull(),
|
||||
distributionId: text("distribution_id").notNull(),
|
||||
scheduledDate: text("scheduled_date").notNull(),
|
||||
scheduleDay: integer("schedule_day"),
|
||||
scheduledAt: text("scheduled_at").notNull(),
|
||||
status: text("status").notNull().default("pending"),
|
||||
likes: integer("likes"),
|
||||
comments: integer("comments"),
|
||||
collects: integer("collects"),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
taskId: varchar("task_id", { length: 64 }).notNull(),
|
||||
distributionId: varchar("distribution_id", { length: 64 }).notNull(),
|
||||
scheduledDate: varchar("scheduled_date", { length: 32 }).notNull(),
|
||||
scheduleDay: int("schedule_day"),
|
||||
scheduledAt: datetime("scheduled_at", { mode: "string", fsp: 3 }).notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||||
likes: int("likes"),
|
||||
comments: int("comments"),
|
||||
collects: int("collects"),
|
||||
statusDescription: text("status_description"),
|
||||
startedAt: text("started_at"),
|
||||
completedAt: text("completed_at"),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
startedAt: datetime("started_at", { mode: "string", fsp: 3 }),
|
||||
completedAt: datetime("completed_at", { mode: "string", fsp: 3 }),
|
||||
createdAt: timestamp("created_at"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("collection_runs_distribution_date_idx").on(
|
||||
table.distributionId,
|
||||
table.scheduledDate,
|
||||
),
|
||||
index("collection_runs_task_date_idx").on(
|
||||
table.taskId,
|
||||
table.scheduledDate,
|
||||
),
|
||||
index("collection_runs_task_date_idx").on(table.taskId, table.scheduledDate),
|
||||
],
|
||||
);
|
||||
|
||||
export const users = sqliteTable(
|
||||
export const users = mysqlTable(
|
||||
"users",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
username: text("username").notNull(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
passwordSalt: text("password_salt").notNull(),
|
||||
passwordIterations: integer("password_iterations").notNull(),
|
||||
role: text("role").notNull(),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
username: varchar("username", { length: 255 }).notNull(),
|
||||
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
|
||||
passwordSalt: varchar("password_salt", { length: 255 }).notNull(),
|
||||
passwordIterations: int("password_iterations").notNull(),
|
||||
role: varchar("role", { length: 32 }).notNull(),
|
||||
createdAt: timestamp("created_at"),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("users_username_idx").on(table.username),
|
||||
uniqueIndex("users_single_super_admin_idx")
|
||||
.on(table.role)
|
||||
.where(sql`${table.role} = 'super_admin'`),
|
||||
],
|
||||
(table) => [uniqueIndex("users_username_idx").on(table.username)],
|
||||
);
|
||||
|
||||
export const authSessions = sqliteTable(
|
||||
export const authSessions = mysqlTable(
|
||||
"auth_sessions",
|
||||
{
|
||||
tokenHash: text("token_hash").primaryKey(),
|
||||
userId: text("user_id").notNull(),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
tokenHash: varchar("token_hash", { length: 128 }).primaryKey(),
|
||||
userId: varchar("user_id", { length: 64 }).notNull(),
|
||||
expiresAt: datetime("expires_at", { mode: "string", fsp: 3 }).notNull(),
|
||||
createdAt: timestamp("created_at"),
|
||||
},
|
||||
(table) => [
|
||||
index("auth_sessions_user_id_idx").on(table.userId),
|
||||
@@ -221,14 +227,32 @@ export const authSessions = sqliteTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const mcpExportTokens = sqliteTable(
|
||||
export const mcpExportTokens = mysqlTable(
|
||||
"mcp_export_tokens",
|
||||
{
|
||||
tokenHash: text("token_hash").primaryKey(),
|
||||
kind: text("kind").notNull(),
|
||||
tokenHash: varchar("token_hash", { length: 128 }).primaryKey(),
|
||||
kind: varchar("kind", { length: 64 }).notNull(),
|
||||
payload: text("payload").notNull(),
|
||||
expiresAt: text("expires_at").notNull(),
|
||||
createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
expiresAt: datetime("expires_at", { mode: "string", fsp: 3 }).notNull(),
|
||||
createdAt: timestamp("created_at"),
|
||||
},
|
||||
(table) => [index("mcp_export_tokens_expires_at_idx").on(table.expiresAt)],
|
||||
);
|
||||
|
||||
export const backgroundJobs = mysqlTable(
|
||||
"background_jobs",
|
||||
{
|
||||
id: varchar("id", { length: 64 }).primaryKey(),
|
||||
type: varchar("type", { length: 64 }).notNull(),
|
||||
payload: text("payload").notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||||
attempts: int("attempts").notNull().default(0),
|
||||
availableAt: datetime("available_at", { mode: "string", fsp: 3 }).notNull(),
|
||||
lockedAt: datetime("locked_at", { mode: "string", fsp: 3 }),
|
||||
completedAt: datetime("completed_at", { mode: "string", fsp: 3 }),
|
||||
lastError: text("last_error"),
|
||||
createdAt: timestamp("created_at"),
|
||||
updatedAt: timestamp("updated_at"),
|
||||
},
|
||||
(table) => [index("background_jobs_pending_idx").on(table.status, table.availableAt)],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user