feat: 快速提交 - 周一 2025/09/22 11:54:13.80

This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-09-22 11:54:14 +01:00
parent 16bb98131e
commit a2b4fcdf36
10 changed files with 848 additions and 25 deletions

View File

@@ -88,6 +88,16 @@ class FeishuClient:
response = requests.request(method, url, timeout=30, **kwargs)
logger.info(f"飞书API响应状态码: {response.status_code}")
# 处理403权限错误
if response.status_code == 403:
try:
error_data = response.json()
logger.error(f"飞书API权限错误: {error_data}")
raise Exception(f"飞书API权限不足: {error_data.get('msg', '未知权限错误')}")
except:
logger.error(f"飞书API权限错误无法解析响应内容")
raise Exception(f"飞书API权限不足状态码: {response.status_code}")
response.raise_for_status()
result = response.json()
logger.info(f"飞书API响应内容: {result}")

View File

@@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
"""
飞书权限检查工具
用于诊断和解决飞书API权限问题
"""
import logging
from typing import Dict, Any, List
from src.integrations.feishu_client import FeishuClient
from src.integrations.config_manager import config_manager
logger = logging.getLogger(__name__)
class FeishuPermissionChecker:
"""飞书权限检查器"""
def __init__(self):
self.feishu_config = config_manager.get_feishu_config()
self.client = None
if self.feishu_config.get("app_id") and self.feishu_config.get("app_secret"):
self.client = FeishuClient(
self.feishu_config["app_id"],
self.feishu_config["app_secret"]
)
def check_permissions(self) -> Dict[str, Any]:
"""
检查飞书应用权限
Returns:
权限检查结果
"""
result = {
"success": False,
"checks": {},
"recommendations": [],
"errors": []
}
if not self.client:
result["errors"].append("飞书客户端未初始化,请检查配置")
return result
# 1. 检查访问令牌
try:
token = self.client._get_access_token()
if token:
result["checks"]["access_token"] = {
"status": "success",
"message": "访问令牌获取成功"
}
else:
result["checks"]["access_token"] = {
"status": "failed",
"message": "无法获取访问令牌"
}
result["errors"].append("无法获取访问令牌")
except Exception as e:
result["checks"]["access_token"] = {
"status": "failed",
"message": f"访问令牌获取失败: {e}"
}
result["errors"].append(f"访问令牌获取失败: {e}")
# 2. 检查应用权限
try:
app_token = self.feishu_config.get("app_token")
table_id = self.feishu_config.get("table_id")
if not app_token or not table_id:
result["errors"].append("缺少app_token或table_id配置")
return result
# 尝试获取表格信息
table_info = self._get_table_info(app_token, table_id)
if table_info:
result["checks"]["table_access"] = {
"status": "success",
"message": "可以访问表格"
}
else:
result["checks"]["table_access"] = {
"status": "failed",
"message": "无法访问表格"
}
result["errors"].append("无法访问表格")
except Exception as e:
result["checks"]["table_access"] = {
"status": "failed",
"message": f"表格访问失败: {e}"
}
result["errors"].append(f"表格访问失败: {e}")
# 3. 检查记录读取权限
try:
records = self.client.get_table_records(app_token, table_id, page_size=1)
if records.get("code") == 0:
result["checks"]["read_records"] = {
"status": "success",
"message": "可以读取记录"
}
else:
result["checks"]["read_records"] = {
"status": "failed",
"message": f"读取记录失败: {records.get('msg', '未知错误')}"
}
result["errors"].append(f"读取记录失败: {records.get('msg', '未知错误')}")
except Exception as e:
result["checks"]["read_records"] = {
"status": "failed",
"message": f"读取记录失败: {e}"
}
result["errors"].append(f"读取记录失败: {e}")
# 4. 检查记录更新权限
try:
# 先获取一条记录进行测试
records = self.client.get_table_records(app_token, table_id, page_size=1)
if records.get("code") == 0 and records.get("data", {}).get("items"):
test_record = records["data"]["items"][0]
record_id = test_record["record_id"]
# 尝试更新一个测试字段
update_result = self.client.update_table_record(
app_token,
table_id,
record_id,
{"测试字段": "权限测试"}
)
if update_result.get("code") == 0:
result["checks"]["update_records"] = {
"status": "success",
"message": "可以更新记录"
}
else:
result["checks"]["update_records"] = {
"status": "failed",
"message": f"更新记录失败: {update_result.get('msg', '未知错误')}"
}
result["errors"].append(f"更新记录失败: {update_result.get('msg', '未知错误')}")
else:
result["checks"]["update_records"] = {
"status": "failed",
"message": "没有记录可用于测试更新权限"
}
result["errors"].append("没有记录可用于测试更新权限")
except Exception as e:
result["checks"]["update_records"] = {
"status": "failed",
"message": f"更新记录测试失败: {e}"
}
result["errors"].append(f"更新记录测试失败: {e}")
# 5. 检查AI建议字段权限
try:
# 检查AI建议字段是否存在
table_fields = self._get_table_fields(app_token, table_id)
if table_fields:
ai_field_exists = any(
field.get("field_name") == "AI建议"
for field in table_fields.get("data", {}).get("items", [])
)
if ai_field_exists:
result["checks"]["ai_field"] = {
"status": "success",
"message": "AI建议字段存在"
}
else:
result["checks"]["ai_field"] = {
"status": "warning",
"message": "AI建议字段不存在"
}
result["recommendations"].append("请在飞书表格中添加'AI建议'字段")
else:
result["checks"]["ai_field"] = {
"status": "failed",
"message": "无法获取表格字段信息"
}
result["errors"].append("无法获取表格字段信息")
except Exception as e:
result["checks"]["ai_field"] = {
"status": "failed",
"message": f"检查AI建议字段失败: {e}"
}
result["errors"].append(f"检查AI建议字段失败: {e}")
# 生成建议
self._generate_recommendations(result)
# 判断整体状态
failed_checks = [check for check in result["checks"].values()
if check["status"] == "failed"]
if not failed_checks:
result["success"] = True
return result
def _get_table_info(self, app_token: str, table_id: str) -> Dict[str, Any]:
"""获取表格信息"""
try:
url = f"{self.client.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}"
return self.client._make_request("GET", url)
except Exception as e:
logger.error(f"获取表格信息失败: {e}")
return None
def _get_table_fields(self, app_token: str, table_id: str) -> Dict[str, Any]:
"""获取表格字段信息"""
try:
url = f"{self.client.base_url}/bitable/v1/apps/{app_token}/tables/{table_id}/fields"
return self.client._make_request("GET", url)
except Exception as e:
logger.error(f"获取表格字段失败: {e}")
return None
def _generate_recommendations(self, result: Dict[str, Any]):
"""生成修复建议"""
recommendations = result["recommendations"]
# 基于检查结果生成建议
if "access_token" in result["checks"] and result["checks"]["access_token"]["status"] == "failed":
recommendations.append("检查飞书应用的app_id和app_secret是否正确")
recommendations.append("确认飞书应用已启用并获取了必要的权限")
if "table_access" in result["checks"] and result["checks"]["table_access"]["status"] == "failed":
recommendations.append("检查app_token和table_id是否正确")
recommendations.append("确认应用有访问该表格的权限")
if "update_records" in result["checks"] and result["checks"]["update_records"]["status"] == "failed":
recommendations.append("检查飞书应用是否有'编辑'权限")
recommendations.append("确认表格没有被锁定或只读")
recommendations.append("检查应用是否被添加到表格的协作者中")
if "ai_field" in result["checks"] and result["checks"]["ai_field"]["status"] == "warning":
recommendations.append("在飞书表格中添加'AI建议'字段")
recommendations.append("确保字段类型为'多行文本''单行文本'")
# 通用建议
if result["errors"]:
recommendations.append("查看飞书开放平台文档了解权限配置")
recommendations.append("联系飞书管理员确认应用权限设置")
def get_permission_summary(self) -> str:
"""获取权限检查摘要"""
result = self.check_permissions()
summary = "飞书权限检查结果:\n"
summary += f"整体状态: {'✅ 正常' if result['success'] else '❌ 异常'}\n\n"
summary += "检查项目:\n"
for check_name, check_result in result["checks"].items():
status_icon = "" if check_result["status"] == "success" else "⚠️" if check_result["status"] == "warning" else ""
summary += f" {status_icon} {check_name}: {check_result['message']}\n"
if result["recommendations"]:
summary += "\n修复建议:\n"
for i, rec in enumerate(result["recommendations"], 1):
summary += f" {i}. {rec}\n"
if result["errors"]:
summary += "\n错误信息:\n"
for i, error in enumerate(result["errors"], 1):
summary += f" {i}. {error}\n"
return summary

View File

@@ -437,12 +437,13 @@ class WorkOrderSyncService:
def _update_feishu_ai_suggestion(self, record_id: str, ai_suggestion: str) -> bool:
"""更新飞书表格中的AI建议"""
try:
result = self.feishu_client.update_record(
result = self.feishu_client.update_table_record(
self.app_token,
self.table_id,
record_id,
{"AI建议": ai_suggestion}
)
logger.info(f"更新飞书AI建议结果: {result}")
return result.get("code") == 0
except Exception as e:
logger.error(f"更新飞书AI建议失败: {e}")

View File

@@ -8,6 +8,7 @@ from flask import Blueprint, request, jsonify, render_template
from src.integrations.feishu_client import FeishuClient
from src.integrations.workorder_sync import WorkOrderSyncService
from src.integrations.config_manager import config_manager
from src.integrations.feishu_permission_checker import FeishuPermissionChecker
import logging
logger = logging.getLogger(__name__)
@@ -322,6 +323,22 @@ def remove_field_mapping():
logger.error(f"移除字段映射失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/check-permissions')
def check_permissions():
"""检查飞书权限"""
try:
checker = FeishuPermissionChecker()
result = checker.check_permissions()
return jsonify({
"success": True,
"permission_check": result,
"summary": checker.get_permission_summary()
})
except Exception as e:
logger.error(f"权限检查失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/field-mapping')
def field_mapping_page():
"""字段映射管理页面"""

View File

@@ -15,6 +15,11 @@ class TSPDashboard {
this.init();
this.restorePageState();
// 添加页面卸载时的清理逻辑
window.addEventListener('beforeunload', () => {
this.destroyAllCharts();
});
}
async generateAISuggestion(workorderId) {
@@ -335,6 +340,14 @@ class TSPDashboard {
this.currentTab = tabName;
// 如果切换到分析页面,重新初始化图表
if (tabName === 'analytics') {
// 延迟一点时间确保DOM已更新
setTimeout(() => {
this.initializeCharts();
}, 100);
}
// 保存当前页面状态
this.savePageState();
@@ -3160,9 +3173,63 @@ class TSPDashboard {
// 初始化图表
initializeCharts() {
this.charts = {};
if (!this.charts) {
this.charts = {};
}
this.updateCharts();
}
// 销毁所有图表
destroyAllCharts() {
if (!this.charts) return;
Object.keys(this.charts).forEach(chartId => {
if (this.charts[chartId]) {
try {
this.charts[chartId].destroy();
} catch (e) {
console.warn(`Error destroying chart ${chartId}:`, e);
}
this.charts[chartId] = null;
}
});
// 清理charts对象
this.charts = {};
}
// 安全的图表创建方法
createChart(canvasId, chartConfig) {
const canvas = document.getElementById(canvasId);
if (!canvas) {
console.error(`Canvas element '${canvasId}' not found`);
return null;
}
// 确保charts对象存在
if (!this.charts) {
this.charts = {};
}
// 销毁现有图表
if (this.charts[canvasId]) {
try {
this.charts[canvasId].destroy();
} catch (e) {
console.warn(`Error destroying chart ${canvasId}:`, e);
}
this.charts[canvasId] = null;
}
try {
const ctx = canvas.getContext('2d');
this.charts[canvasId] = new Chart(ctx, chartConfig);
return this.charts[canvasId];
} catch (e) {
console.error(`Error creating chart ${canvasId}:`, e);
return null;
}
}
// 更新所有图表
async updateCharts() {
@@ -3215,16 +3282,9 @@ class TSPDashboard {
// 更新主图表
updateMainChart(data, chartType) {
const ctx = document.getElementById('mainChart').getContext('2d');
// 销毁现有图表
if (this.charts.mainChart) {
this.charts.mainChart.destroy();
}
const chartData = this.prepareChartData(data, chartType);
this.charts.mainChart = new Chart(ctx, {
const chartConfig = {
type: chartType,
data: chartData,
options: {
@@ -3257,22 +3317,18 @@ class TSPDashboard {
}
}
}
});
};
this.createChart('mainChart', chartConfig);
}
// 更新分布图表
updateDistributionChart(data) {
const ctx = document.getElementById('distributionChart').getContext('2d');
if (this.charts.distributionChart) {
this.charts.distributionChart.destroy();
}
const categories = data.workorders?.by_category || {};
const labels = Object.keys(categories);
const values = Object.values(categories);
this.charts.distributionChart = new Chart(ctx, {
const chartConfig = {
type: 'doughnut',
data: {
labels: labels,
@@ -3302,7 +3358,9 @@ class TSPDashboard {
}
}
}
});
};
this.createChart('distributionChart', chartConfig);
}
// 更新趋势图表
@@ -4901,6 +4959,78 @@ class FeishuSyncManager {
}
}
async checkPermissions() {
try {
this.showNotification('正在检查飞书权限...', 'info');
const response = await fetch('/api/feishu-sync/check-permissions');
const data = await response.json();
if (data.success) {
this.displayPermissionCheck(data.permission_check, data.summary);
this.showNotification('权限检查完成', 'success');
} else {
this.showNotification('权限检查失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('权限检查失败: ' + error.message, 'error');
}
}
displayPermissionCheck(permissionCheck, summary) {
const container = document.getElementById('fieldMappingContent');
let html = '<div class="card"><div class="card-header"><h6 class="card-title mb-0"><i class="fas fa-shield-alt me-2"></i>飞书权限检查结果</h6></div><div class="card-body">';
// 整体状态
const statusClass = permissionCheck.success ? 'success' : 'danger';
const statusIcon = permissionCheck.success ? 'check-circle' : 'exclamation-triangle';
html += `<div class="alert alert-${statusClass}">
<i class="fas fa-${statusIcon}"></i>
整体状态: ${permissionCheck.success ? '正常' : '异常'}
</div>`;
// 检查项目
html += '<h6>检查项目:</h6>';
for (const [checkName, checkResult] of Object.entries(permissionCheck.checks)) {
const statusClass = checkResult.status === 'success' ? 'success' :
checkResult.status === 'warning' ? 'warning' : 'danger';
const statusIcon = checkResult.status === 'success' ? 'check-circle' :
checkResult.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
html += `<div class="alert alert-${statusClass} py-2">
<i class="fas fa-${statusIcon}"></i>
<strong>${checkName}</strong>: ${checkResult.message}
</div>`;
}
// 修复建议
if (permissionCheck.recommendations && permissionCheck.recommendations.length > 0) {
html += '<h6>修复建议:</h6><ul class="list-group mb-3">';
permissionCheck.recommendations.forEach(rec => {
html += `<li class="list-group-item">${rec}</li>`;
});
html += '</ul>';
}
// 错误信息
if (permissionCheck.errors && permissionCheck.errors.length > 0) {
html += '<h6>错误信息:</h6><ul class="list-group">';
permissionCheck.errors.forEach(error => {
html += `<li class="list-group-item list-group-item-danger">${error}</li>`;
});
html += '</ul>';
}
html += '</div></div>';
container.innerHTML = html;
// 显示字段映射管理区域
const section = document.getElementById('fieldMappingSection');
section.style.display = 'block';
}
showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const alert = document.createElement('div');

View File

@@ -1148,6 +1148,9 @@
<button class="btn btn-warning" onclick="openFieldMapping()">
<i class="fas fa-exchange-alt me-1"></i>字段映射管理
</button>
<button class="btn btn-outline-danger" onclick="feishuSync.checkPermissions()">
<i class="fas fa-shield-alt me-1"></i>权限检查
</button>
<button class="btn btn-secondary" onclick="feishuSync.refreshStatus()">
<i class="fas fa-refresh me-1"></i>刷新状态
</button>