feat: application chat api (#3574)

This commit is contained in:
shaohuzhang1 2025-07-12 17:54:37 +08:00 committed by GitHub
parent 4ffd80f184
commit 34842e4ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 112 additions and 96 deletions

View File

@ -153,8 +153,9 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
reasoning_result = reasoning.get_reasoning_content(response) reasoning_result = reasoning.get_reasoning_content(response)
reasoning_result_end = reasoning.get_end_reasoning_content() reasoning_result_end = reasoning.get_end_reasoning_content()
content = reasoning_result.get('content') + reasoning_result_end.get('content') content = reasoning_result.get('content') + reasoning_result_end.get('content')
if 'reasoning_content' in response.response_metadata: meta = {**response.response_metadata, **response.additional_kwargs}
reasoning_content = response.response_metadata.get('reasoning_content', '') if 'reasoning_content' in meta:
reasoning_content = meta.get('reasoning_content', '')
else: else:
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content') reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content')
_write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content) _write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content)

View File

@ -8,9 +8,9 @@ urlpatterns = [
path('embed', views.ChatEmbedView.as_view()), path('embed', views.ChatEmbedView.as_view()),
path('auth/anonymous', views.AnonymousAuthentication.as_view()), path('auth/anonymous', views.AnonymousAuthentication.as_view()),
path('profile', views.AuthProfile.as_view()), path('profile', views.AuthProfile.as_view()),
path('application/profile', views.ApplicationProfile.as_view()), path('application/profile', views.ApplicationProfile.as_view(), name='profile'),
path('chat_message/<str:chat_id>', views.ChatView.as_view()), path('chat_message/<str:chat_id>', views.ChatView.as_view(), name='chat'),
path('open', views.OpenView.as_view()), path('open', views.OpenView.as_view(), name='open'),
path('text_to_speech', views.TextToSpeech.as_view()), path('text_to_speech', views.TextToSpeech.as_view()),
path('speech_to_text', views.SpeechToText.as_view()), path('speech_to_text', views.SpeechToText.as_view()),
path('captcha', views.CaptchaView.as_view(), name='captcha'), path('captcha', views.CaptchaView.as_view(), name='captcha'),

View File

@ -18,7 +18,7 @@ from common.utils.cache_util import get_cache
use_get_data=lambda secret_key, use_get_data: use_get_data, use_get_data=lambda secret_key, use_get_data: use_get_data,
version=Cache_Version.APPLICATION_API_KEY.get_version()) version=Cache_Version.APPLICATION_API_KEY.get_version())
def get_application_api_key(secret_key, use_get_data): def get_application_api_key(secret_key, use_get_data):
application_api_key = QuerySet(ApplicationApiKey).filter(secret_key=secret_key).first() application_api_key = QuerySet(ApplicationApiKey).filter(secret_key=secret_key[7:]).first()
return {'allow_cross_domain': application_api_key.allow_cross_domain, return {'allow_cross_domain': application_api_key.allow_cross_domain,
'cross_domain_list': application_api_key.cross_domain_list} 'cross_domain_list': application_api_key.cross_domain_list}

View File

@ -0,0 +1,69 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file init_doc.py
@date2024/5/24 14:11
@desc:
"""
import hashlib
from django.urls import path, URLPattern
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
from maxkb.const import CONFIG
chat_api_prefix = CONFIG.get_chat_path()[1:] + '/api/'
def init_app_doc(system_urlpatterns):
system_urlpatterns += [
path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由下面两个ui也是根据这个配置文件来生成的
path('doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # swagger-ui的路由
path('redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # redoc的路由
]
def init_chat_doc(system_urlpatterns, chat_urlpatterns):
system_urlpatterns += [
path('doc_chat_schema/',
SpectacularAPIView.as_view(patterns=[
URLPattern(pattern=f'{chat_api_prefix}{str(url.pattern)}', callback=url.callback,
default_args=url.default_args,
name=url.name) for url in chat_urlpatterns if
['chat', 'open', 'profile'].__contains__(url.name)]),
name='chat_schema'), # schema的配置文件的路由下面两个ui也是根据这个配置文件来生成的
path('doc_chat/', SpectacularSwaggerView.as_view(url_name='chat_schema'), name='swagger-ui'), # swagger-ui的路由
path('redoc_chat/', SpectacularRedocView.as_view(url_name='chat_schema'), name='redoc'), # redoc的路由
]
def encrypt(text):
md5 = hashlib.md5()
md5.update(text.encode())
result = md5.hexdigest()
return result
def get_call(application_urlpatterns, patterns, params, func):
def run():
if params['valid']():
func(*params['get_params'](application_urlpatterns, patterns))
return run
init_list = [(init_app_doc, {'valid': lambda: CONFIG.get('DOC_PASSWORD') is not None and encrypt(
CONFIG.get('DOC_PASSWORD')) == 'd4fc097197b4b90a122b92cbd5bbe867',
'get_call': get_call,
'get_params': lambda application_urlpatterns, patterns: (application_urlpatterns,)}),
(init_chat_doc, {'valid': lambda: CONFIG.get('DOC_PASSWORD') is not None and encrypt(
CONFIG.get('DOC_PASSWORD')) == 'd4fc097197b4b90a122b92cbd5bbe867' or True, 'get_call': get_call,
'get_params': lambda application_urlpatterns, patterns: (
application_urlpatterns, patterns)})]
def init_doc(system_urlpatterns, chat_patterns):
for init, params in init_list:
if params['valid']():
get_call(system_urlpatterns, chat_patterns, params, init)()

View File

@ -21,9 +21,10 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.templatetags.static import static as _static from django.templatetags.static import static as _static
from django.urls import path, re_path, include from django.urls import path, re_path, include
from django.views import static from django.views import static
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from rest_framework import status from rest_framework import status
from chat.urls import urlpatterns as chat_urlpatterns
from common.init.init_doc import init_doc
from common.result import Result from common.result import Result
from maxkb import settings from maxkb import settings
from maxkb.conf import PROJECT_DIR from maxkb.conf import PROJECT_DIR
@ -47,11 +48,7 @@ urlpatterns = [
path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')), path(f'{admin_ui_prefix[1:]}/', include('oss.retrieval_urls')),
path(f'{chat_ui_prefix[1:]}/', include('oss.retrieval_urls')), path(f'{chat_ui_prefix[1:]}/', include('oss.retrieval_urls')),
] ]
urlpatterns += [ init_doc(urlpatterns, chat_urlpatterns)
path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由下面两个ui也是根据这个配置文件来生成的
path('doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # swagger-ui的路由
path('redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # redoc的路由
]
urlpatterns.append( urlpatterns.append(
re_path(r'^static/(?P<path>.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'), re_path(r'^static/(?P<path>.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'),
) )

View File

@ -21,18 +21,12 @@
<el-row :gutter="12"> <el-row :gutter="12">
<el-col :span="12" class="mt-16"> <el-col :span="12" class="mt-16">
<div class="flex"> <div class="flex">
<el-text type="info" <el-text type="info">{{ $t('views.applicationOverview.appInfo.publicAccessLink') }}
>{{ $t('views.applicationOverview.appInfo.publicAccessLink') }}
</el-text> </el-text>
<el-switch <el-switch v-model="accessToken.is_active" class="ml-8" size="small" inline-prompt
v-model="accessToken.is_active"
class="ml-8"
size="small"
inline-prompt
:active-text="$t('views.applicationOverview.appInfo.openText')" :active-text="$t('views.applicationOverview.appInfo.openText')"
:inactive-text="$t('views.applicationOverview.appInfo.closeText')" :inactive-text="$t('views.applicationOverview.appInfo.closeText')"
:before-change="() => changeState(accessToken.is_active)" :before-change="() => changeState(accessToken.is_active)" />
/>
</div> </div>
<div class="mt-4 mb-16 url-height flex align-center" style="margin-bottom: 37px"> <div class="mt-4 mb-16 url-height flex align-center" style="margin-bottom: 37px">
@ -45,12 +39,7 @@
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-tooltip effect="dark" :content="$t('common.refresh')" placement="top"> <el-tooltip effect="dark" :content="$t('common.refresh')" placement="top">
<el-button <el-button @click="refreshAccessToken" type="primary" text style="margin-left: 1px">
@click="refreshAccessToken"
type="primary"
text
style="margin-left: 1px"
>
<el-icon> <el-icon>
<RefreshRight /> <RefreshRight />
</el-icon> </el-icon>
@ -58,13 +47,8 @@
</el-tooltip> </el-tooltip>
</div> </div>
<div> <div>
<el-button <el-button v-if="accessToken?.is_active" :disabled="!accessToken?.is_active" tag="a" :href="shareUrl"
v-if="accessToken?.is_active" target="_blank">
:disabled="!accessToken?.is_active"
tag="a"
:href="shareUrl"
target="_blank"
>
<AppIcon iconName="app-create-chat" class="mr-4"></AppIcon> <AppIcon iconName="app-create-chat" class="mr-4"></AppIcon>
{{ $t('views.application.operation.toChat') }} {{ $t('views.application.operation.toChat') }}
</el-button> </el-button>
@ -72,11 +56,8 @@
<AppIcon iconName="app-create-chat" class="mr-4"></AppIcon> <AppIcon iconName="app-create-chat" class="mr-4"></AppIcon>
{{ $t('views.application.operation.toChat') }} {{ $t('views.application.operation.toChat') }}
</el-button> </el-button>
<el-button <el-button :disabled="!accessToken?.is_active" @click="openDialog"
:disabled="!accessToken?.is_active" v-if="permissionPrecise.overview_embed(id)">
@click="openDialog"
v-if="permissionPrecise.overview_embed(id)"
>
<AppIcon iconName="app-export" class="mr-4"></AppIcon> <AppIcon iconName="app-export" class="mr-4"></AppIcon>
{{ $t('views.applicationOverview.appInfo.embedInWebsite') }} {{ $t('views.applicationOverview.appInfo.embedInWebsite') }}
</el-button> </el-button>
@ -88,10 +69,7 @@
{{ $t('views.applicationOverview.appInfo.accessControl') }} {{ $t('views.applicationOverview.appInfo.accessControl') }}
</el-button> </el-button>
<!-- 显示设置 --> <!-- 显示设置 -->
<el-button <el-button @click="openDisplaySettingDialog" v-if="permissionPrecise.overview_display(id)">
@click="openDisplaySettingDialog"
v-if="permissionPrecise.overview_display(id)"
>
<el-icon class="mr-4"> <el-icon class="mr-4">
<Setting /> <Setting />
</el-icon> </el-icon>
@ -101,19 +79,13 @@
</el-col> </el-col>
<el-col :span="12" class="mt-16"> <el-col :span="12" class="mt-16">
<div class="flex"> <div class="flex">
<el-text type="info" <el-text type="info">{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
>{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
</el-text> </el-text>
</div> </div>
<div class="mt-4 mb-16 url-height"> <div class="mt-4 mb-16 url-height">
<div> <div>
<el-text>API {{ $t('common.fileUpload.document') }} </el-text> <el-text>API {{ $t('common.fileUpload.document') }} </el-text>
<el-button <el-button type="primary" link @click="toUrl(apiUrl)" class="vertical-middle lighter break-all">
type="primary"
link
@click="toUrl(apiUrl)"
class="vertical-middle lighter break-all"
>
{{ apiUrl }} {{ apiUrl }}
</el-button> </el-button>
</div> </div>
@ -124,7 +96,7 @@
<span class="vertical-middle lighter break-all ellipsis-1">{{ <span class="vertical-middle lighter break-all ellipsis-1">{{
baseUrl + id baseUrl + id
}}</span> }}</span>
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top"> <el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button type="primary" text @click="copyClick(baseUrl + id)"> <el-button type="primary" text @click="copyClick(baseUrl + id)">
<AppIcon iconName="app-copy"></AppIcon> <AppIcon iconName="app-copy"></AppIcon>
@ -133,10 +105,7 @@
</div> </div>
</div> </div>
<div> <div>
<el-button <el-button @click="openAPIKeyDialog" v-if="permissionPrecise.overview_api_key(id)">
@click="openAPIKeyDialog"
v-if="permissionPrecise.overview_api_key(id)"
>
<el-icon class="mr-4"> <el-icon class="mr-4">
<Key /> <Key />
</el-icon> </el-icon>
@ -152,29 +121,13 @@
{{ $t('views.applicationOverview.monitor.monitoringStatistics') }} {{ $t('views.applicationOverview.monitor.monitoringStatistics') }}
</h4> </h4>
<div class="mb-16"> <div class="mb-16">
<el-select <el-select v-model="history_day" class="mr-12" @change="changeDayHandle" style="width: 180px">
v-model="history_day" <el-option v-for="item in dayOptions" :key="item.value" :label="item.label" :value="item.value" />
class="mr-12"
@change="changeDayHandle"
style="width: 180px"
>
<el-option
v-for="item in dayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
<el-date-picker <el-date-picker v-if="history_day === 'other'" v-model="daterangeValue" type="daterange"
v-if="history_day === 'other'"
v-model="daterangeValue"
type="daterange"
:start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')" :start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')"
:end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')" :end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')" format="YYYY-MM-DD"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" @change="changeDayRangeHandle" />
value-format="YYYY-MM-DD"
@change="changeDayRangeHandle"
/>
</div> </div>
<div v-loading="statisticsLoading"> <div v-loading="statisticsLoading">
<StatisticsCharts :data="statisticsData" /> <StatisticsCharts :data="statisticsData" />
@ -183,11 +136,7 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
<EmbedDialog <EmbedDialog ref="EmbedDialogRef" :data="detail" :api-input-params="mapToUrlParams(apiInputParams)" />
ref="EmbedDialogRef"
:data="detail"
:api-input-params="mapToUrlParams(apiInputParams)"
/>
<APIKeyDialog ref="APIKeyDialogRef" /> <APIKeyDialog ref="APIKeyDialogRef" />
<!-- 社区版访问限制 --> <!-- 社区版访问限制 -->
@ -232,7 +181,7 @@ const {
params: { id }, params: { id },
} = route as any } = route as any
const apiUrl = window.location.origin + '/doc/chat/' const apiUrl = window.location.origin + '/doc_chat/'
const baseUrl = window.location.origin + `${window.MaxKB.chatPrefix}/api/` const baseUrl = window.location.origin + `${window.MaxKB.chatPrefix}/api/`
@ -373,7 +322,7 @@ function refreshAccessToken() {
const str = t('views.applicationOverview.appInfo.refreshToken.refreshSuccess') const str = t('views.applicationOverview.appInfo.refreshToken.refreshSuccess')
updateAccessToken(obj, str) updateAccessToken(obj, str)
}) })
.catch(() => {}) .catch(() => { })
} }
async function changeState(bool: boolean) { async function changeState(bool: boolean) {
@ -419,20 +368,20 @@ function getDetail() {
.map((v: any) => { .map((v: any) => {
apiInputParams.value = v.properties.api_input_field_list apiInputParams.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => { ? v.properties.api_input_field_list.map((v: any) => {
return { return {
name: v.variable, name: v.variable,
value: v.default_value, value: v.default_value,
} }
}) })
: v.properties.input_field_list : v.properties.input_field_list
? v.properties.input_field_list ? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input') .filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => { .map((v: any) => {
return { return {
name: v.variable, name: v.variable,
value: v.default_value, value: v.default_value,
} }
}) })
: [] : []
}) })
}) })