feat:飞书文档补全

This commit is contained in:
Evilom
2026-06-08 10:04:12 +08:00
parent ab2ca1d836
commit 3b77605591
659 changed files with 51251 additions and 1340 deletions
+208
View File
@@ -0,0 +1,208 @@
import requests
import json
import os
from datetime import datetime
from typing import Optional, Dict, Any, List
from .tb_config import TBConfig
class TeambitionAPI:
def __init__(self, config: TBConfig):
self.config = config
self.api_url = config.get('teambition.api_url')
self.cookies = config.get('teambition.cookies', {})
self.export_config = config.get_export_config()
def fetch_tasks_activities(self, task_ids: List[str], max_workers: int = 20) -> List[Dict]:
"""
Fetch activities for specific tasks in parallel.
Returns flattened list of all activities.
"""
import concurrent.futures
all_activities = []
if not task_ids:
return []
print(f"正在获取 {len(task_ids)} 个任务的动态 (并发数 {max_workers})...")
headers = {
'accept': 'application/json',
'content-type': 'application/json',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
}
cookies = {k: v for k, v in self.config.get('teambition.cookies', {}).items() if v}
def fetch_single(t_id):
url = f"https://www.teambition.com/api/tasks/{t_id}/activities"
params = {'count': 50}
try:
r = requests.get(url, headers=headers, cookies=cookies, params=params, timeout=10)
if r.status_code == 200:
return r.json()
except Exception as e:
# print(f"Error fetching task {t_id}: {e}")
pass
return []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_id = {executor.submit(fetch_single, t_id): t_id for t_id in task_ids}
completed_count = 0
for future in concurrent.futures.as_completed(future_to_id):
completed_count += 1
if completed_count % 20 == 0:
print(f" 进度: {completed_count}/{len(task_ids)}...")
data = future.result()
if data:
all_activities.extend(data)
print(f"动态获取完成,共 {len(all_activities)} 条记录")
return all_activities
def fetch_tasks(self) -> Optional[str]:
headers = {
'accept': 'application/json',
'accept-language': 'en,zh;q=0.9,zh-CN;q=0.8',
'content-type': 'application/json',
'origin': 'https://www.teambition.com',
'referer': f'https://www.teambition.com/project/{self.export_config.get("projectId")}/tasks',
'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
'x-request-id': self._generate_request_id(),
'x-timezone': str(self.export_config.get('timezone', 8))
}
cookies = {k: v for k, v in self.cookies.items() if v}
payload = {
'tql': self.export_config.get('tql'),
'fileType': self.export_config.get('fileType'),
'resourceType': self.export_config.get('resourceType'),
'scope': self.export_config.get('scope'),
'projectId': self.export_config.get('projectId'),
'fields': self.export_config.get('fields'),
'timezone': self.export_config.get('timezone'),
'joinParentSubtask': self.export_config.get('joinParentSubtask')
}
try:
# 1. Trigger Export
print("正在触发任务导出...")
response = requests.post(
self.api_url,
headers=headers,
cookies=cookies,
json=payload,
timeout=30
)
response.raise_for_status()
data = response.json()
job_id = data.get('result', {}).get('jobId')
if not job_id:
print("未获取到 Job ID")
return None
print(f"导出任务已创建,Job ID: {job_id}")
# 2. Poll Notifications for Download URL
return self._poll_download_url(job_id, headers, cookies)
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return None
def _poll_download_url(self, job_id: str, headers: Dict, cookies: Dict) -> Optional[str]:
import time
import urllib.request
import ssl
print("正在等待导出完成通知...")
notification_url = "https://notifications.teambition.com/api/notifications"
max_retries = 30 # Wait up to 60s
for i in range(max_retries):
time.sleep(2)
try:
resp = requests.get(notification_url, headers=headers, cookies=cookies)
if resp.status_code == 200:
data = resp.json()
results = data.get('result', [])
# Look for the export completion notification
# We look for notifications that contain "export" or "download" in payload
# Ideally we match against the Project ID or time, but for now we look for the most recent system msg
for note in results:
payload = note.get('payload', {})
# Check if it's the right type of notification (system message about export)
if "已完成导出" in payload.get('summary', '') or "finished export" in payload.get('summary', ''):
# Extract URL
actions = payload.get('actions', [])
download_url = None
for action in actions:
if action.get('fallbackUrl'):
download_url = action.get('fallbackUrl')
break
if action.get('actionUrl') and action.get('actionUrl').startswith('http'):
download_url = action.get('actionUrl')
break
if download_url:
print(f"获取到下载地址: {download_url[:50]}...")
return self._download_file(download_url)
except Exception as e:
print(f"轮询通知失败: {e}")
print("等待导出超时")
return None
def _download_file(self, url: str) -> Optional[str]:
import urllib.request
import ssl
try:
print("正在下载 CSV 文件...")
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
}
req = urllib.request.Request(url, headers=headers)
context = ssl._create_unverified_context()
with urllib.request.urlopen(req, context=context) as response:
return response.read().decode('utf-8-sig') # Decode bytes to string for compatibility
except Exception as e:
print(f"下载文件失败: {e}")
return None
def _generate_request_id(self) -> str:
import uuid
return str(uuid.uuid4())
def save_csv(self, csv_content: str, output_dir: str = None) -> Optional[str]:
if output_dir is None:
output_dir = self.config.get('output.csv_dir', './data')
os.makedirs(output_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"tasks_{timestamp}.csv"
filepath = os.path.join(output_dir, filename)
try:
with open(filepath, 'w', encoding='utf-8-sig') as f:
f.write(csv_content)
print(f"CSV 文件已保存: {filepath}")
return filepath
except Exception as e:
print(f"保存 CSV 文件失败: {e}")
return None