本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
9.9 KiB
技术方案-REQ-用户-004-坐席在线状态查询
基本信息
| 字段 | 内容 |
|---|---|
| 需求编号 | REQ-用户-004 |
| 版本 | v1.2 |
| 日期 | 2026-07-25 |
| 状态 | 已实现(BUG-用户-001 已修复) |
| 作者 | Simon |
| 关联文档 | PRD-REQ-用户-004、BUG-用户-001 |
1. 技术架构设计
1.1 接口设计
1.1.1 新增 API
路径:GET /h5/agents/online-status
所属模块:backend/app/api/h5.py(Nginx 将 /h5/agents/* 代理到 FastAPI /h5/agents/*)
认证方式:公开接口,无需认证
请求参数:无
响应格式:
{
"code": 0,
"message": "success",
"data": {
"online": true
}
}
响应字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| code | int | 0=成功,非0=失败 |
| message | string | 响应描述 |
| data.online | boolean | 是否有在线坐席 |
1.2 数据流设计
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ H5前端 │ HTTP │ 后端API │ SQL │ PostgreSQL │
│ (轮询30秒) │────────▶│ /h5/agents │───────▶│ agents表 │
│ │ │ /online-status│ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ online: true/false │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 标题栏状态 │ │ status='online'│
│ 坐席在线 │ │ 的坐席记录 │
└─────────────┘ └─────────────┘
2. 后端实现
2.1 接口实现
文件:backend/app/api/h5.py
@router.get("/h5/agents/online-status")
async def get_agents_online_status(db: AsyncSession = Depends(get_db)):
"""获取坐席在线状态(H5公开接口)。
查询当前是否有在线坐席,返回 true/false。
Returns:
Dict: 统一响应格式,包含 online 字段
"""
# 查询在线坐席数量
stmt = select(func.count(Agent.id)).where(Agent.status == "online")
result = await db.execute(stmt)
count = result.scalar() or 0
return success_response(data={"online": count > 0})
2.2 依赖导入
from sqlalchemy import select, func
from app.models.agent import Agent
2.3 路由注册
在 backend/app/api/router.py 中确认 h5 路由已注册:
# 确认已有
router.include_router(h5.router, prefix="/h5", tags=["H5"])
3. 前端实现
3.1 Store 修改
文件:frontend-h5/src/stores/conversation.ts
修改内容:
// 将硬编码改为从 API 获取
const agentOnline = ref<boolean>(false)
// 新增获取坐席在线状态的方法
async function fetchAgentOnlineStatus() {
try {
const res = await fetch('/api/h5/agents/online-status')
const data = await res.json()
if (data.code === 0) {
agentOnline.value = data.data.online
}
} catch (e) {
console.error('获取坐席在线状态失败:', e)
// 失败时默认离线,避免误导
agentOnline.value = false
}
}
// 新增轮询定时器
let pollTimer: number | null = null
function startPolling() {
if (pollTimer) return
// 立即获取一次
fetchAgentOnlineStatus()
// 每30秒轮询
pollTimer = window.setInterval(fetchAgentOnlineStatus, 30000)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
// 页面可见性处理
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling()
} else {
fetchAgentOnlineStatus()
startPolling()
}
})
}
// 初始化时启动轮询
onMounted(() => {
startPolling()
})
// 组件卸载时停止轮询
onUnmounted(() => {
stopPolling()
})
3.2 模板修改
文件:frontend-h5/src/components/chat/ChatPanel.vue
现有模板已支持:
v-if="store.agentOnline"显示"坐席在线"v-else显示"坐席离线"
无需修改。
4. WebSocket 断连自动离线
4.1 背景
坐席端关闭浏览器窗口时,需要自动将坐席状态更新为 offline,否则 H5 端将始终显示"坐席在线"。
4.2 实现
文件:backend/app/api/ws.py
在 agent_websocket_endpoint() 的 finally 块中添加离线更新逻辑:
finally:
# 更新坐席状态为 offline(finally 块确保断开时一定执行)
try:
session_factory = _get_session_factory()
async with session_factory() as db:
# ⚠️ agent_id 在 URL 中对应 Agent.user_id(不是 Agent.id)
stmt = select(Agent).where(Agent.user_id == agent_id)
result = await db.execute(stmt)
agent = result.scalar_one_or_none()
if agent and agent.status != "offline":
agent.status = "offline"
await db.commit()
except Exception as e:
logger.warning(f"更新坐席状态失败: {e}")
4.3 Bug 根因(重要教训)
WS URL 参数 {agent_id} 实际传递的是企微 user_id(如 sxn),而非 DB 主键 Agent.id(如 agent-sxn-001)。最初代码错误使用 Agent.id == agent_id,导致每次都查不到坐席,离线状态从未生效。
| 字段 | 值 | 说明 |
|---|---|---|
Agent.id |
agent-sxn-001 |
DB 主键(UUID 格式) |
Agent.user_id |
sxn |
企微 userid |
URL {agent_id} |
sxn |
实际传递的是 user_id |
修复:Agent.id → Agent.user_id
5. Nginx 路由配置
5.1 问题
Nginx 中 /h5/ 被配置为静态文件 alias,导致 /h5/agents/online-status 返回 H5 首页 HTML 而非 JSON。
5.2 解决方案
在 nginx.conf 的 location /h5/ 之前添加代理规则(nginx 最长前缀匹配优先):
# H5 坐席状态 API — /h5/agents/
location /h5/agents/ {
proxy_pass http://backend_api/h5/agents/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# H5 坐席队列状态 API — /h5/queue/
location /h5/queue/ {
proxy_pass http://backend_api/h5/queue/;
...
}
# H5 用户端(兜底,静态文件)
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/index.html;
}
需要同时在 HTTPS (443) 和 HTTP (80) 两个 server block 中添加。
6. 部署与配置
6.1 无需新增配置
- 不需要新的环境变量
- 不需要新的数据库表
- 不需要新的 Redis 缓存
6.2 部署顺序
- 部署后端(重启)
- 部署 H5 前端
7. 测试要点
5.1 后端测试
# 测试接口返回
curl http://localhost:8000/api/h5/agents/online-status
# 预期响应
{"code":0,"message":"success","data":{"online":true}}
5.2 前端测试
| 测试场景 | 预期结果 |
|---|---|
| 有在线坐席 | 显示"坐席在线"(绿色) |
| 无在线坐席 | 显示"坐席离线"(灰色) |
| 30秒后 | 状态自动刷新 |
| 切换浏览器标签页 | 暂停轮询 |
| 切回浏览器标签页 | 立即刷新一次 |
8. 风险与回滚
8.1 风险
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 轮询频率过高 | 增加数据库查询压力 | 限制30秒,且只查 count |
| 接口被恶意调用 | 无(只读数据) | - |
8.2 回滚方案
- 后端:回滚 h5.py 中的接口代码
- 前端:恢复
agentOnline = true硬编码
9. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 |
|---|---|---|---|
| 2026-07-25 | v1.0 | 初始版本 | Simon |
| 2026-07-25 | v1.1 | API 路径修正(/api/h5/ → /h5/);新增 §4 WS 断连自动离线(含 Agent.id→user_id bug 修复);新增 §5 Nginx 路由配置(/h5/agents/ 代理);状态更新为已实现 |
Simon |
| 2026-07-25 | v1.2 | BUG-用户-001 修复:新增 §10 离线限制呼叫人工(后端 shake 加在线检查 + 前端 InputBar 加 agentOnline 判断) | Simon |
10. BUG-用户-001 补充:离线限制呼叫人工(v1.2)
10.1 问题
坐席离线时,用户点击"人工咨询"按钮,后端直接入队 queued,因无在线坐席永久无人接单。
10.2 后端修改
文件:backend/app/api/h5.py — shake 接口
在 conversation.status = "queued" 之前增加坐席在线检查:
# 前置校验:必须有在线坐席
from app.models.agent import Agent
from sqlalchemy import select, func
online_count = await db.execute(
select(func.count(Agent.id)).where(Agent.status == "online")
)
count = online_count.scalar() or 0
if count == 0:
raise AppException(
code=1003,
message="暂无在线坐席,请稍后再试。Duckula(达寇拉)仍在努力为您提供帮助~"
)
10.3 前端修改
文件:frontend-h5/src/components/chat/InputBar.vue
callAgentState 计算属性首行增加坐席在线判断:
// 坐席离线 → 按钮禁用
if (!store.agentOnline) return 'disabled'
按钮提示文案调整:callAgentBtnTitle 中离线时显示"坐席离线,暂不可用"。
10.4 测试要点
| 场景 | 预期 |
|---|---|
| 坐席离线,AI ≥3次 | 按钮灰色,提示"坐席离线,暂不可用" |
| 坐席上线(30s内),AI ≥3次 | 按钮绿色,可点击 |
| 离线时 curl shake | { code: 1003, message: "暂无在线坐席..." } |