技术栈总览 · Tech Stack Overview
| Layer | Technology | Details |
|---|---|---|
| Framework | React 18 | StrictMode, createRoot, Functional Components |
| UI Library | Fluent UI React v9 | @fluentui/react-components, makeStyles |
| Language | TypeScript | Strict mode, interface-first design |
| Bundler | Webpack | TypeScript webpack config |
| State Management | React Hooks | useState, useCallback, useRef — 无 Redux / MobX |
| Hosting Model | Iframe in Angular Shell | PostMessage bridge for communication |
| Streaming | SSE (Server-Sent Events) | 非 WebSocket,单向服务端推送 |
| Backend | fabric-chat-server | Node.js, REST API |
| Authentication | GitHub Device Flow + MSAL | 双重认证: GitHub for Copilot + Fabric Tokens |
📌 关键特征:纯 React Hooks 架构,没有引入任何第三方状态管理库。所有状态通过 useSession() 等自定义 hooks 管理,保持了极高的代码简洁性。
整体架构 · Architecture Overview
fabric-chat 作为一个 独立的 React 应用,运行在 Angular Fabric Shell(src/Modern/Apps/Web)托管的 iframe 中。iframe 与两个服务通信:
- Angular Host (via PostMessage) — 负责 token 获取(MSAL)、主题同步、artifact 搜索、导航、OneDrive 文件选择器、截图
- fabric-chat-server (via REST/SSE) — 负责 Copilot 会话管理、prompt 发送、模型选择、用户输入处理
ASCII 架构图
💡 设计哲学:iframe 隔离确保了 React 应用与 Angular Shell 之间的完全解耦。Chat UX 可以独立开发、测试和部署,不依赖 Shell 的构建管线。
PostMessage Bridge 通信协议
PostMessage Bridge 是一个 双向异步通信协议,基于 window.parent.postMessage() 实现 iframe 与 Angular Host 之间的数据交换。
Iframe → Host(请求方向)
| Message Type | Purpose |
|---|---|
ready | React app 已加载,准备接收 context |
close | 用户点击关闭按钮 |
request-tokens | 需要刷新 Fabric + SQL tokens (MSAL) |
request-search | 通过 host 的 SearchService 搜索 artifacts |
request-recent | 获取最近访问的 workspaces/items |
request-screenshot | 截取 portal 当前页面截图 |
request-open-onedrive-picker | 打开 ODSP 文件选择器浮层 |
navigate | 让 host 导航到指定内部路径 |
Host → Iframe(响应方向)
| Message Type | Purpose |
|---|---|
tokens | { fabricToken, sqlToken } 来自 MSAL |
theme | { isDark } 主题同步 |
context | PromptContext(workspaces, items, currentUrl) |
search-results | ArtifactSearchResult[] |
recent | RecentArtifact[] |
screenshot | { base64, width, height } |
onedrive-picker-result | { files: OneDriveFile[] } |
核心实现:requestFromHost<T>()
Bridge 使用一个通用的 requestFromHost<T>() 辅助函数来处理所有请求-响应模式:
- 超时管理 — 默认 10 秒超时,OneDrive 文件选择器为 5 分钟
- 清理机制 — 自动移除 event listener,防止内存泄漏
- 独立运行降级 — 当不在 iframe 中运行时,返回合理的默认值(空 context、mock tokens)
⚠️ 安全约束:PostMessage 接收端必须验证 event.origin,确保消息来自可信的 *.powerbi.com 域名。
认证流程 · Authentication Flow
采用 双层认证架构,分别解决 GitHub Copilot 访问和 Fabric 资源访问两个独立问题。
📄 GitHubSignIn.tsx · copilotApi.ts (auth APIs)
Layer 1:GitHub Copilot Auth(Device Flow)
Step 1 调用 copilotApi.ts → startGitHubDeviceFlow(),获取 device code + verification URL
Step 2 前端显示 code,引导用户在 GitHub.com 授权
Step 3 轮询 pollGitHubDeviceFlow() 等待用户完成授权
关键 不使用 cookies / localStorage 存 token — 由 server 端 session 管理
Layer 2:Fabric/SQL Tokens(MSAL via Host)
获取方式 通过 PostMessage Bridge 从 Angular host 获取(host 使用 MSAL)
传递方式 每次 API 调用携带 X-Fabric-Token 和 X-SQL-Token headers
刷新策略 Token 刷新对 iframe 透明 — 每次 API 调用都请求 fresh tokens
Auth 状态机
SSE 事件流协议 · SSE Event Stream Protocol
所有 Copilot 响应通过 Server-Sent Events 流式推送到前端。每种事件类型触发特定的状态转换和 UI 更新。
📄 SSE 解析:copilotApi.ts · 状态处理:useSession.ts → applyStreamEvent()
事件类型与状态驱动
| Event | Payload | State Update |
|---|---|---|
assistant.turn_start | — | isLoading=true, clear streamingBlocks |
assistant.reasoning_delta | { content } | 追加到最后一个 reasoning block |
assistant.reasoning | { content } | 完成 reasoning block |
assistant.message_delta | { content } | 追加到最后一个 message block |
assistant.message | { content } | 完成 message block |
assistant.turn_end | — | 提交 blocks 到 messages, isLoading=false |
tool.start | { toolCallId, toolName, arguments } | 添加 tool block 到 streaming |
tool.progress | { toolCallId, content } | 追加内容到 tool result |
tool.complete | { toolCallId, result?, error? } | 完成 tool 状态 |
user_input.request | { question, choices?, allowFreeform } | 显示 QuestionCard |
permission.request | { toolCallId, kind, description } | 显示 PermissionCard |
session.idle | — | isLoading=false |
session.error | { message } | 显示错误, isLoading=false |
session.expired | — | isExpired=true |
TurnBlock 架构
每个 assistant turn 包含一个有序的 TurnBlock 数组,形成一条时间线:
每个 TurnBlock 具有 type 字段(reasoning | tool | message | question)和对应的 payload。流式接收 *_delta 事件时,内容追加到当前 block;收到非 delta 事件时,block 被"密封"并推入已完成列表。
💡 设计意图:TurnBlock 模型将流式增量(delta)与完成态清晰分离。UI 组件只需根据 block type 选择渲染器,无需关心流式/完成状态切换逻辑。
Session 状态管理 · Session State Management
useSession() hook 是整个应用的 中央状态管理器,管理所有会话相关的状态和副作用。
SessionState 接口
核心机制
1. LocalStorage 消息持久化
消息在 turn_end 时写入 LocalStorage(key: chat-session-{id}),支持页面刷新后恢复历史。
2. 多会话支持与切换
内存缓存(Map<string, Message[]>)+ 服务端验证。切换会话时先查内存缓存,miss 则从 LocalStorage 加载,再 miss 则从服务端拉取。
3. 去重保护(StrictMode 防护)
React 18 StrictMode 下组件会 double-fire。useSession 使用 ref-based guard 防止并发创建重复 session。
4. 消息排队
当用户在 turn 进行中发送新消息时,消息进入 queuedMessages 队列。UI 立即显示排队消息(灰色样式),turn_end 后自动发送队列中的下一条。
5. 消息编辑
用户可点击已发送的消息进入编辑模式,修改后重新发送。支持同时变更 model 和 mode。
Chat Pane 渲染架构详解 · Chat Pane Rendering Deep Dive
以下是 Chat Pane 中每个渲染组件的 详细实现分析,包含渲染逻辑、状态管理、交互流程和源码链接。
📂 源码仓库:PowerBIClients → trident/apps/fabric-chat/src (branch: feature/unified-creator-copilot-poc)
7.0 渲染管线总览 · Rendering Pipeline
Chat Pane 的渲染管线由 SSE 事件流 驱动,经过三个阶段最终呈现到 DOM:
7.1 ChatMessageContainer — 消息容器与分组引擎
消息分组算法(Message Grouping)
每个 user prompt 触发一个 turn,turn_end 时所有 streamingBlocks(reasoning/tool/message/question)被收集到 一个 assistant ChatMessage 的 blocks[] 数组中。因此 一个 user prompt → 一个 assistant message → 一个 Copilot 标签头,tool call 不会切割 assistant message,而是作为 blocks 内的 type: 'tool' 项。
分组算法的作用是处理 多个连续 assistant messages 没有 user 消息间隔 的边界情况(如 queued messages 场景:用户在 turn 进行中发送新消息,turn_end 后 queued 消息自动发送,产生连续两个 assistant responses),此时多个 assistant messages 合并为一个 group,共享同一个 Copilot 标签头。
type MessageGroup =
| { kind: 'single'; msg: ChatMessage; index: number } // user/system 消息
| { kind: 'assistant'; msgs: { msg: ChatMessage; index: number }[] }; // 连续 assistant 消息组
// 分组逻辑:遍历 messages,连续的 assistant 消息合并为一个 group
const messageGroups = useMemo(() => {
const groups: MessageGroup[] = [];
for (let i = 0; i < session.messages.length; i++) {
const msg = session.messages[i];
if (msg.role === 'assistant') {
const last = groups[groups.length - 1];
if (last?.kind === 'assistant') {
last.msgs.push({ msg, index: i }); // 合并到现有组
} else {
groups.push({ kind: 'assistant', msgs: [{ msg, index: i }] }); // 新建组
}
} else {
groups.push({ kind: 'single', msg, index: i });
}
}
return groups;
}, [session.messages]);
Block 分派渲染(Block Dispatch)
每个 assistant 消息包含一个 blocks: TurnBlock[] 数组。renderBlock() 根据 block type 选择对应的渲染组件:
function renderBlock(block: TurnBlock, onAnswerQuestion?) {
switch (block.type) {
case 'reasoning': return <ReasoningCard content={block.content} streaming={block.streaming} />;
case 'tool': return <ToolResultCard tool={block.tool} />;
case 'message': return <MessageBubble role="assistant" content={block.content} />;
case 'question': return <QuestionCard pendingQuestion={block} answer={block.answer} ... />;
}
}
渲染层次(由上到下)
| Layer | 条件 | 渲染内容 |
|---|---|---|
| 1. 已提交消息 | 始终 | 按 messageGroups 渲染,user 消息 → MessageBubble,assistant group → Copilot 标签 + blocks |
| 2. 流式 blocks | isLoading && streamingBlocks.length > 0 | 当前正在接收的 turn blocks(reasoning/tool/message) |
| 3. Thinking 指示器 | isLoading && !isStreaming | ThinkingIndicator 动画(SSE 首个事件到达前) |
| 4. 排队消息 | queuedMessages.length > 0 | 灰色半透明气泡 + "Queued" badge |
| 5. 权限审批 | pendingPermission != null | PermissionCard |
| 6. 错误卡片 | session.error | 分类后的错误信息 + retry/dismiss 按钮 |
编辑模式集成
当用户点击某条消息进入编辑模式时,该消息位置被替换为一个内联的 ChatInput 组件(带编辑模式样式)。编辑位置之后的所有消息变为 opacity: 0.4 + pointerEvents: none,视觉上表示"将被覆盖"。
7.2 MessageBubble — 消息气泡与 Markdown 渲染
三种角色渲染模式
| Role | 样式 | 内容渲染 |
|---|---|---|
user | 右对齐,colorBrandBackground2 背景,圆角 12px 12px 2px 12px | 纯文本 + 附件 chip 列表 |
assistant | 左对齐,透明背景,全宽 | simpleMarkdownToHtml() 渲染后通过 dangerouslySetInnerHTML 注入 |
system | 居中,灰色斜体 | 纯文本(如 "Fabric Skills v1.2") |
内部导航拦截
Assistant 消息中的 *.powerbi.com / *.fabric.microsoft.com 链接被转换为 data-internal-nav 属性。点击时通过 navigateParent(path) 通知 Angular host 进行 SPA 导航,避免 iframe 跳转。
const handleMarkdownClick = useCallback((e: React.MouseEvent) => {
const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[data-internal-nav]');
if (!anchor) return;
e.preventDefault();
const path = anchor.getAttribute('data-internal-nav');
if (path) navigateParent(path); // → PostMessage → Angular host 导航
}, []);
附件 Chip 渲染
用户消息气泡底部渲染附件缩略图 chips:图片文件显示 12×12 缩略图,非图片文件显示文档图标。Chips 使用 overflow: hidden + text-overflow: ellipsis,最大宽度 110px。
可编辑交互
用户消息支持 click-to-edit:hover 时显示 colorBrandBackground2Hover 背景变化 + Tooltip "Click to edit"。点击触发 onEdit(messageId),在 ChatMessageContainer 中将该消息替换为内联 ChatInput。
7.3 ReasoningCard — 推理过程可折叠卡片
📄 ReasoningCard.tsx · 📄 expandableCardStyles.tsx
自动摘要算法
summarizeReasoning(content) 提取推理内容的第一行第一句话作为折叠态标题(最长 80 字符 + "...")。这正是 promptContextPrefix.ts 中注入 "reasoning first line = summary" 指令的原因。
function summarizeReasoning(content: string): string {
const firstLine = content.split('\n').find(l => l.trim())?.trim() ?? '';
const firstSentence = firstLine.split(/(?<=[.!?])\s/)[0].trim();
const summary = firstSentence || firstLine;
if (!summary) return 'Thought';
const isTruncated = summary.length > 80 || content.trim().length > summary.length;
return isTruncated ? summary.substring(0, 77) + '...' : summary;
}
段落分组渲染
groupIntoParagraphs() 将推理文本按空行分段,识别列表项(-/•/*/1.),并将列表项附加到前一段落,保持视觉上的逻辑关联。每个段落渲染为一个 bullet item,段落之间有垂直连接线(borderLeft: 1px solid)。
展开/折叠交互
| 状态 | Icon | 行为 |
|---|---|---|
流式接收中 (streaming=true) | Spinner size="tiny" | 强制展开,不可手动折叠 |
| 已完成 + 折叠 | Checkmark16Regular (绿色) | 显示摘要文本,hover 时出现 ChevronRight |
| 已完成 + 展开 | Checkmark16Regular (绿色) | 显示完整段落列表,hover 时出现 ChevronDown |
L 形连接线
展开内容区使用 CSS pseudo-element ::before 绘制 L 形连接线(borderLeft + borderBottom + borderBottomLeftRadius: 6px),视觉上将标题行和展开内容连接起来,这是 expandableCardStyles.tsx 中的共享样式。
7.4 ToolResultCard — 工具执行结果卡片
智能动作摘要引擎 summarizeToolAction()
该函数解析 tool name 和 arguments,生成 人类可读的动作描述:
| Tool Name Pattern | Running Verb | Done Verb | Badge Content | Action Icon |
|---|---|---|---|---|
*read*, view, cat | Reading | Read | 文件名 (shortPath) | BookOpen16 |
*edit*, *patch*, *replace* | Editing | Edited | 文件名 + 行号范围 | Edit16 |
*search*, *grep*, *find* | Searching for | Searched for | 搜索 pattern(带引号) | Search16 |
*list*, ls, *glob* | Listing | Listed | glob pattern | BookOpen16 |
*shell*, *exec*, *bash*, *powershell* | Running | Ran | 命令首行 (50 chars) | Wrench16 |
*fetch*, *http* | Fetching | Fetched | URL (50 chars) | BookOpen16 |
*create*, *write* | Creating | Created | 文件名 | Edit16 |
| 其他 | Capitalized segment | Same | 文件名/name arg | Wrench16 |
本地路径过滤
containsLocalPath() 检测工具结果是否包含本地文件路径(如 C:\Users\... 或 /home/user/...),这些路径对用户无意义,不在折叠态显示。
三态显示
| Status | Icon | Verb | 展开行为 |
|---|---|---|---|
running | Spinner size="tiny" | 动词 -ing 形式 | 自动展开(useEffect 监听 status 变化) |
complete | Action type icon | 动词过去式 | 可手动展开查看 Input/Output |
error | Dismiss16Regular (红色) | "Failed to {verb}" | 可展开查看 Error 详情(红色背景) |
展开区域内容
- Input —
JSON.stringify(tool.arguments, null, 2)格式化显示,带Wrench16Regularicon - Output — 工具执行结果(仅 complete 且无本地路径时显示),等宽字体
- Error — 错误信息,红色背景
colorStatusDangerBackground1
7.5 QuestionCard — 交互式问答卡片
双模式渲染
| 模式 | 样式 | 内容 |
|---|---|---|
| 待回答 | colorBrandStroke1 边框(品牌蓝色高亮) | 问题文本 + 选项按钮列表 + freeform Input(如 allowFreeform=true) |
| 已回答 | colorNeutralStroke1 边框(灰色) | 问题文本 + CheckmarkCircle20Filled (品牌色) + 答案文本 |
选项按钮使用 appearance="outline",左对齐 justifyContent: 'flex-start'。Freeform 输入支持 Enter 提交。
7.6 PermissionCard — 权限审批卡片
信息层次
- 标题(14px semibold)—
pendingPermission.description或"{toolName} requires approval" - 副标题(12px regular)—
"Tool: {toolName} ({kind})" - 详情区(等宽字体,灰色背景)— tool arguments 的 JSON 格式化展示或显式 details 文本
操作按钮
使用 Fluent UI SplitButton:主按钮 "Approve" + 下拉菜单("Approve once" / "Approve for session")。"Skip" 按钮使用 appearance="outline"。
Arguments 注入
ChatMessageContainer 从 streamingBlocks 中查找匹配 toolCallId 的 tool block,将其 arguments 传递给 PermissionCard,让用户在审批前看到工具将要执行的参数。
7.7 ThinkingIndicator — 思考指示器
7 个元素组成的动画序列:4 个圆点 (6px) + 3 个条形 (13–26px),采用 pink/coral 渐变色(#C85BBC → #F38373),每个元素有 0.15s 的动画延迟,形成波浪效果。动画使用 opacity: 0.3 → 1.0 的呼吸效果,周期 0.5s。
显示条件:session.isLoading && streamingBlocks.length === 0 — 即 SSE 连接已建立但第一个 block 事件尚未到达时。一旦收到 assistant.reasoning_delta 或 tool.start,ThinkingIndicator 消失,被实际的 block 卡片替代。
7.8 QueuedMessageBlock — 排队消息
当用户在 turn 进行中发送新消息时,消息不会打断当前流,而是进入 queuedMessages 队列。视觉上显示为 半透明气泡(opacity: 0.7,colorNeutralBackground4 背景)+ 斜体 "Queued" badge。当前 turn 的 turn_end 事件到达后,排队消息被移入主消息列表并自动发送。
7.9 Welcome — 欢迎页
四类建议分类(Fluent UI Tree 组件)
| Category | Icon | 示例建议 |
|---|---|---|
| Explore | 放大镜 + sparkle(绿色渐变 SVG) | "What can I do in Fabric?", "Show me examples of what I can build" |
| Start | 文档 + sparkle(橙/紫渐变 SVG) | "I'm new to Fabric. What should I do first?" |
| Build | 灯泡 + sparkle(黄/橙渐变 SVG) | "Help me analyze my data", "Create a real-time dashboard" |
| Organize | 文档 + sparkle(蓝/teal 渐变 SVG) | "Create a new workspace", "Help me organize my recent items" |
每个分类是一个可折叠的 TreeItem(默认 Explore 展开),子项点击后将建议文本注入到 ChatInput 的 textarea 中。所有 SVG 图标都内联在组件中,使用多色渐变(linearGradient + radialGradient)。
7.10 ChatInput — 输入组件详解
布局结构
输入交互
| 交互 | 行为 |
|---|---|
| Enter | 发送消息(非 loading 时)/ 发送 steered 消息(loading 时,modeOverride='immediate') |
| Shift+Enter | 换行 |
| Ctrl+/ | 打开 AttachmentPickMenu |
| Escape (编辑模式) | 取消编辑 |
| Paste (含图片) | 自动检测 clipboard 中的文件,调用 addFiles() |
| Drag & Drop | 拖入文件时显示蓝色虚线 overlay "Drop files here to attach" |
三种操作模式
const MODES = [
{ value: 'interactive', label: 'Interactive', description: 'Respond to each message' },
{ value: 'autopilot', label: 'Autopilot', description: 'Execute tasks autonomously' },
{ value: 'plan', label: 'Plan', description: 'Plan steps before execution' },
];
Context 合并逻辑
effectiveContext 通过 useMemo 合并三个来源的上下文:
- Host-provided context — 通过 PostMessage 接收的 workspace/items
- User-added context — 通过 AttachmentPickMenu @mention 添加的 artifacts
- OneDrive files — 通过 ODSP picker 选择的文件
用户可以 dismiss host-provided context items(加入 dismissedKeys Set),也可以 restore 已 dismissed 的 items。发送时通过 buildFilteredContext() 过滤掉 dismissed items。
7.11 Attachments — 上下文 Chips 渲染
使用 Fluent UI 的 Overflow + InteractionTag 组件实现自适应布局:
- 三类 Chip:Workspace(
Folder20Regular)、Item(Document20Regular)、URL(Globe20Regular)、File(缩略图/document icon)、OneDrive(CloudArrowUp20Regular) - 空间不足时自动折叠为 "+N" overflow 按钮,点击展开 Menu 列表
- 每个 Chip 有 dismiss 按钮(
InteractionTagSecondary),点击移除 - Tooltip 显示完整名称和上下文信息(如 "Active · Workspace")
7.12 AttachmentPickMenu — 附件选择面板
弹出面板(320px 高,400px 宽),使用 Fluent UI Popover 定位在 inputWrapper 上方。包含:
- 搜索框 — 通过
useArtifactSearch()hook 调用requestSearchFromHost(),委托 Angular host 执行 artifact 搜索(避免 CORS) - Tab 筛选 — All / Workspaces / Items / Context,使用 Fluent UI
TabList - 最近访问 — 通过
requestRecentFromHost()获取用户最近的 workspaces 和 items - 文件上传 — "Upload from computer" 按钮触发隐藏的
<input type="file"> - OneDrive — "Upload from OneDrive" 按钮请求 host 打开 ODSP file picker overlay
7.13 ChatPaneHeader — 标题栏与会话管理
布局
- Copilot icon — 多色 sparkle SVG(蓝/黄/绿/紫渐变,与 Microsoft Copilot 品牌一致)
- "Experimental" badge — 灰色圆角标签
backgroundColor: '#EBEBEB' - New Chat(
ChatAdd20Regular)— 创建新 session - History(
History20Regular)—Menu展示所有 sessions 列表,每项显示 mode + 创建时间,带Delete16Regular删除按钮 - Sign Out(
SignOut20Regular)— 登出 GitHub - Close(
Dismiss24Filled)— 通过notifyParent({ type: 'close' })通知 host 关闭 Chat pane
7.14 共享样式系统 · Shared Styles
ReasoningCard 和 ToolResultCard 共享 useExpandableCardStyles() + useExpandableCard() hook:
- row — 单行头部(icon + label + chevron),
cursor: pointer,13px - body — 展开区域,带 L 形连接线(
::beforepseudo-element,marginLeft: 7px,paddingLeft: 22px) - chevron — 默认
opacity: 0,hover 或 expanded 时opacity: 1 - ExpandableChevron 组件 — 展开时
ChevronDown16,折叠时ChevronRight16
上下文注入 · Context Injection
promptContextPrefix.ts 构建一个结构化的上下文块,prepend 到每条 prompt 前面,让 Copilot 感知用户当前的工作环境。
📄 promptContextPrefix.ts · promptContext.ts
注入内容
- Active workspace/items — 当前打开的 workspace 及其 items
- Open tabs — 当前打开的标签页信息
- Referenced items — 用户通过 @mention 引用的 artifacts
- Current URL — 当前页面 URL,用于导航上下文
- OneDrive files — 通过 ODSP picker 选择的文件引用
PromptContext 模型
interface PromptContext {
workspaces: WorkspaceInfo[];
items: ItemInfo[];
currentUrl: string;
oneDriveFiles?: OneDriveFile[];
}
指令注入(Instruction Injection)
Context prefix 中包含行为指令,引导 Copilot 的输出格式:
- "make item names clickable" — 让 artifact 名称生成为可点击链接
- "reasoning first line = summary" — 推理过程的第一行作为折叠摘要
📌 Context injection 是 透明的 — 用户看不到注入的 prefix,但 Copilot 可以利用其中的信息提供更精准的回答。
文件附件系统 · File Attachment System
📄 fileAttachment.ts · fileValidation.ts
支持的文件类型
| 分类 | 扩展名 |
|---|---|
| Images | .png .jpg .jpeg .gif .webp .bmp |
| Documents | .txt .md .csv .tsv .json .xml .yaml .yml .log |
| Office | .xlsx .xls .docx .doc .pptx .ppt |
| Power BI | .pbix .pbip .pbit |
| Code | .py .sql .r .dax .m .kql .ipynb .js .ts .html .css |
| Data | .parquet |
限制
上传机制
使用 XHR(非 fetch)实现文件上传,原因是需要 upload.onprogress 事件来追踪上传进度。上传完成后,服务端通过 SSE 流式返回处理结果。
附件来源
- 本地文件上传 — 文件选择器 / 拖放 / 粘贴
- OneDrive 文件选择器 — 通过 host bridge 打开 ODSP picker
- Fabric artifact 搜索 — @mention 触发搜索,选择 artifact 作为附件
- 截图捕获 — 请求 host 截取当前 portal 页面
错误处理 · Error Handling
errorClassifier.ts 将原始后端错误分类为 用户友好的错误类别,每种类别有对应的标题、提示和重试策略。
| Category | Pattern | Can Retry |
|---|---|---|
copilotCli | JSON-RPC, connection lost | ✅ |
rateLimit | 429, rate limit, quota | ✅ |
auth | 401, 403, unauthorized | ❌ |
timeout | timeout, ETIMEDOUT | ✅ |
network | Failed to fetch, ECONNREFUSED | ✅ |
serverDown | 5xx, service unavailable | ✅ |
notFound | 404 | ✅ |
unknown | everything else | ✅ |
错误展示结构
每个分类后的错误包含以下信息:
- title — 用户友好的错误标题(如 "连接超时")
- hint — 可操作的建议(如 "请检查网络连接后重试")
- canRetry — 是否显示重试按钮
- 技术详情 — 可展开的原始错误信息,供调试使用
模型选择策略 · Model Selection Strategy
pickDefaultModel() 实现基于优先级的模型自动选择:
📄 App.tsx → pickDefaultModel()
选择优先级
| 优先级 | 条件 | 排序 |
|---|---|---|
| 1 (最高) | Claude Opus ≥ 4.5 | version desc |
| 2 | Claude Sonnet ≥ 4.5 | version desc |
| 3 | GPT / Codex / O-series ≥ 5 | version desc |
| 4 (fallback) | 列表中的最后一项 | — |
💡 设计理由:Claude Opus 在 agentic coding 场景(多步骤推理、工具调用链)中表现显著优于其他模型,因此给予最高优先级。Fallback 保证即使所有首选模型不可用,系统仍能工作。
Markdown 渲染 · Markdown Rendering
使用自定义的轻量级渲染器 simpleMarkdownToHtml(),无外部 Markdown 库依赖。
支持的语法
- GFM 表格 — 标准 GitHub Flavored Markdown 表格语法
- 围栏代码块 — 带语言标识的代码高亮(
```typescript) - 内联格式 — 粗体、斜体、行内代码、链接
- 列表 — 有序列表、无序列表
- 标题 — h1–h6
- 段落 — 自动段落分隔
内部导航(关键特性)
渲染器检测 *.powerbi.com 和 *.fabric.microsoft.com URL,将其转换为 data-internal-nav 属性的链接,实现 SPA 风格的导航(通过 parent bridge 触发 host 导航,而非 iframe 内跳转)。
安全
所有用户内容经过 HTML entity escaping 处理,防止 XSS 攻击。渲染器在转换 Markdown 语法前先转义所有 HTML 特殊字符。
关键设计决策总结 · Key Design Decisions Summary
以下是 fabric-chat UX 中 Top 10 设计决策及其技术 rationale:
| # | 决策 | Rationale |
|---|---|---|
| 1 | Iframe 隔离 | 与 Angular Shell 完全解耦;独立开发/测试/部署;避免 framework 冲突(Angular + React 共存) |
| 2 | PostMessage Bridge | 标准化双向通信协议;类型安全的消息定义;超时/清理/降级内建 |
| 3 | SSE over WebSocket | HTTP 兼容(通过标准代理/CDN);更简单的重连逻辑;单向推送满足需求 |
| 4 | Custom Hooks over Redux | Chat UX 状态相对简单;hooks 足够且零额外依赖;避免 boilerplate |
| 5 | Token-per-request | 每次调用获取 fresh MSAL token;避免 token 过期/刷新的复杂逻辑;对 host 透明 |
| 6 | Fabric Teal 主题 | 自定义 brand ramp 匹配 Fabric 设计体系;深色/浅色模式跟随 host 主题 |
| 7 | Smart Tool 摘要 | action-type 检测(read/edit/search/run)生成紧凑摘要;减少视觉噪音 |
| 8 | Context Injection | workspace/item context 自动注入;Copilot 无需用户手动描述工作环境 |
| 9 | Client-side Markdown | 轻量自定义渲染器;零外部库依赖;支持 Fabric 内部链接转导航 |
| 10 | Device Flow Auth | 在受限 iframe 环境中工作;无需 popup/redirect;server-side session 管理 token |
📌 整体设计哲学:最小依赖、最大隔离、最简状态管理。fabric-chat 作为一个"thin client",将复杂性推到服务端(fabric-chat-server + Copilot CLI SDK),前端保持轻量和可维护。