feat: 快速提交 - 周一 2025/09/22 11:54:13.80
This commit is contained in:
218
AI建议权限问题最终解决方案.md
Normal file
218
AI建议权限问题最终解决方案.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# AI建议权限问题最终解决方案
|
||||
|
||||
## 🔍 问题确认
|
||||
|
||||
通过深度诊断工具确认:
|
||||
- ✅ **访问令牌获取正常**:tenant_access_token获取成功
|
||||
- ✅ **表格访问正常**:可以成功访问表格和字段信息
|
||||
- ✅ **记录读取正常**:可以成功读取表格记录
|
||||
- ✅ **AI建议字段存在**:确认表格中有"AI建议"字段
|
||||
- ❌ **记录写入失败**:403 Forbidden,权限不足
|
||||
|
||||
**根本原因**:飞书应用缺少**记录写入权限**
|
||||
|
||||
## 🛠️ 最终解决方案
|
||||
|
||||
### 步骤1:飞书开放平台权限配置
|
||||
|
||||
#### 1.1 登录飞书开放平台
|
||||
1. 访问:https://open.feishu.cn/
|
||||
2. 使用管理员账号登录
|
||||
3. 进入"应用管理" → "我的应用"
|
||||
|
||||
#### 1.2 找到您的应用
|
||||
- 应用ID:`cli_a8b50ec0eed1500d`
|
||||
- 应用名称:您的TSP智能助手应用
|
||||
|
||||
#### 1.3 添加必需权限
|
||||
在"权限管理"页面,确保应用具有以下权限:
|
||||
|
||||
**核心权限**:
|
||||
```
|
||||
bitable:app # 多维表格应用权限
|
||||
bitable:app:readonly # 多维表格只读权限
|
||||
bitable:app:readwrite # 多维表格读写权限 ⭐ 关键权限
|
||||
base:record:write # 记录写入权限 ⭐ 关键权限
|
||||
```
|
||||
|
||||
#### 1.4 重新发布应用
|
||||
- 权限修改后,点击"发布"或"上线"
|
||||
- 等待权限生效(通常需要1-5分钟)
|
||||
|
||||
### 步骤2:飞书多维表格协作者权限
|
||||
|
||||
#### 2.1 打开飞书多维表格
|
||||
- 使用浏览器打开您的飞书多维表格
|
||||
- 确保您有表格的管理权限
|
||||
|
||||
#### 2.2 添加应用为协作者
|
||||
1. 点击表格右上角的"分享"按钮
|
||||
2. 点击"添加协作者"
|
||||
3. 搜索您的飞书应用名称或应用ID
|
||||
4. 将应用添加为协作者
|
||||
|
||||
#### 2.3 设置协作者权限
|
||||
**重要**:将权限设置为以下之一:
|
||||
- **编辑者** ✅ (推荐)
|
||||
- **管理员** ✅ (完全权限)
|
||||
|
||||
**不要设置为**:
|
||||
- **查看者** ❌ (只能读取,无法写入)
|
||||
|
||||
### 步骤3:验证修复结果
|
||||
|
||||
#### 3.1 使用Web界面验证
|
||||
1. 打开TSP智能助手主页面
|
||||
2. 点击"飞书同步"标签页
|
||||
3. 点击"权限检查"按钮
|
||||
4. 查看检查结果
|
||||
|
||||
#### 3.2 测试AI建议功能
|
||||
1. 点击"同步+AI建议"按钮
|
||||
2. 查看是否还有403错误
|
||||
3. 检查飞书表格中是否出现AI建议
|
||||
|
||||
## 📋 详细修复步骤
|
||||
|
||||
### 🔧 飞书开放平台操作
|
||||
|
||||
1. **登录飞书开放平台**
|
||||
```
|
||||
URL: https://open.feishu.cn/
|
||||
账号: 管理员账号
|
||||
```
|
||||
|
||||
2. **进入应用管理**
|
||||
```
|
||||
路径: 应用管理 → 我的应用
|
||||
应用ID: cli_a8b50ec0eed1500d
|
||||
```
|
||||
|
||||
3. **权限配置**
|
||||
```
|
||||
页面: 权限管理
|
||||
操作: 添加权限
|
||||
权限列表:
|
||||
- bitable:app
|
||||
- bitable:app:readonly
|
||||
- bitable:app:readwrite ⭐
|
||||
- base:record:write ⭐
|
||||
```
|
||||
|
||||
4. **发布应用**
|
||||
```
|
||||
操作: 点击"发布"按钮
|
||||
等待: 1-5分钟权限生效
|
||||
```
|
||||
|
||||
### 🔧 飞书多维表格操作
|
||||
|
||||
1. **打开表格**
|
||||
```
|
||||
方式: 浏览器访问飞书多维表格
|
||||
权限: 确保有管理权限
|
||||
```
|
||||
|
||||
2. **添加协作者**
|
||||
```
|
||||
操作: 点击右上角"分享"按钮
|
||||
步骤: 添加协作者 → 搜索应用名称
|
||||
应用: 您的TSP智能助手应用
|
||||
```
|
||||
|
||||
3. **设置权限**
|
||||
```
|
||||
权限级别: 编辑者 或 管理员
|
||||
不要选择: 查看者
|
||||
保存: 确认设置
|
||||
```
|
||||
|
||||
## 🚨 常见问题解决
|
||||
|
||||
### 问题1:找不到权限设置
|
||||
**解决方案**:
|
||||
- 确保使用管理员账号登录飞书开放平台
|
||||
- 检查应用是否已发布
|
||||
- 联系飞书技术支持
|
||||
|
||||
### 问题2:权限添加后仍然失败
|
||||
**解决方案**:
|
||||
- 等待5-10分钟让权限生效
|
||||
- 重新发布应用
|
||||
- 清除浏览器缓存后重试
|
||||
|
||||
### 问题3:找不到应用名称
|
||||
**解决方案**:
|
||||
- 使用应用ID:`cli_a8b50ec0eed1500d`
|
||||
- 在飞书开放平台搜索应用ID
|
||||
- 确认应用状态为"已发布"
|
||||
|
||||
### 问题4:表格分享设置找不到
|
||||
**解决方案**:
|
||||
- 确保您有表格的管理权限
|
||||
- 使用表格创建者账号
|
||||
- 联系表格管理员协助设置
|
||||
|
||||
## 📊 权限配置检查清单
|
||||
|
||||
### ✅ 飞书开放平台
|
||||
- [ ] 应用已启用并发布
|
||||
- [ ] 已添加`bitable:app`权限
|
||||
- [ ] 已添加`bitable:app:readonly`权限
|
||||
- [ ] 已添加`bitable:app:readwrite`权限 ⭐
|
||||
- [ ] 已添加`base:record:write`权限 ⭐
|
||||
- [ ] 权限修改后已重新发布
|
||||
|
||||
### ✅ 飞书多维表格
|
||||
- [ ] 应用已添加为表格协作者
|
||||
- [ ] 协作者权限设置为"编辑者"或"管理员"
|
||||
- [ ] 表格未被锁定或设置为只读
|
||||
- [ ] "AI建议"字段存在且类型正确
|
||||
|
||||
## 🎯 验证成功标志
|
||||
|
||||
修复成功后,您应该看到:
|
||||
|
||||
1. **权限检查通过**
|
||||
```
|
||||
✅ 访问令牌获取成功
|
||||
✅ 表格访问权限正常
|
||||
✅ 记录读取权限正常
|
||||
✅ 记录写入权限正常
|
||||
✅ AI建议字段存在
|
||||
```
|
||||
|
||||
2. **AI建议功能正常**
|
||||
```
|
||||
- 点击"同步+AI建议"无403错误
|
||||
- 飞书表格中出现AI建议内容
|
||||
- 日志显示"更新飞书AI建议成功"
|
||||
```
|
||||
|
||||
3. **系统日志正常**
|
||||
```
|
||||
2025-09-22 XX:XX:XX - INFO - 更新飞书AI建议成功
|
||||
2025-09-22 XX:XX:XX - INFO - 飞书同步完成
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如果按照以上步骤仍然无法解决问题,请:
|
||||
|
||||
1. **收集信息**:
|
||||
- 飞书应用ID:`cli_a8b50ec0eed1500d`
|
||||
- 表格ID:`tblnl3vJPpgMTSiP`
|
||||
- 完整的错误日志
|
||||
- 权限检查结果截图
|
||||
|
||||
2. **联系支持**:
|
||||
- 飞书开放平台技术支持
|
||||
- TSP智能助手技术支持
|
||||
|
||||
3. **检查企业设置**:
|
||||
- 确认是否有企业级权限限制
|
||||
- 联系企业飞书管理员
|
||||
|
||||
---
|
||||
|
||||
**重要提醒**:403权限错误通常需要飞书管理员权限才能解决,建议联系相关技术人员协助配置。修复完成后,AI建议功能将可以正常工作!🎉
|
||||
@@ -38,15 +38,104 @@ if %errorlevel% neq 0 (
|
||||
)
|
||||
echo ✅ 文件已添加到暂存区
|
||||
|
||||
:: 生成提交信息
|
||||
:: 检查markdown文件修改并生成智能提交信息
|
||||
echo.
|
||||
echo [3/4] 生成提交信息...
|
||||
for /f "tokens=*" %%i in ('git log --oneline -1') do set last_commit=%%i
|
||||
set /p commit_msg="请输入提交信息 (直接回车使用默认): "
|
||||
if "%commit_msg%"=="" (
|
||||
echo [3/4] 分析markdown文件并生成提交信息...
|
||||
|
||||
:: 检查是否有markdown文件修改
|
||||
set md_files=
|
||||
for /f "tokens=*" %%f in ('git diff --name-only --cached 2^>nul ^| findstr /i "\.md$"') do (
|
||||
set md_files=!md_files! %%f
|
||||
)
|
||||
for /f "tokens=*" %%f in ('git diff --name-only 2^>nul ^| findstr /i "\.md$"') do (
|
||||
set md_files=!md_files! %%f
|
||||
)
|
||||
|
||||
set commit_msg=
|
||||
if not "%md_files%"=="" (
|
||||
echo 📝 检测到markdown文件修改:
|
||||
echo %md_files%
|
||||
echo.
|
||||
|
||||
:: 提取markdown文件的主要内容
|
||||
set commit_title=
|
||||
set commit_type=docs
|
||||
|
||||
:: 检查是否有修复相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "修复\|解决\|问题\|错误"') do (
|
||||
set commit_type=fix
|
||||
set commit_title=修复问题
|
||||
goto :found_fix
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 检查是否有新功能相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "功能\|新增\|添加\|实现"') do (
|
||||
set commit_type=feat
|
||||
set commit_title=新增功能
|
||||
goto :found_feature
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 检查是否有优化相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "优化\|性能\|改进\|提升"') do (
|
||||
set commit_type=perf
|
||||
set commit_title=性能优化
|
||||
goto :found_optimization
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 提取文件标题
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /n "^#" ^| head -1') do (
|
||||
set line=%%l
|
||||
set line=!line:*:=!
|
||||
set line=!line:# =!
|
||||
set line=!line:## =!
|
||||
if "!line!" neq "" (
|
||||
set commit_title=!line!
|
||||
goto :found_title
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:found_fix
|
||||
:found_feature
|
||||
:found_optimization
|
||||
:found_title
|
||||
|
||||
if "%commit_title%"=="" (
|
||||
set commit_title=更新文档记录
|
||||
)
|
||||
|
||||
:: 生成提交信息
|
||||
set commit_msg=%commit_type%: %commit_title%
|
||||
echo 📋 生成的提交信息: %commit_msg%
|
||||
echo.
|
||||
) else (
|
||||
echo ℹ️ 没有检测到markdown文件修改
|
||||
set commit_msg=feat: 自动提交 - %date% %time%
|
||||
)
|
||||
|
||||
:: 询问是否使用生成的提交信息
|
||||
set /p confirm="是否使用此提交信息? (y/n/e编辑): "
|
||||
if /i "%confirm%"=="e" (
|
||||
set /p commit_msg="请输入自定义提交信息: "
|
||||
) else if /i "%confirm%" neq "y" (
|
||||
set /p commit_msg="请输入提交信息: "
|
||||
)
|
||||
|
||||
:: 提交更改
|
||||
echo 提交信息: %commit_msg%
|
||||
git commit -m "%commit_msg%"
|
||||
|
||||
@@ -1,11 +1,95 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
echo 🚀 TSP智能助手 - 快速推送
|
||||
echo.
|
||||
|
||||
:: 检查是否有参数
|
||||
if "%1"=="" (
|
||||
set commit_msg=feat: 快速提交 - %date% %time%
|
||||
:: 智能生成提交信息
|
||||
echo 📝 分析markdown文件并生成提交信息...
|
||||
|
||||
:: 检查是否有markdown文件修改
|
||||
set md_files=
|
||||
for /f "tokens=*" %%f in ('git diff --name-only --cached 2^>nul ^| findstr /i "\.md$"') do (
|
||||
set md_files=!md_files! %%f
|
||||
)
|
||||
for /f "tokens=*" %%f in ('git diff --name-only 2^>nul ^| findstr /i "\.md$"') do (
|
||||
set md_files=!md_files! %%f
|
||||
)
|
||||
|
||||
set commit_msg=
|
||||
if not "%md_files%"=="" (
|
||||
echo 📄 检测到markdown文件修改: %md_files%
|
||||
|
||||
:: 提取markdown文件的主要内容
|
||||
set commit_title=
|
||||
set commit_type=docs
|
||||
|
||||
:: 检查是否有修复相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "修复\|解决\|问题\|错误"') do (
|
||||
set commit_type=fix
|
||||
set commit_title=修复问题
|
||||
goto :found_fix
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 检查是否有新功能相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "功能\|新增\|添加\|实现"') do (
|
||||
set commit_type=feat
|
||||
set commit_title=新增功能
|
||||
goto :found_feature
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 检查是否有优化相关内容
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /i "优化\|性能\|改进\|提升"') do (
|
||||
set commit_type=perf
|
||||
set commit_title=性能优化
|
||||
goto :found_optimization
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: 提取文件标题
|
||||
for %%f in (%md_files%) do (
|
||||
if exist "%%f" (
|
||||
for /f "tokens=*" %%l in ('type "%%f" ^| findstr /n "^#" ^| head -1') do (
|
||||
set line=%%l
|
||||
set line=!line:*:=!
|
||||
set line=!line:# =!
|
||||
set line=!line:## =!
|
||||
if "!line!" neq "" (
|
||||
set commit_title=!line!
|
||||
goto :found_title
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:found_fix
|
||||
:found_feature
|
||||
:found_optimization
|
||||
:found_title
|
||||
|
||||
if "%commit_title%"=="" (
|
||||
set commit_title=更新文档记录
|
||||
)
|
||||
|
||||
:: 生成提交信息
|
||||
set commit_msg=%commit_type%: %commit_title%
|
||||
) else (
|
||||
echo ℹ️ 没有检测到markdown文件修改
|
||||
set commit_msg=feat: 快速提交 - %date% %time%
|
||||
)
|
||||
) else (
|
||||
set commit_msg=%1
|
||||
)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
271
src/integrations/feishu_permission_checker.py
Normal file
271
src/integrations/feishu_permission_checker.py
Normal 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
|
||||
@@ -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}")
|
||||
|
||||
@@ -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():
|
||||
"""字段映射管理页面"""
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user