Compare commits

...
14 Commits
Author SHA1 Message Date
wuzhiqiang 3ea8eff34d refactor: 替换密码哈希实现为原生bcrypt并更新依赖
重构CryptUtils类,替换原有的passlib密码哈希逻辑为直接使用bcrypt库;添加密码字节长度限制以符合bcrypt原生72字节上限;更新pyproject.toml配置,新增bcrypt依赖并将passlib设为可选依赖;统一代码缩进格式
2026-09-05 07:48:45 +08:00
wuzhiqiang 7e7afe3907 config: 添加lock文件 2026-09-04 22:07:42 +08:00
wuzhiqiang 4276015cf3 chore: 删除lock文件 2026-09-04 21:40:06 +08:00
wuzhiqiang 59ac17e609 config: 配置源 2026-09-04 21:37:19 +08:00
martsforever 83bd9f93a5 feat: adjust ai configs 2026-06-28 23:39:01 +08:00
martsforever 6282e1434c feat: optimize tool_project_report 2026-06-26 22:54:40 +08:00
martsforever 591213b40d feat: optimize tool_project_report 2026-06-26 22:49:48 +08:00
martsforever b1d6563fe3 feat: 视觉模型默认用 bailian-qwen-plus 2026-04-22 21:35:48 +08:00
martsforever f69a389aed feat: 酒店预定时调用接口超时时间增大 2026-03-27 10:14:40 +08:00
martsforever e41c8db8cf feat:更新豆包模型doubao-vision 2026-03-23 22:31:26 +08:00
martsforever ab073d09b3 feat: optimize code 2026-03-20 14:46:25 +08:00
martsforever 7b9d1253a5 feat: 优化连接相关配置 2026-03-20 13:59:11 +08:00
martsforever 150a416522 feat: FileModel增加content字段 2025-12-22 11:50:17 +08:00
martsforever 3f8a83c950 feat: 合成语音时一两设置为100 2025-12-22 11:30:19 +08:00
15 changed files with 4109 additions and 2979 deletions
+8 -2
View File
@@ -55,7 +55,7 @@ ai_configs = {
'key': env.llm_key_huoshan,
},
'doubao-vision-lite': {
'model': 'doubao-1.5-vision-lite-250315',
'model': 'doubao-1-5-vision-pro-32k-250115',
'url': 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
'key': env.llm_key_huoshan,
},
@@ -76,9 +76,15 @@ ai_configs = {
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
'key': env.llm_key_bailian,
},
'bailian-qwen-plus': {
'model': 'qwen3.6-plus',
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
'key': env.llm_key_bailian,
},
'bailian-embedding': {
'model': 'text-embedding-v4',
'url': 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
# 'url': 'https://llm-z0ugjqj2yf8a7i89.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/embeddings',
"url": "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings",
'key': env.llm_key_bailian,
},
}
+2 -2
View File
@@ -23,7 +23,7 @@ def add_sqlmodel_route(app: FastAPI):
if user_dict.get("id") is None:
raise HTTPException(status_code=500, detail="Update row missing id")
update_user = (await session.exec(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
update_user = (await session.execute(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
if update_user is None:
raise HTTPException(status_code=500, detail="Update row not found")
@@ -41,7 +41,7 @@ def add_sqlmodel_route(app: FastAPI):
if user_dict.get("id") is None:
raise HTTPException(status_code=500, detail="Update row missing id")
delete_user: LlmUser = (await session.exec(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
delete_user: LlmUser = (await session.execute(select(LlmUser).where(LlmUser.id == user_dict["id"]))).first()
if delete_user is None:
raise HTTPException(status_code=500, detail="Delete row not found")
@@ -33,5 +33,5 @@ class VoiceGenerateParamSchema(BaseModel):
model: str = Field(default="sambert-zhichu-v1", description="模型名称")
sample_rate: int = Field(default=16000, description="采样率")
format: str = Field(default="mp3", description="音频格式")
volume: int = Field(default=1, description="音量大小")
volume: int = Field(default=100, description="音量大小")
rate: int = Field(default=1, description="语速")
+2 -2
View File
@@ -112,9 +112,9 @@ add_routes(
add_routes(
app=app,
runnable=RunnableLambda(lambda x: x['messages']) | create_llm("doubao-vision-lite") | StrOutputParser(),
runnable=RunnableLambda(lambda x: x['messages']) | create_llm("bailian-qwen-plus") | StrOutputParser(),
input_type=ModelInputSchema,
path="/doubao-vision-lite"
path="/bailian-qwen-plus"
)
add_routes(
+1
View File
@@ -23,6 +23,7 @@ class FileModel(BasicModel, table=True):
attr1: Optional[str] = Field(default=None, description='扩展属性1')
attr2: Optional[str] = Field(default=None, description='扩展属性2')
attr3: Optional[str] = Field(default=None, description='扩展属性3')
content: Optional[str] = Field(default=None, description='扩展属性内容文本')
FileService = create_model_service(Cls=FileModel)
+1 -1
View File
@@ -34,7 +34,7 @@ async def tool_book_hotel(config: RunnableConfig) -> str:
# print("proj_id", proj_id)
# print("user_id", user_id)
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(
url=f"http://localhost:{env.server_port}/book_hotel",
headers={
+2 -2
View File
@@ -14,7 +14,7 @@ from app.utils.db_utils import async_session, AsyncSessionDep
@tool(
name_or_callable="tool_project_report",
description="查询项目日报周报的工具,在调用这个工具之前要先调用获取时间工具"
description="查询项目日报周报的工具,在调用这个工具之前要先调用获取时间工具。这个工具调用结束之后,需要你对工具执行结果做一个分析总结,不能仅仅回复“工具已经执行完毕”"
)
async def tool_project_report(
project_name: Annotated[str, "项目名称"],
@@ -49,7 +49,7 @@ async def tool_project_report(
"content": doc.content,
})
return json.dumps(user_reports, ensure_ascii=False)
return f"报告信息如下:{json.dumps(user_reports, ensure_ascii=False)}"
async def query_users_reports(session: AsyncSessionDep, user_id_list: List[str], start_time: Optional[str], end_time: Optional[str]):
+22 -13
View File
@@ -1,44 +1,53 @@
from datetime import timedelta, datetime, timezone
from typing import TypedDict, Literal, TypeAlias
import bcrypt
import jwt
from passlib.context import CryptContext
from app.config.env import env
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
BCRYPT_MAX_BYTES = 72
# token的类型,access用于接口认证,refresh用于刷新access token,verify用于激活用户账号
AccessTokenType: TypeAlias = Literal["access", "refresh", "verify", "api"]
class TokenInfo(TypedDict):
# 用户名信息
username: str
# token过期时间
exp: datetime
type: AccessTokenType
def _safe_password_bytes(password: str) -> bytes:
data = password.encode("utf-8")
if len(data) > BCRYPT_MAX_BYTES:
data = data[:BCRYPT_MAX_BYTES]
return data
class CryptUtils:
@staticmethod
def get_password_hash(password: str):
return pwd_context.hash(password)
return bcrypt.hashpw(_safe_password_bytes(password), bcrypt.gensalt()).decode(
"utf-8"
)
@staticmethod
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
try:
return bcrypt.checkpw(
_safe_password_bytes(plain_password), hashed_password.encode("utf-8")
)
except ValueError:
return False
except Exception:
return False
@staticmethod
def create_token(
username: str,
type: AccessTokenType,
expires_delta: timedelta
):
def create_token(username: str, type: AccessTokenType, expires_delta: timedelta):
data: TokenInfo = {
"username": username,
"type": type,
"exp": datetime.now(timezone.utc) + expires_delta
"exp": datetime.now(timezone.utc) + expires_delta,
}
return jwt.encode(data, env.jwt_secret_key, env.jwt_algorithm)
+4 -4
View File
@@ -402,7 +402,7 @@ def create_model_service(
if not row_dict.get('id'):
raise HTTPException(status_code=400, detail="ID不能为空")
# 根据id查询要更新的记录
update_cls = (await session.exec(self.select_cls().where(Cls.id == row_dict.get('id')))).first()
update_cls = (await session.execute(self.select_cls().where(Cls.id == row_dict.get('id')))).scalars().first()
if not update_cls:
# 若记录不存在,抛出异常
raise HTTPException(status_code=500, detail="Update row not found")
@@ -442,7 +442,7 @@ def create_model_service(
# 提取所有待更新记录的id
update_id_list = [row_dict['id'] for row_dict in row_dict_list]
# 根据id查询所有待更新的记录
update_cls_list = (await session.exec(self.select_cls().where(Cls.id.in_(update_id_list)))).all()
update_cls_list = (await session.execute(self.select_cls().where(Cls.id.in_(update_id_list)))).all()
# 若查询到的记录数量与待更新数量不一致,说明部分id不存在
if len(update_cls_list) != len(row_dict_list):
# 抛出异常并提示不存在的id
@@ -480,7 +480,7 @@ def create_model_service(
await before_delete(row_dict, session)
# 根据id查询要删除的记录
delete_cls = (await session.exec(self.select_cls().where(Cls.id == row_dict.get('id')))).first()
delete_cls = (await session.execute(self.select_cls().where(Cls.id == row_dict.get('id')))).scalars().first()
if not delete_cls:
# 若记录不存在,返回删除失败
return False
@@ -509,7 +509,7 @@ def create_model_service(
row_id_list = [row_dict.get("id") for row_dict in row_dict_list]
# 根据id查询所有待删除的记录
delete_cls_list = (await session.exec(self.select_cls().where(Cls.id.in_(row_id_list)))).all()
delete_cls_list = (await session.execute(self.select_cls().where(Cls.id.in_(row_id_list)))).all()
# 若查询到的记录数量与待删除数量不一致,说明部分id不存在
if len(delete_cls_list) != len(row_id_list):
# 抛出异常并提示不存在的id
+22 -22
View File
@@ -1,36 +1,36 @@
import asyncio
import sys
from contextlib import asynccontextmanager
from typing import Annotated, AsyncContextManager
from typing import Annotated
from fastapi.params import Depends
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from sqlalchemy import AsyncAdaptedQueuePool, text
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
from app.config.env import env
from sqlmodel import create_engine
DATABASE_URL = f"mysql+asyncmy://{env.db_username}:{env.db_password}@{env.db_host}:{env.db_port}/{env.db_database}?charset=utf8mb4"
# 创建异步引擎实例,用于异步操作数据库
async_engine = AsyncEngine(create_engine(
# 建议直接使用这个函数,不要嵌套 create_engine(AsyncAdaptedQueuePool)
async_engine = create_async_engine(
DATABASE_URL,
poolclass=AsyncAdaptedQueuePool, # 使用异步适配的队列池
pool_size=5, # 连接池保持的连接数
max_overflow=10, # 允许超过pool_size的最大连接数
pool_timeout=30, # 获取连接的超时时间(秒)
pool_recycle=180, # 连接回收时间(秒)
pool_pre_ping=True, # 启用连接有效性检测
echo=True, # 启用SQL语句日志输出,便于开发调试
future=True, # 启用SQLAlchemy 2.0风格的未来模式API
))
# 连接池保持的连接数
pool_size=5,
# 允许超过pool_size的最大连接数
max_overflow=10,
# 获取连接的超时时间(秒)
pool_timeout=30,
# 连接回收时间(秒)
pool_recycle=60, # 针对 Docker NAT 环境优化
# 启用连接有效性检测
pool_pre_ping=True, # 解决 Lost connection 的核心
# 启用SQL语句日志输出,便于开发调试
echo=True,
# 启用SQLAlchemy 2.0风格的未来模式API
future=True,
)
# 创建一个会话工厂函数
# 使用这个 Session 工厂
async_session = sessionmaker(
bind=async_engine,
async_engine,
class_=AsyncSession,
expire_on_commit=False
)
+1
View File
@@ -22,6 +22,7 @@ def create_llm(
model=_ai_config.get('model'),
temperature=temperature,
disable_streaming=disable_streaming,
extra_body={"enable_thinking": False, "thinking": {"type": "disabled"}}
)
+1 -1
View File
@@ -63,7 +63,7 @@ class MilvusService:
# 检查Milvus连接是否正常
async def check_milvus_connection(self):
await asyncio.sleep(0.1)
await asyncio.sleep(0.5)
await self.async_search(KnowledgeQueryParam(question="hello", kb_code=""))
print("✅ Milvus connection successful:", f"Milvus://{env.milvus_username}:{env.milvus_password}@{env.milvus_uri}/{env.llama_index_database}/{env.llama_index_collection}")
+38 -16
View File
@@ -1,63 +1,81 @@
import json
import uuid
import logging
from contextlib import asynccontextmanager
from typing import Annotated, AsyncContextManager
from typing import Annotated, AsyncContextManager, Any, Callable
import redis.asyncio as redis
from fastapi import Depends
from app.config.env import env
# 设置日志,方便调试
logger = logging.getLogger(__name__)
class RedisUtils():
class RedisUtils:
def __init__(self):
self.redis_pool = None
self.redis_pool: redis.ConnectionPool | None = None
# 检查 Redis 连接,先创建连接池管理对象
# 1. 优化连接池初始化:加入心跳和保活机制
async def check_redis_connection(self):
"""初始化连接池并执行读写测试"""
self.redis_pool = redis.ConnectionPool(
host=env.redis_host,
port=env.redis_port,
password=env.redis_password,
db=env.redis_db,
encoding="utf-8",
decode_responses=True
decode_responses=True,
# --- 解决 10054 报错的核心配置 ---
health_check_interval=5, # 每5秒自动发送PING,保持连接活跃
socket_keepalive=True, # 启用TCP层面的保活探测
socket_connect_timeout=3, # 连接超时设置
retry_on_timeout=True, # 遇到超时自动重试一次
)
# 立即对redis做一次读写测试
async with self.get_redis_connection() as redis_client:
try:
await redis_client.ping()
random_id = str(uuid.uuid4())
await redis_client.set("__init__setup__", random_id)
await redis_client.set("__init__setup__", random_id, ex=10) # 设置10秒过期
cache_random_id = await redis_client.get("__init__setup__")
if cache_random_id != random_id:
raise Exception("Redis 连接失败,读写测试不一致")
raise Exception("Redis 读写测试一致性校验失败")
print("✅ Redis connection successful:", f"redis://{env.redis_password}@${env.redis_host}:{env.redis_port}/{env.redis_db}")
except Exception as e:
print(f"❌ Database connection failed")
logger.error(f"❌ Redis connection failed: {e}")
raise e
# 获取一个Redis连接客户端
# 2. 获取连接:利用连接池自动管理,无需手动关闭 client
@asynccontextmanager
async def get_redis_connection(self) -> AsyncContextManager[redis.Redis]:
if not self.redis_pool:
raise Exception("Redis 连接池未初始化")
if self.redis_pool is None:
await self.check_redis_connection()
# 异步连接池中,Redis 实例创建很轻量
# 上下文管理器结束后,它会自动将连接归还给 pool,而不是物理断开
redis_client = redis.Redis(connection_pool=self.redis_pool)
try:
yield redis_client
finally:
await redis_client.close()
except Exception as e:
logger.error(f"Redis Connection Error: {e}")
raise
# 清理Redis连接池
# 3. 清理连接池
async def close_redis_connection(self):
if self.redis_pool:
await self.redis_pool.disconnect()
print("🚀 Redis connection pool closed.")
redis_utils = RedisUtils()
# 作用:用于在接口中注入得到会话实例对象session,在接口执行完毕之后,自动执行close动作关闭会话
# --- FastAPI 依赖注入 ---
async def get_redis_client() -> redis.Redis:
async with redis_utils.get_redis_connection() as redis_client:
yield redis_client
@@ -66,8 +84,12 @@ async def get_redis_client() -> redis.Redis:
RedisClientDep = Annotated[redis.Redis, Depends(get_redis_client)]
# 从redis中获取key的缓存,如果没有值则执行默认值获取函数,并保存到redis中
async def get_redis_cache(key: str, default_value_getter):
"""
从redis中获取key的缓存,如果没有值则执行默认值获取函数,并保存到redis中
@param key 缓存的key
@param default_value_getter 如果缓存值不存在的情况下则调用异步函数 default_value_getter 来获取默认值,这个函数的返回值必须是一个字典 dict
"""
async with redis_utils.get_redis_connection() as redis_client:
# default_value_getter返回的字段可能嵌套多层对象,这里改成用json字符串缓存
json_string = await redis_client.get(key)
Generated
+3982 -2888
View File
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -20,7 +20,8 @@ psutil = "^7.0.0"
asyncmy = "^0.2.10"
sqlmodel = "^0.0.24"
greenlet = "^3.2.3"
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
passlib = {extras = ["bcrypt"], version = "^1.7.4", optional = true}
bcrypt = "^4.0.0"
pyjwt = "^2.10.1"
python-multipart = "^0.0.20"
langgraph = "^0.6.3"
@@ -33,21 +34,17 @@ aiofiles = "^24.1.0"
redis = "^6.4.0"
websockets = "^15.0.1"
dashscope = "^1.25.1"
psycopg = {extras = ["binary"], version = "^3.3.3"}
[tool.poetry.group.dev.dependencies]
langchain-cli = ">=0.0.15"
# 配置国内镜像源
[[tool.poetry.source]]
name = "tsinghua"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
priority = "primary" # 最高优先级
[[tool.poetry.source]]
name = "aliyun"
url = "https://mirrors.aliyun.com/pypi/simple/"
priority = "supplemental" # 次级优先级
priority = "primary" # 最高优先级
[build-system]
requires = ["poetry-core"]