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
20 changes: 9 additions & 11 deletions apps/common/auth/handle/impl/chat_user_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {}
Expand Down
5 changes: 4 additions & 1 deletion ui/src/conversation-panel/components/message-input/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -57,6 +58,8 @@ export interface MessageInputDeps {
disabled?: Readonly<Ref<boolean>>
/** 是否在用户输入表单中编辑接口传参,仅调试对话开启。 */
apiInputEditable?: boolean
/** 地址栏参数,接口传参不可编辑时优先使用,缺省时用默认值。 */
getRouteQuery?: () => LocationQuery
}

// 上传请求返回前进度条的最大值
Expand All @@ -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('')
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -78,18 +79,38 @@ 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<Ref<ConversationAppInfo | null>>
/** 是否在表单中编辑接口传参;仅调试对话开启,其他场景接口传参来自地址栏。 */
apiInputEditable: boolean
/** 地址栏参数,接口传参不可编辑时使用;不传时只使用默认值。 */
getRouteQuery?: () => LocationQuery
}

/**
* 用户输入参数状态:字段配置取自开始节点,表单值随消息一起发送。
* 表单校验依赖已渲染的表单实例,由 user-form 组件挂载时绑定。
*/
export function createUserFormStore(deps: UserFormDeps) {
const { appInfo, apiInputEditable } = deps
const { appInfo, apiInputEditable, getRouteQuery } = deps

const formData = ref<Dict<DynamicFormValue>>({})
const apiFormData = ref<Dict<DynamicFormValue>>({})
Expand Down Expand Up @@ -139,8 +160,33 @@ export function createUserFormStore(deps: UserFormDeps) {
snapshot = null
}

/**
* 不可编辑时的接口传参:地址栏有值用地址栏,没有用默认值。
* 发送时按当前字段与地址计算,不受智能体信息加载时机、切换智能体清空的影响。
*/
const routeApiFormData = computed(() => {
const query = getRouteQuery?.() ?? {}
const values: Dict<DynamicFormValue> = {}
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<DynamicFormValue> => (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<DynamicFormValue> => ({ ...formData.value, ...apiFormData.value })
const getFormData = (): Dict<DynamicFormValue> => ({ ...formData.value, ...getApiFormData() })

return {
formData,
Expand All @@ -155,6 +201,7 @@ export function createUserFormStore(deps: UserFormDeps) {
validate,
saveSnapshot,
restoreSnapshot,
getMissingApiFields,
getFormData,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<DynamicFormValue> = {}
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)
Expand Down Expand Up @@ -198,7 +167,6 @@ onMounted(() => {
formData.value = {}
}
}
initRouteQueryValue()
unbindValidator = userForm.bindValidator(validate)

if (containerRef.value) {
Expand Down
15 changes: 11 additions & 4 deletions ui/src/conversation-panel/view/chat/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -27,8 +27,10 @@ export interface ChatConversationRouting {
getApplicationId: () => string
/** 地址中的会话 ID,没有时为空字符串。 */
getRouteChatId: () => string
/** 将当前会话写入地址;会话 ID 为空表示新建对话。 */
/** 将当前会话写入地址;会话 ID 为空表示新建对话。需保留地址栏参数(接口传参)。 */
replaceChatId: (chatId: string) => void
/** 地址栏参数,作为接口传参来源。 */
getRouteQuery: () => LocationQuery
}

/** 单应用对话:智能体 ID 取自守卫写入的应用认证前置配置,会话地址为 /:accessToken/c/:chatId。 */
Expand All @@ -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,
}
}

Expand All @@ -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 = {
Expand Down Expand Up @@ -116,6 +122,7 @@ export function createChatConversation(routing: ChatConversationRouting = useApp
stop: msgs.stop,
speechToText: (data) => ConversationApi.postSpeechToText(getApplicationId(), data),
appInfo,
getRouteQuery,
// 门户首页、全部智能体页尚未确定智能体时禁止发送
disabled: computed(() => !getApplicationId()),
})
Expand Down
14 changes: 10 additions & 4 deletions ui/src/conversation-panel/view/portal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand All @@ -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),
Expand All @@ -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,
})

Expand Down
8 changes: 6 additions & 2 deletions ui/src/views/application-detail/overview/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading