diff --git a/apps/common/auth/handle/impl/chat_user_token.py b/apps/common/auth/handle/impl/chat_user_token.py index d973f5f9ee2..3ddf7f36d58 100644 --- a/apps/common/auth/handle/impl/chat_user_token.py +++ b/apps/common/auth/handle/impl/chat_user_token.py @@ -42,14 +42,10 @@ def get_auth(login_type, user_id, application_id): application_access_token_list = QuerySet(ApplicationAccessToken).filter(is_active=True) - if login_type.upper() == str(Operate.ANNOTATION_AUTH): - application_access_token_list = application_access_token_list.filter(authentication=False) - elif login_type.upper() == str(Operate.PASSWORD): - application_access_token_list = application_access_token_list.filter( - authentication=True, authentication_value__type="password" - ) + # 匿名应用所有人都可以访问;登录用户额外可以访问已授权的登录认证应用(匿名 OR 登录授权) + access_condition = Q(authentication=False) - elif login_type_list.__contains__(login_type.upper()): + if login_type_list.__contains__(login_type.upper()): user_group_ids = ( QuerySet(UserGroupRelation) .filter( @@ -77,11 +73,13 @@ def get_auth(login_type, user_id, application_id): ) .values_list("resource_id", flat=True) ) - application_access_token_list = application_access_token_list.filter( - Q(authentication_value__type="login"), - Q(authentication_value__login_value__contains=login_type), - Q(application_id__in=group_qs) | Q(application_id__in=user_qs), + access_condition |= ( + Q(authentication=True) + & Q(authentication_value__type="login") + & Q(authentication_value__login_value__contains=login_type) + & (Q(application_id__in=group_qs) | Q(application_id__in=user_qs)) ) + application_access_token_list = application_access_token_list.filter(access_condition) if application_id: application_access_token_list = application_access_token_list.filter(application_id=application_id) permissions = {} diff --git a/ui/src/conversation-panel/components/message-input/index.ts b/ui/src/conversation-panel/components/message-input/index.ts index 491b56bdc1b..6a256ddab3f 100644 --- a/ui/src/conversation-panel/components/message-input/index.ts +++ b/ui/src/conversation-panel/components/message-input/index.ts @@ -1,4 +1,5 @@ import { ref, computed, reactive, watch, inject, type Ref, type InjectionKey } from 'vue' +import type { LocationQuery } from 'vue-router' import { pick } from 'lodash' import { IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, VIDEO_EXTENSIONS, AUDIO_EXTENSIONS } from '@/constants/file-type' import { isImage, isDocument, isAudio, isVideo } from '@/utils/icon' @@ -57,6 +58,8 @@ export interface MessageInputDeps { disabled?: Readonly> /** 是否在用户输入表单中编辑接口传参,仅调试对话开启。 */ apiInputEditable?: boolean + /** 地址栏参数,接口传参不可编辑时优先使用,缺省时用默认值。 */ + getRouteQuery?: () => LocationQuery } // 上传请求返回前进度条的最大值 @@ -68,7 +71,7 @@ const UPLOAD_PROGRESS_PENDING_MAX = 95 export function createMessageInputStore(deps: MessageInputDeps) { const { conversations, getChatId, renameChat, composerResetSignal, messages, streamLoading, sendMessage, uploadFile, stop, appInfo } = deps const disabled = computed(() => deps.disabled?.value ?? false) - const userForm = createUserFormStore({ appInfo, apiInputEditable: deps.apiInputEditable ?? false }) + const userForm = createUserFormStore({ appInfo, apiInputEditable: deps.apiInputEditable ?? false, getRouteQuery: deps.getRouteQuery }) // 输入状态与附件限制 const question = ref('') diff --git a/ui/src/conversation-panel/components/message-input/user-form/index.ts b/ui/src/conversation-panel/components/message-input/user-form/index.ts index 9da850e4006..b5e60d281da 100644 --- a/ui/src/conversation-panel/components/message-input/user-form/index.ts +++ b/ui/src/conversation-panel/components/message-input/user-form/index.ts @@ -1,4 +1,5 @@ import { computed, ref, watch, type Ref } from 'vue' +import type { LocationQuery } from 'vue-router' import { cloneDeep } from 'lodash' import type { DynamicFormValue, FormField } from '@/components/mk-dynamics-form' import type { Dict } from '@/api/types' @@ -78,10 +79,30 @@ function getApiInputFields(properties: BaseNodeProperties): FormField[] { .filter(isFormField) } +// ===== 地址栏参数 ===== +/** 提问人:不在开始节点配置中,同样由地址栏传入。 */ +const ASKER_FIELD = 'asker' + +/** 地址栏参数值:无值参数(?a)视为未传;vue-router 已解码,无需再次解码。 */ +function getQueryValue(query: LocationQuery, field: string): DynamicFormValue | undefined { + const value = query[field] + if (Array.isArray(value)) { + const values = value.filter((item): item is string => item != null) + return values.length ? values : undefined + } + return value ?? undefined +} + +function hasValue(value: DynamicFormValue | undefined) { + return value != null && value !== '' +} + export interface UserFormDeps { appInfo: Readonly> /** 是否在表单中编辑接口传参;仅调试对话开启,其他场景接口传参来自地址栏。 */ apiInputEditable: boolean + /** 地址栏参数,接口传参不可编辑时使用;不传时只使用默认值。 */ + getRouteQuery?: () => LocationQuery } /** @@ -89,7 +110,7 @@ export interface UserFormDeps { * 表单校验依赖已渲染的表单实例,由 user-form 组件挂载时绑定。 */ export function createUserFormStore(deps: UserFormDeps) { - const { appInfo, apiInputEditable } = deps + const { appInfo, apiInputEditable, getRouteQuery } = deps const formData = ref>({}) const apiFormData = ref>({}) @@ -139,8 +160,33 @@ export function createUserFormStore(deps: UserFormDeps) { snapshot = null } + /** + * 不可编辑时的接口传参:地址栏有值用地址栏,没有用默认值。 + * 发送时按当前字段与地址计算,不受智能体信息加载时机、切换智能体清空的影响。 + */ + const routeApiFormData = computed(() => { + const query = getRouteQuery?.() ?? {} + const values: Dict = {} + for (const field of apiInputFields.value) { + const value = getQueryValue(query, field.field) ?? field.default_value + if (hasValue(value)) values[field.field] = value as DynamicFormValue + } + const asker = getQueryValue(query, ASKER_FIELD) + if (hasValue(asker)) values[ASKER_FIELD] = asker as DynamicFormValue + return values + }) + + /** 实际发送的接口传参:调试对话取表单编辑值,其他场景取地址栏或默认值。 */ + const getApiFormData = (): Dict => (apiInputEditable ? apiFormData.value : routeApiFormData.value) + + /** 缺少值的必填接口传参。 */ + const getMissingApiFields = () => { + const values = getApiFormData() + return apiInputFields.value.filter((field) => field.required && !hasValue(values[field.field])).map((field) => field.field) + } + /** 随消息发送的表单数据,接口传参同名时覆盖用户输入。 */ - const getFormData = (): Dict => ({ ...formData.value, ...apiFormData.value }) + const getFormData = (): Dict => ({ ...formData.value, ...getApiFormData() }) return { formData, @@ -155,6 +201,7 @@ export function createUserFormStore(deps: UserFormDeps) { validate, saveSnapshot, restoreSnapshot, + getMissingApiFields, getFormData, } } diff --git a/ui/src/conversation-panel/components/message-input/user-form/index.vue b/ui/src/conversation-panel/components/message-input/user-form/index.vue index cbe28aa4413..19a95509b45 100644 --- a/ui/src/conversation-panel/components/message-input/user-form/index.vue +++ b/ui/src/conversation-panel/components/message-input/user-form/index.vue @@ -2,7 +2,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRoute } from 'vue-router' import { MkDynamicsForm } from '@/components/mk-dynamics-form' -import type { DynamicFormValue, FormField } from '@/components/mk-dynamics-form' +import type { FormField } from '@/components/mk-dynamics-form' import type { Dict } from '@/api/types' import { MsgWarning } from '@/utils/message' import { useMessageInputStore } from '../index' @@ -104,42 +104,11 @@ const showMoreButton = computed(() => popoverFields.value.length > 0 || (apiInpu const formRefreshKey = ref(0) watch([inlineFields, popoverFields, apiInputFields], () => formRefreshKey.value++) -// ===== 地址栏参数 ===== -function decodeQuery(query: string) { - try { - return decodeURIComponent(query) - } catch { - return query - } -} - -function getRouteQueryValue(field: string) { - const queryValue = route.query[field] - if (queryValue == null) return null - if (Array.isArray(queryValue)) { - return queryValue.filter((value): value is string => value != null).map(decodeQuery) - } - return decodeQuery(queryValue) -} - -/** 接口传参未赋值时,从地址栏同名参数回填;同时回填提问人 asker。 */ -function initRouteQueryValue() { - const queryValues: Dict = {} - for (const field of [...apiInputFields.value.map((apiField) => apiField.field), 'asker']) { - if (apiFormData.value[field]) continue - const queryValue = getRouteQueryValue(field) - if (queryValue) queryValues[field] = queryValue - } - if (Object.keys(queryValues).length) { - apiFormData.value = { ...apiFormData.value, ...queryValues } - } -} - // ===== 校验 ===== -/** 非调试场景接口传参只能来自地址栏,缺少必填参数时提示补充。 */ +/** 非调试场景接口传参来自地址栏或默认值,均缺少的必填参数提示在地址栏补充。 */ function validateApiQuery() { if (apiInputEditable) return Promise.resolve() - const missingFields = apiInputFields.value.filter((field) => field.required && !apiFormData.value[field.field]).map((field) => field.field) + const missingFields = userForm.getMissingApiFields() if (missingFields.length) { MsgWarning(`请在URL中填写参数 ${missingFields.join('、')}的值`) return Promise.reject(false) @@ -198,7 +167,6 @@ onMounted(() => { formData.value = {} } } - initRouteQueryValue() unbindValidator = userForm.bindValidator(validate) if (containerRef.value) { diff --git a/ui/src/conversation-panel/view/chat/index.ts b/ui/src/conversation-panel/view/chat/index.ts index 3720b954b2d..f6f2c7b667e 100644 --- a/ui/src/conversation-panel/view/chat/index.ts +++ b/ui/src/conversation-panel/view/chat/index.ts @@ -1,5 +1,5 @@ import { computed, ref, watch } from 'vue' -import { useRoute, useRouter } from 'vue-router' +import { useRoute, useRouter, type LocationQuery } from 'vue-router' import ApplicationApi from '@/api/chat/application' import ConversationApi from '@/api/chat/conversation' import { useStore } from '@/stores/chat' @@ -27,8 +27,10 @@ export interface ChatConversationRouting { getApplicationId: () => string /** 地址中的会话 ID,没有时为空字符串。 */ getRouteChatId: () => string - /** 将当前会话写入地址;会话 ID 为空表示新建对话。 */ + /** 将当前会话写入地址;会话 ID 为空表示新建对话。需保留地址栏参数(接口传参)。 */ replaceChatId: (chatId: string) => void + /** 地址栏参数,作为接口传参来源。 */ + getRouteQuery: () => LocationQuery } /** 单应用对话:智能体 ID 取自守卫写入的应用认证前置配置,会话地址为 /:accessToken/c/:chatId。 */ @@ -42,8 +44,12 @@ function useApplicationChatRouting(): ChatConversationRouting { getRouteChatId: () => (typeof route.params.chatId === 'string' ? route.params.chatId : ''), replaceChatId: (chatId) => { const { accessToken } = route.params - void router.replace(chatId ? { name: 'chat-home-detail', params: { accessToken, chatId } } : { name: 'chat-home', params: { accessToken } }) + const { query } = route + void router.replace( + chatId ? { name: 'chat-home-detail', params: { accessToken, chatId }, query } : { name: 'chat-home', params: { accessToken }, query }, + ) }, + getRouteQuery: () => route.query, } } @@ -61,7 +67,7 @@ export function createChatUserProfileGetter(): GetChatUserProfile { * 当前会话与地址双向同步,切换智能体时重新加载智能体信息与会话。 */ export function createChatConversation(routing: ChatConversationRouting = useApplicationChatRouting()): ChatConversationBundle { - const { getApplicationId, getRouteChatId, replaceChatId } = routing + const { getApplicationId, getRouteChatId, replaceChatId, getRouteQuery } = routing const { user } = useStore() const api: ConversationApiAdapter = { @@ -116,6 +122,7 @@ export function createChatConversation(routing: ChatConversationRouting = useApp stop: msgs.stop, speechToText: (data) => ConversationApi.postSpeechToText(getApplicationId(), data), appInfo, + getRouteQuery, // 门户首页、全部智能体页尚未确定智能体时禁止发送 disabled: computed(() => !getApplicationId()), }) diff --git a/ui/src/conversation-panel/view/portal/index.ts b/ui/src/conversation-panel/view/portal/index.ts index c15c41c57f2..cf17dcc1fbe 100644 --- a/ui/src/conversation-panel/view/portal/index.ts +++ b/ui/src/conversation-panel/view/portal/index.ts @@ -24,10 +24,14 @@ function usePortalChatRouting(): ChatConversationRouting { const applicationId = getApplicationId() // 门户首页与全部智能体页没有智能体,不同步会话地址 if (!applicationId) return + const { query } = route void router.replace( - chatId ? { name: 'portal-application-chat', params: { applicationId, chatId } } : { name: 'portal-application', params: { applicationId } }, + chatId + ? { name: 'portal-application-chat', params: { applicationId, chatId }, query } + : { name: 'portal-application', params: { applicationId }, query }, ) }, + getRouteQuery: () => route.query, } } @@ -48,7 +52,8 @@ export function createPortalConversation(options: PortalConversationOptions): Po const currentApplicationId = computed(routing.getApplicationId) const isApplicationListActive = computed(() => route.name === 'portal-application-list') - const openApplication = (applicationId: string) => void router.push({ name: 'portal-application', params: { applicationId } }) + // 门户内跳转保留地址栏参数,接口传参对后续打开的智能体同样生效 + const openApplication = (applicationId: string) => void router.push({ name: 'portal-application', params: { applicationId }, query: route.query }) const portalList = createPortalConversationListStore({ pageApplicationConversations: (page, size) => PortalApi.getPortalConversationPage(page, size), @@ -58,8 +63,9 @@ export function createPortalConversation(options: PortalConversationOptions): Po currentApplicationId, isApplicationListActive, openApplication, - openConversation: (applicationId, chatId) => void router.push({ name: 'portal-application-chat', params: { applicationId, chatId } }), - openApplicationList: () => void router.push({ name: 'portal-application-list' }), + openConversation: (applicationId, chatId) => + void router.push({ name: 'portal-application-chat', params: { applicationId, chatId }, query: route.query }), + openApplicationList: () => void router.push({ name: 'portal-application-list', query: route.query }), openApplicationSelector: options.openApplicationSelector, }) diff --git a/ui/src/views/application-detail/overview/index.vue b/ui/src/views/application-detail/overview/index.vue index 5c4e2e189e3..fd4ea816a9b 100644 --- a/ui/src/views/application-detail/overview/index.vue +++ b/ui/src/views/application-detail/overview/index.vue @@ -48,9 +48,13 @@ const apiKeySaving = ref(false) const chatBasePath = CHAT_API_BASE_PATH.replace(/\/api$/, '') const apiDocUrl = new URL(`${CHAT_API_BASE_PATH}-doc/`, window.location.origin).href - +const apiQuery=computed(()=>{ +return application.value?.work_flow?.nodes?.find(n=>n.id==='base-node')?.properties?.api_input_field_list.map((f:any)=>{ + return `${f.variable}=${f.default_value}` +}).join('&') +}) // TODO:访问链接还有问题 -const accessUrl = computed(() => (accessToken.value ? `${window.location.origin}${chatBasePath}/${accessToken.value}` : '')) +const accessUrl = computed(() => (accessToken.value ? `${window.location.origin}${chatBasePath}/${accessToken.value}?${apiQuery.value}` : '')) function toUrl(url?: string) { if (!url) return