feat: sync project understanding for imported devices

This commit is contained in:
kris
2026-04-04 08:29:17 +08:00
parent 01f438e3af
commit 432cf97541
9 changed files with 587 additions and 18 deletions

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import { authorizeDeviceSessionRequest } from "@/lib/boss-device-auth";
import { syncDeviceProjectUnderstanding } from "@/lib/boss-data";
export async function POST(
request: NextRequest,
context: { params: Promise<{ deviceId: string }> },
) {
const { deviceId } = await context.params;
const auth = await authorizeDeviceSessionRequest(request, deviceId);
if (!auth.ok) {
return NextResponse.json(
{ ok: false, message: auth.status === 404 ? "DEVICE_NOT_FOUND" : "UNAUTHORIZED" },
{ status: auth.status },
);
}
try {
const result = await syncDeviceProjectUnderstanding({
deviceId,
requestedByAccount: auth.session.account,
});
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -677,7 +677,7 @@ export interface MasterAgentTask {
deviceImportCandidateId?: string;
deviceImportCandidateFolderName?: string;
projectUnderstandingTargetProjectId?: string;
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply" | "manual_device_sync";
status: MasterAgentTaskStatus;
requestedAt: string;
claimedAt?: string;
@@ -2995,7 +2995,9 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
deviceImportCandidateFolderName: task.deviceImportCandidateFolderName,
projectUnderstandingTargetProjectId: task.projectUnderstandingTargetProjectId,
projectUnderstandingReason:
task.projectUnderstandingReason === "heartbeat_activity" || task.projectUnderstandingReason === "thread_reply"
task.projectUnderstandingReason === "heartbeat_activity" ||
task.projectUnderstandingReason === "thread_reply" ||
task.projectUnderstandingReason === "manual_device_sync"
? task.projectUnderstandingReason
: undefined,
status: task.status ?? "queued",
@@ -5205,7 +5207,7 @@ export async function queueMasterAgentTask(payload: {
deviceImportCandidateId?: string;
deviceImportCandidateFolderName?: string;
projectUnderstandingTargetProjectId?: string;
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply" | "manual_device_sync";
}) {
const task = await mutateState((state) => {
const task: MasterAgentTask = {
@@ -7354,7 +7356,12 @@ function applyProjectUnderstandingSnapshotInState(
return snapshot;
}
function shouldQueueProjectUnderstandingSync(project: Project, observedActivityAt: string, state: BossState) {
function shouldQueueProjectUnderstandingSync(
project: Project,
observedActivityAt: string,
state: BossState,
options?: { force?: boolean },
) {
if (!isDispatchableThreadProject(project)) {
return false;
}
@@ -7362,14 +7369,16 @@ function shouldQueueProjectUnderstandingSync(project: Project, observedActivityA
if (!Number.isFinite(observedTs)) {
return false;
}
const latestWatermark = Date.parse(
project.threadMeta.lastProjectUnderstandingRequestedAt ??
project.threadMeta.lastProjectUnderstandingSyncedAt ??
project.projectUnderstanding?.updatedAt ??
"1970-01-01T00:00:00.000Z",
);
if (Number.isFinite(latestWatermark) && observedTs <= latestWatermark) {
return false;
if (!options?.force) {
const latestWatermark = Date.parse(
project.threadMeta.lastProjectUnderstandingRequestedAt ??
project.threadMeta.lastProjectUnderstandingSyncedAt ??
project.projectUnderstanding?.updatedAt ??
"1970-01-01T00:00:00.000Z",
);
if (Number.isFinite(latestWatermark) && observedTs <= latestWatermark) {
return false;
}
}
return !state.masterAgentTasks.some(
(task) =>
@@ -7380,13 +7389,22 @@ function shouldQueueProjectUnderstandingSync(project: Project, observedActivityA
);
}
function buildProjectUnderstandingSyncPrompt(project: Project, reason: "heartbeat_activity" | "thread_reply") {
function buildProjectUnderstandingSyncPrompt(
project: Project,
reason: "heartbeat_activity" | "thread_reply" | "manual_device_sync",
) {
return [
"你正在向主 Agent 同步当前项目状态。",
`项目名称:${project.name}`,
`线程名称:${project.threadMeta.threadDisplayName}`,
`文件夹:${project.threadMeta.folderName}`,
`同步原因:${reason === "heartbeat_activity" ? "检测到线程有新活动" : "线程刚刚产生了新的执行结果"}`,
`同步原因:${
reason === "heartbeat_activity"
? "检测到线程有新活动"
: reason === "thread_reply"
? "线程刚刚产生了新的执行结果"
: "用户主动要求同步当前设备上的活跃项目理解"
}`,
"",
"只输出 JSON不要输出解释性文字或 Markdown。",
"JSON 结构固定为:",
@@ -7402,14 +7420,17 @@ function buildProjectUnderstandingSyncPrompt(project: Project, reason: "heartbea
async function queueProjectUnderstandingSyncTask(input: {
projectId: string;
observedActivityAt: string;
reason: "heartbeat_activity" | "thread_reply";
reason: "heartbeat_activity" | "thread_reply" | "manual_device_sync";
force?: boolean;
requestedByAccount?: string;
}) {
const state = await readState();
const project = state.projects.find((item) => item.id === input.projectId);
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state)) {
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state, { force: input.force })) {
return null;
}
const requestedByAccount = state.user.account || project.deviceIds[0] || "17600003315";
const requestedByAccount =
input.requestedByAccount?.trim() || state.user.account || project.deviceIds[0] || "17600003315";
const task = await queueMasterAgentTask({
projectId: "master-agent",
taskType: "conversation_reply",
@@ -7442,6 +7463,82 @@ async function queueProjectUnderstandingSyncTask(input: {
return task;
}
export async function syncDeviceProjectUnderstanding(input: {
deviceId: string;
requestedByAccount: string;
limit?: number;
}) {
const state = await readState();
const device = state.devices.find((item) => item.id === input.deviceId);
if (!device) {
throw new Error("DEVICE_NOT_FOUND");
}
const activeProjects = state.projects
.filter(
(project) =>
!project.isGroup &&
project.deviceIds.includes(input.deviceId) &&
isDispatchableThreadProject(project) &&
Boolean(project.threadMeta.codexThreadRef?.trim()),
)
.sort((left, right) =>
String(
right.threadMeta.lastObservedCodexActivityAt ??
right.projectUnderstanding?.updatedAt ??
right.lastMessageAt,
).localeCompare(
String(
left.threadMeta.lastObservedCodexActivityAt ??
left.projectUnderstanding?.updatedAt ??
left.lastMessageAt,
),
),
)
.slice(0, Math.max(1, input.limit ?? 3));
const queuedTasks = [];
for (const project of activeProjects) {
const observedActivityAt =
project.threadMeta.lastObservedCodexActivityAt ??
project.projectUnderstanding?.updatedAt ??
project.lastMessageAt ??
nowIso();
const task = await queueProjectUnderstandingSyncTask({
projectId: project.id,
observedActivityAt,
reason: "manual_device_sync",
force: true,
requestedByAccount: input.requestedByAccount,
});
if (task) {
queuedTasks.push({
taskId: task.taskId,
projectId: project.id,
projectName: project.name,
threadDisplayName: project.threadMeta.threadDisplayName,
});
}
}
return {
ok: true as const,
deviceId: device.id,
deviceName: device.name,
queuedTasks,
activeProjects: activeProjects.map((project) => ({
projectId: project.id,
projectName: project.name,
threadDisplayName: project.threadMeta.threadDisplayName,
lastObservedCodexActivityAt:
project.threadMeta.lastObservedCodexActivityAt ??
project.projectUnderstanding?.updatedAt ??
project.lastMessageAt,
currentProgress: project.projectUnderstanding?.currentProgress ?? "",
})),
};
}
export async function previewDeviceImportResolution(input: { deviceId: string }) {
const state = await readState();
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);