From f8757d1a436912149c1b6e4fcbafc983fb0164cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 11 Aug 2026 13:00:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B2=97=E4=BD=8D=E4=BC=9A=E5=95=86?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=84=8F=E5=9B=BE=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E4=B8=8E=E5=9B=9B=E9=A1=B9=E6=84=8F=E5=9B=BE=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 岗位会商 Step 1 改用 position-roundtable-intent 智能体,输出 coreGoals/riskWarnings/keyPoints/strategicSignificance 四项数组;支持单轮多张协助卡、按轮次计澄清上限,并兼容旧会话 objective/constraints 映射。 Co-authored-by: Cursor --- .../src/position-roundtable/README.md | 25 ++ .../pages/PositionRoundtablePage.tsx | 276 +++++++++++------- .../styles/intent-understanding.css | 9 + .../src/roundtable-planning/api/intent.ts | 116 ++++++-- .../components/BusinessChainPicker.tsx | 8 +- .../components/Step1Panel.tsx | 56 ++-- .../hooks/useStep1Intent.ts | 9 + .../hooks/useStep2Orchestration.ts | 92 +++++- .../roundtable-planning/lib/business-chain.ts | 15 +- .../lib/chain-stages.test.ts | 10 + .../roundtable-planning/lib/chain-stages.ts | 17 ++ .../lib/roundtable-constants.ts | 26 +- .../pages/BusinessChainEditorPage.tsx | 48 +++ .../pages/RoundtablePlanningPage.tsx | 8 + offline-backend-20260512/backend/CLAUDE.md | 30 ++ offline-backend-20260512/backend/README.md | 20 +- .../app/gateway/routers/_roundtable_seed.py | 44 ++- .../position-roundtable-intent/SOUL.md | 145 +++++++++ .../position-roundtable-intent/config.yaml | 3 + .../backend/app/gateway/routers/intent.py | 267 +++++++++++++---- .../gateway/routers/position_roundtable.py | 102 +++++-- .../app/gateway/routers/roundtable_chains.py | 4 + .../test_position_roundtable_sessions.py | 175 +++++++++++ .../tests/test_roundtable_seat_modes.py | 10 + 24 files changed, 1258 insertions(+), 257 deletions(-) create mode 100644 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/position-roundtable-intent/SOUL.md create mode 100644 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/position-roundtable-intent/config.yaml diff --git a/frontend-web/src/position-roundtable/README.md b/frontend-web/src/position-roundtable/README.md index 80587346..2ef1efbd 100644 --- a/frontend-web/src/position-roundtable/README.md +++ b/frontend-web/src/position-roundtable/README.md @@ -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` 作为隐藏上下文发送给当前智能体。 diff --git a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx index 9143bbbc..71b7a466 100644 --- a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx +++ b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx @@ -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 | null | undefined): IntentSummary | null { - if (!snapshot || typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) { +function intentStringList(snapshot: Record, 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 | 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({ - 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 | 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 | 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 ? (
@@ -3715,15 +3770,15 @@ function IntentConfirmedState({ intent }: { intent: IntentSummary }) {
-

任务目标

-

- {desensitize(intent.objective)} -

+

核心目标

+
+ {intent.coreGoals.map((goal, index) =>

{desensitize(goal)}

)} +
-

关键约束

+

风险提示

- {intent.constraints.map((constraint, index) => ( + {intent.riskWarnings.map((constraint, index) => (
["intentReady"]; + intent: PositionIntentSummary | null; editable?: boolean; onEdit?: () => void; collapsed: boolean; @@ -4863,30 +4921,21 @@ function IntentUnderstandingCard({
{intent ? (
-
-

任务意图

-

- {desensitize(intent.objective)} -

-
-
-

关键约束

-
    - {intent.constraints.map((constraint, index) => ( -
  • {desensitize(constraint)}
  • - ))} -
-
- {intent.assumptions && intent.assumptions.length > 0 && ( -
-

关键假设

-
    - {intent.assumptions.map((assumption, index) => ( -
  • {desensitize(assumption)}
  • - ))} -
+ {([ + ["核心目标", intent.coreGoals], + ["风险提示", intent.riskWarnings], + ["关键要点", intent.keyPoints], + ["战略意义", intent.strategicSignificance], + ] as const).map(([label, values]) => ( +
+

{label}

+ {values.length > 0 ? ( +
    + {values.map((value, index) =>
  • {desensitize(value)}
  • )} +
+ ) :

暂无{label}

}
- )} + ))}
) : (
@@ -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 (
@@ -4926,10 +4976,6 @@ function IntentDeliveryOverviewSidebar({

意图理解结果

-
-

任务目的

-

{desensitize(objective)}

-

核心目标

{coreGoals.length > 0 ? ( @@ -4943,17 +4989,25 @@ function IntentDeliveryOverviewSidebar({ )}
-

关键约束

- {keyConstraints.length > 0 ? ( +

风险提示

+ {riskWarnings.length > 0 ? (
    - {keyConstraints.map((constraint, index) => ( -
  • {desensitize(constraint)}
  • + {riskWarnings.map((warning, index) => ( +
  • {desensitize(warning)}
  • ))}
) : ( -

暂无补充约束

+

暂无风险提示

)}
+
+

关键要点

+ {keyPoints.length > 0 ?
    {keyPoints.map((item, index) =>
  • {desensitize(item)}
  • )}
:

暂无关键要点

} +
+
+

战略意义

+ {strategicSignificance.length > 0 ?
    {strategicSignificance.map((item, index) =>
  • {desensitize(item)}
  • )}
:

暂无战略意义

} +
@@ -4999,34 +5053,43 @@ function IntentUnderstandingEditDialog({