feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复

== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
Simon
2026-07-11 23:13:10 +08:00
parent 3d152fc8eb
commit bea288e414
928 changed files with 85169 additions and 54205 deletions
+151 -3
View File
@@ -95,10 +95,29 @@ async def websocket_endpoint(
return
# 步骤3: 从 Redis 查询 token 对应的坐席信息
# Redis 中存储格式: agent:token:{token} -> agent_user_id
# (与坐席登录 API /api/agents/login 存储格式一致)
# 支持两种格式:
# 1. 新格式: user:token:{token} -> JSON {employee_id, roles, ...}
# 2. 旧格式: agent:token:{token} -> employee_id
try:
stored_agent_id = await cache_service.get(f"agent:token:{token}")
import json
# 先尝试新格式
user_info = await cache_service.get(f"user:token:{token}")
if user_info:
try:
user_data = json.loads(user_info)
stored_agent_id = user_data.get("employee_id")
roles = user_data.get("roles", [])
if "agent" not in roles:
# token 没有 agent 角色
await websocket.accept()
await websocket.close(code=WS_CLOSE_UNAUTHORIZED, reason="No agent role")
return
except (json.JSONDecodeError, TypeError):
stored_agent_id = None
else:
# 兼容旧格式
stored_agent_id = await cache_service.get(f"agent:token:{token}")
except Exception as e:
# Redis 不可用时必须拒绝连接:token 验证依赖 Redis,无法验证身份
# 如果降级放行,攻击者可在 Redis 故障时用任意 agent_id 冒充坐席
@@ -319,3 +338,132 @@ async def h5_websocket_endpoint(
# 其他异常
ws_manager.disconnect_employee(employee_id)
logger.warning(f"H5 WebSocket 异常断开: employee_id={employee_id}, error={e}")
# ==========================================================================
# 终端 WebSocket 端点(小鱼易联终端大屏)
# ==========================================================================
@router.websocket("/ws/terminal/{terminal_sn}")
async def terminal_websocket_endpoint(
websocket: WebSocket,
terminal_sn: str,
) -> None:
"""终端 WebSocket 端点主循环(token可选认证)。
做什么:
1. 从 subprotocol / header / query 获取 token(可选)
2. token 存在时验证有效性,不存在时允许连接(仅接收推送)
3. 注册到 ConnectionManager 的终端连接表
4. 进入消息接收循环,处理心跳 ping 和 request_status
5. 连接断开时清理注册信息
认证策略(与坐席/H5端不同):
- 终端查看状态不需要登录(访客可查看)
- token 可选:无token时仅接收状态推送,不能发预定指令
- 有token时记录登录用户身份(可用于预定操作)
Args:
websocket: FastAPI WebSocket 对象
terminal_sn: 终端序列号(从 URL 路径参数获取)
"""
# ======================================================================
# Token 认证(可选 — 无token也允许连接)
# ======================================================================
# 从 subprotocol / header / query 获取 token
subprotocol = websocket.headers.get("sec-websocket-protocol", "")
if subprotocol.startswith("bearer."):
token = subprotocol[7:]
else:
auth_header = websocket.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
token = auth_header[7:]
else:
token = websocket.query_params.get("token", "")
# token 可选:无token也允许连接(仅接收推送,不能发预定指令)
# 有token时验证(用于后续预定操作的身份识别)
is_authenticated = False
if token:
try:
import json
# 尝试新格式
user_info = await cache_service.get(f"user:token:{token}")
if user_info:
try:
user_data = json.loads(user_info)
is_authenticated = True
logger.debug(
f"终端 WS 已认证: sn={terminal_sn}, "
f"user={user_data.get('employee_id')}"
)
except (json.JSONDecodeError, TypeError):
pass
else:
# 兼容旧格式
stored_id = await cache_service.get(f"agent:token:{token}")
if stored_id:
is_authenticated = True
except Exception as e:
logger.warning(f"终端 WS token 验证失败(降级为未认证): sn={terminal_sn}, error={e}")
# ======================================================================
# 建立连接(无论是否认证都接受)
# ======================================================================
await ws_manager.connect_terminal(terminal_sn, websocket, subprotocol=subprotocol)
auth_label = "已认证" if is_authenticated else "未认证(仅查看)"
logger.info(f"终端 WebSocket 连接已建立: sn={terminal_sn}, 状态={auth_label}")
try:
# 消息接收循环
while True:
data = await websocket.receive_json()
# 处理心跳 ping
if data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
logger.debug(f"终端 WebSocket 心跳: sn={terminal_sn}")
# 处理状态刷新请求
elif data.get("type") == "request_status":
meetingroom_id = data.get("data", {}).get("meetingroom_id")
if meetingroom_id:
# 获取最新状态并推送
try:
from app.services.meetingroom_service import MeetingroomService
from app.services.wecom_service import WecomService
from app.config import settings
redis_client = settings.create_redis_client()
wecom_service = WecomService(redis_client)
mr_service = MeetingroomService(wecom_service, redis_client)
status_data = await mr_service.get_current_status(meetingroom_id)
await websocket.send_json({
"type": "room_status_update",
"data": {
"meetingroom_id": meetingroom_id,
"status": status_data.get("status"),
"current_meeting": status_data.get("current_meeting"),
"next_meeting": status_data.get("next_meeting"),
"minutes_to_next": status_data.get("minutes_to_next"),
},
})
except Exception as e:
logger.warning(f"终端请求状态刷新失败: sn={terminal_sn}, error={e}")
else:
logger.debug(
f"终端 WebSocket 收到未知消息: sn={terminal_sn}, "
f"type={data.get('type', 'unknown')}"
)
except WebSocketDisconnect:
ws_manager.disconnect_terminal(terminal_sn)
logger.info(f"终端断开 WebSocket 连接: sn={terminal_sn}")
except Exception as e:
ws_manager.disconnect_terminal(terminal_sn)
logger.warning(f"终端 WebSocket 异常断开: sn={terminal_sn}, error={e}")