磁力小牛重构技术方案
来源:磁力小牛智能客服 2.0 前端技术方案
磁力小牛智能客服 2.0 前端技术方案
零、为什么必须重构
当前的核心问题
旧版框架(ad-im-esp 1.0)已进行 60余 次的迭代版本,当前面临的问题不是个别 bug,而是系统性的架构债务,继续在现有框架上迭代的边际成本越来越高,大体问题如下:
问题收集详情:磁力小牛前端框架开发收集
| 问题 | 现状 | 影响 |
|---|---|---|
| 首屏资源 25M+ 且持续劣化 | 所有卡片全量打包进主 bundle,无法按需加载 | 流水线已出现因产物过大而构建失败;新需求引入依赖只能走微前端,开发成本极高 |
| 本地开发效率极低 | 每次改动需完整构建 2~3 分钟,构建过程脆弱易崩 | 开发节奏被频繁打断,日均有效编码时间损耗严重 |
| 宿主通信协议不透明 | CanalRuntimeContext 直接透传,无文档无约束 | 每次宿主联动需求都要靠看代码猜参数,需求开发难度持续增加 |
| 发布流程全手动 | 5 个步骤需人工操作,kfc 手动发包 → 升级大运河 → 部署 | 紧急发布时人工操作易出错;多需求并行时版本管理混乱 |
| 埋点来源数据失真 | 创编等多个入口无法上报来源,各组件自行上报逻辑分散 | 无法准确统计各入口使用量,产品决策缺乏可靠数据支撑 |
| 新功能开发上限受限 | 历史会话、图片输入等能力在现有架构上无法干净实现 | 历史会话需要独立的 SessionStore 和会话状态管理,强行在旧架构上加只会让债务更重 |
为什么不在旧版上打补丁?
| 方案 | 问题 |
|---|---|
| 继续在 1.0 上迭代 | 体积问题无解(需要动包结构才能按需加载);SessionStore 缺失导致历史会话无法干净实现;每个补丁都增加耦合,后续维护成本只增不减 |
| 局部重构某个模块 | 宿主通信、Store 层、卡片加载三者相互依赖,单独改一个会破坏另外两个;不如一次性系统性解决 |
| 不重构只做新功能 | 历史会话 + 图片输入叠加在现有架构上,体积会进一步突破上限,触发更多流水线失败 |
重构后的核心收益
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 开发体验 | 改一行等 2~3 分钟构建 | Vite HMR ChatHeader → SessionMenuDrawer → SessionList |
| expanded: SessionList + ChatFrame |
mobile关键组合:
```plain text
fullscreen: MobileChatFrame -> MobileChatHeader -> MobileSessionMenuDrawer -> MobileSessionList
预期收益
- 新增宽屏历史会话能力时,不需要复制消息区、输入区和流式渲染。
- PC/Mobile 使用同一份会话、消息、流式和宿主回调数据,但保留各自合理的布局与交互。
- chat-content 不再包含平台条件,业务卡片可以按聊天区宽度自适应。
- body 预留空间、z-index、localStorage 和 Canal 关闭回调都有单一责任位置,降低宿主回归风险。
3.2 历史会话(P0 @luoxiaohong
4.1 功能描述
- 顶部「+ 开启新对话」按钮,创建全新会话
- 历史会话列表按时间分组:今天 / 本月 / 更早
- 每条会话显示标题(取第一条用户消息内容,最多 20 字)
- 点击会话条目切换到对应会话,加载历史消息
- 当前激活会话高亮显示
4.2 数据结构
interface Session {
sessionId: string
title: string // 会话标题(第一条用户消息截取)
createdAt: number // 创建时间戳
updatedAt: number // 最后活跃时间戳
pageId?: string // 关联业务页面
}
interface SessionGroup {
label: '今天' | '本月' | '更早'
sessions: Session[]
}4.3 接口设计(与后端对齐)
| 接口 | 方法 | 说明 |
|---|---|---|
| GET /api/session/list | GET | 获取会话列表,返回按时间分组数据 |
| POST /api/session/create | POST | 创建新会话,返回 sessionId |
| GET /api/session/{sessionId}/messages | GET | 获取会话历史消息列表 |
| DELETE /api/session/{sessionId} | DELETE | 删除会话 |
4.4 状态管理
新增SessionStore,职责独立:
class SessionStore {
sessions: Session[] = []
currentSessionId: string = ''
sessionMessages: Map = new Map()
// 加载会话列表
loadSessions(): Promise
// 切换会话(懒加载消息)
switchSession(sessionId: string): Promise
// 开启新会话
createNewSession(): Promise
// 删除会话
deleteSession(sessionId: string): Promise
}
4.5 窄屏与宽屏的历史会话展示
- 宽屏:左侧固定历史会话面板,始终可见
- 窄屏 PC:隐藏,点击顶部
≡图标以覆盖层形式展开 - 移动端:点击顶部
≡图标以全屏抽屉形式展开
ConversationStore 职责
- activeCid:crypto.randomUUID() 生成,createNewConversation() 时更新
- conversationList:历史会话分页数据
- historyMessages: NormalizedMessage[]:打开历史会话后的消息列表
- session 过期由 EspRuntimeImpl 监听 RuntimeEvents 自动触发 closeConversation
- IM 消息处理入口按 cid 比对,不一致直接丢弃(多窗口隔离)
useConversation() 返回值
{
activeCid: string | null
conversationList: Conversation[]
historyMessages: NormalizedMessage[]
createNewConversation: () ⇒ Promise // 接口 2.2.2
closeConversation: (cid: string) ⇒ Promise // 接口 2.2.5
fetchHistoryList: (page: number) ⇒ Promise // 接口 2.2.3
fetchMessageList: (cid: string, page: number) ⇒ Promise // 接口 2.2.4
submitFeedback: (bizMsgId: string, opsType: 1 | 2) ⇒ Promise // 接口 2.2.7
}
关键约定
- bizMsgId:EspRuntimeImpl 发消息时生成,写入 NormalizedMessage
- 发消息时 conversation.title = 用户输入前 32 字符
- 历史消息通过 normalizeHistoryMessage() 转成 NormalizedMessage 复用现有渲染管道
- useConversation 通过 useRobotContentRuntime() 返回值暴露,不单独导出
五、架构设计
5.1 架构总览
[图片待补充]
| 分层 | 产出物 | 介绍 |
|---|---|---|
| 业务功能 | 业务模块(npm) | 模块化形式提供,通过大运河运行时接入,涵盖会话管理(新建会话、历史会话查看)、消息输入(快捷入口、关键词联想)、消息展示和操作(消息渲染、思考过程、消息评价、消息重试)等核心业务功能 |
| UI 层 | 原子组件(npm / Skill) | 基于 MUI 、专为 AI 驱动界面设计,建设一套小牛原子组件库 + 设计规范体系,使 AI 能通过自然语言直接生成符合设计规范的可用代码 |
| 消息渲染层 | 渲染引擎(npm) | 提供完整的消息渲染能力,支持直出渲染与流式渲染两种渲染流程 |
| 核心功能层 | 核心 SDK(npm) | 提供对话相关的状态存储、上下文共享 |
| 协议层 | 协议规范(文档) | 定义各层通信标准、规范化跨应用通信 |
| 全流程埋点 | 监控体系 | 涵盖用户从平台触点进入到与卡片交互及采纳的全链路业务埋点 |
| Devtool | 研发工具链 | 提供完整的研发调试支持: 消息查看、多环境代理调试、消息 MOCK 消息导入导出 流式速率控制 |
5.2 项目结构设计
[图片待补充]
packages/
├── esp-robot-core/ # 会话建联 / 初始化
│ ├── package.json
│ └── src/
│ ├── connection/ # token + connect + 状态机
│ ├── session/ # openSession + MessageList 桥
│ └── messages/
│
├── runtime/ # 状态、生命周期、语义化命令
│ ├── package.json
│ └── src/
│ ├── EspRuntime.ts
│ ├── createEspRuntime.ts
│ │
│ ├── chat/
│ │ └── ChatStore.ts
│ │
│ ├── streaming/
│ │ ├── StreamingStore.ts
│ │ ├── protocol/
│ │ │ └── parser.ts
│ │ ├── scheduler/
│ │ │ └── queue.ts
│ │ └── types.ts
│ │
│ └── bridge/ # 宿主 ↔ Robot 四条标准通道
│ ├── index.ts # 统一出口,组装四个 channel
│ ├── types.ts # 所有 channel 的公共类型
│ │
│ ├── context/ # 宿主拥有、Robot 消费的状态 HostStore
│ │ ├── ContextChannel.ts # 账号、路由、功能开关、系统版本
│ │ └── types.ts
│ │
│ ├── command/ # 宿主 → Robot,要求 Robot 做事
│ │ ├── CommandChannel.ts # open / close / sendMessage / showBubble
│ │ └── types.ts
│ │
│ ├── request/ # Robot → 宿主,需要宿主响应结果
│ │ ├── RequestChannel.ts # navigate / feedback / updateAiCreate
│ │ └── types.ts
│ │
│ └── event/ # Robot → 宿主,告知已发生的事实(单向)
│ ├── EventChannel.ts # containerClosed / entryClicked / runtimeError
│ └── types.ts
│
├── chat-content/ # 内容匹配、渲染与业务卡片
│ ├── package.json
│ └── src/
│ ├── types/
│ ├── registry/
│ ├── renderer/
│ │ ├── ContentRenderer.tsx
│ │ └── CardRenderer.tsx
│ ├── streaming/
│ ├── cards/
│ │ ├── common-message/
│ │ ├── data-query/
│ │ ├── diagnosis/
│ │ └── ai-create/
│ └── index.ts
│
├── ui/ # 无 Chat 业务语义的视觉原子
│ ├── package.json
│ └── src/
│ ├── Button/
│ └── theme/
│
└── platform/ # 多平台布局与最终装配
├── package.json
└── src/
├── pc/
│ ├── layout/
│ │ ├── FullscreenLayout.tsx
│ │ └── DrawerLayout.tsx
│ └── entry/
│
└── mobile/
├── layout/
│ ├── FullscreenLayout.tsx
│ └── DrawerLayout.tsx
└── entry/
packages/
├── esp-robot-core/ # 会话建联 / 初始化等等 同@ad/ad-im-core
│ ├── package.json
├── runtime/ # 状态、生命周期、语义化命令(当前系统是什么状态?用户或宿主可以执行什么动作?
│ ├── package.json
│ └── src/关键变化:
- 平台装配层:PC/Mobile 只保留布局与平台适配,避免业务逻辑重复,实现端能力独立演进。
- 内容与视觉层:统一消息匹配、流式渲染和业务卡片管理,新增内容只需注册,无需修改主流程。
- Runtime 层:收敛宿主、会话、流式和主题状态,以“状态订阅 + 语义化 Actions”提供统一 interface。
Adapter 层:隔离 IM、HTTP、SSE 和宿主实现,协议变化不再影响上层业务。- UI 层:沉淀无业务语义的视觉原子与局部主题,提高复用率并避免污染宿主页面。
| 我们的设计 | 效运通用IM方案 |
|---|---|
| runtime | @bard/im |
| chat-content/registry | @bard/widget 契约 |
| chat-content/cards | 默认及业务 Widget |
| pc/mobile layout | @bard/skeleton 平台装配 |
| ui/theme | Token 与主题系统 |
5.3 协议层:标准化Bridge P0(解决宿主通信问题)
跨宿主的 callback 本质上只有四种:
| 类型 | 含义 | 例子 |
|---|---|---|
| Context | 宿主拥有、Robot 消费的状态 | 账号、路由、功能开关、系统版本 |
| Command | 宿主要求 Robot 做事 | 打开、关闭、发送消息、展示气泡 |
| Request | Robot 请求宿主做事并等待结果 | 跳转、反馈、更新一键创编状态 |
| Event | Robot 告知宿主已经发生的事实 | 容器关闭、入口点击、Runtime 异常 |
现状: CanalRuntimeContext 把四种语义混在了一个对象里:既有数据,又有 getter、callback、宿主对象和反向注册
目标:把“宿主调用我们、我们调用宿主、双方状态同步、纯通知”分成四条明确通道
[图片待补充]
| 通信类型 | 数据方向 | 重构前 | 重构后 |
|---|---|---|---|
| Context | 宿主 → Robot | global、getter、Location、配置混在 params/dataparams={{ global, location: window.location, systemVersion: () ⇒ global.systemVersion, userConfig: { getAiOneClickSwitch: () ⇒ global.aiOneClickSwitch, }, }} | Robot 获得宿主状态的快照,而不是访问宿主状态本身。const context: HostContext = { user: { accountId: global.userEspAccount.accountUcId, userId: global.user.userId, }, }; await robot.init(context); |
| Command | 宿主 → Robot | onBubbleApiReady 反向注册方法、修改共享 Storeconst showBubbleRef = useRef void) | null>(null); { showBubbleRef.current = api.showBubble; }, }} />; showBubbleRef.current?.({ text: ‘点我可以一键帮你填创编’, }); |
| Request | Robot → 宿主 | Robot 直接调用 callback、下钻 sceneModelglobal.canalRuntimeContext ?.getAdCreateContainer?.() ?.sceneModel ?.updateAiCreateContainerStatusFnMap ?.processing?.(); | Robot 描述“希望宿主完成什么”,宿主自己决定“具体怎么完成”。const unsubscribe = robot.subscribe(event ⇒ { switch (event.type) { case ‘container.closed’: global.setRobotShowContainerInit(undefined, false); break; case ‘entry.clicked’: trackEntryClick(event.source); break; case ‘creation.inference.failed’: showInferenceError(event.reason); break; } }); |
| Event | Robot → 宿主 | onContainerClose、onEntryClick 等零散 callback | 宿主统一订阅 RobotEventRobot 只负责通知事实,不返回结果// 内部通过 HostPort 发出请求: await hostPort.request(‘creation.status.change’, { status: ‘processing’, }); // 宿主实现对应能力: const capabilities: EspCreateHostCapabilities = { async updateCreationStatus({ status }) { const sceneModel = getAdCreateContainer()?.sceneModel; sceneModel ?.updateAiCreateContainerStatusFnMap ?.[status]?.(); }, }; |
最终判断规则
遇到新的跨宿主 callback,可以依次判断:
它是宿主拥有的数据吗?
→ Context
它是宿主要求 Robot 做事吗?
→ Command
它是 Robot 要求宿主做事并等待结果吗?
→ Request
它只是 Robot 告诉宿主某件事已经发生吗?
→ Event5.4 核心层:Store 设计 P0
GlobalStore → 全局配置(用户信息/白名单/主题/sourceType/容器形态)
ChatStore → 会话管理(会话列表/当前会话/切换)
RuntimeStore → 运行时(pageId/)各 Store 职责清晰,不互相持有引用,通过 xxEventBus 解耦通信。
5.3 埋点统一方案 P0
[图片待补充]
| 埋点参数 | 实现/规范 |
|---|---|
| 应用级 | 统一注入 |
| 会话级 | 通过radar dimension 字段注入,贯穿整个会话/radar实例的生命周期 |
| 卡片级 | 默认全埋点收集,warp高阶组件注入自定义参数(统一注入 [图片待补充] 自定义需求通过skill 规范化接入(提供规范(怎么埋?,现有工具(用什么埋?) |
前后对比
| 维度 | 原架构 | 新架构 |
|---|---|---|
| 接入方式 | 每个按钮手写 sendClick 或 reportCardInteraction | 普通点击自动采集,卡片只声明一次上下文 |
| 覆盖率 | 依赖开发主动接入,容易漏埋 | 新增按钮默认进入全埋点 |
| 公共参数 | 各埋点重复拼装 | 应用、会话、卡片分级注入 |
| 卡片识别 | 事件里手工传 contentType/msgId | Message Provider 自动提供 |
| 维护成本 | 改按钮文案或结构时同步维护埋点 | DOM 行为自动识别,业务参数集中维护 |
| 数据规范 | 事件名、字段容易各自定义 | 统一事件模型和参数白名单 |
| 链路分析 | 点击事件彼此孤立 | 可串联应用 → 会话 → 消息 → 卡片 → 操作 |
| 扩展方式 | 新需求继续增加手工埋点 | Provider、原子 adapter、Skill 三个固定 seam |
| 特殊事件 | 与点击埋点混在一起 | 点击全埋点和结果/生命周期埋点职责分离 |
所有唤起入口初始化时必须传入sourceType,在EspEvent.HOST_OPEN事件中携带:
enum SourceType {
AVATAR_BUTTON = 'avatar_button', // 头像悬浮按钮
CREATIVE_EDITOR = 'creative_editor', // 创编页主动唤起
REACH_MGT = 'reach_mgt', // 主动触达
MENU_BAR = 'menu_bar', // 导航栏入口
SHORTCUT = 'shortcut', // 快捷指令
}core/store/GlobalStore持有currentSourceType,所有接口请求(含callActiveRobot)统一从 Store 读取并携带,禁止组件层自行上报。
5.4 UX优化(解决体积问题)P0
方案一:卡片动态加载
每个业务卡片注册到卡片注册表,首屏不加载:
// 卡片注册表(轻量,只包含 ID → 动态 import 映射)
const CardRegistry: Record Promise> = {
'DATA_QUERY': () => import('../cards/DataQueryCard'),
'DIAGNOSIS': () => import('../cards/DiagnosisCard'),
'MATERIAL': () => import('../cards/MaterialCard'),
// ...
}
// MessageList 渲染时按需加载
const CardComponent = React.lazy(() => CardRegistry[message.type]())方案二:图标资源 CDN 化
60+ SVG/PNG 图标迁移至 CDN,通过 url-loader 阈值控制,不再内联入 JS bundle。
方案三:图表库按需引入
@mchart/mobile-react和@mchart/pc-react体积较大,仅在含图表的卡片中引入,不进入主 bundle。
5.5 DX优化:HMR 热重载 P1
采用“双构建链路”:
本地开发:
pnpm dev
→ Vite Dev Server
→ /entry/index.js 物料兼容桥
→ 加载 PcRobot + Common 源码
→ Vite HMR / React Fast Refresh
生产构建:
pnpm build
→ drow build --env prod
→ 现有发布产物保持不变具体改造:
- 新增
vite.config.ts,配置 Less、SVG、路径别名、环境变量及 Common 源码联动。 - 新增
/entry/index.js开发桥,兼容现有浏览器插件的物料加载协议。 - React、ReactDOM 等依赖复用宿主实例,避免重复 React 和 Hook 异常。
- 命令调整:
{ "dev": "DEVICE_ENV=pc vite", "dev:drow": "DEVICE_ENV=pc drow dev", "build": "DEVICE_ENV=pc drow build --env prod" }
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 本地构建 | drow/Webpack 全量打包 | Vite 按需转换模块 |
| 更新方式 | 重新编译并硬刷新 Chrome | HMR/Fast Refresh |
| 页面状态 | 刷新后状态丢失 | 组件状态基本保留 |
| Common修改 | 重新构建整个物料 | Common 模块直接热更新 |
| 冷启动 | 需要生成大型开发 Bundle | Dev Server 快速启动 |
| 生产构建 | drow | 仍然是 drow,无变化 |
| 回退能力 | 无独立备用链路 | 可通过 pnpm dev:drow 回退 |
带来的收益
- 缩短启动和代码反馈时间,修改后通常可在秒级内生效。
- 减少全量编译和浏览器硬刷,提升高频 UI 调试效率。
- 保留组件状态,复杂对话、表单和流式场景不必每次重新操作。
- PcRobot 与 Common 可以联动热更新。
- 生产构建与发布流程不变,改造风险主要隔离在本地开发阶段。
- 双链路保留,Vite 遇到兼容问题时仍可使用
pnpm dev:drow。
5.6 DX优化:Devtool @luoxiaohong P0
**设计理念:**所有调试都围绕 Message 展开,Streaming 是 Message 的子资源,Mock 是 Message 的再生产能力,Export 是 Message + Streaming 的快照
原型设计
[图片待补充]
| 能力 | 详情 |
|---|---|
| 消息查看 | 提取IM数据流中研发需要关注的数据,并能把“游离的SSE消息数据”收进来,形成可视化页面,并翻译(枚举映射)。 |
| 调试 / 模拟 | mock能力:支持复制现有消息(普通/流式),快速 mock。【mock 只影响前端调试,不污染真实发送流程。】支持把本地的mock,一键抄送到QA/产品,进行回测【生成mock码-给到对应的人-导入mock码】模拟宿主传入数据:模拟宿主传入不同数据的各种情况,讲宿主调试与小牛调试分离模拟营销组件接入场景一键部署/发包:支持只发包,和发包并部署 |
| 分析 / 排障 | 实时捞取线上真实日志,从而快速进行问题定位信息,生成分析码,发给小牛领域专家后,AI智能分析、快速定位是否为前端问题。 |
| 产品看数据 | 与全埋点看数据方式相同。 |
形态
- Chrome 插件:负责面板、开关控制、导出、回放、持久化。
- 业务代码里只保留很薄的一层 bridge,负责暴露消息快照、Streaming 关联、mock 注入、回放能力。
5.7 构建&发布流程优化 @luoxiaohong P0
发布粒度细化:将原有的应用级整包发布,拆分为「基座」与「内容」两个独立发布单元,实现局部动态更新
收益:通过拆分发布粒度,按需加载,充分利用缓存、提升流水线构建效率和首屏加载速度,实现一次发布构建一次、两端同步更新。
方案对比
| 方案 | (原小牛) | (青松)子应用(基座)+大运河多模块(内容) | 大运河模块(基座)+大运河子模块(内容) | 大运河模块(基座)+Dolly组件子应用(内容) |
|---|---|---|---|---|
| 方案详述 | [图片待补充] | [图片待补充] | [图片待补充] | [图片待补充] |
| 发布部署(粒度) | 过粗❌最小发布单元:应用级一次变更单个模块(应用) | 过细❌最小发布单元:组件一次变更多个模块(卡片组件) | 适中最小发布单元:内容区一次变更单个模块 | 适中最小发布单元:内容区一次变更单个模块 |
| 发布速度 | 构建2次(双端) | 构建1次 | 构建1次 | 构建1次 |
| 改造成本 | - | 复用 | 复用 | 引入新的发布模式❌ |
| 首屏速度 | 项目全量加载❌ | 按需✅ | 按需✅ | 按需✅ |
| 经验 | - | 青松 | 实践经验少,可能有坑❌ | 营销组件 |
5.8 pageId 动态化@lvmingruiP0
- 从firefly维护一个配置列表,后续直接从firefly新增,无需改动代码
| 原先 | 改后 |
|---|---|
| [图片待补充] | [图片待补充] |
[图片待补充]
| 改前效果 | 改后效果 |
|---|---|
| 前端代码 global.ts PAGE_ID_MAP + calculateRouter 写死新增页面需改代码并发版;漏改 → pageId=99999999 ❌发布周期:1~数天 | 改后:Firefly 配置后台路由规则 + 映射表从代码中拆出运营 / 后端自助配置,秒级热生效;兜底缓存,永不丢失 ✅ |
六、综合对比矩阵(全维度)
| 维度 | 磁力土豆 | 磁力小牛旧仓库 | 磁力小牛 2.0 目标 | 来源推荐 |
|---|---|---|---|---|
| 分层清晰度 | ✅ 5 层严格单向 | ⚠️ 混杂 | ✅ 7 层规范 | 土豆铁律 + 2.0 设计 |
| 流式状态语义 | ✅ turnPhase 三态 | ⚠️ boolean + enum 混用 | ⚠️ isStreaming boolean | 土豆三态 |
| 流式渲染引擎 | ⚠️ 基于 IM chunk | ✅ SSE 专用引擎完整 | ✅ SSE | 旧仓库整体迁移 |
| 卡片解耦 | ✅ View/Container 分离 | ❌ 直接订阅 store | ✅ CardRegistry 规划 | 土豆范式 + 2.0 CardRegistry |
| 卡片按需加载 | ⚠️ 路由级 | ❌ 全进 bundle | ✅ 卡片级 React.lazy | 2.0 最优 |
| 控制层/渲染层 | ❌ 未分离 | ✅ cardControlMap 独立 | — | 旧仓库 cardControlMap |
| 跨卡片聚合 | ❌ 无 | ✅ batchStore | — | 旧仓库迁移 |
| 静默副作用 | ❌ 无 | ✅ atom 回填创编页 | — | 旧仓库迁移 |
| 埋点体系 | ✅ SWC 自动 | ✅ 事件枚举完整 | ✅ 分级注入 | 旧仓库事件枚举 + 2.0 策略 |
| SSE 三段埋点 | ❌ 无 | ✅ 800ms 稳定后上报 | — | 旧仓库完整保留 |
| MessageTrace | ❌ 无 | ✅ 无感注入 | — | 旧仓库完整迁移 |
| 初始化性能计时 | ❌ 无 | ✅ LoggerSpan 链式 | — | 旧仓库迁移 |
| 状态管理 | ✅ Zustand vanilla | ⚠️ MobX + Context 双体系 | ✅ 多 Store + EventBus | 土豆 Zustand 模式 |
| 稳定空引用 | ✅ 模块级常量 | ❌ 内联字面量 | — | 土豆模式 |
| 幂等初始化 | ✅ useState 惰性 + 标志位 | ❌ 无保护 | — | 土豆模式 |
| API 自动生成 | ✅ swet-cli | ❌ 手写 | — | 土豆 swet-cli |
| 构建缓存 | ❌ 无 | ✅ Turborepo | — | 旧仓库 Turborepo |
| 本地 Mock | ❌ 无 | ✅ SSE Mock + URL 开关 | — | 旧仓库完整保留 |
| 首屏白屏 | ✅ 原生 DOM loading 前置 | ❌ 无 | — | 土豆模式 |
| 持久化版本号 | ✅ 版本校验 | ❌ 无 | — | 土豆模式 |
七、迁移策略
- 迁移策略
- 能力迁移优先级:核心聊天流程 → 流式输出 → 各类卡片 → 主动触达 → NPS 卡片逐步迁移:
- 每个卡片迁移后单独验证,迁移完成打标后删除旧卡片代码
- 灰度策略:新建独立项目承接重构,通过宿主侧的白名单切换实现新旧子应用灰度切流,做到物理隔离、风险可控、秒级回滚。
- 自测覆盖
- 配置覆盖率插件,新项目需要达到90%+才可准出
- 核心场景覆盖
| 场景 | 验证内容 |
|---|---|
| 普通问答 | 消息展示、流式输出一致 |
| 多轮对话 | 上下文传递一致 |
| 卡片展示 | 卡片类型、交互行为一致 |
| 用户操作 | 点击、跳转、回填一致 |
| 异常场景 | loading、失败重试一致 |
| 宿主联动 | 页面跳转、参数透传一致 |
八、发布方案(TODO)
核心观测指标
| 指标 | |
|---|---|
| 新链路成功率 | |
| 入口监控 | |
九、里程碑规划
✅ 直接迁移(逻辑不变,仅样式调整)
🔨 改造迁移(有逻辑变更)
🆕 全新开发
一、工程基础(🆕 全新)@luoxiaohong(2pd)
1.1 本地开发环境:Vite HMR @luoxiaohong
做什么:引入 Vite 作为本地开发构建工具,区分 pnpm dev(Vite HMR)和 pnpm build(drow 生产构建)。
现状问题:
- 1.0 用 drow preview,移动端每次改动需完整构建 2\~3 分钟
- 构建过程脆弱,改一两行代码可能构建崩溃,重新等待
收益:
- 代码改动 → 浏览器更新从 **2\~3 分钟 → 秒级( import('./YourCard')`,MessageList 自动按需加载。
Q:图片输入如何接入
发消息时在 params 中携带 images: [{url: 'cdn地址'}],图片需先上传至 KCDN 获取 URL(上传方式待后端确认)。
项目相关文档
- 前端技术方案 2.0:https://docs.corp.kuaishou.com/d/home/fcABp3-tXvraKa54A81Bb8CJ6
- 后端技术方案:https://docs.corp.kuaishou.com/d/home/fcAD-s6amU97sCTT_YD0_Cq69
- 框架问题收集:https://docs.corp.kuaishou.com/d/home/fcACWBveXy79TmyvFIIJ59ycj
---
## 附录:磁力小牛前端框架开发问题收集
> 来源:磁力小牛前端框架开发收集
# 磁力小牛前端框架开发收集
| 问题 | 预期 | 反馈人 | 是否解决 |
|---|---|---|---|
| 埋点上报问题:现在从创编调起小牛,应该是统计不到来源的(本质上也是资源位的一种),现在创编用的主动唤起方法应该也不支持传相关参数埋点上报混乱 | 在 GlobalStore 增加统一的 sourceType 字段,所有唤起入口(创编/悬浮按钮/主动触达等)初始化时必须传入来源标识callActiveRobot 接口统一携带来源参数,禁止各组件自行上报 | @qinsiliang@gaofan05 | |
| 小牛与宿主数据交互问题:目前大运河和小牛的联动还是比较绕,宿主透传了很多东西,协议也不透明。需求开发难度比较高 | 设计标准的 EspEventBus(on/off/emit),替换当前 CanalRuntimeContext 直接透传定义清晰的事件协议(枚举 + TS 类型),宿主和小牛均遵循事件契约通信 | @qinsiliang | |
| 开发调试问题:开发调试费劲,不能热重载,mock数据也费劲调试部署太费劲了,mock数据需要url挂载参数,preview没有生效也不知道是什么原因docs的proxy代理咋老失败呢本地构建很脆弱,经常改动一两行代码构建直接崩掉,又要重新pnpm preview,移动端的每次重新pnpm preview都要2-3分钟 | 直接在ESP Tools里面mock数据,预制每个卡片的mock模版数据区分 dev 和 preview 命令,dev 模式走 HMR 热更新,不经过完整构建proxy 代理失败需增加错误提示和 fallback 机制 | @lidaqing@liushijie06@dengrongyao@hejinsheng | |
| 项目打包问题:项目大小达到临界值,无法新增包,开发需求引入第三方新组件只能使用微前端形式接入之前有流水线因打包产物过大失败问题,可以拆包优化下首屏资源25M过大,持续劣化中 [图片待补充] | 所有卡片单独拆分成一个独立组件,参考青松实现方式,考虑是否走大运河Common/assets 下 60+ 图标资源考虑 CDN 外链 | @liushijie06@dengrongyao@luoxiaohong | |
| 项目发布方式问题:部署上线流程慢,需要本地提交代码->kfc发包(如果在开发过程中有新的上线还要新弄一个版本?)->构建->升级大运河->部署PC和移动得跑两个流水线,发两次包,很少仅单端的变更 | | @liushijie06@luoxiaohong | |
| 与后端交互问题:pageId硬编码路由映射,每新增页面需手动维护 | | @gaofan05 | |
| 与QA联动对于问题复现,需要重复QA提供的对话/步骤,部分场景可能不能稳定复现,研发需要去mock 消息以及流式接口返回 | 对话上下文导出,支持导入复现问题场景 | @luoxiaohong | |
| 与营销交互问题希望提供统一外部组件接入方式,现在存在微前端、组件库多种 | | @liushijie06@chenshuhui05 | |
| | | | |
**新版设计稿:**
| 窄屏 | 宽屏 |
|---|---|
| [图片待补充] | [图片待补充] |
后端技术方案:暂无
前端技术方案:[磁力小牛智能客服 2.0 前端技术方案](https://docs.corp.kuaishou.com/d/home/fcABp3-tXvraKa54A81Bb8CJ6)