Files
deerflow-code/frontend-web/src/report-collaboration/state/message-adapter.ts
T
2026-09-08 09:40:31 +08:00

291 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 无损协议适配:CollaborationMessage → LangGraph SDK Message + BaseStream 外观。
*
* 规则(FE-5):
* - 一对一映射,不概括文本、不吞工具结果。
* - 工具调用挂在所属 ai 消息的 `tool_calls`;返回为 `tool` 消息,`tool_call_id`
* 严格配对(现有 MessageGroup 直接渲染)。
* - AgentScope 归属写入 `additional_kwargs.report_collaboration`。
* - 业务卡(方案组 / 命令确认 / 报告入口)为**前端派生的虚拟 ai 消息**,经
* MessageList 的通用业务卡插槽(`additional_kwargs.report_collaboration_card`)
* 渲染;未传插槽的页面零渲染。
*/
import type { Message } from "@langchain/langgraph-sdk";
import type { BaseStream } from "@langchain/langgraph-sdk/react";
import type { AgentThreadState } from "@/core/threads";
import type {
AgentRun,
CollaborationCommand,
CollaborationMessage,
ReportPlanCandidate,
ReportRequirementSnapshot,
ReportVersion,
} from "@/report-collaboration/api/types";
/** 报告在沙箱中的虚拟路径,对齐问答页 write_file / 深度研究 stub。 */
export const REPORT_OUTPUT_PATH = "/mnt/user-data/outputs/report.md";
// ── 归属标记 ─────────────────────────────────────────────────────────────────
export interface ReportCollaborationKwargs {
agentRunId?: string;
nodeRunId?: string;
phaseId?: string;
/** 消息创建时间,供会商式说话人行展示。 */
createdAt?: string;
/** 前端乐观消息(尚未被服务器确认)。 */
optimistic?: boolean;
}
export function collaborationKwargs(message: CollaborationMessage): ReportCollaborationKwargs {
const kwargs: ReportCollaborationKwargs = {};
if (message.agent_run_id) kwargs.agentRunId = message.agent_run_id;
if (message.node_run_id) kwargs.nodeRunId = message.node_run_id;
if (message.phase_id) kwargs.phaseId = message.phase_id;
if (message.created_at) kwargs.createdAt = message.created_at;
return kwargs;
}
// ── 一对一映射 ───────────────────────────────────────────────────────────────
export function toLangGraphMessage(message: CollaborationMessage): Message {
const additional_kwargs: Record<string, unknown> = {};
const rc = collaborationKwargs(message);
if (Object.keys(rc).length > 0) {
additional_kwargs.report_collaboration = rc;
}
if (message.role === "human") {
return {
id: message.message_id,
type: "human",
content: message.content,
additional_kwargs,
} as unknown as Message;
}
if (message.role === "tool") {
return {
id: message.message_id,
type: "tool",
content: message.content,
name: message.name ?? undefined,
tool_call_id: message.tool_call_id ?? "",
additional_kwargs,
} as unknown as Message;
}
// 思考过程:与问答页 additional_kwargs.reasoning_content 对齐,供 MessageGroup CoT 渲染。
const reasoning = message.metadata?.reasoning_content;
if (typeof reasoning === "string" && reasoning.length > 0) {
additional_kwargs.reasoning_content = reasoning;
}
// ai:tool_calls 原样透传(args 流式期间为部分 JSON 字符串)。
return {
id: message.message_id,
type: "ai",
content: message.content,
name: message.name ?? undefined,
tool_calls: message.tool_calls?.length
? message.tool_calls.map((call) => ({
id: call.id,
name: call.name,
args: call.args,
}))
: undefined,
additional_kwargs,
} as unknown as Message;
}
export function toLangGraphMessages(messages: CollaborationMessage[]): Message[] {
return messages.map(toLangGraphMessage);
}
// ── 虚拟业务卡 ───────────────────────────────────────────────────────────────
export type ReportCollaborationCardPayload =
| {
kind: "plan_group";
plans: ReportPlanCandidate[];
selectedPlanId: string | null;
requirementRevision: number;
}
| {
kind: "requirement_ready";
requirement: ReportRequirementSnapshot;
revision: number;
}
| {
kind: "command_confirm";
command: CollaborationCommand;
}
| {
kind: "report_available";
version: ReportVersion;
versionCount: number;
};
export function hasReportCollaborationCard(message: Message): boolean {
return (
message.type === "ai" &&
typeof (message.additional_kwargs as Record<string, unknown> | undefined)
?.report_collaboration_card === "object"
);
}
export function readReportCollaborationCard(
message: Message,
): ReportCollaborationCardPayload | null {
const payload = (message.additional_kwargs as Record<string, unknown> | undefined)
?.report_collaboration_card;
return (payload as ReportCollaborationCardPayload) ?? null;
}
function buildCardMessage(id: string, payload: ReportCollaborationCardPayload): Message {
return {
id,
type: "ai",
content: "",
additional_kwargs: { report_collaboration_card: payload },
} as unknown as Message;
}
/** 方案组卡(proposal_ready 时注入时间线,点击预览 / 确认后执行)。 */
export function buildPlanGroupCard(
plans: ReportPlanCandidate[],
selectedPlanId: string | null,
requirementRevision: number,
): Message {
const groupId = plans[0]?.proposal_group_id ?? "plans";
return buildCardMessage(`rc-card-plans-${groupId}-r${requirementRevision}`, {
kind: "plan_group",
plans,
selectedPlanId,
requirementRevision,
});
}
/** 需求摘要卡(计划模式:需求已就绪、尚未生成方案)。 */
export function buildRequirementReadyCard(
requirement: ReportRequirementSnapshot,
): Message {
return buildCardMessage(`rc-card-requirement-r${requirement.revision}`, {
kind: "requirement_ready",
requirement,
revision: requirement.revision,
});
}
/** 命令确认卡(command.confirmation.required 之后)。 */
export function buildCommandConfirmCard(command: CollaborationCommand): Message {
return buildCardMessage(`rc-card-confirm-${command.command_id}`, {
kind: "command_confirm",
command,
});
}
/** 报告入口卡(正式版本落库后)。 */
export function buildReportAvailableCard(version: ReportVersion, versionCount: number): Message {
return buildCardMessage(`rc-card-report-${version.id}`, {
kind: "report_available",
version,
versionCount,
});
}
export interface ReportWriteFileSource {
sessionId: string;
markdown: string;
writing: boolean;
writer?: AgentRun | null;
}
/**
* 把 report.delta / 正式版本投影成原生 write_file 消息,让 MessageList
* 步骤条和 ArtifactFileDetail 沙箱走与问答页相同的渲染路径。
*/
export function buildReportWriteFileMessages(source: ReportWriteFileSource): Message[] {
const { markdown, writing, writer } = source;
if (!writing && !markdown) return [];
const sessionId = source.sessionId || "session";
const messageId = `rc-write-${sessionId}-report`;
const toolCallId = `${messageId}-file`;
const additional_kwargs: Record<string, unknown> = {};
if (writer) {
additional_kwargs.report_collaboration = {
agentRunId: writer.agent_run_id,
nodeRunId: writer.node_run_id ?? undefined,
phaseId: writer.phase_id ?? undefined,
createdAt: writer.started_at ?? undefined,
};
}
const aiMessage = {
id: messageId,
type: "ai" as const,
content: "",
name: writer?.display_name ?? "撰写员",
tool_calls: [
{
id: toolCallId,
name: "write_file",
args: { path: REPORT_OUTPUT_PATH, content: markdown },
},
],
additional_kwargs,
} as unknown as Message;
if (writing) return [aiMessage];
const toolResult = {
id: `${toolCallId}-result`,
type: "tool" as const,
name: "write_file",
tool_call_id: toolCallId,
content: "ok",
additional_kwargs,
} as unknown as Message;
return [aiMessage, toolResult];
}
/** 注入的 write_file 行在聚焦某成员时只保留撰写员自己的。 */
export function reportWriteFileBelongsToFocus(
messages: Message[],
focusedAgentRunId: string | null,
): Message[] {
if (!focusedAgentRunId) return messages;
return messages.filter((message) => {
const kwargs = (message.additional_kwargs as Record<string, unknown> | undefined)
?.report_collaboration as { agentRunId?: string } | undefined;
return kwargs?.agentRunId === focusedAgentRunId;
});
}
// ── BaseStream 外观 ─────────────────────────────────────────────────────────
/**
* 构造 MessageList 需要的最小 `BaseStream<AgentThreadState>`:
* MessageList 只读 `messages` / `isLoading` / `isThreadLoading`。
*/
export function buildBaseStreamFacade(
messages: Message[],
isLoading: boolean,
): BaseStream<AgentThreadState> {
return {
messages,
values: { title: "", messages, artifacts: [], todos: [] },
isLoading,
isThreadLoading: false,
error: undefined,
interrupt: undefined,
stop: async () => undefined,
submit: async () => undefined,
joinStream: async () => undefined,
} as unknown as BaseStream<AgentThreadState>;
}