diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index a2e438577f4..5163307e850 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -798,3 +798,11 @@ Workspace 和 System 资源管理的 `getKnowledgeDetail(knowledgeId, section?)` 对应后台独立的设置/策略查看权限;普通详情会去除未授权的配置元数据。 `getKnowledgeMcpConfig` 使用 `//mcp_config`,仅获取 MCP 配置,独立于外部服务设置权限。 文档和图片复用的操作接口按持久化资源类型鉴权;批量图片操作未传 `resource_type` 时由服务端解析实际类型。 + +### 公开对话分享 + +`chat/conversation.ts` 的 `getShareConversation(shareId)` 请求 Chat API 下的 `GET /share/`, +不需要 accessToken 或 applicationId。响应类型 `ChatShareDetail` 经 `@/api/types` 导出,包含 +`abstract` 和按时间正序排列的 `chat_record_list`。当前后端返回旧版 `problem_text`、 +`answer_text`、`answer_text_list` 及仅含上传附件的 `execution_details`,不返回 V3 `question`、`messages`。 +分享页适配旧版字段,不通过原会话接口补查内容。 diff --git a/ui/src/api/chat/conversation.ts b/ui/src/api/chat/conversation.ts index e47972f8a9f..504e8c52897 100644 --- a/ui/src/api/chat/conversation.ts +++ b/ui/src/api/chat/conversation.ts @@ -1,7 +1,7 @@ import { get, post, put, del, postStream } from './core/request' import type { ResponsePage } from './core/types' import { CHAT_API_BASE_PATH as chatApiBase } from '@/api/constants' -import type { ChatLogRecord, PortalConversationSummary } from '@/api/types' +import type { ChatLogRecord, ChatShareDetail, PortalConversationSummary } from '@/api/types' /** v3 对话接口均按智能体划分资源路径。 */ const getApplicationPath = (applicationId: string) => `/v3/application/${applicationId}` @@ -46,6 +46,9 @@ const putConversation = (applicationId: string, chatId: string, data: unknown) = const postShareConversation = (applicationId: string, chatId: string, data: { chat_record_ids: string[] }) => post<{ chat_record_ids: string[] }, { link: string }>(`${getApplicationPath(applicationId)}/chat/${chatId}/share_chat`, data) +/** 获取公开分享的对话记录,无需智能体访问凭证。 */ +const getShareConversation = (shareId: string) => get(`/share/${encodeURIComponent(shareId)}`) + /** 将语音转换为文字。 */ const postSpeechToText = (applicationId: string, data: unknown) => post(`${getApplicationPath(applicationId)}/speech_to_text`, data) @@ -61,5 +64,6 @@ export default { deleteConversation, putConversation, postShareConversation, + getShareConversation, postSpeechToText, } diff --git a/ui/src/api/types/chat-share.ts b/ui/src/api/types/chat-share.ts new file mode 100644 index 00000000000..adb9691b218 --- /dev/null +++ b/ui/src/api/types/chat-share.ts @@ -0,0 +1,16 @@ +import type { ChatContent } from './chat-log' +import type { Dict } from './common' + +/** 公开分享接口返回的旧版问答记录。 */ +export interface ChatShareRecord { + id: string + problem_text: string + answer_text: string + answer_text_list: (ChatContent | ChatContent[])[] + execution_details?: Dict[] +} + +export interface ChatShareDetail { + abstract: string | null + chat_record_list: ChatShareRecord[] +} diff --git a/ui/src/api/types/index.ts b/ui/src/api/types/index.ts index 3f23361052d..638c2420cb2 100644 --- a/ui/src/api/types/index.ts +++ b/ui/src/api/types/index.ts @@ -38,3 +38,5 @@ export type * from './termbase' export type * from './chat-log' export type * from './shared-authorization' + +export type * from './chat-share' diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 6a2ae00e7ac..a8f53d81721 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -966,11 +966,26 @@ Workspace API 内部使用 `getWorkspaceId()`,System 共享 API 不接收工 聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。 `message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表 自行展示流式圆点,开场白放入 `prepend` 插槽,日志空状态放入 `empty` 插槽。日志列表不注入发送能力。 +正式对话的 `message-list` 根据会话 Store 的 `isShared` 展示分享选择态,同轮提问与回答同步选择, +内容只读且选中时显示背景;无记录 ID 的问答不可选。列表底部提供当前已加载轮次的全选、已选数量、 +取消及创建链接,创建时按消息顺序提交记录 ID;成功退出分享,失败保留选择。分享期间主面板隐藏输入框, +消息列表卸载时退出分享态;调试入口不提供分享服务。 +分享组件放在 `message-list/share/`:`ShareMessageRow` 通过插槽承载消息,按记录 ID 切换整轮选择; +`ShareFooter` 负责全选、数量、取消、创建链接及退出清理。两者通过 `v-model` 共享已选记录 ID, +各自在组件内获取分享服务与提交状态;`message-list/index.vue` 仅准备记录 ID、保存已选 ID 并组合组件。 + 消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, 直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 `hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 +### SharePanel(对话模块) + +`conversation-panel/main/share-panel/index.vue` 展示只读问答,通过 `useSharePanelContentStore()` +读取所属视图提供的 `SHARE_PANEL_CONTENT_KEY`。正式对话视图接入当前会话数据,独立分享页 +提供公开接口返回的标题、消息及加载状态,不加载原会话历史。 +回答复制提取内容项正文,不传递内容数组给剪贴板。 + ## 文档维护 仅在目录、公开接口或使用规则变化时更新对应组件章节;不登记内部函数和修复过程。 diff --git a/ui/src/conversation-panel/components/header/chat/index.vue b/ui/src/conversation-panel/components/header/chat/index.vue index d9b1d3f4a4f..89241cffc32 100644 --- a/ui/src/conversation-panel/components/header/chat/index.vue +++ b/ui/src/conversation-panel/components/header/chat/index.vue @@ -30,22 +30,25 @@ const handleNewConversation = () => (props.newConversation ? props.newConversati -
+

{{ list.currentConversation.value?.abstract || '新建对话' }}

- - - - - - - - - - - - - + diff --git a/ui/src/conversation-panel/components/message-list/index.vue b/ui/src/conversation-panel/components/message-list/index.vue index 2bf0ecf3159..627c3f37208 100644 --- a/ui/src/conversation-panel/components/message-list/index.vue +++ b/ui/src/conversation-panel/components/message-list/index.vue @@ -6,15 +6,19 @@ import PrologueContent from '../prologue-content/index.vue' import { formatPrologue } from '../prologue-content/index' import ReverseInfiniteScroll from '../../core/reverse-infinite-scroll/index.vue' import { useMessageListStore } from './index' +import ShareMessageRow from './share/ShareMessageRow.vue' +import ShareFooter from './share/ShareFooter.vue' import { useConversationListStore } from '../../left-sidebar/conversation-list/index' defineProps<{ appName?: string }>() const { appInfo, messages, loading, streamLoading, hasMore, loadMore, onStreamProgress } = useMessageListStore() -const { - // 是否分享 - isShared, - // 切换分享 - switchShared } = useConversationListStore() +const { isShared } = useConversationListStore() +// 分享按完整问答轮次提交,提问与回答共用记录 ID。 +const selectedRecordIds = ref([]) +const messageRecordIds = computed(() => + messages.value.map((message, index) => (message.role === 'ASSISTANT' ? message.recordId : messages.value[index + 1]?.recordId)), +) +const availableRecordIds = computed(() => [...new Set(messageRecordIds.value.filter((id): id is string => Boolean(id)))]) const scrollRef = ref>() // 开场白随智能体信息变化(debug 保存后为最新草稿) @@ -30,25 +34,30 @@ const showUserAvatar = computed(() => appInfo.value?.show_user_avatar)
+ diff --git a/ui/src/conversation-panel/components/message-list/share/ShareFooter.vue b/ui/src/conversation-panel/components/message-list/share/ShareFooter.vue new file mode 100644 index 00000000000..d6e95b772b9 --- /dev/null +++ b/ui/src/conversation-panel/components/message-list/share/ShareFooter.vue @@ -0,0 +1,62 @@ + + + diff --git a/ui/src/conversation-panel/components/message-list/share/ShareMessageRow.vue b/ui/src/conversation-panel/components/message-list/share/ShareMessageRow.vue new file mode 100644 index 00000000000..66fb2cbf225 --- /dev/null +++ b/ui/src/conversation-panel/components/message-list/share/ShareMessageRow.vue @@ -0,0 +1,40 @@ + + + diff --git a/ui/src/conversation-panel/main/chat-panel/index.vue b/ui/src/conversation-panel/main/chat-panel/index.vue index 1d1a366e88c..fd6126b6047 100644 --- a/ui/src/conversation-panel/main/chat-panel/index.vue +++ b/ui/src/conversation-panel/main/chat-panel/index.vue @@ -16,9 +16,6 @@ const list = useConversationListStore() const input = useMessageInputStore() // 拖拽到面板任意处 → 交给 message-input 接收文件 const handleDrop = (e: DragEvent) => input.addFiles(e.dataTransfer?.files) - - - diff --git a/ui/src/conversation-panel/main/share-panel/index.ts b/ui/src/conversation-panel/main/share-panel/index.ts index 88ff0844ec6..1d224726603 100644 --- a/ui/src/conversation-panel/main/share-panel/index.ts +++ b/ui/src/conversation-panel/main/share-panel/index.ts @@ -1,4 +1,6 @@ -import { ref, inject, type InjectionKey } from 'vue' +import { ref, inject, type Ref, type InjectionKey } from 'vue' + +import type { MessageListStore } from '../../components/message-list/index' export interface SharePanelDeps { /** 提交分享:参数为按消息顺序排列的已选 chat_record id */ @@ -55,3 +57,16 @@ export function useSharePanelStore(): SharePanelStore { if (!store) throw new Error('useSharePanelStore 必须在提供 SHARE_PANEL_KEY 的视图内使用') return store } + +/** 分享面板只读展示所需的数据,由所属视图注入。 */ +export type SharePanelContentStore = Pick & { + title: Readonly> +} + +export const SHARE_PANEL_CONTENT_KEY: InjectionKey = Symbol('share-panel-content') + +export function useSharePanelContentStore(): SharePanelContentStore { + const store = inject(SHARE_PANEL_CONTENT_KEY) + if (!store) throw new Error('useSharePanelContentStore 必须在提供 SHARE_PANEL_CONTENT_KEY 的视图内使用') + return store +} diff --git a/ui/src/conversation-panel/main/share-panel/index.vue b/ui/src/conversation-panel/main/share-panel/index.vue index 0a94df4da17..34fa2232386 100644 --- a/ui/src/conversation-panel/main/share-panel/index.vue +++ b/ui/src/conversation-panel/main/share-panel/index.vue @@ -1,108 +1,71 @@