291 lines
9.4 KiB
TypeScript
291 lines
9.4 KiB
TypeScript
/**
|
||
* 无损协议适配: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>;
|
||
}
|