fix: include full v0.93.0 release changes

This commit is contained in:
coso
2026-03-22 00:50:45 +08:00
parent 7765a3b029
commit b48d5b874f
19 changed files with 1507 additions and 235 deletions
@@ -3350,7 +3350,7 @@ export function AgentChatWorkspace({
const {
runtimeTeamState,
clearRuntimeTeamState,
handleRuntimeTeamAfterSend,
prepareRuntimeTeamBeforeSend,
} = useRuntimeTeamFormation({
activeTheme,
projectId,
@@ -6219,6 +6219,46 @@ export function AgentChatWorkspace({
}
}
const preparedRuntimeTeamState = await prepareRuntimeTeamBeforeSend({
input: sourceText,
providerType,
model: effectiveModel,
executionStrategy: sendExecutionStrategy ?? executionStrategy,
purpose: sendOptions?.purpose,
subagentEnabled: effectiveToolPreferences.subagent,
});
const turnTeamBlueprint =
preparedRuntimeTeamState?.status === "formed" &&
preparedRuntimeTeamState.members.length > 0
? {
label: preparedRuntimeTeamState.label?.trim() || undefined,
description:
preparedRuntimeTeamState.summary?.trim() || undefined,
roles: preparedRuntimeTeamState.members.map((member) => ({
id: member.id,
label: member.label,
summary: member.summary,
profileId: member.profileId,
roleKey: member.roleKey,
skillIds: [...member.skillIds],
})),
}
: undefined;
const turnTeamDecision =
preparedRuntimeTeamState?.status === "formed"
? "team_prepared"
: "single_agent";
const turnTeamReason =
preparedRuntimeTeamState?.status === "formed"
? "runtime_team_prepared"
: preparedRuntimeTeamState?.status === "failed"
? "runtime_team_generation_failed"
: !effectiveToolPreferences.subagent
? "subagent_disabled"
: sendOptions?.purpose
? "turn_purpose_override"
: "single_agent_direct";
setInput("");
setMentionedCharacters([]); // 清空引用的角色
@@ -6256,6 +6296,9 @@ export function AgentChatWorkspace({
selectedTeamLabel,
selectedTeamSummary,
selectedTeamRoles: selectedTeam?.roles,
turnTeamDecision,
turnTeamReason,
turnTeamBlueprint,
}),
},
};
@@ -6288,14 +6331,6 @@ export function AgentChatWorkspace({
finalizeAfterSendSuccess(sendBoundary);
handleRuntimeTeamAfterSend({
input: sourceText,
providerType,
model: effectiveModel,
executionStrategy: sendExecutionStrategy ?? executionStrategy,
purpose: sendOptions?.purpose,
});
return true;
} catch (error) {
rollbackAfterSendFailure(sendBoundary);
@@ -6332,9 +6367,9 @@ export function AgentChatWorkspace({
selectedTeamSummary,
providerType,
finalizeAfterSendSuccess,
handleRuntimeTeamAfterSend,
isBlockedByBrowserPreflight,
maybeStartBrowserTaskPreflight,
prepareRuntimeTeamBeforeSend,
resolveSendBoundary,
resolveSendProviderContext,
rollbackAfterSendFailure,
@@ -10396,7 +10431,7 @@ export function AgentChatWorkspace({
selectedTeamLabel ||
"Team Workbench",
subtitle: hasRealTeamGraph
? "画布展示多成员实时协作,右侧侧栏展示总览与角色结构。"
? "主对话保留调度记录,画布按角色分别展示执行过程与结果。"
: runtimeTeamState?.status === "forming"
? "正在根据本轮任务准备 Team,随后会在画布中展示成员编队。"
: runtimeTeamState?.status === "formed"
@@ -196,7 +196,7 @@ export function TeamWorkbenchSummaryPanel({
? runtimeTeamState.errorMessage?.trim() ||
"本轮 Team 准备失败,可继续在当前对话中推进。"
: runtimeSummaryText ||
"这里展示团队总览、运行密度和最近动态;详细协作过程在左侧主画布查看。"}
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
</p>
{!hasRealTeamGraph && runtimeTeamState ? (
<div
@@ -293,6 +293,10 @@ describe("TeamWorkspaceBoard", () => {
});
expect(container.textContent).toContain("Team Workspace");
expect(container.textContent).toContain("角色执行画布");
expect(container.textContent).toContain(
"主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。",
);
expect(container.textContent).toContain("研究员");
expect(container.textContent).toContain("执行器");
expect(container.textContent).toContain("2 位成员已加入");
@@ -313,6 +317,20 @@ describe("TeamWorkspaceBoard", () => {
expect(container.textContent).toContain("命令输出");
expect(container.textContent).toContain("已对 3 个来源完成去重校验。");
const researcherLane = container.querySelector(
'[data-testid="team-workspace-member-lane-child-1"]',
);
const executorLane = container.querySelector(
'[data-testid="team-workspace-member-lane-child-2"]',
);
expect(researcherLane?.textContent).toContain("角色输出");
expect(researcherLane?.textContent).toContain(
"回复:已完成竞品摘要与数据来源梳理。",
);
expect(executorLane?.textContent).toContain(
"计划:先整理落地步骤,再生成第一版实施清单。",
);
const openButton = Array.from(container.querySelectorAll("button")).find(
(element) => element.textContent?.includes("查看对话"),
);
@@ -325,6 +343,82 @@ describe("TeamWorkspaceBoard", () => {
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-1");
});
it("真实成员存在时应优先按蓝图角色顺序排列泳道", async () => {
const container = await renderBoard({
childSubagentSessions: [
{
id: "child-executor",
name: "执行成员",
created_at: 1_710_000_010,
updated_at: 1_710_000_120,
session_type: "sub_agent",
runtime_status: "queued",
latest_turn_status: "queued",
task_summary: "负责提交修复。",
role_hint: "executor",
blueprint_role_id: "runtime-executor",
blueprint_role_label: "执行",
profile_id: "code-executor",
role_key: "executor",
},
{
id: "child-explorer",
name: "分析成员",
created_at: 1_710_000_000,
updated_at: 1_710_000_100,
session_type: "sub_agent",
runtime_status: "running",
latest_turn_status: "running",
task_summary: "负责定位问题。",
role_hint: "explorer",
blueprint_role_id: "runtime-explorer",
blueprint_role_label: "分析",
profile_id: "code-explorer",
role_key: "explorer",
},
],
runtimeTeamState: {
requestId: "runtime-formed-ordered",
status: "formed",
label: "修复 Team",
summary: "分析、执行协同推进。",
members: [
{
id: "runtime-explorer",
label: "分析",
summary: "先定位问题。",
roleKey: "explorer",
profileId: "code-explorer",
skillIds: ["repo-exploration"],
status: "planned",
},
{
id: "runtime-executor",
label: "执行",
summary: "再提交修复。",
roleKey: "executor",
profileId: "code-executor",
skillIds: ["bounded-implementation"],
status: "planned",
},
],
blueprint: null,
updatedAt: Date.now(),
},
});
const laneIds = Array.from(
container.querySelectorAll('[data-testid^="team-workspace-member-lane-"]'),
).map((element) => element.getAttribute("data-testid"));
expect(laneIds).toEqual([
"team-workspace-member-lane-child-explorer",
"team-workspace-member-lane-child-executor",
]);
expect(container.textContent).toContain("蓝图 · 分析");
expect(container.textContent).toContain("蓝图 · 执行");
});
it("子线程视角应展示父会话、最近过程并支持切换 sibling", async () => {
const onOpenSubagentSession = vi.fn();
const onReturnToParentSession = vi.fn();
@@ -420,6 +514,7 @@ describe("TeamWorkspaceBoard", () => {
});
expect(container.textContent).toContain("主线程总览");
expect(container.textContent).toContain("角色执行画布");
expect(container.textContent).toContain("实现代理");
expect(container.textContent).toContain("检索代理");
expect(container.textContent).toContain("队列 1");
@@ -436,6 +531,20 @@ describe("TeamWorkspaceBoard", () => {
"工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
);
const currentLane = container.querySelector(
'[data-testid="team-workspace-member-lane-child-current"]',
);
const siblingLane = container.querySelector(
'[data-testid="team-workspace-member-lane-child-sibling-1"]',
);
expect(currentLane?.textContent).toContain("角色输出");
expect(currentLane?.textContent).toContain(
"推理:先检查 team runtime 的控制面状态,再决定是否等待。",
);
expect(siblingLane?.textContent).toContain(
"工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
);
const returnButton = Array.from(container.querySelectorAll("button")).find(
(element) => element.textContent?.includes("返回父会话"),
);
@@ -100,6 +100,8 @@ interface TeamSessionCard {
model?: string;
originTool?: string;
createdFromTurnId?: string;
blueprintRoleId?: string;
blueprintRoleLabel?: string;
profileId?: string;
profileName?: string;
roleKey?: string;
@@ -588,6 +590,8 @@ function buildCurrentChildSession(
sessionType: "sub_agent",
originTool: subagentParentContext.origin_tool,
createdFromTurnId: subagentParentContext.created_from_turn_id,
blueprintRoleId: subagentParentContext.blueprint_role_id,
blueprintRoleLabel: subagentParentContext.blueprint_role_label,
profileId: subagentParentContext.profile_id,
profileName: subagentParentContext.profile_name,
roleKey: subagentParentContext.role_key,
@@ -640,6 +644,136 @@ function dedupeSessions(
return result;
}
function normalizeComparableText(value?: string | null): string {
return value?.trim().toLocaleLowerCase() || "";
}
function resolveSessionBlueprintRoleId(
session: TeamSessionCard,
runtimeRoles: Array<{
id: string;
label?: string | null;
profileId?: string;
roleKey?: string;
}>,
usedRoleIds: Set<string>,
): string | null {
const explicitRoleId = session.blueprintRoleId?.trim();
if (
explicitRoleId &&
!usedRoleIds.has(explicitRoleId) &&
runtimeRoles.some((role) => role.id === explicitRoleId)
) {
return explicitRoleId;
}
const sessionBlueprintRoleLabel = normalizeComparableText(
session.blueprintRoleLabel,
);
const sessionRoleKey = normalizeComparableText(
session.roleKey || session.roleHint,
);
const sessionProfileId = normalizeComparableText(session.profileId);
const sessionName = normalizeComparableText(session.name);
const candidates = runtimeRoles
.filter((role) => !usedRoleIds.has(role.id))
.map((role) => {
let score = 0;
if (
sessionBlueprintRoleLabel &&
normalizeComparableText(role.label) === sessionBlueprintRoleLabel
) {
score += 8;
}
if (sessionRoleKey && normalizeComparableText(role.roleKey) === sessionRoleKey) {
score += 4;
}
if (
sessionProfileId &&
normalizeComparableText(role.profileId) === sessionProfileId
) {
score += 3;
}
if (sessionName && normalizeComparableText(role.label) === sessionName) {
score += 2;
}
return {
roleId: role.id,
score,
};
})
.filter((candidate) => candidate.score > 0)
.sort((left, right) => right.score - left.score);
if (candidates.length === 0) {
return null;
}
if (
candidates.length > 1 &&
candidates[0]?.score === candidates[1]?.score
) {
return null;
}
return candidates[0]?.roleId ?? null;
}
function orderSessionsByRuntimeRoles(
sessions: TeamSessionCard[],
runtimeTeamState?: TeamWorkspaceRuntimeFormationState | null,
): TeamSessionCard[] {
if (sessions.length <= 1 || !runtimeTeamState) {
return sessions;
}
const runtimeRoles = (
runtimeTeamState.members.length > 0
? runtimeTeamState.members
: runtimeTeamState.blueprint?.roles ?? []
).map((role) => ({
id: role.id,
label: role.label,
profileId: role.profileId,
roleKey: role.roleKey,
}));
if (runtimeRoles.length === 0) {
return sessions;
}
const roleOrder = new Map(runtimeRoles.map((role, index) => [role.id, index]));
const usedRoleIds = new Set<string>();
return [...sessions]
.map((session, index) => {
const matchedRoleId = resolveSessionBlueprintRoleId(
session,
runtimeRoles,
usedRoleIds,
);
if (matchedRoleId) {
usedRoleIds.add(matchedRoleId);
}
return {
session,
index,
matchedRoleId,
roleOrder:
matchedRoleId !== null
? (roleOrder.get(matchedRoleId) ?? Number.MAX_SAFE_INTEGER)
: Number.MAX_SAFE_INTEGER,
};
})
.sort((left, right) => {
if (left.roleOrder !== right.roleOrder) {
return left.roleOrder - right.roleOrder;
}
return left.index - right.index;
})
.map((entry) => entry.session);
}
function buildBoardHeadline(params: {
hasRealTeamGraph: boolean;
isChildSession: boolean;
@@ -730,6 +864,39 @@ function buildRuntimeFormationEmptyDetail(
}
}
function buildSessionLaneEmptyState(params: {
session?: TeamSessionCard | null;
previewState?: SessionActivityPreviewState | null;
}) {
const { session, previewState } = params;
if (previewState?.status === "error") {
return previewState.errorMessage?.trim() || "同步角色输出失败";
}
if (previewState?.status === "loading") {
return "正在同步该角色的最新输出...";
}
if (session?.runtimeStatus === "queued") {
return "已进入队列,等待 orchestrator 拉起执行。";
}
if (session?.runtimeStatus === "running") {
return "角色已开始执行,最新过程会持续刷新到这里。";
}
if (session?.runtimeStatus === "completed") {
return "该角色已完成当前任务,等待 orchestrator 汇总或继续交接。";
}
if (session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted") {
return "该角色执行失败,可在下方焦点区查看细节并决定是否恢复。";
}
return "该角色暂未产出可展示的正文内容。";
}
export function TeamWorkspaceBoard({
className,
embedded = false,
@@ -833,6 +1000,8 @@ export function TeamWorkspaceBoard({
model: session.model,
originTool: session.origin_tool,
createdFromTurnId: session.created_from_turn_id,
blueprintRoleId: session.blueprint_role_id,
blueprintRoleLabel: session.blueprint_role_label,
profileId: session.profile_id,
profileName: session.profile_name,
roleKey: session.role_key,
@@ -1159,6 +1328,16 @@ export function TeamWorkspaceBoard({
);
};
const memberCanvasSessions = useMemo(
() =>
orderSessionsByRuntimeRoles(
isChildSession
? dedupeSessions([currentChildSession, ...visibleSessions])
: visibleSessions,
runtimeTeamState,
),
[currentChildSession, isChildSession, runtimeTeamState, visibleSessions],
);
const railSessions = useMemo(
() =>
dedupeSessions(
@@ -1184,7 +1363,7 @@ export function TeamWorkspaceBoard({
useEffect(() => {
const defaultSelectedId = isChildSession
? currentSessionId ?? railSessions[0]?.id ?? null
: visibleSessions[0]?.id ?? railSessions[0]?.id ?? null;
: memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null;
if (!selectedSessionId) {
setSelectedSessionId(defaultSelectedId);
@@ -1199,17 +1378,17 @@ export function TeamWorkspaceBoard({
if (
!isChildSession &&
selectedSessionId === orchestratorSession?.id &&
visibleSessions.length > 0
memberCanvasSessions.length > 0
) {
setSelectedSessionId(visibleSessions[0]?.id ?? defaultSelectedId);
setSelectedSessionId(memberCanvasSessions[0]?.id ?? defaultSelectedId);
}
}, [
currentSessionId,
isChildSession,
memberCanvasSessions,
orchestratorSession?.id,
railSessions,
selectedSessionId,
visibleSessions,
]);
const selectedSession = useMemo(
@@ -1728,6 +1907,18 @@ export function TeamWorkspaceBoard({
},
[onSendSubagentInput, selectedSession, selectedSessionInputMessage],
);
const memberCanvasTitle = "角色执行画布";
const memberCanvasSubtitle = hasRealTeamGraph
? isChildSession
? "当前成员与同组角色在各自泳道中输出过程与结果,主对话仅保留调度记录。"
: `${visibleSessions.length} 位成员已加入,主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。`
: runtimeTeamState?.status === "forming"
? "正在准备本轮角色泳道,真实成员接入后会在这里独立输出。"
: runtimeTeamState?.status === "formed"
? "本轮编队已完成,等待真实角色接入后在各自泳道中开始执行。"
: runtimeTeamState?.status === "failed"
? "本轮编队失败,暂时无法生成角色泳道。"
: "等待成员加入后,这里会按角色展开各自的输出泳道。";
if (
!subagentParentContext &&
@@ -1825,6 +2016,9 @@ export function TeamWorkspaceBoard({
const selectedPresetOption = getTeamPresetOption(selectedSession?.teamPresetId);
const selectedSkills = selectedSession?.skills ?? [];
const selectedMetadata = [
selectedSession?.blueprintRoleLabel
? `蓝图角色 ${selectedSession.blueprintRoleLabel}`
: null,
selectedSession?.sessionType
? resolveSessionTypeLabel(selectedSession.sessionType)
: null,
@@ -1884,20 +2078,6 @@ export function TeamWorkspaceBoard({
const timelineEntryClassName = embedded
? "border-l-2 border-slate-200 pl-3"
: "rounded-[14px] border border-slate-200 bg-white p-3";
const railTitle = isChildSession ? "同组成员轨道" : "团队成员轨道";
const railSubtitle = isChildSession
? siblingCount > 0
? `当前成员与 ${siblingCount} 位同组成员协作`
: "当前只有一位成员"
: hasRealTeamGraph
? `${visibleSessions.length} 位成员已加入`
: runtimeTeamState?.status === "forming"
? "正在准备本轮成员"
: runtimeTeamState?.status === "formed"
? `${runtimeMembers.length} 个成员待启动`
: runtimeTeamState?.status === "failed"
? "Team 准备失败"
: "等待成员加入";
return (
<section
@@ -2003,10 +2183,10 @@ export function TeamWorkspaceBoard({
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
{railTitle}
{memberCanvasTitle}
</div>
<div className="mt-1 text-sm font-semibold text-slate-900">
{railSubtitle}
{memberCanvasSubtitle}
</div>
</div>
{selectedSession ? (
@@ -2141,17 +2321,17 @@ export function TeamWorkspaceBoard({
</div>
) : null}
{railSessions.length > 0 ? (
{memberCanvasSessions.length > 0 ? (
<div
className={cn(
"mt-3",
stackedRail
? "grid gap-2 md:grid-cols-2"
: "flex gap-3 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
? "grid gap-3 md:grid-cols-2"
: "grid gap-3 xl:grid-cols-2 2xl:grid-cols-3",
)}
data-testid="team-workspace-rail-list"
>
{railSessions.map((session) => {
{memberCanvasSessions.map((session) => {
const meta = resolveStatusMeta(session.runtimeStatus);
const selected = selectedSession?.id === session.id;
const runtimeDetail = buildRuntimeDetailSummary(session);
@@ -2160,10 +2340,17 @@ export function TeamWorkspaceBoard({
sessionActivityPreviewById[session.id]?.entries,
ACTIVITY_TIMELINE_ENTRY_LIMIT,
);
const cardActivityPreview = buildCardActivityPreview(
buildActivityPreviewFromEntry(mergedCardEntries[0]) ??
sessionActivityPreviewById[session.id]?.preview,
);
const previewState = sessionActivityPreviewById[session.id] ?? null;
const cardActivityPreview = buildActivityPreviewFromEntry(
mergedCardEntries[0],
)
?? previewState?.preview
?? null;
const laneEntries = mergedCardEntries.slice(0, 3);
const laneEmptyState = buildSessionLaneEmptyState({
session,
previewState,
});
return (
<button
@@ -2172,13 +2359,14 @@ export function TeamWorkspaceBoard({
aria-pressed={selected}
onClick={() => setSelectedSessionId(session.id)}
className={cn(
"group flex flex-col rounded-[18px] border p-3 text-left transition",
stackedRail ? "w-full" : "w-[258px] shrink-0",
"group flex flex-col rounded-[18px] border p-4 text-left transition",
"w-full",
meta.cardClassName,
selected
? "ring-1 ring-slate-300 bg-slate-50"
? "ring-2 ring-slate-300 bg-slate-50"
: "hover:border-slate-300 hover:bg-slate-50",
)}
data-testid={`team-workspace-member-lane-${session.id}`}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
@@ -2193,6 +2381,11 @@ export function TeamWorkspaceBoard({
) : null}
</div>
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-500">
{session.blueprintRoleLabel ? (
<span className="rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-sky-700">
蓝图 · {session.blueprintRoleLabel}
</span>
) : null}
{session.roleHint ? (
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5">
{session.roleHint}
@@ -2239,29 +2432,47 @@ export function TeamWorkspaceBoard({
)}
/>
</div>
<p
className={cn(
"mt-2 text-sm leading-5 text-slate-600",
stackedRail ? "line-clamp-4" : "line-clamp-2",
)}
>
<p className="mt-2 line-clamp-3 text-sm leading-5 text-slate-600">
{session.taskSummary ||
"暂未生成任务摘要,打开该会话可查看完整上下文。"}
</p>
{cardActivityPreview ? (
<div className="mt-2 border-l-2 border-slate-200 pl-3">
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
<Activity className="h-3 w-3" />
<span>最近过程</span>
</div>
<p
className={cn(
"mt-1 text-[11px] leading-5 text-slate-600",
stackedRail ? "line-clamp-3" : "line-clamp-2",
)}
>
{cardActivityPreview}
</p>
<div className="mt-3 rounded-2xl border border-slate-200 bg-white px-3 py-3">
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
<PanelTop className="h-3 w-3" />
<span>角色输出</span>
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
最近过程
</span>
</div>
<p className="mt-2 line-clamp-5 whitespace-pre-wrap break-words text-[12px] leading-5 text-slate-700">
{buildCardActivityPreview(cardActivityPreview) || laneEmptyState}
</p>
</div>
{laneEntries.length > 0 ? (
<div className="mt-3 space-y-2">
{laneEntries.map((entry) => (
<div
key={`${session.id}-${entry.id}`}
className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2.5"
>
<div className="flex flex-wrap items-center gap-2 text-[11px]">
<span className="font-semibold text-slate-800">
{entry.title}
</span>
<span
className={cn(
"rounded-full px-2 py-0.5 font-medium",
entry.badgeClassName,
)}
>
{entry.statusLabel}
</span>
</div>
<p className="mt-1.5 line-clamp-4 whitespace-pre-wrap break-words text-xs leading-5 text-slate-600">
{entry.detail}
</p>
</div>
))}
</div>
) : null}
{runtimeDetail ? (
@@ -2297,17 +2508,17 @@ export function TeamWorkspaceBoard({
)}
<div className="mt-4 rounded-[20px] border border-dashed border-slate-200 bg-slate-50 p-5 text-sm leading-6 text-slate-500">
{runtimeTeamState?.status === "forming" ? (
"模型正在准备本轮 Team,完成后这里会先展示本轮成员;后续真实成员加入时,再切换为可纵向浏览的团队轨道。"
"模型正在准备本轮 Team,完成后这里会先展示本轮成员;后续真实成员加入时,再切换为按角色独立输出的执行画布。"
) : runtimeTeamState?.status === "formed" ? (
<>
本轮 Team 已就绪。系统开始分派成员后,这里会从当前编队过渡到真实团队轨道。
本轮 Team 已就绪。系统开始分派成员后,这里会从当前编队过渡到真实角色画布。
</>
) : runtimeTeamState?.status === "failed" ? (
runtimeTeamState.errorMessage?.trim() ||
"Team 准备失败,暂时还没有真实团队成员。"
) : (
<>
还没有真实团队成员。系统开始分派成员后,这里会生成可纵向浏览的团队轨道。
还没有真实团队成员。系统开始分派成员后,这里会生成按角色分栏的执行画布。
</>
)}
</div>
@@ -2,13 +2,15 @@ import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { TeamWorkspaceRuntimeFormationState } from "../teamWorkspaceRuntime";
import type { TeamDefinition } from "../utils/teamDefinitions";
import {
shouldGenerateRuntimeTeamAfterSend,
shouldPrepareRuntimeTeamBeforeSend,
useRuntimeTeamFormation,
} from "./useRuntimeTeamFormation";
type HookProps = Parameters<typeof useRuntimeTeamFormation>[0];
type RuntimeTeamPreparationResult = TeamWorkspaceRuntimeFormationState | null;
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
@@ -115,28 +117,32 @@ describe("useRuntimeTeamFormation", () => {
vi.clearAllMocks();
});
it("发送后满足条件时应进入 forming 并在生成完成后切到 formed", async () => {
const generateRuntimeTeam = vi.fn(async () => ({
id: "ephemeral-1",
source: "ephemeral" as const,
label: "自动 Team",
description: "自动生成",
roles: [
{
id: "role-1",
label: "执行者",
summary: "负责执行",
},
],
}));
it("发送前满足条件时应进入 forming 并在生成完成后切到 formed", async () => {
let resolveRuntimeTeam:
| ((team: TeamDefinition) => void)
| null = null;
const generateRuntimeTeam = vi.fn(
async (
_options: Parameters<NonNullable<HookProps["generateRuntimeTeam"]>>[0],
): Promise<TeamDefinition> =>
new Promise((resolve) => {
resolveRuntimeTeam = resolve;
}),
);
const { render, getValue } = renderHook({
generateRuntimeTeam,
});
await render();
act(() => {
getValue().handleRuntimeTeamAfterSend({
const resultRef: { current: RuntimeTeamPreparationResult } = {
current: null,
};
let pending:
| Promise<RuntimeTeamPreparationResult>
| null = null;
await act(async () => {
pending = getValue().prepareRuntimeTeamBeforeSend({
input: "请拆成两个子任务",
providerType: "openai",
model: "gpt-4.1",
@@ -156,20 +162,59 @@ describe("useRuntimeTeamFormation", () => {
);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
resolveRuntimeTeam?.({
id: "ephemeral-1",
source: "ephemeral",
label: "自动 Team",
description: "自动生成",
roles: [
{
id: "role-1",
label: "执行者",
summary: "负责执行",
},
],
});
resultRef.current = await pending;
});
expect(resultRef.current?.status).toBe("formed");
expect(getValue().runtimeTeamState?.status).toBe("formed");
expect(getValue().runtimeTeamState?.label).toBe("自动 Team");
});
it("发送前 Team 生成失败时应返回 failed 状态", async () => {
const { render, getValue } = renderHook({
generateRuntimeTeam: vi.fn(async () => {
throw new Error("模型规划失败");
}),
});
await render();
const resultRef: { current: RuntimeTeamPreparationResult } = {
current: null,
};
await act(async () => {
resultRef.current = await getValue().prepareRuntimeTeamBeforeSend({
input: "请拆解任务",
providerType: "openai",
model: "gpt-4.1",
executionStrategy: "react",
});
});
expect(resultRef.current?.status).toBe("failed");
expect(resultRef.current?.errorMessage).toBe("模型规划失败");
expect(getValue().runtimeTeamState?.status).toBe("failed");
});
it("subagent 关闭且没有真实 team 图时,应清空 runtime team 状态", async () => {
const { render, getValue } = renderHook();
await render();
act(() => {
getValue().triggerRuntimeTeamFormation({
void getValue().triggerRuntimeTeamFormation({
input: "请先规划 team",
providerType: "openai",
model: "gpt-4.1",
@@ -187,12 +232,40 @@ describe("useRuntimeTeamFormation", () => {
expect(getValue().runtimeTeamState).toBeNull();
});
it("本轮不走 Team 且没有真实 team 图时,应清空上一轮 runtime team 状态", async () => {
const { render, getValue } = renderHook();
await render();
await act(async () => {
await getValue().prepareRuntimeTeamBeforeSend({
input: "请拆分成多个角色执行",
providerType: "openai",
model: "gpt-4.1",
executionStrategy: "react",
});
});
expect(getValue().runtimeTeamState?.status).toBe("formed");
await act(async () => {
await getValue().prepareRuntimeTeamBeforeSend({
input: "请直接润色这段文案",
providerType: "openai",
model: "gpt-4.1",
executionStrategy: "react",
purpose: "content_review",
});
});
expect(getValue().runtimeTeamState).toBeNull();
});
it("session 切换时应重置 runtime team 状态", async () => {
const { render, getValue } = renderHook();
await render();
act(() => {
getValue().triggerRuntimeTeamFormation({
void getValue().triggerRuntimeTeamFormation({
input: "请先规划 team",
providerType: "openai",
model: "gpt-4.1",
@@ -210,10 +283,10 @@ describe("useRuntimeTeamFormation", () => {
});
});
describe("shouldGenerateRuntimeTeamAfterSend", () => {
describe("shouldPrepareRuntimeTeamBeforeSend", () => {
it("仅在 subagent 开启、存在项目、非 purpose 且输入非空时返回 true", () => {
expect(
shouldGenerateRuntimeTeamAfterSend({
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: true,
projectId: "project-1",
input: "请拆解任务",
@@ -221,7 +294,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
).toBe(true);
expect(
shouldGenerateRuntimeTeamAfterSend({
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: false,
projectId: "project-1",
input: "请拆解任务",
@@ -229,7 +302,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
).toBe(false);
expect(
shouldGenerateRuntimeTeamAfterSend({
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: true,
projectId: "",
input: "请拆解任务",
@@ -237,7 +310,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
).toBe(false);
expect(
shouldGenerateRuntimeTeamAfterSend({
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: true,
projectId: "project-1",
input: " ",
@@ -245,7 +318,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
).toBe(false);
expect(
shouldGenerateRuntimeTeamAfterSend({
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: true,
projectId: "project-1",
input: "请拆解任务",
@@ -14,9 +14,10 @@ interface TriggerRuntimeTeamFormationParams {
executionStrategy?: "react" | "code_orchestrated" | "auto";
}
interface HandleRuntimeTeamAfterSendParams
interface PrepareRuntimeTeamBeforeSendParams
extends TriggerRuntimeTeamFormationParams {
purpose?: HandleSendOptions["purpose"];
subagentEnabled?: boolean;
}
interface UseRuntimeTeamFormationOptions {
@@ -36,15 +37,17 @@ export interface UseRuntimeTeamFormationResult {
clearRuntimeTeamState: () => void;
triggerRuntimeTeamFormation: (
params: TriggerRuntimeTeamFormationParams,
) => void;
handleRuntimeTeamAfterSend: (params: HandleRuntimeTeamAfterSendParams) => void;
) => Promise<TeamWorkspaceRuntimeFormationState | null>;
prepareRuntimeTeamBeforeSend: (
params: PrepareRuntimeTeamBeforeSendParams,
) => Promise<TeamWorkspaceRuntimeFormationState | null>;
}
function defaultCreateRequestId() {
return crypto.randomUUID();
}
export function shouldGenerateRuntimeTeamAfterSend(params: {
export function shouldPrepareRuntimeTeamBeforeSend(params: {
subagentEnabled: boolean;
projectId?: string | null;
input: string;
@@ -79,7 +82,7 @@ export function useRuntimeTeamFormation({
}, []);
const triggerRuntimeTeamFormation = useCallback(
({
async ({
input,
providerType,
model,
@@ -87,61 +90,60 @@ export function useRuntimeTeamFormation({
}: TriggerRuntimeTeamFormationParams) => {
const normalizedInput = input.trim();
if (!projectId || !normalizedInput) {
return;
return null;
}
const requestId = createRequestId();
runtimeTeamRequestIdRef.current = requestId;
setRuntimeTeamState(
createRuntimeFormationStateFromTeam({
const formingState = createRuntimeFormationStateFromTeam({
requestId,
status: "forming",
blueprintTeam: selectedTeam ?? null,
updatedAt: now(),
});
setRuntimeTeamState(formingState);
try {
const runtimeTeam = await generateRuntimeTeam({
workspaceId: projectId,
providerType,
model,
executionStrategy,
activeTheme,
input: normalizedInput,
blueprintTeam: selectedTeam ?? null,
});
if (runtimeTeamRequestIdRef.current !== requestId) {
return null;
}
const formedState = createRuntimeFormationStateFromTeam({
requestId,
status: "forming",
status: "formed",
runtimeTeam,
blueprintTeam: selectedTeam ?? null,
updatedAt: now(),
}),
);
void generateRuntimeTeam({
workspaceId: projectId,
providerType,
model,
executionStrategy,
activeTheme,
input: normalizedInput,
blueprintTeam: selectedTeam ?? null,
})
.then((runtimeTeam) => {
if (runtimeTeamRequestIdRef.current !== requestId) {
return;
}
setRuntimeTeamState(
createRuntimeFormationStateFromTeam({
requestId,
status: "formed",
runtimeTeam,
blueprintTeam: selectedTeam ?? null,
updatedAt: now(),
}),
);
})
.catch((error) => {
if (runtimeTeamRequestIdRef.current !== requestId) {
return;
}
const errorMessage =
error instanceof Error ? error.message : "Team 生成失败";
setRuntimeTeamState(
createRuntimeFormationStateFromTeam({
requestId,
status: "failed",
blueprintTeam: selectedTeam ?? null,
errorMessage,
updatedAt: now(),
}),
);
});
setRuntimeTeamState(formedState);
return formedState;
} catch (error) {
if (runtimeTeamRequestIdRef.current !== requestId) {
return null;
}
const errorMessage =
error instanceof Error ? error.message : "Team 生成失败";
const failedState = createRuntimeFormationStateFromTeam({
requestId,
status: "failed",
blueprintTeam: selectedTeam ?? null,
errorMessage,
updatedAt: now(),
});
setRuntimeTeamState(failedState);
return failedState;
}
},
[
activeTheme,
@@ -153,34 +155,38 @@ export function useRuntimeTeamFormation({
],
);
const handleRuntimeTeamAfterSend = useCallback(
const prepareRuntimeTeamBeforeSend = useCallback(
({
input,
providerType,
model,
executionStrategy,
purpose,
}: HandleRuntimeTeamAfterSendParams) => {
subagentEnabled: subagentEnabledOverride,
}: PrepareRuntimeTeamBeforeSendParams) => {
const effectiveSubagentEnabled =
subagentEnabledOverride ?? subagentEnabled;
if (
shouldGenerateRuntimeTeamAfterSend({
subagentEnabled,
shouldPrepareRuntimeTeamBeforeSend({
subagentEnabled: effectiveSubagentEnabled,
projectId,
input,
purpose,
})
) {
triggerRuntimeTeamFormation({
return triggerRuntimeTeamFormation({
input,
providerType,
model,
executionStrategy,
});
return;
}
if (!subagentEnabled && !hasRealTeamGraph) {
if (!hasRealTeamGraph) {
clearRuntimeTeamState();
}
return Promise.resolve(null);
},
[
clearRuntimeTeamState,
@@ -205,6 +211,6 @@ export function useRuntimeTeamFormation({
runtimeTeamState,
clearRuntimeTeamState,
triggerRuntimeTeamFormation,
handleRuntimeTeamAfterSend,
prepareRuntimeTeamBeforeSend,
};
}
@@ -121,6 +121,64 @@ describe("harnessRequestMetadata", () => {
]);
});
it("应写入本轮 Team 决策与预编队蓝图", () => {
const metadata = buildHarnessRequestMetadata({
theme: "general",
creationMode: "guided",
chatMode: "agent",
webSearchEnabled: false,
thinkingEnabled: true,
taskModeEnabled: true,
subagentModeEnabled: true,
sessionMode: "default",
turnTeamDecision: "team_prepared",
turnTeamReason: "runtime_team_prepared",
turnTeamBlueprint: {
label: "本轮调试 Team",
description: "先分析,再实现,最后验证。",
roles: [
{
id: "explorer",
label: "分析",
summary: "负责定位问题。",
profileId: "code-explorer",
roleKey: "explorer",
skillIds: ["repo-exploration"],
},
{
id: "executor",
label: "执行",
summary: "负责提交修复。",
},
],
},
});
expect(metadata).toMatchObject({
turn_team_decision: "team_prepared",
turn_team_reason: "runtime_team_prepared",
turn_team_blueprint: {
label: "本轮调试 Team",
description: "先分析,再实现,最后验证。",
roles: [
{
id: "explorer",
label: "分析",
summary: "负责定位问题。",
profile_id: "code-explorer",
role_key: "explorer",
skill_ids: ["repo-exploration"],
},
{
id: "executor",
label: "执行",
summary: "负责提交修复。",
},
],
},
});
});
it("需要人工确认的浏览器任务应标记 user step", () => {
const metadata = buildHarnessRequestMetadata({
theme: "general",
@@ -2,6 +2,14 @@ import type { CreationMode } from "../components/types";
import type { BrowserTaskRequirement } from "../types";
import type { TeamRoleDefinition, TeamDefinitionSource } from "./teamDefinitions";
export type HarnessTurnTeamDecision = "single_agent" | "team_prepared";
export interface HarnessTurnTeamBlueprint {
label?: string | null;
description?: string | null;
roles?: TeamRoleDefinition[] | null;
}
export interface BuildHarnessRequestMetadataOptions {
base?: Record<string, unknown>;
theme: string;
@@ -25,6 +33,9 @@ export interface BuildHarnessRequestMetadataOptions {
selectedTeamLabel?: string | null;
selectedTeamSummary?: string | null;
selectedTeamRoles?: TeamRoleDefinition[] | null;
turnTeamDecision?: HarnessTurnTeamDecision | null;
turnTeamReason?: string | null;
turnTeamBlueprint?: HarnessTurnTeamBlueprint | null;
}
export function extractExistingHarnessMetadata(
@@ -68,8 +79,26 @@ export function buildHarnessRequestMetadata(
selectedTeamLabel,
selectedTeamSummary,
selectedTeamRoles,
turnTeamDecision,
turnTeamReason,
turnTeamBlueprint,
} = options;
const serializeTeamRoles = (roles?: TeamRoleDefinition[] | null) =>
roles && roles.length > 0
? roles.map((role) => ({
id: role.id,
label: role.label,
summary: role.summary,
profile_id: role.profileId || undefined,
role_key: role.roleKey || undefined,
skill_ids:
role.skillIds && role.skillIds.length > 0
? [...role.skillIds]
: undefined,
}))
: undefined;
return {
...(base || {}),
theme,
@@ -89,19 +118,19 @@ export function buildHarnessRequestMetadata(
selected_team_source: selectedTeamSource || undefined,
selected_team_label: selectedTeamLabel || undefined,
selected_team_summary: selectedTeamSummary || undefined,
selected_team_roles:
selectedTeamRoles && selectedTeamRoles.length > 0
? selectedTeamRoles.map((role) => ({
id: role.id,
label: role.label,
summary: role.summary,
profile_id: role.profileId || undefined,
role_key: role.roleKey || undefined,
skill_ids:
role.skillIds && role.skillIds.length > 0
? [...role.skillIds]
: undefined,
}))
selected_team_roles: serializeTeamRoles(selectedTeamRoles),
turn_team_decision: turnTeamDecision || undefined,
turn_team_reason: turnTeamReason || undefined,
turn_team_blueprint:
turnTeamBlueprint &&
(turnTeamBlueprint.label?.trim() ||
turnTeamBlueprint.description?.trim() ||
(turnTeamBlueprint.roles?.length || 0) > 0)
? {
label: turnTeamBlueprint.label?.trim() || undefined,
description: turnTeamBlueprint.description?.trim() || undefined,
roles: serializeTeamRoles(turnTeamBlueprint.roles),
}
: undefined,
browser_requirement: browserRequirement || undefined,
browser_requirement_reason: browserRequirementReason || undefined,
+6
View File
@@ -114,6 +114,8 @@ export interface AsterSubagentSessionInfo {
role_hint?: string;
origin_tool?: string;
created_from_turn_id?: string;
blueprint_role_id?: string;
blueprint_role_label?: string;
profile_id?: string;
profile_name?: string;
role_key?: string;
@@ -156,6 +158,8 @@ export interface AsterSubagentParentContext {
task_summary?: string;
origin_tool?: string;
created_from_turn_id?: string;
blueprint_role_id?: string;
blueprint_role_label?: string;
profile_id?: string;
profile_name?: string;
role_key?: string;
@@ -274,6 +278,8 @@ export interface AgentRuntimeSpawnSubagentRequest {
model?: string;
reasoning_effort?: string;
fork_context?: boolean;
blueprint_role_id?: string;
blueprint_role_label?: string;
profile_id?: string;
profile_name?: string;
role_key?: string;