Files
deerflow-code/frontend-web/src/roundtable-planning/lib/roundtable-constants.ts
T
2026-05-27 20:28:00 +08:00

138 lines
4.0 KiB
TypeScript

/**
* 圆桌规划共享常量与小工具函数。
*
* 这里只放与具体 React 状态无关的纯数据 / 纯函数。复杂的 dialogue 管理、
* 编排循环、API 状态等都在 `hooks/` 与 `components/` 里维护。
*/
import type { IntentSummary } from "@/roundtable-planning/api/intent";
/** 指挥员(人类)所在的固定 seat id。Agent 席位从 2 开始排。 */
export const COMMANDER_SEAT_ID = 1;
/** 编排循环最大轮次。超出后 toast 强停以防死循环。 */
export const MAX_CYCLES = 8;
/** Step 2 角色面板的一行。 */
export type RoleEntry = {
id: number;
name: string;
agentId?: string;
type: "human" | "agent";
status: string;
};
/** 用户在推荐弹窗选定的一个研讨席位。 */
export type SelectedAgent = { agent_id: string; name: string };
/** 研讨席位的头像配色轮换池。 */
export const FALLBACK_AVATAR_TYPES = [
"blue",
"purple",
"emerald",
"amber",
"pink",
] as const;
export function getAvatarTypeFor(_agentId: string, index: number): string {
return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length];
}
export function getDisplayNameFor(agent: SelectedAgent): string {
return agent.name;
}
/**
* 右侧角色面板的模板列表。指挥员固定 id=1,agents 按用户在推荐弹窗里的
* 选择顺序排 id=2..N+1,与可视化圆桌 + agentNameToRole 保持一致。
*/
export function buildRoleTemplates(
agents: SelectedAgent[],
): ReadonlyArray<Omit<RoleEntry, "status">> {
const out: Omit<RoleEntry, "status">[] = [
{ id: COMMANDER_SEAT_ID, name: "指挥员 / 主持人", type: "human" },
];
agents.forEach((a, i) => {
out.push({
id: i + 2,
name: getDisplayNameFor(a),
agentId: a.agent_id,
type: "agent",
});
});
return out;
}
export function makeRolesFromTemplates(
templates: ReadonlyArray<Omit<RoleEntry, "status">>,
defaultStatus: string,
overrides: Record<number, string> = {},
): RoleEntry[] {
return templates.map((t) => ({
...t,
status: overrides[t.id] ?? defaultStatus,
}));
}
/**
* `agent_name`(小写)→ bubble meta 映射。流式输出时根据它反查
* speakerId / 头像 / 中文名,保证可视化圆桌的高亮和气泡渲染对齐。
*/
export function buildAgentNameToRole(
agents: SelectedAgent[],
): Record<string, { speakerId: number; avatarType: string; displayName: string }> {
const out: Record<
string,
{ speakerId: number; avatarType: string; displayName: string }
> = {};
agents.forEach((a, i) => {
out[a.agent_id] = {
speakerId: i + 2,
avatarType: getAvatarTypeFor(a.agent_id, i),
displayName: getDisplayNameFor(a),
};
});
return out;
}
export function nowTime(): string {
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
}
export function newCoordinatorName(): string {
return `roundtable-coordinator-${Date.now().toString(36)}`;
}
/**
* 后端 intent agent 输出的 `[INTENT_READY]\n\`\`\`json {...}\`\`\`` 块。
* 前端用同样形状的正则把摘要从对话气泡里抽出来,渲染成绿色摘要卡片。
*/
export const INTENT_READY_RE = /\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
/**
* 把含 [INTENT_READY] 标记的气泡正文拆成前 / 摘要 / 后三段。
* 摘要 JSON 格式异常时整体保留在 `before`,用户仍能看到原文。
*/
export function splitIntentReadyBlock(text: string): {
before: string;
summary: IntentSummary | null;
after: string;
} {
if (!text) return { before: text, summary: null, after: "" };
const m = INTENT_READY_RE.exec(text);
if (!m) return { before: text, summary: null, after: "" };
let summary: IntentSummary | null = null;
try {
const parsed = JSON.parse(m[1]);
if (parsed && typeof parsed === "object" && typeof parsed.objective === "string") {
summary = parsed as IntentSummary;
}
} catch {
return { before: text, summary: null, after: "" };
}
return {
before: text.slice(0, m.index).trimEnd(),
summary,
after: text.slice(m.index + m[0].length).trimStart(),
};
}