diff --git a/.gitignore b/.gitignore index e29a7d4..d441386 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ wecom-it-desk-server-deploy.zip .workbuddy/logs/ .workbuddy/*.log .workbuddy/*.log.err +# workbuddy 记忆目录(个人上下文,不 入仓) +.workbuddy/memory/ diff --git a/README.md b/README.md index 25e0608..ce53283 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 企微 IT 智能服务台 (IT Smart Desk) +# 企微智能IT支持服务台 (IT Smart Desk) > **环境状态**: 预生产(独立主机,共享域名)→ 正式环境迁移 K8s > **维护者**: 税友集团 IT支持组(宋献) diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 5de8486..2c73cab 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -36,6 +36,7 @@ from app.models.agent import Agent from app.schemas.agent import AgentLogin, AgentResponse, AgentStatusUpdate from app.services.wecom_service import WecomService from app.utils.response import AppException, ERR_UNAUTHORIZED, success_response +from app.utils.error_codes import ErrorCode # 速率限制器实例(与 main.py 共享同一配置) # 移除 env_file=None 参数:slowapi 0.1.9 不支持该参数 @@ -217,24 +218,18 @@ async def agent_login( logger.warning( f"企微API不可达,已注册坐席降级放行: user_id={body.user_id}" ) - # P1 修复: 降级放行时,如果 agent 有 password_hash 则必须验证本地密码 - if existing_agent and existing_agent.password_hash: + # P0 修复: 降级放行时,如果 agent 已设置密码则必须验证本地密码 + if existing_agent: + if existing_agent.password_hash is None: + # 已注册坐席但未设置密码,要求先设置密码 + raise AppException( + 1012, + "首次登录请先设置密码。管理后台 → 坐席管理 → 设置本地密码" + ) if not body.password: - raise AppException(1011, "请输入本地密码") + raise AppException(ErrorCode.AUTH_PASSWORD_WRONG, "请输入本地密码") if not bcrypt.checkpw(body.password.encode('utf-8'), existing_agent.password_hash.encode('utf-8')): - raise AppException(1011, "本地密码错误") - - # P0-#5: 本地密码认证(企微验证失败时的备用认证) - # 检查是否需要本地密码验证 - local_password_verified = False - if body.password and agent and agent.password_hash: - # 验证本地密码 - if bcrypt.checkpw(body.password.encode('utf-8'), agent.password_hash.encode('utf-8')): - local_password_verified = True - logger.info(f"本地密码验证通过: user_id={body.user_id}") - else: - # 本地密码错误,拒绝登录 - raise AppException(1011, "本地密码错误") + raise AppException(ErrorCode.AUTH_PASSWORD_WRONG, "本地密码错误") # 1. 查找或创建坐席记录 stmt = select(Agent).where(Agent.user_id == body.user_id) @@ -571,9 +566,11 @@ async def update_agent_password( # 如果已有旧密码,验证旧密码 if agent.password_hash: if not body.old_password: - raise AppException(1012, "请输入旧密码") + # 2026-06-15 修复: 改用专用 ErrorCode,避免与登录 1012 冲突 + raise AppException(ErrorCode.AUTH_OLD_PASSWORD_REQUIRED, "请输入旧密码") if not bcrypt.checkpw(body.old_password.encode('utf-8'), agent.password_hash.encode('utf-8')): - raise AppException(1013, "旧密码错误") + # 2026-06-15 修复: 改用专用 ErrorCode + raise AppException(ErrorCode.AUTH_OLD_PASSWORD_WRONG, "旧密码错误") # 设置新密码 agent.password_hash = bcrypt.hashpw(body.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') diff --git a/backend/app/api/approval.py b/backend/app/api/approval.py index 1966de4..cc3a635 100644 --- a/backend/app/api/approval.py +++ b/backend/app/api/approval.py @@ -16,23 +16,36 @@ router = APIRouter() # 审批模板配置(可配置化,后续可存入数据库) # ============================================================================= -# 企微审批模板配置 -APPROVAL_TEMPLATES = { - # 模板124 - 资源申请(跳转审批) - "Bs7ucTLPo42dtj8Y1LzBoujijsa6geRWaRxZJjk4X": { - "id": "Bs7ucTLPo42dtj8Y1LzBoujijsa6geRWaRxZJjk4X", +# ============================================================================= +# 企微审批模板配置(从环境变量读取) +# ============================================================================= +# 环境变量: +# APPROVAL_TEMPLATE_RESOURCE - 资源申请模板ID +# APPROVAL_TEMPLATE_DEVICE - 设备申请模板ID + +import os + +APPROVAL_TEMPLATE_RESOURCE = os.getenv("APPROVAL_TEMPLATE_RESOURCE", "") +APPROVAL_TEMPLATE_DEVICE = os.getenv("APPROVAL_TEMPLATE_DEVICE", "") + +# 动态构建审批模板配置 +APPROVAL_TEMPLATES = {} + +if APPROVAL_TEMPLATE_RESOURCE: + APPROVAL_TEMPLATES[APPROVAL_TEMPLATE_RESOURCE] = { + "id": APPROVAL_TEMPLATE_RESOURCE, "name": "资源申请", "type": "jump", # 跳转审批 "keywords": ["申请资源", "要资源", "申请"], - }, - # 模板122 - 设备申请(API提交) - "Bs7ucTGsPuFhxfk8pn8EydxrWxkVetB4JR8Pb6PHS": { - "id": "Bs7ucTGsPuFhxfk8pn8EydxrWxkVetB4JR8Pb6PHS", + } + +if APPROVAL_TEMPLATE_DEVICE: + APPROVAL_TEMPLATES[APPROVAL_TEMPLATE_DEVICE] = { + "id": APPROVAL_TEMPLATE_DEVICE, "name": "设备申请", "type": "api", # API提交 "keywords": ["申请设备", "要设备", "电脑", "笔记本"], - }, -} + } # ============================================================================= diff --git a/backend/app/config.py b/backend/app/config.py index bec15aa..41a73da 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -99,6 +99,14 @@ class Settings(BaseSettings): # 是否启用 Mock 登录(默认 false,生产环境必须关闭) mock_login_enabled: bool = False + # ---------------------------------------------------------------------- + # 审批模板配置(企微审批应用) + # ---------------------------------------------------------------------- + # 资源申请审批模板ID(在企微审批应用设置中获取) + approval_template_resource: str = "" + # 设备申请审批模板ID(在企微审批应用设置中获取) + approval_template_device: str = "" + # ---------------------------------------------------------------------- # Pydantic-settings 配置 # ---------------------------------------------------------------------- diff --git a/backend/app/main.py b/backend/app/main.py index 4720ebe..a8205f0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -15,7 +15,9 @@ import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text # 导入配置(读取环境变量) from app.config import settings @@ -514,6 +516,79 @@ def create_app() -> FastAPI: """ return {"status": "ok", "service": "wecom-it-smart-desk"} + @app.get("/ready", tags=["系统"]) + async def readiness_check(): + """就绪检查端点。 + + 检查服务依赖(DB + Redis),不调用企微 API(避免阻塞)。 + 用于 K8s readinessProbe。 + """ + try: + # 检查数据库 + from app.database import engine + async with engine.connect() as conn: + await conn.execute(text("SELECT 1")) + db_status = "ok" + except Exception as e: + db_status = f"error: {str(e)}" + + try: + # 检查 Redis + from app.config import get_settings + settings = get_settings() + redis_client = settings.create_redis_client() + await redis_client.ping() + redis_status = "ok" + except Exception as e: + redis_status = f"error: {str(e)}" + + if db_status == "ok" and redis_status == "ok": + return {"status": "ready", "db": db_status, "redis": redis_status} + else: + return JSONResponse( + status_code=503, + content={"status": "not_ready", "db": db_status, "redis": redis_status} + ) + + @app.get("/metrics", tags=["系统"]) + async def metrics(): + """指标端点。 + + 返回服务运行指标,用于 Prometheus 采集。 + """ + import psutil + + return { + "status": "ok", + "metrics": { + "cpu_percent": psutil.cpu_percent(interval=0.1), + "memory_percent": psutil.virtual_memory().percent, + "disk_percent": psutil.disk_usage("/").percent, + } + } + + @app.get("/version", tags=["系统"]) + async def version(): + """版本信息端点。 + + 返回服务版本信息。 + """ + import subprocess + try: + git_hash = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + cwd=app_root, + text=True + ).strip()[:8] + except Exception: + git_hash = "unknown" + + return { + "service": "wecom-it-smart-desk", + "version": "1.1.0", + "build": git_hash, + } + # ---------------------------------------------------------------------- # 打印所有已注册的路由(调试用) # ---------------------------------------------------------------------- diff --git a/backend/app/utils/error_codes.py b/backend/app/utils/error_codes.py new file mode 100644 index 0000000..2f6dcf7 --- /dev/null +++ b/backend/app/utils/error_codes.py @@ -0,0 +1,158 @@ +# ============================================================================= +# IT智能服务台 — 错误码定义 +# ============================================================================= +# 说明:统一管理系统错误码,便于前端解析和国际化 +# 格式:E{模块}{序号} +# ============================================================================= + +from enum import Enum + + +class ErrorCode(str, Enum): + """系统错误码枚举""" + + # -------------------------------------------------------------------------- + # 通用错误 (0xxx) + # -------------------------------------------------------------------------- + SUCCESS = "E0000" # 成功 + UNKNOWN_ERROR = "E0001" # 未知错误 + INVALID_PARAMETER = "E0002" # 参数错误 + MISSING_PARAMETER = "E0003" # 缺少参数 + NOT_FOUND = "E0004" # 资源不存在 + UNAUTHORIZED = "E0005" # 未授权 + FORBIDDEN = "E0006" # 禁止访问 + INTERNAL_ERROR = "E0007" # 内部错误 + SERVICE_UNAVAILABLE = "E0008" # 服务不可用 + TIMEOUT = "E0009" # 请求超时 + + # -------------------------------------------------------------------------- + # 认证相关 (1xxx) + # -------------------------------------------------------------------------- + AUTH_FAILED = "E1001" # 认证失败 + AUTH_TOKEN_EXPIRED = "E1002" # Token过期 + AUTH_TOKEN_INVALID = "E1003" # Token无效 + AUTH_PASSWORD_REQUIRED = "E1012" # 登录:首次登录请先设置密码 + AUTH_PASSWORD_WRONG = "E1011" # 登录:本地密码错误 + AUTH_OLD_PASSWORD_REQUIRED = "E1015" # 改密:请输入旧密码(2026-06-15 WB反馈 1012 上下文冲突后拆分) + AUTH_OLD_PASSWORD_WRONG = "E1016" # 改密:旧密码错误(2026-06-15 拆分) + + # -------------------------------------------------------------------------- + # 企微API错误 (2xxx) + # -------------------------------------------------------------------------- + WECOM_API_ERROR = "E2001" # 企微API调用失败 + WECOM_API_TIMEOUT = "E2002" # 企微API超时 + WECOM_TOKEN_INVALID = "E2003" # 企微token无效 + WECOM_USER_NOT_FOUND = "E2004" # 企微用户不存在 + + # -------------------------------------------------------------------------- + # 会话/消息错误 (3xxx) + # -------------------------------------------------------------------------- + CONVERSATION_NOT_FOUND = "E3001" # 会话不存在 + MESSAGE_NOT_FOUND = "E3002" # 消息不存在 + MESSAGE_TOO_LONG = "E3003" # 消息过长 + CONVERSATION_CLOSED = "E3004" # 会话已关闭 + + # -------------------------------------------------------------------------- + # 坐席错误 (4xxx) + # -------------------------------------------------------------------------- + AGENT_NOT_FOUND = "E4001" # 坐席不存在 + AGENT_OFFLINE = "E4002" # 坐席不在线 + AGENT_BUSY = "E4003" # 坐席忙碌 + AGENT_MAX_LOAD = "E4004" # 坐席已达最大接待量 + + # -------------------------------------------------------------------------- + # 审批错误 (5xxx) + # -------------------------------------------------------------------------- + APPROVAL_TEMPLATE_NOT_FOUND = "E5001" # 审批模板不存在 + APPROVAL_FAILED = "E5002" # 审批提交失败 + + # -------------------------------------------------------------------------- + # 文件上传错误 (6xxx) + # -------------------------------------------------------------------------- + FILE_TOO_LARGE = "E6001" # 文件过大 + FILE_TYPE_NOT_ALLOWED = "E6002" # 文件类型不允许 + FILE_UPLOAD_FAILED = "E6003" # 文件上传失败 + + +# 错误码到 HTTP 状态码的映射 +ERROR_CODE_TO_STATUS = { + ErrorCode.SUCCESS: 200, + ErrorCode.INVALID_PARAMETER: 400, + ErrorCode.MISSING_PARAMETER: 400, + ErrorCode.NOT_FOUND: 404, + ErrorCode.UNAUTHORIZED: 401, + ErrorCode.FORBIDDEN: 403, + ErrorCode.INTERNAL_ERROR: 500, + ErrorCode.SERVICE_UNAVAILABLE: 503, + # 认证 + ErrorCode.AUTH_FAILED: 401, + ErrorCode.AUTH_TOKEN_EXPIRED: 401, + ErrorCode.AUTH_TOKEN_INVALID: 401, + ErrorCode.AUTH_PASSWORD_REQUIRED: 401, + ErrorCode.AUTH_PASSWORD_WRONG: 401, + ErrorCode.AUTH_OLD_PASSWORD_REQUIRED: 400, + ErrorCode.AUTH_OLD_PASSWORD_WRONG: 400, + # 企微 + ErrorCode.WECOM_API_ERROR: 502, + ErrorCode.WECOM_API_TIMEOUT: 504, + ErrorCode.WECOM_TOKEN_INVALID: 401, + ErrorCode.WECOM_USER_NOT_FOUND: 404, + # 会话 + ErrorCode.CONVERSATION_NOT_FOUND: 404, + ErrorCode.MESSAGE_NOT_FOUND: 404, + ErrorCode.MESSAGE_TOO_LONG: 400, + ErrorCode.CONVERSATION_CLOSED: 400, + # 坐席 + ErrorCode.AGENT_NOT_FOUND: 404, + ErrorCode.AGENT_OFFLINE: 400, + ErrorCode.AGENT_BUSY: 400, + ErrorCode.AGENT_MAX_LOAD: 400, + # 审批 + ErrorCode.APPROVAL_TEMPLATE_NOT_FOUND: 404, + ErrorCode.APPROVAL_FAILED: 502, + # 文件 + ErrorCode.FILE_TOO_LARGE: 413, + ErrorCode.FILE_TYPE_NOT_ALLOWED: 400, + ErrorCode.FILE_UPLOAD_FAILED: 500, +} + + +def get_error_message(code: ErrorCode) -> str: + """获取错误码对应的默认消息""" + messages = { + ErrorCode.SUCCESS: "操作成功", + ErrorCode.UNKNOWN_ERROR: "未知错误,请稍后重试", + ErrorCode.INVALID_PARAMETER: "参数错误", + ErrorCode.MISSING_PARAMETER: "缺少必要参数", + ErrorCode.NOT_FOUND: "资源不存在", + ErrorCode.UNAUTHORIZED: "未授权,请先登录", + ErrorCode.FORBIDDEN: "禁止访问", + ErrorCode.INTERNAL_ERROR: "服务器内部错误", + ErrorCode.SERVICE_UNAVAILABLE: "服务暂时不可用", + ErrorCode.TIMEOUT: "请求超时", + ErrorCode.AUTH_FAILED: "认证失败", + ErrorCode.AUTH_TOKEN_EXPIRED: "登录已过期,请重新登录", + ErrorCode.AUTH_TOKEN_INVALID: "无效的登录凭证", + ErrorCode.AUTH_PASSWORD_REQUIRED: "首次登录请先设置密码", + ErrorCode.AUTH_PASSWORD_WRONG: "密码错误", + ErrorCode.AUTH_OLD_PASSWORD_REQUIRED: "请输入旧密码", + ErrorCode.AUTH_OLD_PASSWORD_WRONG: "旧密码错误", + ErrorCode.WECOM_API_ERROR: "企业微信服务异常", + ErrorCode.WECOM_API_TIMEOUT: "企业微信服务响应超时", + ErrorCode.WECOM_TOKEN_INVALID: "企业微信凭证无效", + ErrorCode.WECOM_USER_NOT_FOUND: "企业微信用户不存在", + ErrorCode.CONVERSATION_NOT_FOUND: "会话不存在", + ErrorCode.MESSAGE_NOT_FOUND: "消息不存在", + ErrorCode.MESSAGE_TOO_LONG: "消息内容过长", + ErrorCode.CONVERSATION_CLOSED: "会话已结束", + ErrorCode.AGENT_NOT_FOUND: "坐席不存在", + ErrorCode.AGENT_OFFLINE: "坐席不在线", + ErrorCode.AGENT_BUSY: "坐席忙碌中", + ErrorCode.AGENT_MAX_LOAD: "坐席已达到最大接待量", + ErrorCode.APPROVAL_TEMPLATE_NOT_FOUND: "审批模板不存在", + ErrorCode.APPROVAL_FAILED: "审批提交失败", + ErrorCode.FILE_TOO_LARGE: "文件过大", + ErrorCode.FILE_TYPE_NOT_ALLOWED: "不支持的文件类型", + ErrorCode.FILE_UPLOAD_FAILED: "文件上传失败", + } + return messages.get(code, "未知错误") diff --git a/backend/app/utils/logging_config.py b/backend/app/utils/logging_config.py new file mode 100644 index 0000000..05b98a2 --- /dev/null +++ b/backend/app/utils/logging_config.py @@ -0,0 +1,99 @@ +# ============================================================================= +# IT智能服务台 — 日志配置 +# ============================================================================= +# 说明:统一日志格式,支持 JSON 输出便于日志收集 +# ============================================================================= + +import json +import logging +import sys +from datetime import datetime +from typing import Any + + +class JSONFormatter(logging.Formatter): + """JSON 格式日志 formatter""" + + def format(self, record: logging.LogRecord) -> str: + """将日志记录格式化为 JSON""" + log_data: dict[str, Any] = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + "module": record.module, + "function": record.funcName, + "line": record.lineno, + } + + # 添加异常信息 + if record.exc_info: + log_data["exception"] = self.formatException(record.exc_info) + + # 添加额外字段 + if hasattr(record, "request_id"): + log_data["request_id"] = record.request_id + if hasattr(record, "user_id"): + log_data["user_id"] = record.user_id + if hasattr(record, "extra"): + log_data.update(record.extra) + + return json.dumps(log_data, ensure_ascii=False) + + +class PlainFormatter(logging.Formatter): + """普通格式日志 formatter(开发环境使用)""" + + def __init__(self): + super().__init__( + fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging(level: str = "INFO", json_format: bool = False) -> None: + """配置日志系统 + + Args: + level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL) + json_format: 是否使用 JSON 格式输出 + """ + log_level = getattr(logging, level.upper(), logging.INFO) + + # 获取 root logger + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + # 清除现有 handlers + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 创建 console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(log_level) + + # 设置 formatter + if json_format: + formatter = JSONFormatter() + else: + formatter = PlainFormatter() + + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # 设置第三方库日志级别 + logging.getLogger("uvicorn").setLevel(logging.WARNING) + logging.getLogger("fastapi").setLevel(logging.WARNING) + logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) + + +def get_logger(name: str) -> logging.Logger: + """获取 logger 实例 + + Args: + name: logger 名称,通常使用 __name__ + + Returns: + Logger 实例 + """ + return logging.getLogger(name) diff --git a/backend/requirements.txt b/backend/requirements.txt index d380190..693a37d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,11 +9,11 @@ # Web 框架 # -------------------------------------------------------------------------- # FastAPI: 高性能异步 Web 框架,自动生成 Swagger API 文档 -fastapi==0.111.0 +fastapi==0.111.1 # Uvicorn: ASGI 服务器,支持热重载和 WebSocket uvicorn[standard]==0.30.1 # python-multipart: FastAPI 文件上传支持(处理 multipart/form-data 请求) -python-multipart==0.0.9 +python-multipart==0.0.12 # -------------------------------------------------------------------------- # 数据库 @@ -37,7 +37,7 @@ redis==5.0.7 # 数据验证 # -------------------------------------------------------------------------- # pydantic: 数据验证和设置管理,FastAPI 的核心依赖 -pydantic==2.7.4 +pydantic==2.7.5 # pydantic-settings: 从环境变量读取配置,支持 .env 文件 pydantic-settings==2.3.4 @@ -78,3 +78,9 @@ passlib[bcrypt]==1.7.4 qrcode[pil]==7.4.2 # pillow: 图片处理(qrcode[pil] 依赖) pillow==10.4.0 + +# -------------------------------------------------------------------------- +# 监控 +# -------------------------------------------------------------------------- +# psutil: 系统监控(用于 /metrics 端点) +psutil==5.9.8 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index e92ed21..cb32fca 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -33,6 +33,32 @@ from app.models.quick_reply_template import QuickReplyTemplate from app.models.agent_note import AgentNote +# ============================================================================= +# 2026-06-15 修复: monkey-patch starlette.config.Config 强制 UTF-8 读 .env +# 原因: Windows pytest 默认 GBK 读 .env 会 UnicodeDecodeError(0xb0 字节) +# 必须在 conftest 顶部应用,否则 reset_rate_limiter 等 autouse fixture +# 提前 import app 模块触发 .env 读取时会失败 +# ============================================================================= +import starlette.config as _starlette_config + + +def _read_file_utf8(self, file_name): + """强制以 UTF-8 编码读 .env,避免 Windows GBK 默认编码触发 UnicodeDecodeError。""" + result = {} + with open(file_name, encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' in line: + k, v = line.split('=', 1) + result[k.strip()] = v.strip().strip('"').strip("'") + return result + + +_starlette_config.Config._read_file = _read_file_utf8 + + # ============================================================================= # SQLite 内存数据库引擎 # ============================================================================= @@ -184,6 +210,70 @@ def mock_redis() -> MockRedis: return MockRedis() +# ============================================================================= +# 模块级 Mock 外部服务(让子测试可覆盖其行为) +# ============================================================================= +# 2026-06-15 修复: 把 WecomService / AIService mock 提升到模块级 +# 原因: client fixture 内的局部 mock 无法被测试内 `with patch.object(...)` 覆盖 +# → 降级登录测试(需让企微 API "不可达")无法触发降级分支 +# 修复: 新增 mock_wecom_instance fixture,测试通过它改写 side_effect +# client fixture 改用模块级 mock,改写对当前请求立即生效 +# ============================================================================= +mock_wecom_module = AsyncMock() +mock_wecom_module.send_message.return_value = {"errcode": 0, "errmsg": "ok"} + + +async def _mock_get_user_info_default(user_id: str, **kwargs): + """默认的企微 get_user_info 行为:返回动态生成的用户名。 + + 测试可通过 mock_wecom_instance.get_user_info.side_effect = ... 改写。 + """ + return { + "user_id": user_id, + "name": f"用户{user_id}", + "department": "测试部", + "avatar": "", + } + + +mock_wecom_module.get_user_info.side_effect = _mock_get_user_info_default +mock_wecom_module.get_department_users.return_value = [] + +mock_ai_module = AsyncMock() +mock_ai_module.generate_response.return_value = "这是AI的模拟回复" + + +@pytest.fixture +def mock_wecom_instance(): + """暴露模块级 WecomService mock 实例,让测试可改写其行为(模拟降级等)。 + + 使用示例 — 触发降级登录路径: + async def fail(*args, **kwargs): + raise Exception("企微 API 不可达") + mock_wecom_instance.get_user_info.side_effect = fail + # ...发起请求后,用 try/finally 恢复原 side_effect + """ + return mock_wecom_module + + +@pytest.fixture(autouse=True) +def reset_rate_limiter(): + """每个测试前后重置 slowapi 限流器状态,避免 IP 限流干扰测试。 + + 背景: /agents/login 限流 10/min per IP,pytest 连续跑多个测试会撞 429。 + """ + from app.api.agents import limiter as agents_limiter + try: + agents_limiter._storage.reset() + except Exception: + pass + yield + try: + agents_limiter._storage.reset() + except Exception: + pass + + @pytest_asyncio.fixture async def client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncGenerator[AsyncClient, None]: """提供 FastAPI 异步测试客户端。""" @@ -194,6 +284,9 @@ async def client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncGenera async def _override_get_redis(): return mock_redis + # 注: 2026-06-15 UTF-8 monkey-patch 已提升到 conftest 模块级,见文件顶部 + # 原因: reset_rate_limiter 等 autouse fixture 提前 import 触发 .env 读取 + from app.main import create_app from app.database import get_db @@ -210,24 +303,11 @@ async def client(db_session: AsyncSession, mock_redis: MockRedis) -> AsyncGenera # 为什么:测试中不应调用真实企微API/AI大模型 # 怎么做:patch 类构造函数,返回配置了默认返回值的 mock 对象 # ------------------------------------------------------------------ - mock_wecom = AsyncMock() - # 企微消息发送:默认成功 - mock_wecom.send_message.return_value = {"errcode": 0, "errmsg": "ok"} - # 企微通讯录查询:动态返回(根据传入的 user_id 生成对应的名称) - # 为什么:坐席登录时会调用 get_user_info 获取员工姓名 - # 如果返回固定名字,登录接口会用 mock 名字覆盖请求中的 name 参数 - async def _mock_get_user_info(user_id: str, **kwargs): - return { - "user_id": user_id, - "name": f"用户{user_id}", - "department": "测试部", - "avatar": "", - } - mock_wecom.get_user_info.side_effect = _mock_get_user_info - mock_wecom.get_department_users.return_value = [] - - mock_ai = AsyncMock() - mock_ai.generate_response.return_value = "这是AI的模拟回复" + # 使用模块级 mock_wecom_module / mock_ai_module(2026-06-15 修复) + # 原因: 模块级 mock 允许测试通过 mock_wecom_instance fixture 改写行为 + # 例如降级登录测试改 side_effect = raise Exception("企微不可达") + mock_wecom = mock_wecom_module + mock_ai = mock_ai_module # Patch WecomService 类(端点函数中会新建实例) # 注意:只 patch 模块中实际引用的名字 diff --git a/backend/tests/test_agents.py b/backend/tests/test_agents.py new file mode 100644 index 0000000..1e1c608 --- /dev/null +++ b/backend/tests/test_agents.py @@ -0,0 +1,180 @@ +# ============================================================================= +# 企微智能IT支持服务台 — 坐席降级登录测试 +# ============================================================================= +# 覆盖 P0 修复 Fix-4: 企微 API 不可达时,已注册坐席必须验证本地密码 +# 创建日期: 2026-06-15 (Claude Code 补最小测试,因 WB 提交时未含此测试) +# ============================================================================= + +import pytest +import pytest_asyncio +from unittest.mock import AsyncMock, patch + +from app.models.agent import Agent +from app.utils.error_codes import ErrorCode +from tests.conftest import create_test_agent + + +class TestAgentDegradedLogin: + """P0 修复 Fix-4: 降级登录密码验证""" + + @pytest.mark.asyncio + async def test_degraded_login_wrong_password_rejected( + self, client, db_session, mock_redis, mock_wecom_instance + ): + """场景: 企微 API 不可达,坐席有 password_hash,登录用错密码 → 拒绝 + + 验证: + - 状态码非 200(或响应 code 非 0) + - 错误码属于 AUTH_PASSWORD_WRONG 类(1011 当前,2006 改完后) + """ + # 1. 预置坐席:有 password_hash + import bcrypt + + correct_pw = "CorrectP@ss123" + pw_hash = bcrypt.hashpw(correct_pw.encode("utf-8"), bcrypt.gensalt()).decode( + "utf-8" + ) + + agent = create_test_agent( + user_id="degraded_agent_001", + name="降级坐席", + ) + agent.password_hash = pw_hash + db_session.add(agent) + await db_session.flush() + + # 2. 改写 conftest 模块级 mock 行为,让企微 API 抛异常(降级场景触发) + original_side_effect = mock_wecom_instance.get_user_info.side_effect + + async def fail_get_user_info(*args, **kwargs): + raise Exception("企微 API 不可达 - 验证降级路径") + + mock_wecom_instance.get_user_info.side_effect = fail_get_user_info + + try: + # 3. 用错误密码登录 + response = await client.post( + "/agents/login", + json={ + "user_id": "degraded_agent_001", + "name": "降级坐席", + "password": "WrongPassword", + }, + ) + finally: + # 恢复默认 side_effect,避免污染后续测试 + mock_wecom_instance.get_user_info.side_effect = original_side_effect + + # 4. 断言:被拒绝 + assert response.status_code in (200, 401, 403), ( + f"预期被拒绝,实际 status={response.status_code}, body={response.text}" + ) + body = response.json() + # 业务 code 应该非 0 + assert body.get("code") != 0, f"预期失败 code,实际成功: {body}" + + # 错误码: WB 修复后是 AUTH_PASSWORD_WRONG=2006,旧码 1011 也接受 + error_code = body.get("code") + assert error_code in ( + ErrorCode.AUTH_PASSWORD_WRONG.value, # 2006 + 1011, # 旧数字码,WB 接入 ErrorCode 前的过渡 + ), f"错误码不匹配: {error_code}, body={body}" + + @pytest.mark.asyncio + async def test_degraded_login_no_password_blocked( + self, client, db_session, mock_redis, mock_wecom_instance + ): + """场景: 企微 API 不可达,坐席有 password_hash,登录不传密码 → 拒绝""" + # 1. 预置坐席 + import bcrypt + + pw_hash = bcrypt.hashpw(b"AnyP@ss", bcrypt.gensalt()).decode("utf-8") + agent = create_test_agent( + user_id="degraded_agent_002", + name="降级坐席2", + ) + agent.password_hash = pw_hash + db_session.add(agent) + await db_session.flush() + + # 2. 改写 conftest 模块级 mock,让企微 API 抛异常 + original_side_effect = mock_wecom_instance.get_user_info.side_effect + + async def fail_get_user_info(*args, **kwargs): + raise Exception("企微 API 不可达 - 验证降级路径") + + mock_wecom_instance.get_user_info.side_effect = fail_get_user_info + + try: + # 3. 不传 password 登录 + response = await client.post( + "/agents/login", + json={ + "user_id": "degraded_agent_002", + "name": "降级坐席2", + }, + ) + finally: + mock_wecom_instance.get_user_info.side_effect = original_side_effect + + # 4. 断言:被拒绝 + body = response.json() + assert body.get("code") != 0, f"预期被拒绝: {body}" + error_code = body.get("code") + # 2006 (AUTH_PASSWORD_WRONG) 或 1011 (旧码) + assert error_code in ( + ErrorCode.AUTH_PASSWORD_WRONG.value, + 1011, + ), f"错误码不匹配: {error_code}, body={body}" + + @pytest.mark.asyncio + async def test_degraded_login_correct_password_succeeds( + self, client, db_session, mock_redis, mock_wecom_instance + ): + """场景: 企微 API 不可达,坐席有 password_hash,登录用对密码 → 成功 + + 验证降级路径正常工作时,正确密码可以登录 + """ + import bcrypt + + correct_pw = "CorrectP@ss456" + pw_hash = bcrypt.hashpw(correct_pw.encode("utf-8"), bcrypt.gensalt()).decode( + "utf-8" + ) + + agent = create_test_agent( + user_id="degraded_agent_003", + name="降级坐席3", + ) + agent.password_hash = pw_hash + db_session.add(agent) + await db_session.flush() + + # 改写 conftest 模块级 mock,让企微 API 抛异常 + original_side_effect = mock_wecom_instance.get_user_info.side_effect + + async def fail_get_user_info(*args, **kwargs): + raise Exception("企微 API 不可达 - 验证降级路径") + + mock_wecom_instance.get_user_info.side_effect = fail_get_user_info + + try: + response = await client.post( + "/agents/login", + json={ + "user_id": "degraded_agent_003", + "name": "降级坐席3", + "password": correct_pw, + }, + ) + finally: + mock_wecom_instance.get_user_info.side_effect = original_side_effect + + # 降级 + 正确密码应能登录 + body = response.json() + assert body.get("code") == 0, ( + f"预期降级登录成功,实际失败: {body}" + ) + assert "token" in body.get("data", {}), ( + f"响应缺 token: {body}" + ) diff --git a/deploy-server/DEPLOY-GUIDE.md b/deploy-server/DEPLOY-GUIDE.md index b7a14c1..658363b 100644 --- a/deploy-server/DEPLOY-GUIDE.md +++ b/deploy-server/DEPLOY-GUIDE.md @@ -1,4 +1,4 @@ -# 企微IT智能服务台 — 服务器部署指南 +# 企微智能IT支持服务台 — 服务器部署指南 > 目标服务器:`10.90.5.110`(Linux) > 域名:`itsupport.servyou.com.cn` diff --git a/deploy-server/README.md b/deploy-server/README.md index 441641a..51ec67f 100644 --- a/deploy-server/README.md +++ b/deploy-server/README.md @@ -1,4 +1,4 @@ -# IT智能服务台 — 新服务器部署手册 +# 智能IT支持服务台 — 新服务器部署手册 > **目标服务器**:`10.80.0.136`(公司内网) > **域名**:`itsupport.servyou.com.cn` @@ -53,7 +53,7 @@ Host bastion Port 2222 User sxn -# IT智能服务台服务器 +# 智能IT支持服务台服务器 Host itdesk HostName 10.80.0.136 User sxn diff --git a/deploy-server/build-and-deploy.ps1 b/deploy-server/build-and-deploy.ps1 index fac8662..aa455df 100644 --- a/deploy-server/build-and-deploy.ps1 +++ b/deploy-server/build-and-deploy.ps1 @@ -1,5 +1,5 @@ # ============================================================================= -# 企微IT智能服务台 — 打包 + 构建后端镜像 + 部署脚本 +# 企微智能IT支持服务台 — 打包 + 构建后端镜像 + 部署脚本 # ============================================================================= # 功能: # 1. 打包前端构建产物 + nginx配置 + docker-compose.yml + .env @@ -51,7 +51,7 @@ function Write-Error { Write-Host "" Write-Host "========================================" -ForegroundColor Cyan -Write-Host " 企微IT智能服务台 — 打包部署自动化" -ForegroundColor Cyan +Write-Host " 企微智能IT支持服务台 — 打包部署自动化" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host " 模式:$Mode" -ForegroundColor White Write-Host "" diff --git a/deploy-server/build-package.ps1 b/deploy-server/build-package.ps1 index a120a71..d104592 100644 --- a/deploy-server/build-package.ps1 +++ b/deploy-server/build-package.ps1 @@ -1,5 +1,5 @@ # ============================================================================= -# 企微IT智能服务台 — 打包部署脚本 +# 企微智能IT支持服务台 — 打包部署脚本 # ============================================================================= # 功能:将所有部署所需文件打包成一个 zip 文件 # 用法:在 PowerShell 中运行此脚本 @@ -19,7 +19,7 @@ $packageDir = "$deployDir\_package" $zipFile = "$deployDir\it-smart-desk-server-deploy.zip" Write-Host "========================================" -ForegroundColor Cyan -Write-Host " 企微IT智能服务台 — 打包部署文件" -ForegroundColor Cyan +Write-Host " 企微智能IT支持服务台 — 打包部署文件" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" diff --git a/deploy-server/deploy-ragflow.sh b/deploy-server/deploy-ragflow.sh index 6542e51..10266a7 100644 --- a/deploy-server/deploy-ragflow.sh +++ b/deploy-server/deploy-ragflow.sh @@ -1,6 +1,6 @@ #!/bin/bash # ============================================================================= -# IT智能服务台 — RAGFlow 集成部署脚本 +# 智能IT支持服务台 — RAGFlow 集成部署脚本 # 目标服务器:10.90.5.110 # 部署路径:/opt/wecom-it-desk # ============================================================================= @@ -11,7 +11,7 @@ DEPLOY_DIR="/opt/wecom-it-desk" BACKUP_DIR="/opt/wecom-it-desk-backup-$(date +%Y%m%d_%H%M%S)" echo "==========================================" -echo "IT智能服务台 — RAGFlow 集成部署" +echo "智能IT支持服务台 — RAGFlow 集成部署" echo "时间: $(date)" echo "==========================================" diff --git a/deploy-server/deploy.sh b/deploy-server/deploy.sh index 5bd356a..055cf9b 100644 --- a/deploy-server/deploy.sh +++ b/deploy-server/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash # ============================================================================= -# IT智能服务台 — 生产部署脚本 +# 智能IT支持服务台 — 生产部署脚本 # 目标服务器:10.90.5.110 # 部署路径:/opt/wecom-it-desk # ============================================================================= @@ -11,7 +11,7 @@ DEPLOY_DIR="/opt/wecom-it-desk" BACKUP_DIR="/opt/wecom-it-desk-backup-$(date +%Y%m%d_%H%M%S)" echo "==========================================" -echo "IT智能服务台 生产部署" +echo "智能IT支持服务台 生产部署" echo "时间: $(date)" echo "==========================================" diff --git a/deploy-server/docker-compose.yml b/deploy-server/docker-compose.yml index c9a70ae..21eb809 100644 --- a/deploy-server/docker-compose.yml +++ b/deploy-server/docker-compose.yml @@ -1,5 +1,5 @@ # ============================================================================= -# 企微IT智能服务台 — Docker Compose(公司内网服务器版) +# 企微智能IT支持服务台 — Docker Compose(公司内网服务器版) # ============================================================================= # 目标服务器:10.90.5.110 # 域名:itsupport.servyou.com.cn diff --git a/deploy-server/nginx.conf b/deploy-server/nginx.conf index e14210d..0e326b8 100644 --- a/deploy-server/nginx.conf +++ b/deploy-server/nginx.conf @@ -1,5 +1,5 @@ # ============================================================================= -# 企微IT智能服务台 — Nginx 配置(公司内网服务器版 + HTTPS) +# 企微智能IT支持服务台 — Nginx 配置(公司内网服务器版 + HTTPS) # ============================================================================= # 目标服务器:10.90.5.110 # 域名:itsupport.servyou.com.cn @@ -47,6 +47,23 @@ http { application/javascript application/xml+rss application/json application/ld+json; + # ------------------------------------------------------------------ + # 安全响应头 + # ------------------------------------------------------------------ + # 隐藏 nginx 版本号 + server_tokens off; + + # 基础安全头(应用到所有响应) + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "DENY" always; + add_header X-XSS-Protection "0" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + + # API 路径特殊处理(不加 CSP,只加基础安全头) + # 前端路径的 CSP 在各前端 index.html 中单独配置 + # ================================================================= # 上游服务定义(Docker 内部网络) # ================================================================= diff --git a/deploy-server/nginx/nginx.conf b/deploy-server/nginx/nginx.conf index 379be9c..23ae517 100644 --- a/deploy-server/nginx/nginx.conf +++ b/deploy-server/nginx/nginx.conf @@ -1,5 +1,5 @@ # ============================================================================= -# 企微IT智能服务台 — Nginx 配置(公司内网服务器版) +# 企微智能IT支持服务台 — Nginx 配置(公司内网服务器版) # ============================================================================= # 适用场景:独立域名 itsupport.servyou.com.cn,公司内网 DNS 解析 # 与 NAS 版的区别: @@ -67,12 +67,24 @@ http { # ------------------------------------------------------------------ # 安全头 # ------------------------------------------------------------------ + # 基础安全头 add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # CSP 收紧: 去掉 unsafe-inline(生产不需要,只有 dev HMR 需要) + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://*; font-src 'self' data:;" always; + + # 隐私与跨域控制 + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Resource-Policy "same-origin" always; + + # 隐藏服务器版本 + server_tokens off; # ------------------------------------------------------------------ # 健康检查端点 diff --git a/deploy-server/package-deploy.bat b/deploy-server/package-deploy.bat index 2861997..52d7b65 100644 --- a/deploy-server/package-deploy.bat +++ b/deploy-server/package-deploy.bat @@ -1,11 +1,11 @@ @echo off REM ============================================================================= -REM IT智能服务台 — 打包部署脚本(Windows) +REM 智能IT支持服务台 — 打包部署脚本(Windows) REM 目标:生成部署包,通过堡垒机上传到服务器 REM ============================================================================= echo ========================================== -echo IT智能服务台 部署包打包 +echo 智能IT支持服务台 部署包打包 echo 时间: %date% %time% echo ========================================== diff --git a/deploy-server/package.py b/deploy-server/package.py index 398487c..5b664d4 100644 --- a/deploy-server/package.py +++ b/deploy-server/package.py @@ -1,5 +1,5 @@ """ -企微IT智能服务台 — 部署包生成脚本(Windows 兼容版) +企微智能IT支持服务台 — 部署包生成脚本(Windows 兼容版) ======================================================= 功能: 1. 构建前端(H5 + 坐席端) @@ -163,7 +163,7 @@ def create_package(): def main(): print("=" * 50) - print(" IT智能服务台 — 部署包生成") + print(" 智能IT支持服务台 — 部署包生成") print("=" * 50) # 检查是否跳过构建 diff --git a/deploy-server/package.sh b/deploy-server/package.sh index 9bdd846..add07fd 100644 --- a/deploy-server/package.sh +++ b/deploy-server/package.sh @@ -1,6 +1,6 @@ #!/bin/bash # ============================================================================= -# 企微IT智能服务台 — 部署包生成脚本(在开发机上运行) +# 企微智能IT支持服务台 — 部署包生成脚本(在开发机上运行) # ============================================================================= # 功能: # 1. 构建前端(H5 + 坐席端) @@ -28,7 +28,7 @@ PACKAGE_NAME="it-smart-desk-server-deploy" BUILD_DIR="/tmp/$PACKAGE_NAME" echo -e "${GREEN}============================================${NC}" -echo -e "${GREEN} IT智能服务台 — 部署包生成${NC}" +echo -e "${GREEN} 智能IT支持服务台 — 部署包生成${NC}" echo -e "${GREEN}============================================${NC}" # --- 1. 构建前端 --- diff --git a/deploy-server/打包部署.bat b/deploy-server/打包部署.bat index 48faea3..01ed30b 100644 --- a/deploy-server/打包部署.bat +++ b/deploy-server/打包部署.bat @@ -1,6 +1,6 @@ @echo off REM ============================================================================= -REM 企微IT智能服务台 — 打包部署一键执行 +REM 企微智能IT支持服务台 — 打包部署一键执行 REM ============================================================================= REM 功能: REM 1. 打包前端构建产物 + nginx配置 + docker-compose.yml + .env @@ -20,7 +20,7 @@ if "%MODE%"=="" set MODE=local echo. echo ======================================== -echo 企微IT智能服务台 — 打包部署 +echo 企微智能IT支持服务台 — 打包部署 echo ======================================== echo 模式: %MODE% echo. diff --git a/docs/01-项目总览与部署手册.md b/docs/01-项目总览与部署手册.md index f0460b7..402b99f 100644 --- a/docs/01-项目总览与部署手册.md +++ b/docs/01-项目总览与部署手册.md @@ -1,4 +1,4 @@ -# 企微IT智能服务台 — 项目总览与部署手册 +# 企微智能IT支持服务台 — 项目总览与部署手册 > **版本**: v2.1 | **日期**: 2026-06-03 | **编制**: 宋献(IT支持组组长) > **目标读者**: **管理者 / 架构师 / 运维** — 了解项目全貌、架构决策、部署与运维操作 @@ -570,7 +570,7 @@ docker compose down # 停止新系统所有容器 ### TL;DR -企微IT智能服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件**,**116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。 +企微智能IT支持服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件**,**116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。 ### 交付状态 @@ -641,7 +641,7 @@ wecom_it_smart_desk/ ├── ARCHITECTURE.md # 系统架构设计(合并版) ├── 01-项目总览与部署手册.md # 管理者视角部署手册 ├── 开发交付概览.md # 开发交付状态总览 - ├── IT智能服务台-项目迁移文档.md # 工作区迁移记录 + ├── 智能IT支持服务台-项目迁移文档.md # 工作区迁移记录 ├── testing/ # 测试报告目录 │ └── QA_COMPREHENSIVE_REPORT.md # 综合 QA 报告 ├── diagrams/ # Mermaid 图表 diff --git a/docs/ARCHITECTURE-admin.md b/docs/ARCHITECTURE-admin.md index 1644496..e87d5cf 100644 --- a/docs/ARCHITECTURE-admin.md +++ b/docs/ARCHITECTURE-admin.md @@ -1,4 +1,4 @@ -# IT智能服务台 — 管理后台架构设计文档 +# 智能IT支持服务台 — 管理后台架构设计文档 > **文档版本**: v1.0 > **架构师**: 高见远 (Bob) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index caf3e38..9a8d4ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# 企微IT智能服务台 — 系统架构设计文档 +# 企微智能IT支持服务台 — 系统架构设计文档 > **文档版本**: v0.11 > **创建日期**: 2025-07-11 @@ -2877,4 +2877,4 @@ alembic upgrade head --- -> **文档结束** — 本架构设计文档涵盖企微IT智能服务台第一步(消息接管+极简坐席)的完整技术方案,作为工程师编写代码的基准文档。 +> **文档结束** — 本架构设计文档涵盖企微智能IT支持服务台第一步(消息接管+极简坐席)的完整技术方案,作为工程师编写代码的基准文档。 diff --git a/docs/DEPLOY_NAS.md b/docs/DEPLOY_NAS.md index 018e854..ab0bab6 100644 --- a/docs/DEPLOY_NAS.md +++ b/docs/DEPLOY_NAS.md @@ -1,4 +1,4 @@ -# 企微IT智能服务台 — 远程服务器部署指南(预生产) +# 企微智能IT支持服务台 — 远程服务器部署指南(预生产) > **预生产环境**:本系统与 IT 数据查询平台部署在**不同主机**。正式环境将迁移到 K8s。 diff --git a/docs/ExternalSystemAdapter设计文档.md b/docs/ExternalSystemAdapter设计文档.md index 4937704..3dfca4b 100644 --- a/docs/ExternalSystemAdapter设计文档.md +++ b/docs/ExternalSystemAdapter设计文档.md @@ -1,6 +1,6 @@ # ExternalSystemAdapter 抽象层设计文档 -> 版本:V1.0 | 日期:2026-06-11 | 作者:IT智能服务台项目组 +> 版本:V1.0 | 日期:2026-06-11 | 作者:智能IT支持服务台项目组 --- diff --git a/docs/H5用户端右侧栏动态推送评估.md b/docs/H5用户端右侧栏动态推送评估.md index 72c0921..7383ad3 100644 --- a/docs/H5用户端右侧栏动态推送评估.md +++ b/docs/H5用户端右侧栏动态推送评估.md @@ -14,7 +14,7 @@ ### 1. 符合系统定位——"AI驱动" -系统全名是"IT智能服务台 — AI驱动",但当前右侧栏本质是传统信息架构(标签页+列表),AI只在左侧会话区参与。动态推送让右侧也变成AI能力的延伸,整个产品才能名副其实。 +系统全名是"智能IT支持服务台 — AI驱动",但当前右侧栏本质是传统信息架构(标签页+列表),AI只在左侧会话区参与。动态推送让右侧也变成AI能力的延伸,整个产品才能名副其实。 ### 2. 降低用户认知负荷 diff --git a/docs/IT服务台部署修复记录-2026-06-13.md b/docs/IT服务台部署修复记录-2026-06-13.md index 59508aa..a78d75b 100644 --- a/docs/IT服务台部署修复记录-2026-06-13.md +++ b/docs/IT服务台部署修复记录-2026-06-13.md @@ -1,4 +1,4 @@ -# IT智能服务台 - 部署修复记录 +# 智能IT支持服务台 - 部署修复记录 **日期**:2026-06-13 **负责人**:宋献 diff --git a/docs/NAS部署指南.md b/docs/NAS部署指南.md index 742dbea..41604c8 100644 --- a/docs/NAS部署指南.md +++ b/docs/NAS部署指南.md @@ -252,7 +252,7 @@ docker compose -f docker-compose.nas.yml up -d --build 1. 登录 [企微管理后台](https://work.weixin.qq.com/wework_admin/frame) 2. **应用管理** → **自建** → **创建应用** 3. 填写: - - 应用名称:`IT智能服务台` + - 应用名称:`智能IT支持服务台` - 应用logo:上传一个图标 - 可见范围:选择测试部门/人员 diff --git a/docs/PRD-admin.md b/docs/PRD-admin.md index de659a3..18dc779 100644 --- a/docs/PRD-admin.md +++ b/docs/PRD-admin.md @@ -1,4 +1,4 @@ -# IT智能服务台 — 管理后台增量 PRD +# 智能IT支持服务台 — 管理后台增量 PRD > **文档版本**: v1.0 > **创建日期**: 2026-06-16 @@ -28,7 +28,7 @@ | 字段 | 值 | |------|------| -| 产品名称 | IT智能服务台 — 管理后台 | +| 产品名称 | 智能IT支持服务台 — 管理后台 | | 项目代号 | `wecom_it_smart_desk`(第三端:admin) | | 编程语言 | 前端: Vue 3 + TypeScript + Element Plus + Pinia · 后端: FastAPI + SQLAlchemy + PostgreSQL + Redis | | 部署路径 | `/itadmin/`(与 H5 `/itdesk/`、坐席 `/itagent/` 并列) | diff --git a/docs/PRD-增量-人工按钮与术语统一.md b/docs/PRD-增量-人工按钮与术语统一.md index d0f9548..bfd31de 100644 --- a/docs/PRD-增量-人工按钮与术语统一.md +++ b/docs/PRD-增量-人工按钮与术语统一.md @@ -54,7 +54,7 @@ ``` ┌────────────────────────────────────┐ -│ IT智能服务台 [🔔 人工] │ ← 启用状态(橙色) +│ 智能IT支持服务台 [🔔 人工] │ ← 启用状态(橙色) │ [▓▓ 人工] │ ← 禁用状态(灰色) └────────────────────────────────────┘ ``` diff --git a/docs/PRD.md b/docs/PRD.md index 62a97ac..4e98cb9 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,4 +1,4 @@ -# 企微IT智能服务台 — 产品需求文档 (PRD) +# 企微智能IT支持服务台 — 产品需求文档 (PRD) > **文档版本**: v1.0 > **创建日期**: 2025-07-11 @@ -1318,7 +1318,7 @@ | 项目 | 说明 | |------|------| -| **顶部栏** | 左侧:logo 方块 "IT"(渐变紫蓝 26×26px)+ "IT智能服务台"(渐变文字)+ "· 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理"(10px 灰色副标题,max-width 280px 溢出省略) | +| **顶部栏** | 左侧:logo 方块 "IT"(渐变紫蓝 26×26px)+ "智能IT支持服务台"(渐变文字)+ "· 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理"(10px 灰色副标题,max-width 280px 溢出省略) | | **变更范围** | `TopBar.vue`(从 `Workspace.vue` 顶部栏独立) | --- @@ -1451,7 +1451,7 @@ ``` ┌─────────────────────────────────────────────────────────────────────┐ -│ [IT] IT智能服务台 · 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理 │ ☀️/🌙 │ 坐席: 陈思远 │ +│ [IT] 智能IT支持服务台 · 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理 │ ☀️/🌙 │ 坐席: 陈思远 │ ├──────────┬──────────────────────────────────┬───────────────────────┤ │ │ 👤 张伟 · 研发一部 🥇黄金 │ 🤖 AI 智能推荐 │ │ 🔍 搜索 │ 😟焦虑 ⏱8分32秒 💬6轮 🔁重复 │ ┌─────────────────┐ │ @@ -1765,7 +1765,7 @@ class TroubleshootingTemplate(Base): | 系统 | 职责 | 部署位置 | 当前集成度 | |------|------|---------|-----------| -| **IT智能服务台** | 员工端H5 + 坐席工作台 + 管理后台 | NAS Docker (Cloudflare Tunnel) | — | +| **智能IT支持服务台** | 员工端H5 + 坐席工作台 + 管理后台 | NAS Docker (Cloudflare Tunnel) | — | | **Dify** | AI对话引擎(Agent1 员工自助 + Agent2 坐席辅助) | 公司内网 | 100%(dify2openai 集成) | | **RAGFlow** | 知识库检索(Dify 通过 RAGFlow 获取知识) | 公司内网 | 0%(Dify 间接调用) | | **智能IT助手数据处理平台** | 会话数据分析、报表、运营指标 | 公司内网 | 0%(物理隔离) | @@ -1941,7 +1941,7 @@ class TroubleshootingTemplate(Base): --- -> **文档结束** — 本PRD涵盖企微IT智能服务台全部已确认设计决策和约束,作为后续架构设计和开发实施的基准文档。v1.0 新增管理后台远景规划、系统生态与集成规划、阶段细化与并行推进策略。 +> **文档结束** — 本PRD涵盖企微智能IT支持服务台全部已确认设计决策和约束,作为后续架构设计和开发实施的基准文档。v1.0 新增管理后台远景规划、系统生态与集成规划、阶段细化与并行推进策略。 --- diff --git a/docs/RELEASE_NOTES_v0.5.0-beta.md b/docs/RELEASE_NOTES_v0.5.0-beta.md new file mode 100644 index 0000000..3f09193 --- /dev/null +++ b/docs/RELEASE_NOTES_v0.5.0-beta.md @@ -0,0 +1,155 @@ +# Release Notes — v0.5.0-beta(内测版) + +**发布日期**: 2026-06-15 下午 +**目标**: 内测(2-3 个内部用户),生产仍用 v0.4.x +**类型**: 🟡 **beta** — 部分 P0 已修,部分 P0 仍缺 +**负责人**: Simon +**对接 workbuddy brief**: `.workbuddy/memory/2026-06-15-合并任务部署说明.md` 等 6 份 + +--- + +## ⚠️ 发布前必读(用户须知) + +### ✅ 已修复(P0 已修 2/5) + +| # | 标题 | 风险等级 | 修复方式 | +|---|---|---|---| +| Fix-1 | 企微凭据硬编码泄露 | 🟠 中 | 改环境变量 + 旧凭据 `Bs7ucT*` 已轮换 | +| Fix-4 | 降级登录缺密码验证 | 🔴 高 | agents.py L222-232 加 bcrypt 验证,3 测试覆盖 | +| **NEW** | ErrorCode 1012 上下文冲突 | 🟠 中 | 拆 2 个新码 E1015/E1016,前端提示不串语义 | + +### ❌ 仍未修复(P0 缺 3/5,等 WB) + +| # | 标题 | 风险等级 | 状态 | +|---|---|---|---| +| Fix-5 | nginx 缺 2 安全头(Permissions-Policy + COOP) | 🟡 中 | WB 报已修,未验证,延迟到 PR#2 | +| Fix-6 | CSP 含 `unsafe-inline`(XSS 风险) | 🟠 中 | 报已修,未验证 | +| Fix-7 | 项目名 `git mv` 调整 | ⚪ 低 | 报已修,未验证 | +| Doc-P0 | 5 处文档失真 | ⚪ 低 | 评审中,本批未修 | + +### 🚫 不在本次范围 + +- ❌ 应急降级页(BC/DR)代码 — 需求 v4 已写,WB 接单中 +- ❌ 演练 SOP-005 — 待写 +- ❌ 单元测试未跑(被 auto-mode 拒,需手动跑) + +--- + +## 📦 发布内容(本次 8 文档 + 5 脚本 + 5 配置 + 3 代码改动) + +### 1️⃣ 8 份新建文档(凌晨跑批产出) + +| # | 路径 | 行数 | 摘要 | +|---|---|---|---| +| 1 | `docs/审计报告/Dockerfile优化与镜像审计.md` | #44 | Docker 镜像优化建议 | +| 2 | `docs/数据库ER图与环境变量清点.md` | #45 | 16 表 ER + 17 env | +| 3 | `docs/审计报告/依赖漏洞扫描与Lockfile审计.md` | #46 | 5 CVE 识别 | +| 4 | `docs/审计报告/健康检查+错误码+日志结构化.md` | #47 | 40+ 错误码 + JSON 日志 | +| 5 | `docs/审计报告/CORS-CSP-安全Header全套.md` | #48 | 8 安全头配置 | +| 6 | `docs/惊喜报告/🎁惊喜1-项目健康度仪表盘.md` | #49 | 仪表盘说明 | +| 7 | `docs/惊喜报告/🎁惊喜2-README徽章+CHANGELOG+模板.md` | #50 | 文档增强 | +| 8 | `docs/需求-发布预演页面.md`(v4 刚升) | 226 | 应急降级页需求 | +| 附 | `docs/dashboard.html` | - | 健康度仪表盘网页(8KB) | + +### 2️⃣ 5 个脚本(凌晨跑批产出) + +| # | 路径 | 用途 | +|---|---|---| +| 1 | `scripts/dashboard.py` | 生成健康度 HTML | +| 2 | `scripts/oneclick-deploy.sh` | 一键部署(灰度) | +| 3 | `scripts/pre-commit-check.sh` | 提交前自检 | +| 4 | `scripts/backup-gitea.sh` | Gitea 备份 | +| 5 | `scripts/security-audit.sh` | 安全审计 | + +### 3️⃣ 5 份配置(凌晨跑批产出) + +| # | 路径 | 用途 | +|---|---|---| +| 1 | `.dockerignore` | Docker 优化 | +| 2 | `.gitea/dependabot.yml` | 依赖自动更新 | +| 3 | `.gitea/ISSUE_TEMPLATE/bug.md` | Bug 报告模板 | +| 4 | `.gitea/ISSUE_TEMPLATE/feature.md` | Feature 申请模板 | +| 5 | `.gitea/PULL_REQUEST_TEMPLATE.md` | PR 模板 | + +附: `CHANGELOG.md` (5 版本历史) + +### 4️⃣ 3 处代码改动(P0 已修 + 1012 拆码) + +#### Fix-1: 企微凭据轮换 +- 文件: `backend/app/services/wecom_service.py` + `.env` +- 改动: 硬编码 `Bs7ucT*` 改为 `${WECOM_CORP_SECRET}` 环境变量 +- 旧凭据: 已在企微后台轮换,新值仅在 `.env` + +#### Fix-4: 降级登录密码验证 +- 文件: `backend/app/api/agents.py` L222-232 +- 改动: 已注册坐席在企微 API 不可达时,如有 `password_hash` 必须验证本地密码 +- 测试: `backend/tests/test_agents.py` 3 测试(已写,待跑) + +#### 1012 拆码(NEW) +- 文件: `backend/app/utils/error_codes.py` + `backend/app/api/agents.py:581/583` +- 改动: 新增 `AUTH_OLD_PASSWORD_REQUIRED=E1015` + `AUTH_OLD_PASSWORD_WRONG=E1016` +- 原因: 1012 在登录(L226)="首次登录请先设置密码",在改密(L581)="请输入旧密码",合并会丢语义 +- 前端: 需补 E1015/E1016 的 i18n 映射(如有) + +--- + +## 🧪 验证清单(发布前必跑) + +### 自动验证 + +- [ ] `cd backend && python -m pytest tests/test_agents.py -v` → 3 通过 +- [ ] `grep -rn "Bs7ucT" backend/ frontend-h5/ frontend-agent/` → 无输出 +- [ ] `grep -rn "AppException(101[123]" backend/` → 只剩 1 行(登录场景) +- [ ] `npm run build` (frontend-h5) → 成功 +- [ ] `npm run build` (frontend-agent) → 成功 + +### 手动验证(2-3 个内测用户) + +- [ ] 登录功能: 走企微正常登录 + 改密 → 提示正确 +- [ ] 降级登录: 拔网线模拟企微 API 不可达 → 必须输密码 +- [ ] 凭据轮换: 新 `.env` 的 WECOM_CORP_SECRET 生效 +- [ ] 1015/1016: 改密页"请输入旧密码"提示正确显示 + +### 文档验证 + +- [ ] 8 份新文档可打开(浏览器/Markdown 预览器) +- [ ] `docs/dashboard.html` 用浏览器打开看效果 +- [ ] `CHANGELOG.md` 5 版本历史完整 + +--- + +## 🚦 发布决策 + +| 角色 | 动作 | +|---|---| +| **Simon** | 合并 `feature/t-1-t4-merge` → main,tag `v0.5.0-beta` | +| **workbuddy** | 等 Fix-5/6/7 真正验证完,提 PR#2(本批无此 PR) | +| **内测用户** | 用 v0.5.0-beta 跑 1 周,收集问题 | +| **下次发布** | v0.6.0(预计 2026-06-20)— 含应急降级页 + 演练 | + +--- + +## 📋 风险登记 + +| 风险 | 影响 | 缓解 | +|---|---|---| +| Fix-5/6/7 虚报 | XSS + 缺安全头 | PR#2 之前不上生产 | +| 5 文档 P0 失真 | 内部误导 | 评审报告已记,跟正式版一起修 | +| 应急页未做 | 故障时无降级 | 1 周内 WB 接单补 | +| 测试未跑 | Fix-4 未验证 | 用户手动跑 `pytest` | + +--- + +## 🔗 关联文档 + +- 主任务: `.workbuddy/memory/2026-06-15-合并任务部署说明.md` +- 补 4 项: `.workbuddy/memory/2026-06-15-补-4项+测试.md` +- 命名+错误码: `.workbuddy/memory/2026-06-15-补充-命名+错误码.md` +- 1012 拆码: `.workbuddy/memory/2026-06-15-ErrorCode-1012拆码.md` ← **NEW** +- 应急降级页: `.workbuddy/memory/2026-06-15-发布预演页.md` +- 评审报告: `docs/评审报告/2026-06-14-workbuddy-消息评审.md` +- 凌晨跑批汇总: `~/.claude/memory/overnight-batch-2026-06-15.md` + +--- + +🤖 Generated with [Claude Code](https://claude.com/claude-code) diff --git a/docs/aTrust零信任系统集成分析.md b/docs/aTrust零信任系统集成分析.md index 7e56569..bb17501 100644 --- a/docs/aTrust零信任系统集成分析.md +++ b/docs/aTrust零信任系统集成分析.md @@ -438,7 +438,7 @@ aTrust判断终端是否已存在的规则: ``` ┌─────────────────┐ - │ IT智能服务台 │ + │ 智能IT支持服务台 │ │ employee_id │ └────────┬────────┘ │ diff --git a/docs/archive/ARCHITECTURE-v53-incremental.md b/docs/archive/ARCHITECTURE-v53-incremental.md index 5963912..81cb103 100644 --- a/docs/archive/ARCHITECTURE-v53-incremental.md +++ b/docs/archive/ARCHITECTURE-v53-incremental.md @@ -1,4 +1,4 @@ -# IT智能服务台 · 坐席工作台 v5.3 增量架构设计 +# 智能IT支持服务台 · 坐席工作台 v5.3 增量架构设计 > **版本**: v5.3-incremental > **日期**: 2026-06-06 diff --git a/docs/archive/PRD-v53-incremental.md b/docs/archive/PRD-v53-incremental.md index 57782d4..390ed1b 100644 --- a/docs/archive/PRD-v53-incremental.md +++ b/docs/archive/PRD-v53-incremental.md @@ -1,4 +1,4 @@ -# IT智能服务台 · 坐席工作台 v5.3 增量 PRD +# 智能IT支持服务台 · 坐席工作台 v5.3 增量 PRD > **版本**: v5.3 增量迭代 > **日期**: 2026-06-06 @@ -181,7 +181,7 @@ | 项目 | 说明 | |------|------| -| **顶部栏** | 左侧:logo 方块 "IT"(渐变紫蓝 26×26px)+ "IT智能服务台"(渐变文字)+ "· 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理"(10px 灰色副标题,max-width 280px 溢出省略) | +| **顶部栏** | 左侧:logo 方块 "IT"(渐变紫蓝 26×26px)+ "智能IT支持服务台"(渐变文字)+ "· 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理"(10px 灰色副标题,max-width 280px 溢出省略) | | **变更范围** | `TopBar.vue`(从 `Workspace.vue` 顶部栏独立) | --- @@ -314,7 +314,7 @@ ``` ┌─────────────────────────────────────────────────────────────────────┐ -│ [IT] IT智能服务台 · 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理 │ ☀️/🌙 │ 坐席: 陈思远 │ +│ [IT] 智能IT支持服务台 · 坐席工作台 — AI驱动 · 多系统对接 · 一站式处理 │ ☀️/🌙 │ 坐席: 陈思远 │ ├──────────┬──────────────────────────────────┬───────────────────────┤ │ │ 👤 张伟 · 研发一部 🥇黄金 │ 🤖 AI 智能推荐 │ │ 🔍 搜索 │ 😟焦虑 ⏱8分32秒 💬6轮 🔁重复 │ ┌─────────────────┐ │ diff --git a/docs/archive/开发交付概览.md b/docs/archive/开发交付概览.md index b5ddc48..4db72d3 100644 --- a/docs/archive/开发交付概览.md +++ b/docs/archive/开发交付概览.md @@ -1,8 +1,8 @@ -# 企微IT智能服务台 — 第一步开发交付概览 +# 企微智能IT支持服务台 — 第一步开发交付概览 ## TL;DR -企微IT智能服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件**,**116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。 +企微智能IT支持服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件**,**116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。 ## 交付状态 @@ -73,7 +73,7 @@ wecom_it_smart_desk/ ├── ARCHITECTURE.md # 系统架构设计(合并版) ├── 01-项目总览与部署手册.md # 管理者视角部署手册 ├── 开发交付概览.md # 开发交付状态总览 - ├── IT智能服务台-项目迁移文档.md # 工作区迁移记录 + ├── 智能IT支持服务台-项目迁移文档.md # 工作区迁移记录 ├── testing/ # 测试报告目录 │ └── QA_COMPREHENSIVE_REPORT.md # 综合 QA 报告 ├── diagrams/ # Mermaid 图表 diff --git a/docs/dashboard.html b/docs/dashboard.html new file mode 100644 index 0000000..f2a7ba7 --- /dev/null +++ b/docs/dashboard.html @@ -0,0 +1,150 @@ + + +
+ +