# IT智能服务台 - 项目记忆 ## 测试铁律 - **e2e 测试 poll 不能依赖 `after_id`(UUID 排序 ≠ 时间排序)**:messages/poll 的 `after_id` 参数使用数据库主键 UUID 排序,与 `created_at` 时间戳排序不一致。跨会话复用用户时可能 poll 到修复前的旧消息。应使用 `created_at` 时间戳过滤 + 唯一测试用户。 - **宣布修复前必须端到端验证**:容器内测试通过 ≠ 外部用户可用。需经过 Mock 登录 → 发消息 → 查回复 全链路验证。 ## 设计决策(锁定) - AI交互:小段多回合;术语:**员工端"人工坐席"按钮** = 用户呼叫坐席(统一命名);"摇人"=坐席呼叫坐席 - 原型:坐席v5.3 + H5 v1.1;UI:企微浅色扁平,accent=#07C160 - 统一入口 `/itportal/` → user/agent/admin;admin需OTP - H5 布局铁律:人工坐席按钮在"发送键和语音按钮上方";截图说明 PC 显示/移动端隐藏 ## 技术架构 - 前端:坐席(Vue3+Element Plus) / H5(Vue3+Vant4) / 管理后台(Vue3+Element+Tailwind) - 后端:FastAPI + SQLAlchemy + PostgreSQL + Redis(代码在 `app/`) - 字段映射:后端`id`/`sender_type` → 前端`message_id`/`message_type`(`conversation.ts` 的 `mapMessage()`) - WS双连接池:`active_connections`(agent) + `employee_connections`(H5) - H5 参与者系统已完整存在:`ParticipantStrip`/`ParticipantList`/`InviteParticipantSheet` + `useParticipantDisplay` composable;`ROLE_BORDER_COLORS`/`ROLE_LABELS` 常量;`canInvite`=发起人 OR 已在会话中的参与者 ## 部署铁律(必读) - 正式服务器:itsupport.servyou.com.cn (10.90.5.110),出口IP `218.75.34.87` - 堡垒机:sxn@10.212.189.210:2222 (OTP),脚本 `C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py` - **服务器项目根路径**:`/opt/wecom-it-desk/` - 后端卷挂载:`./app:/app/app`,`.py`变更→`docker compose restart backend`;env变更→`docker compose up -d backend`(restart不重载env) - **docker-compose.yml environment 铁律**(2026-07-13 第二次踩坑导致 P0 `[object Object]`):`.env`变量不会自动传入容器,必须在`docker-compose.yml`的`environment:`显式声明每个变量(如 `DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-}`)。后端重启时若发现 env 为空但 `.env` 文件有值,首先检查 docker-compose.yml 是否声明。 - **前端部署铁律**:所有前端 dist 均为 ro bind mount,**只能在宿主机源路径操作**,不可在容器内修改 - H5:`/opt/wecom-it-desk/frontend-h5/dist` → `/usr/share/nginx/html/h5` (ro) [新增于 2026-07-13,此前仅 mount 到 `itdesk` 用于重定向] - H5重定向:`/opt/wecom-it-desk/frontend-h5/dist` → `/usr/share/nginx/html/itdesk` (ro) [用于 `/itdesk/` → `/h5/` 301重定向] - Agent:`/opt/wecom-it-desk/frontend-agent/dist` → `/usr/share/nginx/html/itagent` (ro) - Admin:`/opt/wecom-it-desk/frontend-admin/dist` → `/usr/share/nginx/html/itadmin` (ro) - Portal:`/opt/wecom-it-desk/frontend-portal/dist` → `/usr/share/nginx/html/itportal` (ro) - Terminal:`/opt/wecom-it-desk/frontend-terminal/dist` → `/usr/share/nginx/html/itterminal` (ro) - nginx.conf:`/opt/wecom-it-desk/nginx/nginx.conf` → `/etc/nginx/nginx.conf` (ro) - 部署命令模板:`H5_DIR=/opt/wecom-it-desk/frontend-h5/dist && cp -r $H5_DIR ${H5_DIR}_bak && rm -rf $H5_DIR/* && tar -xzf /tmp/h5-dist-vX.tar.gz -C $H5_DIR/ && docker compose up -d nginx`(**注意:容器重建,不是 nginx -s reload**) - 服务器 nginx `/h5/`:静态 `root /usr/share/nginx/html; try_files $uri /h5/index.html;` + `/h5/api/` 反代后端 - Docker bind mount铁律:rm后重建必须重启容器(或 `nginx -s reload` 热重载) - **dist 分块上传技巧(2026-07-13 验证)**:大文件上传时先排除 video 等不变目录(8.35MB→210KB),写 Python 脚本导入 `jms_ops` 模块复用 `PlinkSession` 做单会话 base64 分块上传(3800 chars/line × 15 lines/batch here-doc),单会话完成上传+部署+验证,避免 session cache 过期后 Chrome/Playwright 启动失败。脚本模板在 `.workbuddy/tmp/upload_and_deploy_h5.py`。 ## Admin 前端拦截器 Bug(2026-07-13 修复) - **根因**:`api/index.ts` 拦截器 `return res.data` 剥离了 axios 外壳,但 10 个文件仍用 `response.data.data` 旧模式 - **影响**:管理后台所有 API 调用 TypeError 被 catch 吞掉 → 集成页面"测试连接"报"请求失败,请检查网络连接" - **修复**:10 文件 25 处 `xxx.data.data` → `xxx`;3 种变量名模式:response/res/leakResp - **铁律**:新增 API 调用时直接用返回值(`const data = await getXxx()`),不要 `response.data.data` - **Vite 构建铁律**:`dist/assets` 超 50 文件时 safe-delete 会阻止构建,需先重命名旧 dist 为 dist_bak 再构建 - **Vite `@` 别名铁律(2026-07-13)**:Windows 上 `resolve.alias: { '@': '/src' }` 会解析为 FS 根路径(`D:\src\`),必须用 `fileURLToPath(new URL('./src', import.meta.url))` - **`.d.ts` vs `.ts` 铁律(2026-07-13)**:含运行时值(如 `const TONE_LABELS = {...}`)的文件不能用 `.d.ts` 扩展名,Vite 构建时会报 `does not provide an export named`。应使用 `.ts` ## 文件上传(关键经验教训) - ⚠️ **elFinder 上传二进制**:旧版手动上传 tar.gz 后 MD5 不匹配(差字节)。**2026-07-13 验证**:`jms_ops.py upload` 命令对 >=100KB 文件走 elFinder Web UI 通道,上传 7.36MB `tar.gz` MD5 完全匹配 ✅,推荐优先使用。如必须用 base64 通道,则参考下一条铁律。 - ⚠️ **JumpServer PTY 4096 字节行缓冲铁律(2026-07-13 严重事故)**:base64 单行字符数若超 4096 会被 PTY 静默丢弃,导致 gzip 流损坏、线上瘫痪。脚本 `.workbuddy/tmp/chunked_upload_v2.py` **必须按 base64 字符数分块 `B64_CHUNK=3800`**(非字节数)。subprocess timeout 设 1800s、`--cmd-timeout` 15s。脚本 print 用 ASCII(禁用 emoji,否则 GBK 崩溃)。上传后比对远程 md5sum 与本地。 - pscp/plink -T不可用;elFinder上传后需手动mv;httpx.Timeout须含default - JumpServer v2.28 变更:登录新增图片验证码(CAPTCHA);connection-token 端点改为 `/api/v1/authentication/connection-token/` ## 外部集成 - 企微通讯录:通讯录同步Secret `BM6iosc3gKnPqkEXmsQN3ErJUpfO-whfMUN646eezB8` 已废弃(2022年8月后新增IP不能调department/list和user/list,errcode=48009,非IP白名单问题)。改用**自建应用Secret + 可见范围设为全公司**,`.env` 中 `WECOM_CONTACT_SECRET=` 留空触发降级。验证:651部门/7098员工 - Dify:`http://yw-dify.dc.servyou-it.com/v1/chat-messages` - 审批意图Key `app-7jkRkAzvX4QM9v9SM3P8mMEO`(✅200,结构化JSON) - 分诊Key `app-z3S9AEUUAVPbtR2rioxpiIvp`(✅200,结构化JSON) - `app-UaTWYdBSwN6VktKQlbh5YN5H`(✅200 纯文本,已禁用) - `app-J3s8sHarZQ2SCaNF3xCppliL`(❌400 "Model credentials not initialized") - **Dify App ID**(Console):`8f0f3d62-f63d-4cf3-815e-b10529c66f1d`(智能IT支持-员工咨询,advanced-chat,85节点) - ⚠️ **Dify 发布铁律**:`advanced-chat`应用改工作流后**必须浏览器手动点「发布」**,否则API返回400 `"Workflow not published"`;此版本 publish 端点不支持 Console API 调用。 - **Dify 原生 API 直连**(v2.1):后端 `_call_dify_native()` 直连 `/v1/chat-messages`(Bearer+blocking),优先于代理;`config.py` 配 `dify_native_base_url`/`dify_native_api_key`,**docker-compose.yml 须显式声明**(`.env` 定义不会自动传入容器)。 - ⚠️ **两次踩坑**:第一次是配置读取时未意识到容器内为空;第二次是 `[object Object]` 根因正是此变量未声明导致原生 API 不可用、回退到 buggy 代理路径。 - ⚠️ **第三次踩坑(2026-07-13 坐席扫码)**:`WECOM_SSO_CALLBACK_BASE` 未声明导致 `qrcode_service.py` 抛异常 `ValueError: 请在环境变量中配置 WECOM_SSO_CALLBACK_BASE`。 - **必须确保 `environment:` 部分有**(新增变量需同时加入 `.env` 和 `docker-compose.yml`): ```yaml - DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-} - DIFY_NATIVE_API_KEY=${DIFY_NATIVE_API_KEY:-} - BAIDU_ASR_APP_ID=${BAIDU_ASR_APP_ID:-} - BAIDU_ASR_API_KEY=${BAIDU_ASR_API_KEY:-} - BAIDU_ASR_SECRET_KEY=${BAIDU_ASR_SECRET_KEY:-} - WECOM_SSO_CALLBACK_BASE=${WECOM_SSO_CALLBACK_BASE:-https://itsupport.servyou.com.cn} ``` - **Dify Prompt 自动更新**(v2.2):Console API Token(`localStorage.console_token`) → `/console/api/apps/{app_id}/workflows/draft` GET/POST 批量改 LLM 节点 System Prompt;发布仍需手动。 - RAGFlow:生产 `http://10.80.0.85:8080/` / API `:9380` - 映射策略:联软(主) > aTrust(VPN) > eHR(静态) ## 企微JS-SDK技术 - 双鉴权:`wx.config()`(jsapi_ticket) + `wx.agentConfig()`(agent_config_ticket),签名算法相同但**不能混用** - `wx.invoke('thirdPartyOpenPage', {oaType:'10001', templateId, thirdNo, extData})` 原生打开审批表单 - 后端端点:`GET /wecom/jsapi-config?url=...&with_agent_config=true` - 前端composable:`frontend-h5/src/composables/useWecomApproval.ts`(懒加载+全降级+超时保护) - 既有bug:`EmergencyDispatcher.vue` 第99-119行 复用jsapi签名给agentConfig(靠3秒超时兜底) ## 已上线功能模块 - 群聊(摇人/邀请/四角色) / 审批(12类型18流程/三级意图) / 代办(getapprovalinfo+Semaphore/缓存45s) - IT资产推送(模板`Bs7ucT...`) / 语音转文字(手机JS-SDK/PC百度ASR) / 截图拍照 / 复杂场景P0~P3 - 会议室预定(终端`/itterminal/`,企微会议室Secret待申请) - 上下文感知智能诊断→修复闭环(三层诊断+三段排队+答题插队+五场景关闭);H5 QueueWaiting/RightPanel双Tab/InputBar三态人工按钮/ResolveConfirmCard;坐席pending_close结单 - 单通道统一消息架构:Dify输出JSON `{text,action,options}` → 后端发双WS(ai_reply+dynamic_recommend) → 文字到气泡/卡片到侧边栏;坐席端 ai_thinking 指示器 + ai_structured/byod_card 渲染 ## H5 版本部署记录 - v4:人工按钮三态"人工坐席",直接调 `store.shakeAgent()` - v5:RightPanel v2.1 删软件安装/资源权限标签,智能推荐直接展示 - **v7(已部署 2026-07-13)**:① InputBar 删除📷截图图标+说明(工具栏仅表情/文件)② ParticipantStrip 加 hover tooltip(姓名+角色+部门,仅PC) + `+邀请`按钮(canInvite) + InviteParticipantSheet 引用 ③ 头像条位于会话框正上方 `v-if="store.participants.length>0"` 保持;④ 修复 CSP 头(添加 `'unsafe-inline'` 和 `https://at.alicdn.com`)⑤ 修复 docker-compose mount(新增 `h5` 目录 bind mount);构建 index-BFvX9hTe.js / index-DLWx4YQ2.css - **v11(已部署 2026-07-13)**:修复"IT资产升级"审批报错"审批模板ID不正确"——原企微审批模板 `Bs7ucTGsPuFhxfk8pn8EydxrWxkVetB4JR8Pb6PHS` 已失效,改用 ITSM 工单系统 `https://devops.dc.servyou-it.com/ITSM/workflow/service/createTicket?name=IT%E8%AE%BE%E5%A4%87%E5%8D%87%E7%BA%A7%E4%B8%8E%E7%A1%AC%E4%BB%B6%E7%BB%B4%E4%BF%AE` - **(2026-07-17)零信任VPN卡片免登录修复**:将ITSM跳转URL从 `devops.dc.servyou-it.com` 改为 `itsm.servyou.com.cn`(企微应用主页域名,从企微工作台打开时免登录) ## 免登录跳转域名选择(2026-07-17) - **ITSM系统**:使用 `itsm.servyou.com.cn`(企微应用主页域名)可实现免登录 - **旧域名** `devops.dc.servyou-it.com` 需要扫码登录 - **原因**:`itsm.servyou.com.cn` 已配置为企微OAuth2.0应用主页,从企微工作台打开时自带身份信息 ## 组织架构树(2026-07-13 重构) - **层级树**:`build_org_tree()` 用企微 `department/list` 的 `parentid` 构建真正层级树(非扁平分组) - 部门节点 ID 加 `dept_` 前缀避免与员工 UserID 冲突;员工出现在其所属的所有部门下 - 三层缓存:`wecom:org_directory`(1800s) + `wecom:dept_list`(1800s) + `wecom:org_tree:{endpoint}`(1800s) - `get_org_directory()` 用 `asyncio.gather` 并行调用企微 API - 降级模式:部门列表不可用时 `_build_flat_tree_by_name()` 按名称分组 - H5 前端:多层级扁平化渲染(非 van-collapse),深度缩进 + 展开折叠 - Agent 前端:el-tree 原生支持多层级,无需改代码 - 测试:`backend/tests/test_org_tree.py` 47 个单元测试 ## 运维工具 - SOP:`docs/10-项目管理/IT智能服务台-标准作业流程SOP.md` - 故障排查:`docs/09-部署运维/00-标准故障排查手册.md` - 全栈改造实施计划:`docs/02-产品需求/AI对话链路全栈改造实施计划-v1.0.md`