Skip to content

Commit c8bcd73

Browse files
feat: conversation api
1 parent 430f62e commit c8bcd73

24 files changed

Lines changed: 366 additions & 254 deletions

File tree

‎ui/AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ ui/
6565
│ │ ├── global/ # Frequently used components auto-registered by Vite
6666
│ │ │ └── mk-icon/ # Unified SVG Symbol and Element Plus icon component
6767
│ │ └── <component-name>/ # Less-frequent shared components imported explicitly
68+
│ ├── conversation-panel/ # Conversation UI, local state, streaming, and API mode selection
6869
│ ├── constants/ # Cross-feature constants grouped by domain
6970
│ │ └── CONSTANT_README.md # Shared constant placement and naming rules
7071
│ ├── layout/ # Shared application shells, headers, sidebars, types, and helpers
@@ -117,7 +118,7 @@ ui/
117118
```
118119

119120
Internationalization is not yet integrated; `src/locales/` has not been created. Chat currently has a
120-
placeholder route page and a reserved independent API directory. Add files to the matching feature
121+
placeholder route page and an independent API client with conversation endpoints. Add files to the matching feature
121122
directory instead of creating parallel top-level structures.
122123

123124
Structural responsibilities:

‎ui/src/api/API_README.md‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# API 目录说明
22

3-
`src/api` 负责前端与服务端之间的通信,按照应用入口隔离 Admin 与 Chat 请求体系。当前只实现
4-
Admin API;Chat 目录作为独立体系预留。
3+
`src/api` 负责前端与服务端之间的通信,按照应用入口隔离 Admin 与 Chat 请求体系。
4+
Admin 与 Chat 分别维护请求客户端和业务接口。
55

66
```text
77
src/api/
88
├── constants.ts # Admin 与 Chat 的 API base 路径常量
99
├── admin/
10+
│ ├── file.ts # 通用文件上传、进度与取消
1011
│ ├── auth/ # Admin 登录认证与当前用户接口
1112
│ │ └── types.ts # 认证 API 与认证 Store 共用类型
1213
│ ├── core/ # Admin 请求基础能力
@@ -18,15 +19,20 @@ src/api/
1819
│ │ ├── shared-resources/ # System 共享资源接口
1920
│ │ └── <resource>.ts # 其他 System 单资源接口
2021
│ ├── workspace/ # 工作空间业务接口
22+
│ │ ├── conversation.ts # 调试对话、历史会话与语音接口
2123
│ │ ├── application/ # 智能体接口
2224
│ │ ├── knowledge/ # 知识库接口
2325
│ │ ├── model/ # 模型接口
2426
│ │ ├── trigger/ # 触发器查询及维护接口
2527
│ │ ├── tool/ # 工具、工具工作流及工具商店接口
2628
│ │ └── <resource>.ts # 工作空间公共资源接口
2729
│ └── provider.ts # Workspace 与 System 共用的模型供应商接口
28-
├── chat/ # Chat 独立请求体系,当前预留
29-
│ └── README.md # Chat API 边界与后续实现说明
30+
├── chat/ # Chat 独立请求体系
31+
│ ├── core/request.ts # JSON 与流式请求
32+
│ ├── core/types.ts # Chat 请求协议类型
33+
│ ├── file.ts # Chat 文件上传、进度与取消
34+
│ ├── conversation.ts # 正式对话、历史会话与语音接口
35+
│ └── README.md # Chat API 边界说明
3036
├── enums/ # 后端固定枚举值
3137
│ ├── index.ts # API 枚举值的唯一导入入口
3238
│ └── <domain>.ts # 按明确业务域拆分的枚举值
@@ -342,3 +348,31 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用
342348
`getRanking` 与 `exportRanking` 通过 `HomeRankingKind` 选择后端排行路径,名称及起止日期
343349
筛选保持一致;分页使用 `ParamsPage` 与 `ResponsePage`。导出沿用 `getExportFile`。
344350
工作空间总量接口返回数值,不与 System 首页的对象响应混用。
351+
352+
### 对话面板接口
353+
354+
`admin/workspace/conversation.ts` 集中维护调试对话的打开、发送、取消、续传、历史会话、
355+
记录分页、删除、修改和语音识别接口,保留可选 `applicationId` 对历史资源范围的选择。
356+
其中 `postSpeechToText(applicationId, data)` 请求指定智能体的
357+
`/workspace/<workspaceId>/application/<applicationId>/speech_to_text`,loading 由调用方管理。
358+
`chat/conversation.ts` 维护正式对话对应的接口。两者使用各自 `core/request.ts` 的请求方法;
359+
`postStream` 返回原始 `Response`,由 `conversation-panel/stream.ts` 解析。
360+
361+
面板内部的 `conversation-panel/common/get-api.ts` 通过 `ChatType` 选择完整 API 对象,
362+
仅负责模式判断,不声明 URL 或发送请求;固定模式的 Store 直接导入对应业务 API。
363+
面板模式值统一维护在 `conversation-panel/common/enums.ts` 的 `CHAT_TYPE`,
364+
`common/types.ts` 中的 `ChatType` 从该对象派生。
365+
366+
### 通用文件上传
367+
368+
`admin/file.ts` 与 `chat/file.ts` 分别提供 `postUploadFile(file, sourceId, sourceType, onProgress?)`,
369+
通过各自请求客户端向 `/oss/file` 提交 FormData 的 `file`、`source_id` 和 `source_type`。
370+
返回值统一为 `{ request, abort }`,`request` 解包得到文件地址,`abort()` 中断客户端请求。
371+
普通上传直接等待 `request`,需要进度时传入 `(percent, event)`;只有能获取上传总量时才回调
372+
0–100 的百分比,100 表示请求体已上传,不代表服务端处理成功,完成状态以 `request` 为准。
373+
取消时 Promise 仍拒绝,由调用方处理状态,请求层不弹出通用错误提示;loading 由调用方在
374+
`finally` 中恢复。
375+
376+
资源类型使用 `@/api/enums` 的 `FILE_SOURCE_TYPE` 与 `@/api/types` 的 `FileSourceType`,
377+
包括知识库、智能体、工具、文档、对话及三种临时文件有效期。对话 Store 使用
378+
`FILE_SOURCE_TYPE.CHAT` 调用对应 File API,对话 API 不再维护上传接口。

‎ui/src/api/admin/core/request.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** 提供 Admin API 的 Axios 实例与常用 HTTP 请求封装。 */
22

3-
import axios, { AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
3+
import axios, { AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type AxiosProgressEvent, type InternalAxiosRequestConfig } from 'axios'
44
import router from '@/router/admin'
55
import { useStore } from '@/stores'
66
import type { ApiResponse } from './types'
@@ -109,6 +109,10 @@ request.interceptors.response.use(
109109
return response
110110
},
111111
async (error: unknown) => {
112+
if (axios.isCancel(error)) {
113+
return Promise.reject(error)
114+
}
115+
112116
if (!axios.isAxiosError<ApiResponse<unknown>>(error)) {
113117
return Promise.reject(error)
114118
}
@@ -214,4 +218,22 @@ export function del<TData = unknown, T = unknown>(url: string, params?: Dict<unk
214218
return promise<T>(request.delete<ApiResponse<T>>(url, { params, data, timeout }))
215219
}
216220

221+
/** 上传文件,支持进度回调与取消,响应统一解包。 */
222+
export function postUpload<T = unknown>(url: string, data: FormData, onProgress?: (percent: number, event: AxiosProgressEvent) => void) {
223+
const controller = new AbortController()
224+
const uploadRequest = promise<T>(
225+
request.post<ApiResponse<T>>(url, data, {
226+
signal: controller.signal,
227+
onUploadProgress: onProgress
228+
? (event) => {
229+
if (event.total && event.total > 0) {
230+
onProgress(Math.min(100, Math.max(0, Math.round((event.loaded / event.total) * 100))), event)
231+
}
232+
}
233+
: undefined,
234+
}),
235+
)
236+
return { request: uploadRequest, abort: () => controller.abort() }
237+
}
238+
217239
export default request

‎ui/src/api/admin/file.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { AxiosProgressEvent } from 'axios'
2+
import { postUpload } from './core/request'
3+
import type { FileSourceType } from '@/api/types'
4+
5+
/** 上传资源文件,支持可选的进度回调与取消操作。 */
6+
const postUploadFile = (
7+
file: File,
8+
sourceId: string,
9+
sourceType: FileSourceType,
10+
onProgress?: (percent: number, event: AxiosProgressEvent) => void,
11+
) => {
12+
const formData = new FormData()
13+
formData.append('file', file)
14+
formData.append('source_id', sourceId)
15+
formData.append('source_type', sourceType)
16+
return postUpload<string>('/oss/file', formData, onProgress)
17+
}
18+
19+
export default { postUploadFile }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { get, post, put, del, postStream } from '../core/request'
2+
import { getWorkspaceId } from '@/utils/resource-context'
3+
import { ADMIN_API_BASE_PATH as adminApiBase } from '@/api/constants'
4+
5+
/** 打开对话。 */
6+
const getConversationOpen = (applicationId: string) => get(`/workspace/${getWorkspaceId()}/application/${applicationId}/open`)
7+
8+
/** 发送对话消息并返回原始流式响应。 */
9+
const postConversationMessage = (chatId: string, data: unknown, applicationId?: string) =>
10+
postStream(adminApiBase, `/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_message`, data)
11+
12+
/** 取消对话消息生成。 */
13+
const postCancelConversationMessage = (chatId: string, applicationId?: string) =>
14+
post(`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/cancel_chat_message`, {})
15+
16+
/** 恢复对话消息流。 */
17+
const postResumeConversationMessage = (chatId: string, chatRecordId: string, applicationId?: string) =>
18+
postStream(
19+
adminApiBase,
20+
`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_record/${chatRecordId}/resume_chat_message`,
21+
)
22+
23+
/** 获取历史会话分页。 */
24+
const getConversationPage = (page: number, size: number, applicationId?: string) => {
25+
const wsId = getWorkspaceId()
26+
if (applicationId) {
27+
return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${page}/${size}`)
28+
}
29+
return get(`/workspace/${wsId}/historical_conversation/${page}/${size}`)
30+
}
31+
32+
/** 获取会话记录分页。 */
33+
const getConversationRecordPage = (chatId: string, page: number, size: number, applicationId?: string) => {
34+
const wsId = getWorkspaceId()
35+
if (applicationId) {
36+
return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation_record/${chatId}/${page}/${size}`)
37+
}
38+
return get(`/workspace/${wsId}/historical_conversation_record/${chatId}/${page}/${size}`)
39+
}
40+
41+
/** 删除会话。 */
42+
const deleteConversation = (chatId: string, applicationId?: string) => {
43+
const wsId = getWorkspaceId()
44+
if (applicationId) {
45+
return del(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`)
46+
}
47+
return del(`/workspace/${wsId}/historical_conversation/${chatId}`)
48+
}
49+
50+
/** 修改会话信息。 */
51+
const putConversation = (chatId: string, data: unknown, applicationId?: string) => {
52+
const wsId = getWorkspaceId()
53+
if (applicationId) {
54+
return put(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`, data)
55+
}
56+
return put(`/workspace/${wsId}/historical_conversation/${chatId}`, data)
57+
}
58+
59+
/** 使用指定智能体将语音转换为文字。 */
60+
const postSpeechToText = (applicationId: string, data: unknown) =>
61+
post(`/workspace/${getWorkspaceId()}/application/${applicationId}/speech_to_text`, data)
62+
63+
export default {
64+
getConversationOpen,
65+
postConversationMessage,
66+
postCancelConversationMessage,
67+
postResumeConversationMessage,
68+
getConversationPage,
69+
getConversationRecordPage,
70+
deleteConversation,
71+
putConversation,
72+
postSpeechToText,
73+
}

‎ui/src/api/chat/README.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# Chat API
22

3-
该目录预留给 Chat 应用的独立请求体系。Chat API 不复用 Admin 请求客户端;开始开发时,
4-
在本目录内单独规划请求基础能力和业务接口。
3+
`core/request.ts` 提供独立的 Axios 客户端、JSON 响应解包和 `postStream` 流式请求,
4+
不复用 Admin 请求客户端。当前 JSON 与流式请求均通过 `useStore()` 获取已有 token 和语言。
5+
流式请求返回原始 `Response`,由对话面板解析,不在请求层维护消息或 loading。
6+
7+
`conversation.ts` 维护正式对话的打开、发送、取消、续传、历史分页、记录分页、删除、修改、
8+
语音识别接口,默认导出完整 API 对象。调试对话接口位于
9+
`admin/workspace/conversation.ts`,面板模式选择位于 `conversation-panel/common/get-api.ts`。
10+
11+
接口命名、类型和请求约定统一遵循 `../API_README.md`。
12+
13+
`file.ts` 提供通用 `postUploadFile`,通过本应用的 `core/request.ts` 中 `postUpload` 支持
14+
可选进度回调及取消,统一返回 `{ request, abort }`。`request` 解包得到文件地址;
15+
取消仍拒绝 Promise,但不弹出通用错误提示。loading 由调用方管理。

‎ui/src/api/chat/conversation.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { get, post, put, del, postStream } from './core/request'
2+
import { CHAT_API_BASE_PATH as chatApiBase } from '@/api/constants'
3+
4+
/** 打开对话。 */
5+
const getConversationOpen = () => get('/open')
6+
7+
/** 发送对话消息并返回原始流式响应。 */
8+
const postConversationMessage = (chatId: string, data: unknown) => postStream(chatApiBase, `/chat_message/${chatId}`, data)
9+
10+
/** 取消对话消息生成。 */
11+
const postCancelConversationMessage = (chatId: string) => post(`/chat_message/${chatId}/cancel`, {})
12+
13+
/** 恢复对话消息流。 */
14+
const postResumeConversationMessage = (chatId: string, chatRecordId: string) =>
15+
postStream(chatApiBase, `/chat_message/${chatId}/resume/${chatRecordId}`)
16+
17+
/** 获取历史会话分页。 */
18+
const getConversationPage = (page: number, size: number) => get(`/historical_conversation/${page}/${size}`)
19+
20+
/** 获取会话记录分页。 */
21+
const getConversationRecordPage = (chatId: string, page: number, size: number) => get(`/historical_conversation_record/${chatId}/${page}/${size}`)
22+
23+
/** 删除会话。 */
24+
const deleteConversation = (chatId: string) => del(`/historical_conversation/${chatId}`)
25+
26+
/** 修改会话信息。 */
27+
const putConversation = (chatId: string, data: unknown) => put(`/historical_conversation/${chatId}`, data)
28+
29+
/** 将语音转换为文字。 */
30+
const postSpeechToText = (data: unknown) => post('/speech_to_text', data)
31+
32+
export default {
33+
getConversationOpen,
34+
postConversationMessage,
35+
postCancelConversationMessage,
36+
postResumeConversationMessage,
37+
getConversationPage,
38+
getConversationRecordPage,
39+
deleteConversation,
40+
putConversation,
41+
postSpeechToText,
42+
}

‎ui/src/api/chat/core/request.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** 提供 Chat API 的 Axios 实例与常用 HTTP 请求封装。 */
22

3-
import axios, { AxiosHeaders, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
3+
import axios, { AxiosHeaders, type AxiosResponse, type AxiosProgressEvent, type InternalAxiosRequestConfig } from 'axios'
44
import { useStore } from '@/stores'
55
import type { ApiResponse } from './types'
66
import type { Dict } from '@/api/types'
@@ -55,6 +55,10 @@ request.interceptors.response.use(
5555
return response
5656
},
5757
async (error: unknown) => {
58+
if (axios.isCancel(error)) {
59+
return Promise.reject(error)
60+
}
61+
5862
if (!axios.isAxiosError<ApiResponse<unknown>>(error)) {
5963
return Promise.reject(error)
6064
}
@@ -93,4 +97,39 @@ export function del<TData = unknown, T = unknown>(url: string, params?: Dict<unk
9397
return promise<T>(request.delete<ApiResponse<T>>(url, { params, data, timeout }))
9498
}
9599

100+
/** 发送流式 POST 请求,返回原始 `Response` 供 SSE 读取。 */
101+
export function postStream(base: string, path: string, data?: unknown) {
102+
const { auth, user } = useStore()
103+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
104+
if (auth.token) {
105+
headers['Authorization'] = `Bearer ${auth.token}`
106+
}
107+
if (user.language) {
108+
headers['Accept-Language'] = user.language
109+
}
110+
return fetch(`${base}${path.startsWith('/') ? path : `/${path}`}`, {
111+
method: 'POST',
112+
headers,
113+
body: data === undefined ? undefined : JSON.stringify(data),
114+
})
115+
}
116+
117+
/** 上传文件,支持进度回调与取消,响应统一解包。 */
118+
export function postUpload<T = unknown>(url: string, data: FormData, onProgress?: (percent: number, event: AxiosProgressEvent) => void) {
119+
const controller = new AbortController()
120+
const uploadRequest = promise<T>(
121+
request.post<ApiResponse<T>>(url, data, {
122+
signal: controller.signal,
123+
onUploadProgress: onProgress
124+
? (event) => {
125+
if (event.total && event.total > 0) {
126+
onProgress(Math.min(100, Math.max(0, Math.round((event.loaded / event.total) * 100))), event)
127+
}
128+
}
129+
: undefined,
130+
}),
131+
)
132+
return { request: uploadRequest, abort: () => controller.abort() }
133+
}
134+
96135
export default request

‎ui/src/api/chat/file.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { AxiosProgressEvent } from 'axios'
2+
import { postUpload } from './core/request'
3+
import type { FileSourceType } from '@/api/types'
4+
5+
/** 上传资源文件,支持可选的进度回调与取消操作。 */
6+
const postUploadFile = (
7+
file: File,
8+
sourceId: string,
9+
sourceType: FileSourceType,
10+
onProgress?: (percent: number, event: AxiosProgressEvent) => void,
11+
) => {
12+
const formData = new FormData()
13+
formData.append('file', file)
14+
formData.append('source_id', sourceId)
15+
formData.append('source_type', sourceType)
16+
return postUpload<string>('/oss/file', formData, onProgress)
17+
}
18+
19+
export default { postUploadFile }

‎ui/src/api/enums/file.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** 文件上传的资源归属及临时文件有效期。 */
2+
export const FILE_SOURCE_TYPE = {
3+
KNOWLEDGE: 'KNOWLEDGE',
4+
APPLICATION: 'APPLICATION',
5+
TOOL: 'TOOL',
6+
DOCUMENT: 'DOCUMENT',
7+
CHAT: 'CHAT',
8+
TEMPORARY_30_MINUTE: 'TEMPORARY_30_MINUTE',
9+
TEMPORARY_120_MINUTE: 'TEMPORARY_120_MINUTE',
10+
TEMPORARY_1_DAY: 'TEMPORARY_1_DAY',
11+
} as const

0 commit comments

Comments
 (0)