feat: 重大功能更新 v1.4.0 - 飞书集成、AI语义相似度、前端优化

主要更新内容:
- 🚀 飞书多维表格集成,支持工单数据同步
- 🤖 AI建议与人工描述语义相似度计算
- 🎨 前端UI全面优化,现代化设计
- 📊 智能知识库入库策略(AI准确率<90%使用人工描述)
- 🔧 代码重构,模块化架构优化
- 📚 完整文档整合和更新
- 🐛 修复配置导入和数据库字段问题

技术特性:
- 使用sentence-transformers进行语义相似度计算
- 快速模式结合TF-IDF和语义方法
- 响应式设计,支持移动端
- 加载状态和动画效果
- 配置化AI准确率阈值
This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-09-19 19:32:42 +01:00
parent 79cf316c63
commit da4736c323
30 changed files with 4778 additions and 1406 deletions

View File

@@ -755,10 +755,7 @@ 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

View File

@@ -21,6 +21,7 @@
- `knowledge.py`: 知识库管理相关API
- `monitoring.py`: 监控相关API
- `system.py`: 系统管理相关API
- ~~`feishu_sync.py`~~: 已合并到主仪表板(删除)
## 蓝图模块说明
@@ -59,6 +60,12 @@
- `/api/backup/*` - 数据备份
- `/api/database/status` - 数据库状态
### 7. 飞书集成功能(已合并)
- **原独立页面**: `http://localhost:5000/feishu-sync`
- **现集成位置**: 主仪表板的"飞书同步"标签页
- **功能**: 飞书多维表格数据同步和管理
- **API端点**: 通过主应用路由提供
## 优势
1. **模块化**: 每个功能模块独立,便于维护
@@ -96,7 +103,16 @@ src/web/
│ ├── system.py # 系统管理
│ └── README.md # 架构说明
├── static/ # 静态文件
│ ├── css/
│ │ └── style.css # 样式文件(包含飞书集成样式)
│ └── js/
│ ├── dashboard.js # 仪表板逻辑(包含飞书同步功能)
│ ├── chat.js # 对话功能
│ └── app.js # 应用主逻辑
└── templates/ # 模板文件
├── dashboard.html # 主仪表板(包含飞书同步标签页)
├── chat.html # 对话页面
└── index.html # 首页
```
## 注意事项
@@ -106,3 +122,17 @@ src/web/
3. 懒加载模式避免启动时的重复初始化
4. 错误处理统一在蓝图内部进行
5. 保持与原有API接口的兼容性
6. 飞书集成功能已从独立蓝图合并到主仪表板
7. 前端JavaScript类管理不同功能模块TSPDashboard、FeishuSyncManager等
## 最新更新 (v1.4.0)
### 功能合并
- **飞书同步页面合并**: 原独立的飞书同步页面已合并到主仪表板
- **统一用户体验**: 所有功能现在都在一个统一的界面中
- **代码优化**: 删除了冗余的蓝图和模板文件
### 架构改进
- **前端模块化**: JavaScript代码按功能模块组织
- **数据库扩展**: 工单表新增12个飞书相关字段
- **字段映射**: 智能映射飞书字段到本地数据库结构

View File

@@ -6,11 +6,36 @@
import os
import pandas as pd
import logging
from datetime import datetime
from flask import Blueprint, request, jsonify, send_file
from werkzeug.utils import secure_filename
from sqlalchemy import text
logger = logging.getLogger(__name__)
# 简化的AI准确率配置类
class SimpleAIAccuracyConfig:
"""简化的AI准确率配置"""
def __init__(self):
self.auto_approve_threshold = 0.95
self.use_human_resolution_threshold = 0.90
self.manual_review_threshold = 0.80
self.ai_suggestion_confidence = 0.95
self.human_resolution_confidence = 0.90
def should_auto_approve(self, similarity: float) -> bool:
return similarity >= self.auto_approve_threshold
def should_use_human_resolution(self, similarity: float) -> bool:
return similarity < self.use_human_resolution_threshold
def get_confidence_score(self, similarity: float, use_human: bool = False) -> float:
if use_human:
return self.human_resolution_confidence
else:
return max(similarity, self.ai_suggestion_confidence)
from src.main import TSPAssistant
from src.core.database import db_manager
from src.core.models import WorkOrder, Conversation, WorkOrderSuggestion, KnowledgeEntry
@@ -250,51 +275,101 @@ def save_workorder_human_resolution(workorder_id):
rec = WorkOrderSuggestion(work_order_id=w.id)
session.add(rec)
rec.human_resolution = human_text
# 计算相似度(使用简单cosine TF-IDF避免外部服务依赖
# 计算语义相似度(使用sentence-transformers进行更准确的语义比较
try:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
texts = [rec.ai_suggestion or "", human_text]
vec = TfidfVectorizer(max_features=1000)
mat = vec.fit_transform(texts)
sim = float(cosine_similarity(mat[0:1], mat[1:2])[0][0])
except Exception:
sim = 0.0
from src.utils.semantic_similarity import calculate_semantic_similarity
ai_text = rec.ai_suggestion or ""
sim = calculate_semantic_similarity(ai_text, human_text)
logger.info(f"AI建议与人工描述语义相似度: {sim:.4f}")
except Exception as e:
logger.error(f"计算语义相似度失败: {e}")
# 回退到传统方法
try:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
texts = [rec.ai_suggestion or "", human_text]
vec = TfidfVectorizer(max_features=1000)
mat = vec.fit_transform(texts)
sim = float(cosine_similarity(mat[0:1], mat[1:2])[0][0])
except Exception:
sim = 0.0
rec.ai_similarity = sim
# 自动审批条件≥0.95
approved = sim >= 0.95
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 自动审批条件
approved = config.should_auto_approve(sim)
rec.approved = approved
# 记录使用人工描述入库的标记当AI准确率低于阈值时
use_human_resolution = config.should_use_human_resolution(sim)
rec.use_human_resolution = use_human_resolution
session.commit()
return jsonify({"success": True, "similarity": sim, "approved": approved})
return jsonify({
"success": True,
"similarity": sim,
"approved": approved,
"use_human_resolution": use_human_resolution
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/approve-to-knowledge', methods=['POST'])
def approve_workorder_to_knowledge(workorder_id):
"""将已审批的AI建议入库为知识条目"""
"""将已审批的AI建议或人工描述入库为知识条目"""
try:
with db_manager.get_session() as session:
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
if not rec or not rec.approved or not rec.ai_suggestion:
return jsonify({"error": "未找到可入库的已审批AI建议"}), 400
# 入库为知识条目(问=工单标题;答=AI建议类目用工单分类
if not rec:
return jsonify({"error": "未找到工单建议记录"}), 400
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 确定使用哪个内容入库
if rec.use_human_resolution and rec.human_resolution:
# AI准确率低于阈值使用人工描述入库
answer_content = rec.human_resolution
confidence_score = config.get_confidence_score(rec.ai_similarity or 0, use_human=True)
verified_by = 'human_resolution'
logger.info(f"工单 {workorder_id} 使用人工描述入库AI相似度: {rec.ai_similarity:.4f}")
elif rec.approved and rec.ai_suggestion:
# AI准确率≥阈值使用AI建议入库
answer_content = rec.ai_suggestion
confidence_score = config.get_confidence_score(rec.ai_similarity or 0, use_human=False)
verified_by = 'auto_approve'
logger.info(f"工单 {workorder_id} 使用AI建议入库相似度: {rec.ai_similarity:.4f}")
else:
return jsonify({"error": "未找到可入库的内容"}), 400
# 入库为知识条目
entry = KnowledgeEntry(
question=w.title or (w.description[:20] if w.description else '工单问题'),
answer=rec.ai_suggestion,
answer=answer_content,
category=w.category or '其他',
confidence_score=0.95,
confidence_score=confidence_score,
is_active=True,
is_verified=True,
verified_by='auto_approve',
verified_by=verified_by,
verified_at=datetime.now()
)
session.add(entry)
session.commit()
return jsonify({"success": True, "knowledge_id": entry.id})
return jsonify({
"success": True,
"knowledge_id": entry.id,
"used_content": "human_resolution" if rec.use_human_resolution else "ai_suggestion",
"confidence_score": confidence_score
})
except Exception as e:
logger.error(f"入库知识库失败: {e}")
return jsonify({"error": str(e)}), 500
@workorders_bp.route('/import', methods=['POST'])

View File

@@ -604,64 +604,406 @@ body {
color: #6c757d;
font-size: 0.8rem;
}
/* f<EFBFBD><EFBFBD>pencaSGr7h_ */
.vehicle-data-card {
background: linear-gradient(135deg, #e8f5e8, #f0f8f0);
border: 1px solid #4caf50;
border-radius: 10px;
margin: 10px 0;
padding: 15px;
box-shadow: 0 2px 10px rgba(76, 175, 80, 0.1);
}
.vehicle-data-header {
border-bottom: 1px solid #4caf50;
padding-bottom: 10px;
margin-bottom: 15px;
}
.vehicle-data-header h5 {
color: #2e7d32;
margin: 0;
font-size: 1.1rem;
}
.vehicle-data-content {
padding: 0;
}
.vehicle-info {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
border-left: 4px solid #4caf50;
}
.vehicle-info h6 {
color: #1976d2;
margin-bottom: 8px;
font-size: 1rem;
}
.vehicle-details p {
margin: 5px 0;
font-size: 0.9rem;
color: #333;
}
.vehicle-details strong {
color: #2e7d32;
}
.vehicle-error {
background: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 5px;
border-left: 4px solid #f44336;
font-style: italic;
}
/* AI建议与人工描述优化样式 */
.ai-suggestion-section {
background: linear-gradient(135deg, #f8f9ff, #e8f2ff);
border: 1px solid #cce7ff;
border-radius: 12px;
padding: 20px;
margin: 15px 0;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
}
.ai-suggestion-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e3f2fd;
}
.ai-suggestion-title {
font-size: 1.1rem;
font-weight: 600;
color: #1976d2;
margin: 0;
display: flex;
align-items: center;
}
.ai-suggestion-title i {
margin-right: 8px;
color: #2196f3;
}
.generate-ai-btn {
background: linear-gradient(135deg, #2196f3, #1976d2);
border: none;
border-radius: 8px;
padding: 8px 16px;
color: white;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
}
.generate-ai-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
background: linear-gradient(135deg, #1976d2, #1565c0);
}
.ai-suggestion-content {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
min-height: 100px;
position: relative;
}
.ai-suggestion-content textarea {
border: none;
background: transparent;
resize: none;
font-size: 0.95rem;
line-height: 1.5;
color: #333;
}
.ai-suggestion-content textarea:focus {
outline: none;
box-shadow: none;
}
.human-resolution-content {
background: #fff8e1;
border: 1px solid #ffcc02;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
position: relative;
}
.human-resolution-content textarea {
border: none;
background: transparent;
resize: none;
font-size: 0.95rem;
line-height: 1.5;
color: #333;
}
.human-resolution-content textarea:focus {
outline: none;
box-shadow: none;
}
.similarity-indicator {
display: flex;
align-items: center;
gap: 12px;
margin: 15px 0;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
border-left: 4px solid #2196f3;
}
.similarity-badge {
font-size: 0.9rem;
font-weight: 600;
padding: 6px 12px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 6px;
}
.similarity-badge.high {
background: linear-gradient(135deg, #4caf50, #2e7d32);
color: white;
}
.similarity-badge.medium {
background: linear-gradient(135deg, #ff9800, #f57c00);
color: white;
}
.similarity-badge.low {
background: linear-gradient(135deg, #f44336, #d32f2f);
color: white;
}
.action-buttons {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.save-human-btn {
background: linear-gradient(135deg, #4caf50, #2e7d32);
border: none;
border-radius: 8px;
padding: 10px 20px;
color: white;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.save-human-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
background: linear-gradient(135deg, #2e7d32, #1b5e20);
}
.approve-btn {
background: linear-gradient(135deg, #ff9800, #f57c00);
border: none;
border-radius: 8px;
padding: 10px 20px;
color: white;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(255, 152, 0, 0.3);
}
.approve-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4);
background: linear-gradient(135deg, #f57c00, #ef6c00);
}
.approve-btn.approved {
background: linear-gradient(135deg, #4caf50, #2e7d32);
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.3);
}
.approve-btn.approved:hover {
background: linear-gradient(135deg, #2e7d32, #1b5e20);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
}
.status-badge {
font-size: 0.85rem;
padding: 4px 8px;
border-radius: 12px;
font-weight: 500;
}
.status-badge.approved {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #4caf50;
}
.status-badge.pending {
background: #fff3e0;
color: #f57c00;
border: 1px solid #ff9800;
}
.status-badge.human-resolution {
background: #e3f2fd;
color: #1976d2;
border: 1px solid #2196f3;
}
/* 加载状态优化 */
.ai-loading {
position: relative;
overflow: hidden;
}
.ai-loading::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
/* 按钮加载状态 */
.btn-loading {
position: relative;
color: transparent !important;
}
.btn-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 成功动画 */
.success-animation {
animation: successPulse 0.6s ease-in-out;
}
@keyframes successPulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* 工具提示优化 */
.tooltip-custom {
position: relative;
cursor: help;
}
.tooltip-custom::before {
content: attr(data-tooltip);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
}
.tooltip-custom::after {
content: '';
position: absolute;
bottom: 115%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.tooltip-custom:hover::before,
.tooltip-custom:hover::after {
opacity: 1;
visibility: visible;
}
/* 响应式优化 */
@media (max-width: 768px) {
.ai-suggestion-section {
padding: 15px;
margin: 10px 0;
}
.ai-suggestion-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.similarity-indicator {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.save-human-btn,
.approve-btn {
width: 100%;
text-align: center;
}
.tooltip-custom::before {
font-size: 0.7rem;
padding: 6px 10px;
}
}
/* f<EFBFBD><EFBFBD><EFBFBD>pencaSGr7h_ */
.vehicle-data-card {
background: linear-gradient(135deg, #e8f5e8, #f0f8f0);
border: 1px solid #4caf50;
border-radius: 10px;
margin: 10px 0;
padding: 15px;
box-shadow: 0 2px 10px rgba(76, 175, 80, 0.1);
}
.vehicle-data-header {
border-bottom: 1px solid #4caf50;
padding-bottom: 10px;
margin-bottom: 15px;
}
.vehicle-data-header h5 {
color: #2e7d32;
margin: 0;
font-size: 1.1rem;
}
.vehicle-data-content {
padding: 0;
}
.vehicle-info {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
border-left: 4px solid #4caf50;
}
.vehicle-info h6 {
color: #1976d2;
margin-bottom: 8px;
font-size: 1rem;
}
.vehicle-details p {
margin: 5px 0;
font-size: 0.9rem;
color: #333;
}
.vehicle-details strong {
color: #2e7d32;
}
.vehicle-error {
background: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 5px;
border-left: 4px solid #f44336;
font-style: italic;
}

View File

@@ -18,19 +18,51 @@ class TSPDashboard {
}
async generateAISuggestion(workorderId) {
const button = document.querySelector(`button[onclick="dashboard.generateAISuggestion(${workorderId})"]`);
const textarea = document.getElementById(`aiSuggestion_${workorderId}`);
try {
// 添加加载状态
if (button) {
button.classList.add('btn-loading');
button.disabled = true;
}
if (textarea) {
textarea.classList.add('ai-loading');
textarea.value = '正在生成AI建议请稍候...';
}
const resp = await fetch(`/api/workorders/${workorderId}/ai-suggestion`, { method: 'POST' });
const data = await resp.json();
if (data.success) {
const ta = document.getElementById(`aiSuggestion_${workorderId}`);
if (ta) ta.value = data.ai_suggestion || '';
if (textarea) {
textarea.value = data.ai_suggestion || '';
textarea.classList.remove('ai-loading');
textarea.classList.add('success-animation');
// 移除成功动画类
setTimeout(() => {
textarea.classList.remove('success-animation');
}, 600);
}
this.showNotification('AI建议已生成', 'success');
} else {
throw new Error(data.error || '生成失败');
}
} catch (e) {
console.error('生成AI建议失败:', e);
if (textarea) {
textarea.value = 'AI建议生成失败请重试';
textarea.classList.remove('ai-loading');
}
this.showNotification('生成AI建议失败: ' + e.message, 'error');
} finally {
// 移除加载状态
if (button) {
button.classList.remove('btn-loading');
button.disabled = false;
}
}
}
@@ -49,10 +81,60 @@ class TSPDashboard {
const apprEl = document.getElementById(`aiApproved_${workorderId}`);
const approveBtn = document.getElementById(`approveBtn_${workorderId}`);
const percent = Math.round((data.similarity || 0) * 100);
if (simEl) { simEl.textContent = `相似度: ${percent}%`; simEl.className = `badge ${percent>=95?'bg-success':percent>=70?'bg-warning':'bg-secondary'}`; }
if (apprEl) { apprEl.textContent = data.approved ? '已自动审批' : '未审批'; apprEl.className = `badge ${data.approved?'bg-success':'bg-secondary'}`; }
if (approveBtn) approveBtn.disabled = !data.approved;
this.showNotification('人工描述已保存并评估完成', 'success');
// 更新相似度显示,使用语义相似度
if (simEl) {
simEl.innerHTML = `<i class="fas fa-percentage"></i>语义相似度: ${percent}%`;
// 使用新的CSS类
if (percent >= 90) {
simEl.className = 'similarity-badge high';
} else if (percent >= 80) {
simEl.className = 'similarity-badge medium';
} else {
simEl.className = 'similarity-badge low';
}
simEl.title = this.getSimilarityExplanation(percent);
}
// 更新审批状态
if (apprEl) {
if (data.use_human_resolution) {
apprEl.textContent = '将使用人工描述入库';
apprEl.className = 'status-badge human-resolution';
} else if (data.approved) {
apprEl.textContent = '已自动审批';
apprEl.className = 'status-badge approved';
} else {
apprEl.textContent = '未审批';
apprEl.className = 'status-badge pending';
}
}
// 更新审批按钮状态
if (approveBtn) {
const canApprove = data.approved || data.use_human_resolution;
approveBtn.disabled = !canApprove;
if (data.use_human_resolution) {
approveBtn.textContent = '使用人工描述入库';
approveBtn.className = 'approve-btn';
approveBtn.title = 'AI准确率低于90%,将使用人工描述入库';
} else if (data.approved) {
approveBtn.textContent = '已自动审批';
approveBtn.className = 'approve-btn approved';
approveBtn.title = 'AI建议与人工描述高度一致';
} else {
approveBtn.textContent = '审批入库';
approveBtn.className = 'approve-btn';
approveBtn.title = '手动审批入库';
}
}
// 显示更详细的反馈信息
const message = this.getSimilarityMessage(percent, data.approved, data.use_human_resolution);
this.showNotification(message, data.approved ? 'success' : data.use_human_resolution ? 'warning' : 'info');
} else {
throw new Error(data.error || '保存失败');
}
@@ -67,7 +149,9 @@ class TSPDashboard {
const resp = await fetch(`/api/workorders/${workorderId}/approve-to-knowledge`, { method: 'POST' });
const data = await resp.json();
if (data.success) {
this.showNotification('已入库为知识条目', 'success');
const contentType = data.used_content === 'human_resolution' ? '人工描述' : 'AI建议';
const confidence = Math.round((data.confidence_score || 0) * 100);
this.showNotification(`已入库为知识条目!使用${contentType},置信度: ${confidence}%`, 'success');
} else {
throw new Error(data.error || '入库失败');
}
@@ -1713,29 +1797,43 @@ class TSPDashboard {
<small class="text-muted">${workorder.satisfaction_score}/5.0</small>
</div>
` : ''}
<h6 class="mt-3">AI建议与人工描述</h6>
<div class="border p-3 rounded">
<div class="mb-2">
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.generateAISuggestion(${workorder.id})">
<div class="ai-suggestion-section">
<div class="ai-suggestion-header">
<h6 class="ai-suggestion-title">
<i class="fas fa-robot"></i>AI建议与人工描述
</h6>
<button class="generate-ai-btn" onclick="dashboard.generateAISuggestion(${workorder.id})">
<i class="fas fa-magic me-1"></i>生成AI建议
</button>
</div>
<div class="mb-2">
<label class="form-label">AI建议</label>
<textarea id="aiSuggestion_${workorder.id}" class="form-control" rows="4" placeholder="点击上方按钮生成..." readonly></textarea>
<div class="ai-suggestion-content">
<label class="form-label fw-bold text-primary mb-2">
<i class="fas fa-brain me-1"></i>AI建议
</label>
<textarea id="aiSuggestion_${workorder.id}" class="form-control" rows="4" placeholder="点击上方按钮生成AI建议..." readonly></textarea>
</div>
<div class="mb-2">
<label class="form-label">人工描述</label>
<div class="human-resolution-content">
<label class="form-label fw-bold text-warning mb-2">
<i class="fas fa-user-edit me-1"></i>人工描述
</label>
<textarea id="humanResolution_${workorder.id}" class="form-control" rows="3" placeholder="请填写人工处理描述..."></textarea>
</div>
<div class="d-flex align-items-center gap-2 mb-2">
<button class="btn btn-sm btn-outline-success" onclick="dashboard.saveHumanResolution(${workorder.id})">
<div class="similarity-indicator">
<button class="save-human-btn" onclick="dashboard.saveHumanResolution(${workorder.id})">
<i class="fas fa-save me-1"></i>保存人工描述并评估
</button>
<span id="aiSim_${workorder.id}" class="badge bg-secondary">相似度: --</span>
<span id="aiApproved_${workorder.id}" class="badge bg-secondary">未审批</span>
<button id="approveBtn_${workorder.id}" class="btn btn-sm btn-outline-primary" onclick="dashboard.approveToKnowledge(${workorder.id})" disabled>
<i class="fas fa-check me-1"></i>入库
<span id="aiSim_${workorder.id}" class="similarity-badge bg-secondary">
<i class="fas fa-percentage"></i>相似度: --
</span>
<span id="aiApproved_${workorder.id}" class="status-badge pending">未审批</span>
</div>
<div class="action-buttons">
<button id="approveBtn_${workorder.id}" class="approve-btn" onclick="dashboard.approveToKnowledge(${workorder.id})" disabled>
<i class="fas fa-check me-1"></i>审批入库
</button>
</div>
</div>
@@ -4043,6 +4141,34 @@ class TSPDashboard {
}, 3000);
}
getSimilarityExplanation(percent) {
if (percent >= 95) {
return "语义高度相似AI建议与人工描述基本一致建议自动审批";
} else if (percent >= 90) {
return "语义较为相似AI建议与人工描述大体一致建议人工审核";
} else if (percent >= 80) {
return "语义部分相似AI建议与人工描述有一定差异需要人工判断";
} else if (percent >= 60) {
return "语义相似度较低AI建议与人工描述差异较大建议使用人工描述";
} else {
return "语义差异很大AI建议与人工描述差异很大优先使用人工描述";
}
}
getSimilarityMessage(percent, approved, useHumanResolution = false) {
if (useHumanResolution) {
return `人工描述已保存!语义相似度: ${percent}%AI准确率低于90%,将使用人工描述入库`;
} else if (approved) {
return `人工描述已保存!语义相似度: ${percent}%,已自动审批入库`;
} else if (percent >= 90) {
return `人工描述已保存!语义相似度: ${percent}%,建议人工审核后审批`;
} else if (percent >= 80) {
return `人工描述已保存!语义相似度: ${percent}%,需要人工判断是否审批`;
} else {
return `人工描述已保存!语义相似度: ${percent}%,建议使用人工描述入库`;
}
}
showCreateWorkOrderModal() {
const modal = new bootstrap.Modal(document.getElementById('createWorkOrderModal'));
modal.show();
@@ -4166,8 +4292,390 @@ class TSPDashboard {
}
}
// 飞书同步管理器
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);
}
}
// 初始化应用
let dashboard;
let feishuSync;
document.addEventListener('DOMContentLoaded', () => {
dashboard = new TSPDashboard();
feishuSync = new FeishuSyncManager();
});

View File

@@ -417,6 +417,10 @@
<i class="fas fa-tasks"></i>
工单管理
</a>
<a class="nav-link" href="#feishu-sync" data-tab="feishu-sync">
<i class="fas fa-sync"></i>
飞书同步
</a>
<a class="nav-link" href="#conversation-history" data-tab="conversation-history">
<i class="fas fa-history"></i>
对话历史
@@ -1034,6 +1038,181 @@
</div>
</div>
<!-- 飞书同步标签页 -->
<div id="feishu-sync-tab" class="tab-content" style="display: none;">
<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 me-2"></i>飞书同步管理
</h5>
</div>
<div class="card-body">
<!-- 配置区域 -->
<div class="row mb-4">
<div class="col-12">
<h6 class="text-muted mb-3">飞书配置</h6>
<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 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">
<h6 class="text-muted mb-3">同步操作</h6>
<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 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>
</div>
</div>
</div>
</div>
</div>
<!-- 对话历史标签页 -->
<div id="conversation-history-tab" class="tab-content" style="display: none;">
<div class="row mb-4">
@@ -2256,6 +2435,35 @@
</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 src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>

View File

@@ -1,662 +0,0 @@
<!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>