feat: add structured message forwarding payloads

This commit is contained in:
kris
2026-03-28 07:20:49 +08:00
parent 227d270505
commit 9613c3c154
2 changed files with 272 additions and 39 deletions

View File

@@ -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>) {