feat: keep project understanding in sync after import
This commit is contained in:
@@ -287,6 +287,9 @@ export interface ThreadConversationMeta {
|
||||
folderName: string;
|
||||
activityIconCount: number;
|
||||
updatedAt: string;
|
||||
lastObservedCodexActivityAt?: string;
|
||||
lastProjectUnderstandingRequestedAt?: string;
|
||||
lastProjectUnderstandingSyncedAt?: string;
|
||||
codexThreadRef?: string;
|
||||
codexFolderRef?: string;
|
||||
}
|
||||
@@ -321,6 +324,7 @@ export interface Project {
|
||||
riskLevel: RiskLevel;
|
||||
contextBudgetPct?: number;
|
||||
contextBudgetLabel?: string;
|
||||
projectUnderstanding?: ProjectUnderstandingSnapshot;
|
||||
messages: Message[];
|
||||
goals: GoalItem[];
|
||||
versions: VersionEntry[];
|
||||
@@ -495,6 +499,17 @@ export interface DeviceImportProjectUnderstanding {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectUnderstandingSnapshot {
|
||||
projectGoal: string;
|
||||
currentProgress: string;
|
||||
technicalArchitecture: string;
|
||||
currentBlockers: string;
|
||||
recommendedNextStep: string;
|
||||
sourceTaskId: string;
|
||||
updatedAt: string;
|
||||
sourceKind: "device_import" | "thread_sync";
|
||||
}
|
||||
|
||||
export interface VerificationCode {
|
||||
id: string;
|
||||
account: string;
|
||||
@@ -661,6 +676,8 @@ export interface MasterAgentTask {
|
||||
deviceImportDraftId?: string;
|
||||
deviceImportCandidateId?: string;
|
||||
deviceImportCandidateFolderName?: string;
|
||||
projectUnderstandingTargetProjectId?: string;
|
||||
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
||||
status: MasterAgentTaskStatus;
|
||||
requestedAt: string;
|
||||
claimedAt?: string;
|
||||
@@ -1724,6 +1741,11 @@ function normalizeThreadMeta(
|
||||
folderName: raw?.folderName ?? fallback?.folderName ?? (project.isGroup ? "群聊" : project.name),
|
||||
activityIconCount: Math.max(1, raw?.activityIconCount ?? fallback?.activityIconCount ?? (project.isGroup ? 2 : 1)),
|
||||
updatedAt: raw?.updatedAt ?? project.updatedAt ?? nowIso(),
|
||||
lastObservedCodexActivityAt: raw?.lastObservedCodexActivityAt ?? fallback?.lastObservedCodexActivityAt,
|
||||
lastProjectUnderstandingRequestedAt:
|
||||
raw?.lastProjectUnderstandingRequestedAt ?? fallback?.lastProjectUnderstandingRequestedAt,
|
||||
lastProjectUnderstandingSyncedAt:
|
||||
raw?.lastProjectUnderstandingSyncedAt ?? fallback?.lastProjectUnderstandingSyncedAt,
|
||||
codexThreadRef: raw?.codexThreadRef,
|
||||
codexFolderRef: raw?.codexFolderRef,
|
||||
};
|
||||
@@ -2783,6 +2805,7 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||||
unreadCount:
|
||||
typeof raw.unreadCount === "number" ? raw.unreadCount : base.unreadCount ?? 0,
|
||||
riskLevel: raw.riskLevel ?? base.riskLevel ?? "low",
|
||||
projectUnderstanding: normalizeProjectUnderstanding(raw.projectUnderstanding, fallback?.projectUnderstanding),
|
||||
messages: ensureArray(raw.messages, base.messages).map(normalizeMessage),
|
||||
goals: ensureArray(raw.goals, base.goals).map((goal) => ({
|
||||
id: goal.id ?? randomToken("goal"),
|
||||
@@ -2813,6 +2836,33 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||||
return project;
|
||||
}
|
||||
|
||||
function normalizeProjectUnderstanding(
|
||||
raw: Partial<ProjectUnderstandingSnapshot> | undefined,
|
||||
fallback?: ProjectUnderstandingSnapshot,
|
||||
): ProjectUnderstandingSnapshot | undefined {
|
||||
if (!raw && !fallback) {
|
||||
return undefined;
|
||||
}
|
||||
const projectGoal = raw?.projectGoal?.trim() ?? fallback?.projectGoal?.trim() ?? "";
|
||||
const currentProgress = raw?.currentProgress?.trim() ?? fallback?.currentProgress?.trim() ?? "";
|
||||
const technicalArchitecture = raw?.technicalArchitecture?.trim() ?? fallback?.technicalArchitecture?.trim() ?? "";
|
||||
const currentBlockers = raw?.currentBlockers?.trim() ?? fallback?.currentBlockers?.trim() ?? "";
|
||||
const recommendedNextStep = raw?.recommendedNextStep?.trim() ?? fallback?.recommendedNextStep?.trim() ?? "";
|
||||
if (!projectGoal && !currentProgress && !technicalArchitecture && !currentBlockers && !recommendedNextStep) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
projectGoal,
|
||||
currentProgress,
|
||||
technicalArchitecture,
|
||||
currentBlockers,
|
||||
recommendedNextStep,
|
||||
sourceTaskId: raw?.sourceTaskId ?? fallback?.sourceTaskId ?? randomToken("mastertask"),
|
||||
updatedAt: raw?.updatedAt ?? fallback?.updatedAt ?? nowIso(),
|
||||
sourceKind: raw?.sourceKind ?? fallback?.sourceKind ?? "thread_sync",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
const base = cloneInitialState();
|
||||
if (!raw) return syncDerivedState(base);
|
||||
@@ -2943,6 +2993,11 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
|
||||
deviceImportDraftId: task.deviceImportDraftId,
|
||||
deviceImportCandidateId: task.deviceImportCandidateId,
|
||||
deviceImportCandidateFolderName: task.deviceImportCandidateFolderName,
|
||||
projectUnderstandingTargetProjectId: task.projectUnderstandingTargetProjectId,
|
||||
projectUnderstandingReason:
|
||||
task.projectUnderstandingReason === "heartbeat_activity" || task.projectUnderstandingReason === "thread_reply"
|
||||
? task.projectUnderstandingReason
|
||||
: undefined,
|
||||
status: task.status ?? "queued",
|
||||
requestedAt: task.requestedAt ?? nowIso(),
|
||||
claimedAt: task.claimedAt,
|
||||
@@ -5149,6 +5204,8 @@ export async function queueMasterAgentTask(payload: {
|
||||
orchestrationBackendLabel?: string;
|
||||
deviceImportCandidateId?: string;
|
||||
deviceImportCandidateFolderName?: string;
|
||||
projectUnderstandingTargetProjectId?: string;
|
||||
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
||||
}) {
|
||||
const task = await mutateState((state) => {
|
||||
const task: MasterAgentTask = {
|
||||
@@ -5180,6 +5237,8 @@ export async function queueMasterAgentTask(payload: {
|
||||
orchestrationBackendLabel: payload.orchestrationBackendLabel,
|
||||
deviceImportCandidateId: payload.deviceImportCandidateId,
|
||||
deviceImportCandidateFolderName: payload.deviceImportCandidateFolderName,
|
||||
projectUnderstandingTargetProjectId: payload.projectUnderstandingTargetProjectId,
|
||||
projectUnderstandingReason: payload.projectUnderstandingReason,
|
||||
status: "queued",
|
||||
requestedAt: nowIso(),
|
||||
};
|
||||
@@ -6139,6 +6198,10 @@ export async function completeMasterAgentTask(payload: {
|
||||
task.taskType === "conversation_reply" &&
|
||||
task.projectId === "master-agent" &&
|
||||
Boolean(task.deviceImportDraftId && task.deviceImportCandidateId);
|
||||
const isProjectUnderstandingSync =
|
||||
task.taskType === "conversation_reply" &&
|
||||
task.projectId === "master-agent" &&
|
||||
Boolean(task.projectUnderstandingTargetProjectId);
|
||||
const isThreadConversationReply =
|
||||
task.taskType === "conversation_reply" &&
|
||||
task.projectId !== "master-agent" &&
|
||||
@@ -6148,6 +6211,18 @@ export async function completeMasterAgentTask(payload: {
|
||||
if (draft) {
|
||||
publishBossEvent("devices.updated", { deviceId: draft.deviceId });
|
||||
}
|
||||
} else if (isProjectUnderstandingSync) {
|
||||
const understanding = parseStructuredProjectUnderstandingReply(task);
|
||||
if (understanding && task.projectUnderstandingTargetProjectId) {
|
||||
applyProjectUnderstandingSnapshotInState(state, {
|
||||
projectId: task.projectUnderstandingTargetProjectId,
|
||||
account: task.requestedByAccount,
|
||||
snapshot: understanding,
|
||||
sourceMessageId: task.requestMessageId,
|
||||
sourceKind: "thread_sync",
|
||||
});
|
||||
publishBossEvent("conversation.updated", { projectId: task.projectUnderstandingTargetProjectId });
|
||||
}
|
||||
} else if (isThreadConversationReply) {
|
||||
const threadProject = state.projects.find(
|
||||
(item) => item.id === (task.targetProjectId ?? task.projectId),
|
||||
@@ -6778,6 +6853,11 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
}>;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const projectUnderstandingSyncRequests: Array<{
|
||||
projectId: string;
|
||||
observedActivityAt: string;
|
||||
reason: "heartbeat_activity";
|
||||
}> = [];
|
||||
const existingDevice = state.devices.find((item) => item.id === payload.deviceId) ?? null;
|
||||
const claimedEnrollment = claimEnrollment(
|
||||
state,
|
||||
@@ -6881,6 +6961,30 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
candidates: normalizedCandidates,
|
||||
});
|
||||
|
||||
for (const candidate of normalizedCandidates) {
|
||||
const matchingProject = state.projects.find(
|
||||
(project) =>
|
||||
!project.isGroup &&
|
||||
project.deviceIds.includes(payload.deviceId) &&
|
||||
((candidate.codexThreadRef && project.threadMeta.codexThreadRef === candidate.codexThreadRef) ||
|
||||
project.threadMeta.threadId === candidate.threadId),
|
||||
);
|
||||
if (!matchingProject) {
|
||||
continue;
|
||||
}
|
||||
matchingProject.threadMeta.lastObservedCodexActivityAt = latestIsoTimestamp(
|
||||
matchingProject.threadMeta.lastObservedCodexActivityAt,
|
||||
candidate.lastActiveAt,
|
||||
) ?? candidate.lastActiveAt;
|
||||
if (shouldQueueProjectUnderstandingSync(matchingProject, candidate.lastActiveAt, state)) {
|
||||
projectUnderstandingSyncRequests.push({
|
||||
projectId: matchingProject.id,
|
||||
observedActivityAt: candidate.lastActiveAt,
|
||||
reason: "heartbeat_activity",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
draft &&
|
||||
shouldAutoSyncHeartbeatCandidates({
|
||||
@@ -6931,8 +7035,12 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
token: claimedEnrollment?.token ?? device.token,
|
||||
pairingStatus: claimedEnrollment?.status,
|
||||
importDraft: draft,
|
||||
projectUnderstandingSyncRequests,
|
||||
};
|
||||
});
|
||||
for (const request of result.projectUnderstandingSyncRequests ?? []) {
|
||||
await queueProjectUnderstandingSyncTask(request);
|
||||
}
|
||||
publishBossEvent("devices.updated", { deviceId: payload.deviceId });
|
||||
publishBossEvent("conversation.updated", { deviceId: payload.deviceId });
|
||||
return result;
|
||||
@@ -7076,9 +7184,25 @@ function listDeviceImportUnderstandingTasks(state: BossState, draftId: string) {
|
||||
function parseDeviceImportUnderstandingReply(
|
||||
task: Pick<MasterAgentTask, "replyBody" | "deviceImportCandidateId" | "targetThreadDisplayName" | "deviceImportCandidateFolderName" | "taskId" | "completedAt" | "requestedAt">,
|
||||
): DeviceImportProjectUnderstanding | null {
|
||||
const understanding = parseStructuredProjectUnderstandingReply(task);
|
||||
const candidateId = task.deviceImportCandidateId?.trim();
|
||||
if (!candidateId || !understanding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
candidateId,
|
||||
threadDisplayName: task.targetThreadDisplayName?.trim() || "未命名线程",
|
||||
folderName: task.deviceImportCandidateFolderName?.trim() || "",
|
||||
...understanding,
|
||||
};
|
||||
}
|
||||
|
||||
function parseStructuredProjectUnderstandingReply(
|
||||
task: Pick<MasterAgentTask, "replyBody" | "taskId" | "completedAt" | "requestedAt">,
|
||||
): ProjectUnderstandingSnapshot | null {
|
||||
const replyBody = task.replyBody?.trim();
|
||||
if (!candidateId || !replyBody) {
|
||||
if (!replyBody) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7109,9 +7233,6 @@ function parseDeviceImportUnderstandingReply(
|
||||
}
|
||||
|
||||
return {
|
||||
candidateId,
|
||||
threadDisplayName: task.targetThreadDisplayName?.trim() || "未命名线程",
|
||||
folderName: task.deviceImportCandidateFolderName?.trim() || "",
|
||||
projectGoal,
|
||||
currentProgress,
|
||||
technicalArchitecture,
|
||||
@@ -7119,6 +7240,7 @@ function parseDeviceImportUnderstandingReply(
|
||||
recommendedNextStep,
|
||||
sourceTaskId: task.taskId,
|
||||
updatedAt: task.completedAt ?? task.requestedAt,
|
||||
sourceKind: "thread_sync",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7145,6 +7267,181 @@ function deriveDeviceImportProjectUnderstandings(state: BossState, draftId: stri
|
||||
return [...latestByCandidate.values()];
|
||||
}
|
||||
|
||||
function applyProjectUnderstandingSnapshotInState(
|
||||
state: BossState,
|
||||
input: {
|
||||
projectId: string;
|
||||
account: string;
|
||||
snapshot: ProjectUnderstandingSnapshot;
|
||||
sourceMessageId?: string;
|
||||
sourceKind: ProjectUnderstandingSnapshot["sourceKind"];
|
||||
},
|
||||
) {
|
||||
const project = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
const snapshot: ProjectUnderstandingSnapshot = {
|
||||
...input.snapshot,
|
||||
sourceKind: input.sourceKind,
|
||||
};
|
||||
project.projectUnderstanding = snapshot;
|
||||
project.threadMeta.lastProjectUnderstandingSyncedAt = snapshot.updatedAt;
|
||||
project.threadMeta.lastObservedCodexActivityAt =
|
||||
latestIsoTimestamp(project.threadMeta.lastObservedCodexActivityAt, snapshot.updatedAt) ?? snapshot.updatedAt;
|
||||
|
||||
const tags = [project.name, project.threadMeta.threadDisplayName].filter(Boolean);
|
||||
if (snapshot.projectGoal) {
|
||||
upsertAutoMasterMemoryInState(state, {
|
||||
account: input.account,
|
||||
scope: "project",
|
||||
projectId: project.id,
|
||||
title: `项目目标 · ${project.name}`,
|
||||
content: snapshot.projectGoal,
|
||||
memoryType: "project_progress",
|
||||
tags: [...tags, "项目目标"],
|
||||
sourceMessageId: input.sourceMessageId,
|
||||
});
|
||||
}
|
||||
if (snapshot.currentProgress) {
|
||||
upsertAutoMasterMemoryInState(state, {
|
||||
account: input.account,
|
||||
scope: "project",
|
||||
projectId: project.id,
|
||||
title: `项目进度 · ${project.name}`,
|
||||
content: snapshot.currentProgress,
|
||||
memoryType: "project_progress",
|
||||
tags: [...tags, "项目进度"],
|
||||
sourceMessageId: input.sourceMessageId,
|
||||
});
|
||||
}
|
||||
if (snapshot.technicalArchitecture) {
|
||||
upsertAutoMasterMemoryInState(state, {
|
||||
account: input.account,
|
||||
scope: "project",
|
||||
projectId: project.id,
|
||||
title: `技术架构 · ${project.name}`,
|
||||
content: snapshot.technicalArchitecture,
|
||||
memoryType: "research_note",
|
||||
tags: [...tags, "技术架构"],
|
||||
sourceMessageId: input.sourceMessageId,
|
||||
});
|
||||
}
|
||||
if (snapshot.currentBlockers) {
|
||||
upsertAutoMasterMemoryInState(state, {
|
||||
account: input.account,
|
||||
scope: "project",
|
||||
projectId: project.id,
|
||||
title: `当前阻塞 · ${project.name}`,
|
||||
content: snapshot.currentBlockers,
|
||||
memoryType: "blocking_issue",
|
||||
tags: [...tags, "阻塞"],
|
||||
sourceMessageId: input.sourceMessageId,
|
||||
});
|
||||
}
|
||||
if (snapshot.recommendedNextStep) {
|
||||
upsertAutoMasterMemoryInState(state, {
|
||||
account: input.account,
|
||||
scope: "project",
|
||||
projectId: project.id,
|
||||
title: `下一步建议 · ${project.name}`,
|
||||
content: snapshot.recommendedNextStep,
|
||||
memoryType: "workflow_rule",
|
||||
tags: [...tags, "下一步"],
|
||||
sourceMessageId: input.sourceMessageId,
|
||||
});
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function shouldQueueProjectUnderstandingSync(project: Project, observedActivityAt: string, state: BossState) {
|
||||
if (!isDispatchableThreadProject(project)) {
|
||||
return false;
|
||||
}
|
||||
const observedTs = Date.parse(observedActivityAt);
|
||||
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;
|
||||
}
|
||||
return !state.masterAgentTasks.some(
|
||||
(task) =>
|
||||
task.taskType === "conversation_reply" &&
|
||||
task.projectId === "master-agent" &&
|
||||
task.projectUnderstandingTargetProjectId === project.id &&
|
||||
(task.status === "queued" || task.status === "running"),
|
||||
);
|
||||
}
|
||||
|
||||
function buildProjectUnderstandingSyncPrompt(project: Project, reason: "heartbeat_activity" | "thread_reply") {
|
||||
return [
|
||||
"你正在向主 Agent 同步当前项目状态。",
|
||||
`项目名称:${project.name}`,
|
||||
`线程名称:${project.threadMeta.threadDisplayName}`,
|
||||
`文件夹:${project.threadMeta.folderName}`,
|
||||
`同步原因:${reason === "heartbeat_activity" ? "检测到线程有新活动" : "线程刚刚产生了新的执行结果"}`,
|
||||
"",
|
||||
"只输出 JSON,不要输出解释性文字或 Markdown。",
|
||||
"JSON 结构固定为:",
|
||||
'{ "projectGoal": "一句中文目标", "currentProgress": "一句中文进度", "technicalArchitecture": "一句中文架构说明", "currentBlockers": "一句中文阻塞说明", "recommendedNextStep": "一句中文建议动作" }',
|
||||
"",
|
||||
"要求:",
|
||||
"1. 只写当前项目最重要、对主 Agent 接手有帮助的事实。",
|
||||
"2. 不要重复内部字段、线程编号、目录路径、设备 ID。",
|
||||
"3. 如果某个字段暂时不清楚,填空字符串。",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function queueProjectUnderstandingSyncTask(input: {
|
||||
projectId: string;
|
||||
observedActivityAt: string;
|
||||
reason: "heartbeat_activity" | "thread_reply";
|
||||
}) {
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!project || !shouldQueueProjectUnderstandingSync(project, input.observedActivityAt, state)) {
|
||||
return null;
|
||||
}
|
||||
const requestedByAccount = state.user.account || project.deviceIds[0] || "17600003315";
|
||||
const task = await queueMasterAgentTask({
|
||||
projectId: "master-agent",
|
||||
taskType: "conversation_reply",
|
||||
requestMessageId: randomToken("project-understanding"),
|
||||
requestText: `请同步项目《${project.name}》当前目标与进度`,
|
||||
executionPrompt: buildProjectUnderstandingSyncPrompt(project, input.reason),
|
||||
requestedBy: requestedByAccount,
|
||||
requestedByAccount,
|
||||
deviceId: project.deviceIds[0] || state.user.boundDeviceId || "mac-studio",
|
||||
targetProjectId: project.id,
|
||||
targetThreadId: project.threadMeta.threadId,
|
||||
targetThreadDisplayName: project.threadMeta.threadDisplayName,
|
||||
targetCodexThreadRef: project.threadMeta.codexThreadRef,
|
||||
targetCodexFolderRef: project.threadMeta.codexFolderRef,
|
||||
projectUnderstandingTargetProjectId: project.id,
|
||||
projectUnderstandingReason: input.reason,
|
||||
});
|
||||
await mutateStateIfChanged((freshState) => {
|
||||
const freshProject = freshState.projects.find((item) => item.id === input.projectId);
|
||||
if (!freshProject) {
|
||||
return { result: null, changed: false };
|
||||
}
|
||||
freshProject.threadMeta.lastProjectUnderstandingRequestedAt = task.requestedAt;
|
||||
freshProject.threadMeta.lastObservedCodexActivityAt = latestIsoTimestamp(
|
||||
freshProject.threadMeta.lastObservedCodexActivityAt,
|
||||
input.observedActivityAt,
|
||||
) ?? input.observedActivityAt;
|
||||
return { result: null, changed: true };
|
||||
});
|
||||
return task;
|
||||
}
|
||||
|
||||
export async function previewDeviceImportResolution(input: { deviceId: string }) {
|
||||
const state = await readState();
|
||||
const draft = state.deviceImportDrafts.find((item) => item.deviceId === input.deviceId);
|
||||
@@ -7477,6 +7774,9 @@ function applyDeviceImportResolutionInState(
|
||||
const selectedCandidates = draft.candidates.filter((candidate) =>
|
||||
draft.selectedCandidateIds.includes(candidate.candidateId),
|
||||
);
|
||||
const understandingsByCandidate = new Map(
|
||||
deriveDeviceImportProjectUnderstandings(state, draft.draftId).map((item) => [item.candidateId, item] as const),
|
||||
);
|
||||
const importedProjects: Project[] = [];
|
||||
for (const item of resolution.items) {
|
||||
const candidate = draft.candidates.find((entry) => entry.candidateId === item.candidateId);
|
||||
@@ -7517,9 +7817,29 @@ function applyDeviceImportResolutionInState(
|
||||
targetProject.threadMeta.codexFolderRef = candidate.codexFolderRef ?? candidate.folderRef;
|
||||
targetProject.threadMeta.codexThreadRef = candidate.codexThreadRef;
|
||||
targetProject.threadMeta.updatedAt = candidate.lastActiveAt;
|
||||
targetProject.threadMeta.lastObservedCodexActivityAt = candidate.lastActiveAt;
|
||||
targetProject.preview = `已导入 ${candidate.threadDisplayName}`;
|
||||
targetProject.updatedAt = nowIso();
|
||||
targetProject.lastMessageAt = targetProject.updatedAt;
|
||||
const understanding = understandingsByCandidate.get(candidate.candidateId);
|
||||
if (understanding) {
|
||||
applyProjectUnderstandingSnapshotInState(state, {
|
||||
projectId: targetProject.id,
|
||||
account: device.account,
|
||||
snapshot: {
|
||||
projectGoal: understanding.projectGoal,
|
||||
currentProgress: understanding.currentProgress,
|
||||
technicalArchitecture: understanding.technicalArchitecture,
|
||||
currentBlockers: understanding.currentBlockers,
|
||||
recommendedNextStep: understanding.recommendedNextStep,
|
||||
sourceTaskId: understanding.sourceTaskId,
|
||||
updatedAt: understanding.updatedAt,
|
||||
sourceKind: "device_import",
|
||||
},
|
||||
sourceMessageId: understanding.sourceTaskId,
|
||||
sourceKind: "device_import",
|
||||
});
|
||||
}
|
||||
importedProjects.push({ ...targetProject });
|
||||
}
|
||||
|
||||
@@ -7959,7 +8279,7 @@ export async function appendProjectMessage(payload: {
|
||||
kind?: MessageKind;
|
||||
attachments?: MessageAttachment[];
|
||||
}) {
|
||||
const message = await mutateState((state) => {
|
||||
const result = await mutateState((state) => {
|
||||
const project = state.projects.find((item) => item.id === payload.projectId);
|
||||
if (!project) throw new Error("PROJECT_NOT_FOUND");
|
||||
|
||||
@@ -8011,11 +8331,24 @@ export async function appendProjectMessage(payload: {
|
||||
project.lastMessageAt = message.sentAt;
|
||||
project.preview = message.body;
|
||||
|
||||
return message;
|
||||
return {
|
||||
message,
|
||||
shouldQueueUnderstandingSync:
|
||||
payload.sender !== "user" &&
|
||||
isDispatchableThreadProject(project) &&
|
||||
Boolean(project.threadMeta.codexThreadRef?.trim()),
|
||||
};
|
||||
});
|
||||
if (result.shouldQueueUnderstandingSync) {
|
||||
await queueProjectUnderstandingSyncTask({
|
||||
projectId: payload.projectId,
|
||||
observedActivityAt: result.message.sentAt,
|
||||
reason: "thread_reply",
|
||||
});
|
||||
}
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.projectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.projectId });
|
||||
return message;
|
||||
return result.message;
|
||||
}
|
||||
|
||||
export async function appendAttachmentMessage(payload: {
|
||||
|
||||
@@ -246,6 +246,28 @@ function buildRuntimeDigest(
|
||||
.filter((update) => update.status === "available")
|
||||
.map((update) => `${update.version} -> ${update.targetScope}`)
|
||||
.join("\n");
|
||||
const activeProjectUnderstandings = state.projects
|
||||
.filter((project) => project.id !== "master-agent" && project.projectUnderstanding)
|
||||
.sort((left, right) =>
|
||||
String(right.projectUnderstanding?.updatedAt ?? right.lastMessageAt).localeCompare(
|
||||
String(left.projectUnderstanding?.updatedAt ?? left.lastMessageAt),
|
||||
),
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((project) => {
|
||||
const understanding = project.projectUnderstanding!;
|
||||
return [
|
||||
`${project.name}:`,
|
||||
understanding.projectGoal ? `目标=${understanding.projectGoal}` : undefined,
|
||||
understanding.currentProgress ? `进度=${understanding.currentProgress}` : undefined,
|
||||
understanding.technicalArchitecture ? `架构=${understanding.technicalArchitecture}` : undefined,
|
||||
understanding.currentBlockers ? `阻塞=${understanding.currentBlockers}` : undefined,
|
||||
understanding.recommendedNextStep ? `下一步=${understanding.recommendedNextStep}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" / ");
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const authSummary = [
|
||||
`登录会话策略:成功登录后默认保持 ${Math.round(AUTH_SESSION_TTL_MS / 24 / 60 / 60_000)} 天。`,
|
||||
@@ -265,6 +287,9 @@ function buildRuntimeDigest(
|
||||
"最新 APP 日志:",
|
||||
recentLogs || "无",
|
||||
"",
|
||||
"活跃项目理解:",
|
||||
activeProjectUnderstandings || "无",
|
||||
"",
|
||||
"高风险线程:",
|
||||
riskyThreads || "无",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user