diff --git a/ui/src/styles/element-plus.scss b/ui/src/styles/element-plus.scss
index e3d02fe399c..7c5d13842c2 100644
--- a/ui/src/styles/element-plus.scss
+++ b/ui/src/styles/element-plus.scss
@@ -127,10 +127,6 @@
min-width: 80px;
}
- &--large {
- font-size: 16px;
- padding: 11px;
- }
&.is-round {
padding: 7px 11px;
}
diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md
index 6a326d6d7e4..a5b5ff12bf6 100644
--- a/ui/src/views/VIEW_README.md
+++ b/ui/src/views/VIEW_README.md
@@ -524,6 +524,19 @@ System 资源管理的修正内容与添加知识库选项仅包含当前智能
分组超过 5 条才显示“查看更多”,剩余数量取分组总数;“查看更多”打开历史对话抽屉,
支持切换智能体、摘要搜索、滚动分页及打开/删除会话,删除后同步刷新侧栏。
+Chat 标题栏的新建对话按钮独立展示,不参与操作计数和收纳。分享、导出及默认插槽操作由功能本地 `header/header-actions/index.vue` 统一展示:移动端且操作超过一个时全部收进更多菜单,
+其余情况直接显示图标按钮,禁用操作仍计数。移动端通过现有 `useDeviceType()` 判断,收纳条件通过 `collapse` 传入,容器不依赖设备或侧栏 Store。Chat/Portal 标题栏默认插槽直接传入 `MkAction` 或接收 `display` 的操作组件,与内置操作
+一起计数;显示条件写在操作节点上,容器统一覆盖 `display`。支持 Fragment,不检查业务组件内部的显示条件。
+
+### 对话导出
+
+Chat/Portal 标题栏通过 `header/header-actions/export/ExportAction.vue` 导出当前已加载消息,不自动补查历史,
+无消息、加载或生成中禁用。桌面为图标格式菜单,移动端收纳后为“更多 → 导出 → 格式”。
+Markdown/HTML 从消息数据生成,跳过思考与工具参数;HTML 下载前清洗,HTML 与图表内容项以代码保留。
+PDF/图片读取所属主面板的消息区生成预览,排除消息操作栏;沙箱 iframe 通过已有 snapshot 协议截图,
+失败提示重试,不静默导出空白内容。PDF 逐页裁切下载,预览支持 PNG 下载。
+导出逻辑与截图依赖按需加载;更多菜单保留已挂载操作,避免关闭菜单时销毁预览。
+
### 对话分享
单应用历史列表、门户分组历史列表与历史抽屉支持先激活所选会话、等待消息加载后进入分享;
diff --git a/ui/tests/conversation-export.test.mjs b/ui/tests/conversation-export.test.mjs
new file mode 100644
index 00000000000..78d3a96f12c
--- /dev/null
+++ b/ui/tests/conversation-export.test.mjs
@@ -0,0 +1,119 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { test } from 'node:test'
+import { compileFunction } from 'node:vm'
+import ts from 'typescript'
+import * as Vue from 'vue'
+
+function loadExport(messages) {
+ const source = readFileSync(
+ new URL('../src/conversation-panel/components/header/header-actions/export/use-conversation-export.ts', import.meta.url),
+ 'utf8',
+ )
+ const { outputText } = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } })
+ const store = { messages: Vue.ref(messages), loading: Vue.ref(false), streamLoading: Vue.ref(false) }
+ const downloads = []
+ const blobs = []
+ const errors = []
+ const dependencies = {
+ vue: Vue,
+ dompurify: { sanitize: (html) => html },
+ '../../../message-list/index': { useMessageListStore: () => store },
+ '../../../../left-sidebar/conversation-list/index': {
+ useConversationListStore: () => ({ currentConversation: Vue.ref({ abstract: '测试/对话' }), currentChatId: Vue.ref('chat-id') }),
+ },
+ '@/utils/message': { MsgError: (message) => errors.push(message) },
+ }
+ const module = { exports: {} }
+ compileFunction(outputText, ['require', 'exports', 'module', 'document', 'URL', 'setTimeout'])(
+ (id) => dependencies[id],
+ module.exports,
+ module,
+ {
+ createElement: () => {
+ const link = { click: () => downloads.push(link.download) }
+ return link
+ },
+ },
+ {
+ createObjectURL: (blob) => {
+ blobs.push(blob)
+ return 'blob:test'
+ },
+ revokeObjectURL() {},
+ },
+ (callback) => callback(),
+ )
+ return { ...module.exports.useConversationExport(), store, downloads, blobs, errors }
+}
+
+test('Markdown 保留已加载问答、嵌套正文和附件,跳过思考及工具参数', async () => {
+ const exporter = loadExport([
+ { role: 'USER', content: [{ type: 'QUESTION', content: '问题', image_list: [{ name: '图片', url: 'https://example.com/image.png' }] }] },
+ {
+ role: 'ASSISTANT',
+ content: [
+ { type: 'REASONING', content: '隐藏思考' },
+ { type: 'TOOL', arguments: '隐藏参数' },
+ { type: 'GROUP', children: [{ type: 'TEXT', content: '**回答**' }] },
+ { type: 'HTML', content: '页面
' },
+ ],
+ },
+ ])
+ await exporter.exportText('markdown')
+ const markdown = await exporter.blobs[0].text()
+ assert.match(markdown, /## 用户\n\n问题/)
+ assert.match(markdown, /!\[图片\]\(\)/)
+ assert.match(markdown, /## 助手\n\n\*\*回答\*\*/)
+ assert.match(markdown, /```html\n页面<\/p>\n```/)
+ assert.doesNotMatch(markdown, /隐藏思考|隐藏参数/)
+ assert.deepEqual(exporter.downloads, ['测试_对话.md'])
+ assert.equal(exporter.loading.value, false)
+})
+
+test('无消息、加载历史或生成中均不发起导出', async () => {
+ const exporter = loadExport([])
+ await exporter.exportText('markdown')
+ exporter.store.messages.value = [{ role: 'USER', content: [{ type: 'QUESTION', content: '问题' }] }]
+ exporter.store.loading.value = true
+ await exporter.exportText('markdown')
+ exporter.store.loading.value = false
+ exporter.store.streamLoading.value = true
+ await exporter.exportText('markdown')
+ assert.equal(exporter.downloads.length, 0)
+ assert.equal(exporter.disabled.value, true)
+})
+
+test('操作收纳仅在移动端超过一个时启用,Fragment 中的外部操作一起计数', () => {
+ const source = readFileSync(new URL('../src/conversation-panel/components/header/header-actions/index.vue', import.meta.url), 'utf8').match(
+ /