Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ ui/
│ │ ├── global/ # Frequently used components auto-registered by Vite
│ │ │ └── mk-icon/ # Unified SVG Symbol and Element Plus icon component
│ │ └── <component-name>/ # Less-frequent shared components imported explicitly
│ ├── conversation-panel/ # Conversation UI, local state, streaming, and API mode selection
│ ├── constants/ # Cross-feature constants grouped by domain
│ │ └── CONSTANT_README.md # Shared constant placement and naming rules
│ ├── layout/ # Shared application shells, headers, sidebars, types, and helpers
Expand Down Expand Up @@ -117,7 +118,7 @@ ui/
```

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

Structural responsibilities:
Expand Down
42 changes: 38 additions & 4 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# API 目录说明

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

```text
src/api/
├── constants.ts # Admin 与 Chat 的 API base 路径常量
├── admin/
│ ├── file.ts # 通用文件上传、进度与取消
│ ├── auth/ # Admin 登录认证与当前用户接口
│ │ └── types.ts # 认证 API 与认证 Store 共用类型
│ ├── core/ # Admin 请求基础能力
Expand All @@ -18,15 +19,20 @@ src/api/
│ │ ├── shared-resources/ # System 共享资源接口
│ │ └── <resource>.ts # 其他 System 单资源接口
│ ├── workspace/ # 工作空间业务接口
│ │ ├── conversation.ts # 调试对话、历史会话与语音接口
│ │ ├── application/ # 智能体接口
│ │ ├── knowledge/ # 知识库接口
│ │ ├── model/ # 模型接口
│ │ ├── trigger/ # 触发器查询及维护接口
│ │ ├── tool/ # 工具、工具工作流及工具商店接口
│ │ └── <resource>.ts # 工作空间公共资源接口
│ └── provider.ts # Workspace 与 System 共用的模型供应商接口
├── chat/ # Chat 独立请求体系,当前预留
│ └── README.md # Chat API 边界与后续实现说明
├── chat/ # Chat 独立请求体系
│ ├── core/request.ts # JSON 与流式请求
│ ├── core/types.ts # Chat 请求协议类型
│ ├── file.ts # Chat 文件上传、进度与取消
│ ├── conversation.ts # 正式对话、历史会话与语音接口
│ └── README.md # Chat API 边界说明
├── enums/ # 后端固定枚举值
│ ├── index.ts # API 枚举值的唯一导入入口
│ └── <domain>.ts # 按明确业务域拆分的枚举值
Expand Down Expand Up @@ -342,3 +348,31 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用
`getRanking` 与 `exportRanking` 通过 `HomeRankingKind` 选择后端排行路径,名称及起止日期
筛选保持一致;分页使用 `ParamsPage` 与 `ResponsePage`。导出沿用 `getExportFile`。
工作空间总量接口返回数值,不与 System 首页的对象响应混用。

### 对话面板接口

`admin/workspace/conversation.ts` 集中维护调试对话的打开、发送、取消、续传、历史会话、
记录分页、删除、修改和语音识别接口,保留可选 `applicationId` 对历史资源范围的选择。
其中 `postSpeechToText(applicationId, data)` 请求指定智能体的
`/workspace/<workspaceId>/application/<applicationId>/speech_to_text`,loading 由调用方管理。
`chat/conversation.ts` 维护正式对话对应的接口。两者使用各自 `core/request.ts` 的请求方法;
`postStream` 返回原始 `Response`,由 `conversation-panel/stream.ts` 解析。

面板内部的 `conversation-panel/common/get-api.ts` 通过 `ChatType` 选择完整 API 对象,
仅负责模式判断,不声明 URL 或发送请求;固定模式的 Store 直接导入对应业务 API。
面板模式值统一维护在 `conversation-panel/common/enums.ts` 的 `CHAT_TYPE`,
`common/types.ts` 中的 `ChatType` 从该对象派生。

### 通用文件上传

`admin/file.ts` 与 `chat/file.ts` 分别提供 `postUploadFile(file, sourceId, sourceType, onProgress?)`,
通过各自请求客户端向 `/oss/file` 提交 FormData 的 `file`、`source_id` 和 `source_type`。
返回值统一为 `{ request, abort }`,`request` 解包得到文件地址,`abort()` 中断客户端请求。
普通上传直接等待 `request`,需要进度时传入 `(percent, event)`;只有能获取上传总量时才回调
0–100 的百分比,100 表示请求体已上传,不代表服务端处理成功,完成状态以 `request` 为准。
取消时 Promise 仍拒绝,由调用方处理状态,请求层不弹出通用错误提示;loading 由调用方在
`finally` 中恢复。

资源类型使用 `@/api/enums` 的 `FILE_SOURCE_TYPE` 与 `@/api/types` 的 `FileSourceType`,
包括知识库、智能体、工具、文档、对话及三种临时文件有效期。对话 Store 使用
`FILE_SOURCE_TYPE.CHAT` 调用对应 File API,对话 API 不再维护上传接口。
24 changes: 23 additions & 1 deletion ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** 提供 Admin API 的 Axios 实例与常用 HTTP 请求封装。 */

import axios, { AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
import axios, { AxiosHeaders, type AxiosRequestConfig, type AxiosResponse, type AxiosProgressEvent, type InternalAxiosRequestConfig } from 'axios'
import router from '@/router/admin'
import { useStore } from '@/stores'
import type { ApiResponse } from './types'
Expand Down Expand Up @@ -109,6 +109,10 @@ request.interceptors.response.use(
return response
},
async (error: unknown) => {
if (axios.isCancel(error)) {
return Promise.reject(error)
}

if (!axios.isAxiosError<ApiResponse<unknown>>(error)) {
return Promise.reject(error)
}
Expand Down Expand Up @@ -214,4 +218,22 @@ export function del<TData = unknown, T = unknown>(url: string, params?: Dict<unk
return promise<T>(request.delete<ApiResponse<T>>(url, { params, data, timeout }))
}

/** 上传文件,支持进度回调与取消,响应统一解包。 */
export function postUpload<T = unknown>(url: string, data: FormData, onProgress?: (percent: number, event: AxiosProgressEvent) => void) {
const controller = new AbortController()
const uploadRequest = promise<T>(
request.post<ApiResponse<T>>(url, data, {
signal: controller.signal,
onUploadProgress: onProgress
? (event) => {
if (event.total && event.total > 0) {
onProgress(Math.min(100, Math.max(0, Math.round((event.loaded / event.total) * 100))), event)
}
}
: undefined,
}),
)
return { request: uploadRequest, abort: () => controller.abort() }
}

export default request
19 changes: 19 additions & 0 deletions ui/src/api/admin/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AxiosProgressEvent } from 'axios'
import { postUpload } from './core/request'
import type { FileSourceType } from '@/api/types'

/** 上传资源文件,支持可选的进度回调与取消操作。 */
const postUploadFile = (
file: File,
sourceId: string,
sourceType: FileSourceType,
onProgress?: (percent: number, event: AxiosProgressEvent) => void,
) => {
const formData = new FormData()
formData.append('file', file)
formData.append('source_id', sourceId)
formData.append('source_type', sourceType)
return postUpload<string>('/oss/file', formData, onProgress)
}

export default { postUploadFile }
73 changes: 73 additions & 0 deletions ui/src/api/admin/workspace/conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { get, post, put, del, postStream } from '../core/request'
import { getWorkspaceId } from '@/utils/resource-context'
import { ADMIN_API_BASE_PATH as adminApiBase } from '@/api/constants'

/** 打开对话。 */
const getConversationOpen = (applicationId: string) => get(`/workspace/${getWorkspaceId()}/application/${applicationId}/open`)

/** 发送对话消息并返回原始流式响应。 */
const postConversationMessage = (chatId: string, data: unknown, applicationId?: string) =>
postStream(adminApiBase, `/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_message`, data)

/** 取消对话消息生成。 */
const postCancelConversationMessage = (chatId: string, applicationId?: string) =>
post(`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/cancel_chat_message`, {})

/** 恢复对话消息流。 */
const postResumeConversationMessage = (chatId: string, chatRecordId: string, applicationId?: string) =>
postStream(
adminApiBase,
`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_record/${chatRecordId}/resume_chat_message`,
)

/** 获取历史会话分页。 */
const getConversationPage = (page: number, size: number, applicationId?: string) => {
const wsId = getWorkspaceId()
if (applicationId) {
return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${page}/${size}`)
}
return get(`/workspace/${wsId}/historical_conversation/${page}/${size}`)
}

/** 获取会话记录分页。 */
const getConversationRecordPage = (chatId: string, page: number, size: number, applicationId?: string) => {
const wsId = getWorkspaceId()
if (applicationId) {
return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation_record/${chatId}/${page}/${size}`)
}
return get(`/workspace/${wsId}/historical_conversation_record/${chatId}/${page}/${size}`)
}

/** 删除会话。 */
const deleteConversation = (chatId: string, applicationId?: string) => {
const wsId = getWorkspaceId()
if (applicationId) {
return del(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`)
}
return del(`/workspace/${wsId}/historical_conversation/${chatId}`)
}

/** 修改会话信息。 */
const putConversation = (chatId: string, data: unknown, applicationId?: string) => {
const wsId = getWorkspaceId()
if (applicationId) {
return put(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`, data)
}
return put(`/workspace/${wsId}/historical_conversation/${chatId}`, data)
}

/** 使用指定智能体将语音转换为文字。 */
const postSpeechToText = (applicationId: string, data: unknown) =>
post(`/workspace/${getWorkspaceId()}/application/${applicationId}/speech_to_text`, data)

export default {
getConversationOpen,
postConversationMessage,
postCancelConversationMessage,
postResumeConversationMessage,
getConversationPage,
getConversationRecordPage,
deleteConversation,
putConversation,
postSpeechToText,
}
15 changes: 13 additions & 2 deletions ui/src/api/chat/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# Chat API

该目录预留给 Chat 应用的独立请求体系。Chat API 不复用 Admin 请求客户端;开始开发时,
在本目录内单独规划请求基础能力和业务接口。
`core/request.ts` 提供独立的 Axios 客户端、JSON 响应解包和 `postStream` 流式请求,
不复用 Admin 请求客户端。当前 JSON 与流式请求均通过 `useStore()` 获取已有 token 和语言。
流式请求返回原始 `Response`,由对话面板解析,不在请求层维护消息或 loading。

`conversation.ts` 维护正式对话的打开、发送、取消、续传、历史分页、记录分页、删除、修改、
语音识别接口,默认导出完整 API 对象。调试对话接口位于
`admin/workspace/conversation.ts`,面板模式选择位于 `conversation-panel/common/get-api.ts`。

接口命名、类型和请求约定统一遵循 `../API_README.md`。

`file.ts` 提供通用 `postUploadFile`,通过本应用的 `core/request.ts` 中 `postUpload` 支持
可选进度回调及取消,统一返回 `{ request, abort }`。`request` 解包得到文件地址;
取消仍拒绝 Promise,但不弹出通用错误提示。loading 由调用方管理。
42 changes: 42 additions & 0 deletions ui/src/api/chat/conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { get, post, put, del, postStream } from './core/request'
import { CHAT_API_BASE_PATH as chatApiBase } from '@/api/constants'

/** 打开对话。 */
const getConversationOpen = () => get('/open')

/** 发送对话消息并返回原始流式响应。 */
const postConversationMessage = (chatId: string, data: unknown) => postStream(chatApiBase, `/chat_message/${chatId}`, data)

/** 取消对话消息生成。 */
const postCancelConversationMessage = (chatId: string) => post(`/chat_message/${chatId}/cancel`, {})

/** 恢复对话消息流。 */
const postResumeConversationMessage = (chatId: string, chatRecordId: string) =>
postStream(chatApiBase, `/chat_message/${chatId}/resume/${chatRecordId}`)

/** 获取历史会话分页。 */
const getConversationPage = (page: number, size: number) => get(`/historical_conversation/${page}/${size}`)

/** 获取会话记录分页。 */
const getConversationRecordPage = (chatId: string, page: number, size: number) => get(`/historical_conversation_record/${chatId}/${page}/${size}`)

/** 删除会话。 */
const deleteConversation = (chatId: string) => del(`/historical_conversation/${chatId}`)

/** 修改会话信息。 */
const putConversation = (chatId: string, data: unknown) => put(`/historical_conversation/${chatId}`, data)

/** 将语音转换为文字。 */
const postSpeechToText = (data: unknown) => post('/speech_to_text', data)

export default {
getConversationOpen,
postConversationMessage,
postCancelConversationMessage,
postResumeConversationMessage,
getConversationPage,
getConversationRecordPage,
deleteConversation,
putConversation,
postSpeechToText,
}
41 changes: 40 additions & 1 deletion ui/src/api/chat/core/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** 提供 Chat API 的 Axios 实例与常用 HTTP 请求封装。 */

import axios, { AxiosHeaders, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
import axios, { AxiosHeaders, type AxiosResponse, type AxiosProgressEvent, type InternalAxiosRequestConfig } from 'axios'
import { useStore } from '@/stores'
import type { ApiResponse } from './types'
import type { Dict } from '@/api/types'
Expand Down Expand Up @@ -55,6 +55,10 @@ request.interceptors.response.use(
return response
},
async (error: unknown) => {
if (axios.isCancel(error)) {
return Promise.reject(error)
}

if (!axios.isAxiosError<ApiResponse<unknown>>(error)) {
return Promise.reject(error)
}
Expand Down Expand Up @@ -93,4 +97,39 @@ export function del<TData = unknown, T = unknown>(url: string, params?: Dict<unk
return promise<T>(request.delete<ApiResponse<T>>(url, { params, data, timeout }))
}

/** 发送流式 POST 请求,返回原始 `Response` 供 SSE 读取。 */
export function postStream(base: string, path: string, data?: unknown) {
const { auth, user } = useStore()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (auth.token) {
headers['Authorization'] = `Bearer ${auth.token}`
}
if (user.language) {
headers['Accept-Language'] = user.language
}
return fetch(`${base}${path.startsWith('/') ? path : `/${path}`}`, {
method: 'POST',
headers,
body: data === undefined ? undefined : JSON.stringify(data),
})
}

/** 上传文件,支持进度回调与取消,响应统一解包。 */
export function postUpload<T = unknown>(url: string, data: FormData, onProgress?: (percent: number, event: AxiosProgressEvent) => void) {
const controller = new AbortController()
const uploadRequest = promise<T>(
request.post<ApiResponse<T>>(url, data, {
signal: controller.signal,
onUploadProgress: onProgress
? (event) => {
if (event.total && event.total > 0) {
onProgress(Math.min(100, Math.max(0, Math.round((event.loaded / event.total) * 100))), event)
}
}
: undefined,
}),
)
return { request: uploadRequest, abort: () => controller.abort() }
}

export default request
19 changes: 19 additions & 0 deletions ui/src/api/chat/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AxiosProgressEvent } from 'axios'
import { postUpload } from './core/request'
import type { FileSourceType } from '@/api/types'

/** 上传资源文件,支持可选的进度回调与取消操作。 */
const postUploadFile = (
file: File,
sourceId: string,
sourceType: FileSourceType,
onProgress?: (percent: number, event: AxiosProgressEvent) => void,
) => {
const formData = new FormData()
formData.append('file', file)
formData.append('source_id', sourceId)
formData.append('source_type', sourceType)
return postUpload<string>('/oss/file', formData, onProgress)
}

export default { postUploadFile }
11 changes: 11 additions & 0 deletions ui/src/api/enums/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** 文件上传的资源归属及临时文件有效期。 */
export const FILE_SOURCE_TYPE = {
KNOWLEDGE: 'KNOWLEDGE',
APPLICATION: 'APPLICATION',
TOOL: 'TOOL',
DOCUMENT: 'DOCUMENT',
CHAT: 'CHAT',
TEMPORARY_30_MINUTE: 'TEMPORARY_30_MINUTE',
TEMPORARY_120_MINUTE: 'TEMPORARY_120_MINUTE',
TEMPORARY_1_DAY: 'TEMPORARY_1_DAY',
} as const
1 change: 1 addition & 0 deletions ui/src/api/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './resource-authorization'
export * from './tool'
export * from './knowledge'
export * from './trigger'
export * from './file'
3 changes: 3 additions & 0 deletions ui/src/api/types/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { FILE_SOURCE_TYPE } from '@/api/enums'

export type FileSourceType = (typeof FILE_SOURCE_TYPE)[keyof typeof FILE_SOURCE_TYPE]
1 change: 1 addition & 0 deletions ui/src/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export type * from './workflow-version'
export type * from './workflow-template'

export type * from './homepage'
export type * from './file'
Loading
Loading