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
7 changes: 7 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,25 @@
},
"dependencies": {
"@antv/layout": "^2.0.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lint": "^6.9.7",
"@codemirror/state": "^6.7.1",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.9",
"@element-plus/icons-vue": "^2.3.2",
"@he-tree/vue": "^2.10.5",
"@logicflow/core": "^2.2.5",
"@logicflow/extension": "^2.3.1",
"axios": "^1.19.0",
"codemirror": "^6.0.2",
"dingtalk-jsapi": "^3.2.9",
"element-plus": "^2.14.2",
"jsencrypt": "^3.5.4",
"nanoid": "^6.0.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"vue": "^3.5.38",
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.10",
"vue-router": "^5.1.0"
},
Expand Down
6 changes: 4 additions & 2 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
`putRole` 和 `deleteKnowledge`。
- 前缀与实际请求方法保持一致:查询使用 `get`,创建和业务动作使用 `post`,完整更新使用
`put`,删除使用 `delete`。局部更新接口真实采用 PATCH 时使用 `patch`。
- 文件导出是直接触发浏览器下载的业务动作,使用 `exportXxx` 命名,例如 `exportTool`。
- HTTP 方法前缀后必须带有明确的业务名称,不导出 `get`、`post`、`list`、`detail`、`login`
或 `logout` 等缺少请求方式或业务含义的名称。
- 函数名不追加 `Api` 后缀,所属业务域由目录和文件名表达。
Expand All @@ -104,9 +105,10 @@ API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文
- Admin Router 与请求客户端直接读取 `window.MaxKB` 运行时路径配置;`Window` 和
`MaxKBRuntimeConfig` 的全局类型统一声明在根目录 `env.d.ts`。
- Admin 普通 JSON 请求使用 Axios;`request.ts` 导出 Axios 实例以及 `promise`、`get`、
`post`、`put``del` 请求封装。
`post`、`put``del` 请求封装。
- 正常 JSON 接口返回 `Promise<T>`,请求层负责解包后端 `{ code, message, data }` 响应。
- 文件下载接口使用 `postBlob` 获取原始 `Blob`,由调用页面负责命名并触发浏览器下载。
- GET 文件导出使用 `getExportFile`,由请求层统一获取 Blob、解析 `Content-Disposition` 文件名
并触发浏览器下载;业务 API 只需传入默认文件名、接口地址和可选请求参数。
- 业务代码通过 `api.method().then(...)` 处理接口成功后的状态变化;通用接口错误由请求层统一
提示,不在调用处重复使用 `try/catch` 或 `.catch()` 提示相同错误。只有业务降级、状态恢复等
非提示类失败处理可以按需保留失败分支。
Expand Down
119 changes: 100 additions & 19 deletions ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/** 提供 Admin API 的 Axios 实例与常用 HTTP 请求封装。 */

import axios, { AxiosHeaders, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
import axios, {
AxiosHeaders,
type AxiosRequestConfig,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from 'axios'
import router from '@/router/admin'
import { useStore } from '@/stores'
import type { ApiResponse, LoadingTarget } from './types'
Expand All @@ -10,6 +15,10 @@ import { MsgError } from '@/utils/message'
const DEFAULT_TIMEOUT = 30 * 60 * 1_000 // 30 minutes
const ADMIN_BASE_PATH = window.MaxKB?.prefix || import.meta.env.VITE_BASE_PATH || '/admin/'

interface ExportRequestConfig extends AxiosRequestConfig {
skipGlobalErrorMessage?: boolean
}

function setRequestHeaders(config: InternalAxiosRequestConfig) {
const { auth, user } = useStore()

Expand Down Expand Up @@ -48,6 +57,47 @@ function finishLoading(loading?: LoadingTarget) {
loading.value = false
}

function extractFilename(contentDisposition?: string) {
if (!contentDisposition) {
return undefined
}

const encodedName = contentDisposition.match(/filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i)?.[1]
const plainName = contentDisposition.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i)
const responseName = encodedName || plainName?.[1] || plainName?.[2]
if (!responseName) {
return undefined
}

const normalizedName = responseName.trim().replace(/^['"]|['"]$/g, '')
try {
return decodeURIComponent(normalizedName)
} catch {
return normalizedName
}
}

async function getResponseErrorMessage(error: unknown) {
if (!axios.isAxiosError<ApiResponse<unknown> | Blob | string>(error)) {
return undefined
}

const responseData = error.response?.data
if (responseData instanceof Blob) {
const text = await responseData.text()
try {
const data = JSON.parse(text) as Partial<ApiResponse<unknown>>
return data.message || text
} catch {
return text
}
}
if (typeof responseData === 'string') {
return responseData
}
return responseData?.message
}

export const request = axios.create({
baseURL: `${ADMIN_BASE_PATH.replace(/\/+$/, '')}/api`,
timeout: DEFAULT_TIMEOUT,
Expand All @@ -68,13 +118,16 @@ request.interceptors.response.use(
}
return response
},
(error: unknown) => {
async (error: unknown) => {
if (!axios.isAxiosError<ApiResponse<unknown>>(error)) {
return Promise.reject(error)
}

const requestUrl = error.config?.url ?? ''
const status = error.response?.status
const responseMessage = await getResponseErrorMessage(error)
const skipGlobalErrorMessage = (error.config as ExportRequestConfig | undefined)
?.skipGlobalErrorMessage

if (error.code === 'ECONNABORTED') {
MsgError(error.message)
Expand All @@ -89,10 +142,14 @@ request.interceptors.response.use(
router.push({ name: 'login' })
}
if (status === 403) {
MsgError(error.response?.data.message || 'No permission to access')
MsgError(responseMessage || 'No permission to access')
}
if (error.code !== 'ECONNABORTED' && ![401, 403, 404].includes(status ?? 0)) {
MsgError(error.response?.data.message || error.message)
if (
error.code !== 'ECONNABORTED' &&
![401, 403, 404].includes(status ?? 0) &&
!skipGlobalErrorMessage
) {
MsgError(responseMessage || error.message)
}

return Promise.reject(error)
Expand Down Expand Up @@ -125,6 +182,44 @@ export function get<T = unknown>(
return promise<T>(request.get<ApiResponse<T>>(url, { params, timeout }), loading)
}

/** 发送 GET 请求并将 Blob 响应下载为文件。 */
export async function getExportFile(
fileName: string,
url: string,
params?: RequestParams,
loading?: LoadingTarget,
): Promise<boolean> {
startLoading(loading)
try {
const response = await request.get<Blob>(url, {
params,
responseType: 'blob',
skipGlobalErrorMessage: true,
} as ExportRequestConfig)

if (response.data.type.includes('application/json')) {
const text = await response.data.text()
try {
const data = JSON.parse(text) as Partial<ApiResponse<unknown>>
MsgError(data.message || text)
} catch {
MsgError(text)
}
throw new Error('Response is not a valid file')
}

const blob = new Blob([response.data], { type: 'application/octet-stream' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = extractFilename(response.headers['content-disposition']) || fileName
link.click()
URL.revokeObjectURL(link.href)
return true
} finally {
finishLoading(loading)
}
}

/** 发送 POST 请求。 */
export function post<TData = unknown, T = unknown>(
url: string,
Expand All @@ -136,20 +231,6 @@ export function post<TData = unknown, T = unknown>(
return promise<T>(request.post<ApiResponse<T>>(url, data, { params, timeout }), loading)
}

/** 发送返回文件 Blob 的 POST 请求。 */
export async function postBlob<TData = unknown>(
url: string,
data?: TData,
loading?: LoadingTarget,
) {
startLoading(loading)
try {
const response = await request.post<Blob>(url, data, { responseType: 'blob' })
return response.data
} finally {
finishLoading(loading)
}
}

/** 发送 PUT 请求。 */
export function put<TData = unknown, T = unknown>(
Expand Down
17 changes: 0 additions & 17 deletions ui/src/api/admin/workspace/model/model-shared.ts

This file was deleted.

24 changes: 24 additions & 0 deletions ui/src/api/admin/workspace/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { get } from '../core/request'
import type { ParamsPage, ResponsePage } from '../core/types'
import type { RequestParams, ModelItem, ToolItem } from '@/api/types'
import { getWorkspaceId } from '@/utils/workspace-context'

const getPrefix = () => {
const workspaceId = getWorkspaceId()
return `/system/shared/workspace/${workspaceId}`
}

/** 获取工作空间共享的模型列表。 */
const getModelList = (query?: RequestParams) => {
return get<ModelItem[]>(`${getPrefix()}/model`, query)
}

/** 获取工作空间共享的工具列表。 */
const getToolPage = (page: ParamsPage, query?: RequestParams) => {
return get<ResponsePage<ToolItem>>(`${getPrefix()}/tool/${page.currentPage}/${page.pageSize}`, query)
}

export default {
getModelList,
getToolPage,
}
59 changes: 47 additions & 12 deletions ui/src/api/admin/workspace/tool/tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { del, get, post, put } from '../../core/request'
import { del, getExportFile, get, post, put } from '../../core/request'
import type { ParamsPage, ResponsePage } from '../../core/types'
import type { RequestParams, ToolPayload, ToolItem } from '@/api/types'
import type {
RequestParams,
ToolDebugPayload,
ToolItem,
ToolPayload,
ToolPylintIssue,
} from '@/api/types'
import { getWorkspaceId } from '@/utils/workspace-context'

const getPrefix = () => {
Expand All @@ -10,30 +16,55 @@ const getPrefix = () => {

/** 获取工具分页列表。 */
const getToolPage = (page: ParamsPage, query?: RequestParams) => {
return get<ResponsePage<ToolItem>>(
`${getPrefix()}/${page.currentPage}/${page.pageSize}`,
query,
)
return get<ResponsePage<ToolItem>>(`${getPrefix()}/${page.currentPage}/${page.pageSize}`, query)
}

/** 更新工作空间工具。 */
const putTool = (toolId: string, payload: ToolPayload) => {
return put<ToolPayload, ToolItem>(`${getPrefix()}/${toolId}`, payload)
/** 删除工作空间工具。 */
const deleteTool = (toolId: string) => {
return del<undefined, boolean>(`${getPrefix()}/${toolId}`)
}

/** 创建工作空间工具。 */
const postTool = (payload: ToolPayload) => {
return post<ToolPayload, ToolItem>(getPrefix(), payload)
}

/** 更新工作空间工具。 */
const putTool = (toolId: string, payload: ToolPayload) => {
return put<ToolPayload, ToolItem>(`${getPrefix()}/${toolId}`, payload)
}

/** 获取工具详情。 */
const getToolDetail = (toolId: string) => {
return get<ToolItem>(`${getPrefix()}/${toolId}`)
}

/** 删除工作空间工具。 */
const deleteTool = (toolId: string) => {
return del<undefined, boolean>(`${getPrefix()}/${toolId}`)
/** 导入工具文件并创建工作空间工具。 */
const postToolImport = (file: File, folderId: string) => {
const payload = new FormData()
payload.append('file', file)
payload.append('folder_id', folderId)
return post<FormData, ToolItem>(`${getPrefix()}/import`, payload)
}

/** 导出工作空间工具文件。 */
const exportTool = (toolId: string, toolName: string) => {
return getExportFile(`${toolName}.tool`, `${getPrefix()}/${toolId}/export`)
}

/** 检查工作空间工具的 Python 代码。 */
const postToolPylint = (code: string) => {
return post<{ code: string }, ToolPylintIssue[]>(`${getPrefix()}/pylint`, { code })
}

// const generateCode = (data: any) => {
// const p = (window.MaxKB?.prefix ? window.MaxKB?.prefix : '/admin') + '/api'
// return postStream(`${p}${getPrefix()}/generate_code`, data)
// }

/** 调试自定义工具代码并返回运行结果。 */
const postToolDebug = (payload: ToolDebugPayload) => {
return post<ToolDebugPayload, unknown>(`${getPrefix()}/debug`, payload)
}

/** 测试工具配置是否可连接。 */
Expand All @@ -58,10 +89,14 @@ const putBatchMoveTools = (toolIds: string[], folderId: string) => {

export default {
getToolPage,
exportTool,
deleteTool,
getToolDetail,

postTool,
postToolDebug,
postToolImport,
postToolPylint,
postToolTestConnection,
putBatchDeleteTools,
putBatchMoveTools,
Expand Down
8 changes: 8 additions & 0 deletions ui/src/api/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ export interface OptionItem<Value extends boolean | number | string = string | n
value: Value
[key: string]: unknown
}


export interface ExportError {
response: {
status: number
data: Blob
}
}
Loading
Loading