docs: update README and CLAUDE.md to v2.2.0

- Added documentation for audit tracking (IP address, invocation method).
- Updated database model descriptions for enhanced WorkOrder and Conversation fields.
- Documented the new UnifiedConfig system.
- Reflected enhanced logging transparency for knowledge base parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhaojie
2026-02-11 00:08:09 +08:00
parent 2026007045
commit c3560b43fd
218 changed files with 3354 additions and 5096 deletions

View File

@@ -14,8 +14,8 @@ from typing import Dict, Any
from flask import Flask, render_template, request, jsonify, send_from_directory, make_response
from flask_cors import CORS
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.config.unified_config import get_config
# 导入核心模块
from src.core.database import db_manager
@@ -37,6 +37,8 @@ from src.web.blueprints.agent import agent_bp
from src.web.blueprints.vehicle import vehicle_bp
from src.web.blueprints.analytics import analytics_bp
from src.web.blueprints.test import test_bp
from src.web.blueprints.feishu_bot import feishu_bot_bp
# 配置日志
logger = logging.getLogger(__name__)
@@ -79,6 +81,8 @@ app.register_blueprint(agent_bp)
app.register_blueprint(vehicle_bp)
app.register_blueprint(analytics_bp)
app.register_blueprint(test_bp)
app.register_blueprint(feishu_bot_bp)
# 页面路由
@app.route('/')
@@ -235,8 +239,9 @@ def get_active_sessions():
# 飞书同步功能已合并到主页面,不再需要单独的路由
if __name__ == '__main__':
import time
app.config['START_TIME'] = time.time()
app.config['SERVER_PORT'] = 5000
app.config['WEBSOCKET_PORT'] = 8765
app.run(debug=True, host='0.0.0.0', port=5000)
config = get_config()
app.run(
debug=config.server.debug,
host=config.server.host,
port=config.server.port
)

View File

@@ -94,7 +94,6 @@ src/web/
├── app.py # 主应用文件 (674行)
├── app_backup.py # 原文件备份
├── blueprints/ # 蓝图目录
│ ├── __init__.py
│ ├── alerts.py # 预警管理
│ ├── workorders.py # 工单管理
│ ├── conversations.py # 对话管理

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""
飞书机器人蓝图
处理来自飞书机器人的事件回调
"""
import logging
import json
import threading
from flask import Blueprint, request, jsonify
from src.integrations.feishu_service import FeishuService
from src.web.service_manager import service_manager
# 初始化日志
logger = logging.getLogger(__name__)
# 创建蓝图
feishu_bot_bp = Blueprint('feishu_bot', __name__, url_prefix='/api/feishu/bot')
# 在模块级别实例化飞书服务,以便复用
# 注意:这假设配置在启动时是固定的。如果配置可热更新,则需要调整。
feishu_service = FeishuService()
def _process_message_in_background(app_context, event_data: dict):
"""
在后台线程中处理消息,避免阻塞飞书的回调请求。
"""
with app_context:
try:
# 1. 解析事件数据
message_id = event_data['event']['message']['message_id']
chat_id = event_data['event']['message']['chat_id']
# 内容是一个JSON字符串需要再次解析
content_json = json.loads(event_data['event']['message']['content'])
text_content = content_json.get('text', '').strip()
logger.info(f"[Feishu Bot] 后台开始处理消息ID: {message_id}, 内容: '{text_content}'")
# 2. 移除@机器人的部分
# 飞书的@消息格式通常是 "@机器人名 实际内容"
if event_data['event']['message'].get('mentions'):
for mention in event_data['event']['message']['mentions']:
# mention['key']是@内容,例如"@_user_1"
# mention['name']是显示的名字
bot_mention_text = f"@{mention['name']}"
if text_content.startswith(bot_mention_text):
text_content = text_content[len(bot_mention_text):].strip()
break
if not text_content:
logger.warning(f"[Feishu Bot] 移除@后内容为空不处理。消息ID: {message_id}")
return
logger.info(f"[Feishu Bot] 清理后的消息内容: '{text_content}'")
# 3. 调用核心服务获取回复
assistant = service_manager.get_assistant()
# 注意process_message_agent 是一个异步方法,需要处理
# 在同步线程中运行异步方法
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError: # 'RuntimeError: There is no current event loop...'
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 调用对话服务
logger.info(f"[Feishu Bot] 调用Agent服务处理消息...")
response_data = loop.run_until_complete(
assistant.process_message_agent(message=text_content, user_id=f"feishu_{chat_id}")
)
logger.info(f"[Feishu Bot] Agent服务返回结果: {response_data}")
# 4. 提取回复并发送
reply_text = response_data.get("message", "抱歉,我暂时无法回答这个问题。")
if isinstance(reply_text, dict): # 有时候返回的可能是字典
reply_text = reply_text.get('content', str(reply_text))
logger.info(f"[Feishu Bot] 准备发送回复到飞书: '{reply_text}'")
success = feishu_service.reply_to_message(message_id, reply_text)
if success:
logger.info(f"[Feishu Bot] 成功回复消息到飞书。消息ID: {message_id}")
else:
logger.error(f"[Feishu Bot] 回复消息到飞书失败。消息ID: {message_id}")
except Exception as e:
logger.error(f"[Feishu Bot] 后台处理消息时发生严重错误: {e}", exc_info=True)
@feishu_bot_bp.route('/event', methods=['POST'])
def handle_feishu_event():
"""
接收并处理飞书事件回调
"""
# 1. 解析请求
data = request.json
logger.info(f"[Feishu Bot] 收到飞书事件回调:\n{json.dumps(data, indent=2)}")
# 2. 安全校验 (如果配置了)
# 此处可以添加Verification Token的校验逻辑
# headers = request.headers
# ...
# 3. 处理URL验证挑战
if data and data.get("type") == "url_verification":
challenge = data.get("challenge", "")
logger.info(f"[Feishu Bot] 收到URL验证请求返回challenge: {challenge}")
return jsonify({"challenge": challenge})
# 4. 处理事件回调
if data and data.get("header", {}).get("event_type") == "im.message.receive_v1":
# 立即响应飞书,防止超时重试
threading.Thread(
target=_process_message_in_background,
args=(request.environ['werkzeug.request'].environ['flask.app'].app_context(), data)
).start()
logger.info("[Feishu Bot] 已将消息处理任务推送到后台线程并立即响应200 OK")
return jsonify({"status": "processing"})
# 5. 对于其他未知事件,也返回成功,避免飞书重试
logger.warning(f"[Feishu Bot] 收到未知类型的事件: {data.get('header', {}).get('event_type')}")
return jsonify({"status": "ignored"})

View File

@@ -7,203 +7,135 @@
import os
import tempfile
import uuid
import logging
from flask import Blueprint, request, jsonify
from src.agent_assistant import TSPAgentAssistant
from src.web.service_manager import service_manager
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
knowledge_bp = Blueprint('knowledge', __name__, url_prefix='/api/knowledge')
_agent_assistant = None
def get_agent_assistant():
"""获取Agent助手实例懒加载"""
global _agent_assistant
if '_agent_assistant' not in globals():
if _agent_assistant is None:
_agent_assistant = TSPAgentAssistant()
return _agent_assistant
@knowledge_bp.route('')
@handle_api_errors
def get_knowledge():
"""获取知识库列表(分页)"""
try:
# 获取分页参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
category_filter = request.args.get('category', '')
verified_filter = request.args.get('verified', '')
# 从数据库获取知识库数据
from src.core.database import db_manager
from src.core.models import KnowledgeEntry
with db_manager.get_session() as session:
# 构建查询
query = session.query(KnowledgeEntry).filter(KnowledgeEntry.is_active == True)
# 应用过滤器
if category_filter:
query = query.filter(KnowledgeEntry.category == category_filter)
if verified_filter:
if verified_filter == 'true':
query = query.filter(KnowledgeEntry.is_verified == True)
elif verified_filter == 'false':
query = query.filter(KnowledgeEntry.is_verified == False)
# 按创建时间倒序排列
query = query.order_by(KnowledgeEntry.created_at.desc())
# 计算总数
total = query.count()
# 分页查询
knowledge_entries = query.offset((page - 1) * per_page).limit(per_page).all()
# 转换为字典
knowledge_data = []
for entry in knowledge_entries:
knowledge_data.append({
'id': entry.id,
'question': entry.question,
'answer': entry.answer,
'category': entry.category,
'confidence_score': entry.confidence_score,
'usage_count': entry.usage_count,
'is_verified': entry.is_verified,
'is_active': entry.is_active,
'created_at': entry.created_at.isoformat() if entry.created_at else None,
'updated_at': entry.updated_at.isoformat() if entry.updated_at else None
})
# 计算分页信息
total_pages = (total + per_page - 1) // per_page
return jsonify({
'knowledge': knowledge_data,
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages
})
except Exception as e:
return jsonify({"error": str(e)}), 500
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
category_filter = request.args.get('category', '')
verified_filter = request.args.get('verified', '')
result = service_manager.get_assistant().knowledge_manager.get_knowledge_paginated(
page=page,
per_page=per_page,
category_filter=category_filter,
verified_filter=verified_filter
)
return jsonify(result)
@knowledge_bp.route('/search')
@handle_api_errors
def search_knowledge():
"""搜索知识库"""
try:
query = request.args.get('q', '')
import logging
logger = logging.getLogger(__name__)
logger.info(f"搜索查询: '{query}'")
if not query.strip():
logger.info("查询为空,返回空结果")
return jsonify([])
# 直接调用知识库管理器的搜索方法
assistant = service_manager.get_assistant()
results = assistant.knowledge_manager.search_knowledge(query, top_k=5)
logger.info(f"搜索结果数量: {len(results)}")
return jsonify(results)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"搜索知识库失败: {e}")
return jsonify({"error": str(e)}), 500
query = request.args.get('q', '')
logger = logging.getLogger(__name__)
logger.info(f"搜索查询: '{query}'")
if not query.strip():
logger.info("查询为空,返回空结果")
return jsonify([])
assistant = service_manager.get_assistant()
results = assistant.knowledge_manager.search_knowledge(query, top_k=5)
logger.info(f"搜索结果数量: {len(results)}")
return jsonify(results)
@knowledge_bp.route('', methods=['POST'])
@handle_api_errors
def add_knowledge():
"""添加知识库条目"""
try:
data = request.get_json()
success = service_manager.get_assistant().knowledge_manager.add_knowledge_entry(
question=data['question'],
answer=data['answer'],
category=data['category'],
confidence_score=data['confidence_score']
)
return jsonify({"success": success, "message": "知识添加成功" if success else "添加失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
data = request.get_json()
success = service_manager.get_assistant().knowledge_manager.add_knowledge_entry(
question=data['question'],
answer=data['answer'],
category=data['category'],
confidence_score=data.get('confidence_score', 0.8)
)
if success:
return create_success_response("知识添加成功")
else:
return create_error_response("添加失败", 500)
@knowledge_bp.route('/stats')
@handle_api_errors
def get_knowledge_stats():
"""获取知识库统计"""
try:
stats = service_manager.get_assistant().knowledge_manager.get_knowledge_stats()
return jsonify(stats)
except Exception as e:
return jsonify({"error": str(e)}), 500
stats = service_manager.get_assistant().knowledge_manager.get_knowledge_stats()
return jsonify(stats)
@knowledge_bp.route('/upload', methods=['POST'])
@handle_api_errors
def upload_knowledge_file():
"""上传文件并生成知识库"""
if 'file' not in request.files:
return create_error_response("没有上传文件", 400)
file = request.files['file']
if file.filename == '':
return create_error_response("没有选择文件", 400)
temp_filename = f"upload_{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
try:
if 'file' not in request.files:
return jsonify({"error": "没有上传文件"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "没有选择文件"}), 400
# 保存文件到临时目录
import tempfile
import os
import uuid
# 创建唯一的临时文件名
temp_filename = f"upload_{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
file.save(temp_path)
assistant = get_agent_assistant()
result = assistant.process_file_to_knowledge(temp_path, file.filename)
return jsonify(result)
finally:
try:
# 保存文件
file.save(temp_path)
# 使用Agent助手处理文件
result = get_agent_assistant().process_file_to_knowledge(temp_path, file.filename)
return jsonify(result)
finally:
# 确保删除临时文件
try:
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as cleanup_error:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"清理临时文件失败: {cleanup_error}")
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"文件上传处理失败: {e}")
return jsonify({"error": str(e)}), 500
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as cleanup_error:
logger = logging.getLogger(__name__)
logger.warning(f"清理临时文件失败: {cleanup_error}")
@knowledge_bp.route('/delete/<int:knowledge_id>', methods=['DELETE'])
@handle_api_errors
def delete_knowledge(knowledge_id):
"""删除知识库条目"""
try:
success = service_manager.get_assistant().knowledge_manager.delete_knowledge_entry(knowledge_id)
return jsonify({"success": success, "message": "删除成功" if success else "删除失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
success = service_manager.get_assistant().knowledge_manager.delete_knowledge_entry(knowledge_id)
if success:
return create_success_response("删除成功")
else:
return create_error_response("删除失败", 404)
@knowledge_bp.route('/verify/<int:knowledge_id>', methods=['POST'])
@handle_api_errors
def verify_knowledge(knowledge_id):
"""验证知识库条目"""
try:
data = request.get_json() or {}
verified_by = data.get('verified_by', 'admin')
success = service_manager.get_assistant().knowledge_manager.verify_knowledge_entry(knowledge_id, verified_by)
return jsonify({"success": success, "message": "验证成功" if success else "验证失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
data = request.get_json() or {}
verified_by = data.get('verified_by', 'admin')
success = service_manager.get_assistant().knowledge_manager.verify_knowledge_entry(knowledge_id, verified_by)
if success:
return create_success_response("验证成功")
else:
return create_error_response("验证失败", 404)
@knowledge_bp.route('/unverify/<int:knowledge_id>', methods=['POST'])
@handle_api_errors
def unverify_knowledge(knowledge_id):
"""取消验证知识库条目"""
try:
success = service_manager.get_assistant().knowledge_manager.unverify_knowledge_entry(knowledge_id)
return jsonify({"success": success, "message": "取消验证成功" if success else "取消验证失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
success = service_manager.get_assistant().knowledge_manager.unverify_knowledge_entry(knowledge_id)
if success:
return create_success_response("取消验证成功")
else:
return create_error_response("取消验证失败", 404)

View File

@@ -16,27 +16,8 @@ from sqlalchemy import text
logger = logging.getLogger(__name__)
# 简化的AI准确率配置类
class SimpleAIAccuracyConfig:
"""简化的AI准确率配置"""
def __init__(self):
self.auto_approve_threshold = 0.95
self.use_human_resolution_threshold = 0.90
self.manual_review_threshold = 0.80
self.ai_suggestion_confidence = 0.95
self.human_resolution_confidence = 0.90
def should_auto_approve(self, similarity: float) -> bool:
return similarity >= self.auto_approve_threshold
def should_use_human_resolution(self, similarity: float) -> bool:
return similarity < self.use_human_resolution_threshold
def get_confidence_score(self, similarity: float, use_human: bool = False) -> float:
if use_human:
return self.human_resolution_confidence
else:
return max(similarity, self.ai_suggestion_confidence)
# 移除SimpleAIAccuracyConfig直接从统一配置获取
from src.config.unified_config import get_config
from src.main import TSPAssistant
from src.core.database import db_manager
@@ -397,14 +378,14 @@ def save_workorder_human_resolution(workorder_id):
rec.ai_similarity = sim
# 使用简化的配置
config = SimpleAIAccuracyConfig()
config = get_config().ai_accuracy
# 自动审批条件
approved = config.should_auto_approve(sim)
approved = sim >= config.auto_approve_threshold
rec.approved = approved
# 记录使用人工描述入库的标记当AI准确率低于阈值时
use_human_resolution = config.should_use_human_resolution(sim)
use_human_resolution = sim < config.use_human_resolution_threshold
rec.use_human_resolution = use_human_resolution
session.commit()
@@ -431,19 +412,19 @@ def approve_workorder_to_knowledge(workorder_id):
return jsonify({"error": "未找到工单建议记录"}), 400
# 使用简化的配置
config = SimpleAIAccuracyConfig()
config = get_config().ai_accuracy
# 确定使用哪个内容入库
if rec.use_human_resolution and rec.human_resolution:
# AI准确率低于阈值使用人工描述入库
answer_content = rec.human_resolution
confidence_score = config.get_confidence_score(rec.ai_similarity or 0, use_human=True)
confidence_score = config.human_resolution_confidence
verified_by = 'human_resolution'
logger.info(f"工单 {workorder_id} 使用人工描述入库AI相似度: {rec.ai_similarity:.4f}")
elif rec.approved and rec.ai_suggestion:
# AI准确率≥阈值使用AI建议入库
answer_content = rec.ai_suggestion
confidence_score = config.get_confidence_score(rec.ai_similarity or 0, use_human=False)
confidence_score = max(rec.ai_similarity or 0, config.ai_suggestion_confidence)
verified_by = 'auto_approve'
logger.info(f"工单 {workorder_id} 使用AI建议入库相似度: {rec.ai_similarity:.4f}")
else:

View File

@@ -21,7 +21,7 @@ class ServiceManager:
if service_name not in self._services:
try:
self._services[service_name] = factory_func()
logger.info(f"服务 {service_name} 已初始化")
logger.debug(f"服务 {service_name} 已初始化")
except Exception as e:
logger.error(f"初始化服务 {service_name} 失败: {e}")
raise

View File

@@ -90,8 +90,21 @@ class WebSocketServer:
await self._send_error(websocket, "缺少必要参数", message_id)
return
# 获取客户端IP
ip_address = None
try:
# websockets 15.x 获取 remote_address 的方式
ip_address = websocket.remote_address[0] if websocket.remote_address else None
except Exception:
pass
# 处理消息
result = self.chat_manager.process_message(session_id, message)
result = self.chat_manager.process_message(
session_id,
message,
ip_address=ip_address,
invocation_method="websocket"
)
response = {
"type": "message_response",
@@ -210,21 +223,9 @@ class WebSocketServer:
await websocket.send(json.dumps(response, ensure_ascii=False))
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
"""处理客户端连接"""
# 检查连接头
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
await websocket.close(code=1002, reason="Invalid connection header")
return
async def handle_client(self, websocket):
"""处理客户端连接(兼容 websockets 15.x"""
# websockets 15.x 版本中handler 只接收 websocket 参数,不再有 path 参数
await self.register_client(websocket)
try:
@@ -238,61 +239,17 @@ class WebSocketServer:
await self.unregister_client(websocket)
async def start_server(self):
"""启动WebSocket服务器"""
"""启动WebSocket服务器(兼容 websockets 15.x"""
logger.info(f"启动WebSocket服务器: ws://{self.host}:{self.port}")
# 添加CORS支持
async def handle_client_with_cors(websocket: WebSocketServerProtocol):
# 获取pathwebsockets在提供process_request时不会将path传递给handler
path = websocket.path
# 设置CORS头
if websocket.request_headers.get("Origin"):
# 允许跨域连接
pass
await self.handle_client(websocket, path)
# websockets 15.x 简化版本:直接传递处理函数
async with websockets.serve(
handle_client_with_cors,
self.handle_client,
self.host,
self.port,
# 添加额外的服务器选项
process_request=self._process_request
self.port
):
await asyncio.Future() # 保持服务器运行
def _process_request(self, path, request_headers):
"""处理HTTP请求支持CORS"""
# 检查是否是WebSocket升级请求
# request_headers 可能是 Headers 对象或 Request 对象
if hasattr(request_headers, 'get'):
upgrade_header = request_headers.get("Upgrade", "").lower()
elif hasattr(request_headers, 'headers'):
upgrade_header = request_headers.headers.get("Upgrade", "").lower()
else:
upgrade_header = ""
if upgrade_header == "websocket":
return None # 允许WebSocket连接
# 对于非WebSocket请求返回简单的HTML页面
return (
200,
[("Content-Type", "text/html; charset=utf-8")],
b"""
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Server</title>
</head>
<body>
<h1>WebSocket Server is running</h1>
<p>This is a WebSocket server. Please use a WebSocket client to connect.</p>
<p>WebSocket URL: ws://localhost:8765</p>
</body>
</html>
"""
)
def run(self):
"""运行服务器"""
asyncio.run(self.start_server())