353 lines
12 KiB
Python
353 lines
12 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from typing import Any, Dict, List
|
|
|
|
from sqlmodel import Session
|
|
|
|
from core.settings import (
|
|
DEFAULT_CHAT_PULL_PAGE_SIZE,
|
|
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
|
DEFAULT_PAGE_SIZE,
|
|
DEFAULT_STT_AUDIO_FILTER,
|
|
DEFAULT_STT_AUDIO_PREPROCESS,
|
|
DEFAULT_STT_DEFAULT_LANGUAGE,
|
|
DEFAULT_STT_FORCE_SIMPLIFIED,
|
|
DEFAULT_STT_INITIAL_PROMPT,
|
|
DEFAULT_STT_MAX_AUDIO_SECONDS,
|
|
DEFAULT_UPLOAD_MAX_MB,
|
|
DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS,
|
|
STT_DEVICE,
|
|
STT_ENABLED_DEFAULT,
|
|
STT_MODEL,
|
|
)
|
|
from models.platform import PlatformSetting
|
|
from schemas.platform import LoadingPageSettings, PlatformSettingsPayload, SystemSettingItem
|
|
|
|
from services.platform_common import utcnow
|
|
|
|
DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS: tuple[str, ...] = ()
|
|
DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS = 7
|
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY = "activity_event_retention_days"
|
|
SYS_AUTH_TOKEN_TTL_DAYS_SETTING_KEY = "sys_auth_token_ttl_days"
|
|
SETTING_KEYS = (
|
|
"page_size",
|
|
"chat_pull_page_size",
|
|
"command_auto_unlock_seconds",
|
|
"upload_max_mb",
|
|
"allowed_attachment_extensions",
|
|
"workspace_download_extensions",
|
|
"speech_enabled",
|
|
)
|
|
PROTECTED_SETTING_KEYS = set(SETTING_KEYS) | {
|
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY,
|
|
}
|
|
DEPRECATED_SETTING_KEYS = {
|
|
"loading_page",
|
|
"speech_max_audio_seconds",
|
|
"speech_default_language",
|
|
"speech_force_simplified",
|
|
"speech_audio_preprocess",
|
|
"speech_audio_filter",
|
|
"speech_initial_prompt",
|
|
"dashboard_activity_page_size",
|
|
}
|
|
SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
|
"page_size": {
|
|
"name": "分页大小",
|
|
"category": "ui",
|
|
"description": "平台各类列表默认每页条数。",
|
|
"value_type": "integer",
|
|
"value": DEFAULT_PAGE_SIZE,
|
|
"is_public": True,
|
|
"sort_order": 5,
|
|
},
|
|
"chat_pull_page_size": {
|
|
"name": "对话懒加载条数",
|
|
"category": "chat",
|
|
"description": "Bot 对话区向上懒加载时每次读取的消息条数。",
|
|
"value_type": "integer",
|
|
"value": DEFAULT_CHAT_PULL_PAGE_SIZE,
|
|
"is_public": True,
|
|
"sort_order": 8,
|
|
},
|
|
"command_auto_unlock_seconds": {
|
|
"name": "发送按钮自动恢复秒数",
|
|
"category": "chat",
|
|
"description": "对话发送后按钮保持停止态的最长秒数,超时后自动恢复为可发送状态。",
|
|
"value_type": "integer",
|
|
"value": DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
|
"is_public": True,
|
|
"sort_order": 9,
|
|
},
|
|
"upload_max_mb": {
|
|
"name": "上传大小限制",
|
|
"category": "upload",
|
|
"description": "单文件上传大小限制,单位 MB。",
|
|
"value_type": "integer",
|
|
"value": DEFAULT_UPLOAD_MAX_MB,
|
|
"is_public": False,
|
|
"sort_order": 10,
|
|
},
|
|
"allowed_attachment_extensions": {
|
|
"name": "允许附件后缀",
|
|
"category": "upload",
|
|
"description": "允许上传的附件后缀列表,留空表示不限制。",
|
|
"value_type": "json",
|
|
"value": list(DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS),
|
|
"is_public": False,
|
|
"sort_order": 20,
|
|
},
|
|
"workspace_download_extensions": {
|
|
"name": "工作区下载后缀",
|
|
"category": "workspace",
|
|
"description": "命中后缀的工作区文件默认走下载模式。",
|
|
"value_type": "json",
|
|
"value": list(DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS),
|
|
"is_public": False,
|
|
"sort_order": 30,
|
|
},
|
|
"speech_enabled": {
|
|
"name": "语音识别开关",
|
|
"category": "speech",
|
|
"description": "控制 Bot 语音转写功能是否启用。",
|
|
"value_type": "boolean",
|
|
"value": STT_ENABLED_DEFAULT,
|
|
"is_public": True,
|
|
"sort_order": 32,
|
|
},
|
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY: {
|
|
"name": "活动事件保留天数",
|
|
"category": "maintenance",
|
|
"description": "bot_activity_event 运维事件的保留天数,超期记录会自动清理。",
|
|
"value_type": "integer",
|
|
"value": DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS,
|
|
"is_public": False,
|
|
"sort_order": 34,
|
|
},
|
|
SYS_AUTH_TOKEN_TTL_DAYS_SETTING_KEY: {
|
|
"name": "登录令牌有效天数",
|
|
"category": "auth",
|
|
"description": "用户登录 JWT 的失效天数,默认 7 天,同时作为 Redis 会话 TTL。",
|
|
"value_type": "integer",
|
|
"value": 7,
|
|
"is_public": False,
|
|
"sort_order": 36,
|
|
},
|
|
}
|
|
|
|
|
|
def normalize_activity_event_retention_days(raw: Any) -> int:
|
|
try:
|
|
value = int(raw)
|
|
except Exception:
|
|
value = DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS
|
|
return max(1, min(3650, value))
|
|
|
|
|
|
def normalize_extension(raw: Any) -> str:
|
|
text = str(raw or "").strip().lower()
|
|
if not text:
|
|
return ""
|
|
if text.startswith("*."):
|
|
text = text[1:]
|
|
if not text.startswith("."):
|
|
text = f".{text}"
|
|
if not re.fullmatch(r"\.[a-z0-9][a-z0-9._+-]{0,31}", text):
|
|
return ""
|
|
return text
|
|
|
|
|
|
def normalize_extension_list(rows: Any) -> List[str]:
|
|
if not isinstance(rows, list):
|
|
return []
|
|
normalized: List[str] = []
|
|
for item in rows:
|
|
ext = normalize_extension(item)
|
|
if ext and ext not in normalized:
|
|
normalized.append(ext)
|
|
return normalized
|
|
|
|
|
|
def legacy_env_int(name: str, default: int, min_value: int, max_value: int) -> int:
|
|
raw = os.getenv(name)
|
|
if raw is None:
|
|
return default
|
|
try:
|
|
value = int(str(raw).strip())
|
|
except Exception:
|
|
value = default
|
|
return max(min_value, min(max_value, value))
|
|
|
|
|
|
def legacy_env_bool(name: str, default: bool) -> bool:
|
|
raw = os.getenv(name)
|
|
if raw is None:
|
|
return default
|
|
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def legacy_env_extensions(name: str, default: List[str]) -> List[str]:
|
|
raw = os.getenv(name)
|
|
if raw is None:
|
|
return list(default)
|
|
source = re.split(r"[,;\s]+", str(raw))
|
|
normalized: List[str] = []
|
|
for item in source:
|
|
ext = normalize_extension(item)
|
|
if ext and ext not in normalized:
|
|
normalized.append(ext)
|
|
return normalized
|
|
|
|
|
|
def bootstrap_platform_setting_values() -> Dict[str, Any]:
|
|
return {
|
|
"page_size": legacy_env_int("PAGE_SIZE", DEFAULT_PAGE_SIZE, 1, 100),
|
|
"chat_pull_page_size": legacy_env_int(
|
|
"CHAT_PULL_PAGE_SIZE",
|
|
DEFAULT_CHAT_PULL_PAGE_SIZE,
|
|
10,
|
|
500,
|
|
),
|
|
"command_auto_unlock_seconds": legacy_env_int(
|
|
"COMMAND_AUTO_UNLOCK_SECONDS",
|
|
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
|
1,
|
|
600,
|
|
),
|
|
"upload_max_mb": legacy_env_int("UPLOAD_MAX_MB", DEFAULT_UPLOAD_MAX_MB, 1, 2048),
|
|
"allowed_attachment_extensions": legacy_env_extensions(
|
|
"ALLOWED_ATTACHMENT_EXTENSIONS",
|
|
list(DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS),
|
|
),
|
|
"workspace_download_extensions": legacy_env_extensions(
|
|
"WORKSPACE_DOWNLOAD_EXTENSIONS",
|
|
list(DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS),
|
|
),
|
|
"speech_enabled": legacy_env_bool("STT_ENABLED", STT_ENABLED_DEFAULT),
|
|
}
|
|
|
|
|
|
def default_platform_settings() -> PlatformSettingsPayload:
|
|
bootstrap = bootstrap_platform_setting_values()
|
|
return PlatformSettingsPayload(
|
|
page_size=int(bootstrap["page_size"]),
|
|
chat_pull_page_size=int(bootstrap["chat_pull_page_size"]),
|
|
command_auto_unlock_seconds=int(bootstrap["command_auto_unlock_seconds"]),
|
|
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
|
allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]),
|
|
workspace_download_extensions=list(bootstrap["workspace_download_extensions"]),
|
|
speech_enabled=bool(bootstrap["speech_enabled"]),
|
|
speech_max_audio_seconds=DEFAULT_STT_MAX_AUDIO_SECONDS,
|
|
speech_default_language=DEFAULT_STT_DEFAULT_LANGUAGE,
|
|
speech_force_simplified=DEFAULT_STT_FORCE_SIMPLIFIED,
|
|
speech_audio_preprocess=DEFAULT_STT_AUDIO_PREPROCESS,
|
|
speech_audio_filter=DEFAULT_STT_AUDIO_FILTER,
|
|
speech_initial_prompt=DEFAULT_STT_INITIAL_PROMPT,
|
|
loading_page=LoadingPageSettings(),
|
|
)
|
|
|
|
|
|
def normalize_setting_key(raw: Any) -> str:
|
|
text = str(raw or "").strip()
|
|
return re.sub(r"[^a-zA-Z0-9_.-]+", "_", text).strip("._-").lower()
|
|
|
|
|
|
def normalize_setting_value(value: Any, value_type: str) -> Any:
|
|
normalized_type = str(value_type or "json").strip().lower() or "json"
|
|
if normalized_type == "integer":
|
|
return int(value or 0)
|
|
if normalized_type == "float":
|
|
return float(value or 0)
|
|
if normalized_type == "boolean":
|
|
if isinstance(value, bool):
|
|
return value
|
|
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
if normalized_type == "string":
|
|
return str(value or "")
|
|
if normalized_type == "json":
|
|
return value
|
|
raise ValueError(f"Unsupported value_type: {normalized_type}")
|
|
|
|
|
|
def read_setting_value(row: PlatformSetting) -> Any:
|
|
try:
|
|
value = json.loads(row.value_json or "null")
|
|
except Exception:
|
|
value = None
|
|
return normalize_setting_value(value, row.value_type)
|
|
|
|
|
|
def setting_item_from_row(row: PlatformSetting) -> Dict[str, Any]:
|
|
return SystemSettingItem(
|
|
key=row.key,
|
|
name=row.name,
|
|
category=row.category,
|
|
description=row.description,
|
|
value_type=row.value_type,
|
|
value=read_setting_value(row),
|
|
is_public=bool(row.is_public),
|
|
sort_order=int(row.sort_order or 100),
|
|
created_at=row.created_at.isoformat() + "Z",
|
|
updated_at=row.updated_at.isoformat() + "Z",
|
|
).model_dump()
|
|
|
|
|
|
def upsert_setting_row(
|
|
session: Session,
|
|
key: str,
|
|
*,
|
|
name: str,
|
|
category: str,
|
|
description: str,
|
|
value_type: str,
|
|
value: Any,
|
|
is_public: bool,
|
|
sort_order: int,
|
|
) -> PlatformSetting:
|
|
normalized_key = normalize_setting_key(key)
|
|
if not normalized_key:
|
|
raise ValueError("Setting key is required")
|
|
normalized_type = str(value_type or "json").strip().lower() or "json"
|
|
normalized_value = normalize_setting_value(value, normalized_type)
|
|
now = utcnow()
|
|
row = session.get(PlatformSetting, normalized_key)
|
|
if row is None:
|
|
row = PlatformSetting(
|
|
key=normalized_key,
|
|
name=str(name or normalized_key),
|
|
category=str(category or "general"),
|
|
description=str(description or ""),
|
|
value_type=normalized_type,
|
|
value_json=json.dumps(normalized_value, ensure_ascii=False),
|
|
is_public=bool(is_public),
|
|
sort_order=int(sort_order or 100),
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
else:
|
|
row.name = str(name or row.name or normalized_key)
|
|
row.category = str(category or row.category or "general")
|
|
row.description = str(description or row.description or "")
|
|
row.value_type = normalized_type
|
|
row.value_json = json.dumps(normalized_value, ensure_ascii=False)
|
|
row.is_public = bool(is_public)
|
|
row.sort_order = int(sort_order or row.sort_order or 100)
|
|
row.updated_at = now
|
|
session.add(row)
|
|
return row
|
|
|
|
|
|
def build_speech_runtime_settings(settings: PlatformSettingsPayload) -> Dict[str, Any]:
|
|
return {
|
|
"enabled": bool(settings.speech_enabled),
|
|
"max_audio_seconds": int(DEFAULT_STT_MAX_AUDIO_SECONDS),
|
|
"default_language": str(DEFAULT_STT_DEFAULT_LANGUAGE or "zh").strip().lower() or "zh",
|
|
"force_simplified": bool(DEFAULT_STT_FORCE_SIMPLIFIED),
|
|
"audio_preprocess": bool(DEFAULT_STT_AUDIO_PREPROCESS),
|
|
"audio_filter": str(DEFAULT_STT_AUDIO_FILTER or "").strip(),
|
|
"initial_prompt": str(DEFAULT_STT_INITIAL_PROMPT or "").strip(),
|
|
"model": STT_MODEL,
|
|
"device": STT_DEVICE,
|
|
}
|