抽出专用多轮RAG消息列表并升级会话模型;强化报告协作画布与检查器;业务链普通用户改为提交审核;扩展 agentfx 报告技能种子,并将含 admin 的用户名自动提权为管理员。 Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
"use client";
|
||
|
||
import { memo, type ReactNode } from "react";
|
||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||
import {
|
||
AlertTriangle,
|
||
Bot,
|
||
Check,
|
||
CircleArrowRight,
|
||
CircleDashed,
|
||
FileCheck,
|
||
Flag,
|
||
LineChart,
|
||
Loader2,
|
||
PenLine,
|
||
RefreshCw,
|
||
Search,
|
||
ShieldCheck,
|
||
ShieldQuestion,
|
||
Users,
|
||
X,
|
||
} from "lucide-react";
|
||
|
||
import type { CollaborationNodeStatus } from "@/report-collaboration/api/types";
|
||
|
||
export type ReportFlowNodeKind = "start" | "end" | "agent";
|
||
|
||
/** 画布自定义节点数据(由 ReportFlowCanvas 组装)。 */
|
||
export interface ReportFlowNodeDatum {
|
||
nodeId: string;
|
||
label: string;
|
||
roleName?: string | null;
|
||
angle?: string | null;
|
||
status: CollaborationNodeStatus;
|
||
attempt: number;
|
||
selected: boolean;
|
||
kind: ReportFlowNodeKind;
|
||
tools: string[];
|
||
/** xyflow Node.data 需要 Record 兼容的索引签名。 */
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
export type ReportFlowNodeType = "collaboration";
|
||
|
||
const STATUS_META: Record<
|
||
CollaborationNodeStatus,
|
||
{ text: string; color: string; icon: ReactNode }
|
||
> = {
|
||
planned: { text: "规划中", color: "var(--rc-text-secondary)", icon: <CircleDashed className="size-3.5" /> },
|
||
queued: { text: "排队中", color: "var(--rc-text-secondary)", icon: <CircleDashed className="size-3.5" /> },
|
||
running: { text: "运行中", color: "var(--rc-running)", icon: <Loader2 className="rc-spin size-3.5" /> },
|
||
validating: { text: "校验中", color: "var(--rc-running)", icon: <ShieldQuestion className="size-3.5" /> },
|
||
completed: { text: "已完成", color: "var(--rc-success)", icon: <Check className="size-3.5" /> },
|
||
failed: { text: "失败", color: "var(--rc-danger)", icon: <X className="size-3.5" /> },
|
||
awaiting_input: { text: "等待用户", color: "var(--rc-waiting)", icon: <AlertTriangle className="size-3.5" /> },
|
||
cancel_requested: { text: "取消中", color: "var(--rc-waiting)", icon: <X className="size-3.5" /> },
|
||
cancelled: { text: "已取消", color: "var(--rc-text-secondary)", icon: <X className="size-3.5" /> },
|
||
superseded: { text: "已被替代", color: "var(--rc-text-secondary)", icon: <RefreshCw className="size-3.5" /> },
|
||
reopened: { text: "已重开", color: "var(--rc-waiting)", icon: <RefreshCw className="size-3.5" /> },
|
||
interrupted: { text: "已中断", color: "var(--rc-waiting)", icon: <AlertTriangle className="size-3.5" /> },
|
||
};
|
||
|
||
export function roleIcon(roleName: string | null | undefined, kind: ReportFlowNodeKind = "agent"): ReactNode {
|
||
if (kind === "start") return <CircleArrowRight />;
|
||
if (kind === "end") return <Flag />;
|
||
switch (roleName) {
|
||
case "协调者":
|
||
return <Users />;
|
||
case "研究员":
|
||
return <Search />;
|
||
case "核验员":
|
||
return <ShieldCheck />;
|
||
case "分析员":
|
||
return <LineChart />;
|
||
case "撰写员":
|
||
return <PenLine />;
|
||
case "审稿员":
|
||
return <FileCheck />;
|
||
default:
|
||
return <Bot />;
|
||
}
|
||
}
|
||
|
||
function FlowPill({ children }: { children: ReactNode }) {
|
||
return <span className="rc-flow-pill rc-flow-pill-neutral">{children}</span>;
|
||
}
|
||
|
||
function FieldRow({ label, children }: { label: string; children: ReactNode }) {
|
||
return (
|
||
<>
|
||
<div className="rc-flow-node-field-label">{label}</div>
|
||
<div className="rc-flow-node-field-value">{children}</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function CollaborationFlowNodeComponent({ data }: NodeProps) {
|
||
const node = data as unknown as ReportFlowNodeDatum;
|
||
const meta = STATUS_META[node.status] ?? STATUS_META.planned;
|
||
const statusText = node.attempt > 1 ? `${meta.text} · 第 ${node.attempt} 次` : meta.text;
|
||
|
||
return (
|
||
<div
|
||
className="rc-flow-node"
|
||
data-status={node.status}
|
||
data-selected={node.selected}
|
||
data-kind={node.kind}
|
||
data-role={node.roleName ?? ""}
|
||
tabIndex={0}
|
||
role="button"
|
||
aria-label={`节点 ${node.label},${statusText}`}
|
||
>
|
||
<Handle type="target" position={Position.Left} className="rc-flow-handle" isConnectable={false} />
|
||
<div className="rc-flow-node-header">
|
||
<span className="rc-flow-node-icon" aria-hidden>
|
||
{roleIcon(node.roleName, node.kind)}
|
||
</span>
|
||
<span className="rc-flow-node-title">{node.label}</span>
|
||
</div>
|
||
<div className="rc-flow-node-body">
|
||
{node.roleName ? (
|
||
<FieldRow label="角色">
|
||
<FlowPill>{node.roleName}</FlowPill>
|
||
</FieldRow>
|
||
) : null}
|
||
{node.angle ? (
|
||
<FieldRow label="角度">
|
||
<FlowPill>{node.angle}</FlowPill>
|
||
</FieldRow>
|
||
) : null}
|
||
{node.tools.length > 0 ? (
|
||
<FieldRow label="技能">
|
||
{node.tools.map((tool) => (
|
||
<FlowPill key={`tool-${tool}`}>{tool}</FlowPill>
|
||
))}
|
||
</FieldRow>
|
||
) : null}
|
||
<FieldRow label="状态">
|
||
<span className="rc-flow-node-status" style={{ color: meta.color }}>
|
||
{meta.icon}
|
||
<span>{statusText}</span>
|
||
</span>
|
||
</FieldRow>
|
||
</div>
|
||
<Handle type="source" position={Position.Right} className="rc-flow-handle" isConnectable={false} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export const CollaborationFlowNode = memo(CollaborationFlowNodeComponent);
|