feat: add structured message forwarding payloads
This commit is contained in:
@@ -2,6 +2,18 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { forwardProjectMessage } from "@/lib/boss-data";
|
||||
|
||||
type ForwardBody =
|
||||
| {
|
||||
mode?: "single";
|
||||
targetProjectId?: string;
|
||||
sourceMessageId?: string;
|
||||
}
|
||||
| {
|
||||
mode?: "bundle";
|
||||
targetProjectId?: string;
|
||||
sourceMessageIds?: string[];
|
||||
};
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string }> },
|
||||
@@ -11,25 +23,60 @@ export async function POST(
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { projectId } = await context.params;
|
||||
const body = (await request.json()) as {
|
||||
targetProjectId?: string;
|
||||
note?: string;
|
||||
};
|
||||
const body = (await request.json()) as ForwardBody;
|
||||
|
||||
if (!body.targetProjectId || !body.note) {
|
||||
const mode = body.mode ?? "single";
|
||||
const targetProjectId = body.targetProjectId;
|
||||
const sourceMessageId: string | undefined =
|
||||
"sourceMessageId" in body ? body.sourceMessageId : undefined;
|
||||
const sourceMessageIds: string[] =
|
||||
"sourceMessageIds" in body && Array.isArray(body.sourceMessageIds)
|
||||
? body.sourceMessageIds
|
||||
: [];
|
||||
|
||||
if (!targetProjectId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "缺少 targetProjectId 或 note" },
|
||||
{ ok: false, message: "缺少 targetProjectId" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (mode === "bundle") {
|
||||
if (sourceMessageIds.length <= 1) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "bundle 转发至少需要 2 条 sourceMessageIds" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
} else if (!sourceMessageId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "single 转发缺少 sourceMessageId" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await forwardProjectMessage({
|
||||
sourceProjectId: projectId,
|
||||
targetProjectId: body.targetProjectId,
|
||||
note: body.note,
|
||||
const result =
|
||||
mode === "bundle"
|
||||
? await forwardProjectMessage({
|
||||
sourceProjectId: projectId,
|
||||
mode: "bundle",
|
||||
targetProjectId,
|
||||
sourceMessageIds,
|
||||
requestedBy: session.account,
|
||||
})
|
||||
: await forwardProjectMessage({
|
||||
sourceProjectId: projectId,
|
||||
mode: "single",
|
||||
targetProjectId,
|
||||
sourceMessageId: sourceMessageId ?? "",
|
||||
requestedBy: session.account,
|
||||
});
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message: result.message ?? null,
|
||||
approvalRequired: Boolean(result.approvalRequired),
|
||||
approvalReason: result.approvalReason ?? null,
|
||||
});
|
||||
return NextResponse.json({ ok: true, message });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
|
||||
@@ -11,12 +11,45 @@ export type DeviceStatus = "online" | "abnormal" | "offline";
|
||||
export type DeviceSource = "production" | "demo";
|
||||
export type GoalState = "pending" | "completed";
|
||||
export type MessageSender = "master" | "device" | "user" | "ops" | "audit";
|
||||
// Forwarding uses a structured contract so the route can distinguish
|
||||
// single-message forwarding, bundle forwarding, and the legacy notice shape.
|
||||
export type MessageKind =
|
||||
| "text"
|
||||
| "voice_intent"
|
||||
| "image_intent"
|
||||
| "video_intent"
|
||||
| "forward_notice";
|
||||
| "forward_notice"
|
||||
| "forward_single"
|
||||
| "forward_bundle";
|
||||
|
||||
export interface ForwardSource {
|
||||
sourceProjectId: string;
|
||||
sourceProjectName: string;
|
||||
sourceThreadId?: string;
|
||||
sourceThreadTitle?: string;
|
||||
sourceMessageId: string;
|
||||
forwardedBy: string;
|
||||
forwardedAt: string;
|
||||
}
|
||||
|
||||
export interface ForwardBundleItem {
|
||||
messageId: string;
|
||||
senderLabel: string;
|
||||
body: string;
|
||||
kind: string;
|
||||
sentAt: string;
|
||||
}
|
||||
|
||||
export interface ForwardBundlePayload {
|
||||
sourceProjectId: string;
|
||||
sourceProjectName: string;
|
||||
sourceThreadId?: string;
|
||||
sourceThreadTitle?: string;
|
||||
itemCount: number;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
items: ForwardBundleItem[];
|
||||
}
|
||||
export type ContextBudgetLevel = "safe" | "watch" | "urgent" | "critical";
|
||||
export type ThreadState =
|
||||
| "idle"
|
||||
@@ -113,6 +146,8 @@ export interface Message {
|
||||
body: string;
|
||||
sentAt: string;
|
||||
kind?: MessageKind;
|
||||
forwardSource?: ForwardSource;
|
||||
forwardBundle?: ForwardBundlePayload;
|
||||
}
|
||||
|
||||
export interface GoalItem {
|
||||
@@ -1807,6 +1842,8 @@ function normalizeMessage(raw: Partial<Message>): Message {
|
||||
body: raw.body ?? "",
|
||||
sentAt: raw.sentAt ?? nowIso(),
|
||||
kind: raw.kind ?? "text",
|
||||
forwardSource: raw.forwardSource,
|
||||
forwardBundle: raw.forwardBundle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4250,49 +4287,198 @@ export async function appendProjectMessage(payload: {
|
||||
return message;
|
||||
}
|
||||
|
||||
export async function forwardProjectMessage(payload: {
|
||||
sourceProjectId: string;
|
||||
targetProjectId: string;
|
||||
note: string;
|
||||
function findProjectMessage(project: Project, messageId: string) {
|
||||
return project.messages.find((message) => message.id === messageId) ?? null;
|
||||
}
|
||||
|
||||
function requiresForwardApproval(source: Project, target: Project) {
|
||||
return source.collaborationMode === "approval_required" && target.id !== "master-agent";
|
||||
}
|
||||
|
||||
function buildForwardSingleMessage(input: {
|
||||
source: Project;
|
||||
target: Project;
|
||||
message: Message;
|
||||
requestedBy: string;
|
||||
}) {
|
||||
const sentAt = nowIso();
|
||||
const body = `转发自《${input.source.name}》到《${input.target.name}》:${input.message.body}`;
|
||||
return {
|
||||
id: randomToken("forward"),
|
||||
sender: "user" as const,
|
||||
senderLabel: "你",
|
||||
body,
|
||||
sentAt,
|
||||
kind: "forward_single" as const,
|
||||
forwardSource: {
|
||||
sourceProjectId: input.source.id,
|
||||
sourceProjectName: input.source.name,
|
||||
sourceThreadId: input.source.threadMeta?.threadId,
|
||||
sourceThreadTitle: input.source.threadMeta?.threadDisplayName,
|
||||
sourceMessageId: input.message.id,
|
||||
forwardedBy: input.requestedBy,
|
||||
forwardedAt: sentAt,
|
||||
},
|
||||
} satisfies Message;
|
||||
}
|
||||
|
||||
function buildForwardBundleMessage(input: {
|
||||
source: Project;
|
||||
target: Project;
|
||||
messages: Message[];
|
||||
requestedBy: string;
|
||||
}) {
|
||||
const sentAt = nowIso();
|
||||
const startedAt = input.messages[0]?.sentAt ?? sentAt;
|
||||
const endedAt = input.messages[input.messages.length - 1]?.sentAt ?? sentAt;
|
||||
const body = `转发自《${input.source.name}》到《${input.target.name}》:${input.messages.length} 条消息,最后一条:${
|
||||
input.messages[input.messages.length - 1]?.body ?? ""
|
||||
}`;
|
||||
return {
|
||||
id: randomToken("forward"),
|
||||
sender: "user" as const,
|
||||
senderLabel: "你",
|
||||
body,
|
||||
sentAt,
|
||||
kind: "forward_bundle" as const,
|
||||
forwardBundle: {
|
||||
sourceProjectId: input.source.id,
|
||||
sourceProjectName: input.source.name,
|
||||
sourceThreadId: input.source.threadMeta?.threadId,
|
||||
sourceThreadTitle: input.source.threadMeta?.threadDisplayName,
|
||||
itemCount: input.messages.length,
|
||||
startedAt,
|
||||
endedAt,
|
||||
items: input.messages.map((message) => ({
|
||||
messageId: message.id,
|
||||
senderLabel: message.senderLabel,
|
||||
body: message.body,
|
||||
kind: message.kind ?? "text",
|
||||
sentAt: message.sentAt,
|
||||
})),
|
||||
},
|
||||
} satisfies Message;
|
||||
}
|
||||
|
||||
export async function forwardProjectMessage(
|
||||
payload:
|
||||
| {
|
||||
sourceProjectId: string;
|
||||
mode: "single";
|
||||
targetProjectId: string;
|
||||
sourceMessageId: string;
|
||||
requestedBy: string;
|
||||
}
|
||||
| {
|
||||
sourceProjectId: string;
|
||||
mode: "bundle";
|
||||
targetProjectId: string;
|
||||
sourceMessageIds: string[];
|
||||
requestedBy: string;
|
||||
},
|
||||
) {
|
||||
const state = await readState();
|
||||
const source = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||||
const target = state.projects.find((item) => item.id === payload.targetProjectId);
|
||||
if (!source || !target) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (requiresForwardApproval(source, target)) {
|
||||
return {
|
||||
approvalRequired: true,
|
||||
approvalReason: "NON_DEVELOPMENT_THREAD_FORWARD",
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (payload.mode === "single") {
|
||||
const sourceMessage = findProjectMessage(source, payload.sourceMessageId);
|
||||
if (!sourceMessage) throw new Error("MESSAGE_NOT_FOUND");
|
||||
|
||||
const message = await mutateState((state) => {
|
||||
const sourceProject = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||||
const targetProject = state.projects.find((item) => item.id === payload.targetProjectId);
|
||||
if (!sourceProject || !targetProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
const sourceLedgerMessage = findProjectMessage(sourceProject, payload.sourceMessageId);
|
||||
if (!sourceLedgerMessage) throw new Error("MESSAGE_NOT_FOUND");
|
||||
|
||||
const message = buildForwardSingleMessage({
|
||||
source: sourceProject,
|
||||
target: targetProject,
|
||||
message: sourceLedgerMessage,
|
||||
requestedBy: payload.requestedBy,
|
||||
});
|
||||
|
||||
targetProject.messages.push(message);
|
||||
targetProject.unreadCount += 1;
|
||||
targetProject.lastMessageAt = message.sentAt;
|
||||
targetProject.preview = message.body;
|
||||
|
||||
sourceProject.messages.push({
|
||||
id: randomToken("forward-log"),
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: `已转发到《${targetProject.name}》。`,
|
||||
sentAt: message.sentAt,
|
||||
kind: "forward_notice",
|
||||
});
|
||||
sourceProject.lastMessageAt = message.sentAt;
|
||||
return message;
|
||||
});
|
||||
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.sourceProjectId });
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.targetProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.sourceProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.targetProjectId });
|
||||
return { message };
|
||||
}
|
||||
|
||||
const sourceMessageIds = payload.mode === "bundle" ? payload.sourceMessageIds : [];
|
||||
const sourceMessages = sourceMessageIds
|
||||
.map((messageId) => findProjectMessage(source, messageId))
|
||||
.filter((message): message is Message => Boolean(message));
|
||||
if (sourceMessages.length <= 1 || sourceMessages.length !== sourceMessageIds.length) {
|
||||
throw new Error("MESSAGE_NOT_FOUND");
|
||||
}
|
||||
|
||||
const message = await mutateState((state) => {
|
||||
const source = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||||
const target = state.projects.find((item) => item.id === payload.targetProjectId);
|
||||
if (!source || !target) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (!payload.note.trim()) throw new Error("FORWARD_NOTE_REQUIRED");
|
||||
const sourceProject = state.projects.find((item) => item.id === payload.sourceProjectId);
|
||||
const targetProject = state.projects.find((item) => item.id === payload.targetProjectId);
|
||||
if (!sourceProject || !targetProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
|
||||
const sentAt = nowIso();
|
||||
const forwardBody = `转发自《${source.name}》:${payload.note.trim()}`;
|
||||
const message: Message = {
|
||||
id: randomToken("forward"),
|
||||
sender: "user",
|
||||
senderLabel: "你",
|
||||
body: forwardBody,
|
||||
sentAt,
|
||||
kind: "forward_notice",
|
||||
};
|
||||
const bundleMessages = sourceMessageIds
|
||||
.map((messageId) => findProjectMessage(sourceProject, messageId))
|
||||
.filter((item): item is Message => Boolean(item));
|
||||
if (bundleMessages.length <= 1 || bundleMessages.length !== sourceMessageIds.length) {
|
||||
throw new Error("MESSAGE_NOT_FOUND");
|
||||
}
|
||||
|
||||
target.messages.push(message);
|
||||
target.unreadCount += 1;
|
||||
target.lastMessageAt = sentAt;
|
||||
target.preview = forwardBody;
|
||||
const message = buildForwardBundleMessage({
|
||||
source: sourceProject,
|
||||
target: targetProject,
|
||||
messages: bundleMessages,
|
||||
requestedBy: payload.requestedBy,
|
||||
});
|
||||
|
||||
source.messages.push({
|
||||
targetProject.messages.push(message);
|
||||
targetProject.unreadCount += 1;
|
||||
targetProject.lastMessageAt = message.sentAt;
|
||||
targetProject.preview = message.body;
|
||||
|
||||
sourceProject.messages.push({
|
||||
id: randomToken("forward-log"),
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent",
|
||||
body: `已把消息转发到《${target.name}》。`,
|
||||
sentAt,
|
||||
body: `已转发到《${targetProject.name}》。`,
|
||||
sentAt: message.sentAt,
|
||||
kind: "forward_notice",
|
||||
});
|
||||
source.lastMessageAt = sentAt;
|
||||
sourceProject.lastMessageAt = message.sentAt;
|
||||
return message;
|
||||
});
|
||||
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.sourceProjectId });
|
||||
publishBossEvent("project.messages.updated", { projectId: payload.targetProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.sourceProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: payload.targetProjectId });
|
||||
return message;
|
||||
return { message };
|
||||
}
|
||||
|
||||
export async function updateUserSettings(settings: Partial<UserSettings>) {
|
||||
|
||||
Reference in New Issue
Block a user