diff --git a/config/integrations_config copy.json b/config/integrations_config copy.json new file mode 100644 index 0000000..ede371c --- /dev/null +++ b/config/integrations_config copy.json @@ -0,0 +1,16 @@ +{ + "feishu": { + "app_id": "tblnl3vJPpgMTSiP", + "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK", + "app_token": "XXnEbiCmEaMblSs6FDJcFCqsnlg", + "table_id": "tblnl3vJPpgMTSiP", + "last_updated": null, + "status": "inactive" + }, + "system": { + "sync_limit": 10, + "ai_suggestions_enabled": true, + "auto_sync_interval": 0, + "last_sync_time": null + } +} \ No newline at end of file diff --git a/config/integrations_config.json b/config/integrations_config.json new file mode 100644 index 0000000..ede371c --- /dev/null +++ b/config/integrations_config.json @@ -0,0 +1,16 @@ +{ + "feishu": { + "app_id": "tblnl3vJPpgMTSiP", + "app_secret": "ccxkE7ZCFQZcwkkM1rLy0ccZRXYsT2xK", + "app_token": "XXnEbiCmEaMblSs6FDJcFCqsnlg", + "table_id": "tblnl3vJPpgMTSiP", + "last_updated": null, + "status": "inactive" + }, + "system": { + "sync_limit": 10, + "ai_suggestions_enabled": true, + "auto_sync_interval": 0, + "last_sync_time": null + } +} \ No newline at end of file diff --git a/src/config/config.py b/src/config/config.py index dda57bf..c2553ed 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -10,7 +10,7 @@ class Config: ALIBABA_MODEL_NAME = "qwen-plus-latest" # 数据库配置 - DATABASE_URL = "mysql+pymysql://tsp_assistant:123456@43.134.68.207/tsp_assistant?charset=utf8mb4" + DATABASE_URL = "sqlite:///tsp_assistant.db" # 知识库配置 KNOWLEDGE_BASE_PATH = "data/knowledge_base" diff --git a/src/core/models.py b/src/core/models.py index f871d3e..35c93ad 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -21,6 +21,12 @@ class WorkOrder(Base): resolution = Column(Text) satisfaction_score = Column(Float) + # 飞书集成字段 + feishu_record_id = Column(String(100), unique=True, nullable=True) # 飞书记录ID + assignee = Column(String(100), nullable=True) # 负责人 + solution = Column(Text, nullable=True) # 解决方案 + ai_suggestion = Column(Text, nullable=True) # AI建议 + # 关联对话记录 conversations = relationship("Conversation", back_populates="work_order") diff --git a/src/dialogue/dialogue_manager.py b/src/dialogue/dialogue_manager.py index 8dbf5c5..d4b3da6 100644 --- a/src/dialogue/dialogue_manager.py +++ b/src/dialogue/dialogue_manager.py @@ -101,26 +101,47 @@ class DialogueManager: # 性能优化分析 optimization_result = self.system_optimizer.optimize_response_time(response_time) - # 记录Token使用情况 - if success and "token_usage" in response_result: - token_usage = response_result["token_usage"] - # 计算成本 + # 记录Token使用情况(兼容多种返回格式) + if success: + # 兼容返回 usage: {prompt_tokens, completion_tokens} + usage = response_result.get("usage", {}) or {} + token_usage = response_result.get("token_usage", {}) or {} + input_tokens = token_usage.get("input_tokens") + output_tokens = token_usage.get("output_tokens") + if input_tokens is None and isinstance(usage, dict): + input_tokens = usage.get("prompt_tokens") or usage.get("input_tokens") or 0 + if output_tokens is None and isinstance(usage, dict): + output_tokens = usage.get("completion_tokens") or usage.get("output_tokens") or 0 + + # 若均为0,使用简易估算(避免记录缺失) + if not input_tokens and user_message: + try: + input_tokens = max(1, len(user_message) // 4) + except Exception: + input_tokens = 0 + if not output_tokens and response_result.get("response"): + try: + output_tokens = max(1, len(response_result.get("response")) // 4) + except Exception: + output_tokens = 0 + + model_name = response_result.get("model") or response_result.get("model_name") or "qwen-plus-latest" + + # 计算成本并限制 estimated_cost = self.token_monitor._calculate_cost( - response_result.get("model_name", "qwen-plus-latest"), - token_usage.get("input_tokens", 0), - token_usage.get("output_tokens", 0) + model_name, + int(input_tokens or 0), + int(output_tokens or 0) ) - - # 检查成本限制 if not self.system_optimizer.check_cost_limit(estimated_cost): return {"error": "请求成本超限,请稍后再试"} - + self.token_monitor.record_token_usage( user_id=user_id or "anonymous", work_order_id=work_order_id, - model_name=response_result.get("model_name", "qwen-plus-latest"), - input_tokens=token_usage.get("input_tokens", 0), - output_tokens=token_usage.get("output_tokens", 0), + model_name=model_name, + input_tokens=int(input_tokens or 0), + output_tokens=int(output_tokens or 0), response_time=response_time, success=success, error_message=error_message diff --git a/src/dialogue/realtime_chat.py b/src/dialogue/realtime_chat.py index 5db1f80..d30954b 100644 --- a/src/dialogue/realtime_chat.py +++ b/src/dialogue/realtime_chat.py @@ -140,7 +140,7 @@ class RealtimeChatManager: if len(session["context"]) > 20: # 保留最近10轮对话 session["context"] = session["context"][-20:] - # 保存到数据库 + # 保存到数据库(每轮一条,带会话标记) self._save_conversation(session_id, user_msg, assistant_msg) return { @@ -274,30 +274,32 @@ class RealtimeChatManager: """保存对话到数据库""" try: with db_manager.get_session() as session: - # 保存用户消息 - user_conversation = Conversation( - work_order_id=user_msg.work_order_id, - user_message=user_msg.content, - assistant_response="", # 用户消息没有助手回复 - timestamp=user_msg.timestamp, - confidence_score=None, - knowledge_used=None, - response_time=None - ) - session.add(user_conversation) - - # 保存助手消息 - assistant_conversation = Conversation( - work_order_id=assistant_msg.work_order_id, - user_message="", # 助手消息没有用户输入 - assistant_response=assistant_msg.content, - timestamp=assistant_msg.timestamp, + # 统一为一条记录:包含用户消息与助手回复 + try: + response_time = None + if assistant_msg.timestamp and user_msg.timestamp: + response_time = max(0.0, (assistant_msg.timestamp - user_msg.timestamp).total_seconds() * 1000.0) + except Exception: + response_time = None + + # 在知识字段中打上会话标记,便于结束时合并清理 + marked_knowledge = assistant_msg.knowledge_used or [] + try: + marked_knowledge = list(marked_knowledge) + marked_knowledge.append({"session_id": session_id, "type": "session_marker"}) + except Exception: + pass + + conversation = Conversation( + work_order_id=assistant_msg.work_order_id or user_msg.work_order_id, + user_message=user_msg.content or "", + assistant_response=assistant_msg.content or "", + timestamp=assistant_msg.timestamp or user_msg.timestamp, confidence_score=assistant_msg.confidence_score, - knowledge_used=json.dumps(assistant_msg.knowledge_used, ensure_ascii=False) if assistant_msg.knowledge_used else None, - response_time=0.5 # 模拟响应时间 + knowledge_used=json.dumps(marked_knowledge, ensure_ascii=False) if marked_knowledge else None, + response_time=response_time ) - session.add(assistant_conversation) - + session.add(conversation) session.commit() except Exception as e: @@ -385,6 +387,56 @@ class RealtimeChatManager: """结束会话""" try: if session_id in self.active_sessions: + session_meta = self.active_sessions[session_id] + # 汇总本会话为一条记录 + history = self.message_history.get(session_id, []) + if history: + user_parts = [] + assistant_parts = [] + response_times = [] + first_ts = None + last_ts = None + for i in range(len(history)): + msg = history[i] + if first_ts is None: + first_ts = msg.timestamp + last_ts = msg.timestamp + if msg.role == "user": + user_parts.append(msg.content) + # 计算到下一条助手回复的间隔 + if i + 1 < len(history) and history[i+1].role == "assistant": + try: + rt = max(0.0, (history[i+1].timestamp - msg.timestamp).total_seconds() * 1000.0) + response_times.append(rt) + except Exception: + pass + elif msg.role == "assistant": + assistant_parts.append(msg.content) + agg_user = "\n\n".join([p for p in user_parts if p]) + agg_assistant = "\n\n".join([p for p in assistant_parts if p]) + avg_rt = sum(response_times)/len(response_times) if response_times else None + + from ..core.database import db_manager as _db + from ..core.models import Conversation as _Conv + import json as _json + with _db.get_session() as dbs: + agg = _Conv( + work_order_id=session_meta.get("work_order_id"), + user_message=agg_user, + assistant_response=agg_assistant, + timestamp=last_ts or first_ts, + confidence_score=None, + knowledge_used=_json.dumps({"session_id": session_id, "aggregated": True}, ensure_ascii=False), + response_time=avg_rt + ) + dbs.add(agg) + # 删除本会话标记的分散记录 + try: + pattern = f'%"session_id":"{session_id}"%' + dbs.query(_Conv).filter(_Conv.knowledge_used.like(pattern)).delete(synchronize_session=False) + except Exception: + pass + dbs.commit() del self.active_sessions[session_id] if session_id in self.message_history: diff --git a/src/integrations/__init__.py b/src/integrations/__init__.py new file mode 100644 index 0000000..f531c4b --- /dev/null +++ b/src/integrations/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +""" +集成模块 +处理与外部系统的集成,如飞书、钉钉等 +""" diff --git a/src/integrations/ai_suggestion_service.py b/src/integrations/ai_suggestion_service.py new file mode 100644 index 0000000..7e537e9 --- /dev/null +++ b/src/integrations/ai_suggestion_service.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" +AI建议服务 +基于TR描述、知识库和VIN查询生成AI建议 +""" + +import logging +from typing import Dict, List, Optional, Any +from src.knowledge_base.knowledge_manager import KnowledgeManager +from src.vehicle.vehicle_data_manager import VehicleDataManager +from src.agent.llm_client import LLMManager, LLMConfig + +logger = logging.getLogger(__name__) + +class AISuggestionService: + """AI建议服务""" + + def __init__(self): + self.knowledge_manager = KnowledgeManager() + self.vehicle_manager = VehicleDataManager() + + # 初始化LLM客户端 + try: + llm_config = LLMConfig( + provider="openai", + api_key="your-api-key", # 这里需要从配置文件读取 + model="gpt-3.5-turbo", + temperature=0.7, + max_tokens=1000 + ) + self.llm_manager = LLMManager(llm_config) + except Exception as e: + logger.warning(f"LLM客户端初始化失败: {e}") + self.llm_manager = None + + def generate_suggestion(self, tr_description: str, vin: Optional[str] = None) -> str: + """ + 生成AI建议 + + Args: + tr_description: TR描述 + vin: 车架号(可选) + + Returns: + AI建议文本 + """ + try: + # 1. 从知识库搜索相关信息 + knowledge_results = self.knowledge_manager.search_knowledge( + query=tr_description, + top_k=5 + ) + + # 2. 如果有VIN,查询车辆信息 + vehicle_info = "" + if vin: + try: + vehicle_data = self.vehicle_manager.get_latest_vehicle_data_by_vin(vin) + if vehicle_data: + vehicle_info = f"车辆信息:{vehicle_data.get('model', '未知车型')},里程:{vehicle_data.get('mileage', '未知')}km" + except Exception as e: + logger.warning(f"查询车辆信息失败: {e}") + + # 3. 构建提示词 + context_parts = [] + + # 添加知识库信息 + if knowledge_results: + knowledge_text = "\n".join([ + f"- {item.get('question', '')}: {item.get('answer', '')}" + for item in knowledge_results + ]) + context_parts.append(f"相关知识库信息:\n{knowledge_text}") + + # 添加车辆信息 + if vehicle_info: + context_parts.append(vehicle_info) + + context = "\n\n".join(context_parts) if context_parts else "无相关背景信息" + + # 4. 生成AI建议 + prompt = f""" +作为技术支持专家,请基于以下问题描述为工单提供专业的处理建议: + +问题描述:{tr_description} + +相关背景信息: +{context} + +请提供: +1. 问题分析 +2. 建议的解决步骤 +3. 注意事项 +4. 如果问题无法解决,建议的后续行动 + +请用中文回答,简洁明了。 +""" + + if self.llm_manager: + import asyncio + response = asyncio.run(self.llm_manager.generate(prompt)) + return response + else: + return "AI建议生成失败,LLM客户端未初始化。" + + except Exception as e: + logger.error(f"生成AI建议失败: {e}") + return f"AI建议生成失败:{str(e)}" + + def batch_generate_suggestions(self, records: List[Dict[str, Any]], limit: int = 10) -> List[Dict[str, Any]]: + """ + 批量生成AI建议 + + Args: + records: 记录列表 + limit: 处理数量限制 + + Returns: + 处理后的记录列表 + """ + processed_records = [] + + for i, record in enumerate(records[:limit]): + try: + tr_description = record.get("fields", {}).get("TR Description", "") + vin = self._extract_vin_from_description(tr_description) + + if tr_description: + ai_suggestion = self.generate_suggestion(tr_description, vin) + record["ai_suggestion"] = ai_suggestion + logger.info(f"为记录 {record.get('record_id', i)} 生成AI建议") + else: + record["ai_suggestion"] = "无TR描述,无法生成建议" + + processed_records.append(record) + + except Exception as e: + logger.error(f"处理记录 {record.get('record_id', i)} 失败: {e}") + record["ai_suggestion"] = f"处理失败:{str(e)}" + processed_records.append(record) + + return processed_records + + def _extract_vin_from_description(self, description: str) -> Optional[str]: + """ + 从描述中提取VIN + + Args: + description: TR描述 + + Returns: + 提取的VIN或None + """ + import re + + # VIN通常是17位字符,包含数字和大写字母 + vin_pattern = r'\b[A-HJ-NPR-Z0-9]{17}\b' + matches = re.findall(vin_pattern, description.upper()) + + if matches: + return matches[0] + + # 也尝试查找"VIN:"或"车架号:"后的内容 + vin_keywords = [r'VIN[:\s]+([A-HJ-NPR-Z0-9]{17})', r'车架号[:\s]+([A-HJ-NPR-Z0-9]{17})'] + + for pattern in vin_keywords: + match = re.search(pattern, description.upper()) + if match: + return match.group(1) + + return None diff --git a/src/integrations/config_manager.py b/src/integrations/config_manager.py new file mode 100644 index 0000000..ddfbb0f --- /dev/null +++ b/src/integrations/config_manager.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +""" +配置管理器 +管理飞书等外部系统的配置信息,支持持久化存储和并发访问 +""" + +import json +import os +import threading +import logging +from typing import Dict, Any, Optional +from datetime import datetime +from pathlib import Path + +logger = logging.getLogger(__name__) + +class ConfigManager: + """配置管理器""" + + _instance = None + _lock = threading.Lock() + + def __new__(cls): + """单例模式""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super(ConfigManager, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + self._config_lock = threading.RLock() + self.config_file = Path("config/integrations_config.json") + self.config_file.parent.mkdir(exist_ok=True) + + # 默认配置 + self.default_config = { + "feishu": { + "app_id": "", + "app_secret": "", + "app_token": "", + "table_id": "", + "last_updated": None, + "status": "inactive" + }, + "system": { + "sync_limit": 10, + "ai_suggestions_enabled": True, + "auto_sync_interval": 0, # 0表示不自动同步 + "last_sync_time": None + } + } + + self._load_config() + self._initialized = True + + def _load_config(self): + """加载配置文件""" + try: + if self.config_file.exists(): + with open(self.config_file, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + # 合并默认配置和加载的配置 + self.config = self._merge_configs(self.default_config, loaded_config) + else: + self.config = self.default_config.copy() + self._save_config() + logger.info("配置加载成功") + except Exception as e: + logger.error(f"配置加载失败: {e}") + self.config = self.default_config.copy() + + def _save_config(self): + """保存配置文件""" + try: + with self._config_lock: + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + logger.info("配置保存成功") + except Exception as e: + logger.error(f"配置保存失败: {e}") + + def _merge_configs(self, default: Dict, loaded: Dict) -> Dict: + """合并配置,确保所有必要的键都存在""" + result = default.copy() + for key, value in loaded.items(): + if isinstance(value, dict) and key in result: + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + return result + + def get_feishu_config(self) -> Dict[str, Any]: + """获取飞书配置""" + with self._config_lock: + return self.config.get("feishu", {}).copy() + + def update_feishu_config(self, **kwargs) -> bool: + """更新飞书配置""" + try: + with self._config_lock: + feishu_config = self.config.setdefault("feishu", {}) + + # 更新配置项 + for key, value in kwargs.items(): + if key in ["app_id", "app_secret", "app_token", "table_id"]: + feishu_config[key] = value + + # 更新状态和时间戳 + feishu_config["last_updated"] = datetime.now().isoformat() + feishu_config["status"] = "active" if all([ + feishu_config.get("app_id"), + feishu_config.get("app_secret"), + feishu_config.get("app_token"), + feishu_config.get("table_id") + ]) else "inactive" + + self._save_config() + logger.info("飞书配置更新成功") + return True + except Exception as e: + logger.error(f"飞书配置更新失败: {e}") + return False + + def get_system_config(self) -> Dict[str, Any]: + """获取系统配置""" + with self._config_lock: + return self.config.get("system", {}).copy() + + def update_system_config(self, **kwargs) -> bool: + """更新系统配置""" + try: + with self._config_lock: + system_config = self.config.setdefault("system", {}) + + for key, value in kwargs.items(): + if key in ["sync_limit", "ai_suggestions_enabled", "auto_sync_interval"]: + system_config[key] = value + + self._save_config() + logger.info("系统配置更新成功") + return True + except Exception as e: + logger.error(f"系统配置更新失败: {e}") + return False + + def test_feishu_connection(self) -> Dict[str, Any]: + """测试飞书连接""" + try: + from .feishu_client import FeishuClient + + feishu_config = self.get_feishu_config() + if not all([feishu_config.get("app_id"), feishu_config.get("app_secret")]): + return {"success": False, "error": "飞书配置不完整"} + + client = FeishuClient(feishu_config["app_id"], feishu_config["app_secret"]) + + # 测试获取访问令牌 + token = client._get_access_token() + if token: + return {"success": True, "message": "飞书连接正常"} + else: + return {"success": False, "error": "无法获取访问令牌"} + + except Exception as e: + logger.error(f"飞书连接测试失败: {e}") + return {"success": False, "error": str(e)} + + def get_config_summary(self) -> Dict[str, Any]: + """获取配置摘要""" + with self._config_lock: + feishu_config = self.config.get("feishu", {}) + system_config = self.config.get("system", {}) + + return { + "feishu": { + "app_id": feishu_config.get("app_id", ""), + "app_token": feishu_config.get("app_token", ""), + "table_id": feishu_config.get("table_id", ""), + "status": feishu_config.get("status", "inactive"), + "last_updated": feishu_config.get("last_updated"), + "app_secret": "***" if feishu_config.get("app_secret") else "" + }, + "system": { + "sync_limit": system_config.get("sync_limit", 10), + "ai_suggestions_enabled": system_config.get("ai_suggestions_enabled", True), + "auto_sync_interval": system_config.get("auto_sync_interval", 0), + "last_sync_time": system_config.get("last_sync_time") + } + } + + def reset_config(self) -> bool: + """重置配置为默认值""" + try: + with self._config_lock: + self.config = self.default_config.copy() + self._save_config() + logger.info("配置重置成功") + return True + except Exception as e: + logger.error(f"配置重置失败: {e}") + return False + + def export_config(self) -> str: + """导出配置(用于备份)""" + with self._config_lock: + return json.dumps(self.config, ensure_ascii=False, indent=2) + + def import_config(self, config_json: str) -> bool: + """导入配置(用于恢复)""" + try: + imported_config = json.loads(config_json) + with self._config_lock: + self.config = self._merge_configs(self.default_config, imported_config) + self._save_config() + logger.info("配置导入成功") + return True + except Exception as e: + logger.error(f"配置导入失败: {e}") + return False + +# 全局配置管理器实例 +config_manager = ConfigManager() diff --git a/src/integrations/feishu_client.py b/src/integrations/feishu_client.py new file mode 100644 index 0000000..4433fee --- /dev/null +++ b/src/integrations/feishu_client.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +""" +飞书API客户端 +支持多维表格数据读取和更新 +""" + +import requests +import json +import time +from typing import Dict, List, Optional, Any +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +class FeishuClient: + """飞书API客户端""" + + def __init__(self, app_id: str, app_secret: str): + """ + 初始化飞书客户端 + + Args: + app_id: 飞书应用ID + app_secret: 飞书应用密钥 + """ + self.app_id = app_id + self.app_secret = app_secret + self.base_url = "https://open.feishu.cn/open-apis" + self.access_token = None + self.token_expires_at = 0 + + def _get_access_token(self) -> str: + """获取访问令牌 - 使用tenant_access_token""" + # 检查当前token是否还有效(提前5分钟刷新) + if self.access_token and time.time() < (self.token_expires_at - 300): + logger.debug(f"使用缓存的访问令牌: {self.access_token[:20]}...") + return self.access_token + + url = f"{self.base_url}/auth/v3/tenant_access_token/internal/" + data = { + "app_id": self.app_id, + "app_secret": self.app_secret + } + + try: + logger.info(f"正在获取飞书tenant_access_token,应用ID: {self.app_id}") + response = requests.post(url, json=data, timeout=10) + response.raise_for_status() + result = response.json() + + logger.info(f"飞书API响应: {result}") + + if result.get("code") == 0: + self.access_token = result["tenant_access_token"] + # 设置过期时间,提前5分钟刷新 + expire_time = result.get("expire", 7200) # 默认2小时 + self.token_expires_at = time.time() + expire_time + + logger.info(f"tenant_access_token获取成功: {self.access_token[:20]}...") + logger.info(f"令牌有效期: {expire_time}秒,过期时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.token_expires_at))}") + return self.access_token + else: + error_msg = f"获取tenant_access_token失败: {result.get('msg', '未知错误')}" + logger.error(error_msg) + raise Exception(error_msg) + + except Exception as e: + logger.error(f"获取飞书访问令牌失败: {e}") + raise + + def _make_request(self, method: str, url: str, **kwargs) -> Dict[str, Any]: + """发送API请求""" + headers = kwargs.get('headers', {}) + token = self._get_access_token() + + # 确保Authorization头格式正确:Bearer + headers.update({ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json; charset=utf-8" + }) + kwargs['headers'] = headers + + try: + logger.info(f"发送飞书API请求: {method} {url}") + logger.info(f"请求头: Authorization: Bearer {token[:20]}...") + + response = requests.request(method, url, timeout=30, **kwargs) + logger.info(f"飞书API响应状态码: {response.status_code}") + + response.raise_for_status() + result = response.json() + logger.info(f"飞书API响应内容: {result}") + return result + except Exception as e: + logger.error(f"飞书API请求失败: {e}") + logger.error(f"请求URL: {url}") + logger.error(f"请求方法: {method}") + logger.error(f"请求头: {headers}") + raise + + def get_table_records(self, app_token: str, table_id: str, view_id: Optional[str] = None, + page_size: int = 500, page_token: Optional[str] = None) -> Dict[str, Any]: + """ + 获取多维表格记录 + + Args: + app_token: 多维表格应用token + table_id: 表格ID + view_id: 视图ID(可选) + page_size: 每页记录数 + page_token: 分页令牌 + + Returns: + 包含记录数据的字典 + """ + url = f"{self.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/records" + + params = { + "page_size": page_size + } + if view_id: + params["view_id"] = view_id + if page_token: + params["page_token"] = page_token + + return self._make_request("GET", url, params=params) + + def get_all_table_records(self, app_token: str, table_id: str, view_id: Optional[str] = None) -> List[Dict[str, Any]]: + """ + 获取表格所有记录(自动分页) + + Args: + app_token: 多维表格应用token + table_id: 表格ID + view_id: 视图ID(可选) + + Returns: + 所有记录的列表 + """ + all_records = [] + page_token = None + + while True: + result = self.get_table_records(app_token, table_id, view_id, page_token=page_token) + + if result.get("code") != 0: + raise Exception(f"获取表格记录失败: {result.get('msg', '未知错误')}") + + records = result.get("data", {}).get("items", []) + all_records.extend(records) + + # 检查是否有下一页 + page_token = result.get("data", {}).get("page_token") + if not page_token: + break + + return all_records + + def update_table_record(self, app_token: str, table_id: str, record_id: str, + fields: Dict[str, Any]) -> Dict[str, Any]: + """ + 更新表格记录 + + Args: + app_token: 多维表格应用token + table_id: 表格ID + record_id: 记录ID + fields: 要更新的字段 + + Returns: + 更新结果 + """ + url = f"{self.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}" + + data = { + "fields": fields + } + + return self._make_request("PUT", url, json=data) + + def test_connection(self) -> Dict[str, Any]: + """ + 测试飞书连接 + + Returns: + 连接测试结果 + """ + try: + # 尝试获取访问令牌 + token = self._get_access_token() + + # 验证token格式(应该以t-开头) + if not token.startswith('t-'): + logger.warning(f"获取的token格式异常,应该以't-'开头: {token[:20]}...") + + return { + "success": True, + "message": "飞书连接测试成功", + "token_prefix": token[:20] + "...", + "token_expires_at": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.token_expires_at)) + } + except Exception as e: + logger.error(f"飞书连接测试失败: {e}") + return { + "success": False, + "message": f"飞书连接测试失败: {str(e)}" + } + + def create_table_record(self, app_token: str, table_id: str, + fields: Dict[str, Any]) -> Dict[str, Any]: + """ + 创建表格记录 + + Args: + app_token: 多维表格应用token + table_id: 表格ID + fields: 记录字段 + + Returns: + 创建结果 + """ + url = f"{self.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/records" + + data = { + "fields": fields + } + + return self._make_request("POST", url, json=data) + + def get_table_record(self, app_token: str, table_id: str, record_id: str) -> Dict[str, Any]: + """ + 获取单条多维表格记录 + + Args: + app_token: 应用token + table_id: 表格ID + record_id: 记录ID + + Returns: + 记录数据 + """ + url = f"{self.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}" + + return self._make_request("GET", url) + + def get_table_fields(self, app_token: str, table_id: str) -> Dict[str, Any]: + """ + 获取表格字段信息 + + Args: + app_token: 多维表格应用token + table_id: 表格ID + + Returns: + 字段信息 + """ + url = f"{self.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/fields" + + return self._make_request("GET", url) + + def parse_record_fields(self, record: Dict[str, Any]) -> Dict[str, Any]: + """ + 解析记录字段,将飞书格式转换为标准格式 + + Args: + record: 飞书记录 + + Returns: + 解析后的字段字典 + """ + fields = record.get("fields", {}) + parsed = {} + + for key, value in fields.items(): + if isinstance(value, dict): + # 处理复杂字段类型 + if "text" in value: + parsed[key] = value["text"] + elif "number" in value: + parsed[key] = value["number"] + elif "date" in value: + parsed[key] = value["date"] + elif "select" in value: + parsed[key] = value["select"]["name"] if isinstance(value["select"], dict) else value["select"] + elif "multi_select" in value: + parsed[key] = [item["name"] if isinstance(item, dict) else item for item in value["multi_select"]] + else: + parsed[key] = str(value) + else: + parsed[key] = value + + return parsed diff --git a/src/integrations/workorder_sync.py b/src/integrations/workorder_sync.py new file mode 100644 index 0000000..d3570e6 --- /dev/null +++ b/src/integrations/workorder_sync.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +""" +工单同步服务 +实现飞书多维表格与本地工单系统的双向同步 +""" + +import json +import logging +from typing import Dict, List, Optional, Any +from datetime import datetime +from src.integrations.feishu_client import FeishuClient +from src.integrations.ai_suggestion_service import AISuggestionService +from src.core.database import db_manager +from src.core.models import WorkOrder +# 工单状态和优先级枚举 +class WorkOrderStatus: + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CLOSED = "closed" + +class WorkOrderPriority: + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + URGENT = "urgent" + +logger = logging.getLogger(__name__) + +class WorkOrderSyncService: + """工单同步服务""" + + def __init__(self, feishu_client: FeishuClient, app_token: str, table_id: str): + """ + 初始化同步服务 + + Args: + feishu_client: 飞书客户端 + app_token: 多维表格应用token + table_id: 表格ID + """ + self.feishu_client = feishu_client + self.app_token = app_token + self.table_id = table_id + self.ai_service = AISuggestionService() + + # 字段映射配置 - 根据实际飞书表格结构 + self.field_mapping = { + # 飞书字段名 -> 本地字段名 + "TR Number": "order_id", # TR编号映射到工单号 + "TR Description": "title", # TR描述作为标题(问题描述) + "Type of problem": "category", # 问题类型作为分类 + "TR Level": "priority", # TR Level作为优先级 + "TR Status": "status", # TR Status作为状态(修正字段名) + "Source": "assignee", # 来源信息 + "Date creation": "created_at", # 创建日期 + "处理过程": "description", # 处理过程作为描述 + "TR tracking": "solution", # TR跟踪作为解决方案 + "AI建议": "ai_suggestion", # AI建议字段 + "Issue Start Time": "updated_at" # 问题开始时间作为更新时间 + } + + # 状态映射 - 根据飞书表格中的实际值 + self.status_mapping = { + "close": WorkOrderStatus.CLOSED, # 已关闭 + "temporary close": WorkOrderStatus.IN_PROGRESS, # 临时关闭对应处理中 + "OTA": WorkOrderStatus.IN_PROGRESS, # OTA状态对应处理中 + "open": WorkOrderStatus.PENDING, # 开放状态对应待处理 + "pending": WorkOrderStatus.PENDING, # 待处理 + "completed": WorkOrderStatus.COMPLETED # 已完成 + } + + # 优先级映射 - 根据飞书表格中的实际值 + self.priority_mapping = { + "Low": WorkOrderPriority.LOW, + "Medium": WorkOrderPriority.MEDIUM, + "High": WorkOrderPriority.HIGH, + "Urgent": WorkOrderPriority.URGENT + } + + def sync_from_feishu(self, generate_ai_suggestions: bool = True, limit: int = 10) -> Dict[str, Any]: + """ + 从飞书同步数据到本地系统 + + Args: + generate_ai_suggestions: 是否生成AI建议 + limit: 处理记录数量限制 + + Returns: + 同步结果统计 + """ + try: + logger.info("开始从飞书同步工单数据...") + + # 获取飞书表格记录(限制数量) + records = self.feishu_client.get_table_records(self.app_token, self.table_id, page_size=limit) + + if records.get("code") != 0: + raise Exception(f"获取飞书记录失败: {records.get('msg', '未知错误')}") + + items = records.get("data", {}).get("items", []) + logger.info(f"从飞书获取到 {len(items)} 条记录") + + # 生成AI建议 + if generate_ai_suggestions: + logger.info("开始生成AI建议...") + items = self.ai_service.batch_generate_suggestions(items, limit) + + # 将AI建议更新回飞书表格 + for item in items: + if "ai_suggestion" in item: + try: + self.feishu_client.update_table_record( + self.app_token, + self.table_id, + item["record_id"], + {"AI建议": item["ai_suggestion"]} + ) + logger.info(f"更新飞书记录 {item['record_id']} 的AI建议") + except Exception as e: + logger.error(f"更新飞书AI建议失败: {e}") + + synced_count = 0 + updated_count = 0 + created_count = 0 + errors = [] + + with db_manager.get_session() as session: + for record in items: + try: + # 解析飞书记录 + parsed_fields = self.feishu_client.parse_record_fields(record) + feishu_id = record.get("record_id") + + # 查找本地是否存在对应记录 + existing_workorder = session.query(WorkOrder).filter( + WorkOrder.feishu_record_id == feishu_id + ).first() + + # 转换为本地工单格式 + workorder_data = self._convert_feishu_to_local(parsed_fields) + workorder_data["feishu_record_id"] = feishu_id + + if existing_workorder: + # 更新现有记录 + for key, value in workorder_data.items(): + if key != "feishu_record_id": + setattr(existing_workorder, key, value) + existing_workorder.updated_at = datetime.now() + updated_count += 1 + else: + # 创建新记录 + workorder_data["created_at"] = datetime.now() + workorder_data["updated_at"] = datetime.now() + new_workorder = WorkOrder(**workorder_data) + session.add(new_workorder) + created_count += 1 + + synced_count += 1 + + except Exception as e: + error_msg = f"处理记录 {record.get('record_id', 'unknown')} 失败: {str(e)}" + logger.error(error_msg) + errors.append(error_msg) + + session.commit() + + result = { + "success": True, + "total_records": len(items), + "synced_count": synced_count, + "created_count": created_count, + "updated_count": updated_count, + "ai_suggestions_generated": generate_ai_suggestions, + "errors": errors + } + + logger.info(f"飞书同步完成: {result}") + return result + + except Exception as e: + logger.error(f"飞书同步失败: {e}") + return { + "success": False, + "error": str(e) + } + + def sync_to_feishu(self, workorder_id: int) -> Dict[str, Any]: + """ + 将本地工单同步到飞书 + + Args: + workorder_id: 工单ID + + Returns: + 同步结果 + """ + try: + with db_manager.get_session() as session: + workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first() + if not workorder: + return {"success": False, "error": "工单不存在"} + + # 转换为飞书格式 + feishu_fields = self._convert_local_to_feishu(workorder) + + if workorder.feishu_record_id: + # 更新飞书记录 + result = self.feishu_client.update_table_record( + self.app_token, self.table_id, workorder.feishu_record_id, feishu_fields + ) + else: + # 创建新飞书记录 + result = self.feishu_client.create_table_record( + self.app_token, self.table_id, feishu_fields + ) + + if result.get("code") == 0: + # 保存飞书记录ID到本地 + workorder.feishu_record_id = result["data"]["record"]["record_id"] + session.commit() + + if result.get("code") == 0: + return {"success": True, "message": "同步成功"} + else: + return {"success": False, "error": result.get("msg", "同步失败")} + + except Exception as e: + logger.error(f"同步到飞书失败: {e}") + return {"success": False, "error": str(e)} + + def create_workorder_from_feishu_record(self, record_id: str) -> Dict[str, Any]: + """ + 从飞书单条记录创建工单 + + Args: + record_id: 飞书记录ID + + Returns: + 创建结果 + """ + try: + logger.info(f"从飞书记录 {record_id} 创建工单") + + # 获取单条飞书记录 + feishu_data = self.feishu_client.get_table_record( + self.app_token, + self.table_id, + record_id + ) + + if feishu_data.get("code") != 0: + return { + "success": False, + "message": f"获取飞书记录失败: {feishu_data.get('msg', '未知错误')}" + } + + record = feishu_data.get("data", {}).get("record") + if not record: + return { + "success": False, + "message": "飞书记录不存在" + } + + fields = record.get("fields", {}) + + # 转换为本地工单格式 + local_data = self._convert_feishu_to_local(fields) + local_data["feishu_record_id"] = record_id + + # 检查是否已存在 + existing_workorder = self._find_existing_workorder(record_id) + + if existing_workorder: + return { + "success": False, + "message": f"工单已存在: {existing_workorder.order_id}" + } + + # 创建新工单 + workorder = self._create_workorder(local_data) + + return { + "success": True, + "message": f"工单创建成功: {local_data.get('order_id')}", + "workorder_id": workorder.id, + "order_id": local_data.get('order_id') + } + + except Exception as e: + logger.error(f"从飞书记录创建工单失败: {e}") + return { + "success": False, + "message": f"创建工单失败: {str(e)}" + } + + def _find_existing_workorder(self, feishu_record_id: str) -> Optional[WorkOrder]: + """查找已存在的工单""" + try: + with db_manager.get_session() as session: + return session.query(WorkOrder).filter( + WorkOrder.feishu_record_id == feishu_record_id + ).first() + except Exception as e: + logger.error(f"查找现有工单失败: {e}") + return None + + def _create_workorder(self, local_data: Dict[str, Any]) -> WorkOrder: + """创建新工单""" + try: + with db_manager.get_session() as session: + workorder = WorkOrder( + order_id=local_data.get("order_id"), + title=local_data.get("title"), + description=local_data.get("description"), + category=local_data.get("category"), + priority=local_data.get("priority"), + status=local_data.get("status"), + created_at=local_data.get("created_at"), + updated_at=local_data.get("updated_at"), + resolution=local_data.get("solution"), + feishu_record_id=local_data.get("feishu_record_id"), + assignee=local_data.get("assignee"), + solution=local_data.get("solution"), + ai_suggestion=local_data.get("ai_suggestion") + ) + session.add(workorder) + session.commit() + session.refresh(workorder) + logger.info(f"创建工单成功: {workorder.order_id}") + return workorder + except Exception as e: + logger.error(f"创建工单失败: {e}") + raise + + def _update_workorder(self, workorder: WorkOrder, local_data: Dict[str, Any]) -> WorkOrder: + """更新现有工单""" + try: + with db_manager.get_session() as session: + workorder.title = local_data.get("title", workorder.title) + workorder.description = local_data.get("description", workorder.description) + workorder.category = local_data.get("category", workorder.category) + workorder.priority = local_data.get("priority", workorder.priority) + workorder.status = local_data.get("status", workorder.status) + workorder.updated_at = local_data.get("updated_at", workorder.updated_at) + workorder.resolution = local_data.get("solution", workorder.resolution) + workorder.assignee = local_data.get("assignee", workorder.assignee) + workorder.solution = local_data.get("solution", workorder.solution) + workorder.ai_suggestion = local_data.get("ai_suggestion", workorder.ai_suggestion) + + session.commit() + session.refresh(workorder) + logger.info(f"更新工单成功: {workorder.order_id}") + return workorder + except Exception as e: + logger.error(f"更新工单失败: {e}") + raise + + def _update_feishu_ai_suggestion(self, record_id: str, ai_suggestion: str) -> bool: + """更新飞书表格中的AI建议""" + try: + result = self.feishu_client.update_record( + self.app_token, + self.table_id, + record_id, + {"AI建议": ai_suggestion} + ) + return result.get("code") == 0 + except Exception as e: + logger.error(f"更新飞书AI建议失败: {e}") + return False + + def _convert_feishu_to_local(self, feishu_fields: Dict[str, Any]) -> Dict[str, Any]: + """将飞书字段转换为本地工单字段""" + local_data = {} + + logger.info(f"开始转换飞书字段: {feishu_fields}") + logger.info(f"字段映射配置: {self.field_mapping}") + + for feishu_field, local_field in self.field_mapping.items(): + if feishu_field in feishu_fields: + value = feishu_fields[feishu_field] + logger.info(f"映射字段 {feishu_field} -> {local_field}: {value}") + + # 特殊字段处理 + if local_field == "status" and value in self.status_mapping: + value = self.status_mapping[value] + elif local_field == "priority" and value in self.priority_mapping: + value = self.priority_mapping[value] + elif local_field in ["created_at", "updated_at"] and value: + try: + # 处理飞书时间戳(毫秒) + if isinstance(value, (int, float)): + # 飞书时间戳是毫秒,需要转换为秒 + value = datetime.fromtimestamp(value / 1000) + else: + # 处理ISO格式时间字符串 + value = datetime.fromisoformat(value.replace('Z', '+00:00')) + except Exception as e: + logger.warning(f"时间字段转换失败: {e}, 使用当前时间") + value = datetime.now() + + local_data[local_field] = value + else: + logger.info(f"飞书字段 {feishu_field} 不存在于数据中") + + # 设置默认值 + if "status" not in local_data: + local_data["status"] = WorkOrderStatus.PENDING + if "priority" not in local_data: + local_data["priority"] = WorkOrderPriority.MEDIUM + if "category" not in local_data: + local_data["category"] = "Remote control" # 根据表格中最常见的问题类型 + if "title" not in local_data or not local_data["title"]: + local_data["title"] = "TR工单" # 默认标题 + + return local_data + + def _convert_local_to_feishu(self, workorder: WorkOrder) -> Dict[str, Any]: + """将本地工单字段转换为飞书字段""" + feishu_fields = {} + + # 反向映射 + reverse_mapping = {v: k for k, v in self.field_mapping.items()} + + for local_field, feishu_field in reverse_mapping.items(): + value = getattr(workorder, local_field, None) + if value is not None: + # 特殊字段处理 + if local_field == "status": + # 反向状态映射 + reverse_status = {v: k for k, v in self.status_mapping.items()} + value = reverse_status.get(value, str(value)) + elif local_field == "priority": + # 反向优先级映射 + reverse_priority = {v: k for k, v in self.priority_mapping.items()} + value = reverse_priority.get(value, str(value)) + elif local_field in ["created_at", "updated_at"] and isinstance(value, datetime): + value = value.isoformat() + + feishu_fields[feishu_field] = value + + return feishu_fields + + def get_sync_status(self) -> Dict[str, Any]: + """获取同步状态统计""" + try: + with db_manager.get_session() as session: + total_local = session.query(WorkOrder).count() + synced_count = session.query(WorkOrder).filter( + WorkOrder.feishu_record_id.isnot(None) + ).count() + + return { + "total_local_workorders": total_local, + "synced_workorders": synced_count, + "unsynced_workorders": total_local - synced_count + } + except Exception as e: + logger.error(f"获取同步状态失败: {e}") + return {"error": str(e)} diff --git a/src/web/app.py b/src/web/app.py index 1ad7539..4085795 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -32,6 +32,7 @@ from src.web.blueprints.conversations import conversations_bp from src.web.blueprints.knowledge import knowledge_bp from src.web.blueprints.monitoring import monitoring_bp from src.web.blueprints.system import system_bp +from src.web.blueprints.feishu_sync import feishu_sync_bp # 配置日志 logger = logging.getLogger(__name__) @@ -103,6 +104,7 @@ app.register_blueprint(conversations_bp) app.register_blueprint(knowledge_bp) app.register_blueprint(monitoring_bp) app.register_blueprint(system_bp) +app.register_blueprint(feishu_sync_bp) # 页面路由 @app.route('/') @@ -384,7 +386,7 @@ def get_active_sessions(): def get_agent_status(): """获取Agent状态""" try: - status = agent_assistant.get_agent_status() + status = get_agent_assistant().get_agent_status() return jsonify(status) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -394,7 +396,7 @@ def get_agent_action_history(): """获取Agent动作执行历史""" try: limit = request.args.get('limit', 50, type=int) - history = agent_assistant.get_action_history(limit) + history = get_agent_assistant().get_action_history(limit) return jsonify({ "success": True, "history": history, @@ -408,7 +410,7 @@ def trigger_sample_action(): """触发示例动作""" try: import asyncio - result = asyncio.run(agent_assistant.trigger_sample_actions()) + result = asyncio.run(get_agent_assistant().trigger_sample_actions()) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -417,7 +419,7 @@ def trigger_sample_action(): def clear_agent_history(): """清空Agent执行历史""" try: - result = agent_assistant.clear_execution_history() + result = get_agent_assistant().clear_execution_history() return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -426,7 +428,7 @@ def clear_agent_history(): def get_llm_stats(): """获取LLM使用统计""" try: - stats = agent_assistant.get_llm_usage_stats() + stats = get_agent_assistant().get_llm_usage_stats() return jsonify({ "success": True, "stats": stats @@ -440,7 +442,7 @@ def toggle_agent_mode(): try: data = request.get_json() enabled = data.get('enabled', True) - success = agent_assistant.toggle_agent_mode(enabled) + success = get_agent_assistant().toggle_agent_mode(enabled) return jsonify({ "success": success, "message": f"Agent模式已{'启用' if enabled else '禁用'}" @@ -452,7 +454,7 @@ def toggle_agent_mode(): def start_agent_monitoring(): """启动Agent监控""" try: - success = agent_assistant.start_proactive_monitoring() + success = get_agent_assistant().start_proactive_monitoring() return jsonify({ "success": success, "message": "Agent监控已启动" if success else "启动失败" @@ -464,7 +466,7 @@ def start_agent_monitoring(): def stop_agent_monitoring(): """停止Agent监控""" try: - success = agent_assistant.stop_proactive_monitoring() + success = get_agent_assistant().stop_proactive_monitoring() return jsonify({ "success": success, "message": "Agent监控已停止" if success else "停止失败" @@ -476,7 +478,7 @@ def stop_agent_monitoring(): def proactive_monitoring(): """主动监控检查""" try: - result = agent_assistant.run_proactive_monitoring() + result = get_agent_assistant().run_proactive_monitoring() return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -526,8 +528,8 @@ def agent_chat(): @app.route('/api/agent/tools/stats') def get_agent_tools_stats(): try: - tools = agent_assistant.agent_core.tool_manager.get_available_tools() - performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report() + tools = get_agent_assistant().agent_core.tool_manager.get_available_tools() + performance = get_agent_assistant().agent_core.tool_manager.get_tool_performance_report() return jsonify({ "success": True, "tools": tools, @@ -536,6 +538,22 @@ def get_agent_tools_stats(): except Exception as e: return jsonify({"error": str(e)}), 500 +@app.route('/api/agent/tools/execute', methods=['POST']) +def execute_agent_tool(): + """执行指定的Agent工具""" + try: + data = request.get_json() or {} + tool_name = data.get('tool') or data.get('name') + parameters = data.get('parameters') or {} + if not tool_name: + return jsonify({"error": "缺少工具名称tool"}), 400 + + import asyncio + result = asyncio.run(get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters)) + return jsonify(result) + except Exception as e: + return jsonify({"error": str(e)}), 500 + @app.route('/api/agent/tools/register', methods=['POST']) def register_custom_tool(): """注册自定义工具(仅登记元数据,函数为占位符)""" @@ -549,7 +567,7 @@ def register_custom_tool(): def _placeholder_tool(**kwargs): return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs} - agent_assistant.agent_core.tool_manager.register_tool( + get_agent_assistant().agent_core.tool_manager.register_tool( name, _placeholder_tool, metadata={"description": description, "custom": True} @@ -561,7 +579,7 @@ def register_custom_tool(): @app.route('/api/agent/tools/unregister/', methods=['DELETE']) def unregister_custom_tool(name): try: - success = agent_assistant.agent_core.tool_manager.unregister_tool(name) + success = get_agent_assistant().agent_core.tool_manager.unregister_tool(name) return jsonify({"success": success}) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -737,6 +755,11 @@ def test_model_response(): except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/feishu-sync') +def feishu_sync(): + """飞书同步管理页面""" + return render_template('feishu_sync.html') + if __name__ == '__main__': import time app.config['START_TIME'] = time.time() diff --git a/src/web/blueprints/conversations.py b/src/web/blueprints/conversations.py index 5311cad..bd683dc 100644 --- a/src/web/blueprints/conversations.py +++ b/src/web/blueprints/conversations.py @@ -8,6 +8,7 @@ from flask import Blueprint, request, jsonify from src.core.database import db_manager from src.core.models import Conversation from src.core.query_optimizer import query_optimizer +from datetime import timedelta conversations_bp = Blueprint('conversations', __name__, url_prefix='/api/conversations') @@ -27,6 +28,10 @@ def get_conversations(): user_id=user_id, date_filter=date_filter ) + # 规范化:移除不存在的user_id字段,避免前端误用 + for conv in result.get('conversations', []): + if 'user_id' in conv and conv['user_id'] is None: + conv.pop('user_id', None) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -40,10 +45,11 @@ def get_conversation_detail(conversation_id): if not conv: return jsonify({"error": "对话不存在"}), 404 + # Conversation模型没有user_id字段,这里用占位或由外层推断 return jsonify({ 'success': True, 'id': conv.id, - 'user_id': conv.user_id, + 'user_id': None, 'user_message': conv.user_message, 'assistant_response': conv.assistant_response, 'timestamp': conv.timestamp.isoformat() if conv.timestamp else None, @@ -88,3 +94,106 @@ def clear_all_conversations(): return jsonify({"success": True, "message": "对话历史已清空"}) except Exception as e: return jsonify({"error": str(e)}), 500 + +@conversations_bp.route('/migrate-merge', methods=['POST']) +def migrate_merge_conversations(): + """一次性迁移:将历史上拆分存储的用户/助手两条记录合并为一条 + 规则: + - 只处理一端为空的记录(user_only 或 assistant_only) + - 优先将 user_only 与其后最近的 assistant_only 合并(同工单且5分钟内) + - 若当前为 assistant_only 且前一条是 user_only 也合并到前一条 + - 合并后删除被吸收的那条记录 + - 可重复执行(幂等):已合并的不再满足“一端为空”的条件 + """ + try: + merged_pairs = 0 + deleted_rows = 0 + time_threshold_seconds = 300 + to_delete_ids = [] + with db_manager.get_session() as session: + conversations = session.query(Conversation).order_by(Conversation.timestamp.asc(), Conversation.id.asc()).all() + total = len(conversations) + i = 0 + + def is_empty(text: str) -> bool: + return (text is None) or (str(text).strip() == '') + + while i < total: + c = conversations[i] + user_only = (not is_empty(c.user_message)) and is_empty(c.assistant_response) + assistant_only = (not is_empty(c.assistant_response)) and is_empty(c.user_message) + + if user_only: + # 向后寻找匹配的assistant_only + j = i + 1 + while j < total: + n = conversations[j] + # 跳过已经标记删除的 + if n.id in to_delete_ids: + j += 1 + continue + # 超过阈值不再尝试 + if c.timestamp and n.timestamp and (n.timestamp - c.timestamp).total_seconds() > time_threshold_seconds: + break + # 同工单或两者都为空均可 + same_wo = (c.work_order_id == n.work_order_id) or (c.work_order_id is None and n.work_order_id is None) + if same_wo and (not is_empty(n.assistant_response)) and is_empty(n.user_message): + # 合并 + c.assistant_response = n.assistant_response + if c.response_time is None and c.timestamp and n.timestamp: + try: + c.response_time = max(0.0, (n.timestamp - c.timestamp).total_seconds() * 1000.0) + except Exception: + pass + # 继承辅助信息 + if (not c.confidence_score) and n.confidence_score is not None: + c.confidence_score = n.confidence_score + if (not c.knowledge_used) and n.knowledge_used: + c.knowledge_used = n.knowledge_used + session.add(c) + to_delete_ids.append(n.id) + merged_pairs += 1 + break + j += 1 + + elif assistant_only: + # 向前与最近的 user_only 合并(如果尚未被其他合并吸收) + j = i - 1 + while j >= 0: + p = conversations[j] + if p.id in to_delete_ids: + j -= 1 + continue + if p.timestamp and c.timestamp and (c.timestamp - p.timestamp).total_seconds() > time_threshold_seconds: + break + same_wo = (c.work_order_id == p.work_order_id) or (c.work_order_id is None and p.work_order_id is None) + if same_wo and (not is_empty(p.user_message)) and is_empty(p.assistant_response): + p.assistant_response = c.assistant_response + if p.response_time is None and p.timestamp and c.timestamp: + try: + p.response_time = max(0.0, (c.timestamp - p.timestamp).total_seconds() * 1000.0) + except Exception: + pass + if (not p.confidence_score) and c.confidence_score is not None: + p.confidence_score = c.confidence_score + if (not p.knowledge_used) and c.knowledge_used: + p.knowledge_used = c.knowledge_used + session.add(p) + to_delete_ids.append(c.id) + merged_pairs += 1 + break + j -= 1 + + i += 1 + + if to_delete_ids: + deleted_rows = session.query(Conversation).filter(Conversation.id.in_(to_delete_ids)).delete(synchronize_session=False) + session.commit() + + return jsonify({ + 'success': True, + 'merged_pairs': merged_pairs, + 'deleted_rows': deleted_rows + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/src/web/blueprints/feishu_sync.py b/src/web/blueprints/feishu_sync.py new file mode 100644 index 0000000..666448a --- /dev/null +++ b/src/web/blueprints/feishu_sync.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- +""" +飞书同步蓝图 +处理飞书多维表格与工单系统的同步 +""" + +from flask import Blueprint, request, jsonify +from src.integrations.feishu_client import FeishuClient +from src.integrations.workorder_sync import WorkOrderSyncService +from src.integrations.config_manager import config_manager +import logging + +logger = logging.getLogger(__name__) + +feishu_sync_bp = Blueprint('feishu_sync', __name__, url_prefix='/api/feishu-sync') + +# 全局同步服务实例 +sync_service = None + +def get_sync_service(): + """获取同步服务实例""" + global sync_service + if sync_service is None: + # 从配置管理器读取飞书配置 + feishu_config = config_manager.get_feishu_config() + + if not all([feishu_config.get("app_id"), feishu_config.get("app_secret"), + feishu_config.get("app_token"), feishu_config.get("table_id")]): + raise Exception("飞书配置不完整,请先配置飞书应用信息") + + feishu_client = FeishuClient(feishu_config["app_id"], feishu_config["app_secret"]) + sync_service = WorkOrderSyncService(feishu_client, feishu_config["app_token"], feishu_config["table_id"]) + + return sync_service + +@feishu_sync_bp.route('/config', methods=['GET', 'POST']) +def manage_config(): + """管理飞书同步配置""" + if request.method == 'GET': + # 返回当前配置 + try: + config_summary = config_manager.get_config_summary() + return jsonify({ + "success": True, + "config": config_summary + }) + except Exception as e: + logger.error(f"获取配置失败: {e}") + return jsonify({"error": str(e)}), 500 + + elif request.method == 'POST': + # 更新配置 + try: + data = request.get_json() + app_id = data.get('app_id') + app_secret = data.get('app_secret') + app_token = data.get('app_token') + table_id = data.get('table_id') + + if not all([app_id, app_secret, app_token, table_id]): + return jsonify({"error": "缺少必要配置参数"}), 400 + + # 更新配置管理器 + success = config_manager.update_feishu_config( + app_id=app_id, + app_secret=app_secret, + app_token=app_token, + table_id=table_id + ) + + if success: + # 重新初始化同步服务 + global sync_service + sync_service = None # 强制重新创建 + + return jsonify({ + "success": True, + "message": "配置更新成功" + }) + else: + return jsonify({"error": "配置更新失败"}), 500 + + except Exception as e: + logger.error(f"更新飞书配置失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/sync-from-feishu', methods=['POST']) +def sync_from_feishu(): + """从飞书同步数据到本地""" + try: + data = request.get_json() or {} + generate_ai = data.get('generate_ai_suggestions', True) + limit = data.get('limit', 10) + + sync_service = get_sync_service() + result = sync_service.sync_from_feishu(generate_ai_suggestions=generate_ai, limit=limit) + + if result.get("success"): + message = f"同步完成:创建 {result['created_count']} 条,更新 {result['updated_count']} 条" + if result.get('ai_suggestions_generated'): + message += ",AI建议已生成并更新到飞书表格" + + return jsonify({ + "success": True, + "message": message, + "details": result + }) + else: + return jsonify({"error": result.get("error")}), 500 + + except Exception as e: + logger.error(f"从飞书同步失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/sync-to-feishu/', methods=['POST']) +def sync_to_feishu(workorder_id): + """将本地工单同步到飞书""" + try: + sync_service = get_sync_service() + result = sync_service.sync_to_feishu(workorder_id) + + if result.get("success"): + return jsonify({ + "success": True, + "message": "同步到飞书成功" + }) + else: + return jsonify({"error": result.get("error")}), 500 + + except Exception as e: + logger.error(f"同步到飞书失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/status') +def get_sync_status(): + """获取同步状态""" + try: + sync_service = get_sync_service() + status = sync_service.get_sync_status() + + return jsonify({ + "success": True, + "status": status + }) + except Exception as e: + logger.error(f"获取同步状态失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/test-connection') +def test_connection(): + """测试飞书连接""" + try: + # 使用配置管理器测试连接 + result = config_manager.test_feishu_connection() + + if result.get("success"): + # 如果连接成功,尝试获取表格字段信息 + try: + sync_service = get_sync_service() + + # 使用新的测试连接方法 + connection_test = sync_service.feishu_client.test_connection() + if not connection_test.get("success"): + return jsonify({ + "success": False, + "message": f"飞书连接测试失败: {connection_test.get('message')}" + }), 400 + + fields_info = sync_service.feishu_client.get_table_fields( + sync_service.app_token, sync_service.table_id + ) + + if fields_info.get("code") == 0: + result["fields"] = fields_info.get("data", {}).get("items", []) + except Exception as e: + logger.warning(f"获取表格字段信息失败: {e}") + + return jsonify(result) + + except Exception as e: + logger.error(f"测试飞书连接失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/create-workorder', methods=['POST']) +def create_workorder_from_feishu(): + """从飞书记录创建工单""" + try: + data = request.get_json() + record_id = data.get('record_id') + + if not record_id: + return jsonify({"success": False, "message": "缺少记录ID"}), 400 + + sync_service = get_sync_service() + result = sync_service.create_workorder_from_feishu_record(record_id) + + if result.get("success"): + return jsonify(result) + else: + return jsonify(result), 400 + + except Exception as e: + logger.error(f"创建工单失败: {e}") + return jsonify({"success": False, "message": str(e)}), 500 + +@feishu_sync_bp.route('/preview-feishu-data') +def preview_feishu_data(): + """预览飞书数据""" + try: + sync_service = get_sync_service() + + # 获取前10条记录进行预览 + records = sync_service.feishu_client.get_table_records( + sync_service.app_token, sync_service.table_id, page_size=10 + ) + + if records.get("code") == 0: + items = records.get("data", {}).get("items", []) + preview_data = [] + + for record in items: + parsed_fields = sync_service.feishu_client.parse_record_fields(record) + preview_data.append({ + "record_id": record.get("record_id"), + "fields": parsed_fields + }) + + return jsonify({ + "success": True, + "preview_data": preview_data, + "total_count": len(preview_data) + }) + else: + return jsonify({ + "success": False, + "error": records.get("msg", "获取数据失败") + }), 500 + + except Exception as e: + logger.error(f"预览飞书数据失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/config/export', methods=['GET']) +def export_config(): + """导出配置""" + try: + config_json = config_manager.export_config() + return jsonify({ + "success": True, + "config": config_json + }) + except Exception as e: + logger.error(f"导出配置失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/config/import', methods=['POST']) +def import_config(): + """导入配置""" + try: + data = request.get_json() + config_json = data.get('config') + + if not config_json: + return jsonify({"error": "缺少配置数据"}), 400 + + success = config_manager.import_config(config_json) + + if success: + # 重新初始化同步服务 + global sync_service + sync_service = None + + return jsonify({ + "success": True, + "message": "配置导入成功" + }) + else: + return jsonify({"error": "配置导入失败"}), 500 + + except Exception as e: + logger.error(f"导入配置失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/config/reset', methods=['POST']) +def reset_config(): + """重置配置""" + try: + success = config_manager.reset_config() + + if success: + # 重新初始化同步服务 + global sync_service + sync_service = None + + return jsonify({ + "success": True, + "message": "配置重置成功" + }) + else: + return jsonify({"error": "配置重置失败"}), 500 + + except Exception as e: + logger.error(f"重置配置失败: {e}") + return jsonify({"error": str(e)}), 500 diff --git a/src/web/blueprints/system.py b/src/web/blueprints/system.py index a8cb912..5a93d35 100644 --- a/src/web/blueprints/system.py +++ b/src/web/blueprints/system.py @@ -306,6 +306,108 @@ def optimize_disk(): except Exception as e: return jsonify({"error": str(e)}), 500 +@system_bp.route('/system-optimizer/clear-cache', methods=['POST']) +def clear_cache(): + """清理应用缓存(内存/Redis均尝试)""" + try: + cleared = False + try: + from src.core.cache_manager import cache_manager + cache_manager.clear() + cleared = True + except Exception: + pass + return jsonify({ + 'success': True, + 'message': '缓存已清理' if cleared else '缓存清理已尝试(可能未启用缓存模块)', + 'progress': 100 + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@system_bp.route('/system-optimizer/optimize-all', methods=['POST']) +def optimize_all(): + """一键优化:CPU/内存/磁盘 + 缓存清理 + 轻量数据库维护""" + try: + import gc + import time + actions = [] + start_time = time.time() + + # 垃圾回收 & 缓存 + try: + collected = gc.collect() + actions.append(f"垃圾回收:{collected}") + except Exception: + actions.append("垃圾回收:跳过") + + try: + from src.core.cache_manager import cache_manager + cache_manager.clear() + actions.append("缓存清理:完成") + except Exception: + actions.append("缓存清理:跳过") + + # 临时文件与日志清理(沿用磁盘优化逻辑的子集) + temp_files_cleaned = 0 + log_files_cleaned = 0 + try: + import os, tempfile + temp_dir = tempfile.gettempdir() + for filename in os.listdir(temp_dir): + if filename.startswith('tsp_') or filename.startswith('tmp_'): + file_path = os.path.join(temp_dir, filename) + try: + if os.path.isfile(file_path): + os.remove(file_path) + temp_files_cleaned += 1 + except Exception: + pass + except Exception: + pass + actions.append(f"临时文件:{temp_files_cleaned}") + + try: + import os, glob + from datetime import datetime, timedelta + log_dir = 'logs' + if os.path.exists(log_dir): + cutoff_date = datetime.now() - timedelta(days=7) + for log_file in glob.glob(os.path.join(log_dir, '*.log')): + try: + file_time = datetime.fromtimestamp(os.path.getmtime(log_file)) + if file_time < cutoff_date: + os.remove(log_file) + log_files_cleaned += 1 + except Exception: + pass + except Exception: + pass + actions.append(f"日志清理:{log_files_cleaned}") + + # 轻量数据库维护(尽力而为):SQLite时执行VACUUM;其他数据库跳过 + try: + engine = db_manager.engine + if str(engine.url).startswith('sqlite'): + with engine.begin() as conn: + conn.exec_driver_sql('VACUUM') + actions.append("SQLite VACUUM:完成") + else: + actions.append("DB维护:跳过(非SQLite)") + except Exception: + actions.append("DB维护:失败") + + optimization_time = round((time.time() - start_time) * 1000, 1) + return jsonify({ + 'success': True, + 'message': '一键优化完成: ' + ','.join(actions) + f',耗时{optimization_time}ms', + 'progress': 100, + 'actions': actions, + 'optimization_time': optimization_time + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + @system_bp.route('/system-optimizer/security-settings', methods=['GET', 'POST']) def security_settings(): """安全设置""" diff --git a/src/web/blueprints/workorders.py b/src/web/blueprints/workorders.py index cdae3d1..de38007 100644 --- a/src/web/blueprints/workorders.py +++ b/src/web/blueprints/workorders.py @@ -31,15 +31,23 @@ def _ensure_workorder_template_file() -> str: # 确保目录存在 os.makedirs('uploads', exist_ok=True) if not os.path.exists(template_path): - # 如果运行目录不存在模板,尝试从项目根相对路径拷贝一份 - repo_template = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'uploads', 'workorder_template.xlsx') - repo_template = os.path.abspath(repo_template) + # 优先从项目根目录的 uploads 拷贝(仓库自带模板) + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) + repo_template = os.path.join(project_root, 'uploads', 'workorder_template.xlsx') try: if os.path.exists(repo_template): import shutil shutil.copyfile(repo_template, template_path) else: - raise FileNotFoundError('模板文件缺失:uploads/workorder_template.xlsx') + # 仓库模板不存在时,自动生成一个最小可用模板 + try: + import pandas as pd + from pandas import DataFrame + columns = ['标题', '描述', '分类', '优先级', '状态', '解决方案', '满意度'] + df: DataFrame = pd.DataFrame(columns=columns) + df.to_excel(template_path, index=False) + except Exception as gen_err: + raise FileNotFoundError('模板文件缺失且自动生成失败,请检查依赖:openpyxl/pandas') from gen_err except Exception as copy_err: raise copy_err return template_path @@ -199,14 +207,15 @@ def generate_workorder_ai_suggestion(workorder_id): if not w: return jsonify({"error": "工单不存在"}), 404 # 调用知识库搜索与LLM生成 - query = f"{w.title} {w.description}" + # 使用问题描述(title)而不是处理过程(description)作为主要查询依据 + query = f"{w.title}" kb_results = get_assistant().search_knowledge(query, top_k=3) kb_list = kb_results.get('results', []) if isinstance(kb_results, dict) else [] # 组装提示词 context = "\n".join([f"Q: {k.get('question','')}\nA: {k.get('answer','')}" for k in kb_list]) from src.core.llm_client import QwenClient llm = QwenClient() - prompt = f"请基于以下工单描述与知识库片段,给出简洁、可执行的处理建议。\n工单描述:\n{w.description}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:" + prompt = f"请基于以下工单问题描述与知识库片段,给出简洁、可执行的处理建议。\n\n问题描述:\n{w.title}\n\n处理过程(仅供参考):\n{w.description}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:" llm_resp = llm.chat_completion(messages=[{"role":"user","content":prompt}], temperature=0.3, max_tokens=800) suggestion = "" if llm_resp and 'choices' in llm_resp: @@ -404,6 +413,11 @@ def download_import_template_file(): """直接返回工单导入模板文件(下载)""" try: template_path = _ensure_workorder_template_file() - return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx') + try: + # Flask>=2 使用 download_name + return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + except TypeError: + # 兼容 Flask<2 的 attachment_filename + return send_file(template_path, as_attachment=True, attachment_filename='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js index aaa00f9..ad7feae 100644 --- a/src/web/static/js/dashboard.js +++ b/src/web/static/js/dashboard.js @@ -924,6 +924,7 @@ class TSPDashboard {
${success}% +
`; @@ -931,6 +932,39 @@ class TSPDashboard { toolsList.innerHTML = toolsHtml; + // 绑定执行事件 + toolsList.querySelectorAll('button[data-tool]').forEach(btn => { + btn.addEventListener('click', async () => { + const tool = btn.getAttribute('data-tool'); + // 简单参数输入(可扩展为动态表单) + let params = {}; + try { + const input = prompt('请输入执行参数(JSON):', '{}'); + if (input) params = JSON.parse(input); + } catch (e) { + this.showNotification('参数格式错误,应为JSON', 'warning'); + return; + } + try { + const resp = await fetch('/api/agent/tools/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tool, parameters: params }) + }); + const res = await resp.json(); + if (res.success) { + this.showNotification(`工具 ${tool} 执行成功`, 'success'); + await this.loadAgentData(); + } else { + this.showNotification(res.error || `工具 ${tool} 执行失败`, 'error'); + } + } catch (err) { + console.error('执行工具失败:', err); + this.showNotification('执行工具失败: ' + err.message, 'error'); + } + }); + }); + // 追加自定义工具注册入口 const addDiv = document.createElement('div'); addDiv.className = 'mt-3'; @@ -1508,7 +1542,7 @@ class TSPDashboard {
${workorder.title}
-

${workorder.description}

+

${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}

${this.getPriorityText(workorder.priority)} ${this.getStatusText(workorder.status)} @@ -1656,8 +1690,14 @@ class TSPDashboard {
问题描述
- ${workorder.description} + ${workorder.title || '无问题描述'}
+ ${workorder.description ? ` +
处理过程
+
+ ${workorder.description} +
+ ` : ''} ${workorder.resolution ? `
解决方案
@@ -1857,7 +1897,7 @@ class TSPDashboard {
- +
@@ -2154,6 +2194,10 @@ class TSPDashboard { } async refreshConversationHistory() { + // 先尝试触发一次合并迁移(幂等,重复调用也安全) + try { + await fetch('/api/conversations/migrate-merge', { method: 'POST' }); + } catch (e) { /* 忽略迁移失败 */ } await this.loadConversationHistory(); this.showNotification('对话历史已刷新', 'success'); } @@ -2206,6 +2250,7 @@ class TSPDashboard { const data = await response.json(); if (data.success) { + data.user_id = data.user_id || '匿名'; this.showConversationModal(data); } else { throw new Error(data.error || '获取对话详情失败'); @@ -2760,8 +2805,11 @@ class TSPDashboard { const data = await response.json(); if (data.success) { - this.showNotification('CPU优化完成', 'success'); + this.showNotification(data.message || 'CPU优化完成', 'success'); this.updateOptimizationProgress('cpu-optimization', data.progress || 100); + // 刷新状态并回落进度条 + await this.loadSystemOptimizer(); + setTimeout(() => this.updateOptimizationProgress('cpu-optimization', 0), 1500); } else { throw new Error(data.error || 'CPU优化失败'); } @@ -2777,8 +2825,10 @@ class TSPDashboard { const data = await response.json(); if (data.success) { - this.showNotification('内存优化完成', 'success'); + this.showNotification(data.message || '内存优化完成', 'success'); this.updateOptimizationProgress('memory-optimization', data.progress || 100); + await this.loadSystemOptimizer(); + setTimeout(() => this.updateOptimizationProgress('memory-optimization', 0), 1500); } else { throw new Error(data.error || '内存优化失败'); } @@ -2794,8 +2844,10 @@ class TSPDashboard { const data = await response.json(); if (data.success) { - this.showNotification('磁盘优化完成', 'success'); + this.showNotification(data.message || '磁盘优化完成', 'success'); this.updateOptimizationProgress('disk-optimization', data.progress || 100); + await this.loadSystemOptimizer(); + setTimeout(() => this.updateOptimizationProgress('disk-optimization', 0), 1500); } else { throw new Error(data.error || '磁盘优化失败'); } @@ -2916,6 +2968,40 @@ class TSPDashboard { this.showNotification('系统状态已刷新', 'success'); } + async clearCache() { + try { + const response = await fetch('/api/system-optimizer/clear-cache', { method: 'POST' }); + const data = await response.json(); + if (data.success) { + this.showNotification(data.message || '缓存已清理', 'success'); + await this.loadSystemOptimizer(); + } else { + throw new Error(data.error || '清理缓存失败'); + } + } catch (error) { + console.error('清理缓存失败:', error); + this.showNotification('清理缓存失败: ' + error.message, 'error'); + } + } + + async optimizeAll() { + try { + const response = await fetch('/api/system-optimizer/optimize-all', { method: 'POST' }); + const data = await response.json(); + if (data.success) { + this.showNotification(data.message || '一键优化完成', 'success'); + await this.loadSystemOptimizer(); + ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 100)); + setTimeout(() => ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 0)), 1500); + } else { + throw new Error(data.error || '一键优化失败'); + } + } catch (error) { + console.error('一键优化失败:', error); + this.showNotification('一键优化失败: ' + error.message, 'error'); + } + } + async loadSecuritySettings() { try { const response = await fetch('/api/system-optimizer/security-settings'); diff --git a/src/web/templates/dashboard.html b/src/web/templates/dashboard.html index ae10f33..0c7707f 100644 --- a/src/web/templates/dashboard.html +++ b/src/web/templates/dashboard.html @@ -450,25 +450,25 @@
-
+
0
活跃会话
-
+
0
活跃预警
-
+
0
待处理工单
-
+
0
知识条目
@@ -1380,25 +1380,25 @@