Skip to content

PRD: Chatbot 消息操作栏与平台转发能力

1. 概述

1.1 背景

Chatbot H5 页面目前仅支持文字对话交互,用户无法对 AI 回复的消息进行二次操作(重试、语音播放、复制、转发)。参照微信小程序已实现的"长按消息→操作栏→转发到微信聊天"模式,本次将补齐 H5 端的消息操作能力,并实现对飞书平台的转发支持。

1.2 目标

  • 为 Chatbot H5 所有 AI 回复消息增加消息操作栏
  • 支持 4 个核心操作:重新生成、语音播放、复制、转发
  • 转发能力适配飞书环境(自建应用内 H5),实现与微信小程序一致的"缩略卡片→点击进入"体验
  • 输出跨平台(微信/飞书)转发的架构方案,便于后续扩展

1.3 用户故事

作为 Chatbot 用户
我想要 对 AI 回复的消息进行重新生成、语音播放、复制、转发
以便 把有用的信息分享给同事,或者重新获取更满意的回答


2. 功能需求

2.1 触发方式

操作触发方式说明
显示操作栏长按 AI 消息文本(约 600ms)松手后操作栏在该消息底部淡入
隐藏操作栏点击页面空白处或点击其他消息的操作栏会切换显示

2.2 操作栏 UI

┌─────────────────────────────────────────────┐
│ AI 生成,仅供参考       🔄  🔊  📋  ↗️       │
└─────────────────────────────────────────────┘
    ↑ 左侧提示文字         ↑ 右侧 4 个功能图标
      (轻量文案)           重新生成 语音 复制 转发
  • 操作栏宽度通栏,不局限于消息气泡宽度
  • 左侧灰色小字:"AI 生成,仅供参考"
  • 右侧 4 个图标平铺展示

2.3 功能按钮说明

按钮图标交互备注
重新生成🔄删除当前 AI 回复,重新发送上一条用户指令详见第 7 节 Dify 后端实现分析
语音播放🔊调用系统 TTS 朗读消息文本(中文语音)点击后播放,再次点击其他消息切换播放
复制📋复制消息纯文本到系统剪贴板成功后弹出 toast "已复制到剪贴板"
转发↗️详见第 3 节核心能力

3. 转发功能设计

3.1 产品目标

一句话描述: 实现"参考微信小程序的消息转发能力,适配飞书平台",用户长按 AI 消息后,通过转发按钮将聊天内容(单条或多条)以飞书小程序卡片形式分享给飞书联系人或群聊。

3.2 核心交互流程

用户 A 在 Chatbot H5 页面
    ↓ 长按消息 → 点击转发按钮

系统判断当前环境
    ├── 微信小程序环境 → wx.shareAppMessage()
    ├── 飞书自建应用环境 → tt.shareAppMessage()
    └── 纯 H5 环境 → navigator.share() / 复制链接 兜底

拉起平台原生联系人/群聊选择器

发送 → 接收方看到一张缩略卡片
    ↓ 点击卡片
接收方进入对应小程序/应用,查看完整消息内容

3.3 转发的内容模型

每条消息需携带类型元数据,转发时根据类型决定缩略卡片如何展示:

消息类型枚举:
  - text          → 纯文本对话
  - room-card     → 会议室预定卡片
  - success-card  → 操作成功卡片(预定成功等)
  - device-card   → 设备控制状态
  - prep-card     → 主动整备通知
  - cite-card     → 引用来源卡片
  - multi         → 多条消息聚合转发

缩略卡片展示规则(以飞书为例):
  ├── text        → 标题:"AI 智能助手"  摘要:消息前 50 字
  ├── room-card   → 标题:"会议室预定"   摘要:会议室名称+时间
  ├── success-card→ 标题:"操作成功"     摘要:核心结果文案
  └── multi       → 标题:"智能助手 · N 条消息"  摘要:各条摘要预览

3.4 多消息转发

场景行为
单条转发按上表规则渲染缩略卡片
多条消息组合转发打包为一张"聚合卡片",标题显示"智能助手 · N 条消息",摘要展示各条消息的简短预览
消息+图片混合缩略卡片以文字摘要为主,图片在"进入应用后"完整展示

3.5 平台差异与适配原则

场景微信飞书
环境微信小程序飞书自建应用内 H5(使用 tt.xxx API)
转发 APIwx.shareAppMessage()tt.shareAppMessage()
缩略卡片小程序卡片(封面+标题+路径)小程序卡片(封面+标题+路径)
点击跳转打开小程序指定页面打开自建应用指定页面
体验一致性✅ 基准体验和微信完全一致

核心结论: 微信和飞书在"缩略卡片→点击进入"的体验上高度一致。开发时可用适配器模式封装,对外暴露统一 .forward(),内部分派到不同平台的 API。


4. 实现建议(给研发参考)

4.1 消息数据结构化

现有 appendBotMessage(htmlContent) 改为携带类型和结构化数据:

每条消息 DOM 节点附加 data 属性:
  data-type     → 消息类型枚举
  data-payload  → 结构化数据 JSON(用于转发时重新渲染缩略卡片摘要)

4.2 跨平台适配层

ForwardManager
  ├── detect()           // 检测运行环境
  ├── setAdapter(type)   // 注入平台适配器
  └── forward(msg)       // 对外统一接口

适配器接口:
  WeChatAdapter.forward(msg)   → wx.shareAppMessage({...})
  FeishuAdapter.forward(msg)   → tt.shareAppMessage({...})
  FallbackAdapter.forward(msg) → navigator.share() / 复制链接

5. 附录:飞书转发实现形式参考资料

A. 小程序卡片转发(推荐方案)

通过 tt.shareAppMessage() 实现,体验与微信小程序卡片完全一致

  • 接收方看到一张缩略卡片(封面图 + 标题 + 摘要)
  • 点击后进入飞书自建应用,加载对应页面
  • 开发成本最低,与微信 API 一一对应

B. 消息卡片转发(未来可考虑升级)

通过飞书消息卡片(Message Card)实现,内容直接在飞书聊天中展开

  • 接收方直接在聊天中看到完整内容,无需点击跳转
  • 可交互:卡片上可嵌入按钮(确认/取消等)
  • 多条消息可拼成一张聚合卡片
  • 缺点:需将 H5 组件翻译成飞书 Card JSON 格式,开发成本较高
  • 建议:可作为 2.0 体验升级项

C. 截图兜底

若平台不支持小程序卡片或消息卡片,可截取消息区域为图片发送:

  • 依赖 html2canvas 等截图库
  • 适用范围最广(任何平台可用)
  • 缺点:不可交互,图片质量受限

7. "重新生成"的 Dify 后端实现分析

7.1 背景说明

后端基于 Dify 平台搭建,LLM 调用经由 Agent 路由层分发到多个 Sub-Agent(如会议室预定 Agent、设备控制 Agent、闲聊 Agent 等)完成回答。每条消息的生成涉及 Dify 的对话记忆(Memory)机制。

7.2 Dify 现状约束

Dify 的 POST /chat-messages API 具有以下特点:

能力支持情况
发送消息到已有会话✅ — 传入 conversation_id 自动追加对话历史
获取会话历史✅ — GET /messages
删除整个会话✅ — DELETE /conversations/:id
删除单条消息❌ — 不支持
重新生成 / 忽略上轮回答❌ — 无原生支持
自定义传入对话历史❌ — 不支持(Memory 由 Dify 内部管理)

这意味着:Dify 的对话记忆是黑盒的,一旦消息写入会话,无法从中途"弹出"某一条。

7.3 方案对比

由于架构中存在多 Sub-Agent 路由,选择"重新生成"的实现方案时需要特别考虑对路由层和各 Agent 的影响。

方案 A:同会话重发 ❌(不推荐)

点击重新生成 → 相同 conversation_id 重新发 query → Dify 带上全部历史(含上轮回答)发 LLM
考量评价
实现成本★ 低 — 前端改几行代码即可
Sub-Agent 影响★ 极差 — 所有 Sub-Agent 的 system prompt 都要增加 "用户可能触发重新生成,请忽略自己之前的回答" 相关指令
稳定性★★ — LLM 仍可能受上轮回答影响,产生"我刚才已经回答过了"等异常回复
上下文完整性★ — 累计的 RAG 引用、工具调用结果都会重入上下文,可能干扰新回答

结论: 因 Sub-Agent 数量多且每个 Agent 的 prompt 设计独立,方案 A 需要在每个 Agent 中增加一致性约束,维护成本高且不可靠。不推荐。

方案 B:删除会话 + 历史回放 ✅(推荐)

1. GET /messages            → 拉取该会话全部消息历史
2. DELETE /conversations/:id → 删除原会话
3. 逐条回放历史消息(跳过最后一轮 user+assistant)
4. 重发用户当前 query
考量评价
实现成本★★ 中 — 需在 Dify 上层封装一个回放逻辑
Sub-Agent 影响★ 无影响 — Sub-Agent 完全无感知,和首次提问一样
稳定性★★★★ — 上下文完全干净,LLM 不会看到旧回答
上下文完整性★★★★ — 保留重新生成前的所有历史上下文,仅去掉最后一轮
性能影响回放 N 轮需要 N 次 Dify API 调用,但 N 通常较小(5-10 轮)
Token 额外消耗回放不产生回答 token,仅消耗 prompt token(约 20-50 条系统 prompt)

回放实现示例(伪代码):

function regenerate(conversationId, userId):
    // 1. 获取全部历史
    history = GET /messages?conversation_id=conversationId
    
    // 2. 去掉最后一轮 user+assistant
    truncated = history[0..-2]  // 移除最后一条 user query 和 assistant answer
    
    // 3. 删除旧会话
    DELETE /conversations/conversationId
    
    // 4. 回放历史(重建新会话)
    newConversationId = null
    for each msg in truncated:
        result = POST /chat-messages {
            query: msg.query,
            conversation_id: newConversationId,
            ...
        }
        newConversationId = result.conversation_id
    
    // 5. 发送重新生成的 query(使用新会话 ID)
    return POST /chat-messages {
        query: lastUserQuery,
        conversation_id: newConversationId,
        ...
    }

方案 C:前端管理上下文(备用方案)

  • 关闭 Dify 内置的 Memory
  • 对话历史由前端/业务层维护,通过 Dify Workflow 的 inputs 传入
  • 重新生成时,前端直接裁剪历史后发送
考量评价
实现成本★★★ 高 — 需改造 Dify 应用为 Workflow 模式,关闭 Memory
Sub-Agent 影响★★★ — 需按新架构重新设计 Agent 路由
灵活性★★★★★ — 前后端完全控制上下文
迁移风险★★★ — 与当前 Dify 基础对话模式不兼容

7.4 推荐结论

生产阶段推荐方案 B(删除会话 + 历史回放)。 理由是:

  1. Sub-Agent 完全无感知,不需要修改任何 Agent 的 system prompt
  2. 上下文干净,回答质量和首次提问一致
  3. 实现相对简单,只需在 Dify Service 层封装一个 regenerate 方法
  4. 额外 token 消耗在可接受范围内(回放 N 轮约等于 N 次无回答的请求)
阶段方案
原型验证(当前)方案 A(同会话重发,快速示意)
正式开发方案 B(删除回放)
长期演进可评估方案 C(如需更精细的上下文控制)

Released under the Private License.