Merge remote-tracking branch 'origin/main' into feat/collaboration

This commit is contained in:
lyzno1
2025-10-20 10:03:57 +08:00
313 changed files with 8233 additions and 4914 deletions

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
@ -16,7 +16,7 @@ type Props = {
type: EditItemType
content: string
readonly?: boolean
onSave: (content: string) => void
onSave: (content: string) => Promise<void>
}
export const EditTitle: FC<{ className?: string; title: string }> = ({ className, title }) => (
@ -46,8 +46,13 @@ const EditItem: FC<Props> = ({
const placeholder = type === EditItemType.Query ? t('appAnnotation.editModal.queryPlaceholder') : t('appAnnotation.editModal.answerPlaceholder')
const [isEdit, setIsEdit] = useState(false)
const handleSave = () => {
onSave(newContent)
// Reset newContent when content prop changes
useEffect(() => {
setNewContent('')
}, [content])
const handleSave = async () => {
await onSave(newContent)
setIsEdit(false)
}

View File

@ -21,7 +21,7 @@ type Props = {
isShow: boolean
onHide: () => void
item: AnnotationItem
onSave: (editedQuery: string, editedAnswer: string) => void
onSave: (editedQuery: string, editedAnswer: string) => Promise<void>
onRemove: () => void
}
@ -46,6 +46,16 @@ const ViewAnnotationModal: FC<Props> = ({
const [currPage, setCurrPage] = React.useState<number>(0)
const [total, setTotal] = useState(0)
const [hitHistoryList, setHitHistoryList] = useState<HitHistoryItem[]>([])
// Update local state when item prop changes (e.g., when modal is reopened with updated data)
useEffect(() => {
setNewQuery(question)
setNewAnswer(answer)
setCurrPage(0)
setTotal(0)
setHitHistoryList([])
}, [question, answer, id])
const fetchHitHistory = async (page = 1) => {
try {
const { data, total }: any = await fetchHitHistoryList(appId, id, {
@ -63,6 +73,12 @@ const ViewAnnotationModal: FC<Props> = ({
fetchHitHistory(currPage + 1)
}, [currPage])
// Fetch hit history when item changes
useEffect(() => {
if (isShow && id)
fetchHitHistory(1)
}, [id, isShow])
const tabs = [
{ value: TabType.annotation, text: t('appAnnotation.viewModal.annotatedResponse') },
{
@ -82,14 +98,20 @@ const ViewAnnotationModal: FC<Props> = ({
},
]
const [activeTab, setActiveTab] = useState(TabType.annotation)
const handleSave = (type: EditItemType, editedContent: string) => {
if (type === EditItemType.Query) {
setNewQuery(editedContent)
onSave(editedContent, newAnswer)
const handleSave = async (type: EditItemType, editedContent: string) => {
try {
if (type === EditItemType.Query) {
await onSave(editedContent, newAnswer)
setNewQuery(editedContent)
}
else {
await onSave(newQuestion, editedContent)
setNewAnswer(editedContent)
}
}
else {
setNewAnswer(editedContent)
onSave(newQuestion, editedContent)
catch (error) {
// If save fails, don't update local state
console.error('Failed to save annotation:', error)
}
}
const [showModal, setShowModal] = useState(false)

View File

@ -22,7 +22,7 @@ const AccessControlDialog = ({
}, [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" open={true} className="relative z-20" onClose={() => null}>
<Dialog as="div" open={true} className="relative z-[99]" onClose={() => null}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -32,7 +32,7 @@ const AccessControlDialog = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="bg-background-overlay/25 fixed inset-0" />
<div className="fixed inset-0 bg-background-overlay" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center">

View File

@ -52,7 +52,7 @@ export default function AddMemberOrGroupDialog() {
</Button>
</PortalToFollowElemTrigger>
{open && <FloatingOverlay />}
<PortalToFollowElemContent className='z-[25]'>
<PortalToFollowElemContent className='z-[100]'>
<div className='relative flex max-h-[400px] w-[400px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
<div className='sticky top-0 z-10 bg-components-panel-bg-blur p-2 pb-0.5 backdrop-blur-[5px]'>
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />

View File

@ -4,7 +4,6 @@ import {
useEffect,
useState,
} from 'react'
import { useAsyncEffect } from 'ahooks'
import { useThemeContext } from '../embedded-chatbot/theme/theme-context'
import {
ChatWithHistoryContext,
@ -18,8 +17,6 @@ import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
@ -201,36 +198,6 @@ const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
installedAppInfo,
className,
}) => {
const [initialized, setInitialized] = useState(false)
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false)
useAsyncEffect(async () => {
if (!initialized) {
if (!installedAppInfo) {
try {
await checkOrSetAccessToken()
}
catch (e: any) {
if (e.status === 404) {
setAppUnavailable(true)
}
else {
setIsUnknownReason(true)
setAppUnavailable(true)
}
}
}
setInitialized(true)
}
}, [])
if (!initialized)
return null
if (appUnavailable)
return <AppUnavailable isUnknownReason={isUnknownReason} />
return (
<ChatWithHistoryWrap
installedAppInfo={installedAppInfo}

View File

@ -25,7 +25,6 @@ import Compliance from './compliance'
import PremiumBadge from '@/app/components/base/premium-badge'
import Avatar from '@/app/components/base/avatar'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
@ -33,6 +32,7 @@ import { IS_CLOUD_EDITION } from '@/config'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import { useLogout } from '@/service/use-common'
export default function AppSelector() {
const itemClassName = `
@ -49,15 +49,12 @@ export default function AppSelector() {
const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
await logout()
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Tokens are now stored in cookies and cleared by backend
// To avoid use other account's education notice info
localStorage.removeItem('education-reverify-prev-expire-at')

View File

@ -77,7 +77,7 @@ export const useNodesSyncDraft = () => {
if (postParams) {
navigator.sendBeacon(
`${API_PREFIX}${postParams.url}?_token=${localStorage.getItem('console_token')}`,
`${API_PREFIX}${postParams.url}`,
JSON.stringify(postParams.params),
)
}

View File

@ -20,6 +20,7 @@ import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
import { AccessMode } from '@/models/access-control'
import { useWebAppStore } from '@/context/web-app-context'
import { webAppLogout } from '@/service/webapp-auth'
type Props = {
data?: SiteInfo
@ -49,11 +50,11 @@ const MenuDropdown: FC<Props> = ({
setOpen(!openRef.current)
}, [setOpen])
const handleLogout = useCallback(() => {
localStorage.removeItem('token')
localStorage.removeItem('webapp_access_token')
const shareCode = useWebAppStore(s => s.shareCode)
const handleLogout = useCallback(async () => {
await webAppLogout(shareCode!)
router.replace(`/webapp-signin?redirect_url=${pathname}`)
}, [router, pathname])
}, [router, pathname, webAppLogout, shareCode])
const [show, setShow] = useState(false)

View File

@ -1,7 +1,3 @@
import { CONVERSATION_ID_INFO } from '../base/chat/constants'
import { fetchAccessToken } from '@/service/share'
import { getProcessedSystemVariablesFromUrlParams } from '../base/chat/utils'
export const isTokenV1 = (token: Record<string, any>) => {
return !token.version
}
@ -9,55 +5,3 @@ export const isTokenV1 = (token: Record<string, any>) => {
export const getInitialTokenV2 = (): Record<string, any> => ({
version: 2,
})
export const checkOrSetAccessToken = async (appCode?: string | null) => {
const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
try {
accessTokenJson = JSON.parse(accessToken)
if (isTokenV1(accessTokenJson))
accessTokenJson = getInitialTokenV2()
}
catch {
}
if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) {
const webAppAccessToken = localStorage.getItem('webapp_access_token')
const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken })
accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken],
[userId || 'DEFAULT']: res.access_token,
}
localStorage.setItem('token', JSON.stringify(accessTokenJson))
localStorage.removeItem(CONVERSATION_ID_INFO)
}
}
export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => {
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
try {
accessTokenJson = JSON.parse(accessToken)
if (isTokenV1(accessTokenJson))
accessTokenJson = getInitialTokenV2()
}
catch {
}
localStorage.removeItem(CONVERSATION_ID_INFO)
accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken],
[user_id || 'DEFAULT']: token,
}
localStorage.setItem('token', JSON.stringify(accessTokenJson))
}
export const removeAccessToken = () => {
localStorage.removeItem('token')
localStorage.removeItem('webapp_access_token')
}

View File

@ -19,10 +19,7 @@ const SwrInitializer = ({
}: SwrInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
const consoleTokenFromLocalStorage = localStorage?.getItem('console_token')
const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token')
// Tokens are now stored in cookies, no need to check localStorage
const pathname = usePathname()
const [init, setInit] = useState(false)
@ -57,21 +54,12 @@ const SwrInitializer = ({
router.replace('/install')
return
}
if (!((consoleToken && refreshToken) || (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage))) {
router.replace('/signin')
return
}
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
if (consoleToken)
localStorage.setItem('console_token', consoleToken)
if (refreshToken)
localStorage.setItem('refresh_token', refreshToken)
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl)
location.replace(redirectUrl)
else
router.replace(pathname)
}
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl)
location.replace(redirectUrl)
else
router.replace(pathname)
setInit(true)
}
@ -79,7 +67,7 @@ const SwrInitializer = ({
router.replace('/signin')
}
})()
}, [isSetupFinished, router, pathname, searchParams, consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage])
}, [isSetupFinished, router, pathname, searchParams])
return init
? (

View File

@ -110,7 +110,7 @@ export const useNodesSyncDraft = () => {
if (postParams) {
console.log('Leader syncing workflow draft on page close')
navigator.sendBeacon(
`${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`,
`${API_PREFIX}/apps/${params.appId}/workflows/draft`,
JSON.stringify(postParams.params),
)
}

View File

@ -178,6 +178,7 @@ const ToolPicker: FC<Props> = ({
mcpTools={mcpTools || []}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
onTagsChange={setTags}
/>
</div>
</PortalToFollowElemContent>

View File

@ -39,7 +39,6 @@ const Item: FC<Props> = ({
key={tool.id}
payload={tool}
viewType={ViewType.tree}
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}

View File

@ -37,6 +37,7 @@ export type ToolDefaultValue = {
paramSchemas: Record<string, any>[]
credential_id?: string
meta?: PluginMeta
output_schema?: Record<string, any>
}
export type DataSourceDefaultValue = {

View File

@ -8,8 +8,8 @@ export enum ScrollPosition {
}
type Params = {
wrapElemRef: React.RefObject<HTMLElement>
nextToStickyELemRef: React.RefObject<HTMLElement>
wrapElemRef: React.RefObject<HTMLElement | null>
nextToStickyELemRef: React.RefObject<HTMLElement | null>
}
const useStickyScroll = ({
wrapElemRef,

View File

@ -21,7 +21,7 @@ const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({
nodes,
children,
}) => {
const storeRef = useRef<DatasetsDetailStoreApi>()
const storeRef = useRef<DatasetsDetailStoreApi>(undefined)
if (!storeRef.current)
storeRef.current = createDatasetsDetailStore()

View File

@ -15,7 +15,7 @@ import {
getOutgoers,
useReactFlow,
} from 'reactflow'
import type { ToolDefaultValue } from '../block-selector/types'
import type { DataSourceDefaultValue, ToolDefaultValue } from '../block-selector/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import { BlockEnum } from '../types'
import { useWorkflowStore } from '../store'
@ -1264,7 +1264,7 @@ export const useNodesInteractions = () => {
currentNodeId: string,
nodeType: BlockEnum,
sourceHandle: string,
toolDefaultValue?: ToolDefaultValue,
toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue,
) => {
if (getNodesReadOnly()) return

View File

@ -212,7 +212,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
agent_strategy_name: tool!.tool_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
agent_output_schema: tool!.output_schema || {},
plugin_unique_identifier: tool!.provider_id,
meta: tool!.meta,
})

View File

@ -28,7 +28,6 @@ const getIcon = (type: InputVarType) => {
[InputVarType.jsonObject]: RiBracesLine,
[InputVarType.singleFile]: RiFileList2Line,
[InputVarType.multiFiles]: RiFileCopy2Line,
[InputVarType.checkbox]: RiCheckboxLine,
} as any)[type] || RiTextSnippet
}

View File

@ -16,7 +16,7 @@ import {
} from '../../../types'
import type { Node } from '../../../types'
import BlockSelector from '../../../block-selector'
import type { ToolDefaultValue } from '../../../block-selector/types'
import type { DataSourceDefaultValue, ToolDefaultValue } from '../../../block-selector/types'
import {
useAvailableBlocks,
useIsChatMode,
@ -57,7 +57,7 @@ export const NodeTargetHandle = memo(({
if (!connected)
setOpen(v => !v)
}, [connected])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
@ -140,7 +140,7 @@ export const NodeSourceHandle = memo(({
e.stopPropagation()
setOpen(v => !v)
}, [])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
handleNodeAdd(
{
nodeType: type,

View File

@ -42,17 +42,17 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
return 'text-util-colors-teal-teal-700'
return 'text-text-accent'
}, [variables, isExceptionVariable])
}, [variables, isExceptionVariable, variableCategory])
}
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const variablesLength = variables.length
const varName = useMemo(() => {
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const variablesLength = variables.length
const isSystem = isSystemVar(variables)
const varName = notShowFullPath ? variables[variablesLength - 1] : variableFullPathName
return `${isSystem ? 'sys.' : ''}${varName}`

View File

@ -53,8 +53,13 @@ import { useAppContext } from '@/context/app-context'
import { useStore } from '@/app/components/workflow/store'
import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration'
type NodeChildProps = {
id: string
data: NodeProps['data']
}
type BaseNodeProps = {
children: ReactElement
children: ReactElement<Partial<NodeChildProps>>
id: NodeProps['id']
data: NodeProps['data']
}

View File

@ -11,6 +11,11 @@ export type OutputVar = Record<string, {
children: null // support nest in the future,
}>
export type CodeDependency = {
name: string
version?: string
}
export type CodeNodeType = CommonNodeType & {
variables: Variable[]
code_language: CodeLanguage

View File

@ -16,7 +16,7 @@ import {
const useConfig = (id: string, payload: HttpNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs?.[payload.type])
const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload)

View File

@ -209,7 +209,7 @@ const ConditionItem = ({
onRemoveCondition?.(caseId, condition.id)
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
const { getMatchedSchemaType } = useMatchSchemaType()
const { schemaTypeDefinitions } = useMatchSchemaType()
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
@ -226,7 +226,7 @@ const ConditionItem = ({
workflowTools,
dataSourceList: dataSourceList ?? [],
},
getMatchedSchemaType,
schemaTypeDefinitions,
})
const newCondition = produce(condition, (draft) => {
@ -241,7 +241,7 @@ const ConditionItem = ({
})
doUpdateCondition(newCondition)
setOpen(false)
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey])
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey, schemaTypeDefinitions])
const showBooleanInput = useMemo(() => {
if(condition.varType === VarType.boolean)