From 252d736f1ff039f133fc5c3be3c0c6ba89198a05 Mon Sep 17 00:00:00 2001 From: wangdan-fit2cloud Date: Tue, 10 Nov 2026 22:06:29 +0800 Subject: [PATCH] feat: voice icon --- ui/README.md | 2 +- ui/src/api/chat/README.md | 15 ++++- ui/src/components.d.ts | 2 + ui/src/components/COMPONENT_README.md | 29 +++++---- .../global/mk-icon/VoiceWaveIcon.vue | 62 +++++++++++++++++++ .../components/message-footer/index.vue | 5 +- .../message-input/voice-recorder/index.ts | 20 +++++- .../message-input/voice-recorder/index.vue | 24 ++++--- ui/src/views/VIEW_README.md | 24 ++++--- 9 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 ui/src/components/global/mk-icon/VoiceWaveIcon.vue diff --git a/ui/README.md b/ui/README.md index 7878be3668d..2b825530839 100644 --- a/ui/README.md +++ b/ui/README.md @@ -34,7 +34,7 @@ Admin 默认访问地址为 `http://localhost:3000/admin/`。在另一个终端 | `VITE_API_TARGET` | 未设置 | 未设置 | 覆盖开发代理的后端地址 | 当前未设置 `VITE_API_TARGET` 时,使用 `vite.config.ts` 中的 `defaultBackendTarget`, -值为 `http://47.120.55.164:38080`。连接本地后端时,在 `env/.env.local` 中配置: +值为 `https://mkv3-ee.fit2cloud.cn/`。连接本地后端时,在 `env/.env.local` 中配置: ```dotenv VITE_API_TARGET=http://127.0.0.1:8080 diff --git a/ui/src/api/chat/README.md b/ui/src/api/chat/README.md index a0105c0e673..16c915cedf4 100644 --- a/ui/src/api/chat/README.md +++ b/ui/src/api/chat/README.md @@ -3,22 +3,29 @@ `core/request.ts` 提供独立的 Axios 客户端、JSON 响应解包和 `postStream` 流式请求, 不复用 Admin 请求客户端。JSON 与流式请求通过 `@/stores/chat` 的 `useStore()` 获取当前认证场景 (门户或单应用)的 token 和语言。 -401 响应(`/auth/` 认证接口除外)不弹出通用提示,调用 `@/router/chat` 的 `reauthenticate()` 重新认证。 +非认证接口返回 401 时不弹出通用提示,调用 `@/router/chat` 的 `reauthenticate()` 重新认证。 流式请求返回原始 `Response`,由对话面板解析,不在请求层维护消息或 loading。 +## 对话 + `conversation.ts` 维护正式对话的打开、发送、取消、续传、历史分页、记录分页、删除、修改、 语音识别接口,默认导出完整 API 对象。调试对话接口位于 `admin/workspace/conversation.ts`,面板模式选择位于 `conversation-panel/common/get-api.ts`。 -Chat 业务接口统一调用后端 v3 路由(`/chat/api/v3/...`);文件上传 `/oss/file` 由 oss 模块提供,不在 v3 下。 +正式对话接口使用后端 `/v3/...` 路由,由请求客户端追加 Chat API 部署前缀; +公开分享读取使用 `/share/`,文件上传使用 `/oss/file`,部分认证使用 `/auth/...`,不统一追加 v3。 通用错误提示使用响应的 `message`;响应为 HTML 时按 HTTP 状态显示通用文案。 -接口命名、类型和请求约定统一遵循 `../API_README.md`。 +接口命名、类型和请求约定统一遵循 [API_README.md](../API_README.md)。 + +## 文件上传 `file.ts` 提供通用 `postUploadFile`,通过本应用的 `core/request.ts` 中 `postUpload` 支持 可选进度回调及取消,统一返回 `{ request, abort }`。`request` 解包得到文件地址; 取消仍拒绝 Promise,但不弹出通用错误提示。loading 由调用方管理。 +## 认证与用户 + `auth.ts` 维护门户与单应用的认证:`getApplicationAuthProfile(accessToken)` 请求 `/v3/profile`, `getPortalAuthProfile()` 请求 `/v3/portal/profile`,两者返回结构相同的 `AuthProfile`,差异字段在 `meta`。 主题配置位于响应顶层 `custom_theme`,不在 `meta` 内;Chat Auth Store 获取或复用该配置后, @@ -36,3 +43,5 @@ Chat 业务接口统一调用后端 v3 路由(`/chat/api/v3/...`);文件 `getDingTalkOauthCallback` 使用 `/auth/dingtalk/oauth2`,传入 `code` 和可选的 `accessToken`。 两者返回 `ChatLoginResponse`,由认证 Store 按场景保存 token;接口需部署端提供对应认证能力。 `/auth/` 与 `/v3/auth/` 下的认证请求返回 401 时直接提示错误,不触发自动重新认证。 + +公开分享与回答反馈的路径、载荷和只读边界统一见主 API 文档的[公开对话分享](../API_README.md#公开对话分享)与[对话回答反馈](../API_README.md#对话回答反馈),页面交互见 View 文档。 diff --git a/ui/src/components.d.ts b/ui/src/components.d.ts index 8c06ac46455..1329df77057 100644 --- a/ui/src/components.d.ts +++ b/ui/src/components.d.ts @@ -59,6 +59,7 @@ declare module 'vue' { TableFilterSingle: typeof import('./components/global/mk-table/mk-table-filter/TableFilterSingle.vue')['default'] ToolIcon: typeof import('./components/global/mk-icon/ToolIcon.vue')['default'] TriggerIcon: typeof import('./components/global/mk-icon/TriggerIcon.vue')['default'] + VoiceWaveIcon: typeof import('./components/global/mk-icon/VoiceWaveIcon.vue')['default'] } } @@ -111,4 +112,5 @@ declare global { const TableFilterSingle: typeof import('./components/global/mk-table/mk-table-filter/TableFilterSingle.vue')['default'] const ToolIcon: typeof import('./components/global/mk-icon/ToolIcon.vue')['default'] const TriggerIcon: typeof import('./components/global/mk-icon/TriggerIcon.vue')['default'] + const VoiceWaveIcon: typeof import('./components/global/mk-icon/VoiceWaveIcon.vue')['default'] } \ No newline at end of file diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index caef7b965b7..adee339d2d0 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -218,6 +218,11 @@ Symbol ID 变化时同步引用。 `--el-loading-spinner-size`。数字按 px 处理,字符串需携带 CSS 单位,例如 `` 或 ``。 +`VoiceWaveIcon` 位于 `global/mk-icon/VoiceWaveIcon.vue`,模板中自动注册,使用纯 CSS 持续循环播放 +五根音柱的缩放动画,不依赖录音或播放状态。`size` 默认 `24`,数字按 px 处理,字符串需携带 +CSS 单位;`color` 默认继承文字颜色,也可通过 `text-primary` 设置主题色,例如 +``。系统启用减少动态效果时显示静态音柱。 + ### MkInfiniteScroll 分页列表的滚动触底加载组件,使用 `IntersectionObserver` 监听组件所在滚动区域的底部,不依赖 @@ -975,6 +980,19 @@ Workspace API 内部使用 `getWorkspaceId()`,System 共享 API 不接收工 聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。 `message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表 自行展示流式圆点,开场白放入 `prepend` 插槽,日志空状态放入 `empty` 插槽。日志列表不注入发送能力。 +消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, +直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 +`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 +Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 + +### Question(对话模块) + +`conversation-panel/components/content/items/question/index.vue` 负责提问展示、附件和操作入口。 +同目录 `QuestionEditor.vue` 接收 `content`、`disabled`,维护本次编辑草稿,通过 `cancel` 和 +`send(question)` 通知入口组件;进入时挂载并聚焦,退出时卸载。入口组件负责发送能力与附件传递。 + +### 分享选择(对话模块) + 正式对话的 `message-list` 根据会话 Store 的 `isShared` 展示分享选择态,同轮提问与回答同步选择, 内容只读且选中时显示背景;无记录 ID 的问答不可选。列表底部提供当前已加载轮次的全选、已选数量、 取消及创建链接,创建时按消息顺序提交记录 ID;成功退出分享,失败保留选择。分享期间主面板隐藏输入框, @@ -991,17 +1009,6 @@ Chat、Portal 视图创建并 provide `SHARE_LINK_KEY`,组件通过 `useShareL `ShareMessageRow` 通过插槽承载消息,`ShareFooter` 展示全选、数量、取消与创建链接并在卸载时退出分享态; 两者只绑定 Store 状态与方法,不维护逻辑。`message-list/index.vue` 只组合组件并传入 Store 提供的记录 ID。 -消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, -直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 -`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 -Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 - -### Question(对话模块) - -`conversation-panel/components/content/items/question/index.vue` 负责提问展示、附件和操作入口。 -同目录 `QuestionEditor.vue` 接收 `content`、`disabled`,维护本次编辑草稿,通过 `cancel` 和 -`send(question)` 通知入口组件;进入时挂载并聚焦,退出时卸载。入口组件负责发送能力与附件传递。 - ### SharePanel(对话模块) `conversation-panel/main/share-panel/index.vue` 是公开分享页的主面板,展示标题栏并组合 diff --git a/ui/src/components/global/mk-icon/VoiceWaveIcon.vue b/ui/src/components/global/mk-icon/VoiceWaveIcon.vue new file mode 100644 index 00000000000..078b32262ca --- /dev/null +++ b/ui/src/components/global/mk-icon/VoiceWaveIcon.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/ui/src/conversation-panel/components/message-footer/index.vue b/ui/src/conversation-panel/components/message-footer/index.vue index fb0f40a5250..c8c7afab922 100644 --- a/ui/src/conversation-panel/components/message-footer/index.vue +++ b/ui/src/conversation-panel/components/message-footer/index.vue @@ -9,7 +9,7 @@ import { datetimeFormat } from '@/utils/time' import type { ChatMessage, ChatRecordMeta } from '../../core/types' import { useExecutionDetailStore } from '../../right-sidebar/execution-detail/index' -import { MESSAGE_LIST_KEY, useMessageListStore } from '../message-list/index' +import { MESSAGE_LIST_KEY } from '../message-list/index' import { useShareLinkStoreOptional } from '../message-list/share/index' import { useShowShare } from '../../view/index' @@ -173,7 +173,8 @@ function openFeedback(status: '0' | '1') { - + + diff --git a/ui/src/conversation-panel/components/message-input/voice-recorder/index.ts b/ui/src/conversation-panel/components/message-input/voice-recorder/index.ts index 16c0c9dac85..47d89aab86a 100644 --- a/ui/src/conversation-panel/components/message-input/voice-recorder/index.ts +++ b/ui/src/conversation-panel/components/message-input/voice-recorder/index.ts @@ -7,7 +7,7 @@ import { MsgError } from '@/utils/message' /** 单次录音最长秒数,到时自动结束并转写。 */ export const VOICE_RECORD_MAX_SECONDS = 60 /** 录音波形柱数量。 */ -const WAVE_BAR_COUNT = 60 +const WAVE_BAR_COUNT = 80 // 关闭 recorder-core 的控制台日志 Recorder.CLog = () => {} @@ -50,8 +50,22 @@ export function useVoiceRecorder(onRecorded: (audio: Blob) => void) { type: 'mp3', bitRate: 128, sampleRate: 16000, - onProcess: (_buffers: unknown, powerLevel: number) => { - waveLevels.value = [...waveLevels.value.slice(1), powerLevel] + onProcess: (buffers: Int16Array[]) => { + // 分段计算最新音频的能量,整排随当前声音刷新,静音时收回点状基线。 + const samples = buffers[buffers.length - 1] + waveLevels.value = Array.from({ length: WAVE_BAR_COUNT }, (_, index) => { + if (!samples?.length) return 0 + const start = Math.floor((index * samples.length) / WAVE_BAR_COUNT) + const end = Math.floor(((index + 1) * samples.length) / WAVE_BAR_COUNT) + if (end === start) return 0 + let energy = 0 + for (let offset = start; offset < end; offset++) { + const amplitude = samples[offset]! / 32768 + energy += amplitude * amplitude + } + const volume = Math.sqrt(energy / (end - start)) + return Math.min(100, Math.max(0, volume - 0.005) * 500) + }) }, }) recorder = instance diff --git a/ui/src/conversation-panel/components/message-input/voice-recorder/index.vue b/ui/src/conversation-panel/components/message-input/voice-recorder/index.vue index 32644f04a1f..a27d08a5126 100644 --- a/ui/src/conversation-panel/components/message-input/voice-recorder/index.vue +++ b/ui/src/conversation-panel/components/message-input/voice-recorder/index.vue @@ -13,8 +13,13 @@ const emit = defineEmits<{ stop: [] }>() -/** 波形柱最大高度(px),与波形容器 h-8 一致。 */ -const WAVE_MAX_HEIGHT = 32 +/** 波形在固定高度容器内围绕中线起伏。 */ +const WAVE_MAX_HEIGHT = 24 +const WAVE_MIN_HEIGHT = 2 + +const waveHeights = computed(() => + props.waveLevels.map((level) => WAVE_MIN_HEIGHT + (Math.max(0, Math.min(100, level)) / 100) * (WAVE_MAX_HEIGHT - WAVE_MIN_HEIGHT)), +) const recordTime = computed(() => { const minutes = String(Math.floor(props.seconds / 60)).padStart(2, '0') @@ -24,14 +29,13 @@ const recordTime = computed(() => {