From 8e6da99dcb35353afc4d1f9aea7ff590cb02f69d Mon Sep 17 00:00:00 2001
From: Nelson <1475262689@qq.com>
Date: Mon, 3 Aug 2026 17:42:17 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E5=8D=87=E5=B9=B6=E5=8F=91?=
=?UTF-8?q?=E5=AE=B9=E9=87=8F=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../apps/admin-web/src/App.vue | 6 ++
.../admin-web/src/config/systemSettings.ts | 26 ++++--
.../apps/admin-web/src/styles.css | 8 ++
.../versions/0028_chat_concurrency_default.py | 84 +++++++++++++++++++
.../apps/backend/app/core/config.py | 6 +-
.../tests/test_production_readiness.py | 9 ++
6 files changed, 130 insertions(+), 9 deletions(-)
create mode 100644 ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue
index 43ca050..1b90f9d 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue
+++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue
@@ -1067,6 +1067,12 @@ async function clearFeishuCache() {
v-model="systemSettingValues[setting.key]"
/>
{{ setting.description }}
+
+ {{ setting.valueHint(systemSettingValues[setting.key]) }}
+
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts b/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts
index 5a92434..9c1958c 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts
+++ b/ai_knowledge_base_v2/apps/admin-web/src/config/systemSettings.ts
@@ -16,6 +16,7 @@ export interface SystemSettingDefinition {
placeholder?: string;
options?: SystemSettingOption[];
description: string;
+ valueHint?: (value: SystemSettingValue) => string;
}
export interface SystemSettingSection {
@@ -181,28 +182,29 @@ export const systemSettingSections: SystemSettingSection[] = [
key: "chat_max_active_requests",
label: "问答最大并发数",
type: "number",
- defaultValue: 2,
+ defaultValue: 30,
min: 1,
max: 1000,
- description: "同一后端进程内同时进入飞书和模型生成链路的最大请求数。",
+ description: "用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。",
+ valueHint: concurrentCapacityHint,
},
{
key: "chat_max_queue_size",
label: "问答最大排队数",
type: "number",
- defaultValue: 20,
+ defaultValue: 90,
min: 0,
max: 10000,
- description: "超过最大并发后允许等待的请求数量,超过后直接提示稍后再试。",
+ description: "超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留,避免课程结束后的集中提问被直接拒绝。",
},
{
key: "chat_queue_timeout_seconds",
label: "问答排队超时(秒)",
type: "number",
- defaultValue: 60,
+ defaultValue: 90,
min: 1,
max: 3600,
- description: "请求在排队中超过该时间后自动结束并提示用户稍后再试。",
+ description: "请求在排队中超过该时间后自动结束。默认 90 秒可覆盖 30 并发下约三轮请求释放。",
},
{
key: "chat_active_lease_seconds",
@@ -292,4 +294,16 @@ export const systemSettingSections: SystemSettingSection[] = [
},
];
+function concurrentCapacityHint(value: SystemSettingValue) {
+ const concurrency = Math.max(1, Number(value) || 1);
+ const lower = roundDownToTen((concurrency * 120) / 25 / 1.4);
+ const upper = roundDownToTen((concurrency * 120) / 25 / 1.2);
+ const capacity = lower === upper ? `约 ${lower} 人` : `约 ${lower}–${upper} 人`;
+ return `容量估算:按单次问答约 25 秒并预留 20%–40% 波动,当前 ${concurrency} 并发可承接 2 分钟内${capacity}集中提问。实际能力还受模型服务商额度和回答时长影响。`;
+}
+
+function roundDownToTen(value: number) {
+ return Math.max(1, Math.floor(value / 10) * 10);
+}
+
export const systemSettingDefinitions = systemSettingSections.flatMap((section) => section.settings);
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/styles.css b/ai_knowledge_base_v2/apps/admin-web/src/styles.css
index 829aa09..056ddf5 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/styles.css
+++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css
@@ -1725,6 +1725,14 @@ textarea {
line-height: 1.5;
}
+.setting-field > .setting-value-hint {
+ padding: 9px 11px;
+ border: 1px solid #cde6dc;
+ border-radius: 8px;
+ background: #f3faf7;
+ color: #246b52;
+}
+
.setting-field .el-input-number,
.setting-field .el-input,
.setting-field .el-select {
diff --git a/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py b/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py
new file mode 100644
index 0000000..2060d86
--- /dev/null
+++ b/ai_knowledge_base_v2/apps/backend/alembic/versions/0028_chat_concurrency_default.py
@@ -0,0 +1,84 @@
+"""raise the default chat concurrency to thirty
+
+Revision ID: 0028_chat_concurrency
+Revises: 0027_content_generation
+"""
+
+from __future__ import annotations
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision = "0028_chat_concurrency"
+down_revision = "0027_content_generation"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ config = sa.table(
+ "sys_system_config",
+ sa.column("config_key", sa.String(length=100)),
+ sa.column("config_value", sa.Text()),
+ sa.column("description", sa.String(length=255)),
+ )
+ connection = op.get_bind()
+ _set_default(
+ connection,
+ config,
+ key="chat_max_active_requests",
+ old_default="2",
+ new_default="30",
+ description="用户正式问答的全局执行上限;Redis 可用时由所有后端实例共享,超过后进入排队。",
+ )
+ _set_default(
+ connection,
+ config,
+ key="chat_max_queue_size",
+ old_default="20",
+ new_default="90",
+ description="超过最大并发后允许等待的请求数量。默认按并发数的 3 倍预留。",
+ )
+ _set_default(
+ connection,
+ config,
+ key="chat_queue_timeout_seconds",
+ old_default="60",
+ new_default="90",
+ description="请求在排队中超过该时间后自动结束。默认可覆盖约三轮请求释放。",
+ )
+
+
+def downgrade() -> None:
+ # 运行时配置可能已经被管理员调整,降级代码版本时不覆盖现场值。
+ pass
+
+
+def _set_default(
+ connection,
+ config,
+ *,
+ key: str,
+ old_default: str,
+ new_default: str,
+ description: str,
+) -> None:
+ current = connection.execute(
+ sa.select(config.c.config_value).where(config.c.config_key == key)
+ ).scalar_one_or_none()
+ if current is None:
+ connection.execute(
+ config.insert().values(
+ config_key=key,
+ config_value=new_default,
+ description=description,
+ )
+ )
+ return
+ if str(current).strip() == old_default:
+ connection.execute(
+ config.update()
+ .where(config.c.config_key == key)
+ .values(config_value=new_default, description=description)
+ )
diff --git a/ai_knowledge_base_v2/apps/backend/app/core/config.py b/ai_knowledge_base_v2/apps/backend/app/core/config.py
index 5fd8425..8c5f4d4 100644
--- a/ai_knowledge_base_v2/apps/backend/app/core/config.py
+++ b/ai_knowledge_base_v2/apps/backend/app/core/config.py
@@ -59,9 +59,9 @@ class Settings(BaseSettings):
default_daily_chat_limit: int = 100
default_user_name_prefix: str = "用户"
- chat_max_active_requests: int = 2
- chat_max_queue_size: int = 20
- chat_queue_timeout_seconds: int = 60
+ chat_max_active_requests: int = 30
+ chat_max_queue_size: int = 90
+ chat_queue_timeout_seconds: int = 90
chat_active_lease_seconds: int = 900
periodic_report_worker_enabled: bool = True
periodic_report_poll_seconds: int = 5
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py b/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py
index 3b4bbc9..b8fd344 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_production_readiness.py
@@ -27,6 +27,7 @@ from app.services.security_state_service import SecurityStateService
from app.services.auth_service import AuthService
from app.services.model_service import ModelClientService
from app.core.observability import RequestObservabilityMiddleware
+from app.core.config import Settings
@pytest.fixture(autouse=True)
@@ -43,6 +44,14 @@ def test_secret_is_encrypted_and_masked():
assert SecretService.masked(encrypted) == MASKED_SECRET
+def test_production_chat_capacity_defaults_cover_expected_burst():
+ settings = Settings(_env_file=None)
+
+ assert settings.chat_max_active_requests == 30
+ assert settings.chat_max_queue_size == 90
+ assert settings.chat_queue_timeout_seconds == 90
+
+
def test_migration_revision_ids_fit_default_alembic_version_column():
versions_dir = Path(__file__).parents[1] / "alembic" / "versions"
revision_pattern = re.compile(r'^revision\s*=\s*["\']([^"\']+)', re.MULTILINE)