修复AI建议逻辑和字段映射问题
- 修复AI建议基于问题描述而不是处理过程生成 - 修复工单详情页面显示逻辑 - 修复飞书时间字段处理(毫秒时间戳转换) - 优化字段映射和转换逻辑 - 添加飞书集成功能 - 改进对话历史合并功能 - 优化系统优化反馈机制
This commit is contained in:
16
config/integrations_config copy.json
Normal file
16
config/integrations_config copy.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
16
config/integrations_config.json
Normal file
16
config/integrations_config.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"]
|
||||
# 计算成本
|
||||
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)
|
||||
)
|
||||
# 记录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(
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
# 统一为一条记录:包含用户消息与助手回复
|
||||
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
|
||||
|
||||
# 保存助手消息
|
||||
assistant_conversation = Conversation(
|
||||
work_order_id=assistant_msg.work_order_id,
|
||||
user_message="", # 助手消息没有用户输入
|
||||
assistant_response=assistant_msg.content,
|
||||
timestamp=assistant_msg.timestamp,
|
||||
# 在知识字段中打上会话标记,便于结束时合并清理
|
||||
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:
|
||||
|
||||
5
src/integrations/__init__.py
Normal file
5
src/integrations/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
集成模块
|
||||
处理与外部系统的集成,如飞书、钉钉等
|
||||
"""
|
||||
171
src/integrations/ai_suggestion_service.py
Normal file
171
src/integrations/ai_suggestion_service.py
Normal file
@@ -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
|
||||
227
src/integrations/config_manager.py
Normal file
227
src/integrations/config_manager.py
Normal file
@@ -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()
|
||||
293
src/integrations/feishu_client.py
Normal file
293
src/integrations/feishu_client.py
Normal file
@@ -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 <token>
|
||||
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
|
||||
461
src/integrations/workorder_sync.py
Normal file
461
src/integrations/workorder_sync.py
Normal file
@@ -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)}
|
||||
@@ -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/<name>', 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()
|
||||
|
||||
@@ -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
|
||||
|
||||
304
src/web/blueprints/feishu_sync.py
Normal file
304
src/web/blueprints/feishu_sync.py
Normal file
@@ -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/<int:workorder_id>', 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
|
||||
@@ -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():
|
||||
"""安全设置"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -924,6 +924,7 @@ class TSPDashboard {
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge ${success >= 80 ? 'bg-success' : success >= 50 ? 'bg-warning' : 'bg-secondary'}">${success}%</span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" data-tool="${tool.name}">执行</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -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 {
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<h6 class="mb-1">${workorder.title}</h6>
|
||||
<p class="text-muted mb-2">${workorder.description}</p>
|
||||
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}</p>
|
||||
<div class="d-flex gap-3">
|
||||
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
|
||||
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
|
||||
@@ -1656,8 +1690,14 @@ class TSPDashboard {
|
||||
<div class="col-md-6">
|
||||
<h6>问题描述</h6>
|
||||
<div class="border p-3 rounded">
|
||||
${workorder.description}
|
||||
${workorder.title || '无问题描述'}
|
||||
</div>
|
||||
${workorder.description ? `
|
||||
<h6 class="mt-3">处理过程</h6>
|
||||
<div class="border p-3 rounded bg-light">
|
||||
${workorder.description}
|
||||
</div>
|
||||
` : ''}
|
||||
${workorder.resolution ? `
|
||||
<h6 class="mt-3">解决方案</h6>
|
||||
<div class="border p-3 rounded bg-light">
|
||||
@@ -1857,7 +1897,7 @@ class TSPDashboard {
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editDescription" class="form-label">描述 *</label>
|
||||
<label for="editDescription" class="form-label">处理过程 *</label>
|
||||
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -450,25 +450,25 @@
|
||||
<!-- 仪表板标签页 -->
|
||||
<div id="dashboard-tab" class="tab-content">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-number" id="total-sessions">0</div>
|
||||
<div class="stat-label">活跃会话</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-number" id="total-alerts">0</div>
|
||||
<div class="stat-label">活跃预警</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-number" id="total-workorders">0</div>
|
||||
<div class="stat-label">待处理工单</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card info">
|
||||
<div class="stat-number" id="knowledge-count">0</div>
|
||||
<div class="stat-label">知识条目</div>
|
||||
@@ -1380,25 +1380,25 @@
|
||||
<!-- 系统优化标签页 -->
|
||||
<div id="system-optimizer-tab" class="tab-content" style="display: none;">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card success">
|
||||
<div class="stat-number" id="cpu-usage">0%</div>
|
||||
<div class="stat-label">CPU使用率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card warning">
|
||||
<div class="stat-number" id="memory-usage-percent">0%</div>
|
||||
<div class="stat-label">内存使用率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card danger">
|
||||
<div class="stat-number" id="disk-usage">0%</div>
|
||||
<div class="stat-label">磁盘使用率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card info">
|
||||
<div class="stat-number" id="network-latency">0ms</div>
|
||||
<div class="stat-label">网络延迟</div>
|
||||
@@ -1440,6 +1440,14 @@
|
||||
<i class="fas fa-hdd me-1"></i>优化磁盘
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-success btn-sm" onclick="dashboard.optimizeAll()">
|
||||
<i class="fas fa-magic me-1"></i>一键优化
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="dashboard.clearCache()">
|
||||
<i class="fas fa-broom me-1"></i>清理缓存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
662
src/web/templates/feishu_sync.html
Normal file
662
src/web/templates/feishu_sync.html
Normal file
@@ -0,0 +1,662 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>飞书同步管理 - TSP助手</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="/static/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>仪表板
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/alerts">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>预警管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/workorders">
|
||||
<i class="fas fa-tasks me-2"></i>工单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/conversations">
|
||||
<i class="fas fa-comments me-2"></i>对话历史
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/knowledge">
|
||||
<i class="fas fa-book me-2"></i>知识库
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/feishu-sync">
|
||||
<i class="fas fa-sync me-2"></i>飞书同步
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/monitoring">
|
||||
<i class="fas fa-chart-line me-2"></i>系统监控
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/settings">
|
||||
<i class="fas fa-cog me-2"></i>系统设置
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-sync me-2"></i>飞书同步管理
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 配置区域 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-cog me-2"></i>飞书配置
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="feishuConfigForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="appId" class="form-label">应用ID</label>
|
||||
<input type="text" class="form-control" id="appId" placeholder="cli_xxxxxxxxxx">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="appSecret" class="form-label">应用密钥</label>
|
||||
<input type="password" class="form-control" id="appSecret" placeholder="输入应用密钥">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="appToken" class="form-label">应用Token</label>
|
||||
<input type="text" class="form-control" id="appToken" placeholder="XXnEbiCmEaMblSs6FDJcFCqsnIg">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="tableId" class="form-label">表格ID</label>
|
||||
<input type="text" class="form-control" id="tableId" placeholder="tblnl3vJPpgMTSiP">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-primary" onclick="feishuSync.saveConfig()">
|
||||
<i class="fas fa-save me-1"></i>保存配置
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="feishuSync.testConnection()">
|
||||
<i class="fas fa-plug me-1"></i>测试连接
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" onclick="feishuSync.exportConfig()">
|
||||
<i class="fas fa-download me-1"></i>导出配置
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" onclick="feishuSync.showImportModal()">
|
||||
<i class="fas fa-upload me-1"></i>导入配置
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="feishuSync.resetConfig()">
|
||||
<i class="fas fa-undo me-1"></i>重置配置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 同步状态 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalLocalWorkorders">0</h5>
|
||||
<p class="card-text">本地工单总数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="syncedWorkorders">0</h5>
|
||||
<p class="card-text">已同步工单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="unsyncedWorkorders">0</h5>
|
||||
<p class="card-text">未同步工单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 同步操作 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-sync-alt me-2"></i>同步操作
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-success" onclick="feishuSync.syncFromFeishu()">
|
||||
<i class="fas fa-download me-1"></i>从飞书同步
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="feishuSync.syncWithAI()">
|
||||
<i class="fas fa-robot me-1"></i>同步+AI建议
|
||||
</button>
|
||||
<button class="btn btn-info" onclick="feishuSync.previewFeishuData()">
|
||||
<i class="fas fa-eye me-1"></i>预览飞书数据
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="feishuSync.refreshStatus()">
|
||||
<i class="fas fa-refresh me-1"></i>刷新状态
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="syncLimit" class="form-label">同步数量限制:</label>
|
||||
<select class="form-select" id="syncLimit" style="width: auto; display: inline-block;">
|
||||
<option value="10">前10条</option>
|
||||
<option value="20">前20条</option>
|
||||
<option value="50">前50条</option>
|
||||
<option value="100">前100条</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 同步进度 -->
|
||||
<div class="progress mb-3" id="syncProgress" style="display: none;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
|
||||
<!-- 同步日志 -->
|
||||
<div class="mt-3">
|
||||
<h6>同步日志</h6>
|
||||
<div id="syncLog" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;">
|
||||
<div class="text-muted">暂无同步记录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览数据 -->
|
||||
<div class="row" id="previewSection" style="display: none;">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-table me-2"></i>飞书数据预览
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="previewTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>记录ID</th>
|
||||
<th>TR编号</th>
|
||||
<th>TR描述</th>
|
||||
<th>问题类型</th>
|
||||
<th>来源</th>
|
||||
<th>优先级/状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入配置模态框 -->
|
||||
<div class="modal fade" id="importConfigModal" tabindex="-1" aria-labelledby="importConfigModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="importConfigModalLabel">导入配置</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="configJson" class="form-label">配置JSON数据:</label>
|
||||
<textarea class="form-control" id="configJson" rows="10" placeholder="粘贴导出的配置JSON数据..."></textarea>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
导入配置将覆盖当前所有配置,请谨慎操作!
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="feishuSync.importConfig()">导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知容器 -->
|
||||
<div id="notificationContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
class FeishuSyncManager {
|
||||
constructor() {
|
||||
this.loadConfig();
|
||||
this.refreshStatus();
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/feishu-sync/config');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
document.getElementById('appId').value = config.feishu.app_id || '';
|
||||
document.getElementById('appSecret').value = '';
|
||||
document.getElementById('appToken').value = config.feishu.app_token || '';
|
||||
document.getElementById('tableId').value = config.feishu.table_id || '';
|
||||
|
||||
// 显示配置状态
|
||||
const statusBadge = config.feishu.status === 'active' ?
|
||||
'<span class="badge bg-success">已配置</span>' :
|
||||
'<span class="badge bg-warning">未配置</span>';
|
||||
|
||||
// 可以在这里添加状态显示
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async saveConfig() {
|
||||
const config = {
|
||||
app_id: document.getElementById('appId').value,
|
||||
app_secret: document.getElementById('appSecret').value,
|
||||
app_token: document.getElementById('appToken').value,
|
||||
table_id: document.getElementById('tableId').value
|
||||
};
|
||||
|
||||
if (!config.app_id || !config.app_secret || !config.app_token || !config.table_id) {
|
||||
this.showNotification('请填写完整的配置信息', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/feishu-sync/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('配置保存成功', 'success');
|
||||
} else {
|
||||
this.showNotification('配置保存失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('配置保存失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
try {
|
||||
this.showNotification('正在测试连接...', 'info');
|
||||
|
||||
const response = await fetch('/api/feishu-sync/test-connection');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('飞书连接正常', 'success');
|
||||
} else {
|
||||
this.showNotification('连接失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('连接测试失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async syncFromFeishu() {
|
||||
try {
|
||||
const limit = document.getElementById('syncLimit').value;
|
||||
this.showNotification('开始从飞书同步数据...', 'info');
|
||||
this.showProgress(true);
|
||||
|
||||
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
generate_ai_suggestions: false,
|
||||
limit: parseInt(limit)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification(data.message, 'success');
|
||||
this.addSyncLog(data.message);
|
||||
this.refreshStatus();
|
||||
} else {
|
||||
this.showNotification('同步失败: ' + data.error, 'error');
|
||||
this.addSyncLog('同步失败: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('同步失败: ' + error.message, 'error');
|
||||
this.addSyncLog('同步失败: ' + error.message);
|
||||
} finally {
|
||||
this.showProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
async syncWithAI() {
|
||||
try {
|
||||
const limit = document.getElementById('syncLimit').value;
|
||||
this.showNotification('开始同步数据并生成AI建议...', 'info');
|
||||
this.showProgress(true);
|
||||
|
||||
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
generate_ai_suggestions: true,
|
||||
limit: parseInt(limit)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification(data.message, 'success');
|
||||
this.addSyncLog(data.message);
|
||||
this.refreshStatus();
|
||||
} else {
|
||||
this.showNotification('同步失败: ' + data.error, 'error');
|
||||
this.addSyncLog('同步失败: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('同步失败: ' + error.message, 'error');
|
||||
this.addSyncLog('同步失败: ' + error.message);
|
||||
} finally {
|
||||
this.showProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
async previewFeishuData() {
|
||||
try {
|
||||
this.showNotification('正在获取飞书数据预览...', 'info');
|
||||
|
||||
const response = await fetch('/api/feishu-sync/preview-feishu-data');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.displayPreviewData(data.preview_data);
|
||||
this.showNotification(`获取到 ${data.total_count} 条预览数据`, 'success');
|
||||
} else {
|
||||
this.showNotification('获取预览数据失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('获取预览数据失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
displayPreviewData(data) {
|
||||
const tbody = document.querySelector('#previewTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${item.record_id}</td>
|
||||
<td>${item.fields['TR Number'] || '-'}</td>
|
||||
<td>${item.fields['TR Description'] || '-'}</td>
|
||||
<td>${item.fields['Type of problem'] || '-'}</td>
|
||||
<td>${item.fields['Source'] || '-'}</td>
|
||||
<td>${item.fields['TR (Priority/Status)'] || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="feishuSync.createWorkorder('${item.record_id}')">
|
||||
<i class="fas fa-plus"></i> 创建工单
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
document.getElementById('previewSection').style.display = 'block';
|
||||
}
|
||||
|
||||
async refreshStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/feishu-sync/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const status = data.status;
|
||||
document.getElementById('totalLocalWorkorders').textContent = status.total_local_workorders || 0;
|
||||
document.getElementById('syncedWorkorders').textContent = status.synced_workorders || 0;
|
||||
document.getElementById('unsyncedWorkorders').textContent = status.unsynced_workorders || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
showProgress(show) {
|
||||
const progress = document.getElementById('syncProgress');
|
||||
if (show) {
|
||||
progress.style.display = 'block';
|
||||
const bar = progress.querySelector('.progress-bar');
|
||||
bar.style.width = '100%';
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
progress.style.display = 'none';
|
||||
const bar = progress.querySelector('.progress-bar');
|
||||
bar.style.width = '0%';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
addSyncLog(message) {
|
||||
const log = document.getElementById('syncLog');
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.innerHTML = `<small class="text-muted">[${timestamp}]</small> ${message}`;
|
||||
|
||||
if (log.querySelector('.text-muted')) {
|
||||
log.innerHTML = '';
|
||||
}
|
||||
|
||||
log.appendChild(logEntry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
async exportConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/feishu-sync/config/export');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// 创建下载链接
|
||||
const blob = new Blob([data.config], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `feishu_config_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showNotification('配置导出成功', 'success');
|
||||
} else {
|
||||
this.showNotification('配置导出失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('配置导出失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showImportModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('importConfigModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async importConfig() {
|
||||
try {
|
||||
const configJson = document.getElementById('configJson').value.trim();
|
||||
|
||||
if (!configJson) {
|
||||
this.showNotification('请输入配置JSON数据', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/feishu-sync/config/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ config: configJson })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('配置导入成功', 'success');
|
||||
this.loadConfig();
|
||||
this.refreshStatus();
|
||||
|
||||
// 关闭模态框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('importConfigModal'));
|
||||
modal.hide();
|
||||
document.getElementById('configJson').value = '';
|
||||
} else {
|
||||
this.showNotification('配置导入失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('配置导入失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async resetConfig() {
|
||||
if (confirm('确定要重置所有配置吗?此操作不可撤销!')) {
|
||||
try {
|
||||
const response = await fetch('/api/feishu-sync/config/reset', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('配置重置成功', 'success');
|
||||
this.loadConfig();
|
||||
this.refreshStatus();
|
||||
} else {
|
||||
this.showNotification('配置重置失败: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('配置重置失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createWorkorder(recordId) {
|
||||
if (confirm(`确定要从飞书记录 ${recordId} 创建工单吗?`)) {
|
||||
try {
|
||||
this.showNotification('正在创建工单...', 'info');
|
||||
|
||||
const response = await fetch('/api/feishu-sync/create-workorder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
record_id: recordId
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification(data.message, 'success');
|
||||
// 刷新工单列表(如果用户在工单页面)
|
||||
if (typeof window.refreshWorkOrders === 'function') {
|
||||
window.refreshWorkOrders();
|
||||
}
|
||||
} else {
|
||||
this.showNotification('创建工单失败: ' + data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showNotification('创建工单失败: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
const container = document.getElementById('notificationContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
container.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
const feishuSync = new FeishuSyncManager();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
启动TSP智能助手综合管理平台
|
||||
@@ -59,19 +58,6 @@ def main():
|
||||
# 导入并启动Flask应用
|
||||
from src.web.app import app
|
||||
|
||||
print("系统功能:")
|
||||
print(" ✓ 智能对话系统")
|
||||
print(" ✓ 对话历史管理")
|
||||
print(" ✓ Token消耗监控")
|
||||
print(" ✓ AI调用成功率监控")
|
||||
print(" ✓ Agent管理")
|
||||
print(" ✓ 预警管理")
|
||||
print(" ✓ 知识库管理")
|
||||
print(" ✓ 工单管理")
|
||||
print(" ✓ 数据分析")
|
||||
print(" ✓ 系统设置")
|
||||
print(" ✓ Redis缓存支持")
|
||||
print(" ✓ 性能优化")
|
||||
print()
|
||||
print("访问地址:")
|
||||
print(" 主页: http://localhost:5000")
|
||||
|
||||
Reference in New Issue
Block a user