- 创建 plugin_manager.py 模块
- PluginManager: 插件管理主类
- ChromeExtensionHandler: Chrome 插件处理
- BotHandler: 飞书/钉钉/Slack 机器人处理
- WebhookIntegration: Zapier/Make Webhook 集成
- WebDAVSync: WebDAV 同步管理
- 创建完整的 Chrome 扩展代码
- manifest.json, background.js, content.js, content.css
- popup.html/js: 弹出窗口界面
- options.html/js: 设置页面
- 支持网页剪藏、选中文本保存、项目选择
- 更新 schema.sql 添加插件相关数据库表
- plugins: 插件配置表
- bot_sessions: 机器人会话表
- webhook_endpoints: Webhook 端点表
- webdav_syncs: WebDAV 同步配置表
- plugin_activity_logs: 插件活动日志表
- 更新 main.py 添加插件相关 API 端点
- GET/POST /api/v1/plugins - 插件管理
- POST /api/v1/plugins/chrome/clip - Chrome 插件保存网页
- POST /api/v1/bots/webhook/{platform} - 接收机器人消息
- GET /api/v1/bots/sessions - 机器人会话列表
- POST /api/v1/webhook-endpoints - 创建 Webhook 端点
- POST /webhook/{type}/{token} - 接收外部 Webhook
- POST /api/v1/webdav-syncs - WebDAV 同步配置
- POST /api/v1/webdav-syncs/{id}/test - 测试 WebDAV 连接
- POST /api/v1/webdav-syncs/{id}/sync - 触发 WebDAV 同步
- 更新 requirements.txt 添加插件依赖
- beautifulsoup4: HTML 解析
- webdavclient3: WebDAV 客户端
- 更新 STATUS.md 和 README.md 开发进度
1367 lines
45 KiB
Python
1367 lines
45 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
InsightFlow Plugin Manager - Phase 7 Task 7
|
||
插件与集成系统:Chrome插件、飞书/钉钉机器人、Zapier/Make集成、WebDAV同步
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import hashlib
|
||
import hmac
|
||
import base64
|
||
import time
|
||
import uuid
|
||
import httpx
|
||
import asyncio
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Any, Callable
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
import sqlite3
|
||
|
||
# WebDAV 支持
|
||
try:
|
||
import webdav4.client as webdav_client
|
||
WEBDAV_AVAILABLE = True
|
||
except ImportError:
|
||
WEBDAV_AVAILABLE = False
|
||
|
||
|
||
class PluginType(Enum):
|
||
"""插件类型"""
|
||
CHROME_EXTENSION = "chrome_extension"
|
||
FEISHU_BOT = "feishu_bot"
|
||
DINGTALK_BOT = "dingtalk_bot"
|
||
ZAPIER = "zapier"
|
||
MAKE = "make"
|
||
WEBDAV = "webdav"
|
||
CUSTOM = "custom"
|
||
|
||
|
||
class PluginStatus(Enum):
|
||
"""插件状态"""
|
||
ACTIVE = "active"
|
||
INACTIVE = "inactive"
|
||
ERROR = "error"
|
||
PENDING = "pending"
|
||
|
||
|
||
@dataclass
|
||
class Plugin:
|
||
"""插件配置"""
|
||
id: str
|
||
name: str
|
||
plugin_type: str
|
||
project_id: str
|
||
status: str = "active"
|
||
config: Dict = field(default_factory=dict)
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
last_used_at: Optional[str] = None
|
||
use_count: int = 0
|
||
|
||
|
||
@dataclass
|
||
class PluginConfig:
|
||
"""插件详细配置"""
|
||
id: str
|
||
plugin_id: str
|
||
config_key: str
|
||
config_value: str
|
||
is_encrypted: bool = False
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
|
||
|
||
@dataclass
|
||
class BotSession:
|
||
"""机器人会话"""
|
||
id: str
|
||
bot_type: str # feishu, dingtalk
|
||
session_id: str # 群ID或会话ID
|
||
session_name: str
|
||
project_id: Optional[str] = None
|
||
webhook_url: str = ""
|
||
secret: str = ""
|
||
is_active: bool = True
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
last_message_at: Optional[str] = None
|
||
message_count: int = 0
|
||
|
||
|
||
@dataclass
|
||
class WebhookEndpoint:
|
||
"""Webhook 端点配置(Zapier/Make集成)"""
|
||
id: str
|
||
name: str
|
||
endpoint_type: str # zapier, make, custom
|
||
endpoint_url: str
|
||
project_id: Optional[str] = None
|
||
auth_type: str = "none" # none, api_key, oauth, custom
|
||
auth_config: Dict = field(default_factory=dict)
|
||
trigger_events: List[str] = field(default_factory=list)
|
||
is_active: bool = True
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
last_triggered_at: Optional[str] = None
|
||
trigger_count: int = 0
|
||
|
||
|
||
@dataclass
|
||
class WebDAVSync:
|
||
"""WebDAV 同步配置"""
|
||
id: str
|
||
name: str
|
||
project_id: str
|
||
server_url: str
|
||
username: str
|
||
password: str = "" # 加密存储
|
||
remote_path: str = "/insightflow"
|
||
sync_mode: str = "bidirectional" # bidirectional, upload_only, download_only
|
||
sync_interval: int = 3600 # 秒
|
||
last_sync_at: Optional[str] = None
|
||
last_sync_status: str = "pending" # pending, success, failed
|
||
last_sync_error: str = ""
|
||
is_active: bool = True
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
sync_count: int = 0
|
||
|
||
|
||
@dataclass
|
||
class ChromeExtensionToken:
|
||
"""Chrome 扩展令牌"""
|
||
id: str
|
||
token: str
|
||
user_id: Optional[str] = None
|
||
project_id: Optional[str] = None
|
||
name: str = ""
|
||
permissions: List[str] = field(default_factory=lambda: ["read", "write"])
|
||
expires_at: Optional[str] = None
|
||
created_at: str = ""
|
||
last_used_at: Optional[str] = None
|
||
use_count: int = 0
|
||
is_revoked: bool = False
|
||
|
||
|
||
class PluginManager:
|
||
"""插件管理主类"""
|
||
|
||
def __init__(self, db_manager=None):
|
||
self.db = db_manager
|
||
self._handlers = {}
|
||
self._register_default_handlers()
|
||
|
||
def _register_default_handlers(self):
|
||
"""注册默认处理器"""
|
||
self._handlers[PluginType.CHROME_EXTENSION] = ChromeExtensionHandler(self)
|
||
self._handlers[PluginType.FEISHU_BOT] = BotHandler(self, "feishu")
|
||
self._handlers[PluginType.DINGTALK_BOT] = BotHandler(self, "dingtalk")
|
||
self._handlers[PluginType.ZAPIER] = WebhookIntegration(self, "zapier")
|
||
self._handlers[PluginType.MAKE] = WebhookIntegration(self, "make")
|
||
self._handlers[PluginType.WEBDAV] = WebDAVSyncManager(self)
|
||
|
||
def get_handler(self, plugin_type: PluginType) -> Optional[Any]:
|
||
"""获取插件处理器"""
|
||
return self._handlers.get(plugin_type)
|
||
|
||
# ==================== Plugin CRUD ====================
|
||
|
||
def create_plugin(self, plugin: Plugin) -> Plugin:
|
||
"""创建插件"""
|
||
conn = self.db.get_conn()
|
||
now = datetime.now().isoformat()
|
||
|
||
conn.execute(
|
||
"""INSERT INTO plugins
|
||
(id, name, plugin_type, project_id, status, config, created_at, updated_at, use_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(plugin.id, plugin.name, plugin.plugin_type, plugin.project_id,
|
||
plugin.status, json.dumps(plugin.config), now, now, 0)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
plugin.created_at = now
|
||
plugin.updated_at = now
|
||
return plugin
|
||
|
||
def get_plugin(self, plugin_id: str) -> Optional[Plugin]:
|
||
"""获取插件"""
|
||
conn = self.db.get_conn()
|
||
row = conn.execute(
|
||
"SELECT * FROM plugins WHERE id = ?", (plugin_id,)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if row:
|
||
return self._row_to_plugin(row)
|
||
return None
|
||
|
||
def list_plugins(self, project_id: str = None, plugin_type: str = None,
|
||
status: str = None) -> List[Plugin]:
|
||
"""列出插件"""
|
||
conn = self.db.get_conn()
|
||
|
||
conditions = []
|
||
params = []
|
||
|
||
if project_id:
|
||
conditions.append("project_id = ?")
|
||
params.append(project_id)
|
||
if plugin_type:
|
||
conditions.append("plugin_type = ?")
|
||
params.append(plugin_type)
|
||
if status:
|
||
conditions.append("status = ?")
|
||
params.append(status)
|
||
|
||
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||
|
||
rows = conn.execute(
|
||
f"SELECT * FROM plugins WHERE {where_clause} ORDER BY created_at DESC",
|
||
params
|
||
).fetchall()
|
||
conn.close()
|
||
|
||
return [self._row_to_plugin(row) for row in rows]
|
||
|
||
def update_plugin(self, plugin_id: str, **kwargs) -> Optional[Plugin]:
|
||
"""更新插件"""
|
||
conn = self.db.get_conn()
|
||
|
||
allowed_fields = ['name', 'status', 'config']
|
||
updates = []
|
||
values = []
|
||
|
||
for field in allowed_fields:
|
||
if field in kwargs:
|
||
updates.append(f"{field} = ?")
|
||
if field == 'config':
|
||
values.append(json.dumps(kwargs[field]))
|
||
else:
|
||
values.append(kwargs[field])
|
||
|
||
if not updates:
|
||
conn.close()
|
||
return self.get_plugin(plugin_id)
|
||
|
||
updates.append("updated_at = ?")
|
||
values.append(datetime.now().isoformat())
|
||
values.append(plugin_id)
|
||
|
||
query = f"UPDATE plugins SET {', '.join(updates)} WHERE id = ?"
|
||
conn.execute(query, values)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return self.get_plugin(plugin_id)
|
||
|
||
def delete_plugin(self, plugin_id: str) -> bool:
|
||
"""删除插件"""
|
||
conn = self.db.get_conn()
|
||
|
||
# 删除关联的配置
|
||
conn.execute("DELETE FROM plugin_configs WHERE plugin_id = ?", (plugin_id,))
|
||
|
||
# 删除插件
|
||
cursor = conn.execute("DELETE FROM plugins WHERE id = ?", (plugin_id,))
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def _row_to_plugin(self, row: sqlite3.Row) -> Plugin:
|
||
"""将数据库行转换为 Plugin 对象"""
|
||
return Plugin(
|
||
id=row['id'],
|
||
name=row['name'],
|
||
plugin_type=row['plugin_type'],
|
||
project_id=row['project_id'],
|
||
status=row['status'],
|
||
config=json.loads(row['config']) if row['config'] else {},
|
||
created_at=row['created_at'],
|
||
updated_at=row['updated_at'],
|
||
last_used_at=row['last_used_at'],
|
||
use_count=row['use_count']
|
||
)
|
||
|
||
# ==================== Plugin Config ====================
|
||
|
||
def set_plugin_config(self, plugin_id: str, key: str, value: str,
|
||
is_encrypted: bool = False) -> PluginConfig:
|
||
"""设置插件配置"""
|
||
conn = self.db.get_conn()
|
||
now = datetime.now().isoformat()
|
||
|
||
# 检查是否已存在
|
||
existing = conn.execute(
|
||
"SELECT id FROM plugin_configs WHERE plugin_id = ? AND config_key = ?",
|
||
(plugin_id, key)
|
||
).fetchone()
|
||
|
||
if existing:
|
||
conn.execute(
|
||
"""UPDATE plugin_configs
|
||
SET config_value = ?, is_encrypted = ?, updated_at = ?
|
||
WHERE id = ?""",
|
||
(value, is_encrypted, now, existing['id'])
|
||
)
|
||
config_id = existing['id']
|
||
else:
|
||
config_id = str(uuid.uuid4())[:8]
|
||
conn.execute(
|
||
"""INSERT INTO plugin_configs
|
||
(id, plugin_id, config_key, config_value, is_encrypted, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||
(config_id, plugin_id, key, value, is_encrypted, now, now)
|
||
)
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return PluginConfig(
|
||
id=config_id,
|
||
plugin_id=plugin_id,
|
||
config_key=key,
|
||
config_value=value,
|
||
is_encrypted=is_encrypted,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
def get_plugin_config(self, plugin_id: str, key: str) -> Optional[str]:
|
||
"""获取插件配置"""
|
||
conn = self.db.get_conn()
|
||
row = conn.execute(
|
||
"SELECT config_value FROM plugin_configs WHERE plugin_id = ? AND config_key = ?",
|
||
(plugin_id, key)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
return row['config_value'] if row else None
|
||
|
||
def get_all_plugin_configs(self, plugin_id: str) -> Dict[str, str]:
|
||
"""获取插件所有配置"""
|
||
conn = self.db.get_conn()
|
||
rows = conn.execute(
|
||
"SELECT config_key, config_value FROM plugin_configs WHERE plugin_id = ?",
|
||
(plugin_id,)
|
||
).fetchall()
|
||
conn.close()
|
||
|
||
return {row['config_key']: row['config_value'] for row in rows}
|
||
|
||
def delete_plugin_config(self, plugin_id: str, key: str) -> bool:
|
||
"""删除插件配置"""
|
||
conn = self.db.get_conn()
|
||
cursor = conn.execute(
|
||
"DELETE FROM plugin_configs WHERE plugin_id = ? AND config_key = ?",
|
||
(plugin_id, key)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def record_plugin_usage(self, plugin_id: str):
|
||
"""记录插件使用"""
|
||
conn = self.db.get_conn()
|
||
now = datetime.now().isoformat()
|
||
|
||
conn.execute(
|
||
"""UPDATE plugins
|
||
SET use_count = use_count + 1, last_used_at = ?
|
||
WHERE id = ?""",
|
||
(now, plugin_id)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
|
||
class ChromeExtensionHandler:
|
||
"""Chrome 扩展处理器"""
|
||
|
||
def __init__(self, plugin_manager: PluginManager):
|
||
self.pm = plugin_manager
|
||
|
||
def create_token(self, name: str, user_id: str = None, project_id: str = None,
|
||
permissions: List[str] = None, expires_days: int = None) -> ChromeExtensionToken:
|
||
"""创建 Chrome 扩展令牌"""
|
||
token_id = str(uuid.uuid4())[:8]
|
||
|
||
# 生成随机令牌
|
||
raw_token = f"if_ext_{base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip('=')}"
|
||
|
||
# 哈希存储
|
||
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
|
||
|
||
now = datetime.now().isoformat()
|
||
expires_at = None
|
||
if expires_days:
|
||
from datetime import timedelta
|
||
expires_at = (datetime.now() + timedelta(days=expires_days)).isoformat()
|
||
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""INSERT INTO chrome_extension_tokens
|
||
(id, token_hash, user_id, project_id, name, permissions, expires_at,
|
||
created_at, is_revoked, use_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(token_id, token_hash, user_id, project_id, name,
|
||
json.dumps(permissions or ["read"]), expires_at, now, False, 0)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return ChromeExtensionToken(
|
||
id=token_id,
|
||
token=raw_token, # 仅返回一次
|
||
user_id=user_id,
|
||
project_id=project_id,
|
||
name=name,
|
||
permissions=permissions or ["read"],
|
||
expires_at=expires_at,
|
||
created_at=now
|
||
)
|
||
|
||
def validate_token(self, token: str) -> Optional[ChromeExtensionToken]:
|
||
"""验证 Chrome 扩展令牌"""
|
||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||
|
||
conn = self.pm.db.get_conn()
|
||
row = conn.execute(
|
||
"""SELECT * FROM chrome_extension_tokens
|
||
WHERE token_hash = ? AND is_revoked = 0""",
|
||
(token_hash,)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if not row:
|
||
return None
|
||
|
||
# 检查是否过期
|
||
if row['expires_at'] and datetime.now().isoformat() > row['expires_at']:
|
||
return None
|
||
|
||
# 更新使用记录
|
||
now = datetime.now().isoformat()
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""UPDATE chrome_extension_tokens
|
||
SET use_count = use_count + 1, last_used_at = ?
|
||
WHERE id = ?""",
|
||
(now, row['id'])
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return ChromeExtensionToken(
|
||
id=row['id'],
|
||
token="", # 不返回实际令牌
|
||
user_id=row['user_id'],
|
||
project_id=row['project_id'],
|
||
name=row['name'],
|
||
permissions=json.loads(row['permissions']),
|
||
expires_at=row['expires_at'],
|
||
created_at=row['created_at'],
|
||
last_used_at=now,
|
||
use_count=row['use_count'] + 1
|
||
)
|
||
|
||
def revoke_token(self, token_id: str) -> bool:
|
||
"""撤销令牌"""
|
||
conn = self.pm.db.get_conn()
|
||
cursor = conn.execute(
|
||
"UPDATE chrome_extension_tokens SET is_revoked = 1 WHERE id = ?",
|
||
(token_id,)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def list_tokens(self, user_id: str = None, project_id: str = None) -> List[ChromeExtensionToken]:
|
||
"""列出令牌"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
conditions = ["is_revoked = 0"]
|
||
params = []
|
||
|
||
if user_id:
|
||
conditions.append("user_id = ?")
|
||
params.append(user_id)
|
||
if project_id:
|
||
conditions.append("project_id = ?")
|
||
params.append(project_id)
|
||
|
||
where_clause = " AND ".join(conditions)
|
||
|
||
rows = conn.execute(
|
||
f"SELECT * FROM chrome_extension_tokens WHERE {where_clause} ORDER BY created_at DESC",
|
||
params
|
||
).fetchall()
|
||
conn.close()
|
||
|
||
tokens = []
|
||
for row in rows:
|
||
tokens.append(ChromeExtensionToken(
|
||
id=row['id'],
|
||
token="", # 不返回实际令牌
|
||
user_id=row['user_id'],
|
||
project_id=row['project_id'],
|
||
name=row['name'],
|
||
permissions=json.loads(row['permissions']),
|
||
expires_at=row['expires_at'],
|
||
created_at=row['created_at'],
|
||
last_used_at=row['last_used_at'],
|
||
use_count=row['use_count'],
|
||
is_revoked=bool(row['is_revoked'])
|
||
))
|
||
|
||
return tokens
|
||
|
||
async def import_webpage(self, token: ChromeExtensionToken, url: str, title: str,
|
||
content: str, html_content: str = None) -> Dict:
|
||
"""导入网页内容"""
|
||
if not token.project_id:
|
||
return {"success": False, "error": "Token not associated with any project"}
|
||
|
||
if "write" not in token.permissions:
|
||
return {"success": False, "error": "Insufficient permissions"}
|
||
|
||
# 创建转录记录(将网页作为文档处理)
|
||
transcript_id = str(uuid.uuid4())[:8]
|
||
now = datetime.now().isoformat()
|
||
|
||
# 构建完整文本
|
||
full_text = f"# {title}\n\nURL: {url}\n\n{content}"
|
||
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""INSERT INTO transcripts
|
||
(id, project_id, filename, full_text, type, created_at)
|
||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||
(transcript_id, token.project_id, f"web_{title[:50]}.md", full_text, "webpage", now)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return {
|
||
"success": True,
|
||
"transcript_id": transcript_id,
|
||
"project_id": token.project_id,
|
||
"url": url,
|
||
"title": title,
|
||
"content_length": len(content)
|
||
}
|
||
|
||
|
||
class BotHandler:
|
||
"""飞书/钉钉机器人处理器"""
|
||
|
||
def __init__(self, plugin_manager: PluginManager, bot_type: str):
|
||
self.pm = plugin_manager
|
||
self.bot_type = bot_type
|
||
|
||
def create_session(self, session_id: str, session_name: str,
|
||
project_id: str = None, webhook_url: str = "",
|
||
secret: str = "") -> BotSession:
|
||
"""创建机器人会话"""
|
||
bot_id = str(uuid.uuid4())[:8]
|
||
now = datetime.now().isoformat()
|
||
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""INSERT INTO bot_sessions
|
||
(id, bot_type, session_id, session_name, project_id, webhook_url, secret,
|
||
is_active, created_at, updated_at, message_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(bot_id, self.bot_type, session_id, session_name, project_id, webhook_url, secret,
|
||
True, now, now, 0)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return BotSession(
|
||
id=bot_id,
|
||
bot_type=self.bot_type,
|
||
session_id=session_id,
|
||
session_name=session_name,
|
||
project_id=project_id,
|
||
webhook_url=webhook_url,
|
||
secret=secret,
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
def get_session(self, session_id: str) -> Optional[BotSession]:
|
||
"""获取会话"""
|
||
conn = self.pm.db.get_conn()
|
||
row = conn.execute(
|
||
"""SELECT * FROM bot_sessions
|
||
WHERE session_id = ? AND bot_type = ?""",
|
||
(session_id, self.bot_type)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if row:
|
||
return self._row_to_session(row)
|
||
return None
|
||
|
||
def list_sessions(self, project_id: str = None) -> List[BotSession]:
|
||
"""列出会话"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
if project_id:
|
||
rows = conn.execute(
|
||
"""SELECT * FROM bot_sessions
|
||
WHERE bot_type = ? AND project_id = ? ORDER BY created_at DESC""",
|
||
(self.bot_type, project_id)
|
||
).fetchall()
|
||
else:
|
||
rows = conn.execute(
|
||
"""SELECT * FROM bot_sessions
|
||
WHERE bot_type = ? ORDER BY created_at DESC""",
|
||
(self.bot_type,)
|
||
).fetchall()
|
||
|
||
conn.close()
|
||
|
||
return [self._row_to_session(row) for row in rows]
|
||
|
||
def update_session(self, session_id: str, **kwargs) -> Optional[BotSession]:
|
||
"""更新会话"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
allowed_fields = ['session_name', 'project_id', 'webhook_url', 'secret', 'is_active']
|
||
updates = []
|
||
values = []
|
||
|
||
for field in allowed_fields:
|
||
if field in kwargs:
|
||
updates.append(f"{field} = ?")
|
||
values.append(kwargs[field])
|
||
|
||
if not updates:
|
||
conn.close()
|
||
return self.get_session(session_id)
|
||
|
||
updates.append("updated_at = ?")
|
||
values.append(datetime.now().isoformat())
|
||
values.append(session_id)
|
||
values.append(self.bot_type)
|
||
|
||
query = f"UPDATE bot_sessions SET {', '.join(updates)} WHERE session_id = ? AND bot_type = ?"
|
||
conn.execute(query, values)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return self.get_session(session_id)
|
||
|
||
def delete_session(self, session_id: str) -> bool:
|
||
"""删除会话"""
|
||
conn = self.pm.db.get_conn()
|
||
cursor = conn.execute(
|
||
"DELETE FROM bot_sessions WHERE session_id = ? AND bot_type = ?",
|
||
(session_id, self.bot_type)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def _row_to_session(self, row: sqlite3.Row) -> BotSession:
|
||
"""将数据库行转换为 BotSession 对象"""
|
||
return BotSession(
|
||
id=row['id'],
|
||
bot_type=row['bot_type'],
|
||
session_id=row['session_id'],
|
||
session_name=row['session_name'],
|
||
project_id=row['project_id'],
|
||
webhook_url=row['webhook_url'],
|
||
secret=row['secret'],
|
||
is_active=bool(row['is_active']),
|
||
created_at=row['created_at'],
|
||
updated_at=row['updated_at'],
|
||
last_message_at=row['last_message_at'],
|
||
message_count=row['message_count']
|
||
)
|
||
|
||
async def handle_message(self, session: BotSession, message: Dict) -> Dict:
|
||
"""处理收到的消息"""
|
||
now = datetime.now().isoformat()
|
||
|
||
# 更新消息统计
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""UPDATE bot_sessions
|
||
SET message_count = message_count + 1, last_message_at = ?
|
||
WHERE id = ?""",
|
||
(now, session.id)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
# 处理消息
|
||
msg_type = message.get('msg_type', 'text')
|
||
content = message.get('content', {})
|
||
|
||
if msg_type == 'text':
|
||
text = content.get('text', '')
|
||
return await self._handle_text_message(session, text, message)
|
||
elif msg_type == 'audio':
|
||
# 处理音频消息
|
||
return await self._handle_audio_message(session, message)
|
||
elif msg_type == 'file':
|
||
# 处理文件消息
|
||
return await self._handle_file_message(session, message)
|
||
|
||
return {"success": False, "error": "Unsupported message type"}
|
||
|
||
async def _handle_text_message(self, session: BotSession, text: str,
|
||
raw_message: Dict) -> Dict:
|
||
"""处理文本消息"""
|
||
# 简单命令处理
|
||
if text.startswith('/help'):
|
||
return {
|
||
"success": True,
|
||
"response": """🤖 InsightFlow 机器人命令:
|
||
/help - 显示帮助
|
||
/status - 查看项目状态
|
||
/analyze <URL> - 分析网页内容
|
||
/search <关键词> - 搜索知识库"""
|
||
}
|
||
|
||
if text.startswith('/status'):
|
||
if not session.project_id:
|
||
return {"success": True, "response": "⚠️ 当前会话未绑定项目"}
|
||
|
||
# 获取项目状态
|
||
summary = self.pm.db.get_project_summary(session.project_id)
|
||
stats = summary.get('statistics', {})
|
||
|
||
return {
|
||
"success": True,
|
||
"response": f"""📊 项目状态:
|
||
实体数量: {stats.get('entity_count', 0)}
|
||
关系数量: {stats.get('relation_count', 0)}
|
||
转录数量: {stats.get('transcript_count', 0)}"""
|
||
}
|
||
|
||
# 默认回复
|
||
return {
|
||
"success": True,
|
||
"response": f"收到消息:{text[:100]}...\n\n使用 /help 查看可用命令"
|
||
}
|
||
|
||
async def _handle_audio_message(self, session: BotSession, message: Dict) -> Dict:
|
||
"""处理音频消息"""
|
||
if not session.project_id:
|
||
return {"success": False, "error": "Session not bound to any project"}
|
||
|
||
# 下载音频文件
|
||
audio_url = message.get('content', {}).get('download_url')
|
||
if not audio_url:
|
||
return {"success": False, "error": "No audio URL provided"}
|
||
|
||
try:
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(audio_url)
|
||
audio_data = response.content
|
||
|
||
# 保存音频文件
|
||
filename = f"bot_audio_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp3"
|
||
|
||
# 这里应该调用 ASR 服务进行转录
|
||
# 简化处理,返回提示
|
||
return {
|
||
"success": True,
|
||
"response": "🎵 收到音频文件,正在处理中...\n分析完成后会通知您。",
|
||
"audio_size": len(audio_data),
|
||
"filename": filename
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": f"Failed to process audio: {str(e)}"}
|
||
|
||
async def _handle_file_message(self, session: BotSession, message: Dict) -> Dict:
|
||
"""处理文件消息"""
|
||
return {
|
||
"success": True,
|
||
"response": "📎 收到文件,正在处理中..."
|
||
}
|
||
|
||
async def send_message(self, session: BotSession, message: str,
|
||
msg_type: str = "text") -> bool:
|
||
"""发送消息到群聊"""
|
||
if not session.webhook_url:
|
||
return False
|
||
|
||
try:
|
||
if self.bot_type == "feishu":
|
||
return await self._send_feishu_message(session, message, msg_type)
|
||
elif self.bot_type == "dingtalk":
|
||
return await self._send_dingtalk_message(session, message, msg_type)
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"Failed to send {self.bot_type} message: {e}")
|
||
return False
|
||
|
||
async def _send_feishu_message(self, session: BotSession, message: str,
|
||
msg_type: str) -> bool:
|
||
"""发送飞书消息"""
|
||
import hashlib
|
||
import base64
|
||
|
||
timestamp = str(int(time.time()))
|
||
|
||
# 生成签名
|
||
if session.secret:
|
||
string_to_sign = f"{timestamp}\n{session.secret}"
|
||
hmac_code = hmac.new(
|
||
session.secret.encode('utf-8'),
|
||
string_to_sign.encode('utf-8'),
|
||
digestmod=hashlib.sha256
|
||
).digest()
|
||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||
else:
|
||
sign = ""
|
||
|
||
payload = {
|
||
"timestamp": timestamp,
|
||
"sign": sign,
|
||
"msg_type": "text",
|
||
"content": {
|
||
"text": message
|
||
}
|
||
}
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
session.webhook_url,
|
||
json=payload,
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
return response.status_code == 200
|
||
|
||
async def _send_dingtalk_message(self, session: BotSession, message: str,
|
||
msg_type: str) -> bool:
|
||
"""发送钉钉消息"""
|
||
import hashlib
|
||
import base64
|
||
|
||
timestamp = str(round(time.time() * 1000))
|
||
|
||
# 生成签名
|
||
if session.secret:
|
||
string_to_sign = f"{timestamp}\n{session.secret}"
|
||
hmac_code = hmac.new(
|
||
session.secret.encode('utf-8'),
|
||
string_to_sign.encode('utf-8'),
|
||
digestmod=hashlib.sha256
|
||
).digest()
|
||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||
sign = urllib.parse.quote(sign)
|
||
else:
|
||
sign = ""
|
||
|
||
payload = {
|
||
"msgtype": "text",
|
||
"text": {
|
||
"content": message
|
||
}
|
||
}
|
||
|
||
url = session.webhook_url
|
||
if sign:
|
||
url = f"{url}×tamp={timestamp}&sign={sign}"
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
url,
|
||
json=payload,
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
return response.status_code == 200
|
||
|
||
|
||
class WebhookIntegration:
|
||
"""Zapier/Make Webhook 集成"""
|
||
|
||
def __init__(self, plugin_manager: PluginManager, endpoint_type: str):
|
||
self.pm = plugin_manager
|
||
self.endpoint_type = endpoint_type
|
||
|
||
def create_endpoint(self, name: str, endpoint_url: str,
|
||
project_id: str = None, auth_type: str = "none",
|
||
auth_config: Dict = None,
|
||
trigger_events: List[str] = None) -> WebhookEndpoint:
|
||
"""创建 Webhook 端点"""
|
||
endpoint_id = str(uuid.uuid4())[:8]
|
||
now = datetime.now().isoformat()
|
||
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""INSERT INTO webhook_endpoints
|
||
(id, name, endpoint_type, endpoint_url, project_id, auth_type, auth_config,
|
||
trigger_events, is_active, created_at, updated_at, trigger_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(endpoint_id, name, self.endpoint_type, endpoint_url, project_id, auth_type,
|
||
json.dumps(auth_config or {}), json.dumps(trigger_events or []), True,
|
||
now, now, 0)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return WebhookEndpoint(
|
||
id=endpoint_id,
|
||
name=name,
|
||
endpoint_type=self.endpoint_type,
|
||
endpoint_url=endpoint_url,
|
||
project_id=project_id,
|
||
auth_type=auth_type,
|
||
auth_config=auth_config or {},
|
||
trigger_events=trigger_events or [],
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
def get_endpoint(self, endpoint_id: str) -> Optional[WebhookEndpoint]:
|
||
"""获取端点"""
|
||
conn = self.pm.db.get_conn()
|
||
row = conn.execute(
|
||
"SELECT * FROM webhook_endpoints WHERE id = ? AND endpoint_type = ?",
|
||
(endpoint_id, self.endpoint_type)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if row:
|
||
return self._row_to_endpoint(row)
|
||
return None
|
||
|
||
def list_endpoints(self, project_id: str = None) -> List[WebhookEndpoint]:
|
||
"""列出端点"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
if project_id:
|
||
rows = conn.execute(
|
||
"""SELECT * FROM webhook_endpoints
|
||
WHERE endpoint_type = ? AND project_id = ? ORDER BY created_at DESC""",
|
||
(self.endpoint_type, project_id)
|
||
).fetchall()
|
||
else:
|
||
rows = conn.execute(
|
||
"""SELECT * FROM webhook_endpoints
|
||
WHERE endpoint_type = ? ORDER BY created_at DESC""",
|
||
(self.endpoint_type,)
|
||
).fetchall()
|
||
|
||
conn.close()
|
||
|
||
return [self._row_to_endpoint(row) for row in rows]
|
||
|
||
def update_endpoint(self, endpoint_id: str, **kwargs) -> Optional[WebhookEndpoint]:
|
||
"""更新端点"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
allowed_fields = ['name', 'endpoint_url', 'project_id', 'auth_type',
|
||
'auth_config', 'trigger_events', 'is_active']
|
||
updates = []
|
||
values = []
|
||
|
||
for field in allowed_fields:
|
||
if field in kwargs:
|
||
updates.append(f"{field} = ?")
|
||
if field in ['auth_config', 'trigger_events']:
|
||
values.append(json.dumps(kwargs[field]))
|
||
else:
|
||
values.append(kwargs[field])
|
||
|
||
if not updates:
|
||
conn.close()
|
||
return self.get_endpoint(endpoint_id)
|
||
|
||
updates.append("updated_at = ?")
|
||
values.append(datetime.now().isoformat())
|
||
values.append(endpoint_id)
|
||
|
||
query = f"UPDATE webhook_endpoints SET {', '.join(updates)} WHERE id = ?"
|
||
conn.execute(query, values)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return self.get_endpoint(endpoint_id)
|
||
|
||
def delete_endpoint(self, endpoint_id: str) -> bool:
|
||
"""删除端点"""
|
||
conn = self.pm.db.get_conn()
|
||
cursor = conn.execute(
|
||
"DELETE FROM webhook_endpoints WHERE id = ?",
|
||
(endpoint_id,)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def _row_to_endpoint(self, row: sqlite3.Row) -> WebhookEndpoint:
|
||
"""将数据库行转换为 WebhookEndpoint 对象"""
|
||
return WebhookEndpoint(
|
||
id=row['id'],
|
||
name=row['name'],
|
||
endpoint_type=row['endpoint_type'],
|
||
endpoint_url=row['endpoint_url'],
|
||
project_id=row['project_id'],
|
||
auth_type=row['auth_type'],
|
||
auth_config=json.loads(row['auth_config']) if row['auth_config'] else {},
|
||
trigger_events=json.loads(row['trigger_events']) if row['trigger_events'] else [],
|
||
is_active=bool(row['is_active']),
|
||
created_at=row['created_at'],
|
||
updated_at=row['updated_at'],
|
||
last_triggered_at=row['last_triggered_at'],
|
||
trigger_count=row['trigger_count']
|
||
)
|
||
|
||
async def trigger(self, endpoint: WebhookEndpoint, event_type: str,
|
||
data: Dict) -> bool:
|
||
"""触发 Webhook"""
|
||
if not endpoint.is_active:
|
||
return False
|
||
|
||
if event_type not in endpoint.trigger_events:
|
||
return False
|
||
|
||
try:
|
||
headers = {"Content-Type": "application/json"}
|
||
|
||
# 添加认证头
|
||
if endpoint.auth_type == "api_key":
|
||
api_key = endpoint.auth_config.get('api_key', '')
|
||
header_name = endpoint.auth_config.get('header_name', 'X-API-Key')
|
||
headers[header_name] = api_key
|
||
elif endpoint.auth_type == "bearer":
|
||
token = endpoint.auth_config.get('token', '')
|
||
headers["Authorization"] = f"Bearer {token}"
|
||
|
||
payload = {
|
||
"event": event_type,
|
||
"timestamp": datetime.now().isoformat(),
|
||
"data": data
|
||
}
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
endpoint.endpoint_url,
|
||
json=payload,
|
||
headers=headers,
|
||
timeout=30.0
|
||
)
|
||
|
||
success = response.status_code in [200, 201, 202]
|
||
|
||
# 更新触发统计
|
||
now = datetime.now().isoformat()
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""UPDATE webhook_endpoints
|
||
SET trigger_count = trigger_count + 1, last_triggered_at = ?
|
||
WHERE id = ?""",
|
||
(now, endpoint.id)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return success
|
||
|
||
except Exception as e:
|
||
print(f"Failed to trigger webhook: {e}")
|
||
return False
|
||
|
||
async def test_endpoint(self, endpoint: WebhookEndpoint) -> Dict:
|
||
"""测试端点"""
|
||
test_data = {
|
||
"message": "This is a test event from InsightFlow",
|
||
"test": True,
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
|
||
success = await self.trigger(endpoint, "test", test_data)
|
||
|
||
return {
|
||
"success": success,
|
||
"endpoint_id": endpoint.id,
|
||
"endpoint_url": endpoint.endpoint_url,
|
||
"message": "Test event sent successfully" if success else "Failed to send test event"
|
||
}
|
||
|
||
|
||
class WebDAVSyncManager:
|
||
"""WebDAV 同步管理"""
|
||
|
||
def __init__(self, plugin_manager: PluginManager):
|
||
self.pm = plugin_manager
|
||
|
||
def create_sync(self, name: str, project_id: str, server_url: str,
|
||
username: str, password: str, remote_path: str = "/insightflow",
|
||
sync_mode: str = "bidirectional",
|
||
sync_interval: int = 3600) -> WebDAVSync:
|
||
"""创建 WebDAV 同步配置"""
|
||
sync_id = str(uuid.uuid4())[:8]
|
||
now = datetime.now().isoformat()
|
||
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""INSERT INTO webdav_syncs
|
||
(id, name, project_id, server_url, username, password, remote_path,
|
||
sync_mode, sync_interval, last_sync_status, is_active, created_at, updated_at, sync_count)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(sync_id, name, project_id, server_url, username, password, remote_path,
|
||
sync_mode, sync_interval, 'pending', True, now, now, 0)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return WebDAVSync(
|
||
id=sync_id,
|
||
name=name,
|
||
project_id=project_id,
|
||
server_url=server_url,
|
||
username=username,
|
||
password=password,
|
||
remote_path=remote_path,
|
||
sync_mode=sync_mode,
|
||
sync_interval=sync_interval,
|
||
last_sync_status='pending',
|
||
is_active=True,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
def get_sync(self, sync_id: str) -> Optional[WebDAVSync]:
|
||
"""获取同步配置"""
|
||
conn = self.pm.db.get_conn()
|
||
row = conn.execute(
|
||
"SELECT * FROM webdav_syncs WHERE id = ?",
|
||
(sync_id,)
|
||
).fetchone()
|
||
conn.close()
|
||
|
||
if row:
|
||
return self._row_to_sync(row)
|
||
return None
|
||
|
||
def list_syncs(self, project_id: str = None) -> List[WebDAVSync]:
|
||
"""列出同步配置"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
if project_id:
|
||
rows = conn.execute(
|
||
"SELECT * FROM webdav_syncs WHERE project_id = ? ORDER BY created_at DESC",
|
||
(project_id,)
|
||
).fetchall()
|
||
else:
|
||
rows = conn.execute(
|
||
"SELECT * FROM webdav_syncs ORDER BY created_at DESC"
|
||
).fetchall()
|
||
|
||
conn.close()
|
||
|
||
return [self._row_to_sync(row) for row in rows]
|
||
|
||
def update_sync(self, sync_id: str, **kwargs) -> Optional[WebDAVSync]:
|
||
"""更新同步配置"""
|
||
conn = self.pm.db.get_conn()
|
||
|
||
allowed_fields = ['name', 'server_url', 'username', 'password',
|
||
'remote_path', 'sync_mode', 'sync_interval', 'is_active']
|
||
updates = []
|
||
values = []
|
||
|
||
for field in allowed_fields:
|
||
if field in kwargs:
|
||
updates.append(f"{field} = ?")
|
||
values.append(kwargs[field])
|
||
|
||
if not updates:
|
||
conn.close()
|
||
return self.get_sync(sync_id)
|
||
|
||
updates.append("updated_at = ?")
|
||
values.append(datetime.now().isoformat())
|
||
values.append(sync_id)
|
||
|
||
query = f"UPDATE webdav_syncs SET {', '.join(updates)} WHERE id = ?"
|
||
conn.execute(query, values)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return self.get_sync(sync_id)
|
||
|
||
def delete_sync(self, sync_id: str) -> bool:
|
||
"""删除同步配置"""
|
||
conn = self.pm.db.get_conn()
|
||
cursor = conn.execute(
|
||
"DELETE FROM webdav_syncs WHERE id = ?",
|
||
(sync_id,)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return cursor.rowcount > 0
|
||
|
||
def _row_to_sync(self, row: sqlite3.Row) -> WebDAVSync:
|
||
"""将数据库行转换为 WebDAVSync 对象"""
|
||
return WebDAVSync(
|
||
id=row['id'],
|
||
name=row['name'],
|
||
project_id=row['project_id'],
|
||
server_url=row['server_url'],
|
||
username=row['username'],
|
||
password=row['password'],
|
||
remote_path=row['remote_path'],
|
||
sync_mode=row['sync_mode'],
|
||
sync_interval=row['sync_interval'],
|
||
last_sync_at=row['last_sync_at'],
|
||
last_sync_status=row['last_sync_status'],
|
||
last_sync_error=row['last_sync_error'] or "",
|
||
is_active=bool(row['is_active']),
|
||
created_at=row['created_at'],
|
||
updated_at=row['updated_at'],
|
||
sync_count=row['sync_count']
|
||
)
|
||
|
||
async def test_connection(self, sync: WebDAVSync) -> Dict:
|
||
"""测试 WebDAV 连接"""
|
||
if not WEBDAV_AVAILABLE:
|
||
return {"success": False, "error": "WebDAV library not available"}
|
||
|
||
try:
|
||
client = webdav_client.Client(
|
||
sync.server_url,
|
||
auth=(sync.username, sync.password)
|
||
)
|
||
|
||
# 尝试列出根目录
|
||
client.list("/")
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Connection successful"
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e)
|
||
}
|
||
|
||
async def sync_project(self, sync: WebDAVSync) -> Dict:
|
||
"""同步项目到 WebDAV"""
|
||
if not WEBDAV_AVAILABLE:
|
||
return {"success": False, "error": "WebDAV library not available"}
|
||
|
||
if not sync.is_active:
|
||
return {"success": False, "error": "Sync is not active"}
|
||
|
||
try:
|
||
client = webdav_client.Client(
|
||
sync.server_url,
|
||
auth=(sync.username, sync.password)
|
||
)
|
||
|
||
# 确保远程目录存在
|
||
remote_project_path = f"{sync.remote_path}/{sync.project_id}"
|
||
try:
|
||
client.mkdir(remote_project_path)
|
||
except:
|
||
pass # 目录可能已存在
|
||
|
||
# 获取项目数据
|
||
project = self.pm.db.get_project(sync.project_id)
|
||
if not project:
|
||
return {"success": False, "error": "Project not found"}
|
||
|
||
# 导出项目数据为 JSON
|
||
entities = self.pm.db.list_project_entities(sync.project_id)
|
||
relations = self.pm.db.list_project_relations(sync.project_id)
|
||
transcripts = self.pm.db.list_project_transcripts(sync.project_id)
|
||
|
||
export_data = {
|
||
"project": {
|
||
"id": project.id,
|
||
"name": project.name,
|
||
"description": project.description
|
||
},
|
||
"entities": [{"id": e.id, "name": e.name, "type": e.type} for e in entities],
|
||
"relations": relations,
|
||
"transcripts": [{"id": t['id'], "filename": t['filename']} for t in transcripts],
|
||
"exported_at": datetime.now().isoformat()
|
||
}
|
||
|
||
# 上传 JSON 文件
|
||
json_content = json.dumps(export_data, ensure_ascii=False, indent=2)
|
||
json_path = f"{remote_project_path}/project_export.json"
|
||
|
||
# 使用临时文件上传
|
||
import tempfile
|
||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||
f.write(json_content)
|
||
temp_path = f.name
|
||
|
||
client.upload_file(temp_path, json_path)
|
||
os.unlink(temp_path)
|
||
|
||
# 更新同步状态
|
||
now = datetime.now().isoformat()
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""UPDATE webdav_syncs
|
||
SET last_sync_at = ?, last_sync_status = ?, sync_count = sync_count + 1
|
||
WHERE id = ?""",
|
||
(now, 'success', sync.id)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Project synced successfully",
|
||
"entities_count": len(entities),
|
||
"relations_count": len(relations),
|
||
"remote_path": json_path
|
||
}
|
||
|
||
except Exception as e:
|
||
# 更新失败状态
|
||
conn = self.pm.db.get_conn()
|
||
conn.execute(
|
||
"""UPDATE webdav_syncs
|
||
SET last_sync_status = ?, last_sync_error = ?
|
||
WHERE id = ?""",
|
||
('failed', str(e), sync.id)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return {
|
||
"success": False,
|
||
"error": str(e)
|
||
}
|
||
|
||
|
||
# Singleton instance
|
||
_plugin_manager = None
|
||
|
||
def get_plugin_manager(db_manager=None):
|
||
"""获取 PluginManager 单例"""
|
||
global _plugin_manager
|
||
if _plugin_manager is None:
|
||
_plugin_manager = PluginManager(db_manager)
|
||
return _plugin_manager
|