feat: add realtime sync and import project understanding

This commit is contained in:
kris
2026-04-04 07:53:27 +08:00
parent 9c53e583ba
commit 908ad8858b
12 changed files with 809 additions and 12 deletions

View File

@@ -482,6 +482,19 @@ export interface DeviceImportResolution {
appliedBy?: string;
}
export interface DeviceImportProjectUnderstanding {
candidateId: string;
threadDisplayName: string;
folderName: string;
projectGoal: string;
currentProgress: string;
technicalArchitecture: string;
currentBlockers: string;
recommendedNextStep: string;
sourceTaskId: string;
updatedAt: string;
}
export interface VerificationCode {
id: string;
account: string;
@@ -646,6 +659,8 @@ export interface MasterAgentTask {
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
deviceImportDraftId?: string;
deviceImportCandidateId?: string;
deviceImportCandidateFolderName?: string;
status: MasterAgentTaskStatus;
requestedAt: string;
claimedAt?: string;
@@ -2926,6 +2941,8 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
: undefined,
orchestrationBackendLabel: task.orchestrationBackendLabel,
deviceImportDraftId: task.deviceImportDraftId,
deviceImportCandidateId: task.deviceImportCandidateId,
deviceImportCandidateFolderName: task.deviceImportCandidateFolderName,
status: task.status ?? "queued",
requestedAt: task.requestedAt ?? nowIso(),
claimedAt: task.claimedAt,
@@ -5130,6 +5147,8 @@ export async function queueMasterAgentTask(payload: {
targetCodexFolderRef?: string;
orchestrationBackendId?: OrchestrationBackendId;
orchestrationBackendLabel?: string;
deviceImportCandidateId?: string;
deviceImportCandidateFolderName?: string;
}) {
const task = await mutateState((state) => {
const task: MasterAgentTask = {
@@ -5159,6 +5178,8 @@ export async function queueMasterAgentTask(payload: {
targetCodexFolderRef: payload.targetCodexFolderRef,
orchestrationBackendId: payload.orchestrationBackendId,
orchestrationBackendLabel: payload.orchestrationBackendLabel,
deviceImportCandidateId: payload.deviceImportCandidateId,
deviceImportCandidateFolderName: payload.deviceImportCandidateFolderName,
status: "queued",
requestedAt: nowIso(),
};
@@ -6114,11 +6135,20 @@ export async function completeMasterAgentTask(payload: {
masterSummary: payload.replyBody?.trim(),
});
} else if (!attachmentProjectId && payload.status === "completed" && task.replyBody) {
const isDeviceImportUnderstanding =
task.taskType === "conversation_reply" &&
task.projectId === "master-agent" &&
Boolean(task.deviceImportDraftId && task.deviceImportCandidateId);
const isThreadConversationReply =
task.taskType === "conversation_reply" &&
task.projectId !== "master-agent" &&
Boolean(task.targetProjectId && task.targetThreadId);
if (isThreadConversationReply) {
if (isDeviceImportUnderstanding) {
const draft = state.deviceImportDrafts.find((item) => item.draftId === task.deviceImportDraftId);
if (draft) {
publishBossEvent("devices.updated", { deviceId: draft.deviceId });
}
} else if (isThreadConversationReply) {
const threadProject = state.projects.find(
(item) => item.id === (task.targetProjectId ?? task.projectId),
);
@@ -7009,7 +7039,110 @@ export async function getLatestDeviceImportDraft(deviceId: string) {
item.deviceImportDraftId === draft.draftId,
) ?? null
: null;
return { draft, resolution, reviewTask };
const understandingTasks = draft
? listDeviceImportUnderstandingTasks(state, draft.draftId)
: [];
const projectUnderstandings = draft
? deriveDeviceImportProjectUnderstandings(state, draft.draftId)
: [];
return { draft, resolution, reviewTask, understandingTasks, projectUnderstandings };
}
function listDeviceImportUnderstandingTasks(state: BossState, draftId: string) {
const latestByCandidate = new Map<string, MasterAgentTask>();
for (const task of state.masterAgentTasks) {
if (
task.taskType !== "conversation_reply" ||
task.deviceImportDraftId !== draftId ||
!task.deviceImportCandidateId
) {
continue;
}
const existing = latestByCandidate.get(task.deviceImportCandidateId);
if (!existing || existing.requestedAt < task.requestedAt) {
latestByCandidate.set(task.deviceImportCandidateId, task);
}
}
return [...latestByCandidate.values()].map((task) => ({
taskId: task.taskId,
candidateId: task.deviceImportCandidateId ?? "",
threadDisplayName: task.targetThreadDisplayName ?? "",
folderName: task.deviceImportCandidateFolderName ?? "",
status: task.status,
updatedAt: task.completedAt ?? task.claimedAt ?? task.requestedAt,
}));
}
function parseDeviceImportUnderstandingReply(
task: Pick<MasterAgentTask, "replyBody" | "deviceImportCandidateId" | "targetThreadDisplayName" | "deviceImportCandidateFolderName" | "taskId" | "completedAt" | "requestedAt">,
): DeviceImportProjectUnderstanding | null {
const candidateId = task.deviceImportCandidateId?.trim();
const replyBody = task.replyBody?.trim();
if (!candidateId || !replyBody) {
return null;
}
const fencedMatch = replyBody.match(/```(?:json)?\s*([\s\S]*?)```/i);
const jsonCandidate = fencedMatch?.[1]?.trim() ?? replyBody;
let parsed:
| {
projectGoal?: string;
currentProgress?: string;
technicalArchitecture?: string;
currentBlockers?: string;
recommendedNextStep?: string;
}
| null = null;
try {
parsed = JSON.parse(jsonCandidate);
} catch {
return null;
}
const projectGoal = parsed?.projectGoal?.trim() ?? "";
const currentProgress = parsed?.currentProgress?.trim() ?? "";
const technicalArchitecture = parsed?.technicalArchitecture?.trim() ?? "";
const currentBlockers = parsed?.currentBlockers?.trim() ?? "";
const recommendedNextStep = parsed?.recommendedNextStep?.trim() ?? "";
if (!projectGoal && !currentProgress && !technicalArchitecture && !currentBlockers && !recommendedNextStep) {
return null;
}
return {
candidateId,
threadDisplayName: task.targetThreadDisplayName?.trim() || "未命名线程",
folderName: task.deviceImportCandidateFolderName?.trim() || "",
projectGoal,
currentProgress,
technicalArchitecture,
currentBlockers,
recommendedNextStep,
sourceTaskId: task.taskId,
updatedAt: task.completedAt ?? task.requestedAt,
};
}
function deriveDeviceImportProjectUnderstandings(state: BossState, draftId: string) {
const latestByCandidate = new Map<string, DeviceImportProjectUnderstanding>();
for (const task of state.masterAgentTasks) {
if (
task.taskType !== "conversation_reply" ||
task.deviceImportDraftId !== draftId ||
task.status !== "completed" ||
!task.deviceImportCandidateId
) {
continue;
}
const understanding = parseDeviceImportUnderstandingReply(task);
if (!understanding) {
continue;
}
const existing = latestByCandidate.get(understanding.candidateId);
if (!existing || existing.updatedAt < understanding.updatedAt) {
latestByCandidate.set(understanding.candidateId, understanding);
}
}
return [...latestByCandidate.values()];
}
export async function previewDeviceImportResolution(input: { deviceId: string }) {

View File

@@ -1579,6 +1579,35 @@ function buildDeviceImportResolutionPrompt(params: {
].join("\n");
}
function buildDeviceImportUnderstandingPrompt(params: {
deviceName: string;
draftId: string;
candidateId: string;
threadDisplayName: string;
folderName: string;
lastActiveAt: string;
}) {
return [
"你正在协助 Boss 控制台完成新设备导入前的项目理解。",
"请以当前活跃线程的视角,直接总结这个项目当前最关键的信息。",
"输出必须是 JSON对象结构如下",
'{ "projectGoal": "一句中文目标", "currentProgress": "一句中文进度", "technicalArchitecture": "一句中文架构说明", "currentBlockers": "一句中文阻塞说明", "recommendedNextStep": "一句中文建议动作" }',
"要求:",
"1. 只输出 JSON不要输出额外解释。",
"2. 用中文,句子短而明确。",
"3. 如果某项信息不确定,也要明确写出当前已知范围,不要留空。",
"",
`deviceName: ${params.deviceName}`,
`draftId: ${params.draftId}`,
`candidateId: ${params.candidateId}`,
`threadDisplayName: ${params.threadDisplayName}`,
`folderName: ${params.folderName}`,
`lastActiveAt: ${params.lastActiveAt}`,
"",
"请回答当前项目:目标、进度、技术架构、阻塞点、下一步建议。",
].join("\n");
}
export async function queueDeviceImportResolutionTask(params: {
deviceId: string;
reviewedBy: string;
@@ -1599,6 +1628,35 @@ export async function queueDeviceImportResolutionTask(params: {
const selectedCandidates = draft.candidates.filter((candidate) =>
draft.selectedCandidateIds.includes(candidate.candidateId),
);
const understandingTasks = await Promise.all(
selectedCandidates
.filter((candidate) => candidate.codexThreadRef?.trim())
.map((candidate) =>
queueMasterAgentTask({
projectId: "master-agent",
taskType: "conversation_reply",
requestMessageId: draft.draftId,
requestText: `请总结线程 ${candidate.threadDisplayName} 当前项目目标与进度`,
executionPrompt: buildDeviceImportUnderstandingPrompt({
deviceName: device.name,
draftId: draft.draftId,
candidateId: candidate.candidateId,
threadDisplayName: candidate.threadDisplayName,
folderName: candidate.folderName,
lastActiveAt: candidate.lastActiveAt,
}),
requestedBy: params.reviewedBy,
requestedByAccount: params.reviewedBy,
deviceId: device.id,
deviceImportDraftId: draft.draftId,
deviceImportCandidateId: candidate.candidateId,
deviceImportCandidateFolderName: candidate.folderName,
targetThreadDisplayName: candidate.threadDisplayName,
targetCodexThreadRef: candidate.codexThreadRef,
targetCodexFolderRef: candidate.codexFolderRef ?? candidate.folderRef,
}),
),
);
const task = await queueMasterAgentTask({
projectId: "master-agent",
taskType: "device_import_resolution",
@@ -1639,6 +1697,13 @@ export async function queueDeviceImportResolutionTask(params: {
deviceImportDraftId: task.deviceImportDraftId,
},
draft: latest.draft ?? undefined,
understandingTasks: understandingTasks.map((item) => ({
taskId: item.taskId,
taskType: item.taskType,
status: item.status,
candidateId: item.deviceImportCandidateId,
threadDisplayName: item.targetThreadDisplayName,
})),
...(latest.resolution ? { resolution: latest.resolution } : {}),
};
}