feat: 岗位会商独立意图智能体与四项意图 schema
岗位会商 Step 1 改用 position-roundtable-intent 智能体,输出 coreGoals/riskWarnings/keyPoints/strategicSignificance 四项数组;支持单轮多张协助卡、按轮次计澄清上限,并兼容旧会话 objective/constraints 映射。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ca32a22c0d
commit
f8757d1a43
24 files changed
+1258
-257
No files matched your search
@@ -121,6 +121,31 @@ PositionRoundtablePage
|
||||
|
||||
后端会冻结链条快照并根据链条阶段创建节点。已启动后意图不可再修改;需要重新识别时必须新建会话。
|
||||
|
||||
The position workspace selects the dedicated built-in `position-roundtable-intent`
|
||||
agent by sending `intent_agent_id` through the shared Step-1 init and stream
|
||||
requests. The thread lifecycle, file upload, and SSE transport stay identical
|
||||
to the generic multi-agent flow. The dedicated agent permits at most five
|
||||
user-assistance rounds; one round may contain multiple independent
|
||||
`ask_clarification` cards. `Step1Panel` renders every card separately and
|
||||
submits the selected answers together, so cards must never be merged into one
|
||||
numbered multi-question card. Only its final `[INTENT_READY]` schema changes to
|
||||
the following four string arrays:
|
||||
|
||||
```json
|
||||
{
|
||||
"coreGoals": ["..."],
|
||||
"riskWarnings": ["..."],
|
||||
"keyPoints": ["..."],
|
||||
"strategicSignificance": ["..."]
|
||||
}
|
||||
```
|
||||
|
||||
The position sidebar and edit dialog render these four fields. Business-chain
|
||||
activation requires all four arrays and at least one core goal or strategic
|
||||
significance. Historic `objective` / `constraints` / `assumptions` snapshots
|
||||
are read through a compatibility mapping only; ordinary multi-agent Step 1 is
|
||||
not changed.
|
||||
|
||||
### 4.2 普通节点问答
|
||||
|
||||
`PositionNodeChat` 在发送前调用 `getPositionNodeContext` 获取受长度控制的任务、意图和已交付上游内容,再以 `task_external_context` 作为隐藏上下文发送给当前智能体。
|
||||
|
||||
@@ -111,7 +111,11 @@ import {
|
||||
type InteractiveMessage,
|
||||
type TimelineEntry,
|
||||
} from "@/roundtable-planning/hooks/useStep1Intent";
|
||||
import { INTENT_AGENT_ID, type IntentSummary } from "@/roundtable-planning/api/intent";
|
||||
import {
|
||||
POSITION_INTENT_AGENT_ID,
|
||||
type IntentSummary,
|
||||
type PositionIntentSummary,
|
||||
} from "@/roundtable-planning/api/intent";
|
||||
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||
import { getAvatarTypeFor } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
import {
|
||||
@@ -197,9 +201,10 @@ function isSessionInTaskScope(
|
||||
|
||||
type ClosureStageKind = PositionBuiltinAgentKind;
|
||||
type IntentEditDraft = {
|
||||
objective: string;
|
||||
constraints: string;
|
||||
assumptions: string;
|
||||
coreGoals: string;
|
||||
riskWarnings: string;
|
||||
keyPoints: string;
|
||||
strategicSignificance: string;
|
||||
};
|
||||
type SandboxThreadSnapshot = {
|
||||
threadId: string;
|
||||
@@ -682,16 +687,38 @@ function mergePositionSession(
|
||||
return { ...current, ...next, nodes: current.nodes };
|
||||
}
|
||||
|
||||
function parseIntentSummary(snapshot: Record<string, unknown> | null | undefined): IntentSummary | null {
|
||||
if (!snapshot || typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) {
|
||||
function intentStringList(snapshot: Record<string, unknown>, key: string): string[] {
|
||||
const value = snapshot[key];
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string" && Boolean(item.trim()))
|
||||
: [];
|
||||
}
|
||||
|
||||
function parsePositionIntentSummary(
|
||||
snapshot: Record<string, unknown> | null | undefined,
|
||||
): PositionIntentSummary | null {
|
||||
if (!snapshot) return null;
|
||||
const positionKeys = ["coreGoals", "riskWarnings", "keyPoints", "strategicSignificance"];
|
||||
if (positionKeys.some((key) => key in snapshot)) {
|
||||
if (!positionKeys.every((key) => Array.isArray(snapshot[key]))) return null;
|
||||
return {
|
||||
coreGoals: intentStringList(snapshot, "coreGoals"),
|
||||
riskWarnings: intentStringList(snapshot, "riskWarnings"),
|
||||
keyPoints: intentStringList(snapshot, "keyPoints"),
|
||||
strategicSignificance: intentStringList(snapshot, "strategicSignificance"),
|
||||
};
|
||||
}
|
||||
|
||||
// Existing sessions created before the dedicated position agent used the
|
||||
// shared Step-1 schema. Keep them readable and editable after the switch.
|
||||
if (typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
objective: snapshot.objective,
|
||||
constraints: snapshot.constraints.filter((item): item is string => typeof item === "string"),
|
||||
assumptions: Array.isArray(snapshot.assumptions)
|
||||
? snapshot.assumptions.filter((item): item is string => typeof item === "string")
|
||||
: undefined,
|
||||
coreGoals: intentStringList(snapshot, "constraints"),
|
||||
riskWarnings: [],
|
||||
keyPoints: intentStringList(snapshot, "assumptions"),
|
||||
strategicSignificance: snapshot.objective.trim() ? [snapshot.objective.trim()] : [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -973,9 +1000,10 @@ export default function PositionRoundtablePage({
|
||||
const [taskSaving, setTaskSaving] = useState(false);
|
||||
const [intentEditorOpen, setIntentEditorOpen] = useState(false);
|
||||
const [intentDraft, setIntentDraft] = useState<IntentEditDraft>({
|
||||
objective: "",
|
||||
constraints: "",
|
||||
assumptions: "",
|
||||
coreGoals: "",
|
||||
riskWarnings: "",
|
||||
keyPoints: "",
|
||||
strategicSignificance: "",
|
||||
});
|
||||
const [intentSaving, setIntentSaving] = useState(false);
|
||||
const [intentCompletedAt, setIntentCompletedAt] = useState("");
|
||||
@@ -1045,7 +1073,7 @@ export default function PositionRoundtablePage({
|
||||
[],
|
||||
);
|
||||
const frozenIntent = useMemo(
|
||||
() => parseIntentSummary(positionSession?.intentSnapshot),
|
||||
() => parsePositionIntentSummary(positionSession?.intentSnapshot),
|
||||
[positionSession?.intentSnapshot],
|
||||
);
|
||||
const task = useMemo(
|
||||
@@ -1106,12 +1134,23 @@ export default function PositionRoundtablePage({
|
||||
? 1
|
||||
: 0,
|
||||
selectedModel,
|
||||
intentAgentId: POSITION_INTENT_AGENT_ID,
|
||||
taskContext: intentTaskContext,
|
||||
initialWelcomeText:
|
||||
"我已读取左侧任务信息。你可以直接补充本次任务的特殊约束;如果没有补充,我会基于现有任务信息进行意图识别。",
|
||||
showInitialWelcome: false,
|
||||
triggerToast,
|
||||
});
|
||||
const livePositionIntent = useMemo(
|
||||
() =>
|
||||
parsePositionIntentSummary(
|
||||
intent.intentReady as unknown as Record<string, unknown> | null,
|
||||
),
|
||||
[intent.intentReady],
|
||||
);
|
||||
const positionIntent = frozenIntent ?? livePositionIntent;
|
||||
const positionIntentHeadline =
|
||||
positionIntent?.strategicSignificance[0] ?? positionIntent?.coreGoals[0];
|
||||
// `useStep1Intent` starts with an empty list for a new intent session. Keep
|
||||
// the complete list here so a restored historical session can display its
|
||||
// first agent turn as well as the user's follow-up turns. The old
|
||||
@@ -1258,23 +1297,31 @@ export default function PositionRoundtablePage({
|
||||
|
||||
const openIntentEditor = useCallback(() => {
|
||||
if (isArchivedSession) return;
|
||||
const currentIntent = frozenIntent ?? intent.intentReady;
|
||||
const currentIntent = positionIntent;
|
||||
setIntentDraft({
|
||||
objective: currentIntent?.objective ?? "",
|
||||
constraints: (currentIntent?.constraints ?? []).join("\n"),
|
||||
assumptions: (currentIntent?.assumptions ?? []).join("\n"),
|
||||
coreGoals: (currentIntent?.coreGoals ?? []).join("\n"),
|
||||
riskWarnings: (currentIntent?.riskWarnings ?? []).join("\n"),
|
||||
keyPoints: (currentIntent?.keyPoints ?? []).join("\n"),
|
||||
strategicSignificance: (currentIntent?.strategicSignificance ?? []).join("\n"),
|
||||
});
|
||||
setIntentEditorOpen(true);
|
||||
}, [frozenIntent, intent.intentReady, isArchivedSession]);
|
||||
}, [isArchivedSession, positionIntent]);
|
||||
|
||||
const handleSaveIntentEdit = useCallback(async () => {
|
||||
if (isArchivedSession) {
|
||||
toast.warning("已归档的会话不能编辑意图理解");
|
||||
return;
|
||||
}
|
||||
const objective = intentDraft.objective.trim();
|
||||
if (!objective) {
|
||||
toast.warning("请填写任务意图");
|
||||
const coreGoals = intentDraft.coreGoals
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const strategicSignificance = intentDraft.strategicSignificance
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
if (coreGoals.length === 0 && strategicSignificance.length === 0) {
|
||||
toast.warning("请至少填写核心目标或战略意义");
|
||||
return;
|
||||
}
|
||||
const splitLines = (value: string) =>
|
||||
@@ -1282,10 +1329,11 @@ export default function PositionRoundtablePage({
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const nextIntent: IntentSummary = {
|
||||
objective,
|
||||
constraints: splitLines(intentDraft.constraints),
|
||||
assumptions: splitLines(intentDraft.assumptions),
|
||||
const nextIntent: PositionIntentSummary = {
|
||||
coreGoals,
|
||||
riskWarnings: splitLines(intentDraft.riskWarnings),
|
||||
keyPoints: splitLines(intentDraft.keyPoints),
|
||||
strategicSignificance,
|
||||
};
|
||||
|
||||
setIntentSaving(true);
|
||||
@@ -1309,7 +1357,7 @@ export default function PositionRoundtablePage({
|
||||
});
|
||||
intent.hydrateFromDraft({
|
||||
intentThreadId: intentThreadId ?? intent.intentThreadId,
|
||||
intentReady: nextIntent,
|
||||
intentReady: nextIntent as unknown as IntentSummary,
|
||||
interactiveMessages: intent.interactiveMessages,
|
||||
timelineEntries: [],
|
||||
});
|
||||
@@ -1325,9 +1373,10 @@ export default function PositionRoundtablePage({
|
||||
ensureEditableSession,
|
||||
intent,
|
||||
intentCompletedAt,
|
||||
intentDraft.assumptions,
|
||||
intentDraft.constraints,
|
||||
intentDraft.objective,
|
||||
intentDraft.coreGoals,
|
||||
intentDraft.keyPoints,
|
||||
intentDraft.riskWarnings,
|
||||
intentDraft.strategicSignificance,
|
||||
intentInputDraft,
|
||||
isArchivedSession,
|
||||
task,
|
||||
@@ -1384,7 +1433,7 @@ export default function PositionRoundtablePage({
|
||||
// multi-agent roundtable Step 1 flow. Do not derive this from a chain seat:
|
||||
// older chains may have no seat agent id (or a removed legacy id), which
|
||||
// would make the configuration panel request a non-existent agent.
|
||||
const intentAgentId = INTENT_AGENT_ID;
|
||||
const intentAgentId = POSITION_INTENT_AGENT_ID;
|
||||
const isIntelligenceDeliveryOverview =
|
||||
activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending";
|
||||
const showIntentDeliveryOverview =
|
||||
@@ -1620,7 +1669,11 @@ export default function PositionRoundtablePage({
|
||||
view.timelineEntries,
|
||||
view.inputDraft,
|
||||
);
|
||||
const intentSnapshot = view.intentReady ? { ...view.intentReady } : null;
|
||||
// 岗位会商持久化的是专属四段式结果;useStep1Intent 为兼容普通会商
|
||||
// 补出的 objective / constraints / assumptions 不应写回岗位会商快照。
|
||||
const intentSnapshot = parsePositionIntentSummary(
|
||||
view.intentReady as unknown as Record<string, unknown> | null,
|
||||
);
|
||||
|
||||
const operation = async () => {
|
||||
const baseSession = candidateSession ?? (await ensureEditableSession(task));
|
||||
@@ -1772,7 +1825,8 @@ export default function PositionRoundtablePage({
|
||||
candidates.sort((left, right) => right.length - left.length)[0] ?? [];
|
||||
hydrateIntentFromDraft({
|
||||
intentThreadId: intentThreadId ?? latest.intentThreadId,
|
||||
intentReady: latestFrozenIntentRef.current ?? latest.intentReady,
|
||||
intentReady:
|
||||
(latestFrozenIntentRef.current as unknown as IntentSummary | null) ?? latest.intentReady,
|
||||
interactiveMessages:
|
||||
interactiveMessages.length > 0
|
||||
? interactiveMessages
|
||||
@@ -1799,7 +1853,8 @@ export default function PositionRoundtablePage({
|
||||
: [];
|
||||
hydrateIntentFromDraft({
|
||||
intentThreadId: intentThreadId ?? latest.intentThreadId,
|
||||
intentReady: latestFrozenIntentRef.current ?? latest.intentReady,
|
||||
intentReady:
|
||||
(latestFrozenIntentRef.current as unknown as IntentSummary | null) ?? latest.intentReady,
|
||||
interactiveMessages: fallbackMessages,
|
||||
timelineEntries: databaseTimeline,
|
||||
});
|
||||
@@ -1997,7 +2052,7 @@ export default function PositionRoundtablePage({
|
||||
};
|
||||
}, [searchParamsString, sessionIdFromUrl, setSearchParams, taskScopeId]);
|
||||
|
||||
const agentIdForPanel = activePosition.isIntentPosition ? INTENT_AGENT_ID : selectedAgentId;
|
||||
const agentIdForPanel = activePosition.isIntentPosition ? POSITION_INTENT_AGENT_ID : selectedAgentId;
|
||||
const { agent: selectedAgent } = useAgent(agentIdForPanel, {
|
||||
enabled: !standaloneDemoMode,
|
||||
});
|
||||
@@ -2009,7 +2064,7 @@ export default function PositionRoundtablePage({
|
||||
if (!activePosition.isIntentPosition) return null;
|
||||
const seat = activeChain?.seats.find((item) => item.position_id === intentPositionId);
|
||||
return {
|
||||
id: INTENT_AGENT_ID,
|
||||
id: POSITION_INTENT_AGENT_ID,
|
||||
name: seat?.name || `${activePosition.name}智能体`,
|
||||
description: seat?.description || activePosition.description,
|
||||
user_id: null,
|
||||
@@ -2072,7 +2127,7 @@ export default function PositionRoundtablePage({
|
||||
let session = await ensureEditableSession(task);
|
||||
session = await savePositionIntent(session.id, {
|
||||
intentThreadId: intent.intentThreadId,
|
||||
intentSnapshot: intent.intentReady,
|
||||
intentSnapshot: positionIntent,
|
||||
conversationSnapshot: buildIntentConversationSnapshot(
|
||||
intent.interactiveMessages,
|
||||
intent.timelineEntries,
|
||||
@@ -2415,17 +2470,17 @@ export default function PositionRoundtablePage({
|
||||
[handlePreviewArtifact],
|
||||
);
|
||||
const openIntentFlowDetail = useCallback(() => {
|
||||
if (!(frozenIntent ?? intent.intentReady)) {
|
||||
if (!positionIntent) {
|
||||
toast.info("任务意图还在梳理中,完成确认后就能查看完整的意图说明。");
|
||||
return;
|
||||
}
|
||||
setFlowDetailItem(
|
||||
buildIntentResultItem(
|
||||
frozenIntent ?? intent.intentReady,
|
||||
positionIntent,
|
||||
intentCompletedAt || positionSession?.updatedAt,
|
||||
),
|
||||
);
|
||||
}, [frozenIntent, intent.intentReady, intentCompletedAt, positionSession?.updatedAt]);
|
||||
}, [positionIntent, intentCompletedAt, positionSession?.updatedAt]);
|
||||
const openNodeFlowDetail = useCallback(
|
||||
(node: PositionRoundtableNode) => {
|
||||
const item = buildNodeResultItem(node, activeChain);
|
||||
@@ -2642,7 +2697,7 @@ export default function PositionRoundtablePage({
|
||||
positionName={activePosition.name}
|
||||
specialAgentConfig
|
||||
onSelectAgent={handleSelectAgent}
|
||||
intentObjective={frozenIntent?.objective ?? intent.intentReady?.objective}
|
||||
intentObjective={positionIntentHeadline}
|
||||
intentCompletedAt={intentCompletedAt}
|
||||
assignedAgentIds={configurationAgentIds}
|
||||
hasSelectedChain={showClosureAgents || Boolean(selectedChain)}
|
||||
@@ -2684,7 +2739,7 @@ export default function PositionRoundtablePage({
|
||||
}
|
||||
interactiveMessages={visibleIntentMessages}
|
||||
intentThreadId={intent.intentThreadId}
|
||||
intentReady={chainStarted ? frozenIntent ?? intent.intentReady : intent.intentReady}
|
||||
intentReady={chainStarted ? positionIntent : intent.intentReady}
|
||||
isIntentSending={intent.isIntentSending}
|
||||
pendingIntentClarificationMsgId={intent.pendingIntentClarificationMsgId}
|
||||
intentClarificationSelections={intent.intentClarificationSelections}
|
||||
@@ -2974,7 +3029,7 @@ export default function PositionRoundtablePage({
|
||||
/>
|
||||
{showIntentDeliveryOverview ? (
|
||||
<IntentDeliveryOverviewSidebar
|
||||
intent={frozenIntent ?? intent.intentReady}
|
||||
intent={positionIntent}
|
||||
chain={activeChain}
|
||||
nodes={positionSession?.nodes ?? []}
|
||||
activeNodeKey={activeNodeKey}
|
||||
@@ -3106,7 +3161,7 @@ export default function PositionRoundtablePage({
|
||||
positionName={activePosition.name}
|
||||
specialAgentConfig={activePosition.isIntentPosition}
|
||||
onSelectAgent={handleSelectAgent}
|
||||
intentObjective={frozenIntent?.objective ?? intent.intentReady?.objective}
|
||||
intentObjective={positionIntentHeadline}
|
||||
intentCompletedAt={intentCompletedAt}
|
||||
assignedAgentIds={configurationAgentIds}
|
||||
hasSelectedChain={showClosureAgents || Boolean(selectedChain)}
|
||||
@@ -3699,7 +3754,7 @@ function PositionNodeEmptyState({
|
||||
}
|
||||
|
||||
/** 已启动会话中的意图是冻结快照;避免在情报岗视图意外覆盖它。 */
|
||||
function IntentConfirmedState({ intent }: { intent: IntentSummary }) {
|
||||
function IntentConfirmedState({ intent }: { intent: PositionIntentSummary }) {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-slate-50/50 px-6 py-7 dark:bg-[#032650]/30">
|
||||
<div className="mx-auto w-full max-w-2xl rounded-2xl border border-emerald-500/20 bg-white p-5 shadow-sm dark:border-emerald-400/25 dark:bg-[#042b59]/65">
|
||||
@@ -3715,15 +3770,15 @@ function IntentConfirmedState({ intent }: { intent: IntentSummary }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 rounded-xl border border-slate-100 bg-slate-50/70 p-4 dark:border-[#2b8ffe]/20 dark:bg-[#032650]/40">
|
||||
<p className="text-[11px] font-semibold text-muted-foreground dark:text-[#8aa0c4]">任务目标</p>
|
||||
<p className="mt-1.5 text-sm font-semibold leading-6 text-slate-800 dark:text-[#e8edf5]">
|
||||
{desensitize(intent.objective)}
|
||||
</p>
|
||||
<p className="text-[11px] font-semibold text-muted-foreground dark:text-[#8aa0c4]">核心目标</p>
|
||||
<div className="mt-1.5 space-y-1.5 text-sm font-semibold leading-6 text-slate-800 dark:text-[#e8edf5]">
|
||||
{intent.coreGoals.map((goal, index) => <p key={`${goal}-${index}`}>{desensitize(goal)}</p>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<p className="text-[11px] font-semibold text-muted-foreground dark:text-[#8aa0c4]">关键约束</p>
|
||||
<p className="text-[11px] font-semibold text-muted-foreground dark:text-[#8aa0c4]">风险提示</p>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
||||
{intent.constraints.map((constraint, index) => (
|
||||
{intent.riskWarnings.map((constraint, index) => (
|
||||
<div
|
||||
key={`${constraint}-${index}`}
|
||||
className="flex items-start gap-2 rounded-lg border border-slate-100 bg-white px-3 py-2 text-xs leading-5 text-slate-700 dark:border-[#2b8ffe]/20 dark:bg-[#032650]/35 dark:text-[#c5d8f1]"
|
||||
@@ -3760,7 +3815,7 @@ function PositionDeliveryOverview({
|
||||
chain,
|
||||
session,
|
||||
}: {
|
||||
intent: IntentSummary | null;
|
||||
intent: PositionIntentSummary | null;
|
||||
intentCompletedAt?: string;
|
||||
nodes: PositionRoundtableNode[];
|
||||
chain: BusinessChain | null;
|
||||
@@ -3893,7 +3948,10 @@ function PositionDeliveryOverview({
|
||||
kind: "intent",
|
||||
agentName: "情报分析智能体",
|
||||
productName: "任务意图理解",
|
||||
summary: intent?.objective || "情报分析岗已确认任务意图并冻结业务链。",
|
||||
summary:
|
||||
intent?.strategicSignificance[0] ||
|
||||
intent?.coreGoals[0] ||
|
||||
"情报分析岗已确认任务意图并冻结业务链。",
|
||||
positionId: intentPositionIdFor(),
|
||||
updatedAt: intentCompletedAt || session?.updatedAt,
|
||||
status: intent ? "done" : "ready",
|
||||
@@ -4821,7 +4879,7 @@ function IntentUnderstandingCard({
|
||||
collapsed,
|
||||
onCollapsedChange,
|
||||
}: {
|
||||
intent: ReturnType<typeof useStep1Intent>["intentReady"];
|
||||
intent: PositionIntentSummary | null;
|
||||
editable?: boolean;
|
||||
onEdit?: () => void;
|
||||
collapsed: boolean;
|
||||
@@ -4863,30 +4921,21 @@ function IntentUnderstandingCard({
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
{intent ? (
|
||||
<div className="space-y-3 p-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务意图</p>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-700 dark:text-[#f2f7ff]">
|
||||
{desensitize(intent.objective)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">关键约束</p>
|
||||
<ul className="mt-1 list-disc space-y-1 pl-4 text-sm leading-6 text-slate-600 marker:text-slate-400 dark:text-[#d8e6f8] dark:marker:text-[#83b6ed]">
|
||||
{intent.constraints.map((constraint, index) => (
|
||||
<li key={`${constraint}-${index}`}>{desensitize(constraint)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{intent.assumptions && intent.assumptions.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">关键假设</p>
|
||||
<ul className="mt-1 list-disc space-y-1 pl-4 text-sm leading-6 text-slate-600 marker:text-slate-400 dark:text-[#d8e6f8] dark:marker:text-[#83b6ed]">
|
||||
{intent.assumptions.map((assumption, index) => (
|
||||
<li key={`${assumption}-${index}`}>{desensitize(assumption)}</li>
|
||||
))}
|
||||
</ul>
|
||||
{([
|
||||
["核心目标", intent.coreGoals],
|
||||
["风险提示", intent.riskWarnings],
|
||||
["关键要点", intent.keyPoints],
|
||||
["战略意义", intent.strategicSignificance],
|
||||
] as const).map(([label, values]) => (
|
||||
<div key={label}>
|
||||
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">{label}</p>
|
||||
{values.length > 0 ? (
|
||||
<ul className="mt-1 list-disc space-y-1 pl-4 text-sm leading-6 text-slate-600 marker:text-slate-400 dark:text-[#d8e6f8] dark:marker:text-[#83b6ed]">
|
||||
{values.map((value, index) => <li key={`${value}-${index}`}>{desensitize(value)}</li>)}
|
||||
</ul>
|
||||
) : <p className="mt-1 text-sm text-slate-500 dark:text-[#9ebfe2]">暂无{label}</p>}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-sm leading-6 text-muted-foreground dark:text-[#d8e6f8]">
|
||||
@@ -4907,16 +4956,17 @@ function IntentDeliveryOverviewSidebar({
|
||||
onSelectNode,
|
||||
onSelectSeat,
|
||||
}: {
|
||||
intent: IntentSummary | null;
|
||||
intent: PositionIntentSummary | null;
|
||||
chain: BusinessChain | null;
|
||||
nodes: PositionRoundtableNode[];
|
||||
activeNodeKey: string | null;
|
||||
onSelectNode: (node: PositionRoundtableNode) => void;
|
||||
onSelectSeat: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void;
|
||||
}) {
|
||||
const objective = intent?.objective?.trim() || "暂无任务目的";
|
||||
const coreGoals = intent?.constraints?.filter(Boolean) ?? [];
|
||||
const keyConstraints = intent?.assumptions?.filter(Boolean) ?? [];
|
||||
const coreGoals = intent?.coreGoals ?? [];
|
||||
const riskWarnings = intent?.riskWarnings ?? [];
|
||||
const keyPoints = intent?.keyPoints ?? [];
|
||||
const strategicSignificance = intent?.strategicSignificance ?? [];
|
||||
|
||||
return (
|
||||
<section className="position-roundtable-intent-delivery-sidebar">
|
||||
@@ -4926,10 +4976,6 @@ function IntentDeliveryOverviewSidebar({
|
||||
<h2>意图理解结果</h2>
|
||||
</header>
|
||||
<div className="position-roundtable-intent-delivery-sections">
|
||||
<article className="position-roundtable-intent-delivery-section position-roundtable-intent-delivery-section--purpose">
|
||||
<h3>任务目的</h3>
|
||||
<p>{desensitize(objective)}</p>
|
||||
</article>
|
||||
<article className="position-roundtable-intent-delivery-section position-roundtable-intent-delivery-section--goals">
|
||||
<h3>核心目标</h3>
|
||||
{coreGoals.length > 0 ? (
|
||||
@@ -4943,17 +4989,25 @@ function IntentDeliveryOverviewSidebar({
|
||||
)}
|
||||
</article>
|
||||
<article className="position-roundtable-intent-delivery-section position-roundtable-intent-delivery-section--constraints">
|
||||
<h3>关键约束</h3>
|
||||
{keyConstraints.length > 0 ? (
|
||||
<h3>风险提示</h3>
|
||||
{riskWarnings.length > 0 ? (
|
||||
<ul>
|
||||
{keyConstraints.map((constraint, index) => (
|
||||
<li key={`${constraint}-${index}`}>{desensitize(constraint)}</li>
|
||||
{riskWarnings.map((warning, index) => (
|
||||
<li key={`${warning}-${index}`}>{desensitize(warning)}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="is-empty">暂无补充约束</p>
|
||||
<p className="is-empty">暂无风险提示</p>
|
||||
)}
|
||||
</article>
|
||||
<article className="position-roundtable-intent-delivery-section position-roundtable-intent-delivery-section--purpose">
|
||||
<h3>关键要点</h3>
|
||||
{keyPoints.length > 0 ? <ul>{keyPoints.map((item, index) => <li key={`${item}-${index}`}>{desensitize(item)}</li>)}</ul> : <p className="is-empty">暂无关键要点</p>}
|
||||
</article>
|
||||
<article className="position-roundtable-intent-delivery-section position-roundtable-intent-delivery-section--significance">
|
||||
<h3>战略意义</h3>
|
||||
{strategicSignificance.length > 0 ? <ul>{strategicSignificance.map((item, index) => <li key={`${item}-${index}`}>{desensitize(item)}</li>)}</ul> : <p className="is-empty">暂无战略意义</p>}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4999,34 +5053,43 @@ function IntentUnderstandingEditDialog({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-1">
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务意图</span>
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">核心目标</span>
|
||||
<Textarea
|
||||
value={draft.objective}
|
||||
onChange={(event) => onChange({ ...draft, objective: event.target.value })}
|
||||
value={draft.coreGoals}
|
||||
onChange={(event) => onChange({ ...draft, coreGoals: event.target.value })}
|
||||
className="min-h-24 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||
placeholder="请输入识别后的任务意图"
|
||||
placeholder="每行一项核心目标"
|
||||
/>
|
||||
</label>
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">
|
||||
关键约束
|
||||
风险提示
|
||||
</span>
|
||||
<Textarea
|
||||
value={draft.constraints}
|
||||
onChange={(event) => onChange({ ...draft, constraints: event.target.value })}
|
||||
value={draft.riskWarnings}
|
||||
onChange={(event) => onChange({ ...draft, riskWarnings: event.target.value })}
|
||||
className="min-h-28 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||
placeholder="每行一条关键约束"
|
||||
placeholder="每行一项风险、依赖或待确认事项"
|
||||
/>
|
||||
</label>
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">
|
||||
补充假设 / 说明
|
||||
关键要点
|
||||
</span>
|
||||
<Textarea
|
||||
value={draft.assumptions}
|
||||
onChange={(event) => onChange({ ...draft, assumptions: event.target.value })}
|
||||
value={draft.keyPoints}
|
||||
onChange={(event) => onChange({ ...draft, keyPoints: event.target.value })}
|
||||
className="min-h-20 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||
placeholder="可选,每行一条"
|
||||
placeholder="每行一项关键要点"
|
||||
/>
|
||||
</label>
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">战略意义</span>
|
||||
<Textarea
|
||||
value={draft.strategicSignificance}
|
||||
onChange={(event) => onChange({ ...draft, strategicSignificance: event.target.value })}
|
||||
className="min-h-20 resize-none dark:border-[#2b8ffe]/35 dark:bg-[#032650]/80"
|
||||
placeholder="每行一项战略意义"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -5983,7 +6046,7 @@ function deliverableFallbackContent(item: ResultItem | null): string {
|
||||
}
|
||||
|
||||
function buildIntentResultItem(
|
||||
intent: IntentSummary | null,
|
||||
intent: PositionIntentSummary | null,
|
||||
updatedAt: string | undefined,
|
||||
): ResultItem {
|
||||
return {
|
||||
@@ -5992,7 +6055,10 @@ function buildIntentResultItem(
|
||||
node: null,
|
||||
agentName: "情报分析智能体",
|
||||
productName: "任务意图理解",
|
||||
summary: intent?.objective || "任务意图已确认,当前没有更多意图摘要内容。",
|
||||
summary:
|
||||
intent?.strategicSignificance[0] ||
|
||||
intent?.coreGoals[0] ||
|
||||
"任务意图已确认,当前没有更多意图摘要内容。",
|
||||
positionId: intentPositionIdFor(),
|
||||
updatedAt,
|
||||
status: intent ? "done" : "ready",
|
||||
|
||||
@@ -397,6 +397,15 @@
|
||||
color: #33bc92;
|
||||
}
|
||||
|
||||
.position-roundtable-intent-delivery-section--significance {
|
||||
border-left: 4px solid #45c3f3;
|
||||
background: rgba(0, 98, 217, 0.12);
|
||||
}
|
||||
|
||||
.position-roundtable-intent-delivery-section--significance h3 {
|
||||
color: #45c3f3;
|
||||
}
|
||||
|
||||
.position-roundtable-intent-delivery-section .is-empty {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import { getBackendBaseURL } from "@/core/config";
|
||||
import type { ClarificationPayload } from "./multi-agent";
|
||||
import { reportRoundtableError } from "./diagnostics-report";
|
||||
import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification";
|
||||
import { normalizeIntentReadySummary } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
|
||||
export type { ClarificationPayload };
|
||||
|
||||
/** Stable built-in agent used by the multi-agent roundtable Step 1 flow. */
|
||||
export const INTENT_AGENT_ID = "roundtable-intent";
|
||||
/** Dedicated Step 1 agent for the position-roundtable workspace. */
|
||||
export const POSITION_INTENT_AGENT_ID = "position-roundtable-intent";
|
||||
|
||||
/**
|
||||
* Task-intent agent (Step 1) API. Mirror of multi-agent.ts but with a single
|
||||
@@ -103,13 +106,18 @@ function parseIntentErrorDetail(value: unknown): IntentErrorDetail | null {
|
||||
export async function initIntent(
|
||||
model?: string,
|
||||
taskContext?: Record<string, unknown> | null,
|
||||
intentAgentId?: string,
|
||||
): Promise<IntentInitResponse> {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/intent/init`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
// 岗位会商在创建意图线程时就写入父任务标识。这样即使用户尚未确认
|
||||
// 业务链,第一阶段的对话也已经可以按 taskId 恢复。
|
||||
body: JSON.stringify({ model, task_context: taskContext ?? undefined }),
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
task_context: taskContext ?? undefined,
|
||||
intent_agent_id: intentAgentId || undefined,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const raw = await res.text().catch(() => "");
|
||||
@@ -140,6 +148,19 @@ export interface IntentSummary {
|
||||
constraints: string[];
|
||||
/** Optional assumptions the agent is making on the user's behalf. */
|
||||
assumptions?: string[];
|
||||
/** Present only for the dedicated position-roundtable intent agent. */
|
||||
coreGoals?: string[];
|
||||
riskWarnings?: string[];
|
||||
keyPoints?: string[];
|
||||
strategicSignificance?: string[];
|
||||
}
|
||||
|
||||
/** Final hand-off shape emitted only by the position-roundtable intent agent. */
|
||||
export interface PositionIntentSummary {
|
||||
coreGoals: string[];
|
||||
riskWarnings: string[];
|
||||
keyPoints: string[];
|
||||
strategicSignificance: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,6 +188,8 @@ export interface StreamIntentRequest {
|
||||
threadId: string;
|
||||
message: string;
|
||||
model?: string;
|
||||
/** Explicit built-in Step-1 agent. Omit to retain the multi-agent default. */
|
||||
intentAgentId?: string;
|
||||
/** Optional hidden business/task context injected into the model run. */
|
||||
taskContext?: Record<string, unknown> | null;
|
||||
/** Step 1 用户上传的参考文件(可选)。 */
|
||||
@@ -189,8 +212,7 @@ export interface StreamIntentRequest {
|
||||
* Fires for every `ask_clarification` tool message parsed out of the live
|
||||
* stream (the intent agent may emit several in a single turn). Lets the UI
|
||||
* surface each assistance question in real-time instead of waiting for the
|
||||
* terminal frame — which only carries the *first* question. `index` is the
|
||||
* 0-based arrival order within this turn.
|
||||
* terminal frame. `index` is the 0-based arrival order within this turn.
|
||||
*/
|
||||
onClarification?: (clarification: ClarificationPayload, index: number) => void;
|
||||
}
|
||||
@@ -211,8 +233,8 @@ export interface StreamIntentResult {
|
||||
clarification: ClarificationPayload | null;
|
||||
/**
|
||||
* All `ask_clarification` questions the agent asked this turn, in arrival
|
||||
* order. Collected from the live `type:"tool"` frames (the terminal frame
|
||||
* only carries the first). Empty unless status === "clarification".
|
||||
* order. Collected live and reconciled against the terminal frame so a
|
||||
* reconnect cannot lose cards. Empty unless status === "clarification".
|
||||
*/
|
||||
clarifications: ClarificationPayload[];
|
||||
/**
|
||||
@@ -238,6 +260,7 @@ function isFinalIntentFrame(value: unknown): value is {
|
||||
options?: unknown[];
|
||||
allow_custom?: boolean;
|
||||
allow_multiple?: boolean;
|
||||
clarifications?: unknown[];
|
||||
error_detail?: unknown;
|
||||
} {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
@@ -330,6 +353,41 @@ function extractClarificationToolMessage(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one clarification included in the terminal status frame. This is
|
||||
* the reconnect / proxy fallback for cases where one or more live `tool`
|
||||
* frames were not delivered to the browser.
|
||||
*/
|
||||
function parseTerminalClarification(
|
||||
value: unknown,
|
||||
): { payload: ClarificationPayload; toolCallId: string } | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const raw = value as Record<string, unknown>;
|
||||
const clarificationType =
|
||||
typeof raw.clarification_type === "string" ? raw.clarification_type : "";
|
||||
const toolCallId = typeof raw.id === "string" ? raw.id : "";
|
||||
return {
|
||||
toolCallId,
|
||||
payload: {
|
||||
id: toolCallId || undefined,
|
||||
question: typeof raw.question === "string" ? raw.question : "",
|
||||
clarificationType,
|
||||
context:
|
||||
typeof raw.clarification_context === "string"
|
||||
? raw.clarification_context
|
||||
: typeof raw.context === "string"
|
||||
? raw.context
|
||||
: "",
|
||||
options: Array.isArray(raw.options) ? raw.options : [],
|
||||
allowCustom: raw.allow_custom !== false,
|
||||
allowMultiple: resolveAllowMultiple(
|
||||
typeof raw.allow_multiple === "boolean" ? raw.allow_multiple : undefined,
|
||||
clarificationType,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an SSE connection to /api/intent/stream and resolve when the upstream
|
||||
* stream completes. Each call is one user turn; reuse the same threadId
|
||||
@@ -343,6 +401,7 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
thread_id: req.threadId,
|
||||
message: req.message,
|
||||
model: req.model,
|
||||
intent_agent_id: req.intentAgentId || undefined,
|
||||
task_context: req.taskContext ?? undefined,
|
||||
// 仅在有文件时带上,空数组后端也安全忽略。
|
||||
files: req.files && req.files.length > 0 ? req.files : undefined,
|
||||
@@ -390,8 +449,7 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
};
|
||||
|
||||
// Clarifications collected live from `type:"tool"` frames, deduped by
|
||||
// tool_call_id. The terminal frame only carries the first question, so this
|
||||
// is the source of truth for multi-question turns.
|
||||
// tool_call_id, then reconciled against the complete terminal list.
|
||||
const seenClarificationIds = new Set<string>();
|
||||
const collectClarification = (payload: ClarificationPayload, toolCallId: string) => {
|
||||
const dedupeKey = toolCallId || `idx-${result.clarifications.length}`;
|
||||
@@ -438,7 +496,7 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
result.status = parsed.status;
|
||||
if (typeof parsed.content === "string") result.content = parsed.content;
|
||||
if (parsed.summary && typeof parsed.summary === "object") {
|
||||
result.summary = parsed.summary as IntentSummary;
|
||||
result.summary = normalizeIntentReadySummary(parsed.summary);
|
||||
}
|
||||
if (parsed.status === "error") {
|
||||
result.errorDetail = parseIntentErrorDetail(parsed.error_detail);
|
||||
@@ -448,28 +506,26 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
detail: result.errorDetail,
|
||||
});
|
||||
}
|
||||
if (parsed.status === "clarification" && result.clarifications.length === 0) {
|
||||
// Fallback path: no live `type:"tool"` clarification frames were seen
|
||||
// (older backend, or single-question turn) — synthesize one from the
|
||||
// terminal frame so the UI still gets a card. When tool frames *were*
|
||||
// seen, `clarifications` already holds the full (possibly multi-question)
|
||||
// set and we keep it.
|
||||
const clarificationType =
|
||||
typeof parsed.clarification_type === "string" ? parsed.clarification_type : "";
|
||||
const fallback: ClarificationPayload = {
|
||||
question: typeof parsed.question === "string" ? parsed.question : "",
|
||||
clarificationType,
|
||||
context:
|
||||
typeof parsed.clarification_context === "string" ? parsed.clarification_context : "",
|
||||
options: Array.isArray(parsed.options) ? parsed.options : [],
|
||||
allowCustom: parsed.allow_custom !== false,
|
||||
allowMultiple: resolveAllowMultiple(
|
||||
typeof parsed.allow_multiple === "boolean" ? parsed.allow_multiple : undefined,
|
||||
clarificationType,
|
||||
),
|
||||
};
|
||||
result.clarification = fallback;
|
||||
result.clarifications = [fallback];
|
||||
if (parsed.status === "clarification") {
|
||||
// Newer backend frames contain every card. Merge them even when a
|
||||
// subset already arrived live: an SSE reconnect can otherwise leave
|
||||
// the user with only the first card in a multi-question assistance
|
||||
// round. Older backends still fall back to the legacy top-level card.
|
||||
const terminalCards = Array.isArray(parsed.clarifications)
|
||||
? parsed.clarifications
|
||||
.map(parseTerminalClarification)
|
||||
.filter((item): item is { payload: ClarificationPayload; toolCallId: string } => item !== null)
|
||||
: [];
|
||||
if (terminalCards.length > 0) {
|
||||
terminalCards.forEach((card, index) => {
|
||||
collectClarification(card.payload, card.toolCallId || `terminal-${index}`);
|
||||
});
|
||||
} else if (result.clarifications.length === 0) {
|
||||
const fallback = parseTerminalClarification(parsed);
|
||||
if (fallback) {
|
||||
collectClarification(fallback.payload, fallback.toolCallId || "terminal-0");
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ export function BusinessChainPicker({
|
||||
// 席位「会商技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
const humanValidationById = new Map(
|
||||
selectedChain.seats.map((s) => [s.agent_id, s.human_validation ?? false]),
|
||||
);
|
||||
const humanValidationOf = (id: string) => humanValidationById.get(id) ?? false;
|
||||
// 席位「推理深度覆盖」(链条配置 seat_mode):按 agent_id 取,null/缺省 = 跟随链条级 seatMode。
|
||||
const seatModeById = new Map(selectedChain.seats.map((s) => [s.agent_id, s.seat_mode ?? null]));
|
||||
const seatModeOf = (id: string) => seatModeById.get(id) ?? null;
|
||||
@@ -162,12 +166,12 @@ export function BusinessChainPicker({
|
||||
// dag:席位顺序按 stage 展平(与 plan 一致)。
|
||||
const agents = dagPlan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), seatMode: seatModeOf(id) }));
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), human_validation: humanValidationOf(id), seatMode: seatModeOf(id) }));
|
||||
onConfirm({ agents, mode: "dag", chain: chainMeta, plan: dagPlan, dagExecMode, gatherFirst: selectedChain.gatherFirst ?? false });
|
||||
return;
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), human_validation: humanValidationOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
onConfirm({ agents, mode: "chain", chain: chainMeta, gatherFirst: selectedChain.gatherFirst ?? false });
|
||||
};
|
||||
|
||||
|
||||
@@ -688,6 +688,15 @@ function Step1Message({
|
||||
|
||||
/** [INTENT_READY] 标记块解析出 IntentSummary 后的绿色摘要卡。 */
|
||||
function IntentReadySummaryCard({ summary }: { summary: IntentSummary }) {
|
||||
const positionSections =
|
||||
summary.coreGoals && summary.riskWarnings && summary.keyPoints && summary.strategicSignificance
|
||||
? ([
|
||||
["核心目标", summary.coreGoals],
|
||||
["风险提示", summary.riskWarnings],
|
||||
["关键要点", summary.keyPoints],
|
||||
["战略意义", summary.strategicSignificance],
|
||||
] as const)
|
||||
: null;
|
||||
return (
|
||||
<div className="mt-1 border border-emerald-200 bg-emerald-50/50 rounded-xl p-3 space-y-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -696,30 +705,35 @@ function IntentReadySummaryCard({ summary }: { summary: IntentSummary }) {
|
||||
任务意图已明确
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">核心目标</span>
|
||||
<p className="text-[11.5px] text-slate-800 leading-relaxed">{summary.objective}</p>
|
||||
</div>
|
||||
{summary.constraints?.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">约束条件</span>
|
||||
<ul className="list-disc pl-4 space-y-0.5 text-[11px] text-slate-700">
|
||||
{summary.constraints.map((c, i) => (
|
||||
<li key={i} className="leading-snug">{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
{positionSections ? positionSections.map(([label, values]) => (
|
||||
<div key={label} className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">{label}</span>
|
||||
{values.length > 0 ? <ul className="list-disc pl-4 space-y-0.5 text-[11px] text-slate-700">
|
||||
{values.map((value, index) => <li key={index} className="leading-snug">{value}</li>)}
|
||||
</ul> : <p className="text-[11px] text-slate-500">暂无{label}</p>}
|
||||
</div>
|
||||
)}
|
||||
{summary.assumptions && summary.assumptions.length > 0 && (
|
||||
)) : <>
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">关键假设</span>
|
||||
<ul className="list-disc pl-4 space-y-0.5 text-[11px] text-slate-700">
|
||||
{summary.assumptions.map((a, i) => (
|
||||
<li key={i} className="leading-snug">{a}</li>
|
||||
))}
|
||||
</ul>
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">核心目标</span>
|
||||
<p className="text-[11.5px] text-slate-800 leading-relaxed">{summary.objective}</p>
|
||||
</div>
|
||||
)}
|
||||
{summary.constraints?.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">约束条件</span>
|
||||
<ul className="list-disc pl-4 space-y-0.5 text-[11px] text-slate-700">
|
||||
{summary.constraints.map((c, i) => <li key={i} className="leading-snug">{c}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{summary.assumptions && summary.assumptions.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">关键假设</span>
|
||||
<ul className="list-disc pl-4 space-y-0.5 text-[11px] text-slate-700">
|
||||
{summary.assumptions.map((a, i) => <li key={i} className="leading-snug">{a}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -90,6 +90,8 @@ export interface UseStep1IntentOptions {
|
||||
currentStep: number;
|
||||
/** 全局选中模型;调用 `/api/intent/init` / `/api/intent/stream` 时透传。 */
|
||||
selectedModel: string;
|
||||
/** Optional built-in agent override for a workspace with a distinct schema. */
|
||||
intentAgentId?: string;
|
||||
/** Optional hidden task/business context for each intent-agent turn. */
|
||||
taskContext?: Record<string, unknown> | null;
|
||||
/** Optional task-aware welcome bubble text before the first user turn. */
|
||||
@@ -178,6 +180,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
const {
|
||||
currentStep,
|
||||
selectedModel,
|
||||
intentAgentId,
|
||||
taskContext,
|
||||
initialWelcomeText,
|
||||
showInitialWelcome = true,
|
||||
@@ -214,6 +217,10 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
useEffect(() => {
|
||||
selectedModelRef.current = selectedModel;
|
||||
}, [selectedModel]);
|
||||
const intentAgentIdRef = useRef<string | undefined>(intentAgentId);
|
||||
useEffect(() => {
|
||||
intentAgentIdRef.current = intentAgentId;
|
||||
}, [intentAgentId]);
|
||||
const taskContextRef = useRef<Record<string, unknown> | null>(taskContext ?? null);
|
||||
useEffect(() => {
|
||||
taskContextRef.current = taskContext ?? null;
|
||||
@@ -265,6 +272,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
const res = await initIntent(
|
||||
selectedModelRef.current || undefined,
|
||||
taskContextRef.current,
|
||||
intentAgentIdRef.current,
|
||||
);
|
||||
if (step1InitGenRef.current !== myGen) return;
|
||||
setIntentThreadId(res.thread_id);
|
||||
@@ -443,6 +451,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
threadId: intentThreadId,
|
||||
message: trimmed,
|
||||
model: selectedModelRef.current || undefined,
|
||||
intentAgentId: intentAgentIdRef.current,
|
||||
taskContext: taskContextRef.current,
|
||||
files: fileRefs,
|
||||
signal: aborter.signal,
|
||||
|
||||
@@ -33,6 +33,13 @@ import { useUpdateSubtask } from "@/core/tasks/context";
|
||||
/** 干预消息携带的上传文件引用(与 streamMultiAgent 的 files 字段同形)。 */
|
||||
type RunFileRef = NonNullable<StreamMultiAgentRequest["files"]>[number];
|
||||
|
||||
type SeatDelivery = { name: string; content: string };
|
||||
|
||||
/** In-memory cursor for a business-chain human-validation pause. */
|
||||
type HumanValidationContinuation =
|
||||
| { mode: "chain"; nextSeatIndex: number }
|
||||
| { mode: "dag"; nextStageIndex: number; priorDeliveries: SeatDelivery[] };
|
||||
|
||||
/** 读取并清空 ref(consume-once):首个 leader run 拿到文件后清空,后续轮次不再带。 */
|
||||
function takeOnce<T>(ref: { current: T | null }): T | undefined {
|
||||
const v = ref.current ?? undefined;
|
||||
@@ -286,6 +293,8 @@ export interface UseStep2OrchestrationOptions {
|
||||
* OR。recommend / 未选链条 → 空 {}(各席位仅靠其自身开关)。
|
||||
*/
|
||||
seatSkillDirectives?: Record<string, boolean>;
|
||||
/** Per-seat human-validation checkpoints inherited from the selected chain. */
|
||||
humanValidations?: Record<string, boolean>;
|
||||
/**
|
||||
* 每席位「推理深度覆盖」映射 {agentId: SeatMode | null}(沿用所选业务链条里每个席位的配置)。
|
||||
* 派活该 special 席位时按 agentId 查表,**席位覆盖优先、否则跟随链条级 `selectedMode`**:
|
||||
@@ -456,6 +465,7 @@ export function useStep2Orchestration(
|
||||
selectedModel,
|
||||
selectedMode,
|
||||
seatSkillDirectives = {},
|
||||
humanValidations = {},
|
||||
seatModes = {},
|
||||
gatherFirst = false,
|
||||
orchestrationMode,
|
||||
@@ -593,6 +603,7 @@ export function useStep2Orchestration(
|
||||
const selectedModeRef = useRef<SeatMode>(selectedMode);
|
||||
// 每席位技能识别强化映射(所选业务链条各席位开关)同样打到 ref,席位调用按 agentId 读最新值。
|
||||
const seatSkillDirectivesRef = useRef<Record<string, boolean>>(seatSkillDirectives);
|
||||
const humanValidationsRef = useRef<Record<string, boolean>>(humanValidations);
|
||||
// 每席位推理深度覆盖映射(所选业务链条各席位 seatMode)打到 ref,席位调用按 agentId 读最新值;
|
||||
// 覆盖优先、否则跟随 selectedModeRef。
|
||||
const seatModesRef = useRef<Record<string, SeatMode | null>>(seatModes);
|
||||
@@ -624,6 +635,7 @@ export function useStep2Orchestration(
|
||||
// 澄清回复都改走 **leader 自由派活循环**(runOrchestration),由总控接着派活,而**不是**
|
||||
// 重新把整张 DAG 从头跑一遍(致命 bug 修复)。新任务 / 重新进入时由 resetInitGate 复位。
|
||||
const dagPlanExecutedRef = useRef<boolean>(false);
|
||||
const humanValidationContinuationRef = useRef<HumanValidationContinuation | null>(null);
|
||||
const selectedAgentsRef = useRef<SelectedAgent[]>(selectedAgents);
|
||||
useEffect(() => {
|
||||
orchestrationModeRef.current = orchestrationMode;
|
||||
@@ -643,6 +655,9 @@ export function useStep2Orchestration(
|
||||
useEffect(() => {
|
||||
seatSkillDirectivesRef.current = seatSkillDirectives;
|
||||
}, [seatSkillDirectives]);
|
||||
useEffect(() => {
|
||||
humanValidationsRef.current = humanValidations;
|
||||
}, [humanValidations]);
|
||||
useEffect(() => {
|
||||
seatModesRef.current = seatModes;
|
||||
}, [seatModes]);
|
||||
@@ -1213,6 +1228,21 @@ export function useStep2Orchestration(
|
||||
[],
|
||||
);
|
||||
|
||||
const pauseForHumanValidation = useCallback(
|
||||
(continuation: HumanValidationContinuation, seatNames: string[]) => {
|
||||
humanValidationContinuationRef.current = continuation;
|
||||
appendDialogue({
|
||||
sender: "系统",
|
||||
avatarType: "blue",
|
||||
content: `已完成 ${seatNames.join("、")} 的输出,按业务链条配置暂停,等待人工校验。可直接点击“恢复会商”继续,或在下方输入修改意见后提交。`,
|
||||
confidence: "等待人工校验",
|
||||
});
|
||||
setIsRoundTableRunning(false);
|
||||
triggerToast("已暂停等待人工校验:可恢复会商,或输入修改意见后继续");
|
||||
},
|
||||
[appendDialogue, triggerToast],
|
||||
);
|
||||
|
||||
const phaseLabel = useCallback(
|
||||
(
|
||||
phase: StreamPhase,
|
||||
@@ -2012,11 +2042,17 @@ export function useStep2Orchestration(
|
||||
|
||||
const chainSeats = selectedAgentsRef.current;
|
||||
const total = chainSeats.length;
|
||||
const continuation = humanValidationContinuationRef.current;
|
||||
const startSeatIndex =
|
||||
continuation?.mode === "chain" ? continuation.nextSeatIndex : 0;
|
||||
if (continuation?.mode === "chain") {
|
||||
humanValidationContinuationRef.current = null;
|
||||
}
|
||||
const leaderThreadId = resolveSeatThreadId(ids, coord);
|
||||
let finishedAllSeats = true;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
for (let i = startSeatIndex; i < total; i += 1) {
|
||||
if (isStale()) {
|
||||
finishedAllSeats = false;
|
||||
break;
|
||||
@@ -2179,6 +2215,14 @@ export function useStep2Orchestration(
|
||||
}
|
||||
|
||||
setConsensusPercentage(Math.min(95, Math.round(((i + 1) / total) * 90)));
|
||||
if (humanValidationsRef.current[seat.agent_id]) {
|
||||
pauseForHumanValidation(
|
||||
{ mode: "chain", nextSeatIndex: i + 1 },
|
||||
[seatName],
|
||||
);
|
||||
finishedAllSeats = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── (c) 总控收口:综合全链交付,产出最终结论 ──
|
||||
@@ -2257,6 +2301,7 @@ export function useStep2Orchestration(
|
||||
handleUpstreamFrame,
|
||||
ingestSubagentTaskFrame,
|
||||
resolveSeatThreadId,
|
||||
pauseForHumanValidation,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2300,23 +2345,37 @@ export function useStep2Orchestration(
|
||||
dagPlanExecutedRef.current = true;
|
||||
|
||||
const leaderThreadId = resolveSeatThreadId(ids, coord);
|
||||
const continuation = humanValidationContinuationRef.current;
|
||||
const startStageIndex =
|
||||
continuation?.mode === "dag" ? continuation.nextStageIndex : 0;
|
||||
if (continuation?.mode === "dag") {
|
||||
humanValidationContinuationRef.current = null;
|
||||
}
|
||||
let finishedAllStages = true;
|
||||
// 跨 stage 累积的**完整交付** {name, content}[] —— 注入下一 stage 席位的 run context
|
||||
// (peerDeliveries),供 read_peer_delivery 工具按需取全文(§12.9 传摘要按需传全文)。
|
||||
// 研讨前并行取数(gatherFirst)的产物作为初始注入:首个 stage 席位开局即带共享资料。
|
||||
const priorDeliveries: Array<{ name: string; content: string }> = [...gatheredDeliveriesRef.current];
|
||||
const priorDeliveries: SeatDelivery[] =
|
||||
continuation?.mode === "dag"
|
||||
? [...continuation.priorDeliveries]
|
||||
: [...gatheredDeliveriesRef.current];
|
||||
|
||||
// Phase 7:初始化 DAG 流程图节点状态 —— 计划内所有席位置 pending,执行过程中逐个点亮。
|
||||
const markDagNode = (agentId: string, state: ChainNodeState) =>
|
||||
setDagNodeStates((prev) => ({ ...prev, [agentId.toLowerCase()]: state }));
|
||||
setDagNodeStates(
|
||||
Object.fromEntries(
|
||||
plan.stages.flatMap((st) => st.agentIds.map((id) => [id.toLowerCase(), "pending" as ChainNodeState])),
|
||||
plan.stages.flatMap((st, stageIndex) =>
|
||||
st.agentIds.map((id) => [
|
||||
id.toLowerCase(),
|
||||
(stageIndex < startStageIndex ? "done" : "pending") as ChainNodeState,
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
for (let s = 0; s < plan.stages.length; s += 1) {
|
||||
for (let s = startStageIndex; s < plan.stages.length; s += 1) {
|
||||
if (isStale()) {
|
||||
finishedAllStages = false;
|
||||
break;
|
||||
@@ -2558,6 +2617,21 @@ export function useStep2Orchestration(
|
||||
setConsensusPercentage(
|
||||
Math.min(95, Math.round(((s + 1) / plan.stages.length) * 90)),
|
||||
);
|
||||
const validationSeatNames = stageSeats
|
||||
.filter(({ agentId }) => humanValidationsRef.current[agentId])
|
||||
.map(({ name }) => name);
|
||||
if (validationSeatNames.length > 0) {
|
||||
pauseForHumanValidation(
|
||||
{
|
||||
mode: "dag",
|
||||
nextStageIndex: s + 1,
|
||||
priorDeliveries: [...priorDeliveries],
|
||||
},
|
||||
validationSeatNames,
|
||||
);
|
||||
finishedAllStages = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 计划阶段全部完成 → **交给总控(leader 自由派活循环)继续** ──
|
||||
@@ -2613,6 +2687,7 @@ export function useStep2Orchestration(
|
||||
ingestParallelDelta,
|
||||
resetParallelStreams,
|
||||
runOrchestration,
|
||||
pauseForHumanValidation,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2746,8 +2821,9 @@ export function useStep2Orchestration(
|
||||
// 改走 leader 自由派活循环,由总控接着派活,而不是把 DAG 从头重跑一遍。
|
||||
if (
|
||||
mode === "dag" &&
|
||||
!dagPlanExecutedRef.current &&
|
||||
(orchestrationPlanRef.current?.stages.length ?? 0) > 0
|
||||
(humanValidationContinuationRef.current?.mode === "dag" ||
|
||||
(!dagPlanExecutedRef.current &&
|
||||
(orchestrationPlanRef.current?.stages.length ?? 0) > 0))
|
||||
) {
|
||||
return runDagOrchestration(seedMessage, ids, coord);
|
||||
}
|
||||
@@ -3070,6 +3146,7 @@ export function useStep2Orchestration(
|
||||
|
||||
const resetInitGate = useCallback(() => {
|
||||
step2InitRef.current = false;
|
||||
humanValidationContinuationRef.current = null;
|
||||
// 新一轮研讨:允许 DAG 计划重新执行一遍。
|
||||
dagPlanExecutedRef.current = false;
|
||||
// 新一轮研讨:允许「研讨前并行取数」再取一次(清掉上一轮取数产物 + 闸门)。
|
||||
@@ -3086,6 +3163,7 @@ export function useStep2Orchestration(
|
||||
// 这里只重复设 cancel ref 与 UI 标记,确保从 Step 2 在编排中切到草稿时
|
||||
// UI 立刻翻成静止态。
|
||||
orchestrationCancelRef.current = true;
|
||||
humanValidationContinuationRef.current = null;
|
||||
setIsOrchestrating(false);
|
||||
setIsInitializing(false);
|
||||
setPendingClarification(null);
|
||||
@@ -3166,6 +3244,7 @@ export function useStep2Orchestration(
|
||||
return false;
|
||||
}
|
||||
dagPlanExecutedRef.current = true;
|
||||
humanValidationContinuationRef.current = null;
|
||||
orchestrationCancelRef.current = true;
|
||||
abortAllInFlight();
|
||||
isOrchestratingRef.current = false;
|
||||
@@ -3208,6 +3287,7 @@ export function useStep2Orchestration(
|
||||
hasAutoOpenedRef.current = new Set();
|
||||
pendingInjectPromptRef.current = null;
|
||||
dagPlanExecutedRef.current = false;
|
||||
humanValidationContinuationRef.current = null;
|
||||
gatherPhaseDoneRef.current = false;
|
||||
gatheredDeliveriesRef.current = [];
|
||||
deselectArtifact();
|
||||
|
||||
@@ -36,6 +36,11 @@ export interface ChainSeat {
|
||||
* (无需 DB 列),在链条编辑器里逐席位开关。
|
||||
*/
|
||||
seat_skill_directive?: boolean;
|
||||
/**
|
||||
* Human-validation checkpoint. When enabled, Step 2 pauses after this seat
|
||||
* (or its DAG stage) completes and reuses the existing resume/intervention UI.
|
||||
*/
|
||||
human_validation?: boolean;
|
||||
/**
|
||||
* 可选「席位推理深度覆盖」:覆盖链条级 `seatMode`,只对该席位生效。值是同一套
|
||||
* `SeatMode`(flash/thinking/pro/ultra);null / 缺省 = 跟随链条级默认。用于「卡顿提速」:
|
||||
@@ -109,7 +114,7 @@ export interface SeatSelectionResult {
|
||||
* `model` 为该席位在来源链条里配置的模型覆盖(空 / 缺省 = 跟随全局模型)。
|
||||
* `seat_skill_directive` 为该席位在来源链条里配置的「技能识别强化」开关(缺省 = 关)。
|
||||
*/
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean; seatMode?: SeatMode | null }>;
|
||||
agents: Array<{ agent_id: string; name: string; model?: string | null; seat_skill_directive?: boolean; human_validation?: boolean; seatMode?: SeatMode | null }>;
|
||||
/**
|
||||
* 决定 Step 2 走哪种编排:
|
||||
* - `recommend` 总控自由派活;
|
||||
@@ -171,6 +176,10 @@ export function buildChainSeatSelection(
|
||||
// 席位「技能识别强化」开关(链条配置):按 agent_id 取,缺省 = 关。
|
||||
const skillById = new Map(chain.seats.map((s) => [s.agent_id, s.seat_skill_directive ?? false]));
|
||||
const skillOf = (id: string) => skillById.get(id) ?? false;
|
||||
const humanValidationById = new Map(
|
||||
chain.seats.map((s) => [s.agent_id, s.human_validation ?? false]),
|
||||
);
|
||||
const humanValidationOf = (id: string) => humanValidationById.get(id) ?? false;
|
||||
// 席位「推理深度覆盖」(链条配置 seat_mode):按 agent_id 取,null/缺省 = 跟随链条级 seatMode。
|
||||
// 输出到 SeatSelectionResult.agents[].seatMode(camel,喂给编排/作业的 seatModes map)。
|
||||
const seatModeById = new Map(chain.seats.map((s) => [s.agent_id, s.seat_mode ?? null]));
|
||||
@@ -196,12 +205,12 @@ export function buildChainSeatSelection(
|
||||
if (hasParallel || hasGoal || hasCoord) {
|
||||
const agents = plan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), seatMode: seatModeOf(id) }));
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id), model: modelOf(id), seat_skill_directive: skillOf(id), human_validation: humanValidationOf(id), seatMode: seatModeOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "sandbox", seatMode: chain.seatMode, gatherFirst: chain.gatherFirst ?? false };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id), model: modelOf(s.agent_id), seat_skill_directive: skillOf(s.agent_id), human_validation: humanValidationOf(s.agent_id), seatMode: seatModeOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta, seatMode: chain.seatMode, gatherFirst: chain.gatherFirst ?? false };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
moveSeatToStage,
|
||||
moveStage,
|
||||
removeSeat,
|
||||
setSeatHumanValidation,
|
||||
setStageGoal,
|
||||
splitSeatToNewStage,
|
||||
stageGroupsToSeats,
|
||||
@@ -122,6 +123,15 @@ test("setStageGoal is immutable + out-of-range no-op", () => {
|
||||
assert.equal(setStageGoal(groups, 9, "x"), groups); // 越界返回原引用
|
||||
});
|
||||
|
||||
test("setSeatHumanValidation updates only the selected seat and is immutable", () => {
|
||||
const groups = grp([seat("a"), seat("b")], [seat("c")]);
|
||||
const next = setSeatHumanValidation(groups, "b", true);
|
||||
assert.equal(next[0].seats[1].human_validation, true);
|
||||
assert.equal(next[0].seats[0].human_validation, undefined);
|
||||
assert.equal(next[1].seats[0].human_validation, undefined);
|
||||
assert.equal(groups[0].seats[1].human_validation, undefined);
|
||||
});
|
||||
|
||||
// ── lookups ───────────────────────────────────────────────────────────────────
|
||||
|
||||
test("findStageIndexOfSeat / countSeats", () => {
|
||||
|
||||
@@ -141,6 +141,23 @@ export function setSeatSkillDirective(groups: StageGroups, agentId: string, enab
|
||||
});
|
||||
}
|
||||
|
||||
/** Update a seat's human-validation checkpoint without changing other stages. */
|
||||
export function setSeatHumanValidation(
|
||||
groups: StageGroups,
|
||||
agentId: string,
|
||||
enabled: boolean,
|
||||
): StageGroups {
|
||||
return groups.map((g) => {
|
||||
if (!g.seats.some((s) => s.agent_id === agentId)) return g;
|
||||
return {
|
||||
...g,
|
||||
seats: g.seats.map((s) =>
|
||||
s.agent_id === agentId ? { ...s, human_validation: enabled } : s,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个席位的「推理深度覆盖」(不可变,供编辑器席位下拉 onChange)。
|
||||
* `mode` 为 null = 清除覆盖(跟随链条级 seatMode)。按 agent_id 定位,跨 stage 适用。
|
||||
|
||||
@@ -292,9 +292,29 @@ function toStringArray(value: unknown): string[] {
|
||||
* 把解析出的对象归一成 IntentSummary。只要 `objective` 是非空字符串就认。
|
||||
* constraints / assumptions 容忍模型写成字符串 / null / 缺失,统一归到 string[]。
|
||||
*/
|
||||
function normalizeIntentSummary(parsed: unknown): IntentSummary | null {
|
||||
export function normalizeIntentReadySummary(parsed: unknown): IntentSummary | null {
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
const positionKeys = ["coreGoals", "riskWarnings", "keyPoints", "strategicSignificance"];
|
||||
if (positionKeys.some((key) => key in obj)) {
|
||||
if (!positionKeys.every((key) => Array.isArray(obj[key]))) return null;
|
||||
const coreGoals = toStringArray(obj.coreGoals);
|
||||
const riskWarnings = toStringArray(obj.riskWarnings);
|
||||
const keyPoints = toStringArray(obj.keyPoints);
|
||||
const strategicSignificance = toStringArray(obj.strategicSignificance);
|
||||
// Keep the long-standing Step-1 state machine compatible while retaining
|
||||
// every position-specific field verbatim for its dedicated UI and storage.
|
||||
return {
|
||||
objective:
|
||||
strategicSignificance[0] || coreGoals[0] || keyPoints[0] || "岗位任务意图已识别",
|
||||
constraints: riskWarnings,
|
||||
assumptions: keyPoints,
|
||||
coreGoals,
|
||||
riskWarnings,
|
||||
keyPoints,
|
||||
strategicSignificance,
|
||||
};
|
||||
}
|
||||
const objective =
|
||||
typeof obj.objective === "string" ? obj.objective.trim() : "";
|
||||
if (!objective) return null;
|
||||
@@ -310,14 +330,14 @@ function normalizeIntentSummary(parsed: unknown): IntentSummary | null {
|
||||
* 1) 原文直接 parse
|
||||
* 2) 转义字符串内的裸控制字符(换行 / 制表符)后再 parse
|
||||
* 3) 再去掉尾随逗号后 parse
|
||||
* 三次都失败才放弃。任一成功都过 `normalizeIntentSummary` 兜字段类型。
|
||||
* 三次都失败才放弃。任一成功都过 `normalizeIntentReadySummary` 兜字段类型。
|
||||
*/
|
||||
function parseIntentSummary(raw: string): IntentSummary | null {
|
||||
const sanitized = sanitizeJsonControlChars(raw);
|
||||
const candidates = [raw, sanitized, stripTrailingCommas(sanitized)];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const summary = normalizeIntentSummary(JSON.parse(candidate));
|
||||
const summary = normalizeIntentReadySummary(JSON.parse(candidate));
|
||||
if (summary) return summary;
|
||||
} catch {
|
||||
// 下一个候选
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
Sparkles,
|
||||
SplitSquareVertical,
|
||||
Trash2,
|
||||
UserCheck,
|
||||
UserPlus,
|
||||
Wand2,
|
||||
X,
|
||||
@@ -127,6 +128,7 @@ import {
|
||||
seatExistsInGroups,
|
||||
setSeatMode as setSeatModeOnGroups,
|
||||
setSeatModel,
|
||||
setSeatHumanValidation,
|
||||
setSeatSkillDirective,
|
||||
setStageGoal,
|
||||
splitSeatToNewStage,
|
||||
@@ -453,6 +455,10 @@ export default function BusinessChainEditorPage() {
|
||||
setStageGroups((prev) => setSeatSkillDirective(prev, agentId, enabled));
|
||||
}, []);
|
||||
|
||||
const setSeatHumanValidationCb = useCallback((agentId: string, enabled: boolean) => {
|
||||
setStageGroups((prev) => setSeatHumanValidation(prev, agentId, enabled));
|
||||
}, []);
|
||||
|
||||
// 席位「推理深度覆盖」:编辑器席位小图标下拉 onChange(null = 清除,跟随链条级 seatMode)。
|
||||
const setSeatModeCb = useCallback((agentId: string, mode: SeatMode | null) => {
|
||||
setStageGroups((prev) => setSeatModeOnGroups(prev, agentId, mode));
|
||||
@@ -670,6 +676,7 @@ export default function BusinessChainEditorPage() {
|
||||
model: s.model ?? null,
|
||||
// 席位「会商技能识别强化」开关:随席位一起落库(缺省 = false 关)。
|
||||
seat_skill_directive: s.seat_skill_directive ?? false,
|
||||
human_validation: s.human_validation ?? false,
|
||||
// 席位「推理深度覆盖」:随席位一起落库(null = 跟随链条级 seatMode)。
|
||||
seat_mode: s.seat_mode ?? null,
|
||||
// 岗位协同专用岗位归属:普通圆桌忽略,但保存时必须原样保留。
|
||||
@@ -1272,6 +1279,7 @@ export default function BusinessChainEditorPage() {
|
||||
onSplitSeat={splitSeat}
|
||||
onSeatModelChange={setSeatModelCb}
|
||||
onSeatSkillDirectiveChange={setSeatSkillDirectiveCb}
|
||||
onSeatHumanValidationChange={setSeatHumanValidationCb}
|
||||
onSeatModeChange={setSeatModeCb}
|
||||
positionConfigMode={positionConfigMode}
|
||||
positionOptions={positionOptions}
|
||||
@@ -1455,6 +1463,7 @@ function StagesColumn({
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onSeatHumanValidationChange,
|
||||
onSeatModeChange,
|
||||
positionConfigMode,
|
||||
positionOptions,
|
||||
@@ -1476,6 +1485,7 @@ function StagesColumn({
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatHumanValidationChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatModeChange: (agentId: string, mode: SeatMode | null) => void;
|
||||
/** 仅岗位协同入口启用;普通圆桌编辑器不显示岗位控件。 */
|
||||
positionConfigMode: boolean;
|
||||
@@ -1586,6 +1596,7 @@ function StagesColumn({
|
||||
onSplitSeat={onSplitSeat}
|
||||
onSeatModelChange={onSeatModelChange}
|
||||
onSeatSkillDirectiveChange={onSeatSkillDirectiveChange}
|
||||
onSeatHumanValidationChange={onSeatHumanValidationChange}
|
||||
onSeatModeChange={onSeatModeChange}
|
||||
positionConfigMode={positionConfigMode}
|
||||
positionOptions={positionOptions}
|
||||
@@ -1620,6 +1631,7 @@ function StageSection({
|
||||
onSplitSeat,
|
||||
onSeatModelChange,
|
||||
onSeatSkillDirectiveChange,
|
||||
onSeatHumanValidationChange,
|
||||
onSeatModeChange,
|
||||
positionConfigMode,
|
||||
positionOptions,
|
||||
@@ -1643,6 +1655,7 @@ function StageSection({
|
||||
onSplitSeat: (agentId: string) => void;
|
||||
onSeatModelChange: (agentId: string, model: string | null) => void;
|
||||
onSeatSkillDirectiveChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatHumanValidationChange: (agentId: string, enabled: boolean) => void;
|
||||
onSeatModeChange: (agentId: string, mode: SeatMode | null) => void;
|
||||
positionConfigMode: boolean;
|
||||
positionOptions: readonly PositionOption[];
|
||||
@@ -1809,6 +1822,7 @@ function StageSection({
|
||||
onSplit={() => onSplitSeat(seat.agent_id)}
|
||||
onModelChange={(model) => onSeatModelChange(seat.agent_id, model)}
|
||||
onSkillDirectiveChange={(enabled) => onSeatSkillDirectiveChange(seat.agent_id, enabled)}
|
||||
onHumanValidationChange={(enabled) => onSeatHumanValidationChange(seat.agent_id, enabled)}
|
||||
onSeatModeChange={(mode) => onSeatModeChange(seat.agent_id, mode)}
|
||||
showPositionConfig={positionConfigMode}
|
||||
positionOptions={positionOptions}
|
||||
@@ -1837,6 +1851,7 @@ function SeatStrip({
|
||||
onSplit,
|
||||
onModelChange,
|
||||
onSkillDirectiveChange,
|
||||
onHumanValidationChange,
|
||||
onSeatModeChange,
|
||||
showPositionConfig,
|
||||
positionOptions,
|
||||
@@ -1857,6 +1872,7 @@ function SeatStrip({
|
||||
onModelChange: (model: string | null) => void;
|
||||
/** 席位「会商技能识别强化」开关变更。 */
|
||||
onSkillDirectiveChange: (enabled: boolean) => void;
|
||||
onHumanValidationChange: (enabled: boolean) => void;
|
||||
/** 席位「推理深度覆盖」变更(null = 跟随链条级 seatMode)。 */
|
||||
onSeatModeChange: (mode: SeatMode | null) => void;
|
||||
/** 仅岗位协同入口展示的岗位配置。 */
|
||||
@@ -2058,6 +2074,38 @@ function SeatStrip({
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
title="人工校验:该席位完成后暂停会商,等待用户恢复或输入修改意见"
|
||||
className={cn(
|
||||
"p-1 transition",
|
||||
R_CTRL,
|
||||
seat.human_validation
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<UserCheck className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-xs font-semibold text-foreground">人工校验</p>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
开启后,该席位所在的执行单元完成时会自动暂停。用户可直接点击“恢复会商”继续,或在下方输入修改意见后提交;不会新增校验卡片。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={seat.human_validation ?? false}
|
||||
onCheckedChange={onHumanValidationChange}
|
||||
aria-label={`切换 ${seat.name || seat.agent_id} 的人工校验`}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDetail}
|
||||
|
||||
@@ -409,6 +409,10 @@ export default function RoundtablePlanningPage({
|
||||
// Step 2 每席位「技能识别强化」开关映射 {agentId: bool}:由所选业务链条里**每个席位**的同名
|
||||
// 配置决定(recommend 无链条 → 空 {})。透传给每个席位 run,与各席位智能体自身的开关取 OR。
|
||||
const [seatSkillDirectives, setSeatSkillDirectives] = useState<Record<string, boolean>>({});
|
||||
// Per-seat human-validation checkpoints from the selected business chain.
|
||||
// A checkpoint pauses Step 2 after the configured seat/stage completes and
|
||||
// reuses the existing resume button plus bottom intervention composer.
|
||||
const [humanValidations, setHumanValidations] = useState<Record<string, boolean>>({});
|
||||
// Step 2 每席位「推理深度覆盖」映射 {agentId: SeatMode|null}:由所选业务链条里**每个席位**单独配
|
||||
// 的推理深度决定(null/未配 → 跟随链条级 seatMode)。透传给席位 run(覆盖优先、否则跟随 seatMode)。
|
||||
// 「卡顿提速」用:把次要席位降到 flash 即可大幅缩短第二步耗时。
|
||||
@@ -573,6 +577,7 @@ export default function RoundtablePlanningPage({
|
||||
selectedModel,
|
||||
selectedMode: seatMode,
|
||||
seatSkillDirectives,
|
||||
humanValidations,
|
||||
seatModes,
|
||||
gatherFirst,
|
||||
orchestrationMode,
|
||||
@@ -1058,6 +1063,9 @@ export default function RoundtablePlanningPage({
|
||||
setSeatSkillDirectives(
|
||||
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.seat_skill_directive ?? false])),
|
||||
);
|
||||
setHumanValidations(
|
||||
Object.fromEntries(result.agents.map((a) => [a.agent_id, a.human_validation ?? false])),
|
||||
);
|
||||
// 每席位推理深度覆盖:取所选业务链条里**每席位**单独配的 seatMode → {agentId: SeatMode|null}
|
||||
// 映射(recommend / 未配 → null,跟随链条级 seatMode)。
|
||||
setSeatModes(
|
||||
|
||||
Reference in new issue
Block a user