修复toekn权限和语音聊天功能

This commit is contained in:
taiyi 2026-05-26 13:12:09 +08:00
parent 5822ca6aee
commit 60931d408c
10 changed files with 433 additions and 157 deletions

View File

@ -83,12 +83,16 @@ async def list_users(
wallet_result = await db.execute(select(UserWallet).where(UserWallet.user_id == user.id))
wallet = wallet_result.scalar_one_or_none()
# 将状态从整数转换为字符串格式
# status: 1 或非零 = active, 0 或 null = disabled
status_str = 'active' if (user.status and user.status != 0) else 'disabled'
user_dict = {
"id": user.id,
"phone": user.phone,
"nickname": user.nickname,
"is_admin": user.role == 2,
"status": user.status,
"status": status_str,
"available_tokens": wallet.available_tokens if wallet else 0,
"daily_quota": wallet.daily_quota if wallet else 0,
"extra_quota": wallet.extra_quota if wallet else 0,

View File

@ -61,9 +61,12 @@ async def send_message_endpoint(
实现分层提示基础提示 + 场景提示 + 记忆 + 用户输入
"""
try:
# 解析用户ID为数字类型
numeric_user_id = await resolve_user_id(current_user["id"], db)
token_service = TokenService(db)
token_check = await token_service.check_available_tokens(current_user["id"])
token_check = await token_service.check_available_tokens(numeric_user_id)
if not token_check["has_tokens"]:
raise HTTPException(
status_code=402,
@ -76,7 +79,7 @@ async def send_message_endpoint(
result = await send_message(
db,
user_id=current_user["id"],
user_id=numeric_user_id,
message=message_data.message,
pet_id=message_data.pet_id,
background_id=message_data.background_id,
@ -84,14 +87,14 @@ async def send_message_endpoint(
)
charge_result = await token_service.charge_tokens(
user_id=current_user["id"],
user_id=numeric_user_id,
mode="text_chat",
tokens_used=result.get("tokens_used"),
fallback_amount=1
)
if not charge_result.get("success"):
logger.warning(
f"Failed to charge tokens for user {current_user['id']} in text_chat_send: "
f"Failed to charge tokens for user {numeric_user_id} in text_chat_send: "
f"{charge_result.get('message')}"
)
@ -224,22 +227,20 @@ async def websocket_chat_endpoint(websocket: WebSocket, user_id: str):
async for db_session in get_db():
try:
is_guest = isinstance(user_id, str) and user_id.startswith("guest_")
numeric_user_id = await resolve_user_id(user_id, db_session)
token_service = TokenService(db_session)
# guest用户跳过token检查
if not is_guest:
token_check = await token_service.check_available_tokens(numeric_user_id)
if not token_check["has_tokens"]:
await websocket.send_text(json.dumps({
"type": "error",
"error": "insufficient_tokens",
"message": "您的可用token已耗尽请联系管理员充值",
"available_tokens": token_check["available_tokens"],
"timestamp": datetime.now().isoformat()
}))
break
# 检查用户token
token_check = await token_service.check_available_tokens(numeric_user_id)
if not token_check["has_tokens"]:
await websocket.send_text(json.dumps({
"type": "error",
"error": "insufficient_tokens",
"message": "您的可用token已耗尽请联系管理员充值",
"available_tokens": token_check["available_tokens"],
"timestamp": datetime.now().isoformat()
}))
break
await websocket.send_text(json.dumps({
"type": "typing_start",
@ -258,7 +259,7 @@ async def websocket_chat_endpoint(websocket: WebSocket, user_id: str):
):
if isinstance(chunk, dict):
remaining_tokens = None
if chunk.get("type") == "stats" and not is_guest:
if chunk.get("type") == "stats" and not user_id.startswith("guest_"):
charge_result = await token_service.charge_tokens(
user_id=numeric_user_id,
mode="text_chat",
@ -1000,6 +1001,8 @@ async def websocket_volcano_bidirectional_endpoint(websocket: WebSocket, user_id
3. 支持流式音频返回
"""
from config.settings import settings
from services.token_service import TokenService
from utils.database import get_db
import uuid
await websocket.accept()
@ -1023,6 +1026,27 @@ async def websocket_volcano_bidirectional_endpoint(websocket: WebSocket, user_id
message_type = message.get("type")
if message_type == "start_session":
# 检查用户token
token_check_result = None
async for db_session in get_db():
try:
numeric_user_id = await resolve_user_id(user_id, db_session)
token_service = TokenService(db_session)
token_check_result = await token_service.check_available_tokens(numeric_user_id)
finally:
await db_session.close()
break
if token_check_result and not token_check_result.get("has_tokens"):
await websocket.send_text(json.dumps({
"type": "error",
"error": "insufficient_tokens",
"message": "您的可用token已耗尽请联系管理员充值",
"available_tokens": token_check_result.get("available_tokens", 0),
"timestamp": datetime.now().isoformat()
}))
continue
text = message.get("text", "欢迎使用AI宠物语音助手")
voice_type = message.get("voice_type", "zh_female_xiaohe_uranus_bigtts")
encoding = message.get("encoding", "mp3")

View File

@ -60,6 +60,8 @@ async def _charge_voice_tokens(
) -> Dict[str, Any]:
"""统一语音扣费入口,补充清晰日志。"""
session_state = session_state or {}
# 检查是否已经扣费
if session_state.get("token_charged"):
logger.info(
"[VOICE_CHARGE_SKIP] user_id=%s mode=%s reason=%s conversation_id=%s trace_id=%s already_charged=True",
@ -71,27 +73,49 @@ async def _charge_voice_tokens(
)
return {"success": True, "skipped": True, "message": "already charged"}
# 获取或创建数据库会话和token服务
token_service = session_state.get("token_service")
db_session = session_state.get("db_session")
if token_service is None:
# 如果没有提供会话,则创建新会话并在同一上下文中完成所有操作
if token_service is None or db_session is None:
async with AsyncSessionLocal() as db:
token_service = TokenService(db)
numeric_user_id = await resolve_user_id(user_id, db)
return await _charge_voice_tokens(
user_id=user_id,
charge_cfg_fallback = fallback_amount if fallback_amount is not None else VOICE_TOKEN_FALLBACKS.get(mode, 200)
result = await token_service.charge_tokens(
user_id=numeric_user_id,
mode=mode,
session_state={**session_state, "token_service": token_service, "db_session": db},
tokens_used=tokens_used,
fallback_amount=fallback_amount,
reason=reason,
fallback_amount=charge_cfg_fallback,
)
if db_session is None:
async with AsyncSessionLocal() as db:
numeric_user_id = await resolve_user_id(user_id, db)
else:
numeric_user_id = await resolve_user_id(user_id, db_session)
token_used_amount = int(result.get("amount") or 0)
logger.info(
"[VOICE_CHARGE_%s] user_id=%s mode=%s reason=%s conversation_id=%s trace_id=%s amount=%s available_tokens=%s success=%s message=%s",
"OK" if result.get("success") else "FAIL",
user_id,
mode,
reason,
session_state.get("conversation_id"),
session_state.get("trace_id"),
token_used_amount,
result.get("available_tokens"),
result.get("success"),
result.get("message"),
)
if result.get("success"):
session_state["token_charged"] = True
session_state["tokens_used"] = tokens_used or session_state.get("tokens_used")
# 确保事务提交虽然charge_tokens已经提交但这里再次确认
await db.commit()
return result
# 使用已提供的会话
numeric_user_id = await resolve_user_id(user_id, db_session)
charge_cfg_fallback = fallback_amount if fallback_amount is not None else VOICE_TOKEN_FALLBACKS.get(mode, 200)
result = await token_service.charge_tokens(
user_id=numeric_user_id,
@ -677,13 +701,6 @@ def _is_guest_user_id(user_id: Optional[str]) -> bool:
async def _check_user_has_tokens(user_id: str) -> Dict[str, Any]:
"""检查用户是否还有可用token"""
if _is_guest_user_id(user_id):
return {
"has_tokens": True,
"available_tokens": 999999,
"message": "Guest user bypassed token check"
}
async with AsyncSessionLocal() as db:
token_service = TokenService(db)
try:

View File

@ -52,7 +52,6 @@ class TokenService:
Returns:
包含统计信息的字典
"""
# 计算总Token数
total_tokens = input_tokens + output_tokens
# 创建聊天记录
@ -70,14 +69,13 @@ class TokenService:
created_at=datetime.now()
)
# 保存聊天记录(直接使用 db_session不依赖 ChatLogManager
# 保存聊天记录
self.db_session.add(chat_log)
await self.db_session.commit()
# 检查用户配额
quota_info = await self._check_user_quota(user_id)
if not quota_info["has_quota"]:
logger.warning(f"User {user_id} has no remaining quota")
logger.warning(f"[TOKEN] User {user_id} has no remaining quota")
return {
"success": False,
"reason": "insufficient_quota",
@ -87,10 +85,10 @@ class TokenService:
"total_tokens": total_tokens
}
# 扣除可用token按实际消耗的token数
# 扣除可用token
consume_result = await self.consume_available_tokens(user_id, total_tokens)
if not consume_result["success"]:
logger.warning(f"User {user_id} failed to consume tokens: {consume_result['message']}")
logger.warning(f"[TOKEN] User {user_id} failed to consume tokens: {consume_result['message']}")
return {
"success": False,
"reason": "insufficient_quota",
@ -100,8 +98,11 @@ class TokenService:
"total_tokens": total_tokens
}
# 提交事务
await self.db_session.commit()
logger.info(
f"Token usage recorded: user={user_id}, "
f"[TOKEN] Token usage recorded: user={user_id}, "
f"input_tokens={input_tokens}, output_tokens={output_tokens}, "
f"total={total_tokens}, remaining={consume_result['available_tokens']}"
)
@ -119,12 +120,6 @@ class TokenService:
async def _check_user_quota(self, user_id: int) -> Dict[str, Any]:
"""
检查用户配额情况
Args:
user_id: 用户ID
Returns:
配额信息字典
"""
wallet = await self.wallet_manager.get_user_wallet(user_id)
@ -153,12 +148,6 @@ class TokenService:
async def check_available_tokens(self, user_id: int) -> Dict[str, Any]:
"""
检查用户可用token是否充足
Args:
user_id: 用户ID
Returns:
可用token信息字典
"""
wallet = await self.wallet_manager.get_user_wallet(user_id)
@ -183,21 +172,28 @@ class TokenService:
Args:
user_id: 用户ID
amount: 消耗数量按对话次数
amount: 消耗数量
Returns:
消耗结果信息字典
"""
wallet = await self.wallet_manager.get_user_wallet(user_id)
logger.info(f"[TOKEN] Attempting to consume {amount} tokens for user {user_id}")
# 使用FOR UPDATE锁定记录防止并发问题
wallet = await self._get_wallet_for_update(user_id)
if not wallet:
logger.error(f"[TOKEN] User {user_id} wallet not found")
return {
"success": False,
"message": "User wallet not found"
}
available_tokens = wallet.available_tokens or 0
logger.debug(f"[TOKEN] User {user_id} has {available_tokens} tokens available, needs {amount}")
if available_tokens < amount:
logger.warning(f"[TOKEN] User {user_id} insufficient tokens: available={available_tokens}, required={amount}")
return {
"success": False,
"message": "Insufficient available tokens",
@ -205,21 +201,33 @@ class TokenService:
"required": amount
}
# 执行扣费
wallet.available_tokens = available_tokens - amount
wallet.total_consumed = (wallet.total_consumed or 0) + amount
wallet.updated_at = datetime.now()
await self.wallet_manager.update(wallet)
# 使用merge确保对象被正确跟踪
updated_wallet = await self.db_session.merge(wallet)
logger.info(f"Consumed {amount} tokens from user {user_id}, remaining: {wallet.available_tokens}")
logger.info(f"[TOKEN] Consumed {amount} tokens from user {user_id}, remaining: {updated_wallet.available_tokens}")
return {
"success": True,
"message": "Tokens consumed successfully",
"available_tokens": wallet.available_tokens,
"available_tokens": updated_wallet.available_tokens,
"consumed": amount
}
async def _get_wallet_for_update(self, user_id: int):
"""
获取钱包记录并加锁防止并发修改
"""
from models.database import UserWallet
result = await self.db_session.execute(
select(UserWallet).where(UserWallet.user_id == user_id).with_for_update()
)
return result.scalar_one_or_none()
async def charge_tokens(
self,
user_id: int,
@ -239,8 +247,11 @@ class TokenService:
Returns:
计费结果信息字典
"""
logger.info(f"[TOKEN] Charging tokens for user {user_id}, mode={mode}")
token_check = await self.check_available_tokens(user_id)
if not token_check.get("has_tokens"):
logger.warning(f"[TOKEN] User {user_id} has no tokens remaining")
return {
"success": False,
"reason": "insufficient_tokens",
@ -252,12 +263,20 @@ class TokenService:
amount = 0
if isinstance(tokens_used, dict):
amount = int(tokens_used.get("total") or 0)
# 优先查找 total 或 total_tokens 字段
amount = int(tokens_used.get("total") or tokens_used.get("total_tokens") or 0)
# 如果没有 total 字段,则计算 input + output
if amount <= 0:
input_tokens = int(tokens_used.get("input") or tokens_used.get("input_tokens") or 0)
output_tokens = int(tokens_used.get("output") or tokens_used.get("output_tokens") or 0)
amount = input_tokens + output_tokens
if amount <= 0:
amount = int(fallback_amount or 0)
if amount <= 0:
logger.info(f"[TOKEN] No charge required for user {user_id}, amount={amount}")
return {
"success": True,
"mode": mode,
@ -268,6 +287,7 @@ class TokenService:
consume_result = await self.consume_available_tokens(user_id, amount)
if not consume_result.get("success"):
logger.warning(f"[TOKEN] Failed to consume tokens for user {user_id}: {consume_result.get('message')}")
return {
"success": False,
"reason": "insufficient_tokens",
@ -277,6 +297,11 @@ class TokenService:
"message": consume_result.get("message", "Failed to consume tokens")
}
# 提交事务
await self.db_session.commit()
logger.info(f"[TOKEN] Successfully charged {amount} tokens for user {user_id}, mode={mode}")
return {
"success": True,
"mode": mode,
@ -315,12 +340,12 @@ class TokenService:
input_tokens = int(tokens_used.get("input") or 0)
output_tokens = int(tokens_used.get("output") or 0)
if input_tokens + output_tokens <= 0:
total = int(tokens_used.get("total") or 0)
total = int(tokens_used.get("total") or tokens_used.get("total_tokens") or 0)
input_tokens = total
if input_tokens + output_tokens <= 0:
input_tokens = int(fallback_amount or 0)
# 直接通过 db_session 写入 ChatLog,不依赖 ChatLogManager其 create 方法为桩实现)
# 写入 ChatLog
chat_log = ChatLog(
user_id=user_id,
trace_id=trace_id,
@ -335,9 +360,9 @@ class TokenService:
created_at=datetime.now()
)
self.db_session.add(chat_log)
await self.db_session.commit()
if skip_charge:
await self.db_session.commit()
return {
"success": True,
"mode": mode,
@ -355,54 +380,40 @@ class TokenService:
async def _reset_daily_quota_if_needed(self, wallet: UserWallet) -> None:
"""
检查并重置每日配额如果跨天了
Args:
wallet: 用户钱包对象
"""
today = date.today()
# 如果上次重置时间不是今天,则重置每日配额
if wallet.last_reset_time != today:
logger.info(f"Resetting daily quota for user {wallet.user_id}")
wallet.daily_quota = 20 # 重置为默认每日配额
logger.info(f"[TOKEN] Resetting daily quota for user {wallet.user_id}")
wallet.daily_quota = 20
wallet.last_reset_time = today
await self.wallet_manager.update(wallet)
await self.db_session.merge(wallet)
await self.db_session.commit()
async def _consume_quota(self, user_id: int, amount: int = 1) -> bool:
"""
扣除用户配额
Args:
user_id: 用户ID
amount: 扣除数量
Returns:
是否成功扣除
扣除用户配额旧版方法保留兼容性
"""
wallet = await self.wallet_manager.get_user_wallet(user_id)
wallet = await self._get_wallet_for_update(user_id)
if not wallet:
return False
# 检查剩余配额
remaining = wallet.daily_quota + wallet.extra_quota
if remaining < amount:
return False
# 优先扣除每日配额,再扣除额外配额
if wallet.daily_quota >= amount:
wallet.daily_quota -= amount
else:
# 扣除所有每日配额
amount_to_deduct_from_extra = amount - wallet.daily_quota
wallet.daily_quota = 0
wallet.extra_quota -= amount_to_deduct_from_extra
# 增加总消耗数
wallet.total_consumed += amount
# 更新钱包
await self.wallet_manager.update(wallet)
await self.db_session.merge(wallet)
await self.db_session.commit()
return True
@ -414,16 +425,7 @@ class TokenService:
) -> Dict[str, Any]:
"""
获取用户Token使用统计
Args:
user_id: 用户ID
start_date: 开始日期
end_date: 结束日期
Returns:
统计信息字典
"""
# 构建查询条件(使用 coalesce 避免 NULL
base_conditions = [ChatLog.user_id == user_id]
if start_date:
base_conditions.append(ChatLog.created_at >= start_date)
@ -449,24 +451,20 @@ class TokenService:
ChatLog.conversation_id.like("realtime-%")
)
# 执行查询
total_input = (await self.db_session.execute(total_input_query)).scalar() or 0
total_output = (await self.db_session.execute(total_output_query)).scalar() or 0
total_chats = (await self.db_session.execute(total_chats_query)).scalar() or 0
voice_tokens = (await self.db_session.execute(voice_query)).scalar() or 0
realtime_tokens = (await self.db_session.execute(realtime_query)).scalar() or 0
# 钱包维度的消耗:以钱包总消耗为准,更适合反映实际扣费
quota_info = await self._check_user_quota(user_id)
wallet_consumed = quota_info.get("total_consumed", 0) or 0
chatlog_total = total_input + total_output
# 如果钱包没有累计到消耗,但日志里有数据,则优先展示日志值;反之展示钱包值
total_tokens = max(chatlog_total, wallet_consumed)
if total_tokens <= 0:
total_tokens = chatlog_total or wallet_consumed or 0
# 计算文字聊天TokenChatLog总Token - 语音聊天Token - 实时通话Token
text_chat_tokens = max(0, chatlog_total - voice_tokens - realtime_tokens)
return {
@ -498,37 +496,24 @@ class TokenService:
) -> Dict[str, Any]:
"""
获取系统整体Token使用统计
Args:
start_date: 开始日期
end_date: 结束日期
Returns:
系统统计信息字典
"""
# 构建基础查询
base_query = select(ChatLog)
# 添加日期过滤
if start_date:
base_query = base_query.where(ChatLog.created_at >= start_date)
if end_date:
base_query = base_query.where(ChatLog.created_at <= end_date)
# 获取所有记录
result = await self.db_session.execute(base_query)
all_logs = result.scalars().all()
# 计算统计信息
total_input_tokens = sum(log.tokens_input for log in all_logs)
total_output_tokens = sum(log.tokens_output for log in all_logs)
total_chats = len(all_logs)
# 获取唯一用户数
unique_users = len(set(log.user_id for log in all_logs))
# 计算平均响应时间
avg_response_time = 0
if all_logs:
avg_response_time = sum(log.duration_ms for log in all_logs) / len(all_logs)
@ -549,26 +534,66 @@ class TokenService:
async def gift_quota(self, user_id: int, amount: int, quota_type: str = "extra") -> bool:
"""
管理员赠送配额给用户
Args:
user_id: 用户ID
amount: 赠送数量
quota_type: 配额类型 ("daily" "extra")
Returns:
是否成功
"""
wallet = await self.wallet_manager.get_user_wallet(user_id)
wallet = await self._get_wallet_for_update(user_id)
if not wallet:
return False
if quota_type == "daily":
wallet.daily_quota += amount
else: # extra
else:
wallet.extra_quota += amount
await self.wallet_manager.update(wallet)
await self.db_session.merge(wallet)
await self.db_session.commit()
logger.info(f"Gifted {amount} {quota_type} quota to user {user_id}")
logger.info(f"[TOKEN] Gifted {amount} {quota_type} quota to user {user_id}")
return True
async def set_available_tokens(self, user_id: int, amount: int) -> bool:
"""
设置用户可用token管理员操作
"""
wallet = await self._get_wallet_for_update(user_id)
if not wallet:
# 创建新钱包
wallet = UserWallet(
user_id=user_id,
available_tokens=amount,
daily_quota=0,
extra_quota=0,
total_consumed=0,
last_reset_time=date.today()
)
self.db_session.add(wallet)
else:
wallet.available_tokens = amount
await self.db_session.commit()
logger.info(f"[TOKEN] Set available tokens for user {user_id} to {amount}")
return True
async def add_available_tokens(self, user_id: int, amount: int) -> bool:
"""
增加用户可用token管理员操作
"""
wallet = await self._get_wallet_for_update(user_id)
if not wallet:
wallet = UserWallet(
user_id=user_id,
available_tokens=amount,
daily_quota=0,
extra_quota=0,
total_consumed=0,
last_reset_time=date.today()
)
self.db_session.add(wallet)
else:
wallet.available_tokens = (wallet.available_tokens or 0) + amount
await self.db_session.commit()
logger.info(f"[TOKEN] Added {amount} tokens to user {user_id}")
return True

View File

@ -0,0 +1,115 @@
"""
Token扣费流程测试脚本
用于验证token扣费逻辑是否正常工作
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sqlalchemy import select
from services.token_service import TokenService
from utils.database import AsyncSessionLocal
async def test_token_charge():
"""测试token扣费流程"""
print("=== Token扣费流程测试 ===")
async with AsyncSessionLocal() as session:
# 创建测试用户钱包
test_user_id = 1
from models.database import UserWallet
# 先检查是否已有钱包
result = await session.execute(
select(UserWallet).where(UserWallet.user_id == test_user_id)
)
wallet = result.scalar_one_or_none()
if not wallet:
# 创建新钱包
wallet = UserWallet(
user_id=test_user_id,
available_tokens=100,
daily_quota=0,
extra_quota=0,
total_consumed=0
)
session.add(wallet)
await session.commit()
await session.refresh(wallet)
print(f"创建测试钱包: user_id={wallet.user_id}, available_tokens={wallet.available_tokens}")
else:
print(f"找到现有钱包: user_id={wallet.user_id}, available_tokens={wallet.available_tokens}")
# 重置钱包金额以便测试
wallet.available_tokens = 100
wallet.total_consumed = 0
await session.commit()
await session.refresh(wallet)
# 创建token服务
token_service = TokenService(session)
# 测试1: 检查可用token
print("\n--- 测试1: 检查可用token ---")
check_result = await token_service.check_available_tokens(test_user_id)
print(f"检查结果: {check_result}")
# 测试2: 执行扣费
print("\n--- 测试2: 执行扣费 ---")
print(f"扣费前可用token: {wallet.available_tokens}")
charge_result = await token_service.charge_tokens(
user_id=test_user_id,
mode="test",
tokens_used={"total": 10},
fallback_amount=5
)
print(f"扣费结果: {charge_result}")
# 重新查询钱包状态
await session.refresh(wallet)
print(f"扣费后可用token: {wallet.available_tokens}")
print(f"总消耗token: {wallet.total_consumed}")
# 测试3: 再次扣费(测试多次扣费)
print("\n--- 测试3: 再次扣费 ---")
charge_result2 = await token_service.charge_tokens(
user_id=test_user_id,
mode="test2",
tokens_used=None,
fallback_amount=20
)
print(f"扣费结果: {charge_result2}")
await session.refresh(wallet)
print(f"第二次扣费后可用token: {wallet.available_tokens}")
print(f"总消耗token: {wallet.total_consumed}")
# 测试4: 测试余额不足的情况
print("\n--- 测试4: 测试余额不足 ---")
# 设置余额为0
wallet.available_tokens = 0
await session.commit()
check_result = await token_service.check_available_tokens(test_user_id)
print(f"余额为0时检查结果: {check_result}")
charge_result3 = await token_service.charge_tokens(
user_id=test_user_id,
mode="test3",
tokens_used={"total": 5}
)
print(f"余额不足时扣费结果: {charge_result3}")
print("\n=== 测试完成 ===")
if __name__ == "__main__":
asyncio.run(test_token_charge())

View File

@ -22,8 +22,6 @@ async def get_db() -> AsyncGenerator[Session, None]:
获取数据库会话
这是一个简化版本实际应该使用异步数据库会话
"""
# 在实际应用中,这里应该创建异步数据库会话
# 目前返回模拟会话
class MockSession:
def execute(self, query):
return MockResult()
@ -80,9 +78,11 @@ class UserWalletManager(BaseManager):
async def update(self, wallet):
"""更新钱包记录"""
# 使用 merge 方法确保对象被当前会话跟踪
merged_wallet = await self.db_session.merge(wallet)
await self.db_session.commit()
await self.db_session.refresh(wallet)
return wallet
await self.db_session.refresh(merged_wallet)
return merged_wallet
class ChatLogManager(BaseManager):
async def get_user_chat_history(self, user_id: int, conversation_id: str, limit: int = 20, offset: int = 0):

View File

@ -8,6 +8,28 @@ import apiClient from '@/api/index'
import { chatAPI } from '@/api/index'
import { ElMessage } from 'element-plus'
// 统计更新事件回调
let onStatsUpdateCallbacks = []
// 注册统计更新回调
export function onChatStatsUpdate(callback) {
onStatsUpdateCallbacks.push(callback)
return () => {
onStatsUpdateCallbacks = onStatsUpdateCallbacks.filter(cb => cb !== callback)
}
}
// 触发统计更新
function triggerStatsUpdate() {
onStatsUpdateCallbacks.forEach(cb => {
try {
cb()
} catch (error) {
console.error('[ChatStore] Stats update callback error:', error)
}
})
}
export const useChatStore = defineStore('chat', () => {
// ============ 状态 ============
const messages = ref([])
@ -24,7 +46,8 @@ export const useChatStore = defineStore('chat', () => {
const audioContext = ref(null)
const mediaRecorder = ref(null)
const audioChunks = ref([])
const permissionStream = ref(null)
// 麦克风权限状态
const microphonePermissionState = ref('prompt') // 'granted' | 'denied' | 'prompt'
const hasRequestedPermission = ref(false)
@ -474,7 +497,7 @@ export const useChatStore = defineStore('chat', () => {
break
case 'chat_ended':
// AI回复结束
// AI 回复结束
console.log('[ChatStore] Chat ended, total content:', pendingChatContent.value?.slice(0, 50))
const streamingMsg = messages.value.find(m => m.isStreaming)
if (streamingMsg) {
@ -482,6 +505,8 @@ export const useChatStore = defineStore('chat', () => {
streamingMsg.isStreaming = false
}
pendingChatContent.value = ''
// 触发统计更新
triggerStatsUpdate()
break
case 'tts_start':
@ -520,6 +545,8 @@ export const useChatStore = defineStore('chat', () => {
case 'session_stopped':
console.log('[ChatStore] Voice session stopped')
resetAudioState()
// 触发统计更新
triggerStatsUpdate()
break
case 'dialog_error':
@ -600,6 +627,8 @@ export const useChatStore = defineStore('chat', () => {
background_id: currentBackgroundId.value
}
voiceSessionReady.value = false
const sent = sendVoiceMessage({
type: 'start_session',
...sessionConfig
@ -608,6 +637,37 @@ export const useChatStore = defineStore('chat', () => {
if (!sent) {
throw new Error('语音连接中断,请稍后重试')
}
const isSessionReady = await waitForVoiceSessionReady(5000)
if (!isSessionReady) {
throw new Error('语音会话启动失败,请稍后重试')
}
}
function waitForVoiceSessionReady(timeoutMs = 5000) {
return new Promise((resolve) => {
if (voiceSessionReady.value) {
resolve(true)
return
}
const start = Date.now()
const tick = () => {
if (voiceSessionReady.value) {
resolve(true)
return
}
if (Date.now() - start >= timeoutMs) {
resolve(false)
return
}
setTimeout(tick, 50)
}
tick()
})
}
/**
@ -700,6 +760,7 @@ export const useChatStore = defineStore('chat', () => {
}, 300)
}
isVoiceConnected.value = false
voiceSessionReady.value = false
}
// ============ 实时通话功能 ============
@ -889,6 +950,8 @@ export const useChatStore = defineStore('chat', () => {
console.log('[ChatStore] Call ended')
callStartTime.value = null
resetAudioState()
// 触发统计更新
triggerStatsUpdate()
break
case 'error':
@ -1073,18 +1136,20 @@ export const useChatStore = defineStore('chat', () => {
try {
console.log('[ChatStore] Requesting microphone permission...');
// 先尝试简单的约束,提高移动端兼容性
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
})
console.log('[ChatStore] Microphone permission granted');
microphonePermissionState.value = 'granted'
hasRequestedPermission.value = true
// 获取到权限后立即释放流,避免占用麦克风
stream.getTracks().forEach(track => track.stop())
permissionStream.value = stream
return true
} catch (error) {
console.error('请求麦克风权限失败:', error)
@ -1134,21 +1199,22 @@ export const useChatStore = defineStore('chat', () => {
return false
}
let stream;
try {
// 尝试最佳配置
stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
})
} catch (e) {
console.warn('[ChatStore] Failed to get user media with strict constraints, falling back to basic audio', e);
// 回退到基础音频配置
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
let stream = permissionStream.value
if (!stream || stream.getTracks().every(t => t.readyState === 'ended')) {
try {
stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
})
} catch (e) {
console.warn('[ChatStore] Failed to get user media with strict constraints, falling back to basic audio', e);
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
}
permissionStream.value = stream
}
// 创建或恢复 AudioContext

View File

@ -60,6 +60,7 @@ export const useUserStore = defineStore('user', {
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
localStorage.setItem('user', JSON.stringify(user))
console.log('[UserStore] Token saved to localStorage, checking:', localStorage.getItem('access_token')?.substring(0, 50) + '...')
@ -86,6 +87,7 @@ export const useUserStore = defineStore('user', {
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
localStorage.setItem('user', JSON.stringify(user))
return { success: true, user }
} catch (error) {
@ -110,6 +112,7 @@ export const useUserStore = defineStore('user', {
localStorage.setItem('access_token', access_token)
localStorage.setItem('refresh_token', refresh_token)
localStorage.setItem('user', JSON.stringify(user))
return { success: true, user }
} catch (error) {
@ -171,6 +174,7 @@ export const useUserStore = defineStore('user', {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user')
},
// 加载用户资料

View File

@ -429,7 +429,7 @@
</template>
<script>
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
UserFilled,
@ -454,6 +454,7 @@ import { useUserStore } from '@/stores/userStore'
import { backgroundAPI } from '@/api'
import { useRouter } from 'vue-router'
import userApi from '@/services/userApi'
import { onChatStatsUpdate } from '@/stores/chatStore'
export default {
name: 'ProfileView',
@ -856,11 +857,27 @@ export default {
}
}
//
let cleanupStatsListener = null
//
onMounted(async () => {
await loadUserInfo()
await loadUserBackgrounds()
await loadUserStats() //
//
cleanupStatsListener = onChatStatsUpdate(() => {
console.log('[ProfileView] Chat stats updated, refreshing...')
loadUserStats()
})
})
//
onUnmounted(() => {
if (cleanupStatsListener) {
cleanupStatsListener()
}
})
return {

View File

@ -5,8 +5,10 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const env = loadEnv(process.env.NODE_ENV || 'development', process.cwd(), '')
const proxyUrl = env.VITE_PROXY_TARGET || env.PROXY_URL || 'http://8.152.169.84:8001'
@ -121,7 +123,9 @@ export default defineConfig({
},
css: {
preprocessorOptions: {
scss: {}
scss: {
api: 'modern-compiler'
}
}
}
})