--- date: 2026-07-02 type: feat title: 私董会单会话限制 + 方案B 气泡 + 简化开始卡片 origin: docs/brainstorms/2026-07-02-private-board-restrictions-and-scheme-b-bubbles-requirements.md status: ready --- ## Summary 收尾私董会(@board)模块的 UI 细节与单会话状态约束,四处协同改动:(1) 在 `ChatInput.vue` 的"私董会"按钮 click 处拦截"当前会话已存在进行中的私董会"场景,弹 a-modal 提示并提供快捷新建会话按钮;(2) 将 `BoardBannerCard.vue` 从带边框/紫条/进度条/专家 chip 的重样式简化为单行标题+副标题;(3) 给 `MessageShell.vue` 中所有 `role === 'assistant'` 的消息内容添加方案 B 风格的浅灰圆角矩形气泡(F1-A 独立 token / F4-A 排除所有 card-bearing 类型 / D4-方案1 `:empty` 隐藏);(4) 将 `UserBubble.vue` 普通文本消息改为 demo 中的深色右对齐气泡(`--color-primary` + `--text-inverse`),@board/@team 命令卡片保持现有浅色背景。改动范围仅限前端 Vue 组件与少量 store/type/token 文件,不动后端、不动方案 B 调色板、不动 StickyModeHeader 顶部条。 ## Problem Frame 私董会功能上线后存在三处 UI 粗糙点(详见 origin: docs/brainstorms/2026-07-02-private-board-restrictions-and-scheme-b-bubbles-requirements.md): 1. **单会话多私董会无约束**:`ChatInput.vue:75` 的"私董会"按钮 `@click` 直接 `showBoardModal = true`,对当前会话是否已存在私董会无任何判断。连续两次 `SendMessage("@board:...")` 会创建第二个私董会,叠加在第一个未结束的私董会之上,`boardState.experts` 被覆盖、轮次错乱、`StickyModeHeader` 头像数与实际不符。 2. **BoardBannerCard 样式过重**:`BoardBannerCard.vue` 使用了 card+border+shadow+4px 紫条+进度条+专家 chip pill 等装饰,与方案 B 整体"克制、不重样式"取向冲突。方案 B 截图中的"开始"区域是单行文本,不带装饰。 3. **方案 B 气泡未落地**:方案 B 截图中专家发言区域**有**浅灰圆角矩形气泡包裹内容,与 ChatGPT / Notion AI 风格一致。当前 `MessageShell.vue:178-184` 的 `.message-shell__content` 没有背景/边框/圆角,气泡效果完全缺失。 ## Requirements 完整继承 origin 文档的 13 条 R-IDs(R1-R13)并新增 R14-R19(F4-A / D4-方案1 / U4 决策固化),共 19 条,分组如下: **单会话私董会限制(R1-R4)**: - R1:`ChatInput.vue` "私董会"按钮 `@click` 在打开 `BoardMeetingModal` 前检查 `chatStore.boardState`,`status === 'discussing' | 'concluding'` 时弹 a-modal,**不**打开 modal - R2:a-modal 标题"当前会话已存在私董会",副文"请新建会话来创建新的私董会",按钮"我知道了"+ "新建会话"(主操作);"新建会话"流程:关 modal → `chatStore.createConversation()` → `chatStore.selectConversation(newId, true)` → 不自动打开 modal - R3:`boardState === null` 或 `status === 'completed' | 'dissolved'` 时保持当前行为 - R4:以前端 `chatStore.boardState` 为权威源,不依赖后端 / `is_board` 标记 **BoardBannerCard 简化(R5-R8)**: - R5:重构为单行标题+副标题,保留 `topic / maxRounds / currentRound` props 向后兼容,删除 `experts` prop - R6:模板输出两行:`私董会 — {topic}` + `轮次:第 {currentRound} / {maxRounds} 轮` - R7:删除 `.board-banner-card` 的重样式(background/border/border-radius/box-shadow)及 `__bar / __chip` 等重样式类,保留 `.board-banner-card` 容器(仅 margin/padding)+ `__title / __meta` 最小样式 - R8:`useMessageRenderer.ts` 中 `board_started` 渲染路径不变 **AssistantText 浅灰气泡(R9-R15)**: - R9:`MessageShell.vue` 的 `.message-shell__content` 加 `background: var(--bg-message-bubble)` + `border-radius: var(--radius-md)` + `padding: var(--space-3) var(--space-4)` + `border: 1px solid var(--border-color)` + `color: var(--text-primary)`,仅 `role === 'assistant'` 时生效。**F1-A 决策**:引入独立 token `--bg-message-bubble`(light `#ffffff` / dark `#1f1f1f`),与 `--bg-secondary`(inline code/table 背景)解耦,避免气泡背景与代码/表格背景视觉冲突 - R10:不使用 `!important`,不覆盖代码块/表格/路由 tag 样式 - R11:不影响 `BoardRoundCard` 内 `AssistantText` 渲染 - R12:私董会专家发言、普通 chat 通过 `MessageShell + AssistantText` 统一获得气泡;@team 阶段使用 `TeamPlanCard`、Debate 使用 `DebateArgumentCard`/`DebateSummaryCard`/`DebateBannerCard` 等自带 card chrome 的组件(通过 `MessageShell` 渲染但不走 `AssistantText`),由 R14 排除气泡 - R13:气泡宽度继承现有 `width: 100%; max-width: 100%`,不强制固定最大宽度 - R14:**F4-A 决策(Round 4 扩展)**:气泡选择器排除所有自带 card chrome 的 assistant 消息类型(共 9 种):`board_conclusion`(`BoardConclusionCard` → `.board-conclusion-card`,full chrome:bg + border + radius + shadow)、`team_plan`(`TeamPlanCard` → `.team-plan-card`,full chrome)、`debate_banner`(`DebateBannerCard` → `.debate-banner`,partial chrome:bg + left-border + radius)、`debate_argument`(`DebateArgumentCard` → `.debate-argument`,partial)、`debate_summary`(`DebateSummaryCard` → `.debate-summary`,partial)、`debate_resolved`(`DebateConclusionCard` → `.debate-conclusion`,partial + 4 变体 bg)、`collaboration_graph`(`CollaborationGraphCard` → `.collab-graph`,partial)、`review_result`(`ReviewResultCard` → `.review-card`,partial + 3 变体 bg)、`risk_flagged`(`RiskFlagCard` → `.risk-card`,partial)。**chrome 区分(Round 4 修正)**:仅 `BoardConclusionCard` + `TeamPlanCard` 有完整 chrome(bg + border + radius + shadow),其余 7 种只有 bg + left-border + radius(无 shadow、无 full border)。**已验证根 class 名(Round 4 R4-F1)**:注意 7 种 partial chrome 卡片 root class 名与组件名不匹配,不带 `-card` 后缀(`.debate-banner` / `.debate-argument` / `.debate-summary` / `.debate-conclusion` / `.collab-graph` / `.review-card` / `.risk-card`)。实现方式:在 `MessageShell.vue` 通过 `messageType` prop 或 `:has()` 选择器排除(具体机制依 G3 决策)。**第三种架构替代方案(Round 4 R4-A3)**:在 `MessageRenderSpec` 接口(`useMessageRenderer.ts:44-48`)新增 `bubble: boolean` 字段,在 renderer 层集中决策是否包裹气泡,避免 messageType prop 污染或 `:has()` 选择器脆弱性——见 Open Questions Round 4。**未来防护(Round 4 R4-A4)**:新增 card-bearing 类型时需手动加入 F4-A 排除列表,或采用上述 `bubble` 字段方案由 renderer 集中管理——见 Open Questions Round 4 - R15:**D4-方案1 决策**:空 slot 内容(pre-stream thinking / tool-call-only)时气泡不渲染背景——通过 `:empty` 选择器隐藏 `background / border / padding`,仅显示 thinking dots。有内容流入后自动恢复气泡样式 **UserBubble 普通文本深色气泡(R16-R19)**: - R16:`UserBubble.vue` 的普通文本消息(``)样式改为 demo 中的深色右对齐气泡:`background: var(--color-primary)` + `color: var(--text-inverse)` + `padding: var(--space-3) var(--space-4)` + `max-width: 70%` - R17:@board/@team 命令卡片(`.user-bubble__command`)和文件附件保持现有 `--bg-tertiary` 浅色背景,不应用深色气泡样式 - R18:dark mode 自动反转——`--color-primary` 在 dark mode 下为 `#fbfbfa`(浅),`--text-inverse` 为 `#1a1a1a`(深),user 气泡自动变为浅色背景 + 深色文字 - R19:通过 `isPlainText` computed(`!fileAttachment && !commandBubble`)+ `.user-bubble--text` modifier class 区分普通文本与命令卡片,不修改 `.user-bubble` 默认样式 ## Key Technical Decisions - **拦截点选择 ChatInput 按钮 click,不放在 BoardMeetingModal 内部** — 最自然的 UX 拦截点,避免用户填表后才发现不能发起。判断依据 `chatStore.boardState` 实时状态,不依赖后端 / `is_board` 标记,避免 reload 后误判(见 origin R4)。 - **私董会状态判定取 `discussing | concluding`** — `BoardState.status` 类型为 `"discussing" | "concluding" | "completed" | "dissolved"`(chatStream.ts:65 确认)。`completed | dissolved` 的旧私董会不阻塞新私董会发起,符合 origin R3 要求。 - **"新建会话"按钮流程:`createConversation()` → `selectConversation(newId, true)`** — `createConversation()` 是同步函数(chatStore.ts:333 确认),自动设置 `currentConversationId` 但不加载服务端会话;后续 `selectConversation(newId, true)` 强制 reload 确保状态干净。不自动打开 `BoardMeetingModal`,由用户在新会话中再次点击"私董会"按钮继续(见 origin R2)。 - **BoardBannerCard 简化为单行标题+副标题,不删除组件本身** — 保留 `BoardBannerCard.vue` 文件,仅重构 template + style。`useMessageRenderer.ts` 的 `board_started` 渲染路径不变(origin R8),保持 streaming 期间与 reload 后视觉一致。 - **气泡样式挂在 `MessageShell.vue` 的 `.message-shell__content` 上,不挂在 `AssistantText.vue`** — `MessageShell` 是所有 assistant 消息的统一外壳,挂在这里可一次性覆盖 board_speech / board_summary / 普通聊天 / @team / Debate 等场景(origin R12),且不影响 `UserBubble.vue` 的 user 消息样式。**card-bearing 类型例外(F4-A Round 4 扩展,共 9 种)**:`board_conclusion`/`team_plan`/`debate_banner`/`debate_argument`/`debate_summary`/`debate_resolved`/`collaboration_graph`/`review_result`/`risk_flagged` 渲染的 card 组件自带 card chrome(详见 R14),不被气泡包裹。 - **气泡使用 CSS 变量,禁止硬编码** — 新增 `--bg-message-bubble: #ffffff` (light) / `#1f1f1f` (dark) 到 `tokens.css`,与 `--bg-secondary`(inline code/table 背景 `#fbfbfa`)解耦(F1-A 决策)。dark mode 自动切换。禁止硬编码 `#f3f4f6` / `#fbfbfa` / `#ffffff` 等值(project rules 硬约束)。 - **`role === 'assistant'` 时启用气泡,`role === 'user'` 不启用** — 通过 `.message-shell--assistant .message-shell__content` 选择器限定,避免污染 user 消息的 `UserBubble` 样式。 - **F4-A 排除所有 card-bearing 类型(Round 4 扩展,共 9 种)** — 排除 9 种 card-bearing assistant 消息类型:`board_conclusion`/`team_plan`/`debate_banner`/`debate_argument`/`debate_summary`/`debate_resolved`/`collaboration_graph`/`review_result`/`risk_flagged`。**chrome 区分(Round 4 修正)**:仅 `BoardConclusionCard`(`.board-conclusion-card`)+ `TeamPlanCard`(`.team-plan-card`)有完整 chrome(bg + border + radius + shadow);其余 7 种(`DebateBannerCard`→`.debate-banner`、`DebateArgumentCard`→`.debate-argument`、`DebateSummaryCard`→`.debate-summary`、`DebateConclusionCard`→`.debate-conclusion`、`CollaborationGraphCard`→`.collab-graph`、`ReviewResultCard`→`.review-card`、`RiskFlagCard`→`.risk-card`)只有 bg + left-border + radius(无 shadow、无 full border)。气泡选择器需排除这些类型,避免"card 嵌套在气泡里"的视觉冲突。实现方式:在 `MessageShell.vue` 增加 `messageType` prop(或 `:has()` 选择器),card-bearing 类型不加气泡样式。具体机制依 G3 决策。**第三种替代方案**:在 `MessageRenderSpec` 新增 `bubble: boolean` 字段由 renderer 集中决策(见 Open Questions Round 4 R4-A3)。**未来防护**:新增 card 类型需手动加入排除列表或采用 `bubble` 字段方案(见 Open Questions Round 4 R4-A4)。 - **D4-方案1 `:empty` 隐藏空气泡** — 空 slot 内容(pre-stream thinking / tool-call-only)时通过 `:empty` 选择器隐藏 `background / border / padding`,仅显示 thinking dots 动画。有内容流入后自动恢复气泡样式,无需 JS 判断。 - **U4 仅普通文本消息深色气泡,命令卡片保持浅色** — `UserBubble.vue` 同时渲染三种内容(普通文本 / @board|@team 命令卡片 / 文件附件),命令卡片含结构化信息(header + experts list),深色背景会降低可读性。通过 `isPlainText` computed + `.user-bubble--text` modifier 精确限定深色样式仅作用于普通文本,命令卡片和文件附件继承 `.user-bubble` 默认 `--bg-tertiary` 浅色背景。 - **U4 用 `--color-primary` + `--text-inverse` 自动适配 dark mode** — `--color-primary` 在 light mode 为 `#1a1a1a`(深),dark mode 为 `#fbfbfa`(浅);`--text-inverse` 反之。user 气泡在 dark mode 下自动反转为浅色背景 + 深色文字,无需额外 dark mode 覆盖。 ## Implementation Units ### U1. ChatInput 拦截 + a-modal 弹窗 + 快捷新建会话 **Goal:** 在 `ChatInput.vue` 的"私董会"按钮 click 处增加拦截逻辑:当前会话已存在进行中的私董会时弹 a-modal 提示,提供"我知道了"和"新建会话"两个按钮;"新建会话"按钮调用 `chatStore.createConversation()` + `selectConversation(newId, true)` 跳转到新空会话。 **Requirements:** R1, R2, R3, R4 **Dependencies:** 无 **Files:** - src/agentkit/server/frontend/src/components/chat/ChatInput.vue(修改) - src/agentkit/server/frontend/src/components/chat/\_\_tests\_\_/ChatInput.test.ts(新建) **Approach:** - 在 `