--- 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 排除 conclusion / 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、@team 阶段、Debate 等都通过 `MessageShell + AssistantText` 统一获得气泡 - R13:气泡宽度继承现有 `width: 100%; max-width: 100%`,不强制固定最大宽度 - R14:**F4-A 决策**:气泡选择器排除 `board_conclusion` 类型——`BoardConclusionCard` 自带 card chrome(background/border/border-radius/shadow),保留其自身样式,不再被气泡包裹。实现方式:在 `MessageShell.vue` 通过 `messageType` prop 或 `:not(:has(.board-conclusion-card))` 选择器排除 - 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 消息样式。**board_conclusion 例外**(F4-A):`BoardConclusionCard` 自带 card chrome,不被气泡包裹。 - **气泡使用 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 排除 conclusion 类型** — `board_conclusion` 消息渲染的 `BoardConclusionCard` 自带 `background: var(--bg-primary)` + `border` + `border-radius: var(--radius-card)` + `box-shadow`,气泡选择器需排除该类型,避免"白卡片嵌套在白气泡里"的视觉冲突。实现方式:在 `MessageShell.vue` 增加 `messageType` prop(或 `:not(:has(.board-conclusion-card))` 选择器),conclusion 类型不加气泡样式。 - **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:** - 在 `