372 lines
13 KiB
TypeScript

import { canvasAgentActivities, canvasAgentProgressLabel } from "./progress";
import { and, desc, eq, gt, lt, sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { canvasAgentRun, canvasProject, chat } from "@/lib/db/schema";
import {
AgentRunControlChangedError,
type AgentRunSnapshot,
type AgentRunView,
type CanvasAgentRunStatus,
} from "./protocol";
import { inferCanvasTaskRequirements } from "./requirements";
import { inferVideoTaskRequirements } from "./video-protocol";
const LEASE_STALE_MS = 90_000;
type Run = typeof canvasAgentRun.$inferSelect;
export class AgentRunError extends Error {
status: number;
constructor(message: string, status = 409) {
super(message);
this.status = status;
}
}
export async function assertAgentRunOwnership(userId: string, projectId: string, chatId: string) {
const [ownedProject] = await db
.select({ id: canvasProject.id })
.from(canvasProject)
.where(and(eq(canvasProject.id, projectId), eq(canvasProject.userId, userId)))
.limit(1);
const [ownedChat] = await db
.select({ id: chat.id })
.from(chat)
.where(and(eq(chat.id, chatId), eq(chat.userId, userId)))
.limit(1);
if (!ownedProject || !ownedChat) throw new AgentRunError("画布或对话不存在", 404);
}
export function agentRunView(run: Run): AgentRunView {
const stale = run.status === "running" && Date.now() - run.updatedAt.getTime() > LEASE_STALE_MS;
return {
assistantMessageId: run.assistantMessageId,
activities: canvasAgentActivities(run.snapshot),
id: run.id,
videoSettings: run.snapshot.videoSettings,
chatId: run.chatId,
projectId: run.projectId,
status: stale ? "interrupted" : (run.status as CanvasAgentRunStatus),
message: stale ? "任务连接已中断,已保留执行记录" : canvasAgentProgressLabel(run.snapshot.message),
steps: run.snapshot.steps,
toolCount: run.snapshot.calls.length,
resultNodeIds: run.snapshot.resultNodeIds,
inputs: [
...run.snapshot.inputs,
...run.pendingInputs.map((input) => ({ ...input, applied: false })),
],
updatedAt: run.updatedAt.toISOString(),
cancelRequested: run.cancelRequested,
resumable:
(stale || run.status !== "running") &&
run.status !== "completed" &&
!run.snapshot.calls.some((call) => call.status === "started" || call.uncertain),
};
}
export async function getAgentRun(
userId: string,
chatId: string,
projectId: string,
runId?: string,
) {
const [run] = await db
.select()
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.userId, userId),
eq(canvasAgentRun.chatId, chatId),
eq(canvasAgentRun.projectId, projectId),
...(runId ? [eq(canvasAgentRun.id, runId)] : []),
),
)
.orderBy(desc(canvasAgentRun.createdAt))
.limit(1);
return run || null;
}
export async function acquireAgentRun(params: {
id: string;
userId: string;
chatId: string;
projectId: string;
snapshot: AgentRunSnapshot;
assistantMessageId: string;
resumeRunId?: string;
}) {
await assertAgentRunOwnership(params.userId, params.projectId, params.chatId);
return db.transaction(async (tx) => {
await tx.execute(
sql`select pg_advisory_xact_lock(hashtext(${`canvas-agent:${params.userId}:${params.chatId}`}))`,
);
const staleBefore = new Date(Date.now() - LEASE_STALE_MS);
const [active] = await tx
.select({ id: canvasAgentRun.id })
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.userId, params.userId),
eq(canvasAgentRun.chatId, params.chatId),
eq(canvasAgentRun.status, "running"),
gt(canvasAgentRun.updatedAt, staleBefore),
),
)
.limit(1);
if (active) throw new AgentRunError("这个对话已有任务正在处理,请使用补充要求或停止操作。");
const [existing] = await tx
.select()
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.userId, params.userId),
eq(canvasAgentRun.chatId, params.chatId),
eq(canvasAgentRun.id, params.resumeRunId || params.id),
),
)
.limit(1);
if (params.resumeRunId && (!existing || existing.projectId !== params.projectId)) {
throw new AgentRunError("找不到需要继续的任务", 404);
}
if (
existing &&
(existing.snapshot.workspace || "canvas") !== (params.snapshot.workspace || "canvas")
)
throw new AgentRunError("任务与当前画布类型不一致", 400);
if (existing && !params.resumeRunId)
throw new AgentRunError("此消息已经处理,请刷新查看结果。");
if (existing?.snapshot.calls.some((call) => call.status === "started" || call.uncertain)) {
throw new AgentRunError(
"有工具的执行结果尚未确认。为避免重复生图,请先核对画布结果,再创建新任务。",
);
}
// Revoke expired workers before a new run acquires this conversation.
await tx
.update(canvasAgentRun)
.set({ status: "interrupted" })
.where(
and(
eq(canvasAgentRun.userId, params.userId),
eq(canvasAgentRun.chatId, params.chatId),
eq(canvasAgentRun.status, "running"),
lt(canvasAgentRun.updatedAt, staleBefore),
),
);
const leaseId = crypto.randomUUID();
const snapshot = existing?.snapshot ?? params.snapshot;
if (existing) {
if (existing.status === "completed") throw new AgentRunError("该任务已完成,无需重新执行。");
snapshot.message = "正在从已完成的步骤继续";
if (snapshot.workspace === "video" && params.snapshot.videoSettings) {
snapshot.videoSettings = params.snapshot.videoSettings;
snapshot.videoRequirements = {
...snapshot.videoRequirements!,
duration: snapshot.videoSettings.duration,
ratio: snapshot.videoSettings.ratio,
};
}
snapshot.messages.push({ role: "user", content: params.snapshot.objective });
snapshot.inputs.push({ id: params.id, text: params.snapshot.objective, applied: true });
updateRequirements(snapshot, params.snapshot.objective);
const [run] = await tx
.update(canvasAgentRun)
.set({
status: "running",
leaseId,
cancelRequested: false,
snapshot,
assistantMessageId: params.assistantMessageId,
updatedAt: new Date(),
})
.where(eq(canvasAgentRun.id, existing.id))
.returning();
return run;
}
const [run] = await tx
.insert(canvasAgentRun)
.values({
id: params.id,
userId: params.userId,
chatId: params.chatId,
projectId: params.projectId,
snapshot,
leaseId,
assistantMessageId: params.assistantMessageId,
createdAt: new Date(),
updatedAt: new Date(),
})
.onConflictDoNothing({ target: canvasAgentRun.id })
.returning();
if (!run) throw new AgentRunError("此消息已经处理,请刷新查看结果。");
return run;
});
}
export async function checkpointAgentRun(
run: Pick<Run, "id" | "leaseId">,
snapshot: AgentRunSnapshot,
status: CanvasAgentRunStatus = "running",
) {
return db.transaction(async (tx) => {
const [current] = await tx
.select()
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.id, run.id),
eq(canvasAgentRun.leaseId, run.leaseId),
eq(canvasAgentRun.status, "running"),
),
)
.for("update")
.limit(1);
if (!current) throw new AgentRunError("任务执行权已变更,已停止本次执行。");
if (status === "completed" && (current.cancelRequested || current.pendingInputs.length)) {
throw new AgentRunControlChangedError("完成前收到新要求或停止请求");
}
const [saved] = await tx
.update(canvasAgentRun)
.set({ snapshot, status, updatedAt: new Date() })
.where(eq(canvasAgentRun.id, run.id))
.returning();
return saved;
});
}
export async function heartbeatAgentRun(run: Pick<Run, "id" | "leaseId">) {
const [saved] = await db
.update(canvasAgentRun)
.set({ updatedAt: new Date() })
.where(
and(
eq(canvasAgentRun.id, run.id),
eq(canvasAgentRun.leaseId, run.leaseId),
eq(canvasAgentRun.status, "running"),
),
)
.returning();
return saved || null;
}
export async function consumeAgentRunInputs(
run: Pick<Run, "id" | "leaseId">,
snapshot: AgentRunSnapshot,
) {
return db.transaction(async (tx) => {
const [current] = await tx
.select()
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.id, run.id),
eq(canvasAgentRun.leaseId, run.leaseId),
eq(canvasAgentRun.status, "running"),
),
)
.for("update")
.limit(1);
if (!current) throw new AgentRunError("任务已停止");
if (current.cancelRequested) return { cancelled: true, inputs: [] };
const inputs = current.pendingInputs;
for (const input of inputs) {
snapshot.messages.push({ role: "user", content: input.text });
snapshot.inputs.push({ ...input, applied: true });
updateRequirements(snapshot, input.text);
}
if (inputs.length)
await tx
.update(canvasAgentRun)
.set({ snapshot, pendingInputs: [], updatedAt: new Date() })
.where(eq(canvasAgentRun.id, run.id));
return { cancelled: false, inputs };
});
}
function updateRequirements(snapshot: AgentRunSnapshot, text: string) {
if (snapshot.workspace === "video") {
const inferred = inferVideoTaskRequirements(text);
const current = snapshot.videoRequirements!;
snapshot.videoRequirements = {
...current,
expectsVideo: /不要(?:生成|制作|执行).{0,4}视频/.test(text)
? false
: current.expectsVideo || inferred.expectsVideo,
expectsScript: current.expectsScript || inferred.expectsScript,
...(inferred.expectedVideoCount
? {
expectedVideoCount: /再|补/.test(text)
? (current.expectedVideoCount || 1) + inferred.expectedVideoCount
: inferred.expectedVideoCount,
}
: {}),
...(inferred.duration ? { duration: inferred.duration } : {}),
...(inferred.ratio ? { ratio: inferred.ratio } : {}),
};
if (snapshot.videoSettings) {
if (inferred.duration && inferred.duration <= 120)
snapshot.videoSettings.duration = inferred.duration;
const ratios = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
if (inferred.ratio && ratios.includes(inferred.ratio))
snapshot.videoSettings.ratio = inferred.ratio as typeof snapshot.videoSettings.ratio;
}
return;
}
const inferred = inferCanvasTaskRequirements(text, snapshot.requirements?.sourceCount);
const current = snapshot.requirements ?? {};
const plan = snapshot.calls.filter((call) => call.name === "planCanvasTask").at(-1)?.output;
const previousCount = current.expectedImageCount ?? Number(plan?.expectedImageCount || 0);
snapshot.requirements = {
...current,
...(inferred.aspectRatio ? { aspectRatio: inferred.aspectRatio } : {}),
...(inferred.expectedImageCount
? {
expectedImageCount: /再(?:出|做|生成)|补(?:出|做)?/.test(text)
? previousCount + inferred.expectedImageCount
: inferred.expectedImageCount,
}
: {}),
};
}
export async function controlAgentRun(params: {
userId: string;
projectId: string;
chatId: string;
runId: string;
action: "steer" | "cancel";
inputId?: string;
text?: string;
}) {
return db.transaction(async (tx) => {
const [run] = await tx
.select()
.from(canvasAgentRun)
.where(
and(
eq(canvasAgentRun.id, params.runId),
eq(canvasAgentRun.userId, params.userId),
eq(canvasAgentRun.projectId, params.projectId),
eq(canvasAgentRun.chatId, params.chatId),
),
)
.for("update")
.limit(1);
if (!run) throw new AgentRunError("任务不存在", 404);
if (agentRunView(run).status !== "running")
throw new AgentRunError("任务已结束或中断,请继续任务后再补充。");
if (params.action === "cancel") {
const [updated] = await tx
.update(canvasAgentRun)
.set({ cancelRequested: true })
.where(eq(canvasAgentRun.id, run.id))
.returning();
return agentRunView(updated);
}
if (run.cancelRequested) throw new AgentRunError("任务正在停止,不能再补充要求。");
const inputId = params.inputId!;
if (
run.pendingInputs.some((input) => input.id === inputId) ||
run.snapshot.inputs.some((input) => input.id === inputId)
)
return agentRunView(run);
if (run.pendingInputs.length >= 10)
throw new AgentRunError("待处理补充要求过多,请等待当前步骤完成。");
const [updated] = await tx
.update(canvasAgentRun)
.set({ pendingInputs: [...run.pendingInputs, { id: inputId, text: params.text! }] })
.where(eq(canvasAgentRun.id, run.id))
.returning();
return agentRunView(updated);
});
}