chore: 清理根目录冗余历史与废弃副本

**根目录项目文档删除**(已迁入 docs/01-产品文档/):
- README.md / CHANGELOG.md / CONTRIBUTING.md
- mkdocs.yml / openapi.json / set-real-ip-patch.md
- (原 configs/ 移至根目录后被 git 识别为 rename)

**废弃源码副本清理**:
- frontend-h5/ 整个目录被 src/frontend-h5/ 取代(src 重组后未删除)
- frontend-h5-dist-part[1-3].bin 旧版部署分片

**历史归档清理**(根目录的 *.zip / *.bin / *.b64):
- agent-v1.4/1.5/1.6/1.7.zip
- chunk_0000.bin
- diagnose-500.b64 / fix-prod.b64

**根目录散落调试脚本清理**(已迁入 scripts/):
- build_frontend.ps1 / debug_*.py / test_*.py(18 个)
- deploy_*.py / pack_fix*.py / split_upload_v8.py
- tmp_upload_*.py / append_upload_v8.py
- add_*.py / add_full.py / add_neo4j.py
- update_*.py / verify_prompt.py / update_mfa.*
- split-b64.ps1 / fix-prod.sh / diagnose-500.sh / stage1-probe.sh
- start-backend.sh / run_db_check.py / main_original.py
- list_nas_*.ps1 / probe_nas.ps1
- init_neo4j*.cypher / fix_*.cypher / fix_alembic.sql
- mkdocs.yml / openapi.json / set-real-ip-patch.md

合计 82 文件删除(git 自动识别大量 rename 已在前序 commit 中处理)

**未 commit 保留在工作区的临时文件**(已 restore --staged):
- $null              bash 输出重定向残留
- --selector          git checkout 残留空文件
- test_api2.py        临时调试
- test_output.txt     chunk 列表临时输出
- .workbuddy/outputs/ WorkBuddy 工作区产物(应由 .gitignore 排除)
- archives/           历史归档 85MB(不适合入仓)
- 02meiti/            已 .gitignore
This commit is contained in:
Simon
2026-08-03 18:51:25 +08:00
parent 53b7bbcff7
commit aaebe31bd8
82 changed files with 12 additions and 5015 deletions
-350
View File
@@ -1,350 +0,0 @@
# 变更日志 (Changelog)
本项目的所有重要变更都会记录在此文件。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [未发布] - 2026-07-15
### 🐛 问题修复 (Bug Fixes)
#### 坐席端 Ctrl+V 粘贴功能修复 (2026-07-15 10:00 部署)
- **问题**:坐席端输入框(ReplyBox.vue / InputBox.vue)无法通过 Ctrl+V 粘贴文本
- **根因**`handlePaste` 函数在 `clipboardData` 为空时直接 return,阻止了默认粘贴行为
- **修复**:添加 `clipboardData``items.length` 空检查,确保纯文本粘贴时让浏览器执行默认行为
- 任务说明书:`docs/10-项目管理/任务说明书/任务说明书-79-坐席端粘贴功能修复.md`
---
## [未发布] - 2026-07-13
### 🚀 功能增强 (Features)
#### Agent 坐席端 v5 — AI 结构化消息 + 思考指示器(2026-07-13 01:38 部署)
- **ai_structured 只读渲染**:坐席端 `MessageBubble.vue` 新增 AI 结构化消息渲染分支(文字 + 只读选项标签 + 推荐摘要)
- **byod_card 渲染**:补全之前缺失的 `byod_card` 消息类型渲染分支
- **AI 思考指示器**`ChatArea.vue` 新增 `aiThinkingText` 计算属性 + 脉冲动画 CSS,坐席可实时看到 AI 正在思考
- **handleNewMessage 透传修复**:修复 `msg_type``extra_data` 硬编码为 `'text'` 的问题,正确透传消息类型
- **ai_thinking 双推**:后端 `ai_thinking` WS 消息同时推送给员工端和坐席端
- 验证:JS hash `index-2BTn4SZz.js` ✅,5 容器全部 healthy ✅
#### 坐席端历史会话查询功能(2026-07-13 11:30 部署)
- **功能位置**UserInfoBar(员工信息栏)等级 chips 后方新增"历史会话"开关
- **跨会话聚合**:查询该员工所有会话的消息,合并为一条时间线(按 created_at DESC 排序)
- **会话分隔条**`ConversationSeparator.vue` 组件,显示首条消息摘要(前20字)+ 当前会话高亮标记
- **游标分页**:支持 `before` 游标参数,滚动到顶部自动加载更多历史消息
- **后端 API**`GET /employees/{employee_id}/history-messages`(需认证),返回 `HistoryMessageListResponse`items + has_more + conversation_summaries
- **前端状态管理**`conversation.ts` store 新增 `historyMode`/`historyMessages`/`historyLoading`/`historyHasMore` 等状态
- **只读模式**:历史模式下隐藏回复框,显示"查看历史会话,仅供阅读"提示条
- 测试:28/28 全部 PASSSOP 2轮)
- 验证:前端生产包 grep 检查通过 ✅,后端 API 权限校验生效 ✅
#### H5 员工端 v4 — 人工坐席交互改造(#116, 2026-07-13 部署)
- **人工按钮三态文案统一**为"人工坐席"(原"人工(需更多对话)"等多态文案)
- **按钮位置调整**:移至发送键 + 语音转文字图标上方(`.input-bar__controls` 容器内垂直堆叠)
- **删除 CallAgentModal 弹窗动画**:点击按钮直接调用 `store.shakeAgent()`,无中间浮窗
- **截图快捷键提示改版**:改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V",排列在工具图标后
- **移动端隐藏截图提示**CSS `@media (max-width: 768px)` 媒体查询
- **AI 转人工提示**:"已为您呼叫人工坐席,请稍等!"(原"少主,折旧为您去摇人,稍等….")
- **坐席接入提示**:"坐席正在查看您的信息,请等待处理回复!"(原"坐席已为您服务,请稍后….")
- **删除"摇铃呼叫坐席"入口和文案**
- **清理孤儿组件** `MessageList.vue`(全项目无引用)
- **删除 CSS** `@keyframes shake` 动画及相关变量
- **DB 同步**PostgreSQL `funny_phrases` 表 3 条记录(shake/connected/keywordUPDATE
- 验证:JS hash `index-eQVEQIDL.js``index-B6dzwk-X.js` ✅,JS 包内容检查通过
#### AI 对话链路全栈改造 Phase 1-6#59-#69, 2026-07-13 01:38 生产部署)
- **Phase 1 ✅**Dify Prompt JSON 输出 + 后端 blocking + JSON 解析 + 双 WS 推送 + 错误降级(30s 超时 / 15s still_thinking
- **Phase 2 ✅**:审批关键词收窄(~40→~25 强意图词)+ 两级分类 Prompt v4.0(4粗→12细)+ 删除前端 `checkApprovalIntent()`
- **Phase 3 ✅**WS 扩展(`ai_thinking` + `dynamic_recommend`+ `MessageBubble` ai_structured 渲染 + `RightPanel` v2(手风琴 + 底部标签)+ `DynamicRecommend.vue`(新建)+ `sendOptionSelect` WS 回传
- **Phase 4 ✅**`VisionService` 接入(`_enrich_image_content` + `_fetch_recent_employee_text` 5秒融合)+ 图片消息跳过关键词拦截 + 降级策略
- **Phase 5 ✅**:坐席端 `ai_thinking` WS + 指示器 UI + `MessageBubble` ai_structured/byod_card 渲染 + `handleNewMessage` 修复
- **Phase 6 ✅**`diagnosis_stage` 字段(6种值)→ `closing_service` 辅助方法 + `response_time_ms` 计时 + 慢响应告警(>10s)
#### 上下文感知智能诊断→修复闭环(2026-07-12 部署)
- **三层诊断**API → Script → AI 递进式诊断
- **三段排队**VIP → info_locked → not locked
- **答题插队**:员工答题期间优先处理
- **五场景关闭**:五种场景自动关闭会话
- 后端:迁移 0526表+6列)/ `queue_service` / `quiz_service` / `closing_service` / `seed_quiz` / 每日3:00定时生成
- H5 前端:`QueueWaiting` / `RightPanel` 双Tab / `InputBar` 三态"人工"按钮 / `ResolveConfirmCard`
- 坐席前端:`pending_close` 结单流程;信息锁定(Dify 步骤完成 + 有效回答率≥70%)
#### 坐席端布局优化 v2.02026-07-12 部署)
- 8 新增 + 7 修改 + 3 删除
- `QuickReplyBar` L1+L2 悬浮;`ReplyBox` 左右分区;右栏 260↔560px 模式切换
- **键盘快捷键 v2.3**:纯数字 1~9 上下文路由(AI/L1/L2);ESC 分层撤销;Shift+Space 用 `event.code` 匹配(不受 IME 影响)
- `useKeyboardShortcuts.ts` 中央管理器,IME/ScreenCapture 守卫
#### 知识库迭代 3 功能(2026-07-12 部署)
- 分诊交互(H5+坐席+Dify 独立应用)
- 拓扑预览(ECharts 只读)
- 代答排除(4种匹配器)
- 44 文件 43 测试通过;迁移 051
### 🐛 缺陷修复 (Bug Fixes)
- 修复:代办事项企微审批 API 返回空列表(8 个问题逐一修复)
1. `WECOM_APPROVAL_SECRET` 未注入容器 → docker-compose.yml 添加环境变量
2. Redis 无密码认证 → Redis command 添加 `--requirepass`
3. Docker bind mount `./app:/app/app` 丢失 → 恢复卷挂载
4. 企微 `getapprovaldata` API 已废弃(404) → 改用 `getapprovalinfo` 新 API`new_cursor` 分页 + `sp_no_list`
5. `token_manager.py` 两处 `cached.decode("utf-8")` 报错 → `isinstance` 安全检查(Redis `decode_responses=True` 返回 str
6. errcode=60020 IP 不在"审批"应用白名单 → 改用 `TokenManager`IT 支持应用 Secret,IP 已在白名单)
7. errcode=301025 invalid filter → 企微 API 每个 filter key 只能出现一次,去掉 API 层 template_id 过滤,改代码层过滤
8. `_extract_current_approver` 字段名全错 → `record.status``record.sp_status``record.approver[]``record.details[].approver.userid`(经 JSON dump 确认实际 API 返回结构)
- 修复:验证通过,sxn 名下 2 条审批待办正确返回(IT 资产外修申请)
- 修复:nginx 容器配置丢失导致页面加载失败
- 修复:后端 h5.py `_require_wework_ua` NameError 导致 OAuth 认证失败
### 🔐 安全 (Security)
- P0:WS token 改走 `Sec-WebSocket-Protocol` subprotocol(已修)
- P0:坐席登录加 `password_hash` bcrypt 字段
- P0:`/ws/` 路径 nginx access_log 关闭
- P0:5 鉴权漏洞全部修复(消息 5 端点)
- WECOM_SECRET 集中化(待 NAS Vault)
- Gitea 凭据走 wincred,不入文件
### 🏗️ 基础设施 (Infrastructure)
- 蓝绿部署支持:新增 docker-compose-green.yml、switch-blue-green.sh、nginx-green-upstream.conf
- Green 环境端口:后端 5002Nginx 5080/5443
- Gitea 自托管部署(Synology 套件 8418 端口)
- Tailscale Funnel 暴露给 workbuddy 沙箱
- 分支保护:main 需 PR + 1 reviewer
- workbuddy-claude 配 access token + 自动跑批
- 备份脚本(7 天保留 + cron 3 点)
- **服务器部署路径修正**:确认服务器项目根路径 `/opt/wecom-it-desk/`,所有前端 dist 均为 ro bind mount
- **前端部署命令模板**`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 exec wecom_it_nginx nginx -s reload`
### 📚 文档 (Documentation)
- 新增 8 份审计/设计报告(Dockerfile / ER / 依赖 / 健康检查 / CORS / 一键部署 / 健康度 / 惊喜汇总)
- 4 份 ADR(ADRs 001-004)
- 4 份 SOP(SOPs 001-004)
- 2 份路线图(阶段 1 盘点 + 阶段 4-5 规划)
- Wingman 设计文档
- 4 前端审计 + 16 项统一优化路线
- AI 对话链路全栈改造实施计划 v1.0(`docs/02-产品需求/AI对话链路全栈改造实施计划-v1.0.md`
### 🛠️ 工具链 (Tooling)
- `scripts/pre-commit-check.sh`:4 件套预检(鉴权+依赖+alembic+配置)
- `scripts/backup-gitea.sh`:Gitea 备份 + 恢复
- `scripts/security-audit.sh`:5 工具集成审计
- `scripts/generate-api-docs.sh`:OpenAPI + Swagger UI + ReDoc
- `scripts/dashboard.py`:项目健康度仪表盘
- `scripts/oneclick-deploy.sh`:一键部署
---
## [0.5.0] - 2026-05-30
### ✨ 新增 (Added)
- 阶段 1 完成度 66%(47 项功能盘点)
- H5 员工端完整功能(11 组件)
- 坐席工作台三栏(23 组件)
- 管理后台 13+ 视图
- 统一入口 portal
- WebSocket 实时通信
- WebSocket fallback 轮询
- Dify AI 集成(基础)
- 4 个外部系统集成(火绒/联软/aTrust/eHR)
- 快速回复 + 排障模板 + 待办事项
### 🐛 修复 (Fixed)
- 5 鉴权漏洞
- WS token 泄露到 URL 和日志
- 坐席登录缺 password
- Mock login bypass
### 📈 性能 (Performance)
- 4 前端路由级代码分割
- WebSocket 长连接(替代轮询)
- 模板缓存(Redis)
---
## [0.4.0] - 2026-04-15
### ✨ 新增
- RBAC 角色管理(user/agent/admin)
- 角色自动映射(企微标签 + eHR 字段)
- 配置变更日志(审计)
- 趣味话术(摇人/等待/接入)
- 审批流程链接
- 软件下载入口
### 🐛 修复
- 部门权限粒度
- 紧急度评分算法
- VIP 标记自动匹配
---
## [0.3.0] - 2026-03-01
### ✨ 新增
- AI 草稿回复(坐席采纳)
- AI 实质性回复计数
- 紧急度评分(1-5)
- 标签系统(举手/情绪/需介入)
- 影响范围评估
- 阻断性标记
---
## [0.2.0] - 2026-01-15
### ✨ 新增
- 4 前端基础架构(Vue 3 + Vite + TS + Pinia)
- 16 张数据表
- 核心 API(40+ 端点)
- OAuth2 企微登录
- 消息收发(文本/图片/文件/语音)
- 会话分配/抢单/转接
- 协作坐席(摇人)
- 邀请功能(P0-09~11)
---
## [0.1.0] - 2025-12-01
### ✨ 初始版本
- 项目初始化
- 基础 FastAPI 框架
- SQLAlchemy 2.0 + async
- Alembic 迁移
- Docker Compose 编排
- 4 前端工程搭建
- 企微回调基础
---
## 版本说明
- **0.x.y** - 阶段 1-5 演进(0.1-0.5 已发布,0.6+ 阶段 2 启动)
- **1.0.0** - 正式版目标(预计 2026-12,阶段 5 完成后)
> 📌 **文档同步说明**:各版本的详细变更记录请参考 `docs/archive/RELEASE_NOTES_*.md`,本文档仅保留版本概览。
## 图例
- ✨ 新增 - 新功能
- 🐛 修复 - Bug 修复
- 📈 性能 - 性能优化
- 🔐 安全 - 安全修复
- ⚠️ 弃用 - 即将移除
- 🏗️ 基础设施 - 部署/工具/流程
- 📚 文档 - 文档更新
- 🛠️ 工具链 - 工具脚本
[未发布]: https://gitea.simon.local/simon/wecom/wecom_it_smart_desk/compare/v0.7.0...HEAD
## [v0.7.1] - 2026-06-23(规划中)
> **决策背景**(2026-06-22):v0.7.0.1-hotfix1(QR 码生成)上线后,生产仍报 2 个 bug:
> - 员工/坐席扫码登录报错(`/api/auth_qrcode/scan` 失败)
> - 管理员 sxn 登录报错(`agents.otp_secret` 列不存在 — alembic 010 未跑)
> 用户决策:**不再修 7.0.1**,直接进 v0.7.1 统一治理。
### 🔧 修复 (Fixed)
#### P0 — 登录失败
- **管理员 sxn 登录报错**:根因 — alembic 010 `agents.otp_secret` 列未在生产数据库创建
- 修复:合并 `otp_secret/otp_enabled`(010)与 `mfa_secret/mfa_enabled`(023)双字段,模型统一引用 `mfa_secret/mfa_enabled`
- migration:重写 021_rbac(原文件丢失),统一 010-025 chain
- **员工/坐席扫码登录报错**:根因待查(预计 ticket 状态机 / WecomService 初始化 / 高并发 session)
- 修复:在 dev 复现,出 patch
#### P0 — 基础设施
- **修 `/api/ready` import error**(原 defer to v0.7.1)
- **审计 alembic chain**:`021_rbac` 缺失 / 022-025 chain 错乱,出 `docs/alembic_history_audit.md`
### 🆕 新增 (Added)
#### P1 — 体验优化
- **企微入口 SSO**(原 v0.7.1+ backlog):识别 WeChat Work User-Agent,自动识别员工身份 + 跳对应端点,扫码登录降级为 fallback
#### P1 — 权限
- **管理后台 RBAC 细粒度角色权限**:5 角色 + 4 资源 + 4 操作 + 3 数据范围
### 📝 文档 (Documentation)
- `docs/DEPLOY-QUICK-v0.7.1.md` — 一键部署操作包(基于 7.0 模板)
- `docs/alembic_history_audit.md` — chain 审计报告
- `docs/USER-GUIDE-WECOM-SSO.md` — 企微 SSO 用户手册
---
## [v0.7.0] - 2026-06-21
### 🎉 新增 (Added)
#### 扫码登录(阶段 1.1-1.3)
- 后端 `app/api/auth_qrcode.py` (236 行) — 4 端点 create / poll / scan / confirm
- 后端 `app/services/qrcode_service.py` (487 行) — 业务逻辑 + dev 模式 mock OAuth
- 后端 `app/schemas/qrcode.py` (127 行) — Pydantic 模型
- 后端 alembic migration 022_qrcode_login(数据存 Redis,无 schema 变更)
- 前端 `frontend-agent/src/views/Login.vue` — ElementPlus 扫码 UI + 倒计时
- 前端 `frontend-portal/src/views/QrcodeLogin.vue` — 角色自动分发
- 前端 `useQrcodeLogin.ts` composable (agent + portal 双端) — 2s 轮询 + 120s TTL
- 前端 `frontend-portal/src/router/index.ts` — 默认 `/``/qrcode-login`
- 文档 `docs/NGINX-DOMAIN-ROUTING.md` — 单域名 + 多路径架构
- 文档 `docs/USER-GUIDE-QRCODE-MFA.md` — 员工/坐席/管理员用户手册
#### MFA 二次认证(阶段 2.1-2.4)
- 后端 `app/api/mfa.py` (389 行) — 6 端点:status / bind/start / bind/confirm / verify / disable / admin/reset
- 后端 `app/services/mfa_service.py` (179 行) — pyotp TOTP + Redis verified TTL 1800s
- 后端 `app/models/agent.py` — mfa_secret / mfa_enabled / mfa_bound_at / mfa_last_verified_at
- 后端 alembic migration 023_mfa_fields — User MFA 4 列
- 前端 `frontend-agent/src/api/mfa.ts` — 5 个用户端 API
- 前端 `frontend-agent/src/views/MfaBind.vue` — 4 步绑定流程
- 前端 `frontend-agent/src/composables/useHighRiskOtp.ts` — 高危弹窗 30 分钟超时
- 前端 `frontend-admin/src/api/mfa.ts` — 管理员视角 API
- 前端 `frontend-admin/src/views/MfaManage.vue` — MFA 管理表格(搜索/过滤/分页)
#### 高危操作守卫(阶段 1.3 task #19)
- 后端 `app/services/high_risk_guard.py` (291 行) — HighRiskGuard service 类
- 后端 `app/api/high_risk_routes.py` (327 行) — 演示端点 + 白名单查询
- 后端 `app/dependencies.py` — HIGH_RISK_OPERATIONS 5 类白名单 + require_high_risk_otp 依赖
- 5 类高危操作:改权限 / 改配置 / 导出数据 / 封号 / 新增账号或重置
### 🐛 修复 (Fixed)
- WS endpoint `missing argument 'request'` 错误(加 8 个回归测试)
- messages.id VARCHAR → UUID(migration 025,加 8 个兼容测试)
- wordfilter API 适配(1.0.6:Wordfilter 实例 + addWords + blacklisted)
- conftest SQLite ARRAY/JSONB 编译补丁(quiz.keywords / themes.palette)
- conftest autouse 业务表清理(feedback 事务隔离)
- h5_client 用 127.0.0.1 跳过企微 UA 检测
- test_conversation_grab wecom mock 默认 name 不覆盖 body.name
- Gitea push token 从 URL 清理(`http://workbuddy-claude@...`)
### 🔐 安全 (Security)
- 高危操作必须过 OTP 二次验证(管理员 30 分钟内)
- WS 推送端点签名保护(防 request: Request 加回去)
- nginx access_log 脱敏脚本(删 Authorization / Cookie)
- 5 鉴权漏洞已修(2026-06-14 评审清单)
### 📚 文档 (Documentation)
- `docs/E2E-CHECKLIST-v0.7.0.md` (176 行) — 35 项 E2E 验收清单
- `docs/DEPLOY-QUICK-v0.7.0.md` (252 行) — 一键部署操作包(分步+回滚+预计时间)
- `docs/DEPLOY-LOGIN-MIGRATION-v0.7.0.md` (220 行) — 部署手册
- `docs/NGINX-DOMAIN-ROUTING.md` (256 行) — nginx 域名分发
- `docs/USER-GUIDE-QRCODE-MFA.md` (165 行) — 用户手册
### 📈 测试 (Test)
- 新增 78 测试全过(扫码 13 + MFA 21 + 高危 28 + WS/UUID 16)
- 4 xfailed(端点路径不一致 pre-existing,已标 xfail)
- 修 5 处 pre-existing 失败(+27 测试):content_moderation / conversation_grab / feedback / h5_oauth / SQLite 编译
- 全量 pytest: 470 passed, 4 xfailed, 64 failed(pre-existing 设计问题)
### 📦 Commits(本次 session 5 个)
- `1255e95` docs: v0.7.0 一键部署操作包
- `c33abb6` fix(tests): h5_client 用 127.0.0.1 跳过企微 UA 检测
- `a9b97de` fix(tests): wordfilter API 适配 + SQLite ARRAY/JSONB 补丁 + 事务隔离
- `e96fbb2` docs: v0.7.0 E2E 验收清单
- `bf872da` feat(merge): 4 个 worktree 合入 main(扫码+MFA+高危+P0)
[0.7.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/compare/v0.6.0...v0.7.0
[0.5.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/releases/tag/v0.5.0
[0.4.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/releases/tag/v0.4.0
[0.3.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/releases/tag/v0.3.0
[0.2.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/releases/tag/v0.2.0
[0.1.0]: https://gitea.simon.local/simon/wecom_it_smart_desk/releases/tag/v0.1.0
-213
View File
@@ -1,213 +0,0 @@
# 贡献指南 (CONTRIBUTING)
**适用范围**: 企微 IT 智能服务台 (`wecom_it_smart_desk`)
**维护者**: 宋献(项目负责人)+ Claude(评审协作)+ workbuddy(自动化开发)
**最后更新**: 2026-06-14
---
## 📌 仓库入口
- **Gitea(公网 Funnel**: `https://ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk`
- **Gitea(内网 LAN**: `http://100.85.152.112:8418/simon/wecom_it_smart_desk`
- **Tailscale 私网**: `100.85.152.112:8418`
---
## 🌿 分支模型
| 分支 | 用途 | 保护规则 |
|---|---|---|
| `main` | 稳定可发布版本 | 🔒 禁止直推,需 PR + 1 reviewer |
| `develop` | 主开发分支 | 🟡 允许 push |
| `feature/*` | 新功能(从 develop 拉) | 🟢 自由 |
| `hotfix/*` | 紧急修复(从 main 拉) | 🟢 自由,合入需评审 |
| `release/*` | 发布准备 | 🟡 自由,合入 main 需评审 |
**主分支**: `main`(默认推送目标)
---
## 📝 Commit 规范
**格式** (Conventional Commits):
```
<type>(<scope>): <subject>
<body>
<footer>
```
**type 取值**:
| type | 用途 | 示例 |
|---|---|---|
| `feat` | 新功能 | `feat(messages): 撤回消息端点` |
| `fix` | Bug 修复 | `fix(h5): 修复参与者权限校验` |
| `refactor` | 重构(无新功能 / 无 Bug 修复) | `refactor(agents): 提取鉴权中间件` |
| `docs` | 文档变更 | `docs: 评审报告 workbuddy-2026-06-14` |
| `chore` | 构建/工具/依赖 | `chore: 强化 .gitignore` |
| `security` | 安全相关 | `security: P0 鉴权止血` |
| `perf` | 性能优化 | `perf(messages): 消息批量插入` |
| `test` | 测试相关 | `test: 加 mark_read 鉴权测试` |
**scope 取值**: 模块名,如 `agents` / `messages` / `h5` / `frontend-agent` / `nginx` / `workbuddy`
**subject**: 中文,不超过 50 字,**祈使句**,如 "修复 xx" 而非 "修复了 xx"
**body** (可选): 详细说明,**每行 ≤ 72 字**
**footer** (可选): 关联 Issue / workbuddy 任务编号,如:
```
Refs: #18
Refs: workbuddy-2026-06-14-任务-修遗留
```
**示例**:
```
security(ws): WS token 从 URL 改 header 鉴权
【workbuddy 推送 2026-06-14】
- ws.py 服务端: 优先 Authorization: Bearer header, query 降级
- ws.ts 前端: 待 workbuddy 改 Sec-WebSocket-Protocol 方案
- 详见 docs/评审报告/workbuddy-2026-06-14-P0安全.md
Refs: #18
Refs: workbuddy-2026-06-14-任务-修遗留
```
---
## 🔄 PR 流程
### 推送前自检清单
**所有 P0 修复推送前必须 4 件套自检**:
- [ ] **鉴权**: 新增/修改端点是否有 `Depends(get_current_agent)``_get_current_employee`?
- [ ] **依赖**: 改代码是否同步 `requirements.txt` / `package.json`?
- [ ] **alembic**: 数据库 schema 变化是否生成迁移脚本?
- [ ] **配置**: nginx / docker / conf 变化 plan 写了是否做完?
### PR 流程
1. **本地开发**
```bash
git checkout develop
git pull
git checkout -b feature/xxx
# 改代码
git add .
git commit -m "feat(xxx): ..."
git push origin feature/xxx
```
2. **开 PR**(走 Gitea Web 或 API)
- 标题 = commit subject
- 描述 = body 内容 + 关联评审报告 / workbuddy 任务
- Reviewer: `simon`(主) + 可选 workbuddy auto-review
3. **评审员评审**(Gitea UI)
- 🟢 **P0 鉴权 / 安全**: 必须 Claude 评审 + 通过
- 🟡 **功能 / 重构**: 至少 1 reviewer 通过
- 🟢 **docs / chore**: 自审即可
4. **合并**
- 评审通过 + status check 绿 → squash merge → 删 feature 分支
---
## 🔒 main 分支保护规则
由 Gitea API 配置,目前设定:
| 项 | 值 |
|---|---|
| 禁止直推 | ✅ |
| 需 PR | ✅ |
| Approvals 数 | 1 |
| Dismiss stale approvals | ✅ |
| 状态检查必须通过 | ✅(待配) |
| 管理员限制 | ✅(管理员也走 PR) |
**配分支保护**:
```bash
curl -X POST \
-H "Authorization: token <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"enable_push": false,
"enable_pull_request": true,
"required_approvals": 1,
"dismiss_stale_approvals": true,
"block_admin_merge": true
}' \
"https://ds923plus.tail58d872.ts.net/api/v1/repos/simon/wecom_it_smart_desk/branch_protections/main"
```
---
## 🤖 workbuddy 推送规则
workbuddy 自动化开发,推送必须满足:
1. **完整自检**: 鉴权 + 依赖 + alembic + 配置 4 件套
2. **评审报告**: 每次推送**生成** `docs/评审报告/workbuddy-{日期}-{主题}.md`
3. **workbuddy 记忆更新**: `.workbuddy/memory/{日期}-{主题}.md`
4. **5 项遗留**: 上一轮评审遗留的 5 项必须修完才能合入下一轮
5. **不叠加新功能**: 评审未消化前不推新功能(见 `docs/评审报告/` 历次教训)
**评审失败处理**:
- 评审标 🔴 P0 → 立即修,不接受反驳(除非评审员改判)
- 评审标 🟡 P1 → 列入遗留表(workbuddy 记忆 + 风险跟踪表)
- 评审标 🟢 P2 → 知识库积累,不强制修
---
## 🆘 紧急修复 (hotfix)
**场景**: 生产 P0 漏洞 / 数据丢失风险
**流程**:
1. 从 main 拉 `hotfix/xxx`
2. 改 + 测(用预生产环境)
3. PR → main(快通道,reviewer 优先 @ 宋献)
4. 评审通过 → 立即合并 + 部署
5. 同步 cherry-pick 回 develop
**禁止**:
- ❌ 跳过评审
- ❌ 推 main 直接部署
- ❌ 评审未通过就部署
---
## 📚 关联文档
- [`README.md`](README.md) — 项目总览(新人快速入门)
- [`docs/01-项目总览与部署手册.md`](docs/01-项目总览与部署手册.md) — 完整架构设计与部署详情
- [`docs/智能IT服务系统运维手册.md`](docs/智能IT服务系统运维手册.md) — 统一运维手册
- [`docs/索引.md`](docs/索引.md) — 文档目录索引(快速导航)
- [`CHANGELOG.md`](CHANGELOG.md) — 版本变更概览
- [`docs/archive-归档/`](docs/archive-归档/) — 历史版本详情
- [`.workbuddy/memory/`](.workbuddy/memory/) — workbuddy 任务记忆
---
## 📑 文档同步规则
**更新文档时需同步关联文档**
| 更新内容 | 需同步的文档 |
|---------|-------------|
| 新功能/重构 | README.md(进度)+ CHANGELOG.md + 相关 docs/*.md |
| 部署变更 | 智能IT服务系统运维手册.md + 01-项目总览与部署手册.md |
| 安全修复 | CHANGELOG.mdSecurity 章节)+ 评审报告 |
| API 变更 | README.mdAPI 概览)+ 01-项目总览与部署手册.md |
**文档目录规范**
- `docs/` 目录下按类型分子目录(deploy/, SOPs/, ADRs/, 评审报告/, archive/ 等)
- 根目录保留 README.md / CONTRIBUTING.md / CHANGELOG.mdGit 生态标准)
- 新增文档优先放在 `docs/`,避免根目录文件膨胀
-213
View File
@@ -1,213 +0,0 @@
# 企微智能IT支持服务台 (IT Smart Desk)
> **环境状态**: 预生产(独立主机,共享域名)→ 正式环境迁移 K8s
> **维护者**: 税友集团 IT支持组(宋献)
> **最后更新**: 2026-06-03
---
## 📖 阅读指南(按对象)
| 阅读对象 | 推荐文档 | 内容 |
|---------|---------|------|
| **新人/产品经理** | 本文档 + docs/ARCHITECTURE.md 前半部分 | 项目背景、功能清单、如何使用 |
| **开发人员** | docs/ARCHITECTURE.md + backend/app/ 代码 | 架构设计、API 接口、数据模型、开发规范 |
| **运维/部署人员** | 本文档「部署」章节 + scripts/deploy.sh | Docker 编排、Nginx 配置、环境变量 |
| **测试人员** | docs/ARCHITECTURE.md 「任务清单」 | 功能模块划分、待完善项 |
---
## 🎯 项目背景
税友集团内部 IT 支持渠道分散(企微群、电话、走访),缺乏统一 SLA 追踪。本项目构建一个 **AI + 人工坐席协作** 的智能服务台:
- **员工端**H5):企微 OAuth2 免登,AI 自动回复 + 人工兜底,支持「敲桌子」趣味呼叫
- **坐席端**(Web):三栏工作台,会话分配/抢单/协作/转接,实时 WebSocket 推送
- **AI 层**:接入 RAGFLOW/Dify 知识库,自动回复常见 IT 问题
**核心指标**:AI 自助解决率 55%(实际 1-5月已达 70.2%
---
## ✅ 当前实现进度
### 后端(FastAPI + PostgreSQL + Redis
- [x] 企微回调加解密(AES-CBC-256
- [x] 消息路由(VIP 识别、紧急度评分 1-5、标记检测)
- [x] WebSocket 实时推送(心跳、重连、定向广播)
- [x] 会话全生命周期(创建→分配→处理→结单→转接)
- [x] 坐席管理(登录、状态切换、在线列表)
- [x] H5 端 OAuth2 认证、审批链接、软件下载
- [x] 应急模式(系统故障时手动开启)
- [x] Alembic 数据库迁移(初始表结构)
- [ ] **AI 回复集成**(需接入 Dify,目前新会话直接进入队列)
- [ ] 自动化测试(pytest
### 坐席前端(Vue 3 + Element Plus
- [x] 登录页(用户ID + 姓名,无密码)
- [x] 三栏工作台(会话列表 + 对话区 + AI 助手面板)
- [x] 6 分区会话列表(待接单/我的/协作/其他坐席/AI处理/已结单)
- [x] 协作功能(摇人邀请、接受/拒绝)
- [x] WebSocket + 轮询双模式(自动降级)
- [ ] 操作步骤、风险提示、用户信息面板(需确认后端数据)
### 员工前端(Vue 3 + Vant
- [x] OAuth2 静默授权登录
- [x] 聊天界面(员工/坐席/AI/系统 4 种消息气泡)
- [x] 「敲桌子」呼叫坐席(7 种 SVG 动画)
- [x] AI 助手面板、审批流程链接、软件下载
- [ ] AI 回复展示(依赖后端 AI 集成)
### 部署
- [x] Docker Compose 4 容器编排(nginx + backend + postgres + redis
- [x] Nginx 反向代理(与数据平台共享域名,独立主机部署)
- [x] 部署脚本(build.sh + deploy.sh
- [ ] HTTPS 启用(nginx.conf 已预留模板)
- [ ] **预生产环境验证**(独立主机,路径路由到数据平台远程主机)
---
## 🚀 快速启动
### 前置条件
- Docker Desktop / Docker Compose
- 企微企业应用(需配置 Token、EncodingAESKey、CorpID
- 公钥上传至服务器(部署用)
### 本地开发
```bash
# 1. 复制环境变量
cp .env.example .env
# 编辑 .env 填入企微凭据和数据库密码
# 2. 启动数据库(仅 PostgreSQL
docker compose up -d postgres redis
# 3. 后端开发模式
cd backend
python -m venv venv && venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload
# 4. 前端开发模式(另开终端)
cd frontend-agent
npm install && npm run dev
```
### 预生产部署
> **注意**:预生产环境中,智能咨询系统与数据平台在不同主机上。部署前需将 `nginx/nginx.conf` 中 `DATAQUERY_HOST` 替换为数据平台实际 IP。
```bash
# 1. 修改 nginx 反代地址
# 编辑 nginx/nginx.conf,将 DATAQUERY_HOST 改为数据平台主机 IP
# 2. 使用部署脚本
bash scripts/deploy.sh deploy
# 3. 或手动
docker compose up -d --build
```
> 正式环境将迁移到 K8s 集群,届时部署方式另行调整。
---
## 📂 项目结构
```
wecom_it_smart_desk/
├── backend/ # FastAPI 后端
│ ├── app/
│ │ ├── api/ # 8 个路由模块
│ │ ├── models/ # 9 个数据模型
│ │ ├── services/ # 核心服务(消息路由、会话管理、企微API)
│ │ ├── schemas/ # Pydantic 请求/响应 Schema
│ │ └── utils/ # 加解密、Token管理、WebSocket
│ └── alembic/ # 数据库迁移
├── frontend-agent/ # 坐席工作台(Vue 3 + Element Plus
├── frontend-h5/ # 员工端(Vue 3 + Vant
├── nginx/ # Nginx 反向代理配置
├── scripts/ # 构建和部署脚本
├── docs/ # 项目文档(架构/PRD/测试报告等)
└── docker-compose.yml # 容器编排
```
---
## 🔧 核心配置项(.env
| 变量 | 说明 | 示例 |
|------|------|------|
| `WECOM_CORPID` | 企微企业ID | `ww...` |
| `WECOM_AGENT_TOKEN` | 企微应用 Token | `your_token` |
| `WECOM_AGENT_ENCODING_AES_KEY` | 企微消息加解密密钥 | `32位Base64` |
| `DATABASE_URL` | 数据库连接串 | `postgresql+asyncpg://...` |
| `REDIS_URL` | Redis 连接串 | `redis://...` |
| `BACKEND_PORT` | 后端端口(容器内需 8000) | `8000` |
---
## 📊 API 端点概览
| 模块 | 端点前缀 | 核心功能 |
|------|---------|---------|
| 坐席管理 | `/api/v1/agents` | 登录、状态切换、在线列表 |
| 会话管理 | `/api/v1/conversations` | 列表、详情、分配、结单、转接 |
| 消息管理 | `/api/v1/messages` | 消息列表、发送、轮询 |
| 企微回调 | `/api/v1/wecom` | GET 验证、POST 接收消息 |
| H5 员工端 | `/api/v1/h5` | OAuth2、消息、举手、审批 |
| 快速回复 | `/api/v1/quick-replies` | 模板 CRUD |
| 系统 | `/api/v1/system` | 应急模式开关 |
> 详细请求/响应格式见 `docs/ARCHITECTURE.md` 或运行后访问 `/docs`Swagger UI
---
## 🐛 已知问题 / 待完善
1. **AI 回复未集成**:目前新会话直接进入 `queued` 状态,需接入 Dify 工作流
2. **无自动化测试**:后端 pytest、前端 Vitest 均未配置
3. **Alembic 迁移不完整**:仅初始迁移,后续模型变更需手动管理
4. **HTTPS 未启用**nginx.conf 有模板但未配置证书
5. **VIP 缓存未实现**`message_router.py` 中 Redis 缓存被注释
---
## 📝 相关文档
- **docs/01-项目总览与部署手册.md**:完整项目背景、架构设计、部署运维(本文档的详细版本)
- **docs/智能IT服务系统运维手册.md**:统一运维文档,涵盖部署/监控/故障处理
- **scripts/deploy.sh**:部署脚本详细说明(5 种运行模式)
- **docs/archive/**:历史版本文档归档
---
## 📞 联系
- **项目负责人**:宋献 — 税友集团 IT支持组
- **企业微信**:通过内部企微联系
- **Issue 反馈**:在项目目录创建任务文档或联系开发组
---
*最后更新:2026-06-03 - 合并文档,反映当前实际完成进度*
---
## 🏛️ 仓库与治理
- **Gitea 仓库(公网 Funnel**: `https://ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk`
- **Gitea 内网地址(LAN 加速)**: `http://100.85.152.112:8418/simon/wecom_it_smart_desk`
- **贡献指南**: [`CONTRIBUTING.md`](CONTRIBUTING.md) — 分支模型 + Commit 规范 + PR 流程
- **评审报告**: [`docs/评审报告/`](docs/评审报告/) — 历次 workbuddy 推送评审
- **风险跟踪表**: [`docs/风险跟踪表.md`](docs/风险跟踪表.md) — 22 项审计追踪
- **workbuddy 记忆**: [`.workbuddy/memory/`](.workbuddy/memory/) — workbuddy 启动读这里接任务
### 评审与提交约定
- 🔴 **所有 P0 鉴权修复必须走评审**`docs/评审报告/` 留档,含 workbuddy 推送)
- 🟡 **端点变更需 `Depends(get_current_agent)` 或 `_get_current_employee` 鉴权依赖**
- 🟡 **数据库 schema 变化必须 alembic 迁移**(无手动 ALTER
- 🟢 **workbuddy 推送前自检**: 鉴权 + 依赖 + alembic + 配置 4 件套
- 🟢 **任何部署包 / SSL 私钥 / 推送 token 不入仓**(见 `.gitignore`
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
from neo4j import GraphDatabase
uri = "bolt://neo4j:7687"
user = "neo4j"
password = "Wecom@2026"
driver = GraphDatabase.driver(uri, auth=(user, password))
with driver.session() as session:
# 清除旧数据
session.run("MATCH (n) DETACH DELETE n")
# 添加测试数据(带完整属性)
session.run("""
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001", created_at: datetime(), updated_at: datetime()})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加", uuid: "action-001", created_at: datetime()})
CREATE (i1)-[:HAS_ACTION]->(a1)
""")
session.run("""
CREATE (i2:Issue {name: "网络连不上", category: "网络问题", uuid: "issue-002", created_at: datetime(), updated_at: datetime()})
CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置", uuid: "action-002", created_at: datetime()})
CREATE (i2)-[:HAS_ACTION]->(a2)
""")
session.run("""
CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题", uuid: "issue-003", created_at: datetime(), updated_at: datetime()})
CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录", uuid: "action-003", created_at: datetime()})
CREATE (i3)-[:HAS_ACTION]->(a3)
""")
# 验证
result = session.run("MATCH (i:Issue)-[:HAS_ACTION]->(a:Action) RETURN i.uuid as issue_uuid, i.name as issue_name, a.uuid as action_uuid, a.name as action_name")
for record in result:
print(f"Issue: {record['issue_uuid']} - {record['issue_name']} -> Action: {record['action_uuid']} - {record['action_name']}")
driver.close()
print("Done!")
-4
View File
@@ -1,4 +0,0 @@
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001"})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"})
CREATE (i1)-[:HAS_ACTION]->(a1)
RETURN "done"
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
from neo4j import GraphDatabase
uri = "bolt://neo4j:7687"
user = "neo4j"
password = "Wecom@2026"
driver = GraphDatabase.driver(uri, auth=(user, password))
with driver.session() as session:
# 清除旧数据
session.run("MATCH (n) DETACH DELETE n")
# 添加测试数据
session.run("""
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001"})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"})
CREATE (i1)-[:HAS_ACTION]->(a1)
""")
session.run("""
CREATE (i2:Issue {name: "网络连不上", category: "网络问题", uuid: "issue-002"})
CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置"})
CREATE (i2)-[:HAS_ACTION]->(a2)
""")
session.run("""
CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题", uuid: "issue-003"})
CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录"})
CREATE (i3)-[:HAS_ACTION]->(a3)
""")
# 验证
result = session.run("MATCH (i:Issue) RETURN i.uuid, i.name")
for record in result:
print(f"uuid: {record['i.uuid']}, name: {record['i.name']}")
driver.close()
print("Done!")
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-83
View File
@@ -1,83 +0,0 @@
"""Append remaining bytes to partially uploaded file."""
import base64
import hashlib
import subprocess
import sys
PYTHON = r"C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe"
JMS = r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py"
LOCAL_FILE = r"D:\资料\03-项目开发\wecom_it_smart_desk\deploy_agent_v8.tar.gz"
REMOTE_FILE = "/tmp/deploy_agent_v8.tar.gz"
CHUNK_SIZE = 12 * 1024 # 12KB
def run_jms(*args, timeout=30):
cmd = [PYTHON, JMS] + list(args)
result = subprocess.run(cmd, capture_output=True, timeout=timeout)
out = result.stdout.decode('utf-8', errors='replace') if result.stdout else ''
err = result.stderr.decode('utf-8', errors='replace') if result.stderr else ''
return out + err
def main():
with open(LOCAL_FILE, "rb") as f:
data = f.read()
local_md5 = hashlib.md5(data).hexdigest()
local_size = len(data)
# Check current remote file size
result = run_jms("exec", "-c", f"stat -c %s {REMOTE_FILE}", "--cmd-timeout", "15")
# Parse the number from output
remote_size = 0
for line in result.split('\n'):
line = line.strip()
if line.isdigit():
remote_size = int(line)
break
print(f"Local size: {local_size}")
print(f"Remote size: {remote_size}")
if remote_size >= local_size:
# File already complete, just verify MD5
print("File already complete, verifying MD5...")
result = run_jms("exec", "-c", f"md5sum {REMOTE_FILE}", "--cmd-timeout", "15")
print(f"Remote MD5: {result.strip()}")
print(f"Local MD5: {local_md5}")
if local_md5 in result:
print("\n✅ MD5 verified!")
else:
print("\n❌ MD5 mismatch, need to re-upload!")
return
# Upload remaining bytes
remaining = data[remote_size:]
total_chunks = (len(remaining) + CHUNK_SIZE - 1) // CHUNK_SIZE
print(f"Remaining: {len(remaining)} bytes ({total_chunks} chunks)")
for i in range(total_chunks):
chunk = remaining[i * CHUNK_SIZE : (i + 1) * CHUNK_SIZE]
b64 = base64.b64encode(chunk).decode("ascii")
cmd = f'echo -n "{b64}" | base64 -d >> {REMOTE_FILE}'
run_jms("exec", "-c", cmd, "--cmd-timeout", "15")
print(f" Appended chunk {i + 1}/{total_chunks}")
# Verify
print("\nVerifying MD5...")
result = run_jms("exec", "-c", f"md5sum {REMOTE_FILE}", "--cmd-timeout", "15")
print(f"Remote MD5: {result.strip()}")
print(f"Local MD5: {local_md5}")
if local_md5 in result:
print("\n✅ MD5 verified - upload complete!")
else:
print("\n❌ MD5 mismatch!")
# Check final size
result = run_jms("exec", "-c", f"stat -c %s {REMOTE_FILE}", "--cmd-timeout", "15")
for line in result.split('\n'):
if line.strip().isdigit():
print(f"Final remote size: {line.strip()}")
break
sys.exit(1)
if __name__ == "__main__":
main()
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env pwsh
# 构建 P2/P3 前端
$ErrorActionPreference = "Stop"
# 设置 npm 镜像
$env:NPM_CONFIG_REGISTRY = "https://registry.npmmirror.com"
# 构建 frontend-agent
Write-Host "Building frontend-agent..."
Set-Location "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent"
& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" install
if ($LASTEXITCODE -ne 0) { throw "npm install failed" }
& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" run build
if ($LASTEXITCODE -ne 0) { throw "npm run build failed" }
Write-Host "frontend-agent built successfully!"
# 构建 frontend-h5
Write-Host "Building frontend-h5..."
Set-Location "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-h5"
& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" install
if ($LASTEXITCODE -ne 0) { throw "npm install failed" }
& "C:\Users\simon\.workbuddy\binaries\node\versions\22.22.2\node.exe" "C:\Users\simon\AppData\Roaming\npm\node_modules\npm\bin\npm-cli.js" run build
if ($LASTEXITCODE -ne 0) { throw "npm run build failed" }
Write-Host "frontend-h5 built successfully!"
Write-Host "All done!"
BIN
View File
Binary file not shown.
-26
View File
@@ -1,26 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
keyword = "打印机驱动安装"
# 直接测试 Cypher 查询
try:
data = await neo4j_client.execute_read_query(
"MATCH (i:Issue) WHERE i.name CONTAINS $keyword RETURN i.uuid AS uuid, i.name AS name, i.category AS category LIMIT 5",
{"keyword": keyword}
)
print(f"Data type: {type(data)}")
print(f"Data: {data}")
if data:
print(f"First record uuid: {data[0].get('uuid')}")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
asyncio.run(test())
-27
View File
@@ -1,27 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.graph_query_service import get_graph_query_service
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
graph_service = await get_graph_query_service(neo4j_client)
# 测试关键词提取
question = "打印机驱动安装"
keywords = graph_service._extract_keywords(question)
print(f"Question: {question}")
print(f"Keywords: {keywords}")
# 测试每个关键词的查询
for kw in keywords:
print(f"\n=== Querying keyword: {kw} ===")
try:
result = await graph_service._query_by_keyword(kw)
print(f"Result: {result}")
except Exception as e:
print(f"Error: {e}")
asyncio.run(test())
-27
View File
@@ -1,27 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.graph_query_service import get_graph_query_service
from app.services.neo4j_client import get_neo4j_client
import logging
logging.basicConfig(level=logging.DEBUG)
neo4j_client = await get_neo4j_client()
graph_service = await get_graph_query_service(neo4j_client)
keyword = "打印机驱动安装"
print(f"Querying keyword: {keyword}")
# 直接调用 Neo4j 查询
try:
data = await neo4j_client.execute_read_query(
"""MATCH (i:Issue) WHERE i.name CONTAINS $keyword RETURN i.uuid AS uuid, i.name AS name, i.category AS category LIMIT 5""",
{"keyword": keyword}
)
print(f"Raw data: {data}")
except Exception as e:
print(f"Error: {e}")
asyncio.run(test())
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env python3
"""打包 P2/P3 部署文件"""
import os
import tarfile
# 待上传的后端文件
backend_files = [
'backend/app/utils/token_counter.py',
'backend/app/services/automation/context_compressor.py',
'backend/app/services/automation/snapshot_service.py',
'backend/app/services/automation/correction_service.py',
'backend/alembic/versions/049_add_p2_p3_tables.py',
'backend/tests/test_p2_p3.py',
# 修改的文件
'backend/app/models/automation.py',
'backend/app/constants.py',
'backend/app/config.py',
'backend/app/schemas/automation.py',
'backend/app/api/automation.py',
]
# 创建临时打包
with tarfile.open('backend-p2-p3-deploy.tar.gz', 'w:gz') as tar:
for f in backend_files:
if os.path.exists(f):
tar.add(f, arcname=f)
print(f'Added: {f}')
else:
print(f'NOT FOUND: {f}')
print('Done!')
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env python3
"""重新打包前端部署文件 - 使用正确目录结构"""
import os
import tarfile
os.chdir('D:/资料/03-项目开发/wecom_it_smart_desk')
# 重新打包 frontend-agent
print("Repacking frontend-agent...")
if os.path.exists('frontend-agent/dist'):
with tarfile.open('frontend-agent-dist-v2.tar.gz', 'w:gz') as tar:
tar.add('frontend-agent/dist', arcname='dist')
print('Created: frontend-agent-dist-v2.tar.gz')
# 重新打包 frontend-h5
print("Repacking frontend-h5...")
if os.path.exists('frontend-h5/dist'):
with tarfile.open('frontend-h5-dist-v2.tar.gz', 'w:gz') as tar:
tar.add('frontend-h5/dist', arcname='dist')
print('Created: frontend-h5-dist-v2.tar.gz')
print('Done!')
-1
View File
@@ -1 +0,0 @@
IyEvYmluL2Jhc2gKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQojIC9pdGRlc2svIDUwMCDplJnor6/or4rmlq3ohJrmnKwKIyDlnKjnlJ/kuqfmnI3liqHlmaggMTAuODAuMC4xMzYg5LiK6LeRKFNTSCDnmbvlvZXlkI4pOgojICAgY2QgL29wdC93ZWNvbS1pdC1kZXNrCiMgICBiYXNoIGRpYWdub3NlLTUwMC5zaCA+IC90bXAvZGlhZy5sb2cgMj4mMQojICAgY2F0IC90bXAvZGlhZy5sb2cKIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQoKZWNobyAiPT09PT09PT09PSAxLiDlrrnlmajnirbmgIEgPT09PT09PT09PSIKZG9ja2VyIGNvbXBvc2UgcHMKCmVjaG8gIiIKZWNobyAiPT09PT09PT09PSAyLiAvb3B0L3dlY29tLWl0LWRlc2sg55uu5b2V57uT5p6EID09PT09PT09PT0iCmxzIC1sYSAvb3B0L3dlY29tLWl0LWRlc2svIDI+JjEgfCBoZWFkIC0yMAplY2hvICItLS0gZnJvbnRlbmQtaDUvZGlzdCAtLS0iCmxzIC1sYSAvb3B0L3dlY29tLWl0LWRlc2svZnJvbnRlbmQtaDUvZGlzdC8gMj4mMSB8IGhlYWQgLTEwCmVjaG8gIi0tLSBmcm9udGVuZC1oNS9kaXN0L2Fzc2V0cyAtLS0iCmxzIC1sYSAvb3B0L3dlY29tLWl0LWRlc2svZnJvbnRlbmQtaDUvZGlzdC9hc3NldHMvIDI+JjEgfCBoZWFkIC0xMAplY2hvICItLS0gZnJvbnRlbmQtYWdlbnQvZGlzdC9hc3NldHMgLS0tIgpscyAtbGEgL29wdC93ZWNvbS1pdC1kZXNrL2Zyb250ZW5kLWFnZW50L2Rpc3QvYXNzZXRzLyAyPiYxIHwgaGVhZCAtMTAKZWNobyAiLS0tIGZyb250ZW5kLXBvcnRhbC9kaXN0L2Fzc2V0cyAtLS0iCmxzIC1sYSAvb3B0L3dlY29tLWl0LWRlc2svZnJvbnRlbmQtcG9ydGFsL2Rpc3QvYXNzZXRzLyAyPiYxIHwgaGVhZCAtMTAKZWNobyAiLS0tIGZyb250ZW5kLWFkbWluL2Rpc3QvYXNzZXRzIC0tLSIKbHMgLWxhIC9vcHQvd2Vjb20taXQtZGVzay9mcm9udGVuZC1hZG1pbi9kaXN0L2Fzc2V0cy8gMj4mMSB8IGhlYWQgLTEwCgplY2hvICIiCmVjaG8gIj09PT09PT09PT0gMy4gbmdpbngg5a655Zmo5YaF5paH5Lu25qOA5p+lID09PT09PT09PT0iCmRvY2tlciBjb21wb3NlIGV4ZWMgbmdpbnggbHMgLWxhIC91c3Ivc2hhcmUvbmdpbngvaHRtbC8gMj4mMSB8IGhlYWQgLTIwCmVjaG8gIi0tLSAvdXNyL3NoYXJlL25naW54L2h0bWwvaXRkZXNrIC0tLSIKZG9ja2VyIGNvbXBvc2UgZXhlYyBuZ2lueCBscyAtbGEgL3Vzci9zaGFyZS9uZ2lueC9odG1sL2l0ZGVzay8gMj4mMSB8IGhlYWQgLTEwCmVjaG8gIi0tLSAvdXNyL3NoYXJlL25naW54L2h0bWwvaXRkZXNrL2Fzc2V0cyAtLS0iCmRvY2tlciBjb21wb3NlIGV4ZWMgbmdpbnggbHMgLWxhIC91c3Ivc2hhcmUvbmdpbngvaHRtbC9pdGRlc2svYXNzZXRzLyAyPiYxIHwgaGVhZCAtMTAKZWNobyAiLS0tIC91c3Ivc2hhcmUvbmdpbngvc3NsLyAtLS0iCmRvY2tlciBjb21wb3NlIGV4ZWMgbmdpbnggbHMgLWxhIC9ldGMvbmdpbngvc3NsLyAyPiYxIHwgaGVhZCAtMTAKCmVjaG8gIiIKZWNobyAiPT09PT09PT09PSA0LiBuZ2lueCDphY3nva7lrp7pmYXnlJ/mlYjniYjmnKwo5aS06YOoIDUwIOihjCk9PT09PT09PT09Igpkb2NrZXIgY29tcG9zZSBleGVjIG5naW54IGNhdCAvZXRjL25naW54L25naW54LmNvbmYgMj4mMSB8IGhlYWQgLTUwCgplY2hvICIiCmVjaG8gIj09PT09PT09PT0gNS4gbmdpbngg5a655Zmo56uv5Y+j55uR5ZCsID09PT09PT09PT0iCmRvY2tlciBjb21wb3NlIGV4ZWMgbmdpbnggbmV0c3RhdCAtdGxucCAyPiYxIHwgaGVhZCAtMTAKZWNobyAiKOayoSBuZXRzdGF0IOeUqCBzczopIgpkb2NrZXIgY29tcG9zZSBleGVjIG5naW54IHNzIC10bG5wIDI+JjEgfCBoZWFkIC0xMAoKZWNobyAiIgplY2hvICI9PT09PT09PT09IDYuIOebtOaOpSBjdXJsIOa1i+ivleWQhOi3r+W+hCA9PT09PT09PT09IgplY2hvICItLS0gL2l0ZGVzay8gKOWuueWZqOWGhSkgLS0tIgpkb2NrZXIgY29tcG9zZSBleGVjIG5naW54IGN1cmwgLWtzSSBodHRwczovL2xvY2FsaG9zdC9pdGRlc2svIDI+JjEgfCBoZWFkIC0yMAplY2hvICItLS0gL2l0ZGVzay8gKOWuueWZqOWkluS4u+acuiA0NDMpIC0tLSIKY3VybCAta3NJIGh0dHBzOi8vbG9jYWxob3N0OjQ0My9pdGRlc2svIDI+JjEgfCBoZWFkIC0yMAplY2hvICItLS0gL2l0cG9ydGFsLyAtLS0iCmN1cmwgLWtzSSBodHRwczovL2xvY2FsaG9zdDo0NDMvaXRwb3J0YWwvIDI+JjEgfCBoZWFkIC0yMAplY2hvICItLS0gL2l0ZGVzay9hc3NldHMvICjmjqIgNDA0KSAtLS0iCmN1cmwgLWtzSSBodHRwczovL2xvY2FsaG9zdDo0NDMvaXRkZXNrL2Fzc2V0cy8gMj4mMSB8IGhlYWQgLTIwCgplY2hvICIiCmVjaG8gIj09PT09PT09PT0gNy4g5Li75py65a6e6ZmFIFVSTCDln5/lkI0gPT09PT09PT09PSIKY3VybCAta3NJIGh0dHBzOi8vaXRzdXBwb3J0LnNlcnZ5b3UuY29tLmNuL2l0ZGVzay8gMj4mMSB8IGhlYWQgLTIwCmVjaG8gIi0tLSIKY3VybCAta3NJIGh0dHBzOi8vaXRzdXBwb3J0LnNlcnZ5b3UuY29tLmNuL2l0cG9ydGFsLyAyPiYxIHwgaGVhZCAtMjAKZWNobyAiLS0tIgpjdXJsIC1rc0kgaHR0cHM6Ly9pdHN1cHBvcnQuc2VydnlvdS5jb20uY24vaXRhZ2VudC8gMj4mMSB8IGhlYWQgLTIwCmVjaG8gIi0tLSIKY3VybCAta3NJIGh0dHBzOi8vaXRzdXBwb3J0LnNlcnZ5b3UuY29tLmNuL2l0YWRtaW4vIDI+JjEgfCBoZWFkIC0yMAoKZWNobyAiIgplY2hvICI9PT09PT09PT09IDguIG5naW54IGFjY2VzcyBsb2cg5pyA6L+RIDMwIOihjCjmib4gNTAwIOivt+axgik9PT09PT09PT09Igpkb2NrZXIgY29tcG9zZSBleGVjIG5naW54IHRhaWwgLTMwIC92YXIvbG9nL25naW54L2FjY2Vzcy5sb2cgMj4mMQplY2hvICIiCmVjaG8gIj09PT09PT09PT0gOS4gbmdpbnggZXJyb3IgbG9nIOacgOi/kSAzMCDooYwgPT09PT09PT09PSIKZG9ja2VyIGNvbXBvc2UgZXhlYyBuZ2lueCB0YWlsIC0zMCAvdmFyL2xvZy9uZ2lueC9lcnJvci5sb2cgMj4mMQoKZWNobyAiIgplY2hvICI9PT09PT09PT09IDEwLiBiYWNrZW5kIOWuueWZqOWBpeW6tyA9PT09PT09PT09Igpkb2NrZXIgY29tcG9zZSBwcyBiYWNrZW5kCmVjaG8gIi0tLSBiYWNrZW5kIGhlYWx0aCBlbmRwb2ludCAtLS0iCmRvY2tlciBjb21wb3NlIGV4ZWMgYmFja2VuZCBjdXJsIC1rcyBodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL2hlYWx0aCAyPiYxIHwgaGVhZCAtNQoKZWNobyAiIgplY2hvICI9PT09PT09PT09IDExLiDnnIvkuIDkuIvlkI7nq6/orr/pl64gL2FwaS9oNS9tZSAoSDUg5ZCv5Yqo5pe25Lya6LCDKT09PT09PT09PT0iCmVjaG8gIi0tLSAvYXBpL2g1L21lIOaXoCB0b2tlbiAtLS0iCmN1cmwgLWtzIC1pIC1YIEdFVCBodHRwczovL2l0c3VwcG9ydC5zZXJ2eW91LmNvbS5jbi9hcGkvaDUvbWUgMj4mMSB8IGhlYWQgLTEwCg==
-84
View File
@@ -1,84 +0,0 @@
#!/bin/bash
# =============================================================================
# /itdesk/ 500 错误诊断脚本
# 在生产服务器 10.90.5.110 上跑(PuTTY 登录后):
# cd /opt/wecom-it-desk
# bash diagnose-500.sh > /tmp/diag.log 2>&1
# cat /tmp/diag.log
# =============================================================================
echo "========== 1. 容器状态 =========="
docker compose ps
echo ""
echo "========== 2. /opt/wecom-it-desk 目录结构 =========="
ls -la /opt/wecom-it-desk/ 2>&1 | head -20
echo "--- frontend-h5/dist ---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/ 2>&1 | head -10
echo "--- frontend-h5/dist/assets ---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/assets/ 2>&1 | head -10
echo "--- frontend-agent/dist/assets ---"
ls -la /opt/wecom-it-desk/frontend-agent/dist/assets/ 2>&1 | head -10
echo "--- frontend-portal/dist/assets ---"
ls -la /opt/wecom-it-desk/frontend-portal/dist/assets/ 2>&1 | head -10
echo "--- frontend-admin/dist/assets ---"
ls -la /opt/wecom-it-desk/frontend-admin/dist/assets/ 2>&1 | head -10
echo ""
echo "========== 3. nginx 容器内文件检查 =========="
docker compose exec nginx ls -la /usr/share/nginx/html/ 2>&1 | head -20
echo "--- /usr/share/nginx/html/itdesk ---"
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/ 2>&1 | head -10
echo "--- /usr/share/nginx/html/itdesk/assets ---"
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/assets/ 2>&1 | head -10
echo "--- /usr/share/nginx/ssl/ ---"
docker compose exec nginx ls -la /etc/nginx/ssl/ 2>&1 | head -10
echo ""
echo "========== 4. nginx 配置实际生效版本(头部 50 行)=========="
docker compose exec nginx cat /etc/nginx/nginx.conf 2>&1 | head -50
echo ""
echo "========== 5. nginx 容器端口监听 =========="
docker compose exec nginx netstat -tlnp 2>&1 | head -10
echo "(没 netstat 用 ss:)"
docker compose exec nginx ss -tlnp 2>&1 | head -10
echo ""
echo "========== 6. 直接 curl 测试各路径 =========="
echo "--- /itdesk/ (容器内) ---"
docker compose exec nginx curl -ksI https://localhost/itdesk/ 2>&1 | head -20
echo "--- /itdesk/ (容器外主机 443) ---"
curl -ksI https://localhost:443/itdesk/ 2>&1 | head -20
echo "--- /itportal/ ---"
curl -ksI https://localhost:443/itportal/ 2>&1 | head -20
echo "--- /itdesk/assets/ (探 404) ---"
curl -ksI https://localhost:443/itdesk/assets/ 2>&1 | head -20
echo ""
echo "========== 7. 主机实际 URL 域名 =========="
curl -ksI https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -20
echo "---"
curl -ksI https://itsupport.servyou.com.cn/itportal/ 2>&1 | head -20
echo "---"
curl -ksI https://itsupport.servyou.com.cn/itagent/ 2>&1 | head -20
echo "---"
curl -ksI https://itsupport.servyou.com.cn/itadmin/ 2>&1 | head -20
echo ""
echo "========== 8. nginx access log 最近 30 行(找 500 请求)=========="
docker compose exec nginx tail -30 /var/log/nginx/access.log 2>&1
echo ""
echo "========== 9. nginx error log 最近 30 行 =========="
docker compose exec nginx tail -30 /var/log/nginx/error.log 2>&1
echo ""
echo "========== 10. backend 容器健康 =========="
docker compose ps backend
echo "--- backend health endpoint ---"
docker compose exec backend curl -ks http://localhost:8000/api/health 2>&1 | head -5
echo ""
echo "========== 11. 看一下后端访问 /api/h5/me (H5 启动时会调)=========="
echo "--- /api/h5/me 无 token ---"
curl -ks -i -X GET https://itsupport.servyou.com.cn/api/h5/me 2>&1 | head -10
-1
View File
@@ -1 +0,0 @@
IyEvYmluL2Jhc2gKc2V0ICtlICAgIyBjb2xsZWN0IGV2ZXJ5dGhpbmcsIGRvbid0IGJhaWwKCmVjaG8gJyMjIyMjIyMjIyMjIyBTVEVQIDE6IExvY2F0ZSBwcm9qZWN0IGRpcmVjdG9yeSAjIyMjIyMjIyMjIyMnCmNkIC9vcHQvd2Vjb20taXQtZGVzayAyPiYxCmVjaG8gIkN1cnJlbnQgZGlyOiAkKHB3ZCkiCmxzIC1sYSBkb2NrZXItY29tcG9zZS55bWwgMj4mMQplY2hvICcnCgplY2hvICcjIyMjIyMjIyMjIyMgU1RFUCAyOiBEaWFnbm9zZSAoUkVBRC1PTkxZKSAjIyMjIyMjIyMjIyMnCmVjaG8gJy0tLSBBbGwgd2Vjb21faXRfIGNvbnRhaW5lcnMgLS0tJwpkb2NrZXIgcHMgLWEgLS1mb3JtYXQgInRhYmxlIHt7Lk5hbWVzfX1cdHt7LlN0YXR1c319IiB8IGdyZXAgLUUgIndlY29tX2l0X3xOQU1FUyIKZWNobyAnJwplY2hvICctLS0gRGlzayBzcGFjZSAtLS0nCmRmIC1oIC9vcHQgMj4mMQplY2hvICcnCmVjaG8gJy0tLSBiYWNrZW5kIGxhc3QgNjAgbG9nIGxpbmVzIC0tLScKZG9ja2VyIGxvZ3Mgd2Vjb21faXRfYmFja2VuZCAtLXRhaWwgNjAgMj4mMQplY2hvICcnCmVjaG8gJy0tLSBiYWNrZW5kIGludGVybmFsIGhlYWx0aCBjaGVjayAtLS0nCmRvY2tlciBleGVjIHdlY29tX2l0X2JhY2tlbmQgY3VybCAtcyAtbyAtIC13ICJcbkhUVFBfQ09ERTogJXtodHRwX2NvZGV9XG4iIC0tbWF4LXRpbWUgNSBodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoIDI+JjEKZWNobyAnJwoKZWNobyAnIyMjIyMjIyMjIyMjIFNURVAgMzogUmVzdGFydCBmcm9tIGNvcnJlY3QgZGlyZWN0b3J5ICMjIyMjIyMjIyMjIycKY2QgL29wdC93ZWNvbS1pdC1kZXNrCmRvY2tlciBjb21wb3NlIHVwIC1kIDI+JjEKZWNobyAnJwplY2hvICdXYWl0aW5nIDE1cyBmb3Igc2VydmljZXMgdG8gc3RhYmlsaXplLi4uJwpzbGVlcCAxNQplY2hvICcnCmVjaG8gJy0tLSBDb250YWluZXJzIGFmdGVyIHJlc3RhcnQgLS0tJwpkb2NrZXIgcHMgLWEgLS1mb3JtYXQgInRhYmxlIHt7Lk5hbWVzfX1cdHt7LlN0YXR1c319IiB8IGdyZXAgLUUgIndlY29tX2l0X3xOQU1FUyIKZWNobyAnJwoKZWNobyAnIyMjIyMjIyMjIyMjIFNURVAgNDogRW5kLXRvLWVuZCB2ZXJpZmljYXRpb24gIyMjIyMjIyMjIyMjJwplY2hvICctLS0gYmFja2VuZCAvaGVhbHRoIC0tLScKY3VybCAtcyAtbyAtIC13ICJcbkhUVFBfQ09ERTogJXtodHRwX2NvZGV9XG4iIC0tbWF4LXRpbWUgNSBodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoCmVjaG8gJycKZWNobyAnLS0tIG5naW54IHJvdXRlcyAoZXhwZWN0IDIwMC8zMDEvMzAyKSAtLS0nCmZvciBwYXRoIGluIC8gL2l0YWdlbnQvIC9pdGg1LyAvaXRhZG1pbi87IGRvCiAgY29kZT0kKGN1cmwgLXMgLW8gL2Rldi9udWxsIC13ICIle2h0dHBfY29kZX0iIC0tbWF4LXRpbWUgNSAiaHR0cDovL2xvY2FsaG9zdCR7cGF0aH0iKQogIGVjaG8gIiAgJHBhdGggLT4gSFRUUCAkY29kZSIKZG9uZQplY2hvICcnCmVjaG8gJyMjIyMjIyMjIyMjIyBET05FICMjIyMjIyMjIyMjIycKZWNobyAnUGFzdGUgQUxMIG91dHB1dCBhYm92ZSBiYWNrIHRvIENsYXVkZSBmb3IgZGlhZ25vc2lzJwo=
-46
View File
@@ -1,46 +0,0 @@
#!/bin/bash
set +e # collect everything, don't bail
echo '############ STEP 1: Locate project directory ############'
cd /opt/wecom-it-desk 2>&1
echo "Current dir: $(pwd)"
ls -la docker-compose.yml 2>&1
echo ''
echo '############ STEP 2: Diagnose (READ-ONLY) ############'
echo '--- All wecom_it_ containers ---'
docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep -E "wecom_it_|NAMES"
echo ''
echo '--- Disk space ---'
df -h /opt 2>&1
echo ''
echo '--- backend last 60 log lines ---'
docker logs wecom_it_backend --tail 60 2>&1
echo ''
echo '--- backend internal health check ---'
docker exec wecom_it_backend curl -s -o - -w "\nHTTP_CODE: %{http_code}\n" --max-time 5 http://localhost:8000/health 2>&1
echo ''
echo '############ STEP 3: Restart from correct directory ############'
cd /opt/wecom-it-desk
docker compose up -d 2>&1
echo ''
echo 'Waiting 15s for services to stabilize...'
sleep 15
echo ''
echo '--- Containers after restart ---'
docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep -E "wecom_it_|NAMES"
echo ''
echo '############ STEP 4: End-to-end verification ############'
echo '--- backend /health ---'
curl -s -o - -w "\nHTTP_CODE: %{http_code}\n" --max-time 5 http://localhost:8000/health
echo ''
echo '--- nginx routes (expect 200/301/302) ---'
for path in / /itagent/ /ith5/ /itadmin/; do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost${path}")
echo " $path -> HTTP $code"
done
echo ''
echo '############ DONE ############'
echo 'Paste ALL output above back to Claude for diagnosis'
-1
View File
@@ -1 +0,0 @@
UPDATE alembic_version SET version_num = '041_message_server_timestamp';
-17
View File
@@ -1,17 +0,0 @@
// 清除旧数据
MATCH (n) DETACH DELETE n
// 重新创建测试数据(带 uuid)
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题", uuid: "issue-001"})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"})
CREATE (i1)-[:HAS_ACTION]->(a1)
CREATE (i2:Issue {name: "网络连不上", category: "网络问题", uuid: "issue-002"})
CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置"})
CREATE (i2)-[:HAS_ACTION]->(a2)
CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题", uuid: "issue-003"})
CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录"})
CREATE (i3)-[:HAS_ACTION]->(a3)
RETURN "测试数据添加完成"
Binary file not shown.
Binary file not shown.
Binary file not shown.
-295
View File
@@ -1,295 +0,0 @@
<!-- =============================================================================
// 企微IT智能服务台 — H5用户端聊天主页面
// =============================================================================
// 说明:响应式双栏/单栏布局
// - 桌面端(≥500px):左栏对话区 + 拖拽手柄 + 右栏三段式面板
// - 手机端(<500px):全宽单栏对话区,无右侧面板
// - 拖拽手柄:左右栏宽度可手动调节(桌面端)
// ============================================================================= -->
<template>
<div class="chat-view">
<!-- 左栏对话区始终显示 -->
<div
class="chat-view__left"
:class="{ 'chat-view__left--full': !showRightPanel }"
:style="!isMobile && showRightPanel ? { flex: `0 0 ${leftWidth}%`, maxWidth: `${leftWidth}%` } : {}"
>
<ChatPanel />
</div>
<!-- 拖拽手柄左右栏分隔条仅桌面端显示 -->
<div
v-if="showRightPanel && !isMobile"
class="chat-view__resize-handle"
@mousedown="handleResizeStart"
>
<div class="chat-view__resize-grip"></div>
</div>
<!-- 右栏三段式面板仅桌面端显示 -->
<div
v-if="showRightPanel"
class="chat-view__right"
>
<RightPanel />
</div>
</div>
</template>
<script setup lang="ts">
/**
* ChatView 聊天主页面
* 响应式布局:
* - 桌面端(≥500px):双栏 — 左栏对话区 + 右栏三段式面板
* - 手机端(<500px):单栏 — 全宽对话区,无右侧面板
* 桌面端支持拖拽手柄手动调节左右栏宽度
* 右侧面板三段式:AI推送区 / 常用资源标签页 / 趣味问答
*/
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' // 新增:useRoute 用于读取 URL 参数
import { useConversationStore } from '@/stores/conversation'
import { useEmployeeStore } from '@/stores/employee' // 新增:检查登录状态
import { useH5WebSocket } from '@/composables/useH5WebSocket' // H5 WebSocket
import ChatPanel from '@/components/chat/ChatPanel.vue'
import RightPanel from '@/components/assistant/RightPanel.vue'
import { joinConversation as joinConversationApi } from '@/api/conversation' // 邀请功能
// 新增:路由实例(用于跳转登录页)
const router = useRouter()
const route = useRoute() // 用于读取 URL 参数(邀请链接等)
// 新增:员工状态管理(用于检查登录状态)
const employeeStore = useEmployeeStore()
const store = useConversationStore()
// H5 WebSocket 连接(实时推送参与者变更、新消息等事件)
const h5ws = useH5WebSocket()
/** 当前窗口宽度 */
const windowWidth = ref<number>(window.innerWidth)
/** 左栏宽度百分比(默认60%) */
const leftWidth = ref<number>(60)
/** 是否正在拖拽调节宽度 */
const isResizing = ref<boolean>(false)
/** 是否为移动端窄屏(宽度 < 500px,与原型图对齐) */
const isMobile = computed(() => windowWidth.value < 500)
/** 是否显示右栏面板(桌面端始终显示,手机端隐藏) */
const showRightPanel = computed(() => {
// 桌面端:始终显示右栏
if (!isMobile.value) return true
// 手机端:不显示右栏
return false
})
/**
* ── 拖拽调节左右栏宽度 ──
* 方案:只固定左侧宽度,右侧 flex:1 自动填满,不留空白
* 左栏最小宽度 40%,右栏最小宽度 220px
*/
/** 拖拽开始:记录初始 X 坐标和初始左栏宽度 */
function handleResizeStart(event: MouseEvent): void {
isResizing.value = true
const startX = event.clientX
const startLeftWidth = leftWidth.value
const containerWidth = windowWidth.value
/**
* 拖拽中:根据鼠标移动距离换算为百分比,更新左栏宽度
*/
function handleMouseMove(e: MouseEvent): void {
if (!isResizing.value) return
const deltaX = e.clientX - startX
const deltaPercent = (deltaX / containerWidth) * 100
let newLeftWidth = startLeftWidth + deltaPercent
// 限制最小/最大宽度:左栏 40%~80%
newLeftWidth = Math.max(40, Math.min(80, newLeftWidth))
leftWidth.value = newLeftWidth
}
/**
* 拖拽结束:移除事件监听
*/
function handleMouseUp(): void {
isResizing.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
/**
* 监听窗口大小变化
* 更新 windowWidth 以响应式调整布局
*/
function handleResize(): void {
windowWidth.value = window.innerWidth
}
// 生命周期:挂载时添加 resize 监听 + 检查登录状态 + 初始化应用
onMounted(() => {
window.addEventListener('resize', handleResize)
// 🔒 登录状态检查:未登录则跳转登录页
if (!employeeStore.isAuthenticated) {
console.warn('[ChatView] 未登录,跳转登录页')
router.push('/login')
return
}
// 初始化应用(获取用户信息、会话、审批链接等)
store.initialize()
// 🔌 建立 WebSocket 连接(实时推送参与者变更、新消息等)
// 做什么:登录后建立 WS 连接,替代纯轮询
// 为什么:WS 推送比3秒轮询更实时,参与者变更可立即可见
h5ws.connect()
})
// 📋 邀请链接处理:URL 中的 invite 和 eid 参数
// 做什么:被邀请人点击企微卡片后,自动加入会话并切换视图
// 为什么:实现邀请-加入闭环(P0-10)
// 修复(2026-06-12):原实现使用 setTimeout(1000) 等待 store 初始化,
// 存在竞态风险(初始化超过1秒时会失败)。现改为 watch 监听 initialized 变化,
// 确保 store 完全初始化后再执行邀请加入逻辑。
const inviteId = route.query.invite as string
const eid = route.query.eid as string
if (inviteId && eid) {
console.log(`[ChatView] 检测到邀请链接: conv=${inviteId}, eid=${eid}`)
// 监听 store 初始化完成,然后执行邀请加入
const stopWatch = watch(
() => store.initialized,
async (isInitialized) => {
if (!isInitialized) return
// 停止监听(只执行一次)
stopWatch()
try {
// H5 专用端点通过 Token 认证获取 employee_id,无需传递 eid
await joinConversationApi(inviteId)
console.log('[ChatView] 已成功加入邀请的会话')
// 切换到邀请的会话(刷新会话信息 + 加载消息)
await store.switchToConversation(inviteId)
} catch (err: any) {
console.error('[ChatView] 加入会话失败:', err)
// 如果是拒绝性错误(未被邀请/会话已结束),不切换
const code = err?.response?.data?.code
if (code !== 3034 && code !== 3033) {
// 其他错误(如重复加入),仍尝试切换到该会话
try {
await store.switchToConversation(inviteId)
} catch (switchErr) {
console.error('[ChatView] 切换会话也失败:', switchErr)
}
}
} finally {
// 清理 URL 参数(避免刷新后重复加入)
router.replace({ path: route.path, query: {} })
}
},
{ immediate: true } // 如果已经初始化完成,立即执行
)
}
// 生命周期:卸载时移除 resize 监听 + 停止轮询 + 断开WS + 清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
h5ws.disconnect()
store.cleanup()
})
</script>
<style scoped>
/* 聊天主页面容器:双栏 Flex 布局 */
.chat-view {
display: flex;
width: 100%;
height: 100dvh; /* 修复:用 dvh 单位,移动端友好 */
background-color: var(--bg-primary);
position: relative;
}
/* 左栏:对话区 */
.chat-view__left {
flex: 0 0 60%;
max-width: 60%;
height: 100%;
overflow: hidden;
}
/* 左栏全宽(移动端右栏收起时) */
.chat-view__left--full {
flex: 0 0 100%;
max-width: 100%;
}
/* 拖拽手柄:左右栏分隔条 */
.chat-view__resize-handle {
flex: 0 0 6px;
width: 6px;
cursor: col-resize;
background-color: var(--border-color);
position: relative;
z-index: 10;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
/* 拖拽手柄悬停和拖拽中高亮 */
.chat-view__resize-handle:hover,
.chat-view__resize-handle:active {
background-color: var(--accent);
}
/* 拖拽手柄上的抓握指示点 */
.chat-view__resize-grip {
width: 2px;
height: 32px;
border-radius: 1px;
background-color: var(--text-placeholder);
transition: background-color 0.2s;
}
.chat-view__resize-handle:hover .chat-view__resize-grip {
background-color: var(--bg-secondary);
}
/* 右栏:三段式面板 */
.chat-view__right {
flex: 1;
height: 100%;
overflow: hidden;
}
/* 移动端适配(<500px */
@media (max-width: 499px) {
/* 移动端左栏全宽 */
.chat-view__left {
flex: 0 0 100%;
max-width: 100%;
}
}
</style>
-14
View File
@@ -1,14 +0,0 @@
// 添加测试数据到 Neo4j 图谱
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题"})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"})
CREATE (i1)-[:HAS_ACTION]->(a1)
CREATE (i2:Issue {name: "网络连不上", category: "网络问题"})
CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线 2. 重启路由器 3. 检查IP配置"})
CREATE (i2)-[:HAS_ACTION]->(a2)
CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题"})
CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络 2. 清除缓存 3. 重新登录"})
CREATE (i3)-[:HAS_ACTION]->(a3)
RETURN "测试数据添加完成"
-26
View File
@@ -1,26 +0,0 @@
// 添加更多测试数据到 Neo4j 图谱
CREATE (i1:Issue {name: "打印机驱动安装", category: "硬件问题"})
CREATE (a1:Action {name: "打印机驱动安装步骤", description: "1. 打开控制面板 2. 添加打印机 3. 选择手动添加"})
CREATE (i1)-[:HAS_ACTION]->(a1)
CREATE (i2:Issue {name: "网络连不上", category: "网络问题"})
CREATE (a2:Action {name: "网络诊断步骤", description: "1. 检查网线是否插好 2. 重启路由器 3. 检查IP配置"})
CREATE (i2)-[:HAS_ACTION]->(a2)
CREATE (i3:Issue {name: "邮箱无法收发", category: "软件问题"})
CREATE (a3:Action {name: "邮箱故障排除", description: "1. 检查网络连接 2. 清除浏览器缓存 3. 重新登录邮箱"})
CREATE (i3)-[:HAS_ACTION]->(a3)
CREATE (i4:Issue {name: "VPN连接失败", category: "网络问题"})
CREATE (a4:Action {name: "VPN问题排查", description: "1. 检查VPN客户端是否最新 2. 确认账号密码正确 3. 尝试更换VPN服务器"})
CREATE (i4)-[:HAS_ACTION]->(a4)
CREATE (i5:Issue {name: "电脑蓝屏", category: "系统问题"})
CREATE (a5:Action {name: "蓝屏解决方案", description: "1. 记录蓝屏错误代码 2. 重启电脑进入安全模式 3. 检查最近安装的软件"})
CREATE (i5)-[:HAS_ACTION]->(a5)
CREATE (i6:Issue {name: "打印机无法连接", category: "硬件问题"})
CREATE (a6:Action {name: "打印机连接排查", description: "1. 检查打印机电源 2. 确认网络连接 3. 重新安装打印机驱动"})
CREATE (i6)-[:HAS_ACTION]->(a6)
RETURN "更多测试数据添加完成"
-64
View File
@@ -1,64 +0,0 @@
# NAS full /volume1/ scan with sudo (English-only)
# Step 1: User runs `sudo -v` first (password stays local, never enters Claude)
# Step 2: This script reuses that 15-min sudo session
$ErrorActionPreference = "Continue"
$outputFile = "$PSScriptRoot\nas_volumes.txt"
chcp 65001 | Out-Null
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host "===================================" -ForegroundColor Cyan
Write-Host " NAS Full Scan (with sudo)" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "PREREQUISITE: open another terminal and run:" -ForegroundColor Yellow
Write-Host " ssh simon@100.85.152.112" -ForegroundColor White
Write-Host " sudo -v <- enter simon's password here, password NOT sent to Claude" -ForegroundColor White
Write-Host " (keep that SSH session open for 15 min, sudo session cached)" -ForegroundColor White
Write-Host ""
Read-Host "Press Enter after you have done sudo -v above"
# Force allocation so sudo can read password from terminal if needed
$cmd = @"
sudo bash <<'NAS_EOF'
echo '===== [1] All top-level entries under /volume1/ ====='
ls -la /volume1/ 2>&1
echo ''
echo '===== [2] Direct children sizes (1-3 minutes) ====='
du -sh /volume1/*/ 2>/dev/null | sort -rh
echo ''
echo '===== [3] Disk space ====='
df -h /volume1 2>&1 | head -3
echo ''
echo '===== [4] /volume1/homes/ ====='
ls -la /volume1/homes/ 2>&1 | head -20
echo ''
echo '===== [5] /volume1/homes/simon/ top dirs by size ====='
du -sh /volume1/homes/simon/*/ 2>/dev/null | sort -rh | head -20
echo ''
echo '===== [6] /volume1/docker/ top dirs by size (likely big) ====='
du -sh /volume1/docker/*/ 2>/dev/null | sort -rh | head -20
echo ''
echo '===== [7] Largest top-level dirs (top 15) ====='
du -sh /volume1/* 2>/dev/null | sort -rh | head -15
echo ''
echo '===== [8] Mounts / storage pools ====='
mount | grep -E 'volume|tank' 2>&1 | head -10
echo ''
echo '===== DONE ====='
NAS_EOF
"@
ssh -t simon@100.85.152.112 "$cmd" 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8
Write-Host ""
Write-Host "===================================" -ForegroundColor Green
Write-Host " Done. Output saved to:" -ForegroundColor Green
Write-Host " $outputFile" -ForegroundColor White
Write-Host "===================================" -ForegroundColor Green
Write-Host ""
Write-Host "Please paste the ENTIRE contents of nas_volumes.txt back" -ForegroundColor Yellow
Write-Host "(or just tell me which top-level dir is largest)" -ForegroundColor Yellow
Write-Host ""
Read-Host "Press Enter to close"
-43
View File
@@ -1,43 +0,0 @@
# NAS /volume1/ directory listing scan script
# Double-click or run in PowerShell, lists all top-level dirs with sizes
$ErrorActionPreference = "Continue"
$outputFile = "$PSScriptRoot\nas_volumes.txt"
# Force UTF-8 console encoding for SSH output
chcp 65001 | Out-Null
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host "===================================" -ForegroundColor Cyan
Write-Host " NAS /volume1/ Directory Scan" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Scanning... du on large dirs may take 1-3 minutes" -ForegroundColor Yellow
Write-Host ""
$cmd = @"
echo '===== Top-level dirs in /volume1/ ====='
ls -la /volume1/ 2>&1 | grep -v '^total'
echo ''
echo '===== Size by dir (largest first, may take minutes) ====='
du -sh /volume1/*/ 2>/dev/null | sort -rh
echo ''
echo '===== /volume1/homes/ ====='
ls -la /volume1/homes/ 2>/dev/null | head -20
echo ''
echo '===== /volume1/homes/simon/ content ====='
ls -la /volume1/homes/simon/ 2>/dev/null | head -30
du -sh /volume1/homes/simon/*/ 2>/dev/null | sort -rh | head -20
echo ''
echo '===== DONE ====='
"@
ssh simon@100.85.152.112 $cmd 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8
Write-Host ""
Write-Host "===================================" -ForegroundColor Green
Write-Host " Done. Output saved to:" -ForegroundColor Green
Write-Host " $outputFile" -ForegroundColor White
Write-Host "===================================" -ForegroundColor Green
Write-Host ""
Read-Host "Press Enter to close"
-1047
View File
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
site_name: WeCom IT 智能服务台
site_description: 企微 IT 智能服务台项目文档
site_author: Simon & Claude
docs_dir: docs
site_dir: site
theme:
name: material
language: zh
features:
- navigation.instant
- navigation.tabs
- search.suggest
nav:
- 首页: 01-项目总览/01-项目总览与部署手册-20260704.md
- 文档索引: 01-项目总览/00-索引-20260704.md
- 总览:
- 运维手册: 01-项目总览/01-智能IT服务系统运维手册-20260704.md
- 项目全面评估: 01-项目总览/03-项目全面评估报告-20260625.md
- 产品需求:
- PRD v1.2: 02-产品需求/02-产品需求文档PRD-v1.2-20260704.md
- 架构:
- 系统架构设计: 03-技术架构/00-系统架构设计文档-v1.3.md
- 部署:
- 一键部署操作包: 09-部署运维/deploy/10-一键部署操作包-v0.7.0.md
- NAS部署指南: 09-部署运维/deploy/09-NAS部署指南-群晖Cloudflare.md
- 发布说明 v0.7.1: 09-部署运维/deploy/03-RELEASE-NOTES-v0.7.1-20260623.md
- 安全:
- OTP 双因素认证: 09-部署运维/deploy/06-OTP二次验证实现.md
- 安全审计报告: 08-安全审计/审计报告-安全审计/02-安全审计报告-20260614.md
- 故障排查:
- 标准故障排查手册: 09-部署运维/00-标准故障排查手册.md
- 测试质量:
- E2E 验收清单: 06-测试质量/testing-测试/E2E-CHECKLIST-v0.7.0.md
- QA 综合报告: 06-测试质量/testing-测试/QA_COMPREHENSIVE_REPORT.md
- 项目管理:
- 任务总索引: 10-项目管理/01-任务总索引.md
- 项目状态看板: 10-项目管理/05-项目状态看板/01-项目状态看板.md
- 风险跟踪表: 10-项目管理/02-风险跟踪表.md
markdown_extensions:
- admonition
- codehilite
- toc:
permalink: true
Binary file not shown.
-9
View File
@@ -1,9 +0,0 @@
hostkeys_find_by_key_hostfile: hostkeys_foreach failed for C:\\Users\\simon/.ssh/known_hosts: Permission denied
Failed to add the host to the list of known hosts (C:\\Users\\simon/.ssh/known_hosts).
client_input_hostkeys: hostkeys_foreach failed for C:\\Users\\simon/.ssh/known_hosts: Permission denied
Password:
sudo: timed out reading password
sudo: a password is required
Connection to 100.85.152.112 closed.
-1
View File
File diff suppressed because one or more lines are too long
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
import tarfile
import os
os.chdir(r'D:\资料\03-项目开发\wecom_it_smart_desk')
with tarfile.open('neo4j-fix-v2.tar.gz', 'w:gz') as tar:
tar.add('backend/app/services/neo4j_client.py', arcname='app/services/neo4j_client.py')
print('Created neo4j-fix-v2.tar.gz')
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
import tarfile
import os
os.chdir(r'D:\资料\03-项目开发\wecom_it_smart_desk')
with tarfile.open('neo4j-fix-v3.tar.gz', 'w:gz') as tar:
tar.add('backend/app/services/neo4j_client.py', arcname='app/services/neo4j_client.py')
print('Created neo4j-fix-v3.tar.gz')
-70
View File
@@ -1,70 +0,0 @@
# NAS probe script (English-only, prevents PowerShell 5.1 GBK encoding issue)
# Output saved to nas_probe_output.txt
$ErrorActionPreference = "Continue"
$outputFile = "$PSScriptRoot\nas_probe_output.txt"
chcp 65001 | Out-Null
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host "===================================" -ForegroundColor Cyan
Write-Host " NAS Probe Script" -ForegroundColor Cyan
Write-Host "===================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Connecting via Tailscale: simon@100.85.152.112" -ForegroundColor Yellow
Write-Host "Read-only probe, output saved to:" -ForegroundColor Yellow
Write-Host " $outputFile" -ForegroundColor White
Write-Host ""
Write-Host "SSH will prompt for the simon user password..." -ForegroundColor Yellow
Write-Host ""
$cmd = @"
echo '===== [1] DSM Version ====='
cat /etc.defaults/VERSION 2>/dev/null | head -10
uname -a
echo ''
echo '===== [2] Docker availability ====='
which docker && docker --version
ls /var/packages/ContainerManager/target/usr/bin/docker 2>/dev/null
/var/packages/ContainerManager/target/usr/bin/docker --version 2>&1
echo ''
echo '===== [3] All containers (running + stopped) ====='
/var/packages/ContainerManager/target/usr/bin/docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1 | head -40
echo ''
echo '===== [4] /volume1/docker structure ====='
ls -la /volume1/docker/ 2>&1 | head -40
echo '--- sub-dir sizes ---'
du -sh /volume1/docker/*/ 2>/dev/null | head -30
echo ''
echo '===== [5] Listening ports (22/80/443/3000/3022/18080) ====='
ss -tln 2>&1 | head -30
echo ''
echo '===== [6] Tailscale ====='
ls /var/packages/Tailscale/target/bin/ 2>/dev/null
/var/packages/Tailscale/target/bin/tailscale status 2>/dev/null | head -10
echo ''
echo '===== [7] Existing Gitea ====='
/var/packages/ContainerManager/target/usr/bin/docker ps -a | grep -i gitea
ls -la /volume1/docker/gitea 2>&1 | head -10
echo ''
echo '===== [8] Disk space ====='
df -h /volume1 2>&1 | head -3
echo ''
echo '===== [9] User and permissions ====='
id
echo ''
echo '===== [10] Installed packages ====='
ls /var/packages/ 2>/dev/null | grep -iE 'docker|container|tail|portain'
echo ''
echo '===== DONE ====='
"@
ssh simon@100.85.152.112 $cmd 2>&1 | Tee-Object -FilePath $outputFile -Encoding UTF8
Write-Host ""
Write-Host "===================================" -ForegroundColor Green
Write-Host " Done. Output saved to:" -ForegroundColor Green
Write-Host " $outputFile" -ForegroundColor White
Write-Host "===================================" -ForegroundColor Green
Write-Host ""
Read-Host "Press Enter to close"
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
"""重新打包部署前端"""
import os
import tarfile
os.chdir('D:/资料/03-项目开发/wecom_it_smart_desk')
# 重新打包 frontend-agent
print("Repacking frontend-agent...")
if os.path.exists('frontend-agent/dist'):
with tarfile.open('frontend-agent-dist-v3.tar.gz', 'w:gz') as tar:
tar.add('frontend-agent/dist', arcname='dist')
print('Created: frontend-agent-dist-v3.tar.gz')
print('Done!')
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/env python3
"""在服务器上执行数据库迁移"""
import subprocess
import sys
cmd = [
"python",
"C:\\Users\\simon\\.workbuddy\\skills\\jumpserver-ops\\scripts\\jms_ops.py",
"exec",
"-c",
"cd /opt/wecom-it-desk && docker compose exec -T backend alembic current"
]
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
print(result.stderr)
-362
View File
@@ -1,362 +0,0 @@
# nginx 真实 IP 还原 — 生产部署(小白友好版)
> 术语速查:**nginx** = 你这台服务器的"门卫",负责把用户请求分发给后端 / 把静态文件返回给浏览器
> **配置** = nginx 的工作规则,改配置 = 改门卫的工作方式
---
## 我们要做啥(整体目标)
**一句话目标**:`https://itsupport.servyou.com.cn/itadmin/` 之前返回 403(被门卫拦了),原因是门卫把"代理服务器 IP"当成了"用户 IP",而代理 IP 不在白名单里。这次我们改门卫的规则,让它从请求头里读"真实用户 IP"。
**一共 7 个动作**:
| # | 动作 | 大概多久 | 风险 |
|---|---|---|---|
| 1 | PuTTY 连上服务器 | 1 分钟 | ⚪ 无风险 |
| 2 | 备份当前配置 | 几秒 | 🟢 备份原文件,可还原 |
| 3 | 写入 13 行新规则 | 几秒 | 🟡 改配置,但有备份 |
| 4 | 确认写入正确 | 几秒 | ⚪ 只读不写 |
| 5 | 检查配置语法 | 几秒 | ⚪ 只读不写 |
| 6 | 让 nginx 重新读规则 | 1 秒 | 🟡 短暂重载,服务不中断 |
| 7 | 浏览器看效果 | 几秒 | ⚪ 只读 |
**总耗时**:第一次大概 5-10 分钟;熟练了 2 分钟
**整体风险**:🟢 **低** — 每一步都给了"回滚"按钮,改坏了随时能恢复
---
## PuTTY 是啥?在哪儿打开?
**PuTTY** = 一个 SSH 客户端软件,作用是让你从你的 Windows 电脑远程连到公司的 Linux 服务器
**打开方式**:
-`Win 键` → 输入 `putty` → 回车
- 或者开始菜单 → 找到 PuTTY 图标
打开后会看到一个灰底配置界面,我们要填 4 项:
```
┌──────────────────────────────────────┐
│ Host Name (or IP address) │ ← 填: 10.212.189.210
│ Port │ ← 填: 2222
│ Connection type │ ← 选: SSH(默认就是)
│ Saved Sessions │ ← 填: wecom-bastion(起个名)
└──────────────────────────────────────┘
点 Save 保存 → 点 Open 开始连接
```
连接后会黑底白字,提示 `login as:` → 输入 `sxn` 回车 → 提示 `password:` → 输入你的堡垒机密码(输入时屏幕不显示,正常,输完回车就行)
**注意**:输错密码不会锁账号,直接重新输
---
## 动作 1:PuTTY 连服务器(⚪ 无风险)
> **为啥要连服务器?**:改配置必须在服务器上操作,你 Windows 这边只是"遥控器"
连上堡垒机后,黑底白字会显示一个类似 `sxn@jump-host:~$` 的提示符,说明你已经到堡垒机了。
**决策树**:
```
你现在看到了堡垒机提示符(类似 sxn@jump-host:~$)
├─ 是 → 在 PuTTY 里继续输入下面命令
└─ 否 → 截图发给我,卡哪儿了
```
贴下面的命令(右键 = 粘贴,Enter = 执行):
```bash
# 从堡垒机跳到真正的生产服务器
ssh sxn@10.90.5.110
```
回车后可能要输密码(堡垒机和目标机密码可能不同,试一下你之前用过的那个)
**✅ 成功长这样**:
```text
sxn@prod-server:~$
```
**❌ 失败常见**:
- `Permission denied` → 密码错了,重输
- `Connection timed out` → 网络问题,可能 VPN 没连
- 卡住不动 → 可能需要输 `yes` 确认服务器指纹,看到 `(yes/no/[fingerprint])?` 就输 `yes` 回车
---
## 动作 2:备份当前配置(🟢 低风险,改坏了能还原)
> **为啥要备份?**:运维铁律 — **改任何东西之前先备份**,这样改坏了能用备份还原,不会把生产搞挂
```bash
# 进入 nginx 配置所在目录
cd /opt/wecom-it-desk/nginx
# 复制一份当前配置,文件名带当前时间(分),方便区分
sudo cp nginx.conf nginx.conf.bak-$(date +%H%M)
# 列出所有备份文件,确认刚才那行成功
ls -la nginx.conf.bak-*
```
**为啥用 `$(date +%H%M)`?**:这个写法会自动拼上当前时间(比如 1430 表示 14:30),每次备份文件名都不一样,不会覆盖之前的备份
**✅ 成功长这样**:
```text
-rw-r--r-- 1 root root 4821 Jun 15 14:30 nginx.conf.bak-1430
```
**❌ 失败常见**:
- `cp: cannot stat 'nginx.conf'` → 当前不在 nginx 目录,先 `cd /opt/wecom-it-desk/nginx` 进去
- `Permission denied` → 缺 `sudo`,命令前面加 `sudo` 重试
---
## 动作 3:写入 13 行新规则(🟡 中风险,但有备份兜底)
> **写入啥?**:13 行 nginx 配置,告诉 nginx"从请求头 X-Forwarded-For 里读真实用户 IP"
>
> **为啥要这样做?**:用户通过公司 WAF/堡垒机访问,WAF 会把真实 IP 放在 `X-Forwarded-For` 请求头里,但 nginx 默认只看直连 IP,所以才误判 403
**重要**:把下面**从 `cat > /tmp/patch.py``PYEOF`** 的**整段**一次性粘贴进 PuTTY(右键 = 粘贴)。整段会作为一条命令执行。
```bash
# 创建一个 python 脚本到 /tmp/patch.py
cat > /tmp/patch.py << 'PYEOF'
fp = '/opt/wecom-it-desk/nginx/nginx.conf'
with open(fp) as f:
c = f.read()
patch = '''
# ------------------------------------------------------------------
# 真实 IP 还原(2026-06-15 v0.5.1 修复)
# ------------------------------------------------------------------
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
'''
old = 'error_log /var/log/nginx/error.log warn;'
new = old + patch
new_c = c.replace(old, new, 1)
with open(fp, 'w') as f:
f.write(new_c)
print('patched, +{} bytes'.format(len(new_c) - len(c)))
PYEOF
# 运行这个 python 脚本,它会自动把上面那 13 行插入到 nginx.conf
sudo python3 /tmp/patch.py
```
**术语解释**:
- `cat > /tmp/patch.py` → 创建一个文件,内容是后面所有内容
- `<< 'PYEOF' ... PYEOF` → 这种写法叫 **heredoc**(直译"这里是文档"),作用是把多行文字原样写入文件
- `sudo` → 以管理员身份运行(改系统文件需要权限)
**✅ 成功长这样**:
```text
patched, +492 bytes
```
**❌ 失败常见**:
- `Permission denied` → 缺 `sudo`,或者 nginx.conf 不存在
- `NameError: name 'fp' is not defined` → heredoc 没贴完整,最末尾的 `PYEOF` 没贴上
- 没任何输出 → python 没运行,看光标有没有新行,可能没回车
---
## 动作 4:确认写入正确(⚪ 无风险,只读)
> **为啥要确认?**:虽然脚本说写入了,但**人眼看到才真的算**。这步只读不写,放心跑
```bash
# 在 nginx.conf 里搜索"真实 IP 还原"关键字,并显示后面 13 行
sudo grep -A 13 "真实 IP 还原" /opt/wecom-it-desk/nginx/nginx.conf
```
**✅ 成功长这样**(应该看到完整 13 行):
```nginx
# 真实 IP 还原(2026-06-15 v0.5.1 修复)
# ------------------------------------------------------------------
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
```
**❌ 失败**:
- 啥也没输出 → 写入失败,回到动作 3 重做
- 只输出一两行 → heredoc 没贴全,需要回滚后重来
---
## 动作 5:检查配置语法(⚪ 无风险,只读不执行)
> **为啥要检查?**:这个命令 nginx 会"假装"按新配置启动,只检查语法,不会真的重启。**通过 = 配置写得对,放心用;不通过 = 写得有问题,继续走会出问题**
```bash
# 在 nginx 容器(就是跑 nginx 服务的那个小 Linux)内,做配置语法检查
docker compose exec nginx nginx -t
```
**术语解释**:
- `docker compose` → 管理这台服务器上所有"容器"的命令
- `exec` → "钻进"某个容器里执行命令
- `nginx -t` → nginx 自带的"语法检查"工具(全称 `--test`)
**✅ 成功长这样**:
```text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
**❌ 失败**(`test is successful` 没出现):
- `unexpected "}"` / `unknown directive` → 写错字了,回去动作 4 看看哪里对不上
- **直接停下,不要继续** → 复制错误信息贴回给我
---
## 动作 6:让 nginx 重新读规则(🟡 中风险,但服务不中断)
> **"重新读"是啥意思?**:nginx 现在用的还是旧配置,我们让 nginx 不用重启(不会断服务)就把新配置加载进来。这个动作叫"热加载"或 "reload"
>
> **会断网吗?**:不会,reload 是无缝的,用户那边无感知
```bash
# 通知 nginx 容器内的 master 进程重新读配置
docker compose exec nginx nginx -s reload
```
**术语解释**:`-s reload` = 发信号(英文 signal)给 nginx,告诉它"重读配置"
**✅ 成功长这样**(没报错即成功):
```text
2026/06/15 14:35:12 [notice] 1#1: signal process started
```
**❌ 失败**:
- `nginx: [error]` 开头 → 配置没通过,回去动作 5 看哪里没对
- 啥也没输出 → 命令没执行,看光标位置
---
## 动作 7:浏览器看效果(⚪ 无风险)
**为啥这步是浏览器而不是 curl?**:curl 看响应头,浏览器看真实页面。**人眼看到才作数**
**操作步骤**:
1. 打开浏览器
2. **开隐身模式**(`Ctrl + Shift + N`,Chrome / Edge 都是这个快捷键)
- **为啥要隐身?**:隐身模式不读本地缓存,看到的就是 nginx **当下**返回的
3. 地址栏输入 `https://itsupport.servyou.com.cn/itadmin/`
4. 按回车
**✅ 成功长这样**:
- 页面正常显示
-`F12` 打开开发者工具 → `Network` 选项卡 → 顶部那一行状态码是 **200**(不是 403)
**❌ 失败**:
- 仍然是 403 → 见下面"如果还是 403"段
- 502 / 504 → nginx 后面那个服务挂了,贴错误给我
- 页面打不开(连接被拒) → DNS 没配,联系 IT 运维
---
## 如果还是 403 — 看 WAF 出口 IP(诊断)
> **啥是 WAF?**:公司部署在 nginx 前面的"统一入口",所有用户请求先经过 WAF 再到 nginx。WAF 自己的 IP 不一定在你写的 4 段内网里,所以还得加
```bash
# 看 nginx 最后 20 条访问日志,找 $remote_addr 是不是 WAF 的 IP
docker compose exec nginx tail -20 /var/log/nginx/access.log
```
**日志长这样**:
```text
10.80.5.123 - - [15/Jun/2026:14:35:45 +0800] "GET /itadmin/ HTTP/1.1" 403 ...
^^^^^^^
这就是 $remote_addr
```
把那个 IP 数字(比如 `10.80.5.123`)贴回给我,我会:
1. 给你追加一行 `set_real_ip_from 10.80.5.123;`
2. 让你重跑动作 5 + 动作 6
---
## 如果改坏了 — 回滚(啥时候都能用)
> **啥时候用?**:任何一个动作出问题,你都可以直接回滚到动作 2 备份的版本
```bash
# 列出所有备份,挑最近的一个
ls -la /opt/wecom-it-desk/nginx/nginx.conf.bak-*
```
```bash
# 用最近那个备份覆盖当前配置(把 1430 换成上面列出的真实时间)
sudo cp /opt/wecom-it-desk/nginx/nginx.conf.bak-1430 /opt/wecom-it-desk/nginx/nginx.conf
```
```bash
# 重新加载回滚后的配置
docker compose exec nginx nginx -s reload
```
回滚后页面应该回到改之前的状态(403 回来),说明回滚成功
---
## 一张图看懂流程
```
PuTTY 连服务器
备份原配置
写入 13 行新规则
确认写入正确 ──→ ❌ 不对 ──→ 重做写入 / 回滚
│ ✅
检查配置语法 ──→ ❌ 语法错 ──→ 复制错误贴回给我,不要继续
│ ✅
重载 nginx ─────→ ❌ 报错 ──→ 检查容器状态 / 找 Claude
│ ✅
浏览器看效果 ──→ ❌ 还是 403 ──→ 看 WAF 出口 IP,贴给 Claude
│ ✅
🎉 完成
```
---
## 我建议你第一次做
**第一次建议**:动作 1 → 动作 2 → **停一下,截图发给我** → 我确认备份成功 → 你再继续动作 3 之后
**熟练了以后**:一口气跑完动作 1-7,中间不打断
---
## 关联
- 评审报告:`review-p0-security-2026-06-14.md` P0-3
- 待办:`ip-whitelist-trust-proxies-todo.md` — v1.0 前必须收窄 4 段 → 4 个 IP
- 本地配置:`deploy-server/nginx/nginx.conf`(已包含 patch,下次重打包自动带)
- 服务器 IP 变更:`project-production-server-ip-2026-06-15.md` — 10.80.0.136 已下线,用 10.90.5.110
- 客户端约束:`feedback-putty-not-openssh.md` — 用 PuTTY,不用 `ssh -J`
- 命令行规范:`feedback-cmd-step-by-step.md` — 每行一条 + 中文注释
- 小白引导规范:`feedback-beginner-friendly-guide.md` — 讲清目标+风险、术语解释、出错兜底
-8
View File
@@ -1,8 +0,0 @@
$b = [System.IO.File]::ReadAllText('fix-prod.b64').Trim()
$n = $b.Length
$half = [int]($n / 2)
$s1 = $b.Substring(0, $half)
$s2 = $b.Substring($half)
[System.IO.File]::WriteAllText('fix-prod.s1', $s1)
[System.IO.File]::WriteAllText('fix-prod.s2', $s2)
Write-Host "Total=$n seg1=$($s1.Length) seg2=$($s2.Length)"
-67
View File
@@ -1,67 +0,0 @@
"""Split and upload large file to server via JumpServer base64 chunks."""
import base64
import hashlib
import os
import subprocess
import sys
import time
PYTHON = r"C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe"
JMS = r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py"
LOCAL_FILE = r"D:\资料\03-项目开发\wecom_it_smart_desk\deploy_agent_v8.tar.gz"
REMOTE_FILE = "/tmp/deploy_agent_v8.tar.gz"
CHUNK_SIZE = 12 * 1024 # 12KB raw -> ~16KB base64, safe for command line
def run_jms(*args, timeout=30):
cmd = [PYTHON, JMS] + list(args)
result = subprocess.run(cmd, capture_output=True, timeout=timeout)
out = result.stdout.decode('utf-8', errors='replace') if result.stdout else ''
err = result.stderr.decode('utf-8', errors='replace') if result.stderr else ''
return out + err
def main():
with open(LOCAL_FILE, "rb") as f:
data = f.read()
md5 = hashlib.md5(data).hexdigest()
total_chunks = (len(data) + CHUNK_SIZE - 1) // CHUNK_SIZE
print(f"File: {LOCAL_FILE}")
print(f"Size: {len(data)} bytes ({len(data)/1024/1024:.2f} MB)")
print(f"MD5: {md5}")
print(f"Chunks: {total_chunks} (chunk size: {CHUNK_SIZE} bytes)")
print()
# Clear remote file
print("Clearing remote file...")
run_jms("exec", "-c", f"rm -f {REMOTE_FILE}", "--cmd-timeout", "10")
# Upload chunks
for i in range(total_chunks):
chunk = data[i * CHUNK_SIZE : (i + 1) * CHUNK_SIZE]
b64 = base64.b64encode(chunk).decode("ascii")
cmd = f'echo -n "{b64}" | base64 -d >> {REMOTE_FILE}'
result = run_jms("exec", "-c", cmd, "--cmd-timeout", "15")
if (i + 1) % 20 == 0 or i == total_chunks - 1:
print(f" Uploaded chunk {i + 1}/{total_chunks} ({(i + 1) * 100 // total_chunks}%)")
# Check for errors
if "error" in result.lower() and "traceback" not in result.lower():
# jms_ops.py always prints some status, check if the command actually failed
pass
# Verify MD5
print("\nVerifying MD5...")
result = run_jms("exec", "-c", f"md5sum {REMOTE_FILE}", "--cmd-timeout", "15")
print(f" Remote MD5: {result.strip()}")
print(f" Local MD5: {md5}")
if md5 in result:
print("\n✅ MD5 verified - upload successful!")
else:
print("\n❌ MD5 mismatch - upload may be corrupted!")
sys.exit(1)
if __name__ == "__main__":
main()
-78
View File
@@ -1,78 +0,0 @@
cat > /tmp/gitea-stage1.sh <<'NAS_EOF'
#!/bin/bash
set +e # don't bail on error, collect everything
DOCKER=/var/packages/ContainerManager/target/usr/bin/docker
echo '===== [1] Disk space ====='
df -h /volume1
echo ''
echo '===== [2] Docker version ====='
$DOCKER --version 2>&1
$DOCKER info 2>&1 | head -20
echo ''
echo '===== [3] Existing containers (running + stopped) ====='
$DOCKER ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1
echo ''
echo '===== [4] Existing images (gitea-related highlighted) ====='
$DOCKER images --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}' 2>&1
echo '--- gitea images only ---'
$DOCKER images 2>&1 | grep -i gitea
echo ''
echo '===== [5] /volume1/docker structure (top-level) ====='
ls -la /volume1/docker/ 2>&1 | head -30
echo '--- sub-dir sizes (top 20) ---'
sudo du -sh /volume1/docker/*/ 2>/dev/null | sort -rh | head -20
echo ''
echo '===== [6] /volume1/docker/gitea exists? ====='
ls -la /volume1/docker/gitea 2>&1
echo ''
echo '===== [7] Listening ports (3000/2222 must be free) ====='
ss -tln 2>&1 | grep -E ':3000|:2222|:80|:443' || echo '(none of 3000/2222/80/443 in use)'
echo ''
echo '===== [8] Tailscale ====='
/var/packages/Tailscale/target/bin/tailscale status 2>&1 | head -10
ip -4 addr show tailscale0 2>&1 | grep inet
echo ''
echo '===== [9] Docker daemon registry config ====='
cat /var/packages/ContainerManager/etc/docker/daemon.json 2>&1
echo ''
echo '===== [10] Test Docker Hub reachability ====='
curl -s -o /dev/null -w 'docker.io: HTTP %{http_code}, time %{time_total}s\n' \
--max-time 8 https://registry-1.docker.io/v2/ 2>&1
curl -s -o /dev/null -w 'gcr.io: HTTP %{http_code}, time %{time_total}s\n' \
--max-time 8 https://gcr.io/v2/ 2>&1
curl -s -o /dev/null -w 'tencentyun mirror: HTTP %{http_code}, time %{time_total}s\n' \
--max-time 8 https://mirror.ccs.tencentyun.com/v2/ 2>&1
echo ''
echo '===== [11] User & groups (is simon in docker group?) ====='
id
groups
echo ''
echo '===== [12] CPU / memory ====='
free -h
nproc
echo ''
echo '===== STAGE 1 DONE ====='
NAS_EOF
chmod +x /tmp/gitea-stage1.sh
echo '=== SCRIPT WRITTEN: /tmp/gitea-stage1.sh ==='
echo '=== Press ENTER to execute (sudo will prompt for password) ==='
read
sudo bash /tmp/gitea-stage1.sh 2>&1 | tee /tmp/gitea-stage1.log
echo ''
echo '=== LOG SAVED: /tmp/gitea-stage1.log ==='
echo '=== Paste the entire output above back to Claude ==='
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
docker run -d \
--name wecom_it_backend \
--network wecom-it-desk_it-desk-internal \
-e DATABASE_URL=postgresql://wecom:wecom_secret_2026@postgres:5432/wecom_it_desk \
-e REDIS_URL=redis://:R3dIs2026Secure@redis:6379/0 \
-e WECOM_CORP_ID=wwa8c87970b2011f41 \
-e WECOM_AGENT_ID=1000133 \
-e WECOM_SECRET=EOtQslW7WD8Rna8Nm9WnwCW-ozHP3tustL4mFnet6O8 \
-e WECOM_TOKEN=wAqMCP \
-e WECOM_ENCODING_AES_KEY=KQY3cEsBc3rdi3xua9rPd5WxH8kYOhyASzWZQf75aJS \
-e CORS_ORIGINS=https://itsupport.servyou.com.cn,http://itsupport.servyou.com.cn \
wecom-it-desk-backend:latest \
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2
+3
View File
@@ -0,0 +1,3 @@
import requests
r = requests.get('http://127.0.0.1:8000/h5/agents/online-status')
print(r.text)
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env python3
"""Verify WeCom contact sync works after IP whitelist configuration."""
import httpx
import json
CORP_ID = "wwa8c87970b2011f41"
CONTACT_SECRET = "BM6iosc3gKnPqkEXmsQN3ErJUpfO-whfMUN646eezB8"
# Step 1: Get contact access token
print("=" * 60)
print("Step 1: Get contact access token")
resp = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={CORP_ID}&corpsecret={CONTACT_SECRET}", timeout=10)
token_data = resp.json()
print(f" errcode: {token_data.get('errcode')}")
print(f" errmsg: {token_data.get('errmsg')}")
if 'access_token' not in token_data:
print(" FAILED: No access token returned")
exit(1)
token = token_data['access_token']
print(f" access_token: {token[:30]}...")
# Step 2: Get department list
print("\n" + "=" * 60)
print("Step 2: Get department list")
resp2 = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token={token}", timeout=10)
dept_data = resp2.json()
print(f" errcode: {dept_data.get('errcode')}")
print(f" errmsg: {dept_data.get('errmsg')}")
departments = dept_data.get('department', [])
print(f" department count: {len(departments)}")
if departments:
print(f" first 5 departments:")
for d in departments[:5]:
print(f" - id={d.get('id')}, name={d.get('name')}, parentid={d.get('parentid')}")
# Step 3: Get members of root department (id=1)
print("\n" + "=" * 60)
print("Step 3: Get members of root department (id=1)")
resp3 = httpx.get(f"https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token={token}&department_id=1&fetch_child=1", timeout=15)
user_data = resp3.json()
print(f" errcode: {user_data.get('errcode')}")
print(f" errmsg: {user_data.get('errmsg')}")
users = user_data.get('userlist', [])
print(f" user count: {len(users)}")
if users:
print(f" first 3 users:")
for u in users[:3]:
print(f" - userid={u.get('userid')}, name={u.get('name')}, department={u.get('department')}")
print("\n" + "=" * 60)
print("SUMMARY:")
print(f" Token: OK")
print(f" Departments: {len(departments)}")
print(f" Users: {len(users)}")
if dept_data.get('errcode') == 0 and len(departments) > 0:
print(" RESULT: SUCCESS - Contact sync is working!")
else:
print(" RESULT: STILL FAILING")
-3
View File
@@ -1,3 +0,0 @@
MATCH (i:Issue)
WHERE i.name CONTAINS '打印机驱动'
RETURN i.name
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env python3
"""Test the approval detect-intent API endpoint."""
import json
import httpx
# Test the API endpoint from inside the container
url = "https://localhost/api/approval/detect-intent"
test_messages = [
"我要申请VPN",
"我的电脑坏了",
"我要申请办公用品",
"你好,今天天气怎么样?",
]
for msg in test_messages:
print(f"\n=== Testing: {msg} ===")
try:
resp = httpx.post(
url,
json={"text": msg},
headers={"Content-Type": "application/json"},
timeout=30.0,
verify=False, # self-signed cert
)
print(f" HTTP: {resp.status_code}")
print(f" Response: {resp.text[:500]}")
if resp.status_code == 200:
data = resp.json()
result = data.get("data", data)
print(f" is_approval: {result.get('is_approval_request')}")
print(f" confidence: {result.get('confidence')}")
print(f" type: {result.get('approval_type')}")
print(f" source: {result.get('source')}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
-34
View File
@@ -1,34 +0,0 @@
#!/usr/bin/env python3
"""Test detect-intent API from inside backend container (direct FastAPI)."""
import json
import httpx
# Backend FastAPI listens on 8000 inside the container
url = "http://localhost:8000/api/approval/detect-intent"
test_messages = [
"我要申请VPN",
"我的电脑坏了",
"我要申请办公用品",
"你好,今天天气怎么样?",
]
for msg in test_messages:
print(f"\n=== Testing: {msg} ===")
try:
resp = httpx.post(
url,
json={"text": msg},
timeout=30.0,
)
print(f" HTTP: {resp.status_code}")
print(f" Response: {resp.text[:500]}")
if resp.status_code == 200:
data = resp.json()
result = data.get("data", data)
print(f" is_approval: {result.get('is_approval_request')}")
print(f" confidence: {result.get('confidence')}")
print(f" type: {result.get('approval_type')}")
print(f" source: {result.get('source')}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
-34
View File
@@ -1,34 +0,0 @@
#!/usr/bin/env python3
"""Test detect-intent API - correct path without /api prefix."""
import json
import httpx
# Backend routes are at root level (no /api prefix, nginx adds it)
url = "http://localhost:8000/approval/detect-intent"
test_messages = [
"我要申请VPN",
"我的电脑坏了",
"我要申请办公用品",
"你好,今天天气怎么样?",
]
for msg in test_messages:
print(f"\n=== Testing: {msg} ===")
try:
resp = httpx.post(
url,
json={"text": msg},
timeout=30.0,
)
print(f" HTTP: {resp.status_code}")
print(f" Response: {resp.text[:500]}")
if resp.status_code == 200:
data = resp.json()
result = data.get("data", data)
print(f" is_approval: {result.get('is_approval_request')}")
print(f" confidence: {result.get('confidence')}")
print(f" type: {result.get('approval_type')}")
print(f" source: {result.get('source')}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify API 连接"""
import httpx
import asyncio
import sys
async def test_dify():
url = "http://yw-dify.dc.servyou-it.com/v1/chat-messages"
headers = {"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO"}
payload = {
"query": "test",
"user": "test_user",
"response_mode": "blocking"
}
try:
async with httpx.AsyncClient(timeout=15) as client:
print(f"Testing Dify API: {url}")
response = await client.post(url, json=payload, headers=headers)
print(f"Status: {response.status_code}")
print(f"Response: {response.text[:500]}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(test_dify())
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify API v1 端点"""
import httpx
import asyncio
import sys
async def test_dify():
# Test the base URL
async with httpx.AsyncClient(timeout=5) as client:
print("=== Test 1: Base URL ===")
r = await client.get("http://yw-dify.dc.servyou-it.com/")
print(f"Status: {r.status_code}, Location: {r.headers.get('location', 'N/A')}")
print("\n=== Test 2: API v1/chat-messages ===")
url = "http://yw-dify.dc.servyou-it.com/v1/chat-messages"
headers = {"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO"}
payload = {
"query": "hello",
"user": "test",
"response_mode": "blocking"
}
r = await client.post(url, json=payload, headers=headers)
print(f"Status: {r.status_code}")
print(f"Response: {r.text[:500]}")
if __name__ == "__main__":
asyncio.run(test_dify())
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""Test Dify approval intent detection API directly."""
import json
import sys
import httpx
BASE_URL = "http://yw-dify.dc.servyou-it.com/dify2openai"
API_KEY = "app-JWI7u1LTn9XPVe95KL6dHzPx"
url = f"{BASE_URL.rstrip('/')}/v1/chat/completions"
body = {
"model": "dify",
"messages": [
{"role": "system", "content": "你是IT服务台审批意图识别器。"},
{"role": "user", "content": "我要申请VPN"},
],
"temperature": 0,
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
print(f"URL: {url}")
print(f"Body: {json.dumps(body, ensure_ascii=False)}")
print("---")
try:
resp = httpx.post(url, json=body, headers=headers, timeout=30.0)
print(f"HTTP Status: {resp.status_code}")
print(f"Response Headers: {dict(resp.headers)}")
print(f"Response Body (raw): {resp.text[:2000]}")
print("---")
# Try parsing as JSON
try:
data = resp.json()
print(f"Response JSON: {json.dumps(data, ensure_ascii=False, indent=2)[:2000]}")
choices = data.get("choices") or []
if choices:
content = choices[0].get("message", {}).get("content", "")
print(f"Content: {content[:500]}")
# Try parsing content as JSON
try:
parsed = json.loads(content)
print(f"Parsed: {json.dumps(parsed, ensure_ascii=False, indent=2)}")
except json.JSONDecodeError as e:
print(f"Content is NOT valid JSON: {e}")
else:
print("No choices in response")
except Exception as e:
print(f"Response is not JSON: {e}")
except Exception as e:
print(f"Request FAILED: {type(e).__name__}: {e}")
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""Test Dify API with composite API key format."""
import json
import os
import httpx
# The proxy expects composite format: <dify_base_url>|<api_key>|<app_type>
# Main Dify uses: http://yw-dify.dc.servyou-it.com/v1|app-UaTWYdBSwN6VktKQlbh5YN5H|Chat
# Approval should use: http://yw-dify.dc.servyou-it.com/v1|app-JWI7u1LTn9XPVe95KL6dHzPx|Chat
PROXY_URL = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions"
APPROVAL_KEY_RAW = "app-JWI7u1LTn9XPVe95KL6dHzPx"
APPROVAL_KEY_COMPOSITE = "http://yw-dify.dc.servyou-it.com/v1|app-JWI7u1LTn9XPVe95KL6dHzPx|Chat"
body = {
"model": "dify",
"messages": [
{"role": "user", "content": "我要申请VPN"},
],
"temperature": 0,
}
# Test 1: Raw API key (current, failing)
print("=== Test 1: Raw API key (current) ===")
headers = {"Authorization": f"Bearer {APPROVAL_KEY_RAW}", "Content-Type": "application/json"}
try:
resp = httpx.post(PROXY_URL, json=body, headers=headers, timeout=15.0)
print(f" HTTP: {resp.status_code}")
print(f" Body: {resp.text[:300]}")
except Exception as e:
print(f" ERROR: {e}")
# Test 2: Composite API key
print("\n=== Test 2: Composite API key ===")
headers = {"Authorization": f"Bearer {APPROVAL_KEY_COMPOSITE}", "Content-Type": "application/json"}
try:
resp = httpx.post(PROXY_URL, json=body, headers=headers, timeout=30.0)
print(f" HTTP: {resp.status_code}")
print(f" Body: {resp.text[:500]}")
if resp.status_code == 200:
data = resp.json()
choices = data.get("choices") or []
if choices:
content = choices[0].get("message", {}).get("content", "")
print(f" Content: {content[:300]}")
try:
parsed = json.loads(content)
print(f" Parsed JSON: {json.dumps(parsed, ensure_ascii=False, indent=2)}")
except:
print(f" Content is not JSON")
except Exception as e:
print(f" ERROR: {e}")
# Test 3: Check main Dify key format
print("\n=== Test 3: Main Dify env vars ===")
print(f" DIFY_API_KEY = {os.environ.get('DIFY_API_KEY', 'NOT SET')}")
print(f" DIFY_API_URL = {os.environ.get('DIFY_API_URL', 'NOT SET')}")
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify DNS 解析"""
import socket
host = "yw-dify.dc.servyou-it.com"
try:
ip = socket.gethostbyname(host)
print(f"{host} -> {ip}")
except Exception as e:
print(f"DNS lookup failed: {e}")
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env python3
"""测试容器能否访问 Dify"""
import httpx
import socket
# 测试 DNS 解析
try:
ip = socket.gethostbyname('yw-dify.dc.servyou-it.com')
print(f"DNS resolved: yw-dify.dc.servyou-it.com -> {ip}")
except Exception as e:
print(f"DNS failed: {e}")
# 测试 HTTP 连接
async def test_dify():
async with httpx.AsyncClient(timeout=10) as client:
# 测试根路径
try:
r = await client.get('http://yw-dify.dc.servyou-it.com/')
print(f"GET / -> {r.status_code}")
except Exception as e:
print(f"GET / failed: {type(e).__name__}: {e}")
# 测试 API 端点
try:
r = await client.post(
'http://yw-dify.dc.servyou-it.com/v1/chat-messages',
json={"inputs": {}, "query": "test", "response_mode": "blocking", "user": "test"}
)
print(f"POST /v1/chat-messages -> {r.status_code}")
except Exception as e:
print(f"POST /v1/chat-messages failed: {type(e).__name__}: {e}")
if __name__ == "__main__":
import asyncio
asyncio.run(test_dify())
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
"""测试内部网络 Dify 服务"""
import httpx
import asyncio
async def test_internal():
async with httpx.AsyncClient(timeout=10) as client:
# 测试 RAGFlow (有 Dify API)
print("=== RAGFlow 8080 (Dify API) ===")
try:
r = await client.post(
"http://10.80.0.85:8080/v1/chat-messages",
json={"query": "hello", "user": "test"},
headers={"Content-Type": "application/json"}
)
print(f"Status: {r.status_code}")
print(f"Response: {r.text[:200]}")
except Exception as e:
print(f"Error: {e}")
print()
# 测试内部 Dify 服务 (尝试常见内网地址)
print("=== Try Internal Dify Services ===")
candidates = [
"http://10.80.0.85:8081",
"http://10.80.0.86:8080",
"http://dify:8080",
"http://dify:80",
]
for url in candidates:
try:
r = await client.get(url)
print(f"{url}: {r.status_code}")
except Exception as e:
print(f"{url}: FAIL - {type(e).__name__}")
if __name__ == "__main__":
asyncio.run(test_internal())
-25
View File
@@ -1,25 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify 直接通过 IP 访问"""
import httpx
import asyncio
async def test_by_ip():
async with httpx.AsyncClient(timeout=30) as client:
# 直接通过 IP 访问
print("=== Direct IP: 10.80.0.240 ===")
try:
r = await client.post(
"http://10.80.0.240/v1/chat-messages",
json={"query": "hello", "user": "test"},
headers={
"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO",
"Content-Type": "application/json"
}
)
print(f"Status: {r.status_code}")
print(f"Response: {r.text[:300]}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(test_by_ip())
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""Test multiple Dify API keys to find a working one"""
import httpx
import json
import time
base_url = "http://yw-dify.dc.servyou-it.com"
# Test multiple API keys
api_keys = [
("app-7jkRkAzvX4QM9v9SM3P8mMEO", "审批意图副本"),
("app-J3s8sHarZQ2SCaNF3xCppliL", "自建应用"),
("app-z3S9AEUUAVPbtR2rioxpiIvp", "分诊应用"),
]
for api_key, name in api_keys:
url = f"{base_url}/v1/chat-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"inputs": {},
"query": "密码忘记了怎么办",
"response_mode": "blocking",
"user": "test_verify"
}
print(f"=== Testing: {name} ({api_key[:20]}...) ===")
start = time.time()
try:
with httpx.Client(timeout=httpx.Timeout(30.0)) as client:
resp = client.post(url, headers=headers, json=payload)
elapsed = (time.time() - start) * 1000
print(f"Status: {resp.status_code}, Time: {elapsed:.0f}ms")
if resp.status_code == 200:
data = resp.json()
answer = data.get('answer', '')
print(f"conversation_id: {data.get('conversation_id', 'N/A')}")
print(f"answer (first 300 chars): {answer[:300]}")
# Try JSON parse
try:
parsed = json.loads(answer)
print(f"JSON Parse: SUCCESS - keys: {list(parsed.keys())}")
except:
print(f"JSON Parse: FAILED (plain text)")
else:
print(f"Error: {resp.text[:300]}")
except Exception as e:
print(f"Exception: {type(e).__name__}: {e}")
print()
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env python3
"""Test Dify API with different model parameter formats."""
import json
import httpx
BASE_URL = "http://yw-dify.dc.servyou-it.com/dify2openai"
API_KEY = "app-JWI7u1LTn9XPVe95KL6dHzPx"
url = f"{BASE_URL.rstrip('/')}/v1/chat/completions"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
# Test different model parameter formats
test_models = [
"dify",
"", # empty string
"gpt-3.5-turbo",
"deepseek-chat",
"default",
]
for model in test_models:
body = {
"model": model,
"messages": [
{"role": "user", "content": "我要申请VPN"},
],
"temperature": 0,
}
print(f"\n=== model={repr(model)} ===")
try:
resp = httpx.post(url, json=body, headers=headers, timeout=15.0)
print(f" HTTP Status: {resp.status_code}")
body_text = resp.text[:500]
print(f" Response: {body_text}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
# Also test without model parameter at all
print(f"\n=== No model parameter ===")
body = {
"messages": [
{"role": "user", "content": "我要申请VPN"},
],
"temperature": 0,
}
try:
resp = httpx.post(url, json=body, headers=headers, timeout=15.0)
print(f" HTTP Status: {resp.status_code}")
print(f" Response: {resp.text[:500]}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
# Test with the main AI chat API key to compare
# First, check what API key the main Dify uses
print(f"\n=== Check main Dify config ===")
import os
main_key = os.environ.get("DIFY_API_KEY", "")
main_base = os.environ.get("DIFY_BASE_URL", "")
print(f" DIFY_API_KEY: {main_key[:20]}..." if main_key else " DIFY_API_KEY: not set")
print(f" DIFY_BASE_URL: {main_base}")
if main_key:
main_url = f"{main_base.rstrip('/')}/v1/chat/completions" if main_base else url
main_headers = {"Authorization": f"Bearer {main_key}", "Content-Type": "application/json"}
body = {
"model": "dify",
"messages": [{"role": "user", "content": "你好"}],
"temperature": 0,
}
print(f"\n=== Test main Dify API with model='dify' ===")
try:
resp = httpx.post(main_url, json=body, headers=main_headers, timeout=15.0)
print(f" HTTP Status: {resp.status_code}")
print(f" Response: {resp.text[:500]}")
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify 原生 API 和 dify2openai 代理"""
import httpx
import asyncio
async def test_dify():
async with httpx.AsyncClient(timeout=30) as client:
# 1. 测试原生 API 路径
print("=== Test 1: Dify Native API (v1/chat-messages) ===")
url1 = "http://yw-dify.dc.servyou-it.com/v1/chat-messages"
headers1 = {
"Authorization": "Bearer app-7jkRkAzvX4QM9v9SM3P8mMEO",
"Content-Type": "application/json"
}
payload1 = {
"inputs": {"user_query": "hello"},
"query": "hello",
"response_mode": "blocking",
"user": "test-user-001"
}
try:
r1 = await client.post(url1, json=payload1, headers=headers1)
print(f"Status: {r1.status_code}")
print(f"Response: {r1.text[:300]}")
except Exception as e:
print(f"Error: {e}")
print()
# 2. 测试 dify2openai 代理路径
print("=== Test 2: dify2openai Proxy ===")
url2 = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions"
headers2 = {
"Authorization": "Bearer http://yw-dify.dc.servyou-it.com/v1|app-7jkRkAzvX4QM9v9SM3P8mMEO|Chat",
"Content-Type": "application/json"
}
payload2 = {
"model": "Chat",
"messages": [{"role": "user", "content": "hello"}],
"stream": False
}
try:
r2 = await client.post(url2, json=payload2, headers=headers2)
print(f"Status: {r2.status_code}")
print(f"Response: {r2.text[:300]}")
except Exception as e:
print(f"Error: {e}")
print()
# 3. 测试 Dify 健康检查
print("=== Test 3: Dify Health Check ===")
try:
r3 = await client.get("http://yw-dify.dc.servyou-it.com/")
print(f"Status: {r3.status_code}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(test_dify())
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify 多个端口"""
import httpx
import asyncio
async def test_ports():
async with httpx.AsyncClient(timeout=10) as client:
ports = [80, 8080, 3000, 5000]
for port in ports:
url = f"http://10.80.0.240:{port}/"
try:
r = await client.get(url)
print(f"Port {port}: {r.status_code}")
except Exception as e:
print(f"Port {port}: FAIL - {type(e).__name__}")
if __name__ == "__main__":
asyncio.run(test_ports())
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env python3
"""测试 Dify dify2openai 代理"""
import httpx
import asyncio
import sys
async def test_dify_proxy():
# Test the dify2openai proxy path
async with httpx.AsyncClient(timeout=15) as client:
print("=== Test dify2openai proxy ===")
url = "http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions"
headers = {
"Authorization": "Bearer http://yw-dify.dc.servyou-it.com/v1|app-7jkRkAzvX4QM9v9SM3P8mMEO|Chat",
"Content-Type": "application/json"
}
payload = {
"model": "Chat",
"messages": [{"role": "user", "content": "hello"}],
"stream": False
}
r = await client.post(url, json=payload, headers=headers)
print(f"Status: {r.status_code}")
print(f"Response: {r.text[:500]}")
if __name__ == "__main__":
asyncio.run(test_dify_proxy())
-25
View File
@@ -1,25 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.graph_query_service import get_graph_query_service
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
graph_service = await get_graph_query_service(neo4j_client)
keyword = "打印机驱动安装"
print(f"Testing find_issues_by_keyword for: {keyword}")
try:
issues = await neo4j_client.find_issues_by_keyword(keyword, limit=5)
print(f"Issues found: {len(issues)}")
for issue in issues:
print(f" - uuid: {issue.uuid}, name: {issue.name}")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
asyncio.run(test())
-48
View File
@@ -1,48 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.graph_query_service import get_graph_query_service
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
graph_service = await get_graph_query_service(neo4j_client)
keyword = "打印机驱动安装"
print(f"=== Full test for keyword: {keyword} ===")
# Step 1: find_issues_by_keyword
issues = await neo4j_client.find_issues_by_keyword(keyword, limit=5)
print(f"1. find_issues_by_keyword: {len(issues)} issues")
if not issues:
print("No issues found!")
return
issue = issues[0]
print(f" Issue: uuid={issue.uuid}, name={issue.name}")
# Step 2: find_actions_by_issue
print(f"2. Calling find_actions_by_issue with uuid={issue.uuid}")
actions = await neo4j_client.find_actions_by_issue(issue.uuid)
print(f" find_actions_by_issue: {len(actions)} actions")
if not actions:
print("No actions found!")
return
action = actions[0]
print(f" Action: name={action.name}, description={action.description[:30]}...")
# Step 3: Build result
from app.services.graph_query_service import SolutionResult
result = SolutionResult(
issue_name=issue.name,
issue_uuid=issue.uuid,
solution=action.description or action.name,
action_name=action.name,
confidence=0.8
)
print(f"3. SolutionResult: {result}")
asyncio.run(test())
-29
View File
@@ -1,29 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.graph_query_service import get_graph_query_service
from app.services.neo4j_client import get_neo4j_client
neo4j_client = await get_neo4j_client()
print(f'Neo4j client: {neo4j_client}')
if neo4j_client:
graph_service = await get_graph_query_service(neo4j_client)
print(f'Graph service: {graph_service}')
# 测试查询
question = "打印机驱动安装"
print(f'Querying: {question}')
result = await graph_service.find_solution_by_question(question)
print(f'Result: {result}')
if result:
print(f"图谱命中! action_name={result.action_name}, solution={result.solution[:50]}")
else:
print("图谱未命中")
else:
print('Neo4j client is None')
asyncio.run(test())
-9
View File
@@ -1,9 +0,0 @@
import sys
sys.path.insert(0, 'backend')
from app.services.asset_recommend_service import AssetRecommendService
svc = AssetRecommendService()
cards = svc.match_keywords('VPN连不上怎么办')
print(f'Found {len(cards)} cards')
for c in cards:
print(f' - {c.title}: {c.description}')
-9
View File
@@ -1,9 +0,0 @@
import sys
sys.path.insert(0, '/app')
from app.services.asset_recommend_service import AssetRecommendService
svc = AssetRecommendService()
cards = svc.match_keywords('VPN连不上怎么办')
print(f'Found {len(cards)} cards')
for c in cards:
print(f' - {c.title}: {c.description}')
-16
View File
@@ -1,16 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
from app.services.neo4j_client import get_neo4j_client
async def test():
client = await get_neo4j_client()
print(f'Neo4j client: {client}')
if client:
healthy = await client.health_check()
print(f'Health check: {healthy}')
else:
print('Neo4j client is None')
asyncio.run(test())
-18
View File
@@ -1,18 +0,0 @@
import asyncio
import sys
sys.path.insert(0, '/app')
async def test():
from app.services.neo4j_client import get_neo4j_client
client = await get_neo4j_client()
print(f'Neo4j client: {client}')
if client:
try:
healthy = await client.health_check()
print(f'Health check: {healthy}')
except Exception as e:
print(f'Health check error: {e}')
else:
print('Neo4j client is None - connection failed')
asyncio.run(test())
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""Test old Dify key - show full response"""
import httpx
import json
import time
base_url = "http://yw-dify.dc.servyou-it.com"
api_key = "app-UaTWYdBSwN6VktKQlbh5YN5H"
url = f"{base_url}/v1/chat-messages"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"inputs": {},
"query": "密码忘记了怎么办",
"response_mode": "blocking",
"user": "test_verify"
}
print(f"=== Testing old key with full response ===")
start = time.time()
try:
with httpx.Client(timeout=httpx.Timeout(30.0)) as client:
resp = client.post(url, headers=headers, json=payload)
elapsed = (time.time() - start) * 1000
print(f"Status: {resp.status_code}, Time: {elapsed:.0f}ms")
print()
data = resp.json()
print(f"conversation_id: {data.get('conversation_id', 'N/A')}")
answer = data.get('answer', '')
print(f"answer (first 800 chars):")
print(answer[:800])
print()
# Try JSON parse
try:
parsed = json.loads(answer)
print("=== JSON Parse: SUCCESS ===")
print(f"Keys: {list(parsed.keys())}")
for k, v in parsed.items():
print(f" {k}: {str(v)[:200]}")
except json.JSONDecodeError as e:
print(f"=== JSON Parse: FAILED ===")
print(f"Error: {e}")
print("Answer is plain text, not JSON")
except Exception as e:
print(f"Exception: {type(e).__name__}: {e}")
+9
View File
@@ -0,0 +1,9 @@
AutomationProgress-ZNcjTS_y.js mtime:Mon Jul 27 13:17:37 2026 size:17621
Bind-CQazI6pw.js mtime:Mon Jul 27 13:17:37 2026 size:4686
index-BfdW7yhB.js mtime:Mon Jul 27 13:17:37 2026 size:545
index-CKnwh2hG.js mtime:Mon Jul 27 13:17:37 2026 size:10871
index-CKUql3z2.js mtime:Mon Jul 27 13:17:37 2026 size:2021
index-CLTtV6Qh.js mtime:Mon Jul 27 13:17:37 2026 size:364021
index-D0lTVszr.js mtime:Mon Jul 27 13:17:37 2026 size:3354
Login-aOQVz0XV.js mtime:Mon Jul 27 13:17:37 2026 size:3611
WeworkOnly-DjpKNzoh.js mtime:Mon Jul 27 13:17:37 2026 size:1772
-78
View File
@@ -1,78 +0,0 @@
"""Upload large file to server via plink PTY base64 chunks."""
import subprocess
import base64
import hashlib
import sys
import os
LOCAL_FILE = r"D:\资料\03-项目开发\wecom_it_smart_desk\tmp-h5-dist.tar.gz"
REMOTE_FILE = "/tmp/h5-dist.tar.gz"
CHUNK_SIZE = 4000 # chars per chunk
PLINK = r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\plink.exe"
HOST = "sxn@10.212.189.210"
PORT = "2222"
PASSWORD = os.environ.get("JMS_PASSWORD", "") # May be cached
def run_plink(commands):
"""Run commands via plink PTY."""
cmd = [PLINK, "-P", str(PORT), "-batch", HOST]
stdin_data = "\n".join(commands) + "\nexit\n"
result = subprocess.run(
cmd,
input=stdin_data,
capture_output=True,
text=True,
timeout=300
)
return result.stdout + result.stderr
def main():
# Read file
with open(LOCAL_FILE, "rb") as f:
data = f.read()
local_md5 = hashlib.md5(data).hexdigest()
print(f"File: {LOCAL_FILE}")
print(f"Size: {len(data)} bytes")
print(f"MD5: {local_md5}")
# Base64 encode
b64 = base64.b64encode(data).decode("ascii")
total_chunks = (len(b64) + CHUNK_SIZE - 1) // CHUNK_SIZE
print(f"Base64 length: {len(b64)} chars, {total_chunks} chunks")
# Clear remote file
print("Clearing remote file...")
run_plink([f"> {REMOTE_FILE}.b64"])
# Send chunks
for i in range(total_chunks):
start = i * CHUNK_SIZE
end = min(start + CHUNK_SIZE, len(b64))
chunk = b64[start:end]
cmd = f"echo '{chunk}' >> {REMOTE_FILE}.b64"
run_plink([cmd])
if (i + 1) % 10 == 0 or i == total_chunks - 1:
print(f" Sent chunk {i+1}/{total_chunks}")
# Decode and verify
print("Decoding and verifying...")
verify_cmds = [
f"base64 -d {REMOTE_FILE}.b64 > {REMOTE_FILE}",
f"wc -c < {REMOTE_FILE}",
f"md5sum {REMOTE_FILE}",
f"rm -f {REMOTE_FILE}.b64",
]
output = run_plink(verify_cmds)
print(f"Server output:\n{output}")
if local_md5 in output:
print(f"\n✅ MD5 match! Upload successful.")
return 0
else:
print(f"\n❌ MD5 mismatch! Expected: {local_md5}")
return 1
if __name__ == "__main__":
sys.exit(main())
-87
View File
@@ -1,87 +0,0 @@
"""
大文件上传脚本 - 通过 JumpServer plink PTY 使用 base64 通道上传
使用 4000 字符的大块,比 jms_ops.py 的 500 字符快 8 倍
"""
import sys, os, base64, hashlib, time
# 导入 jms_ops 模块
SKILL_DIR = r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts"
sys.path.insert(0, SKILL_DIR)
import jms_ops
def upload_large_file(local_path, remote_path, chunk_size=4000):
"""通过 plink PTY 上传大文件,使用大块 base64 编码"""
local_data = open(local_path, 'rb').read()
local_md5 = hashlib.md5(local_data).hexdigest()
b64_data = base64.b64encode(local_data).decode('ascii')
total_chunks = (len(b64_data) + chunk_size - 1) // chunk_size
print(f"📤 上传: {local_path}{remote_path}")
print(f" 原始: {len(local_data)} bytes, base64: {len(b64_data)} chars")
print(f"{total_chunks} 块发送 (每块 {chunk_size} chars)")
# 获取 token + 启动会话
tokens = jms_ops.get_connection_tokens(1)
if not tokens:
print("❌ 获取 token 失败")
return False
token_id, token_secret = tokens[0]
session = jms_ops.PlinkSession(f"JMS-{token_id}", token_secret)
if not session.connect():
print("❌ 会话启动失败")
return False
try:
# 1. 清空目标文件
session.run_command(f'> {remote_path}', timeout=5)
# 2. 逐块追加 (大块)
start_time = time.time()
for i in range(0, len(b64_data), chunk_size):
chunk = b64_data[i:i+chunk_size]
chunk_num = i // chunk_size + 1
cmd = f"echo '{chunk}' | base64 -d >> {remote_path}"
r = session.run_command(cmd, timeout=15)
if not r["success"]:
print(f" ❌ 块 {chunk_num}/{total_chunks} 发送失败")
return False
if chunk_num % 50 == 0 or chunk_num == total_chunks:
elapsed = time.time() - start_time
pct = chunk_num / total_chunks * 100
print(f" 📦 已发送 {chunk_num}/{total_chunks} 块 ({pct:.0f}%) - {elapsed:.1f}s")
# 3. 验证大小
r = session.run_command(f'wc -c < {remote_path}', timeout=5)
if r["success"]:
remote_size = int(r["output"].strip()) if r["output"].strip().isdigit() else -1
if remote_size == len(local_data):
elapsed = time.time() - start_time
print(f" ✅ 上传成功! 大小匹配 ({remote_size} bytes), 耗时 {elapsed:.1f}s")
# 4. MD5 验证
r2 = session.run_command(f'md5sum {remote_path}', timeout=5)
if r2["success"]:
remote_md5 = r2["output"].split()[0]
if remote_md5 == local_md5:
print(f" ✅ MD5 匹配! 文件完整")
else:
print(f" ⚠️ MD5 不匹配 (本地 {local_md5[:12]}, 远程 {remote_md5[:12]})")
return True
else:
print(f" ❌ 大小不匹配 (本地 {len(local_data)}, 远程 {remote_size})")
return False
else:
print(" ⚠️ 无法验证远程文件大小")
return False
finally:
session.close()
if __name__ == '__main__':
local = r"D:\资料\03-项目开发\wecom_it_smart_desk\tmp-agent-dist.tar.gz"
remote = "/tmp/agent-dist.tar.gz"
success = upload_large_file(local, remote, chunk_size=4000)
if success:
print("\n✅ 上传完成,可以在服务器上解压了")
else:
print("\n❌ 上传失败")
sys.exit(1)
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env python3
"""Update ALL LLM nodes including 同事问题优化 with JSON output prompt."""
import json, requests, textwrap
BASE_URL = "https://yw-dify.dc.servyou-it.com"
APP_ID = "8f0f3d62-f63d-4cf3-815e-b10529c66f1d"
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNzY4ZDE2YTEtNjM5NS00YzExLWFmNmUtMjNlMGIwZjFmYTU4IiwiZXhwIjoxNzgzODg1NDE5LCJpc3MiOiJTRUxGX0hPU1RFRCIsInN1YiI6IkNvbnNvbGUgQVBJIFBhc3Nwb3J0In0.sYVPuklc92wNsZm5QILCYOuuWqemlsbkhDj7AltWJlw"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
NEW_PROMPT = textwrap.dedent('''
你是企业IT智能服务助手「Duckula」。你的职责是帮助员工解决IT问题、引导操作流程。
### 核心规则
1. **回复必须为 JSON 格式**,包含四个字段:`text`、`action`、`options`、`diagnosis_stage`
2. **文字简短**`text` 字段控制在 50 字以内,用口语化表达,像朋友聊天
3. **一次只聚焦一个问题**:不要一次性给出所有解决方案,逐步引导用户
4. **诊断阶段**:每次回复必须标注当前 `diagnosis_stage`,帮助系统判断诊断进度
### JSON 输出格式
{
"text": "简短的回复文字(50字以内)",
"action": null,
"options": null,
"diagnosis_stage": "gathering_info"
}
### diagnosis_stage 字段说明
| 值 | 含义 | 使用场景 |
|----|------|---------|
| `initial` | 初始接触 | 用户刚描述问题,AI 尚未开始诊断 |
| `gathering_info` | 信息收集中 | AI 正在通过选项/追问收集更多细节 |
| `diagnosing` | 诊断中 | 信息已足够,AI 正在分析问题原因 |
| `recommending` | 给出建议 | AI 正在提供解决方案或操作指引 |
| `resolved` | 已解决 | AI 认为问题已解决,可建议关闭会话 |
| `escalating` | 建议转人工 | AI 无法解决,建议转人工坐席 |
### 三种回复场景
#### 场景 1:审批/操作推荐(文字 + 审批卡片)
当用户表达申请意图(如"申请VPN""想换电脑"),在 `action` 中填充操作入口信息:
{
"text": "我来帮您提交VPN账号申请,请点击下方卡片。",
"action": {
"type": "approval_card",
"approval_type": "账号权限申请",
"title": "VPN 账号申请",
"description": "1-2 个工作日审批完成"
},
"options": null,
"diagnosis_stage": "recommending"
}
`action` 字段说明:
- `type`: 固定为 `"approval_card"`
- `approval_type`: 12种审批类型之一
- `title`: 卡片标题(10字以内)
- `description`: 一句话说明(20字以内)
#### 场景 2:交互式排查(文字 + 选项按钮)
当需要用户补充信息来定位问题时,在 `options` 中提供选项:
{
"text": "电脑蓝屏了?蓝屏时有错误代码吗?",
"action": null,
"options": [
{"label": "有错误代码", "value": "has_code"},
{"label": "没有", "value": "no_code"},
{"label": "不确定", "value": "unsure"}
],
"diagnosis_stage": "gathering_info"
}
`options` 字段说明:
- 最多 4 个选项
- `label`: 按钮文字(8字以内)
- `value`: 选项值(英文短标识)
- 选项应该互斥且覆盖主要可能性
#### 场景 3:纯文字回复
当不需要卡片或选项时,`action` 和 `options` 设为 `null`
{
"text": "好的,VPN账号一般1-2个工作日审批完成,届时会通过企微通知您。",
"action": null,
"options": null,
"diagnosis_stage": "resolved"
}
### 回复风格要求
- **口语化**:用"""咱们""我来帮你"等自然表达,不用"尊敬的用户"
- **简短有力**:每条回复只解决一个问题或引导一步操作
- **主动引导**:回复末尾可以带一个追问(如"具体是什么报错?"
- **不暴露技术细节**:不说"API调用失败""系统错误"等,用"我暂时没查到相关信息"代替
### 审批意图识别规则
当用户消息包含以下信号时,在 `action` 中推送审批卡片:
| 用户表达 | approval_type | action.title |
|---------|--------------|-------------|
| "申请电脑/笔记本/显示器" | 设备申请 | 设备申请 |
| "VPN/账号/权限" + "申请/开通" | 账号权限申请 | 账号权限申请 |
| "申请软件/软件授权" | 软件服务申请 | 软件服务申请 |
| "报废/送修/退还设备" | 资产处置申请 | 资产处置申请 |
| "会议室设备故障" | 会议室故障报修 | 故障报修 |
| "公共邮箱/共享邮箱" | 公共邮箱账号申请 | 公共邮箱申请 |
| "网络准入/终端准入" | 终端设备网络准入 | 网络准入申请 |
| "活动技术支持/会议保障" | 活动与会议技术支持 | 技术支持申请 |
**注意**:仅当用户有明确申请意图时才推送卡片。如果用户只是在咨询(如"VPN怎么用"),不推卡片,走正常问答。
### IT知识库问答规则
当用户提出IT问题时:
1. 利用知识库内容回答
2. 回答要简短(50字以内),不要大段复制知识库内容
3. 如果需要分步骤指导,先说第一步 + 提供选项让用户确认是否继续
4. 如果知识库中没有相关信息,诚实告知并建议转人工
### 输出约束
- **必须输出合法 JSON**,不要在 JSON 外添加任何文字
- **不要使用 markdown 代码块包裹**,直接输出 JSON 原文
- **中文引号**:JSON 字符串内使用中文内容时,字符串本身用英文双引号
- **null 处理**:无 `action` 或 `options` 时必须设为 `null`,不能省略字段
### 示例
用户:"我的VPN连不上了"
{"text": "VPN连不上了?先确认下,您是电脑端还是手机端?", "action": null, "options": [{"label": "电脑端", "value": "pc"}, {"label": "手机端", "value": "mobile"}]}
用户:"电脑端"
{"text": "好的,电脑端VPN。您用的是零信任客户端还是传统VPN?", "action": null, "options": [{"label": "零信任", "value": "zero_trust"}, {"label": "传统VPN", "value": "traditional"}, {"label": "不确定", "value": "unsure"}]}
用户:"我要申请VPN账号"
{"text": "我来帮您提交VPN账号申请,请点击下方卡片。", "action": {"type": "approval_card", "approval_type": "账号权限申请", "title": "VPN账号申请", "description": "1-2个工作日审批完成"}, "options": null}
用户:"打印机连不上"
{"text": "打印机连不上?是网络打印机还是USB直连的?", "action": null, "options": [{"label": "网络打印机", "value": "network"}, {"label": "USB直连", "value": "usb"}, {"label": "不确定", "value": "unsure"}]}
用户:"谢谢"
{"text": "不客气!有问题随时找我~", "action": null, "options": null}
用户:"电脑蓝屏了"
{"text": "电脑蓝屏了?别急,蓝屏时有错误代码吗?", "action": null, "options": [{"label": "有错误代码", "value": "has_code"}, {"label": "没有", "value": "no_code"}, {"label": "不确定", "value": "unsure"}]}
用户:"密码忘了"
{"text": "密码忘了?是企微密码还是电脑开机密码?", "action": null, "options": [{"label": "企微密码", "value": "wecom"}, {"label": "电脑密码", "value": "pc"}, {"label": "邮箱密码", "value": "email"}]}
用户:"企微密码"
{"text": "企微密码可以自助重置,请点击下方卡片。", "action": {"type": "approval_card", "approval_type": "账号权限申请", "title": "密码重置", "description": "自助重置或提交申请"}, "options": null}
''').strip()
def main():
# Get current workflow
url = f"{BASE_URL}/console/api/apps/{APP_ID}/workflows/draft"
r = requests.get(url, headers=HEADERS, timeout=30)
print(f"GET workflow draft: {r.status_code}")
r.raise_for_status()
workflow = r.json()
# Update ALL LLM nodes with system prompt
nodes = workflow.get("graph", {}).get("nodes", [])
updated_count = 0
for node in nodes:
data = node.get("data", {})
if data.get("type") == "llm":
title = data.get("title", "")
prompt_template = data.get("prompt_template", [])
for pt in prompt_template:
if pt.get("role") == "system":
old_len = len(pt.get("text", ""))
pt["text"] = NEW_PROMPT
updated_count += 1
print(f" Updated '{title}' ({node['id']}): {old_len} -> {len(NEW_PROMPT)} chars")
break
print(f"\nTotal LLM nodes updated: {updated_count}")
# Save workflow
print("\nSaving workflow draft...")
r = requests.post(url, headers=HEADERS, json=workflow, timeout=30)
print(f"POST workflow draft: {r.status_code}")
r.raise_for_status()
print("Saved!")
if __name__ == "__main__":
main()
-311
View File
@@ -1,311 +0,0 @@
#!/usr/bin/env python3
"""Update Dify app system prompt to JSON output format via Console API."""
import json, requests, sys, textwrap
BASE_URL = "https://yw-dify.dc.servyou-it.com"
APP_ID = "8f0f3d62-f63d-4cf3-815e-b10529c66f1d"
TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNzY4ZDE2YTEtNjM5NS00YzExLWFmNmUtMjNlMGIwZjFmYTU4IiwiZXhwIjoxNzgzODg1NDE5LCJpc3MiOiJTRUxGX0hPU1RFRCIsInN1YiI6IkNvbnNvbGUgQVBJIFBhc3Nwb3J0In0.sYVPuklc92wNsZm5QILCYOuuWqemlsbkhDj7AltWJlw"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
# New JSON output system prompt
NEW_PROMPT = textwrap.dedent('''
你是企业IT智能服务助手「Duckula」。你的职责是帮助员工解决IT问题、引导操作流程。
### 核心规则
1. **回复必须为 JSON 格式**,包含七个字段:`text`、`action`、`options`、`diagnosis_stage`、`intent_type`、`business_category`、`routing_confidence`
2. **文字简短**`text` 字段控制在 50 字以内,用口语化表达,像朋友聊天
3. **一次只聚焦一个问题**:不要一次性给出所有解决方案,逐步引导用户
4. **诊断阶段**:每次回复必须标注当前 `diagnosis_stage`,帮助系统判断诊断进度
5. **路由意图标注**:每次回复必须判断消息是否属于非IT业务,填写 `intent_type` 等三个路由字段
### JSON 输出格式
{
"text": "简短的回复文字(50字以内)",
"action": null,
"options": null,
"diagnosis_stage": "gathering_info",
"intent_type": "it_consult",
"business_category": null,
"routing_confidence": 0.0
}
### diagnosis_stage 字段说明
| 值 | 含义 | 使用场景 |
|----|------|---------|
| `initial` | 初始接触 | 用户刚描述问题,AI 尚未开始诊断 |
| `gathering_info` | 信息收集中 | AI 正在通过选项/追问收集更多细节 |
| `diagnosing` | 诊断中 | 信息已足够,AI 正在分析问题原因 |
| `recommending` | 给出建议 | AI 正在提供解决方案或操作指引 |
| `resolved` | 已解决 | AI 认为问题已解决,可建议关闭会话 |
| `escalating` | 建议转人工 | AI 无法解决,建议转人工坐席 |
### 路由意图字段说明(intent_type / business_category / routing_confidence
**intent_type** 四选一:
| 值 | 含义 | 判定标准 |
|----|------|---------|
| `approval` | 审批请求 | 用户想申请 VPN/设备/权限/软件等 |
| `it_consult` | IT咨询 | 电脑/网络/系统/账号等 IT 问题 |
| `non_it_routing` | 非IT业务 | 行政/人力资源/财务/法务/物业类问题 |
| `chitchat` | 闲聊 | 打招呼、闲聊、无关内容 |
**business_category**(仅 intent_type=non_it_routing 时填写,否则 null):
| 值 | 覆盖关键词示例 |
|----|---------------|
| `行政` | 打印机、复印机、扫描仪、保洁、名片印刷 |
| `人力资源` | 工牌、考勤、入职、离职、社保、公积金 |
| `财务` | 报销、发票、工资、付款 |
| `法务` | 合同、协议、盖章、律师 |
| `行政-物业` | 空调、灯、门禁卡、车位、物业维修 |
**routing_confidence**0.0~1.0 置信度。明确属于某业务类别给 0.8 以上;不确定给 0.5 以下。
**注意**intent_type=non_it_routing 时,`text` 仍正常回复用户(如"这个问题属于行政范畴"),`action` 填 null,系统会自动推荐对应业务联系人。
### 三种回复场景
#### 场景 1:审批/操作推荐(文字 + 审批卡片)
当用户表达申请意图(如"申请VPN""想换电脑"),在 `action` 中填充操作入口信息:
{
"text": "我来帮您提交VPN账号申请,请点击下方卡片。",
"action": {
"type": "approval_card",
"approval_type": "账号权限申请",
"title": "VPN账号申请",
"description": "1-2 个工作日审批完成"
},
"options": null,
"diagnosis_stage": "recommending",
"intent_type": "approval",
"business_category": null,
"routing_confidence": 0.0
}
`action` 字段说明:
- `type`: 固定为 `"approval_card"`
- `approval_type`: 12种审批类型之一
- `title`: 卡片标题(10字以内)
- `description`: 一句话说明(20字以内)
#### 场景 2:交互式排查(文字 + 选项按钮)
当需要用户补充信息来定位问题时,在 `options` 中提供选项:
{
"text": "电脑蓝屏了?蓝屏时有错误代码吗?",
"action": null,
"options": [
{"label": "有错误代码", "value": "has_code"},
{"label": "没有", "value": "no_code"},
{"label": "不确定", "value": "unsure"}
],
"diagnosis_stage": "gathering_info",
"intent_type": "it_consult",
"business_category": null,
"routing_confidence": 0.0
}
`options` 字段说明:
- 最多 4 个选项
- `label`: 按钮文字(8字以内)
- `value`: 选项值(英文短标识)
- 选项应该互斥且覆盖主要可能性
#### 场景 3:纯文字回复
当不需要卡片或选项时,`action` 和 `options` 设为 `null`
{
"text": "好的,VPN账号一般1-2个工作日审批完成,届时会通过企微通知您。",
"action": null,
"options": null,
"diagnosis_stage": "resolved",
"intent_type": "approval",
"business_category": null,
"routing_confidence": 0.0
}
#### 场景 4:非IT业务路由(D1 合并新增)
当用户消息属于行政/人力/财务/法务/物业类非IT业务时,标注 `intent_type=non_it_routing`
用户:"打印机坏了,行政那边谁负责?"
{
"text": "打印机问题属于行政范畴,我为您推荐行政联系人。",
"action": null,
"options": null,
"diagnosis_stage": "recommending",
"intent_type": "non_it_routing",
"business_category": "行政",
"routing_confidence": 0.9
}
用户:"工牌丢了怎么补办?"
{
"text": "工牌补办属于人力资源业务,我为您推荐人事联系人。",
"action": null,
"options": null,
"diagnosis_stage": "recommending",
"intent_type": "non_it_routing",
"business_category": "人力资源",
"routing_confidence": 0.9
}
### 回复风格要求
- **口语化**:用"""咱们""我来帮你"等自然表达,不用"尊敬的用户"
- **简短有力**:每条回复只解决一个问题或引导一步操作
- **主动引导**:回复末尾可以带一个追问(如"具体是什么报错?"
- **不暴露技术细节**:不说"API调用失败""系统错误"等,用"我暂时没查到相关信息"代替
### 审批意图识别规则
当用户消息包含以下信号时,在 `action` 中推送审批卡片:
| 用户表达 | approval_type | action.title |
|---------|--------------|-------------|
| "申请电脑/笔记本/显示器" | 设备申请 | 设备申请 |
| "VPN/账号/权限" + "申请/开通" | 账号权限申请 | 账号权限申请 |
| "申请软件/软件授权" | 软件服务申请 | 软件服务申请 |
| "报废/送修/退还设备" | 资产处置申请 | 资产处置申请 |
| "会议室设备故障" | 会议室故障报修 | 故障报修 |
| "公共邮箱/共享邮箱" | 公共邮箱账号申请 | 公共邮箱申请 |
| "网络准入/终端准入" | 终端设备网络准入 | 网络准入申请 |
| "活动技术支持/会议保障" | 活动与会议技术支持 | 技术支持申请 |
**注意**:仅当用户有明确申请意图时才推送卡片。如果用户只是在咨询(如"VPN怎么用"),不推卡片,走正常问答。
### IT知识库问答规则
当用户提出IT问题时:
1. 利用知识库内容回答
2. 回答要简短(50字以内),不要大段复制知识库内容
3. 如果需要分步骤指导,先说第一步 + 提供选项让用户确认是否继续
4. 如果知识库中没有相关信息,诚实告知并建议转人工
### 输出约束
- **必须输出合法 JSON**,不要在 JSON 外添加任何文字
- **不要使用 markdown 代码块包裹**,直接输出 JSON 原文
- **中文引号**:JSON 字符串内使用中文内容时,字符串本身用英文双引号
- **null 处理**:无 `action` 或 `options` 时必须设为 `null`,不能省略字段
### 示例
用户:"我的VPN连不上了"
{"text": "VPN连不上了?先确认下,您是电脑端还是手机端?", "action": null, "options": [{"label": "电脑端", "value": "pc"}, {"label": "手机端", "value": "mobile"}], "diagnosis_stage": "gathering_info", "intent_type": "it_consult", "business_category": null, "routing_confidence": 0.0}
用户:"电脑端"
{"text": "好的,电脑端VPN。您用的是零信任客户端还是传统VPN?", "action": null, "options": [{"label": "零信任", "value": "zero_trust"}, {"label": "传统VPN", "value": "traditional"}, {"label": "不确定", "value": "unsure"}], "diagnosis_stage": "gathering_info", "intent_type": "it_consult", "business_category": null, "routing_confidence": 0.0}
用户:"我要申请VPN账号"
{"text": "我来帮您提交VPN账号申请,请点击下方卡片。", "action": {"type": "approval_card", "approval_type": "账号权限申请", "title": "VPN账号申请", "description": "1-2个工作日审批完成"}, "options": null, "diagnosis_stage": "recommending", "intent_type": "approval", "business_category": null, "routing_confidence": 0.0}
用户:"打印机连不上"
{"text": "打印机连不上?是网络打印机还是USB直连的?", "action": null, "options": [{"label": "网络打印机", "value": "network"}, {"label": "USB直连", "value": "usb"}, {"label": "不确定", "value": "unsure"}], "diagnosis_stage": "gathering_info", "intent_type": "it_consult", "business_category": null, "routing_confidence": 0.0}
用户:"谢谢"
{"text": "不客气!有问题随时找我~", "action": null, "options": null, "diagnosis_stage": "resolved", "intent_type": "chitchat", "business_category": null, "routing_confidence": 0.0}
用户:"电脑蓝屏了"
{"text": "电脑蓝屏了?别急,蓝屏时有错误代码吗?", "action": null, "options": [{"label": "有错误代码", "value": "has_code"}, {"label": "没有", "value": "no_code"}, {"label": "不确定", "value": "unsure"}], "diagnosis_stage": "gathering_info", "intent_type": "it_consult", "business_category": null, "routing_confidence": 0.0}
用户:"密码忘了"
{"text": "密码忘了?是企微密码还是电脑开机密码?", "action": null, "options": [{"label": "企微密码", "value": "wecom"}, {"label": "电脑密码", "value": "pc"}, {"label": "邮箱密码", "value": "email"}], "diagnosis_stage": "gathering_info", "intent_type": "it_consult", "business_category": null, "routing_confidence": 0.0}
用户:"企微密码"
{"text": "企微密码可以自助重置,请点击下方卡片。", "action": {"type": "approval_card", "approval_type": "账号权限申请", "title": "密码重置", "description": "自助重置或提交申请"}, "options": null, "diagnosis_stage": "recommending", "intent_type": "approval", "business_category": null, "routing_confidence": 0.0}
用户:"工牌丢了怎么补办?"
{"text": "工牌补办属于人力资源业务,我为您推荐人事联系人。", "action": null, "options": null, "diagnosis_stage": "recommending", "intent_type": "non_it_routing", "business_category": "人力资源", "routing_confidence": 0.9}
用户:"报销流程怎么走?"
{"text": "报销属于财务业务范畴,我为您推荐财务联系人。", "action": null, "options": null, "diagnosis_stage": "recommending", "intent_type": "non_it_routing", "business_category": "财务", "routing_confidence": 0.9}
''').strip()
def get_workflow():
"""Get current workflow draft."""
url = f"{BASE_URL}/console/api/apps/{APP_ID}/workflows/draft"
r = requests.get(url, headers=HEADERS, timeout=30)
print(f"GET workflow draft: {r.status_code}")
r.raise_for_status()
return r.json()
def update_llm_prompt(workflow, new_prompt):
"""Find the main LLM node and update its system prompt."""
# The main LLM nodes that generate final answers have titles like "本地大模型分析"
# We target the one that feeds into the final answer/整合回复 node
nodes = workflow.get("graph", {}).get("nodes", [])
updated_count = 0
for node in nodes:
data = node.get("data", {})
if data.get("type") == "llm":
title = data.get("title", "")
# Target the main analysis LLM nodes
if "本地大模型分析" in title:
prompt_template = data.get("prompt_template", [])
for pt in prompt_template:
if pt.get("role") == "system":
old_text = pt.get("text", "")
print(f"Found LLM node '{title}' (id={node['id']}), system prompt length: {len(old_text)}")
pt["text"] = new_prompt
updated_count += 1
print(f" -> Updated to new prompt (length: {len(new_prompt)})")
break
return updated_count
def save_workflow(workflow):
"""Save workflow draft."""
url = f"{BASE_URL}/console/api/apps/{APP_ID}/workflows/draft"
r = requests.post(url, headers=HEADERS, json=workflow, timeout=30)
print(f"POST workflow draft: {r.status_code}")
if r.status_code != 200:
print(f"Error: {r.text[:500]}")
r.raise_for_status()
return r.json()
def publish_app():
"""Publish the app to make changes live."""
url = f"{BASE_URL}/console/api/apps/{APP_ID}/publish"
r = requests.post(url, headers=HEADERS, timeout=30)
print(f"POST publish: {r.status_code}")
if r.status_code != 200:
print(f"Error: {r.text[:500]}")
r.raise_for_status()
return r.json()
def main():
print("=== Step 1: Get workflow draft ===")
workflow = get_workflow()
print("\n=== Step 2: Update LLM system prompt ===")
count = update_llm_prompt(workflow, NEW_PROMPT)
print(f"Updated {count} LLM node(s)")
if count == 0:
print("ERROR: No LLM nodes found to update!")
sys.exit(1)
print("\n=== Step 3: Save workflow draft ===")
save_workflow(workflow)
print("\n=== Step 4: Publish app ===")
publish_app()
print("\n✅ All done! Dify app updated and published.")
if __name__ == "__main__":
main()
-2
View File
@@ -1,2 +0,0 @@
UPDATE agents SET mfa_enabled = true, mfa_secret = 'JBSWY3DPEHPK3PXP' WHERE user_id = 'sxn';
SELECT user_id, mfa_enabled, mfa_secret FROM agents WHERE user_id = 'sxn';
-1
View File
@@ -1 +0,0 @@
UPDATE agents SET mfa_enabled = true, mfa_secret = 'JBSWY3DPEHPK3PXP' WHERE user_id = 'sxn'
-14
View File
@@ -1,14 +0,0 @@
import sys, json
d = json.load(sys.stdin)
nodes = d.get('graph', {}).get('nodes', [])
llms = [n for n in nodes if n.get('data', {}).get('type') == 'llm']
print(f'Total LLM nodes: {len(llms)}')
for n in llms:
title = n['data']['title']
sys_prompt = [p for p in n['data'].get('prompt_template', []) if p.get('role') == 'system']
if sys_prompt:
text = sys_prompt[0].get('text', '')
print(f' {title}: sys_prompt_len={len(text)}, contains_json={"JSON" in text or "json" in text}')
else:
print(f' {title}: NO system prompt')