From a430284aa21e3ae1f0d5654e55b2ad2852519cc2 Mon Sep 17 00:00:00 2001 From: wwf <yearningwang@iqtogether.com> Date: 星期三, 04 六月 2025 15:17:49 +0800 Subject: [PATCH] 初始化 --- service/base.ts | 268 +++++++++++++++++++++++++++++++++++------------------ 1 files changed, 175 insertions(+), 93 deletions(-) diff --git a/service/base.ts b/service/base.ts index 43e65f8..22b1a43 100644 --- a/service/base.ts +++ b/service/base.ts @@ -1,16 +1,12 @@ -import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX, PUBLIC_WEB_PREFIX, WEB_PREFIX } from '@/config' import { refreshAccessTokenOrRelogin } from './refresh-token' +import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config' import Toast from '@/app/components/base/toast' import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type' import type { VisionFile } from '@/types/app' import type { - AgentLogResponse, IterationFinishedResponse, IterationNextResponse, IterationStartedResponse, - LoopFinishedResponse, - LoopNextResponse, - LoopStartedResponse, NodeFinishedResponse, NodeStartedResponse, ParallelBranchFinishedResponse, @@ -21,10 +17,27 @@ WorkflowStartedResponse, } from '@/types/workflow' import { removeAccessToken } from '@/app/components/share/utils' -import type { FetchOptionType, ResponseError } from './fetch' -import { ContentType, base, baseOptions, getAccessToken } from './fetch' import { asyncRunSafe } from '@/utils' const TIME_OUT = 100000 + +const ContentType = { + json: 'application/json', + stream: 'text/event-stream', + audio: 'audio/mpeg', + form: 'application/x-www-form-urlencoded; charset=UTF-8', + download: 'application/octet-stream', // for download + upload: 'multipart/form-data', // for upload +} + +const baseOptions = { + method: 'GET', + mode: 'cors', + credentials: 'include', // always send cookies銆丠TTP Basic authentication. + headers: new Headers({ + 'Content-Type': ContentType.json, + }), + redirect: 'follow', +} export type IOnDataMoreInfo = { conversationId?: string @@ -57,14 +70,9 @@ export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void export type IOnTextReplace = (textReplace: TextReplaceResponse) => void -export type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void -export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void -export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void -export type IOnAgentLog = (agentLog: AgentLogResponse) => void export type IOtherOptions = { isPublicAPI?: boolean - isMarketplaceAPI?: boolean bodyStringify?: boolean needAllResponseContent?: boolean deleteContentType?: boolean @@ -92,10 +100,17 @@ onTTSChunk?: IOnTTSChunk onTTSEnd?: IOnTTSEnd onTextReplace?: IOnTextReplace - onLoopStart?: IOnLoopStarted - onLoopNext?: IOnLoopNext - onLoopFinish?: IOnLoopFinished - onAgentLog?: IOnAgentLog +} + +type ResponseError = { + code: string + message: string + status: number +} + +type FetchOptionType = Omit<RequestInit, 'body'> & { + params?: Record<string, any> + body?: BodyInit | Record<string, any> | null } function unicodeToChar(text: string) { @@ -103,12 +118,30 @@ return '' return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => { - return String.fromCharCode(Number.parseInt(p1, 16)) + return String.fromCharCode(parseInt(p1, 16)) }) } function requiredWebSSOLogin() { - globalThis.location.href = `${PUBLIC_WEB_PREFIX}/webapp-signin?redirect_url=${globalThis.location.pathname}` + globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}` +} + +function getAccessToken(isPublicAPI?: boolean) { + if (isPublicAPI) { + const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] + const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) + let accessTokenJson = { [sharedToken]: '' } + try { + accessTokenJson = JSON.parse(accessToken) + } + catch (e) { + + } + return accessTokenJson[sharedToken] + } + else { + return localStorage.getItem('console_token') || '' + } } export function format(text: string) { @@ -134,9 +167,6 @@ onIterationStart?: IOnIterationStarted, onIterationNext?: IOnIterationNext, onIterationFinish?: IOnIterationFinished, - onLoopStart?: IOnLoopStarted, - onLoopNext?: IOnLoopNext, - onLoopFinish?: IOnLoopFinished, onNodeRetry?: IOnNodeRetry, onParallelBranchStarted?: IOnParallelBranchStarted, onParallelBranchFinished?: IOnParallelBranchFinished, @@ -144,7 +174,6 @@ onTTSChunk?: IOnTTSChunk, onTTSEnd?: IOnTTSEnd, onTextReplace?: IOnTextReplace, - onAgentLog?: IOnAgentLog, ) => { if (!response.ok) throw new Error('Network response was not ok') @@ -169,7 +198,7 @@ try { bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json } - catch { + catch (e) { // mute handle message cut off onData('', isFirstMessage, { conversationId: bufferObj?.conversation_id, @@ -230,15 +259,6 @@ else if (bufferObj.event === 'iteration_completed') { onIterationFinish?.(bufferObj as IterationFinishedResponse) } - else if (bufferObj.event === 'loop_started') { - onLoopStart?.(bufferObj as LoopStartedResponse) - } - else if (bufferObj.event === 'loop_next') { - onLoopNext?.(bufferObj as LoopNextResponse) - } - else if (bufferObj.event === 'loop_completed') { - onLoopFinish?.(bufferObj as LoopFinishedResponse) - } else if (bufferObj.event === 'node_retry') { onNodeRetry?.(bufferObj as NodeFinishedResponse) } @@ -253,9 +273,6 @@ } else if (bufferObj.event === 'text_replace') { onTextReplace?.(bufferObj as TextReplaceResponse) - } - else if (bufferObj.event === 'agent_log') { - onAgentLog?.(bufferObj as AgentLogResponse) } else if (bufferObj.event === 'tts_message') { onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type) @@ -284,11 +301,119 @@ read() } -const baseFetch = base +const baseFetch = <T>( + url: string, + fetchOptions: FetchOptionType, + { + isPublicAPI = false, + bodyStringify = true, + needAllResponseContent, + deleteContentType, + getAbortController, + silent, + }: IOtherOptions, +): Promise<T> => { + const options: typeof baseOptions & FetchOptionType = Object.assign({}, baseOptions, fetchOptions) + if (getAbortController) { + const abortController = new AbortController() + getAbortController(abortController) + options.signal = abortController.signal + } + const accessToken = getAccessToken(isPublicAPI) + options.headers.set('Authorization', `Bearer ${accessToken}`) -export const upload = async (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => { + if (deleteContentType) { + options.headers.delete('Content-Type') + } + else { + const contentType = options.headers.get('Content-Type') + if (!contentType) + options.headers.set('Content-Type', ContentType.json) + } + const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX - const token = await getAccessToken(isPublicAPI) + let urlWithPrefix = (url.startsWith('http://') || url.startsWith('https://')) + ? url + : `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}` + + const { method, params, body } = options + // handle query + if (method === 'GET' && params) { + const paramsArray: string[] = [] + Object.keys(params).forEach(key => + paramsArray.push(`${key}=${encodeURIComponent(params[key])}`), + ) + if (urlWithPrefix.search(/\?/) === -1) + urlWithPrefix += `?${paramsArray.join('&')}` + + else + urlWithPrefix += `&${paramsArray.join('&')}` + + delete options.params + } + + if (body && bodyStringify) + options.body = JSON.stringify(body) + + // Handle timeout + return Promise.race([ + new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('request timeout')) + }, TIME_OUT) + }), + new Promise((resolve, reject) => { + globalThis.fetch(urlWithPrefix, options as RequestInit) + .then((res) => { + const resClone = res.clone() + // Error handler + if (!/^(2|3)\d{2}$/.test(String(res.status))) { + const bodyJson = res.json() + switch (res.status) { + case 401: + return Promise.reject(resClone) + case 403: + bodyJson.then((data: ResponseError) => { + if (!silent) + Toast.notify({ type: 'error', message: data.message }) + if (data.code === 'already_setup') + globalThis.location.href = `${globalThis.location.origin}/signin` + }) + break + // fall through + default: + bodyJson.then((data: ResponseError) => { + if (!silent) + Toast.notify({ type: 'error', message: data.message }) + }) + } + return Promise.reject(resClone) + } + + // handle delete api. Delete api not return content. + if (res.status === 204) { + resolve({ result: 'success' }) + return + } + + // return data + if (options.headers.get('Content-type') === ContentType.download || options.headers.get('Content-type') === ContentType.audio) + resolve(needAllResponseContent ? resClone : res.blob()) + + else resolve(needAllResponseContent ? resClone : res.json()) + }) + .catch((err) => { + if (!silent) + Toast.notify({ type: 'error', message: err }) + reject(err) + }) + }), + ]) as Promise<T> +} + +export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => { + const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX + const token = getAccessToken(isPublicAPI) const defaultOptions = { method: 'POST', url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''), @@ -323,7 +448,7 @@ }) } -export const ssePost = async ( +export const ssePost = ( url: string, fetchOptions: FetchOptionType, otherOptions: IOtherOptions, @@ -350,28 +475,19 @@ onTTSChunk, onTTSEnd, onTextReplace, - onAgentLog, onError, getAbortController, - onLoopStart, - onLoopNext, - onLoopFinish, } = otherOptions const abortController = new AbortController() - - const token = localStorage.getItem('console_token') const options = Object.assign({}, baseOptions, { method: 'POST', signal: abortController.signal, - headers: new Headers({ - Authorization: `Bearer ${token}`, - }), - } as RequestInit, fetchOptions) + }, fetchOptions) - const contentType = (options.headers as Headers).get('Content-Type') + const contentType = options.headers.get('Content-Type') if (!contentType) - (options.headers as Headers).set('Content-Type', ContentType.json) + options.headers.set('Content-Type', ContentType.json) getAbortController?.(abortController) @@ -384,12 +500,12 @@ if (body) options.body = JSON.stringify(body) - const accessToken = await getAccessToken(isPublicAPI) - ; (options.headers as Headers).set('Authorization', `Bearer ${accessToken}`) + const accessToken = getAccessToken(isPublicAPI) + options.headers.set('Authorization', `Bearer ${accessToken}`) globalThis.fetch(urlWithPrefix, options as RequestInit) .then((res) => { - if (!/^[23]\d{2}$/.test(String(res.status))) { + if (!/^(2|3)\d{2}$/.test(String(res.status))) { if (res.status === 401) { refreshAccessTokenOrRelogin(TIME_OUT).then(() => { ssePost(url, fetchOptions, otherOptions) @@ -424,31 +540,7 @@ return } onData?.(str, isFirstMessage, moreInfo) - }, - onCompleted, - onThought, - onMessageEnd, - onMessageReplace, - onFile, - onWorkflowStarted, - onWorkflowFinished, - onNodeStarted, - onNodeFinished, - onIterationStart, - onIterationNext, - onIterationFinish, - onLoopStart, - onLoopNext, - onLoopFinish, - onNodeRetry, - onParallelBranchStarted, - onParallelBranchFinished, - onTextChunk, - onTTSChunk, - onTTSEnd, - onTextReplace, - onAgentLog, - ) + }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace) }).catch((e) => { if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) Toast.notify({ type: 'error', message: e }) @@ -466,7 +558,7 @@ const errResp: Response = err as any if (errResp.status === 401) { const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(errResp.json()) - const loginUrl = `${WEB_PREFIX}/signin` + const loginUrl = `${globalThis.location.origin}/signin` if (parseErr) { globalThis.location.href = loginUrl return Promise.reject(err) @@ -498,11 +590,11 @@ return Promise.reject(err) } if (code === 'not_init_validated' && IS_CE_EDITION) { - globalThis.location.href = `${WEB_PREFIX}/init` + globalThis.location.href = `${globalThis.location.origin}/init` return Promise.reject(err) } if (code === 'not_setup' && IS_CE_EDITION) { - globalThis.location.href = `${WEB_PREFIX}/install` + globalThis.location.href = `${globalThis.location.origin}/install` return Promise.reject(err) } @@ -510,7 +602,7 @@ const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT)) if (refreshErr === null) return baseFetch<T>(url, options, otherOptionsForBaseFetch) - if (!location.pathname.includes('/signin') || !IS_CE_EDITION) { + if (location.pathname !== '/signin' || !IS_CE_EDITION) { globalThis.location.href = loginUrl return Promise.reject(err) } @@ -541,18 +633,8 @@ return get<T>(url, options, { ...otherOptions, isPublicAPI: true }) } -// For Marketplace API -export const getMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { - return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true }) -} - export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions) -} - -// For Marketplace API -export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { - return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true }) } export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => { -- Gitblit v1.8.0