base.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
  2. import { refreshAccessTokenOrRelogin } from './refresh-token'
  3. import Toast from '@/app/components/base/toast'
  4. import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type'
  5. import type { VisionFile } from '@/types/app'
  6. import type {
  7. AgentLogResponse,
  8. IterationFinishedResponse,
  9. IterationNextResponse,
  10. IterationStartedResponse,
  11. NodeFinishedResponse,
  12. NodeStartedResponse,
  13. ParallelBranchFinishedResponse,
  14. ParallelBranchStartedResponse,
  15. TextChunkResponse,
  16. TextReplaceResponse,
  17. WorkflowFinishedResponse,
  18. WorkflowStartedResponse,
  19. } from '@/types/workflow'
  20. import { removeAccessToken } from '@/app/components/share/utils'
  21. import type { FetchOptionType, ResponseError } from './fetch'
  22. import { ContentType, base, baseOptions, getAccessToken } from './fetch'
  23. import { asyncRunSafe } from '@/utils'
  24. const TIME_OUT = 100000
  25. export type IOnDataMoreInfo = {
  26. conversationId?: string
  27. taskId?: string
  28. messageId: string
  29. errorMessage?: string
  30. errorCode?: string
  31. }
  32. export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
  33. export type IOnThought = (though: ThoughtItem) => void
  34. export type IOnFile = (file: VisionFile) => void
  35. export type IOnMessageEnd = (messageEnd: MessageEnd) => void
  36. export type IOnMessageReplace = (messageReplace: MessageReplace) => void
  37. export type IOnAnnotationReply = (messageReplace: AnnotationReply) => void
  38. export type IOnCompleted = (hasError?: boolean, errorMessage?: string) => void
  39. export type IOnError = (msg: string, code?: string) => void
  40. export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void
  41. export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void
  42. export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
  43. export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
  44. export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void
  45. export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void
  46. export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void
  47. export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void
  48. export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void
  49. export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void
  50. export type IOnTextChunk = (textChunk: TextChunkResponse) => void
  51. export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void
  52. export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void
  53. export type IOnTextReplace = (textReplace: TextReplaceResponse) => void
  54. export type IOnAgentLog = (agentLog: AgentLogResponse) => void
  55. export type IOtherOptions = {
  56. isPublicAPI?: boolean
  57. isMarketplaceAPI?: boolean
  58. bodyStringify?: boolean
  59. needAllResponseContent?: boolean
  60. deleteContentType?: boolean
  61. silent?: boolean
  62. onData?: IOnData // for stream
  63. onThought?: IOnThought
  64. onFile?: IOnFile
  65. onMessageEnd?: IOnMessageEnd
  66. onMessageReplace?: IOnMessageReplace
  67. onError?: IOnError
  68. onCompleted?: IOnCompleted // for stream
  69. getAbortController?: (abortController: AbortController) => void
  70. onWorkflowStarted?: IOnWorkflowStarted
  71. onWorkflowFinished?: IOnWorkflowFinished
  72. onNodeStarted?: IOnNodeStarted
  73. onNodeFinished?: IOnNodeFinished
  74. onIterationStart?: IOnIterationStarted
  75. onIterationNext?: IOnIterationNext
  76. onIterationFinish?: IOnIterationFinished
  77. onNodeRetry?: IOnNodeRetry
  78. onParallelBranchStarted?: IOnParallelBranchStarted
  79. onParallelBranchFinished?: IOnParallelBranchFinished
  80. onTextChunk?: IOnTextChunk
  81. onTTSChunk?: IOnTTSChunk
  82. onTTSEnd?: IOnTTSEnd
  83. onTextReplace?: IOnTextReplace
  84. onAgentLog?: IOnAgentLog
  85. }
  86. function unicodeToChar(text: string) {
  87. if (!text)
  88. return ''
  89. return text.replace(/\\u[0-9a-f]{4}/g, (_match, p1) => {
  90. return String.fromCharCode(Number.parseInt(p1, 16))
  91. })
  92. }
  93. function requiredWebSSOLogin() {
  94. globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}`
  95. }
  96. export function format(text: string) {
  97. let res = text.trim()
  98. if (res.startsWith('\n'))
  99. res = res.replace('\n', '')
  100. return res.replaceAll('\n', '<br/>').replaceAll('```', '')
  101. }
  102. const handleStream = (
  103. response: Response,
  104. onData: IOnData,
  105. onCompleted?: IOnCompleted,
  106. onThought?: IOnThought,
  107. onMessageEnd?: IOnMessageEnd,
  108. onMessageReplace?: IOnMessageReplace,
  109. onFile?: IOnFile,
  110. onWorkflowStarted?: IOnWorkflowStarted,
  111. onWorkflowFinished?: IOnWorkflowFinished,
  112. onNodeStarted?: IOnNodeStarted,
  113. onNodeFinished?: IOnNodeFinished,
  114. onIterationStart?: IOnIterationStarted,
  115. onIterationNext?: IOnIterationNext,
  116. onIterationFinish?: IOnIterationFinished,
  117. onNodeRetry?: IOnNodeRetry,
  118. onParallelBranchStarted?: IOnParallelBranchStarted,
  119. onParallelBranchFinished?: IOnParallelBranchFinished,
  120. onTextChunk?: IOnTextChunk,
  121. onTTSChunk?: IOnTTSChunk,
  122. onTTSEnd?: IOnTTSEnd,
  123. onTextReplace?: IOnTextReplace,
  124. onAgentLog?: IOnAgentLog,
  125. ) => {
  126. if (!response.ok)
  127. throw new Error('Network response was not ok')
  128. const reader = response.body?.getReader()
  129. const decoder = new TextDecoder('utf-8')
  130. let buffer = ''
  131. let bufferObj: Record<string, any>
  132. let isFirstMessage = true
  133. function read() {
  134. let hasError = false
  135. reader?.read().then((result: any) => {
  136. if (result.done) {
  137. onCompleted && onCompleted()
  138. return
  139. }
  140. buffer += decoder.decode(result.value, { stream: true })
  141. const lines = buffer.split('\n')
  142. try {
  143. lines.forEach((message) => {
  144. if (message.startsWith('data: ')) { // check if it starts with data:
  145. try {
  146. bufferObj = JSON.parse(message.substring(6)) as Record<string, any>// remove data: and parse as json
  147. }
  148. catch (e) {
  149. // mute handle message cut off
  150. onData('', isFirstMessage, {
  151. conversationId: bufferObj?.conversation_id,
  152. messageId: bufferObj?.message_id,
  153. })
  154. return
  155. }
  156. if (bufferObj.status === 400 || !bufferObj.event) {
  157. onData('', false, {
  158. conversationId: undefined,
  159. messageId: '',
  160. errorMessage: bufferObj?.message,
  161. errorCode: bufferObj?.code,
  162. })
  163. hasError = true
  164. onCompleted?.(true, bufferObj?.message)
  165. return
  166. }
  167. if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
  168. // can not use format here. Because message is splitted.
  169. onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
  170. conversationId: bufferObj.conversation_id,
  171. taskId: bufferObj.task_id,
  172. messageId: bufferObj.id,
  173. })
  174. isFirstMessage = false
  175. }
  176. else if (bufferObj.event === 'agent_thought') {
  177. onThought?.(bufferObj as ThoughtItem)
  178. }
  179. else if (bufferObj.event === 'message_file') {
  180. onFile?.(bufferObj as VisionFile)
  181. }
  182. else if (bufferObj.event === 'message_end') {
  183. onMessageEnd?.(bufferObj as MessageEnd)
  184. }
  185. else if (bufferObj.event === 'message_replace') {
  186. onMessageReplace?.(bufferObj as MessageReplace)
  187. }
  188. else if (bufferObj.event === 'workflow_started') {
  189. onWorkflowStarted?.(bufferObj as WorkflowStartedResponse)
  190. }
  191. else if (bufferObj.event === 'workflow_finished') {
  192. onWorkflowFinished?.(bufferObj as WorkflowFinishedResponse)
  193. }
  194. else if (bufferObj.event === 'node_started') {
  195. onNodeStarted?.(bufferObj as NodeStartedResponse)
  196. }
  197. else if (bufferObj.event === 'node_finished') {
  198. onNodeFinished?.(bufferObj as NodeFinishedResponse)
  199. }
  200. else if (bufferObj.event === 'iteration_started') {
  201. onIterationStart?.(bufferObj as IterationStartedResponse)
  202. }
  203. else if (bufferObj.event === 'iteration_next') {
  204. onIterationNext?.(bufferObj as IterationNextResponse)
  205. }
  206. else if (bufferObj.event === 'iteration_completed') {
  207. onIterationFinish?.(bufferObj as IterationFinishedResponse)
  208. }
  209. else if (bufferObj.event === 'node_retry') {
  210. onNodeRetry?.(bufferObj as NodeFinishedResponse)
  211. }
  212. else if (bufferObj.event === 'parallel_branch_started') {
  213. onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse)
  214. }
  215. else if (bufferObj.event === 'parallel_branch_finished') {
  216. onParallelBranchFinished?.(bufferObj as ParallelBranchFinishedResponse)
  217. }
  218. else if (bufferObj.event === 'text_chunk') {
  219. onTextChunk?.(bufferObj as TextChunkResponse)
  220. }
  221. else if (bufferObj.event === 'text_replace') {
  222. onTextReplace?.(bufferObj as TextReplaceResponse)
  223. }
  224. else if (bufferObj.event === 'agent_log') {
  225. onAgentLog?.(bufferObj as AgentLogResponse)
  226. }
  227. else if (bufferObj.event === 'tts_message') {
  228. onTTSChunk?.(bufferObj.message_id, bufferObj.audio, bufferObj.audio_type)
  229. }
  230. else if (bufferObj.event === 'tts_message_end') {
  231. onTTSEnd?.(bufferObj.message_id, bufferObj.audio)
  232. }
  233. }
  234. })
  235. buffer = lines[lines.length - 1]
  236. }
  237. catch (e) {
  238. onData('', false, {
  239. conversationId: undefined,
  240. messageId: '',
  241. errorMessage: `${e}`,
  242. })
  243. hasError = true
  244. onCompleted?.(true, e as string)
  245. return
  246. }
  247. if (!hasError)
  248. read()
  249. })
  250. }
  251. read()
  252. }
  253. const baseFetch = base
  254. export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<any> => {
  255. const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
  256. const token = getAccessToken(isPublicAPI)
  257. const defaultOptions = {
  258. method: 'POST',
  259. url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''),
  260. headers: {
  261. Authorization: `Bearer ${token}`,
  262. },
  263. data: {},
  264. }
  265. options = {
  266. ...defaultOptions,
  267. ...options,
  268. headers: { ...defaultOptions.headers, ...options.headers },
  269. }
  270. return new Promise((resolve, reject) => {
  271. const xhr = options.xhr
  272. xhr.open(options.method, options.url)
  273. for (const key in options.headers)
  274. xhr.setRequestHeader(key, options.headers[key])
  275. xhr.withCredentials = true
  276. xhr.responseType = 'json'
  277. xhr.onreadystatechange = function () {
  278. if (xhr.readyState === 4) {
  279. if (xhr.status === 201)
  280. resolve(xhr.response)
  281. else
  282. reject(xhr)
  283. }
  284. }
  285. xhr.upload.onprogress = options.onprogress
  286. xhr.send(options.data)
  287. })
  288. }
  289. export const ssePost = (
  290. url: string,
  291. fetchOptions: FetchOptionType,
  292. otherOptions: IOtherOptions,
  293. ) => {
  294. const {
  295. isPublicAPI = false,
  296. onData,
  297. onCompleted,
  298. onThought,
  299. onFile,
  300. onMessageEnd,
  301. onMessageReplace,
  302. onWorkflowStarted,
  303. onWorkflowFinished,
  304. onNodeStarted,
  305. onNodeFinished,
  306. onIterationStart,
  307. onIterationNext,
  308. onIterationFinish,
  309. onNodeRetry,
  310. onParallelBranchStarted,
  311. onParallelBranchFinished,
  312. onTextChunk,
  313. onTTSChunk,
  314. onTTSEnd,
  315. onTextReplace,
  316. onAgentLog,
  317. onError,
  318. getAbortController,
  319. } = otherOptions
  320. const abortController = new AbortController()
  321. const token = localStorage.getItem('console_token')
  322. const options = Object.assign({}, baseOptions, {
  323. method: 'POST',
  324. signal: abortController.signal,
  325. headers: new Headers({
  326. Authorization: `Bearer ${token}`,
  327. }),
  328. } as RequestInit, fetchOptions)
  329. const contentType = (options.headers as Headers).get('Content-Type')
  330. if (!contentType)
  331. (options.headers as Headers).set('Content-Type', ContentType.json)
  332. getAbortController?.(abortController)
  333. const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
  334. const urlWithPrefix = (url.startsWith('http://') || url.startsWith('https://'))
  335. ? url
  336. : `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
  337. const { body } = options
  338. if (body)
  339. options.body = JSON.stringify(body)
  340. const accessToken = getAccessToken(isPublicAPI)
  341. options.headers!.set('Authorization', `Bearer ${accessToken}`)
  342. globalThis.fetch(urlWithPrefix, options as RequestInit)
  343. .then((res) => {
  344. if (!/^(2|3)\d{2}$/.test(String(res.status))) {
  345. if (res.status === 401) {
  346. refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
  347. ssePost(url, fetchOptions, otherOptions)
  348. }).catch(() => {
  349. res.json().then((data: any) => {
  350. if (isPublicAPI) {
  351. if (data.code === 'web_sso_auth_required')
  352. requiredWebSSOLogin()
  353. if (data.code === 'unauthorized') {
  354. removeAccessToken()
  355. globalThis.location.reload()
  356. }
  357. }
  358. })
  359. })
  360. }
  361. else {
  362. res.json().then((data) => {
  363. Toast.notify({ type: 'error', message: data.message || 'Server Error' })
  364. })
  365. onError?.('Server Error')
  366. }
  367. return
  368. }
  369. return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
  370. if (moreInfo.errorMessage) {
  371. onError?.(moreInfo.errorMessage, moreInfo.errorCode)
  372. // TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
  373. if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.' && !moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property'))
  374. Toast.notify({ type: 'error', message: moreInfo.errorMessage })
  375. return
  376. }
  377. onData?.(str, isFirstMessage, moreInfo)
  378. }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace, onAgentLog)
  379. }).catch((e) => {
  380. if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property'))
  381. Toast.notify({ type: 'error', message: e })
  382. onError?.(e)
  383. })
  384. }
  385. // base request
  386. export const request = async<T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  387. try {
  388. const otherOptionsForBaseFetch = otherOptions || {}
  389. const [err, resp] = await asyncRunSafe<T>(baseFetch(url, options, otherOptionsForBaseFetch))
  390. if (err === null)
  391. return resp
  392. const errResp: Response = err as any
  393. if (errResp.status === 401) {
  394. const [parseErr, errRespData] = await asyncRunSafe<ResponseError>(errResp.json())
  395. const loginUrl = `${globalThis.location.origin}/signin`
  396. if (parseErr) {
  397. globalThis.location.href = loginUrl
  398. return Promise.reject(err)
  399. }
  400. // special code
  401. const { code, message } = errRespData
  402. // webapp sso
  403. if (code === 'web_sso_auth_required') {
  404. requiredWebSSOLogin()
  405. return Promise.reject(err)
  406. }
  407. if (code === 'unauthorized_and_force_logout') {
  408. localStorage.removeItem('console_token')
  409. localStorage.removeItem('refresh_token')
  410. globalThis.location.reload()
  411. return Promise.reject(err)
  412. }
  413. const {
  414. isPublicAPI = false,
  415. silent,
  416. } = otherOptionsForBaseFetch
  417. if (isPublicAPI && code === 'unauthorized') {
  418. removeAccessToken()
  419. globalThis.location.reload()
  420. return Promise.reject(err)
  421. }
  422. if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) {
  423. Toast.notify({ type: 'error', message, duration: 4000 })
  424. return Promise.reject(err)
  425. }
  426. if (code === 'not_init_validated' && IS_CE_EDITION) {
  427. globalThis.location.href = `${globalThis.location.origin}/init`
  428. return Promise.reject(err)
  429. }
  430. if (code === 'not_setup' && IS_CE_EDITION) {
  431. globalThis.location.href = `${globalThis.location.origin}/install`
  432. return Promise.reject(err)
  433. }
  434. // refresh token
  435. const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT))
  436. if (refreshErr === null)
  437. return baseFetch<T>(url, options, otherOptionsForBaseFetch)
  438. if (location.pathname !== '/signin' || !IS_CE_EDITION) {
  439. globalThis.location.href = loginUrl
  440. return Promise.reject(err)
  441. }
  442. if (!silent) {
  443. Toast.notify({ type: 'error', message })
  444. return Promise.reject(err)
  445. }
  446. globalThis.location.href = loginUrl
  447. return Promise.reject(err)
  448. }
  449. else {
  450. return Promise.reject(err)
  451. }
  452. }
  453. catch (error) {
  454. console.error(error)
  455. return Promise.reject(error)
  456. }
  457. }
  458. // request methods
  459. export const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  460. return request<T>(url, Object.assign({}, options, { method: 'GET' }), otherOptions)
  461. }
  462. // For public API
  463. export const getPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  464. return get<T>(url, options, { ...otherOptions, isPublicAPI: true })
  465. }
  466. // For Marketplace API
  467. export const getMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  468. return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
  469. }
  470. export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  471. return request<T>(url, Object.assign({}, options, { method: 'POST' }), otherOptions)
  472. }
  473. // For Marketplace API
  474. export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  475. return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
  476. }
  477. export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  478. return post<T>(url, options, { ...otherOptions, isPublicAPI: true })
  479. }
  480. export const put = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  481. return request<T>(url, Object.assign({}, options, { method: 'PUT' }), otherOptions)
  482. }
  483. export const putPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  484. return put<T>(url, options, { ...otherOptions, isPublicAPI: true })
  485. }
  486. export const del = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  487. return request<T>(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)
  488. }
  489. export const delPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  490. return del<T>(url, options, { ...otherOptions, isPublicAPI: true })
  491. }
  492. export const patch = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  493. return request<T>(url, Object.assign({}, options, { method: 'PATCH' }), otherOptions)
  494. }
  495. export const patchPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
  496. return patch<T>(url, options, { ...otherOptions, isPublicAPI: true })
  497. }