Skip to content

Commit 1da185c

Browse files
Merge remote-tracking branch 'upstream/v2' into feat/intent-node/config-output-reason
2 parents bc1113d + 675fdd0 commit 1da185c

File tree

28 files changed

+862
-583
lines changed

28 files changed

+862
-583
lines changed

apps/application/serializers/application.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,8 +1036,8 @@ def edit(self, instance: Dict, with_valid=True):
10361036
if 'work_flow' in instance:
10371037
# 修改语音配置相关
10381038
self.update_work_flow_model(instance)
1039-
if 'mcp_servers' in instance:
1040-
ToolExecutor().validate_mcp_transport(instance.get('mcp_servers'))
1039+
if 'mcp_servers' in instance and len(instance.get('mcp_servers', {})) > 0:
1040+
ToolExecutor().validate_mcp_transport(json.dumps(instance.get('mcp_servers')))
10411041
update_keys = ['name', 'desc', 'model_id', 'multiple_rounds_dialogue', 'prologue', 'status',
10421042
'knowledge_setting', 'model_setting', 'problem_optimization', 'dialogue_number',
10431043
'stt_model_id', 'tts_model_id', 'tts_model_enable', 'stt_model_enable', 'tts_type',

apps/tools/serializers/tool.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,17 +613,19 @@ def one(self):
613613
'size': skill_file.file_size,
614614
} if skill_file else None
615615
work_flow = {}
616+
is_publish = False
616617
if tool.tool_type == 'WORKFLOW':
617618
tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first()
618619
if tool_workflow:
619620
work_flow = tool_workflow.work_flow
620-
621+
is_publish = tool_workflow.is_publish
621622
return {
622623
**ToolModelSerializer(tool).data,
623624
'init_params': tool.init_params if tool.init_params else {},
624625
'nick_name': nick_name,
625626
'fileList': [skill_file_dict] if tool.tool_type == 'SKILL' else [],
626-
'work_flow': work_flow
627+
'work_flow': work_flow,
628+
'is_publish': is_publish
627629
}
628630

629631
def export(self):

apps/users/serializers/login.py

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
@desc:
88
"""
99
import base64
10-
import datetime
1110
import json
11+
import logging
1212

1313
from captcha.image import ImageCaptcha
1414
from django.core import signing
@@ -23,9 +23,10 @@
2323
from common.database_model_manage.database_model_manage import DatabaseModelManage
2424
from common.exception.app_exception import AppApiException
2525
from common.utils.common import password_encrypt, get_random_chars
26-
from common.utils.rsa_util import encrypt, decrypt
26+
from common.utils.rsa_util import decrypt
2727
from maxkb.const import CONFIG
2828
from users.models import User
29+
from common.utils.logger import maxkb_logger
2930

3031

3132
class LoginRequest(serializers.Serializer):
@@ -48,26 +49,34 @@ class LoginResponse(serializers.Serializer):
4849

4950

5051
def record_login_fail(username: str, expire: int = 600):
51-
"""记录登录失败次数"""
52+
"""记录登录失败次数(原子)返回当前失败计数"""
5253
if not username:
53-
return
54+
return 0
5455
fail_key = system_get_key(f'system_{username}')
55-
fail_count = cache.get(fail_key, version=system_version)
56-
if fail_count is None:
56+
try:
57+
fail_count = cache.incr(fail_key, 1, version=system_version)
58+
except ValueError:
59+
# key 不存在,初始化并设置过期
5760
cache.set(fail_key, 1, timeout=expire, version=system_version)
58-
else:
59-
cache.incr(fail_key, 1, version=system_version)
61+
fail_count = 1
62+
return fail_count
6063

6164

6265
def record_login_fail_lock(username: str, expire: int = 10):
66+
"""
67+
使用 cache.incr 保证原子递增,并在不存在时初始化计数器并返回当前值。
68+
这里的计数器用于判断是否应当进入“锁定”分支,避免依赖非原子 get -> set 的组合。
69+
"""
6370
if not username:
64-
return
71+
return 0
6572
fail_key = system_get_key(f'system_{username}_lock_count')
66-
fail_count = cache.get(fail_key, version=system_version)
67-
if fail_count is None:
73+
try:
74+
fail_count = cache.incr(fail_key, 1, version=system_version)
75+
except ValueError:
76+
# key 不存在,初始化并设置过期(分钟转秒)
6877
cache.set(fail_key, 1, timeout=expire * 60, version=system_version)
69-
else:
70-
cache.incr(fail_key, 1, version=system_version)
78+
fail_count = 1
79+
return fail_count
7180

7281

7382
class LoginSerializer(serializers.Serializer):
@@ -93,8 +102,16 @@ def login(instance):
93102
encrypted_data = instance.get("encryptedData", "")
94103

95104
if encrypted_data:
96-
decrypted_data = json.loads(decrypt(encrypted_data))
97-
instance.update(decrypted_data)
105+
try:
106+
decrypted_raw = decrypt(encrypted_data)
107+
# decrypt 可能返回非 JSON 字符串,防护解析异常
108+
decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {}
109+
if isinstance(decrypted_data, dict):
110+
instance.update(decrypted_data)
111+
except Exception as e:
112+
maxkb_logger.exception("Failed to decrypt/parse encryptedData for user %s: %s", username, e)
113+
raise AppApiException(500, _("Invalid encrypted data"))
114+
98115
try:
99116
LoginRequest(data=instance).is_valid(raise_exception=True)
100117
except serializers.ValidationError:
@@ -128,7 +145,7 @@ def login(instance):
128145
LoginSerializer._validate_captcha(username, captcha)
129146

130147
# 验证用户凭据
131-
user = QuerySet(User).filter(
148+
user = User.objects.filter(
132149
username=username,
133150
password=password_encrypt(password)
134151
).first()
@@ -190,34 +207,61 @@ def _validate_captcha(username: str, captcha: str) -> None:
190207

191208
@staticmethod
192209
def _handle_failed_login(username: str, is_license_valid: bool, failed_attempts: int, lock_time: int) -> None:
193-
"""处理登录失败"""
194-
record_login_fail(username)
195-
record_login_fail_lock(username, lock_time)
210+
"""处理登录失败
211+
212+
修复要点:
213+
- 使用 record_login_fail / record_login_fail_lock 两个原子 incr 来记录失败;
214+
- 不再依赖精确等于 0 的比较来触发锁,而是基于原子计数 >= 阈值来决定进入锁定分支;
215+
- 使用 cache.add 原子创建锁键,cache.add 保证只有第一个成功创建者可写入该键;
216+
其他并发到达的请求若发现计数已到达阈值也应当返回“已锁定”响应,避免出现绕过。
217+
"""
218+
# 记录普通失败计数(供验证码触发使用)
219+
try:
220+
record_login_fail(username)
221+
except Exception:
222+
maxkb_logger.exception("Failed to record login fail for user %s", username)
223+
224+
# 记录用于锁定判断的失败计数(按 lock_time 作为初始化过期分钟)
225+
lock_fail_count = 0
226+
try:
227+
lock_fail_count = record_login_fail_lock(username, lock_time)
228+
except Exception:
229+
maxkb_logger.exception("Failed to record lock fail count for user %s", username)
196230

231+
# 如果不是企业版或禁用锁定功能,直接返回(但计数已经记录)
197232
if not is_license_valid or failed_attempts <= 0:
198233
return
199234

200-
fail_count = cache.get(system_get_key(f'system_{username}_lock_count'), version=system_version) or 0
201-
remain_attempts = failed_attempts - fail_count
202-
203-
if remain_attempts > 0:
235+
# 当计数小于阈值,告知剩余尝试次数
236+
if lock_fail_count < failed_attempts:
237+
remain_attempts = failed_attempts - lock_fail_count
204238
raise AppApiException(
205239
1005,
206240
_("Login failed %s times, account will be locked, you have %s more chances !") % (
207241
failed_attempts, remain_attempts
208242
)
209243
)
210-
elif remain_attempts == 0:
211-
cache.set(
244+
245+
# 当计数达到或超过阈值时,尝试原子创建锁键;无论 cache.add 返回 True/False,都返回已锁定响应,
246+
# 因为若为 False 说明其他并发请求已将账户标记为锁定,行为应一致。
247+
try:
248+
locked = cache.add(
212249
system_get_key(f'system_{username}_lock'),
213250
1,
214251
timeout=lock_time * 60,
215252
version=system_version
216253
)
217-
raise AppApiException(
218-
1005,
219-
_("This account has been locked for %s minutes, please try again later") % lock_time
220-
)
254+
if locked:
255+
maxkb_logger.info("Account %s locked by setting cache key", username)
256+
else:
257+
maxkb_logger.info("Account %s lock key already present (another request set it)", username)
258+
except Exception:
259+
maxkb_logger.exception("Failed to set lock key for user %s", username)
260+
261+
raise AppApiException(
262+
1005,
263+
_("This account has been locked for %s minutes, please try again later") % lock_time
264+
)
221265

222266

223267
class CaptchaResponse(serializers.Serializer):

installer/sandbox.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ long syscall(long number, ...) {
511511
case SYS_accept:
512512
case SYS_accept4:
513513
case SYS_sendto:
514+
case SYS_sendmsg:
514515
case SYS_recvmsg:
515516
case SYS_getsockopt:
516517
case SYS_setsockopt:
@@ -542,6 +543,9 @@ long syscall(long number, ...) {
542543
case SYS_shmget:
543544
case SYS_shmctl:
544545
case SYS_prctl:
546+
case SYS_io_uring_setup:
547+
case SYS_io_uring_enter:
548+
case SYS_io_uring_register:
545549
if (!allow_access_syscall()) {
546550
throw_permission_denied_err(true, "access syscall %ld", number);
547551
}

installer/start-maxkb.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,16 @@ fi
1111
mkdir -p /opt/maxkb/python-packages
1212

1313
rm -f /opt/maxkb-app/tmp/*
14+
15+
INIT_SHELL_DIR="/opt/maxkb/local/init-shells"
16+
if [ -d "$INIT_SHELL_DIR" ]; then
17+
find "$INIT_SHELL_DIR" -maxdepth 1 -type f -name "*.sh" | sort | while IFS= read -r f; do
18+
if bash "$f"; then
19+
echo "[OK] init-shell >>> $f"
20+
else
21+
echo "[ERROR] init-shell >>> $f failed with exit code $?" >&2
22+
fi
23+
done
24+
fi
25+
1426
python /opt/maxkb-app/main.py start

ui/src/components/ai-chat/index.vue

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ import bus from '@/bus'
195195
import { throttle } from 'lodash-es'
196196
import { copyClick } from '@/utils/clipboard'
197197
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
198+
import { getWrite } from '@/utils/chat'
198199
199200
provide('upload', (file: any, loading?: Ref<boolean>) => {
200201
return props.type === 'debug-ai-chat'
@@ -560,86 +561,7 @@ function getChartOpenId(chat?: any, problem?: string, re_chat?: boolean, other_p
560561
})
561562
}
562563
563-
/**
564-
* 获取一个递归函数,处理流式数据
565-
* @param chat 每一条对话记录
566-
* @param reader 流数据
567-
* @param stream 是否是流式数据
568-
*/
569-
const getWrite = (chat: any, reader: any, stream: boolean) => {
570-
let tempResult = ''
571-
572-
const write_stream = async () => {
573-
try {
574-
while (true) {
575-
const { done, value } = await reader.read()
576-
577-
if (done) {
578-
ChatManagement.close(chat.id)
579-
return
580-
}
581-
582-
const decoder = new TextDecoder('utf-8')
583-
let str = decoder.decode(value, { stream: true })
584-
585-
tempResult += str
586-
const split = tempResult.match(/data:.*?}\n\n/g)
587-
if (split) {
588-
str = split.join('')
589-
tempResult = tempResult.replace(str, '')
590-
591-
// 批量处理所有 chunk
592-
for (const item of split) {
593-
const chunk = JSON.parse(item.replace('data:', ''))
594-
chat.chat_id = chunk.chat_id
595-
chat.record_id = chunk.chat_record_id
596-
597-
if (!chunk.is_end) {
598-
ChatManagement.appendChunk(chat.id, chunk)
599-
}
600-
601-
if (chunk.is_end) {
602-
return Promise.resolve()
603-
}
604-
}
605-
}
606-
// 如果没有匹配到完整chunk,继续读取下一块
607-
}
608-
} catch (e) {
609-
return Promise.reject(e)
610-
}
611-
}
612-
613-
const write_json = async () => {
614-
try {
615-
while (true) {
616-
const { done, value } = await reader.read()
617-
618-
if (done) {
619-
const result_block = JSON.parse(tempResult)
620-
if (result_block.code === 500) {
621-
return Promise.reject(result_block.message)
622-
} else {
623-
if (result_block.content) {
624-
ChatManagement.append(chat.id, result_block.content)
625-
}
626-
}
627-
ChatManagement.close(chat.id)
628-
return
629-
}
630564
631-
if (value) {
632-
const decoder = new TextDecoder('utf-8')
633-
tempResult += decoder.decode(value)
634-
}
635-
}
636-
} catch (e) {
637-
return Promise.reject(e)
638-
}
639-
}
640-
641-
return stream ? write_stream : write_json
642-
}
643565
const errorWrite = (chat: any, message?: string) => {
644566
ChatManagement.addChatRecord(chat, 50, loading)
645567
ChatManagement.write(chat.id)

ui/src/components/dynamics-form/constructor/data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,9 @@ const input_type_list = [
5959
label: t('dynamicsForm.input_type_list.Model'),
6060
value: 'Model',
6161
},
62+
{
63+
label: t('dynamicsForm.input_type_list.Knowledge'),
64+
value: 'Knowledge',
65+
},
6266
]
6367
export { input_type_list }

0 commit comments

Comments
 (0)