fix: auto-fix code issues (cron)
- 修复重复导入/字段 - 修复异常处理 - 修复PEP8格式问题 - 添加类型注解
This commit is contained in:
+136
-175
@@ -1,192 +1,153 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
InsightFlow 代码自动修复脚本 - 增强版
|
InsightFlow 代码自动修复脚本
|
||||||
自动修复代码中的常见问题
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_DIR = Path("/root/.openclaw/workspace/projects/insightflow")
|
||||||
|
BACKEND_DIR = PROJECT_DIR / "backend"
|
||||||
|
|
||||||
def run_ruff_check(directory: str) -> list[dict]:
|
def run_flake8():
|
||||||
"""运行 ruff 检查并返回问题列表"""
|
"""运行 flake8 检查"""
|
||||||
try:
|
result = subprocess.run(
|
||||||
result = subprocess.run(
|
["flake8", "--max-line-length=120", "--ignore=E501,W503", "."],
|
||||||
["ruff", "check", "--select = E, W, F, I", "--output-format = json", directory],
|
cwd=BACKEND_DIR,
|
||||||
capture_output = True,
|
capture_output=True,
|
||||||
text = True,
|
text=True
|
||||||
check = False,
|
)
|
||||||
)
|
return result.stdout
|
||||||
if result.stdout:
|
|
||||||
return json.loads(result.stdout)
|
|
||||||
return []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ruff check failed: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
def fix_missing_imports():
|
||||||
|
"""修复缺失的导入"""
|
||||||
|
fixes = []
|
||||||
|
|
||||||
|
# 检查 workflow_manager.py 中的 urllib
|
||||||
|
workflow_file = BACKEND_DIR / "workflow_manager.py"
|
||||||
|
if workflow_file.exists():
|
||||||
|
content = workflow_file.read_text()
|
||||||
|
if "import urllib" not in content and "urllib" in content:
|
||||||
|
# 在文件开头添加导入
|
||||||
|
lines = content.split('\n')
|
||||||
|
import_idx = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('import ') or line.startswith('from '):
|
||||||
|
import_idx = i + 1
|
||||||
|
lines.insert(import_idx, 'import urllib.parse')
|
||||||
|
workflow_file.write_text('\n'.join(lines))
|
||||||
|
fixes.append("workflow_manager.py: 添加 urllib.parse 导入")
|
||||||
|
|
||||||
|
# 检查 plugin_manager.py 中的 urllib
|
||||||
|
plugin_file = BACKEND_DIR / "plugin_manager.py"
|
||||||
|
if plugin_file.exists():
|
||||||
|
content = plugin_file.read_text()
|
||||||
|
if "import urllib" not in content and "urllib" in content:
|
||||||
|
lines = content.split('\n')
|
||||||
|
import_idx = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('import ') or line.startswith('from '):
|
||||||
|
import_idx = i + 1
|
||||||
|
lines.insert(import_idx, 'import urllib.parse')
|
||||||
|
plugin_file.write_text('\n'.join(lines))
|
||||||
|
fixes.append("plugin_manager.py: 添加 urllib.parse 导入")
|
||||||
|
|
||||||
|
# 检查 main.py 中的 PlainTextResponse
|
||||||
|
main_file = BACKEND_DIR / "main.py"
|
||||||
|
if main_file.exists():
|
||||||
|
content = main_file.read_text()
|
||||||
|
if "PlainTextResponse" in content and "from fastapi.responses import" in content:
|
||||||
|
# 检查是否已导入
|
||||||
|
if "PlainTextResponse" not in content.split('from fastapi.responses import')[1].split('\n')[0]:
|
||||||
|
# 添加导入
|
||||||
|
content = content.replace(
|
||||||
|
"from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse",
|
||||||
|
"from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse"
|
||||||
|
)
|
||||||
|
# 实际上已经导入了,可能是误报
|
||||||
|
|
||||||
|
return fixes
|
||||||
|
|
||||||
def fix_bare_except(content: str) -> str:
|
def fix_unused_imports():
|
||||||
"""修复裸异常捕获 - 将 bare except Exception: 改为 except Exception:"""
|
"""修复未使用的导入"""
|
||||||
pattern = r'except\s*:\s*\n'
|
fixes = []
|
||||||
replacement = 'except Exception:\n'
|
|
||||||
return re.sub(pattern, replacement, content)
|
# code_reviewer.py 中的未使用导入
|
||||||
|
code_reviewer = PROJECT_DIR / "code_reviewer.py"
|
||||||
|
if code_reviewer.exists():
|
||||||
|
content = code_reviewer.read_text()
|
||||||
|
original = content
|
||||||
|
# 移除未使用的导入
|
||||||
|
content = re.sub(r'^import os\n', '', content, flags=re.MULTILINE)
|
||||||
|
content = re.sub(r'^import subprocess\n', '', content, flags=re.MULTILINE)
|
||||||
|
content = re.sub(r'^from typing import Any\n', '', content, flags=re.MULTILINE)
|
||||||
|
if content != original:
|
||||||
|
code_reviewer.write_text(content)
|
||||||
|
fixes.append("code_reviewer.py: 移除未使用的导入")
|
||||||
|
|
||||||
|
return fixes
|
||||||
|
|
||||||
|
def fix_formatting():
|
||||||
|
"""使用 autopep8 修复格式问题"""
|
||||||
|
fixes = []
|
||||||
|
|
||||||
|
# 运行 autopep8 修复格式问题
|
||||||
|
result = subprocess.run(
|
||||||
|
["autopep8", "--in-place", "--aggressive", "--max-line-length=120", "."],
|
||||||
|
cwd=BACKEND_DIR,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
fixes.append("使用 autopep8 修复了格式问题")
|
||||||
|
|
||||||
|
return fixes
|
||||||
|
|
||||||
def fix_undefined_names(content: str, filepath: str) -> str:
|
def main():
|
||||||
"""修复未定义的名称"""
|
print("=" * 60)
|
||||||
lines = content.split('\n')
|
|
||||||
modified = False
|
|
||||||
|
|
||||||
import_map = {
|
|
||||||
'ExportEntity': 'from export_manager import ExportEntity',
|
|
||||||
'ExportRelation': 'from export_manager import ExportRelation',
|
|
||||||
'ExportTranscript': 'from export_manager import ExportTranscript',
|
|
||||||
'WorkflowManager': 'from workflow_manager import WorkflowManager',
|
|
||||||
'PluginManager': 'from plugin_manager import PluginManager',
|
|
||||||
'OpsManager': 'from ops_manager import OpsManager',
|
|
||||||
'urllib': 'import urllib.parse',
|
|
||||||
}
|
|
||||||
|
|
||||||
undefined_names = set()
|
|
||||||
for name, import_stmt in import_map.items():
|
|
||||||
if name in content and import_stmt not in content:
|
|
||||||
undefined_names.add((name, import_stmt))
|
|
||||||
|
|
||||||
if undefined_names:
|
|
||||||
import_idx = 0
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.startswith('import ') or line.startswith('from '):
|
|
||||||
import_idx = i + 1
|
|
||||||
|
|
||||||
for name, import_stmt in sorted(undefined_names):
|
|
||||||
lines.insert(import_idx, import_stmt)
|
|
||||||
import_idx += 1
|
|
||||||
modified = True
|
|
||||||
|
|
||||||
if modified:
|
|
||||||
return '\n'.join(lines)
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def fix_file(filepath: str, issues: list[dict]) -> tuple[bool, list[str], list[str]]:
|
|
||||||
"""修复单个文件的问题"""
|
|
||||||
with open(filepath, 'r', encoding = 'utf-8') as f:
|
|
||||||
original_content = f.read()
|
|
||||||
|
|
||||||
content = original_content
|
|
||||||
fixed_issues = []
|
|
||||||
manual_fix_needed = []
|
|
||||||
|
|
||||||
for issue in issues:
|
|
||||||
code = issue.get('code', '')
|
|
||||||
message = issue.get('message', '')
|
|
||||||
line_num = issue['location']['row']
|
|
||||||
|
|
||||||
if code == 'F821':
|
|
||||||
content = fix_undefined_names(content, filepath)
|
|
||||||
if content != original_content:
|
|
||||||
fixed_issues.append(f"F821 - {message} (line {line_num})")
|
|
||||||
else:
|
|
||||||
manual_fix_needed.append(f"F821 - {message} (line {line_num})")
|
|
||||||
elif code == 'E501':
|
|
||||||
manual_fix_needed.append(f"E501 (line {line_num})")
|
|
||||||
|
|
||||||
content = fix_bare_except(content)
|
|
||||||
|
|
||||||
if content != original_content:
|
|
||||||
with open(filepath, 'w', encoding = 'utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
return True, fixed_issues, manual_fix_needed
|
|
||||||
|
|
||||||
return False, fixed_issues, manual_fix_needed
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
base_dir = Path("/root/.openclaw/workspace/projects/insightflow")
|
|
||||||
backend_dir = base_dir / "backend"
|
|
||||||
|
|
||||||
print(" = " * 60)
|
|
||||||
print("InsightFlow 代码自动修复")
|
print("InsightFlow 代码自动修复")
|
||||||
print(" = " * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
print("\n1. 扫描代码问题...")
|
all_fixes = []
|
||||||
issues = run_ruff_check(str(backend_dir))
|
|
||||||
|
# 1. 修复缺失的导入
|
||||||
issues_by_file = {}
|
print("\n[1/3] 修复缺失的导入...")
|
||||||
for issue in issues:
|
fixes = fix_missing_imports()
|
||||||
filepath = issue.get('filename', '')
|
all_fixes.extend(fixes)
|
||||||
if filepath not in issues_by_file:
|
for f in fixes:
|
||||||
issues_by_file[filepath] = []
|
print(f" ✓ {f}")
|
||||||
issues_by_file[filepath].append(issue)
|
|
||||||
|
# 2. 修复未使用的导入
|
||||||
print(f" 发现 {len(issues)} 个问题,分布在 {len(issues_by_file)} 个文件中")
|
print("\n[2/3] 修复未使用的导入...")
|
||||||
|
fixes = fix_unused_imports()
|
||||||
issue_types = {}
|
all_fixes.extend(fixes)
|
||||||
for issue in issues:
|
for f in fixes:
|
||||||
code = issue.get('code', 'UNKNOWN')
|
print(f" ✓ {f}")
|
||||||
issue_types[code] = issue_types.get(code, 0) + 1
|
|
||||||
|
# 3. 修复格式问题
|
||||||
print("\n2. 问题类型统计:")
|
print("\n[3/3] 修复 PEP8 格式问题...")
|
||||||
for code, count in sorted(issue_types.items(), key = lambda x: -x[1]):
|
fixes = fix_formatting()
|
||||||
print(f" - {code}: {count} 个")
|
all_fixes.extend(fixes)
|
||||||
|
for f in fixes:
|
||||||
print("\n3. 尝试自动修复...")
|
print(f" ✓ {f}")
|
||||||
fixed_files = []
|
|
||||||
all_fixed_issues = []
|
print("\n" + "=" * 60)
|
||||||
all_manual_fixes = []
|
print(f"修复完成!共修复 {len(all_fixes)} 个问题")
|
||||||
|
print("=" * 60)
|
||||||
for filepath, file_issues in issues_by_file.items():
|
|
||||||
if not os.path.exists(filepath):
|
# 再次运行 flake8 检查
|
||||||
continue
|
print("\n重新运行 flake8 检查...")
|
||||||
|
remaining = run_flake8()
|
||||||
modified, fixed, manual = fix_file(filepath, file_issues)
|
if remaining:
|
||||||
if modified:
|
lines = remaining.strip().split('\n')
|
||||||
fixed_files.append(filepath)
|
print(f" 仍有 {len(lines)} 个问题需要手动处理")
|
||||||
all_fixed_issues.extend(fixed)
|
else:
|
||||||
all_manual_fixes.extend([(filepath, m) for m in manual])
|
print(" ✓ 所有问题已修复!")
|
||||||
|
|
||||||
print(f" 直接修改了 {len(fixed_files)} 个文件")
|
return all_fixes
|
||||||
print(f" 自动修复了 {len(all_fixed_issues)} 个问题")
|
|
||||||
|
|
||||||
print("\n4. 运行 ruff 自动格式化...")
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["ruff", "format", str(backend_dir)],
|
|
||||||
capture_output = True,
|
|
||||||
check = False,
|
|
||||||
)
|
|
||||||
print(" 格式化完成")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" 格式化失败: {e}")
|
|
||||||
|
|
||||||
print("\n5. 再次检查...")
|
|
||||||
remaining_issues = run_ruff_check(str(backend_dir))
|
|
||||||
print(f" 剩余 {len(remaining_issues)} 个问题需要手动处理")
|
|
||||||
|
|
||||||
report = {
|
|
||||||
'total_issues': len(issues),
|
|
||||||
'fixed_files': len(fixed_files),
|
|
||||||
'fixed_issues': len(all_fixed_issues),
|
|
||||||
'remaining_issues': len(remaining_issues),
|
|
||||||
'issue_types': issue_types,
|
|
||||||
'manual_fix_needed': all_manual_fixes[:30],
|
|
||||||
}
|
|
||||||
|
|
||||||
return report
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
report = main()
|
main()
|
||||||
print("\n" + " = " * 60)
|
|
||||||
print("修复报告")
|
|
||||||
print(" = " * 60)
|
|
||||||
print(f"总问题数: {report['total_issues']}")
|
|
||||||
print(f"修复文件数: {report['fixed_files']}")
|
|
||||||
print(f"自动修复问题数: {report['fixed_issues']}")
|
|
||||||
print(f"剩余问题数: {report['remaining_issues']}")
|
|
||||||
print(f"\n需要手动处理的问题 (前30个):")
|
|
||||||
for filepath, issue in report['manual_fix_needed']:
|
|
||||||
print(f" - {filepath}: {issue}")
|
|
||||||
|
|||||||
+375
-375
File diff suppressed because it is too large
Load Diff
+109
-109
@@ -13,13 +13,13 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
DB_PATH = os.getenv("DB_PATH", "/app/data/insightflow.db")
|
DB_PATH = os.getenv("DB_PATH", "/app/data/insightflow.db")
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyStatus(Enum):
|
class ApiKeyStatus(Enum):
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
EXPIRED = "expired"
|
EXPIRED = "expired"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -37,18 +37,18 @@ class ApiKey:
|
|||||||
last_used_at: str | None
|
last_used_at: str | None
|
||||||
revoked_at: str | None
|
revoked_at: str | None
|
||||||
revoked_reason: str | None
|
revoked_reason: str | None
|
||||||
total_calls: int = 0
|
total_calls: int = 0
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyManager:
|
class ApiKeyManager:
|
||||||
"""API Key 管理器"""
|
"""API Key 管理器"""
|
||||||
|
|
||||||
# Key 前缀
|
# Key 前缀
|
||||||
KEY_PREFIX = "ak_live_"
|
KEY_PREFIX = "ak_live_"
|
||||||
KEY_LENGTH = 48 # 总长度: 前缀(8) + 随机部分(40)
|
KEY_LENGTH = 48 # 总长度: 前缀(8) + 随机部分(40)
|
||||||
|
|
||||||
def __init__(self, db_path: str = DB_PATH) -> None:
|
def __init__(self, db_path: str = DB_PATH) -> None:
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self._init_db()
|
self._init_db()
|
||||||
|
|
||||||
def _init_db(self) -> None:
|
def _init_db(self) -> None:
|
||||||
@@ -117,7 +117,7 @@ class ApiKeyManager:
|
|||||||
def _generate_key(self) -> str:
|
def _generate_key(self) -> str:
|
||||||
"""生成新的 API Key"""
|
"""生成新的 API Key"""
|
||||||
# 生成 40 字符的随机字符串
|
# 生成 40 字符的随机字符串
|
||||||
random_part = secrets.token_urlsafe(30)[:40]
|
random_part = secrets.token_urlsafe(30)[:40]
|
||||||
return f"{self.KEY_PREFIX}{random_part}"
|
return f"{self.KEY_PREFIX}{random_part}"
|
||||||
|
|
||||||
def _hash_key(self, key: str) -> str:
|
def _hash_key(self, key: str) -> str:
|
||||||
@@ -131,10 +131,10 @@ class ApiKeyManager:
|
|||||||
def create_key(
|
def create_key(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
permissions: list[str] = None,
|
permissions: list[str] = None,
|
||||||
rate_limit: int = 60,
|
rate_limit: int = 60,
|
||||||
expires_days: int | None = None,
|
expires_days: int | None = None,
|
||||||
) -> tuple[str, ApiKey]:
|
) -> tuple[str, ApiKey]:
|
||||||
"""
|
"""
|
||||||
创建新的 API Key
|
创建新的 API Key
|
||||||
@@ -143,32 +143,32 @@ class ApiKeyManager:
|
|||||||
tuple: (原始key(仅返回一次), ApiKey对象)
|
tuple: (原始key(仅返回一次), ApiKey对象)
|
||||||
"""
|
"""
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
permissions = ["read"]
|
permissions = ["read"]
|
||||||
|
|
||||||
key_id = secrets.token_hex(16)
|
key_id = secrets.token_hex(16)
|
||||||
raw_key = self._generate_key()
|
raw_key = self._generate_key()
|
||||||
key_hash = self._hash_key(raw_key)
|
key_hash = self._hash_key(raw_key)
|
||||||
key_preview = self._get_preview(raw_key)
|
key_preview = self._get_preview(raw_key)
|
||||||
|
|
||||||
expires_at = None
|
expires_at = None
|
||||||
if expires_days:
|
if expires_days:
|
||||||
expires_at = (datetime.now() + timedelta(days = expires_days)).isoformat()
|
expires_at = (datetime.now() + timedelta(days=expires_days)).isoformat()
|
||||||
|
|
||||||
api_key = ApiKey(
|
api_key = ApiKey(
|
||||||
id = key_id,
|
id=key_id,
|
||||||
key_hash = key_hash,
|
key_hash=key_hash,
|
||||||
key_preview = key_preview,
|
key_preview=key_preview,
|
||||||
name = name,
|
name=name,
|
||||||
owner_id = owner_id,
|
owner_id=owner_id,
|
||||||
permissions = permissions,
|
permissions=permissions,
|
||||||
rate_limit = rate_limit,
|
rate_limit=rate_limit,
|
||||||
status = ApiKeyStatus.ACTIVE.value,
|
status=ApiKeyStatus.ACTIVE.value,
|
||||||
created_at = datetime.now().isoformat(),
|
created_at=datetime.now().isoformat(),
|
||||||
expires_at = expires_at,
|
expires_at=expires_at,
|
||||||
last_used_at = None,
|
last_used_at=None,
|
||||||
revoked_at = None,
|
revoked_at=None,
|
||||||
revoked_reason = None,
|
revoked_reason=None,
|
||||||
total_calls = 0,
|
total_calls=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
@@ -203,16 +203,16 @@ class ApiKeyManager:
|
|||||||
Returns:
|
Returns:
|
||||||
ApiKey if valid, None otherwise
|
ApiKey if valid, None otherwise
|
||||||
"""
|
"""
|
||||||
key_hash = self._hash_key(key)
|
key_hash = self._hash_key(key)
|
||||||
|
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
row = conn.execute("SELECT * FROM api_keys WHERE key_hash = ?", (key_hash, )).fetchone()
|
row = conn.execute("SELECT * FROM api_keys WHERE key_hash = ?", (key_hash, )).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
api_key = self._row_to_api_key(row)
|
api_key = self._row_to_api_key(row)
|
||||||
|
|
||||||
# 检查状态
|
# 检查状态
|
||||||
if api_key.status != ApiKeyStatus.ACTIVE.value:
|
if api_key.status != ApiKeyStatus.ACTIVE.value:
|
||||||
@@ -220,7 +220,7 @@ class ApiKeyManager:
|
|||||||
|
|
||||||
# 检查是否过期
|
# 检查是否过期
|
||||||
if api_key.expires_at:
|
if api_key.expires_at:
|
||||||
expires = datetime.fromisoformat(api_key.expires_at)
|
expires = datetime.fromisoformat(api_key.expires_at)
|
||||||
if datetime.now() > expires:
|
if datetime.now() > expires:
|
||||||
# 更新状态为过期
|
# 更新状态为过期
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -232,18 +232,18 @@ class ApiKeyManager:
|
|||||||
|
|
||||||
return api_key
|
return api_key
|
||||||
|
|
||||||
def revoke_key(self, key_id: str, reason: str = "", owner_id: str | None = None) -> bool:
|
def revoke_key(self, key_id: str, reason: str = "", owner_id: str | None = None) -> bool:
|
||||||
"""撤销 API Key"""
|
"""撤销 API Key"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
# 验证所有权(如果提供了 owner_id)
|
# 验证所有权(如果提供了 owner_id)
|
||||||
if owner_id:
|
if owner_id:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT owner_id FROM api_keys WHERE id = ?", (key_id, )
|
"SELECT owner_id FROM api_keys WHERE id = ?", (key_id, )
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row or row[0] != owner_id:
|
if not row or row[0] != owner_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE api_keys
|
UPDATE api_keys
|
||||||
SET status = ?, revoked_at = ?, revoked_reason = ?
|
SET status = ?, revoked_at = ?, revoked_reason = ?
|
||||||
@@ -260,17 +260,17 @@ class ApiKeyManager:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
def get_key_by_id(self, key_id: str, owner_id: str | None = None) -> ApiKey | None:
|
def get_key_by_id(self, key_id: str, owner_id: str | None = None) -> ApiKey | None:
|
||||||
"""通过 ID 获取 API Key(不包含敏感信息)"""
|
"""通过 ID 获取 API Key(不包含敏感信息)"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
if owner_id:
|
if owner_id:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT * FROM api_keys WHERE id = ? AND owner_id = ?", (key_id, owner_id)
|
"SELECT * FROM api_keys WHERE id = ? AND owner_id = ?", (key_id, owner_id)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
else:
|
else:
|
||||||
row = conn.execute("SELECT * FROM api_keys WHERE id = ?", (key_id, )).fetchone()
|
row = conn.execute("SELECT * FROM api_keys WHERE id = ?", (key_id, )).fetchone()
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
return self._row_to_api_key(row)
|
return self._row_to_api_key(row)
|
||||||
@@ -278,17 +278,17 @@ class ApiKeyManager:
|
|||||||
|
|
||||||
def list_keys(
|
def list_keys(
|
||||||
self,
|
self,
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
status: str | None = None,
|
status: str | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[ApiKey]:
|
) -> list[ApiKey]:
|
||||||
"""列出 API Keys"""
|
"""列出 API Keys"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
query = "SELECT * FROM api_keys WHERE 1 = 1"
|
query = "SELECT * FROM api_keys WHERE 1 = 1"
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if owner_id:
|
if owner_id:
|
||||||
query += " AND owner_id = ?"
|
query += " AND owner_id = ?"
|
||||||
@@ -301,20 +301,20 @@ class ApiKeyManager:
|
|||||||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
|
|
||||||
rows = conn.execute(query, params).fetchall()
|
rows = conn.execute(query, params).fetchall()
|
||||||
return [self._row_to_api_key(row) for row in rows]
|
return [self._row_to_api_key(row) for row in rows]
|
||||||
|
|
||||||
def update_key(
|
def update_key(
|
||||||
self,
|
self,
|
||||||
key_id: str,
|
key_id: str,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
permissions: list[str] | None = None,
|
permissions: list[str] | None = None,
|
||||||
rate_limit: int | None = None,
|
rate_limit: int | None = None,
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""更新 API Key 信息"""
|
"""更新 API Key 信息"""
|
||||||
updates = []
|
updates = []
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
updates.append("name = ?")
|
updates.append("name = ?")
|
||||||
@@ -336,14 +336,14 @@ class ApiKeyManager:
|
|||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
# 验证所有权
|
# 验证所有权
|
||||||
if owner_id:
|
if owner_id:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT owner_id FROM api_keys WHERE id = ?", (key_id, )
|
"SELECT owner_id FROM api_keys WHERE id = ?", (key_id, )
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row or row[0] != owner_id:
|
if not row or row[0] != owner_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
query = f"UPDATE api_keys SET {', '.join(updates)} WHERE id = ?"
|
query = f"UPDATE api_keys SET {', '.join(updates)} WHERE id = ?"
|
||||||
cursor = conn.execute(query, params)
|
cursor = conn.execute(query, params)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
@@ -365,11 +365,11 @@ class ApiKeyManager:
|
|||||||
api_key_id: str,
|
api_key_id: str,
|
||||||
endpoint: str,
|
endpoint: str,
|
||||||
method: str,
|
method: str,
|
||||||
status_code: int = 200,
|
status_code: int = 200,
|
||||||
response_time_ms: int = 0,
|
response_time_ms: int = 0,
|
||||||
ip_address: str = "",
|
ip_address: str = "",
|
||||||
user_agent: str = "",
|
user_agent: str = "",
|
||||||
error_message: str = "",
|
error_message: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""记录 API 调用日志"""
|
"""记录 API 调用日志"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
@@ -395,18 +395,18 @@ class ApiKeyManager:
|
|||||||
|
|
||||||
def get_call_logs(
|
def get_call_logs(
|
||||||
self,
|
self,
|
||||||
api_key_id: str | None = None,
|
api_key_id: str | None = None,
|
||||||
start_date: str | None = None,
|
start_date: str | None = None,
|
||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""获取 API 调用日志"""
|
"""获取 API 调用日志"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
query = "SELECT * FROM api_call_logs WHERE 1 = 1"
|
query = "SELECT * FROM api_call_logs WHERE 1 = 1"
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if api_key_id:
|
if api_key_id:
|
||||||
query += " AND api_key_id = ?"
|
query += " AND api_key_id = ?"
|
||||||
@@ -423,16 +423,16 @@ class ApiKeyManager:
|
|||||||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
|
|
||||||
rows = conn.execute(query, params).fetchall()
|
rows = conn.execute(query, params).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
def get_call_stats(self, api_key_id: str | None = None, days: int = 30) -> dict:
|
def get_call_stats(self, api_key_id: str | None = None, days: int = 30) -> dict:
|
||||||
"""获取 API 调用统计"""
|
"""获取 API 调用统计"""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
# 总体统计
|
# 总体统计
|
||||||
query = f"""
|
query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_calls,
|
COUNT(*) as total_calls,
|
||||||
COUNT(CASE WHEN status_code < 400 THEN 1 END) as success_calls,
|
COUNT(CASE WHEN status_code < 400 THEN 1 END) as success_calls,
|
||||||
@@ -444,15 +444,15 @@ class ApiKeyManager:
|
|||||||
WHERE created_at >= date('now', '-{days} days')
|
WHERE created_at >= date('now', '-{days} days')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = []
|
params = []
|
||||||
if api_key_id:
|
if api_key_id:
|
||||||
query = query.replace("WHERE created_at", "WHERE api_key_id = ? AND created_at")
|
query = query.replace("WHERE created_at", "WHERE api_key_id = ? AND created_at")
|
||||||
params.insert(0, api_key_id)
|
params.insert(0, api_key_id)
|
||||||
|
|
||||||
row = conn.execute(query, params).fetchone()
|
row = conn.execute(query, params).fetchone()
|
||||||
|
|
||||||
# 按端点统计
|
# 按端点统计
|
||||||
endpoint_query = f"""
|
endpoint_query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
endpoint,
|
endpoint,
|
||||||
method,
|
method,
|
||||||
@@ -462,19 +462,19 @@ class ApiKeyManager:
|
|||||||
WHERE created_at >= date('now', '-{days} days')
|
WHERE created_at >= date('now', '-{days} days')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
endpoint_params = []
|
endpoint_params = []
|
||||||
if api_key_id:
|
if api_key_id:
|
||||||
endpoint_query = endpoint_query.replace(
|
endpoint_query = endpoint_query.replace(
|
||||||
"WHERE created_at", "WHERE api_key_id = ? AND created_at"
|
"WHERE created_at", "WHERE api_key_id = ? AND created_at"
|
||||||
)
|
)
|
||||||
endpoint_params.insert(0, api_key_id)
|
endpoint_params.insert(0, api_key_id)
|
||||||
|
|
||||||
endpoint_query += " GROUP BY endpoint, method ORDER BY calls DESC"
|
endpoint_query += " GROUP BY endpoint, method ORDER BY calls DESC"
|
||||||
|
|
||||||
endpoint_rows = conn.execute(endpoint_query, endpoint_params).fetchall()
|
endpoint_rows = conn.execute(endpoint_query, endpoint_params).fetchall()
|
||||||
|
|
||||||
# 按天统计
|
# 按天统计
|
||||||
daily_query = f"""
|
daily_query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
date(created_at) as date,
|
date(created_at) as date,
|
||||||
COUNT(*) as calls,
|
COUNT(*) as calls,
|
||||||
@@ -483,16 +483,16 @@ class ApiKeyManager:
|
|||||||
WHERE created_at >= date('now', '-{days} days')
|
WHERE created_at >= date('now', '-{days} days')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
daily_params = []
|
daily_params = []
|
||||||
if api_key_id:
|
if api_key_id:
|
||||||
daily_query = daily_query.replace(
|
daily_query = daily_query.replace(
|
||||||
"WHERE created_at", "WHERE api_key_id = ? AND created_at"
|
"WHERE created_at", "WHERE api_key_id = ? AND created_at"
|
||||||
)
|
)
|
||||||
daily_params.insert(0, api_key_id)
|
daily_params.insert(0, api_key_id)
|
||||||
|
|
||||||
daily_query += " GROUP BY date(created_at) ORDER BY date"
|
daily_query += " GROUP BY date(created_at) ORDER BY date"
|
||||||
|
|
||||||
daily_rows = conn.execute(daily_query, daily_params).fetchall()
|
daily_rows = conn.execute(daily_query, daily_params).fetchall()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -510,30 +510,30 @@ class ApiKeyManager:
|
|||||||
def _row_to_api_key(self, row: sqlite3.Row) -> ApiKey:
|
def _row_to_api_key(self, row: sqlite3.Row) -> ApiKey:
|
||||||
"""将数据库行转换为 ApiKey 对象"""
|
"""将数据库行转换为 ApiKey 对象"""
|
||||||
return ApiKey(
|
return ApiKey(
|
||||||
id = row["id"],
|
id=row["id"],
|
||||||
key_hash = row["key_hash"],
|
key_hash=row["key_hash"],
|
||||||
key_preview = row["key_preview"],
|
key_preview=row["key_preview"],
|
||||||
name = row["name"],
|
name=row["name"],
|
||||||
owner_id = row["owner_id"],
|
owner_id=row["owner_id"],
|
||||||
permissions = json.loads(row["permissions"]),
|
permissions=json.loads(row["permissions"]),
|
||||||
rate_limit = row["rate_limit"],
|
rate_limit=row["rate_limit"],
|
||||||
status = row["status"],
|
status=row["status"],
|
||||||
created_at = row["created_at"],
|
created_at=row["created_at"],
|
||||||
expires_at = row["expires_at"],
|
expires_at=row["expires_at"],
|
||||||
last_used_at = row["last_used_at"],
|
last_used_at=row["last_used_at"],
|
||||||
revoked_at = row["revoked_at"],
|
revoked_at=row["revoked_at"],
|
||||||
revoked_reason = row["revoked_reason"],
|
revoked_reason=row["revoked_reason"],
|
||||||
total_calls = row["total_calls"],
|
total_calls=row["total_calls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# 全局实例
|
# 全局实例
|
||||||
_api_key_manager: ApiKeyManager | None = None
|
_api_key_manager: ApiKeyManager | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_api_key_manager() -> ApiKeyManager:
|
def get_api_key_manager() -> ApiKeyManager:
|
||||||
"""获取 API Key 管理器实例"""
|
"""获取 API Key 管理器实例"""
|
||||||
global _api_key_manager
|
global _api_key_manager
|
||||||
if _api_key_manager is None:
|
if _api_key_manager is None:
|
||||||
_api_key_manager = ApiKeyManager()
|
_api_key_manager = ApiKeyManager()
|
||||||
return _api_key_manager
|
return _api_key_manager
|
||||||
|
|||||||
+231
-231
@@ -15,29 +15,29 @@ from typing import Any
|
|||||||
class SharePermission(Enum):
|
class SharePermission(Enum):
|
||||||
"""分享权限级别"""
|
"""分享权限级别"""
|
||||||
|
|
||||||
READ_ONLY = "read_only" # 只读
|
READ_ONLY = "read_only" # 只读
|
||||||
COMMENT = "comment" # 可评论
|
COMMENT = "comment" # 可评论
|
||||||
EDIT = "edit" # 可编辑
|
EDIT = "edit" # 可编辑
|
||||||
ADMIN = "admin" # 管理员
|
ADMIN = "admin" # 管理员
|
||||||
|
|
||||||
|
|
||||||
class CommentTargetType(Enum):
|
class CommentTargetType(Enum):
|
||||||
"""评论目标类型"""
|
"""评论目标类型"""
|
||||||
|
|
||||||
ENTITY = "entity" # 实体评论
|
ENTITY = "entity" # 实体评论
|
||||||
RELATION = "relation" # 关系评论
|
RELATION = "relation" # 关系评论
|
||||||
TRANSCRIPT = "transcript" # 转录文本评论
|
TRANSCRIPT = "transcript" # 转录文本评论
|
||||||
PROJECT = "project" # 项目级评论
|
PROJECT = "project" # 项目级评论
|
||||||
|
|
||||||
|
|
||||||
class ChangeType(Enum):
|
class ChangeType(Enum):
|
||||||
"""变更类型"""
|
"""变更类型"""
|
||||||
|
|
||||||
CREATE = "create" # 创建
|
CREATE = "create" # 创建
|
||||||
UPDATE = "update" # 更新
|
UPDATE = "update" # 更新
|
||||||
DELETE = "delete" # 删除
|
DELETE = "delete" # 删除
|
||||||
MERGE = "merge" # 合并
|
MERGE = "merge" # 合并
|
||||||
SPLIT = "split" # 拆分
|
SPLIT = "split" # 拆分
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -136,10 +136,10 @@ class TeamSpace:
|
|||||||
class CollaborationManager:
|
class CollaborationManager:
|
||||||
"""协作管理主类"""
|
"""协作管理主类"""
|
||||||
|
|
||||||
def __init__(self, db_manager = None) -> None:
|
def __init__(self, db_manager=None) -> None:
|
||||||
self.db = db_manager
|
self.db = db_manager
|
||||||
self._shares_cache: dict[str, ProjectShare] = {}
|
self._shares_cache: dict[str, ProjectShare] = {}
|
||||||
self._comments_cache: dict[str, list[Comment]] = {}
|
self._comments_cache: dict[str, list[Comment]] = {}
|
||||||
|
|
||||||
# ============ 项目分享 ============
|
# ============ 项目分享 ============
|
||||||
|
|
||||||
@@ -147,57 +147,57 @@ class CollaborationManager:
|
|||||||
self,
|
self,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
created_by: str,
|
created_by: str,
|
||||||
permission: str = "read_only",
|
permission: str = "read_only",
|
||||||
expires_in_days: int | None = None,
|
expires_in_days: int | None = None,
|
||||||
max_uses: int | None = None,
|
max_uses: int | None = None,
|
||||||
password: str | None = None,
|
password: str | None = None,
|
||||||
allow_download: bool = False,
|
allow_download: bool = False,
|
||||||
allow_export: bool = False,
|
allow_export: bool = False,
|
||||||
) -> ProjectShare:
|
) -> ProjectShare:
|
||||||
"""创建项目分享链接"""
|
"""创建项目分享链接"""
|
||||||
share_id = str(uuid.uuid4())
|
share_id = str(uuid.uuid4())
|
||||||
token = self._generate_share_token(project_id)
|
token = self._generate_share_token(project_id)
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
expires_at = None
|
expires_at = None
|
||||||
if expires_in_days:
|
if expires_in_days:
|
||||||
expires_at = (datetime.now() + timedelta(days = expires_in_days)).isoformat()
|
expires_at = (datetime.now() + timedelta(days=expires_in_days)).isoformat()
|
||||||
|
|
||||||
password_hash = None
|
password_hash = None
|
||||||
if password:
|
if password:
|
||||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
share = ProjectShare(
|
share = ProjectShare(
|
||||||
id = share_id,
|
id=share_id,
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
token = token,
|
token=token,
|
||||||
permission = permission,
|
permission=permission,
|
||||||
created_by = created_by,
|
created_by=created_by,
|
||||||
created_at = now,
|
created_at=now,
|
||||||
expires_at = expires_at,
|
expires_at=expires_at,
|
||||||
max_uses = max_uses,
|
max_uses=max_uses,
|
||||||
use_count = 0,
|
use_count=0,
|
||||||
password_hash = password_hash,
|
password_hash=password_hash,
|
||||||
is_active = True,
|
is_active=True,
|
||||||
allow_download = allow_download,
|
allow_download=allow_download,
|
||||||
allow_export = allow_export,
|
allow_export=allow_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 保存到数据库
|
# 保存到数据库
|
||||||
if self.db:
|
if self.db:
|
||||||
self._save_share_to_db(share)
|
self._save_share_to_db(share)
|
||||||
|
|
||||||
self._shares_cache[token] = share
|
self._shares_cache[token] = share
|
||||||
return share
|
return share
|
||||||
|
|
||||||
def _generate_share_token(self, project_id: str) -> str:
|
def _generate_share_token(self, project_id: str) -> str:
|
||||||
"""生成分享令牌"""
|
"""生成分享令牌"""
|
||||||
data = f"{project_id}:{datetime.now().timestamp()}:{uuid.uuid4()}"
|
data = f"{project_id}:{datetime.now().timestamp()}:{uuid.uuid4()}"
|
||||||
return hashlib.sha256(data.encode()).hexdigest()[:32]
|
return hashlib.sha256(data.encode()).hexdigest()[:32]
|
||||||
|
|
||||||
def _save_share_to_db(self, share: ProjectShare) -> None:
|
def _save_share_to_db(self, share: ProjectShare) -> None:
|
||||||
"""保存分享记录到数据库"""
|
"""保存分享记录到数据库"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO project_shares
|
INSERT INTO project_shares
|
||||||
@@ -224,12 +224,12 @@ class CollaborationManager:
|
|||||||
)
|
)
|
||||||
self.db.conn.commit()
|
self.db.conn.commit()
|
||||||
|
|
||||||
def validate_share_token(self, token: str, password: str | None = None) -> ProjectShare | None:
|
def validate_share_token(self, token: str, password: str | None = None) -> ProjectShare | None:
|
||||||
"""验证分享令牌"""
|
"""验证分享令牌"""
|
||||||
# 从缓存或数据库获取
|
# 从缓存或数据库获取
|
||||||
share = self._shares_cache.get(token)
|
share = self._shares_cache.get(token)
|
||||||
if not share and self.db:
|
if not share and self.db:
|
||||||
share = self._get_share_from_db(token)
|
share = self._get_share_from_db(token)
|
||||||
|
|
||||||
if not share:
|
if not share:
|
||||||
return None
|
return None
|
||||||
@@ -250,7 +250,7 @@ class CollaborationManager:
|
|||||||
if share.password_hash:
|
if share.password_hash:
|
||||||
if not password:
|
if not password:
|
||||||
return None
|
return None
|
||||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
if password_hash != share.password_hash:
|
if password_hash != share.password_hash:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -258,42 +258,42 @@ class CollaborationManager:
|
|||||||
|
|
||||||
def _get_share_from_db(self, token: str) -> ProjectShare | None:
|
def _get_share_from_db(self, token: str) -> ProjectShare | None:
|
||||||
"""从数据库获取分享记录"""
|
"""从数据库获取分享记录"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM project_shares WHERE token = ?
|
SELECT * FROM project_shares WHERE token = ?
|
||||||
""",
|
""",
|
||||||
(token, ),
|
(token, ),
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return ProjectShare(
|
return ProjectShare(
|
||||||
id = row[0],
|
id=row[0],
|
||||||
project_id = row[1],
|
project_id=row[1],
|
||||||
token = row[2],
|
token=row[2],
|
||||||
permission = row[3],
|
permission=row[3],
|
||||||
created_by = row[4],
|
created_by=row[4],
|
||||||
created_at = row[5],
|
created_at=row[5],
|
||||||
expires_at = row[6],
|
expires_at=row[6],
|
||||||
max_uses = row[7],
|
max_uses=row[7],
|
||||||
use_count = row[8],
|
use_count=row[8],
|
||||||
password_hash = row[9],
|
password_hash=row[9],
|
||||||
is_active = bool(row[10]),
|
is_active=bool(row[10]),
|
||||||
allow_download = bool(row[11]),
|
allow_download=bool(row[11]),
|
||||||
allow_export = bool(row[12]),
|
allow_export=bool(row[12]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def increment_share_usage(self, token: str) -> None:
|
def increment_share_usage(self, token: str) -> None:
|
||||||
"""增加分享链接使用次数"""
|
"""增加分享链接使用次数"""
|
||||||
share = self._shares_cache.get(token)
|
share = self._shares_cache.get(token)
|
||||||
if share:
|
if share:
|
||||||
share.use_count += 1
|
share.use_count += 1
|
||||||
|
|
||||||
if self.db:
|
if self.db:
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE project_shares
|
UPDATE project_shares
|
||||||
@@ -307,7 +307,7 @@ class CollaborationManager:
|
|||||||
def revoke_share_link(self, share_id: str, revoked_by: str) -> bool:
|
def revoke_share_link(self, share_id: str, revoked_by: str) -> bool:
|
||||||
"""撤销分享链接"""
|
"""撤销分享链接"""
|
||||||
if self.db:
|
if self.db:
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE project_shares
|
UPDATE project_shares
|
||||||
@@ -325,7 +325,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM project_shares
|
SELECT * FROM project_shares
|
||||||
@@ -335,23 +335,23 @@ class CollaborationManager:
|
|||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
|
|
||||||
shares = []
|
shares = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
shares.append(
|
shares.append(
|
||||||
ProjectShare(
|
ProjectShare(
|
||||||
id = row[0],
|
id=row[0],
|
||||||
project_id = row[1],
|
project_id=row[1],
|
||||||
token = row[2],
|
token=row[2],
|
||||||
permission = row[3],
|
permission=row[3],
|
||||||
created_by = row[4],
|
created_by=row[4],
|
||||||
created_at = row[5],
|
created_at=row[5],
|
||||||
expires_at = row[6],
|
expires_at=row[6],
|
||||||
max_uses = row[7],
|
max_uses=row[7],
|
||||||
use_count = row[8],
|
use_count=row[8],
|
||||||
password_hash = row[9],
|
password_hash=row[9],
|
||||||
is_active = bool(row[10]),
|
is_active=bool(row[10]),
|
||||||
allow_download = bool(row[11]),
|
allow_download=bool(row[11]),
|
||||||
allow_export = bool(row[12]),
|
allow_export=bool(row[12]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return shares
|
return shares
|
||||||
@@ -366,46 +366,46 @@ class CollaborationManager:
|
|||||||
author: str,
|
author: str,
|
||||||
author_name: str,
|
author_name: str,
|
||||||
content: str,
|
content: str,
|
||||||
parent_id: str | None = None,
|
parent_id: str | None = None,
|
||||||
mentions: list[str] | None = None,
|
mentions: list[str] | None = None,
|
||||||
attachments: list[dict] | None = None,
|
attachments: list[dict] | None = None,
|
||||||
) -> Comment:
|
) -> Comment:
|
||||||
"""添加评论"""
|
"""添加评论"""
|
||||||
comment_id = str(uuid.uuid4())
|
comment_id = str(uuid.uuid4())
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
comment = Comment(
|
comment = Comment(
|
||||||
id = comment_id,
|
id=comment_id,
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
target_type = target_type,
|
target_type=target_type,
|
||||||
target_id = target_id,
|
target_id=target_id,
|
||||||
parent_id = parent_id,
|
parent_id=parent_id,
|
||||||
author = author,
|
author=author,
|
||||||
author_name = author_name,
|
author_name=author_name,
|
||||||
content = content,
|
content=content,
|
||||||
created_at = now,
|
created_at=now,
|
||||||
updated_at = now,
|
updated_at=now,
|
||||||
resolved = False,
|
resolved=False,
|
||||||
resolved_by = None,
|
resolved_by=None,
|
||||||
resolved_at = None,
|
resolved_at=None,
|
||||||
mentions = mentions or [],
|
mentions=mentions or [],
|
||||||
attachments = attachments or [],
|
attachments=attachments or [],
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.db:
|
if self.db:
|
||||||
self._save_comment_to_db(comment)
|
self._save_comment_to_db(comment)
|
||||||
|
|
||||||
# 更新缓存
|
# 更新缓存
|
||||||
key = f"{target_type}:{target_id}"
|
key = f"{target_type}:{target_id}"
|
||||||
if key not in self._comments_cache:
|
if key not in self._comments_cache:
|
||||||
self._comments_cache[key] = []
|
self._comments_cache[key] = []
|
||||||
self._comments_cache[key].append(comment)
|
self._comments_cache[key].append(comment)
|
||||||
|
|
||||||
return comment
|
return comment
|
||||||
|
|
||||||
def _save_comment_to_db(self, comment: Comment) -> None:
|
def _save_comment_to_db(self, comment: Comment) -> None:
|
||||||
"""保存评论到数据库"""
|
"""保存评论到数据库"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO comments
|
INSERT INTO comments
|
||||||
@@ -435,13 +435,13 @@ class CollaborationManager:
|
|||||||
self.db.conn.commit()
|
self.db.conn.commit()
|
||||||
|
|
||||||
def get_comments(
|
def get_comments(
|
||||||
self, target_type: str, target_id: str, include_resolved: bool = True
|
self, target_type: str, target_id: str, include_resolved: bool = True
|
||||||
) -> list[Comment]:
|
) -> list[Comment]:
|
||||||
"""获取评论列表"""
|
"""获取评论列表"""
|
||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
if include_resolved:
|
if include_resolved:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
@@ -461,7 +461,7 @@ class CollaborationManager:
|
|||||||
(target_type, target_id),
|
(target_type, target_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
comments = []
|
comments = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
comments.append(self._row_to_comment(row))
|
comments.append(self._row_to_comment(row))
|
||||||
return comments
|
return comments
|
||||||
@@ -469,21 +469,21 @@ class CollaborationManager:
|
|||||||
def _row_to_comment(self, row) -> Comment:
|
def _row_to_comment(self, row) -> Comment:
|
||||||
"""将数据库行转换为Comment对象"""
|
"""将数据库行转换为Comment对象"""
|
||||||
return Comment(
|
return Comment(
|
||||||
id = row[0],
|
id=row[0],
|
||||||
project_id = row[1],
|
project_id=row[1],
|
||||||
target_type = row[2],
|
target_type=row[2],
|
||||||
target_id = row[3],
|
target_id=row[3],
|
||||||
parent_id = row[4],
|
parent_id=row[4],
|
||||||
author = row[5],
|
author=row[5],
|
||||||
author_name = row[6],
|
author_name=row[6],
|
||||||
content = row[7],
|
content=row[7],
|
||||||
created_at = row[8],
|
created_at=row[8],
|
||||||
updated_at = row[9],
|
updated_at=row[9],
|
||||||
resolved = bool(row[10]),
|
resolved=bool(row[10]),
|
||||||
resolved_by = row[11],
|
resolved_by=row[11],
|
||||||
resolved_at = row[12],
|
resolved_at=row[12],
|
||||||
mentions = json.loads(row[13]) if row[13] else [],
|
mentions=json.loads(row[13]) if row[13] else [],
|
||||||
attachments = json.loads(row[14]) if row[14] else [],
|
attachments=json.loads(row[14]) if row[14] else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_comment(self, comment_id: str, content: str, updated_by: str) -> Comment | None:
|
def update_comment(self, comment_id: str, content: str, updated_by: str) -> Comment | None:
|
||||||
@@ -491,8 +491,8 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE comments
|
UPDATE comments
|
||||||
@@ -509,9 +509,9 @@ class CollaborationManager:
|
|||||||
|
|
||||||
def _get_comment_by_id(self, comment_id: str) -> Comment | None:
|
def _get_comment_by_id(self, comment_id: str) -> Comment | None:
|
||||||
"""根据ID获取评论"""
|
"""根据ID获取评论"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute("SELECT * FROM comments WHERE id = ?", (comment_id, ))
|
cursor.execute("SELECT * FROM comments WHERE id = ?", (comment_id, ))
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
return self._row_to_comment(row)
|
return self._row_to_comment(row)
|
||||||
return None
|
return None
|
||||||
@@ -521,8 +521,8 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE comments
|
UPDATE comments
|
||||||
@@ -539,7 +539,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
# 只允许作者或管理员删除
|
# 只允许作者或管理员删除
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
@@ -554,13 +554,13 @@ class CollaborationManager:
|
|||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
def get_project_comments(
|
def get_project_comments(
|
||||||
self, project_id: str, limit: int = 50, offset: int = 0
|
self, project_id: str, limit: int = 50, offset: int = 0
|
||||||
) -> list[Comment]:
|
) -> list[Comment]:
|
||||||
"""获取项目下的所有评论"""
|
"""获取项目下的所有评论"""
|
||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM comments
|
SELECT * FROM comments
|
||||||
@@ -571,7 +571,7 @@ class CollaborationManager:
|
|||||||
(project_id, limit, offset),
|
(project_id, limit, offset),
|
||||||
)
|
)
|
||||||
|
|
||||||
comments = []
|
comments = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
comments.append(self._row_to_comment(row))
|
comments.append(self._row_to_comment(row))
|
||||||
return comments
|
return comments
|
||||||
@@ -587,32 +587,32 @@ class CollaborationManager:
|
|||||||
entity_name: str,
|
entity_name: str,
|
||||||
changed_by: str,
|
changed_by: str,
|
||||||
changed_by_name: str,
|
changed_by_name: str,
|
||||||
old_value: dict | None = None,
|
old_value: dict | None = None,
|
||||||
new_value: dict | None = None,
|
new_value: dict | None = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
) -> ChangeRecord:
|
) -> ChangeRecord:
|
||||||
"""记录变更"""
|
"""记录变更"""
|
||||||
record_id = str(uuid.uuid4())
|
record_id = str(uuid.uuid4())
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
record = ChangeRecord(
|
record = ChangeRecord(
|
||||||
id = record_id,
|
id=record_id,
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
change_type = change_type,
|
change_type=change_type,
|
||||||
entity_type = entity_type,
|
entity_type=entity_type,
|
||||||
entity_id = entity_id,
|
entity_id=entity_id,
|
||||||
entity_name = entity_name,
|
entity_name=entity_name,
|
||||||
changed_by = changed_by,
|
changed_by=changed_by,
|
||||||
changed_by_name = changed_by_name,
|
changed_by_name=changed_by_name,
|
||||||
changed_at = now,
|
changed_at=now,
|
||||||
old_value = old_value,
|
old_value=old_value,
|
||||||
new_value = new_value,
|
new_value=new_value,
|
||||||
description = description,
|
description=description,
|
||||||
session_id = session_id,
|
session_id=session_id,
|
||||||
reverted = False,
|
reverted=False,
|
||||||
reverted_at = None,
|
reverted_at=None,
|
||||||
reverted_by = None,
|
reverted_by=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.db:
|
if self.db:
|
||||||
@@ -622,7 +622,7 @@ class CollaborationManager:
|
|||||||
|
|
||||||
def _save_change_to_db(self, record: ChangeRecord) -> None:
|
def _save_change_to_db(self, record: ChangeRecord) -> None:
|
||||||
"""保存变更记录到数据库"""
|
"""保存变更记录到数据库"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO change_history
|
INSERT INTO change_history
|
||||||
@@ -655,16 +655,16 @@ class CollaborationManager:
|
|||||||
def get_change_history(
|
def get_change_history(
|
||||||
self,
|
self,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
entity_type: str | None = None,
|
entity_type: str | None = None,
|
||||||
entity_id: str | None = None,
|
entity_id: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[ChangeRecord]:
|
) -> list[ChangeRecord]:
|
||||||
"""获取变更历史"""
|
"""获取变更历史"""
|
||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
|
|
||||||
if entity_type and entity_id:
|
if entity_type and entity_id:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -697,7 +697,7 @@ class CollaborationManager:
|
|||||||
(project_id, limit, offset),
|
(project_id, limit, offset),
|
||||||
)
|
)
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
records.append(self._row_to_change_record(row))
|
records.append(self._row_to_change_record(row))
|
||||||
return records
|
return records
|
||||||
@@ -705,22 +705,22 @@ class CollaborationManager:
|
|||||||
def _row_to_change_record(self, row) -> ChangeRecord:
|
def _row_to_change_record(self, row) -> ChangeRecord:
|
||||||
"""将数据库行转换为ChangeRecord对象"""
|
"""将数据库行转换为ChangeRecord对象"""
|
||||||
return ChangeRecord(
|
return ChangeRecord(
|
||||||
id = row[0],
|
id=row[0],
|
||||||
project_id = row[1],
|
project_id=row[1],
|
||||||
change_type = row[2],
|
change_type=row[2],
|
||||||
entity_type = row[3],
|
entity_type=row[3],
|
||||||
entity_id = row[4],
|
entity_id=row[4],
|
||||||
entity_name = row[5],
|
entity_name=row[5],
|
||||||
changed_by = row[6],
|
changed_by=row[6],
|
||||||
changed_by_name = row[7],
|
changed_by_name=row[7],
|
||||||
changed_at = row[8],
|
changed_at=row[8],
|
||||||
old_value = json.loads(row[9]) if row[9] else None,
|
old_value=json.loads(row[9]) if row[9] else None,
|
||||||
new_value = json.loads(row[10]) if row[10] else None,
|
new_value=json.loads(row[10]) if row[10] else None,
|
||||||
description = row[11],
|
description=row[11],
|
||||||
session_id = row[12],
|
session_id=row[12],
|
||||||
reverted = bool(row[13]),
|
reverted=bool(row[13]),
|
||||||
reverted_at = row[14],
|
reverted_at=row[14],
|
||||||
reverted_by = row[15],
|
reverted_by=row[15],
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_entity_version_history(self, entity_type: str, entity_id: str) -> list[ChangeRecord]:
|
def get_entity_version_history(self, entity_type: str, entity_id: str) -> list[ChangeRecord]:
|
||||||
@@ -728,7 +728,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM change_history
|
SELECT * FROM change_history
|
||||||
@@ -738,7 +738,7 @@ class CollaborationManager:
|
|||||||
(entity_type, entity_id),
|
(entity_type, entity_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
records.append(self._row_to_change_record(row))
|
records.append(self._row_to_change_record(row))
|
||||||
return records
|
return records
|
||||||
@@ -748,8 +748,8 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE change_history
|
UPDATE change_history
|
||||||
@@ -766,7 +766,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
|
|
||||||
# 总变更数
|
# 总变更数
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -775,7 +775,7 @@ class CollaborationManager:
|
|||||||
""",
|
""",
|
||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
total_changes = cursor.fetchone()[0]
|
total_changes = cursor.fetchone()[0]
|
||||||
|
|
||||||
# 按类型统计
|
# 按类型统计
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -785,7 +785,7 @@ class CollaborationManager:
|
|||||||
""",
|
""",
|
||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
# 按实体类型统计
|
# 按实体类型统计
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -795,7 +795,7 @@ class CollaborationManager:
|
|||||||
""",
|
""",
|
||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
entity_type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
entity_type_counts = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
# 最近活跃的用户
|
# 最近活跃的用户
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -808,7 +808,7 @@ class CollaborationManager:
|
|||||||
""",
|
""",
|
||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
top_contributors = [{"name": row[0], "changes": row[1]} for row in cursor.fetchall()]
|
top_contributors = [{"name": row[0], "changes": row[1]} for row in cursor.fetchall()]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_changes": total_changes,
|
"total_changes": total_changes,
|
||||||
@@ -827,27 +827,27 @@ class CollaborationManager:
|
|||||||
user_email: str,
|
user_email: str,
|
||||||
role: str,
|
role: str,
|
||||||
invited_by: str,
|
invited_by: str,
|
||||||
permissions: list[str] | None = None,
|
permissions: list[str] | None = None,
|
||||||
) -> TeamMember:
|
) -> TeamMember:
|
||||||
"""添加团队成员"""
|
"""添加团队成员"""
|
||||||
member_id = str(uuid.uuid4())
|
member_id = str(uuid.uuid4())
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
# 根据角色设置默认权限
|
# 根据角色设置默认权限
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
permissions = self._get_default_permissions(role)
|
permissions = self._get_default_permissions(role)
|
||||||
|
|
||||||
member = TeamMember(
|
member = TeamMember(
|
||||||
id = member_id,
|
id=member_id,
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
user_id = user_id,
|
user_id=user_id,
|
||||||
user_name = user_name,
|
user_name=user_name,
|
||||||
user_email = user_email,
|
user_email=user_email,
|
||||||
role = role,
|
role=role,
|
||||||
joined_at = now,
|
joined_at=now,
|
||||||
invited_by = invited_by,
|
invited_by=invited_by,
|
||||||
last_active_at = None,
|
last_active_at=None,
|
||||||
permissions = permissions,
|
permissions=permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.db:
|
if self.db:
|
||||||
@@ -857,7 +857,7 @@ class CollaborationManager:
|
|||||||
|
|
||||||
def _get_default_permissions(self, role: str) -> list[str]:
|
def _get_default_permissions(self, role: str) -> list[str]:
|
||||||
"""获取角色的默认权限"""
|
"""获取角色的默认权限"""
|
||||||
permissions_map = {
|
permissions_map = {
|
||||||
"owner": ["read", "write", "delete", "share", "admin", "export"],
|
"owner": ["read", "write", "delete", "share", "admin", "export"],
|
||||||
"admin": ["read", "write", "delete", "share", "export"],
|
"admin": ["read", "write", "delete", "share", "export"],
|
||||||
"editor": ["read", "write", "export"],
|
"editor": ["read", "write", "export"],
|
||||||
@@ -868,7 +868,7 @@ class CollaborationManager:
|
|||||||
|
|
||||||
def _save_member_to_db(self, member: TeamMember) -> None:
|
def _save_member_to_db(self, member: TeamMember) -> None:
|
||||||
"""保存成员到数据库"""
|
"""保存成员到数据库"""
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO team_members
|
INSERT INTO team_members
|
||||||
@@ -896,7 +896,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM team_members WHERE project_id = ?
|
SELECT * FROM team_members WHERE project_id = ?
|
||||||
@@ -905,7 +905,7 @@ class CollaborationManager:
|
|||||||
(project_id, ),
|
(project_id, ),
|
||||||
)
|
)
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
members.append(self._row_to_team_member(row))
|
members.append(self._row_to_team_member(row))
|
||||||
return members
|
return members
|
||||||
@@ -913,16 +913,16 @@ class CollaborationManager:
|
|||||||
def _row_to_team_member(self, row) -> TeamMember:
|
def _row_to_team_member(self, row) -> TeamMember:
|
||||||
"""将数据库行转换为TeamMember对象"""
|
"""将数据库行转换为TeamMember对象"""
|
||||||
return TeamMember(
|
return TeamMember(
|
||||||
id = row[0],
|
id=row[0],
|
||||||
project_id = row[1],
|
project_id=row[1],
|
||||||
user_id = row[2],
|
user_id=row[2],
|
||||||
user_name = row[3],
|
user_name=row[3],
|
||||||
user_email = row[4],
|
user_email=row[4],
|
||||||
role = row[5],
|
role=row[5],
|
||||||
joined_at = row[6],
|
joined_at=row[6],
|
||||||
invited_by = row[7],
|
invited_by=row[7],
|
||||||
last_active_at = row[8],
|
last_active_at=row[8],
|
||||||
permissions = json.loads(row[9]) if row[9] else [],
|
permissions=json.loads(row[9]) if row[9] else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_member_role(self, member_id: str, new_role: str, updated_by: str) -> bool:
|
def update_member_role(self, member_id: str, new_role: str, updated_by: str) -> bool:
|
||||||
@@ -930,8 +930,8 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
permissions = self._get_default_permissions(new_role)
|
permissions = self._get_default_permissions(new_role)
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE team_members
|
UPDATE team_members
|
||||||
@@ -948,7 +948,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute("DELETE FROM team_members WHERE id = ?", (member_id, ))
|
cursor.execute("DELETE FROM team_members WHERE id = ?", (member_id, ))
|
||||||
self.db.conn.commit()
|
self.db.conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
@@ -958,7 +958,7 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT permissions FROM team_members
|
SELECT permissions FROM team_members
|
||||||
@@ -967,11 +967,11 @@ class CollaborationManager:
|
|||||||
(project_id, user_id),
|
(project_id, user_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
permissions = json.loads(row[0]) if row[0] else []
|
permissions = json.loads(row[0]) if row[0] else []
|
||||||
return permission in permissions or "admin" in permissions
|
return permission in permissions or "admin" in permissions
|
||||||
|
|
||||||
def update_last_active(self, project_id: str, user_id: str) -> None:
|
def update_last_active(self, project_id: str, user_id: str) -> None:
|
||||||
@@ -979,8 +979,8 @@ class CollaborationManager:
|
|||||||
if not self.db:
|
if not self.db:
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
cursor = self.db.conn.cursor()
|
cursor = self.db.conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE team_members
|
UPDATE team_members
|
||||||
@@ -993,12 +993,12 @@ class CollaborationManager:
|
|||||||
|
|
||||||
|
|
||||||
# 全局协作管理器实例
|
# 全局协作管理器实例
|
||||||
_collaboration_manager = None
|
_collaboration_manager = None
|
||||||
|
|
||||||
|
|
||||||
def get_collaboration_manager(db_manager = None) -> None:
|
def get_collaboration_manager(db_manager=None) -> None:
|
||||||
"""获取协作管理器单例"""
|
"""获取协作管理器单例"""
|
||||||
global _collaboration_manager
|
global _collaboration_manager
|
||||||
if _collaboration_manager is None:
|
if _collaboration_manager is None:
|
||||||
_collaboration_manager = CollaborationManager(db_manager)
|
_collaboration_manager = CollaborationManager(db_manager)
|
||||||
return _collaboration_manager
|
return _collaboration_manager
|
||||||
|
|||||||
+294
-294
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ class DocumentProcessor:
|
|||||||
"""文档处理器 - 提取 PDF/DOCX 文本"""
|
"""文档处理器 - 提取 PDF/DOCX 文本"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.supported_formats = {
|
self.supported_formats = {
|
||||||
".pdf": self._extract_pdf,
|
".pdf": self._extract_pdf,
|
||||||
".docx": self._extract_docx,
|
".docx": self._extract_docx,
|
||||||
".doc": self._extract_docx,
|
".doc": self._extract_docx,
|
||||||
@@ -31,18 +31,18 @@ class DocumentProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
{"text": "提取的文本内容", "format": "文件格式"}
|
{"text": "提取的文本内容", "format": "文件格式"}
|
||||||
"""
|
"""
|
||||||
ext = os.path.splitext(filename.lower())[1]
|
ext = os.path.splitext(filename.lower())[1]
|
||||||
|
|
||||||
if ext not in self.supported_formats:
|
if ext not in self.supported_formats:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported file format: {ext}. Supported: {list(self.supported_formats.keys())}"
|
f"Unsupported file format: {ext}. Supported: {list(self.supported_formats.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
extractor = self.supported_formats[ext]
|
extractor = self.supported_formats[ext]
|
||||||
text = extractor(content)
|
text = extractor(content)
|
||||||
|
|
||||||
# 清理文本
|
# 清理文本
|
||||||
text = self._clean_text(text)
|
text = self._clean_text(text)
|
||||||
|
|
||||||
return {"text": text, "format": ext, "filename": filename}
|
return {"text": text, "format": ext, "filename": filename}
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ class DocumentProcessor:
|
|||||||
try:
|
try:
|
||||||
import PyPDF2
|
import PyPDF2
|
||||||
|
|
||||||
pdf_file = io.BytesIO(content)
|
pdf_file = io.BytesIO(content)
|
||||||
reader = PyPDF2.PdfReader(pdf_file)
|
reader = PyPDF2.PdfReader(pdf_file)
|
||||||
|
|
||||||
text_parts = []
|
text_parts = []
|
||||||
for page in reader.pages:
|
for page in reader.pages:
|
||||||
page_text = page.extract_text()
|
page_text = page.extract_text()
|
||||||
if page_text:
|
if page_text:
|
||||||
text_parts.append(page_text)
|
text_parts.append(page_text)
|
||||||
|
|
||||||
@@ -66,10 +66,10 @@ class DocumentProcessor:
|
|||||||
try:
|
try:
|
||||||
import pdfplumber
|
import pdfplumber
|
||||||
|
|
||||||
text_parts = []
|
text_parts = []
|
||||||
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
with pdfplumber.open(io.BytesIO(content)) as pdf:
|
||||||
for page in pdf.pages:
|
for page in pdf.pages:
|
||||||
page_text = page.extract_text()
|
page_text = page.extract_text()
|
||||||
if page_text:
|
if page_text:
|
||||||
text_parts.append(page_text)
|
text_parts.append(page_text)
|
||||||
return "\n\n".join(text_parts)
|
return "\n\n".join(text_parts)
|
||||||
@@ -85,10 +85,10 @@ class DocumentProcessor:
|
|||||||
try:
|
try:
|
||||||
import docx
|
import docx
|
||||||
|
|
||||||
doc_file = io.BytesIO(content)
|
doc_file = io.BytesIO(content)
|
||||||
doc = docx.Document(doc_file)
|
doc = docx.Document(doc_file)
|
||||||
|
|
||||||
text_parts = []
|
text_parts = []
|
||||||
for para in doc.paragraphs:
|
for para in doc.paragraphs:
|
||||||
if para.text.strip():
|
if para.text.strip():
|
||||||
text_parts.append(para.text)
|
text_parts.append(para.text)
|
||||||
@@ -96,7 +96,7 @@ class DocumentProcessor:
|
|||||||
# 提取表格中的文本
|
# 提取表格中的文本
|
||||||
for table in doc.tables:
|
for table in doc.tables:
|
||||||
for row in table.rows:
|
for row in table.rows:
|
||||||
row_text = []
|
row_text = []
|
||||||
for cell in row.cells:
|
for cell in row.cells:
|
||||||
if cell.text.strip():
|
if cell.text.strip():
|
||||||
row_text.append(cell.text.strip())
|
row_text.append(cell.text.strip())
|
||||||
@@ -114,7 +114,7 @@ class DocumentProcessor:
|
|||||||
def _extract_txt(self, content: bytes) -> str:
|
def _extract_txt(self, content: bytes) -> str:
|
||||||
"""提取纯文本"""
|
"""提取纯文本"""
|
||||||
# 尝试多种编码
|
# 尝试多种编码
|
||||||
encodings = ["utf-8", "gbk", "gb2312", "latin-1"]
|
encodings = ["utf-8", "gbk", "gb2312", "latin-1"]
|
||||||
|
|
||||||
for encoding in encodings:
|
for encoding in encodings:
|
||||||
try:
|
try:
|
||||||
@@ -123,7 +123,7 @@ class DocumentProcessor:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 如果都失败了,使用 latin-1 并忽略错误
|
# 如果都失败了,使用 latin-1 并忽略错误
|
||||||
return content.decode("latin-1", errors = "ignore")
|
return content.decode("latin-1", errors="ignore")
|
||||||
|
|
||||||
def _clean_text(self, text: str) -> str:
|
def _clean_text(self, text: str) -> str:
|
||||||
"""清理提取的文本"""
|
"""清理提取的文本"""
|
||||||
@@ -131,29 +131,29 @@ class DocumentProcessor:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
# 移除多余的空白字符
|
# 移除多余的空白字符
|
||||||
lines = text.split("\n")
|
lines = text.split("\n")
|
||||||
cleaned_lines = []
|
cleaned_lines = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
# 移除空行,但保留段落分隔
|
# 移除空行,但保留段落分隔
|
||||||
if line:
|
if line:
|
||||||
cleaned_lines.append(line)
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
# 合并行,保留段落结构
|
# 合并行,保留段落结构
|
||||||
text = "\n\n".join(cleaned_lines)
|
text = "\n\n".join(cleaned_lines)
|
||||||
|
|
||||||
# 移除多余的空格
|
# 移除多余的空格
|
||||||
text = " ".join(text.split())
|
text = " ".join(text.split())
|
||||||
|
|
||||||
# 移除控制字符
|
# 移除控制字符
|
||||||
text = "".join(char for char in text if ord(char) >= 32 or char in "\n\r\t")
|
text = "".join(char for char in text if ord(char) >= 32 or char in "\n\r\t")
|
||||||
|
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
def is_supported(self, filename: str) -> bool:
|
def is_supported(self, filename: str) -> bool:
|
||||||
"""检查文件格式是否支持"""
|
"""检查文件格式是否支持"""
|
||||||
ext = os.path.splitext(filename.lower())[1]
|
ext = os.path.splitext(filename.lower())[1]
|
||||||
return ext in self.supported_formats
|
return ext in self.supported_formats
|
||||||
|
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ class SimpleTextExtractor:
|
|||||||
|
|
||||||
def extract(self, content: bytes, filename: str) -> str:
|
def extract(self, content: bytes, filename: str) -> str:
|
||||||
"""尝试提取文本"""
|
"""尝试提取文本"""
|
||||||
encodings = ["utf-8", "gbk", "latin-1"]
|
encodings = ["utf-8", "gbk", "latin-1"]
|
||||||
|
|
||||||
for encoding in encodings:
|
for encoding in encodings:
|
||||||
try:
|
try:
|
||||||
@@ -173,15 +173,15 @@ class SimpleTextExtractor:
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return content.decode("latin-1", errors = "ignore")
|
return content.decode("latin-1", errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 测试
|
# 测试
|
||||||
processor = DocumentProcessor()
|
processor = DocumentProcessor()
|
||||||
|
|
||||||
# 测试文本提取
|
# 测试文本提取
|
||||||
test_text = "Hello World\n\nThis is a test document.\n\nMultiple paragraphs."
|
test_text = "Hello World\n\nThis is a test document.\n\nMultiple paragraphs."
|
||||||
result = processor.process(test_text.encode("utf-8"), "test.txt")
|
result = processor.process(test_text.encode("utf-8"), "test.txt")
|
||||||
print(f"Text extraction test: {len(result['text'])} chars")
|
print(f"Text extraction test: {len(result['text'])} chars")
|
||||||
print(result["text"][:100])
|
print(result["text"][:100])
|
||||||
|
|||||||
+451
-451
File diff suppressed because it is too large
Load Diff
+68
-68
@@ -12,8 +12,8 @@ import httpx
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# API Keys
|
# API Keys
|
||||||
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
||||||
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -27,9 +27,9 @@ class EntityEmbedding:
|
|||||||
class EntityAligner:
|
class EntityAligner:
|
||||||
"""实体对齐器 - 使用 embedding 进行相似度匹配"""
|
"""实体对齐器 - 使用 embedding 进行相似度匹配"""
|
||||||
|
|
||||||
def __init__(self, similarity_threshold: float = 0.85) -> None:
|
def __init__(self, similarity_threshold: float = 0.85) -> None:
|
||||||
self.similarity_threshold = similarity_threshold
|
self.similarity_threshold = similarity_threshold
|
||||||
self.embedding_cache: dict[str, list[float]] = {}
|
self.embedding_cache: dict[str, list[float]] = {}
|
||||||
|
|
||||||
def get_embedding(self, text: str) -> list[float] | None:
|
def get_embedding(self, text: str) -> list[float] | None:
|
||||||
"""
|
"""
|
||||||
@@ -45,25 +45,25 @@ class EntityAligner:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 检查缓存
|
# 检查缓存
|
||||||
cache_key = hash(text)
|
cache_key = hash(text)
|
||||||
if cache_key in self.embedding_cache:
|
if cache_key in self.embedding_cache:
|
||||||
return self.embedding_cache[cache_key]
|
return self.embedding_cache[cache_key]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
f"{KIMI_BASE_URL}/v1/embeddings",
|
f"{KIMI_BASE_URL}/v1/embeddings",
|
||||||
headers = {
|
headers={
|
||||||
"Authorization": f"Bearer {KIMI_API_KEY}",
|
"Authorization": f"Bearer {KIMI_API_KEY}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
json = {"model": "k2p5", "input": text[:500]}, # 限制长度
|
json={"model": "k2p5", "input": text[:500]}, # 限制长度
|
||||||
timeout = 30.0,
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
|
||||||
embedding = result["data"][0]["embedding"]
|
embedding = result["data"][0]["embedding"]
|
||||||
self.embedding_cache[cache_key] = embedding
|
self.embedding_cache[cache_key] = embedding
|
||||||
return embedding
|
return embedding
|
||||||
|
|
||||||
except (httpx.HTTPError, json.JSONDecodeError, KeyError) as e:
|
except (httpx.HTTPError, json.JSONDecodeError, KeyError) as e:
|
||||||
@@ -81,20 +81,20 @@ class EntityAligner:
|
|||||||
Returns:
|
Returns:
|
||||||
相似度分数 (0-1)
|
相似度分数 (0-1)
|
||||||
"""
|
"""
|
||||||
vec1 = np.array(embedding1)
|
vec1 = np.array(embedding1)
|
||||||
vec2 = np.array(embedding2)
|
vec2 = np.array(embedding2)
|
||||||
|
|
||||||
# 余弦相似度
|
# 余弦相似度
|
||||||
dot_product = np.dot(vec1, vec2)
|
dot_product = np.dot(vec1, vec2)
|
||||||
norm1 = np.linalg.norm(vec1)
|
norm1 = np.linalg.norm(vec1)
|
||||||
norm2 = np.linalg.norm(vec2)
|
norm2 = np.linalg.norm(vec2)
|
||||||
|
|
||||||
if norm1 == 0 or norm2 == 0:
|
if norm1 == 0 or norm2 == 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
return float(dot_product / (norm1 * norm2))
|
return float(dot_product / (norm1 * norm2))
|
||||||
|
|
||||||
def get_entity_text(self, name: str, definition: str = "") -> str:
|
def get_entity_text(self, name: str, definition: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
构建用于 embedding 的实体文本
|
构建用于 embedding 的实体文本
|
||||||
|
|
||||||
@@ -113,9 +113,9 @@ class EntityAligner:
|
|||||||
self,
|
self,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
definition: str = "",
|
definition: str = "",
|
||||||
exclude_id: str | None = None,
|
exclude_id: str | None = None,
|
||||||
threshold: float | None = None,
|
threshold: float | None = None,
|
||||||
) -> object | None:
|
) -> object | None:
|
||||||
"""
|
"""
|
||||||
查找相似的实体
|
查找相似的实体
|
||||||
@@ -131,54 +131,54 @@ class EntityAligner:
|
|||||||
相似的实体或 None
|
相似的实体或 None
|
||||||
"""
|
"""
|
||||||
if threshold is None:
|
if threshold is None:
|
||||||
threshold = self.similarity_threshold
|
threshold = self.similarity_threshold
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from db_manager import get_db_manager
|
from db_manager import get_db_manager
|
||||||
|
|
||||||
db = get_db_manager()
|
db = get_db_manager()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 获取项目的所有实体
|
# 获取项目的所有实体
|
||||||
entities = db.get_all_entities_for_embedding(project_id)
|
entities = db.get_all_entities_for_embedding(project_id)
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 获取查询实体的 embedding
|
# 获取查询实体的 embedding
|
||||||
query_text = self.get_entity_text(name, definition)
|
query_text = self.get_entity_text(name, definition)
|
||||||
query_embedding = self.get_embedding(query_text)
|
query_embedding = self.get_embedding(query_text)
|
||||||
|
|
||||||
if query_embedding is None:
|
if query_embedding is None:
|
||||||
# 如果 embedding API 失败,回退到简单匹配
|
# 如果 embedding API 失败,回退到简单匹配
|
||||||
return self._fallback_similarity_match(entities, name, exclude_id)
|
return self._fallback_similarity_match(entities, name, exclude_id)
|
||||||
|
|
||||||
best_match = None
|
best_match = None
|
||||||
best_score = threshold
|
best_score = threshold
|
||||||
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
if exclude_id and entity.id == exclude_id:
|
if exclude_id and entity.id == exclude_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 获取实体的 embedding
|
# 获取实体的 embedding
|
||||||
entity_text = self.get_entity_text(entity.name, entity.definition)
|
entity_text = self.get_entity_text(entity.name, entity.definition)
|
||||||
entity_embedding = self.get_embedding(entity_text)
|
entity_embedding = self.get_embedding(entity_text)
|
||||||
|
|
||||||
if entity_embedding is None:
|
if entity_embedding is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 计算相似度
|
# 计算相似度
|
||||||
similarity = self.compute_similarity(query_embedding, entity_embedding)
|
similarity = self.compute_similarity(query_embedding, entity_embedding)
|
||||||
|
|
||||||
if similarity > best_score:
|
if similarity > best_score:
|
||||||
best_score = similarity
|
best_score = similarity
|
||||||
best_match = entity
|
best_match = entity
|
||||||
|
|
||||||
return best_match
|
return best_match
|
||||||
|
|
||||||
def _fallback_similarity_match(
|
def _fallback_similarity_match(
|
||||||
self, entities: list[object], name: str, exclude_id: str | None = None
|
self, entities: list[object], name: str, exclude_id: str | None = None
|
||||||
) -> object | None:
|
) -> object | None:
|
||||||
"""
|
"""
|
||||||
回退到简单的相似度匹配(不使用 embedding)
|
回退到简单的相似度匹配(不使用 embedding)
|
||||||
@@ -191,7 +191,7 @@ class EntityAligner:
|
|||||||
Returns:
|
Returns:
|
||||||
最相似的实体或 None
|
最相似的实体或 None
|
||||||
"""
|
"""
|
||||||
name_lower = name.lower()
|
name_lower = name.lower()
|
||||||
|
|
||||||
# 1. 精确匹配
|
# 1. 精确匹配
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
@@ -212,7 +212,7 @@ class EntityAligner:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def batch_align_entities(
|
def batch_align_entities(
|
||||||
self, project_id: str, new_entities: list[dict], threshold: float | None = None
|
self, project_id: str, new_entities: list[dict], threshold: float | None = None
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
批量对齐实体
|
批量对齐实体
|
||||||
@@ -226,16 +226,16 @@ class EntityAligner:
|
|||||||
对齐结果列表 [{"new_entity": {...}, "matched_entity": {...}, "similarity": 0.9}]
|
对齐结果列表 [{"new_entity": {...}, "matched_entity": {...}, "similarity": 0.9}]
|
||||||
"""
|
"""
|
||||||
if threshold is None:
|
if threshold is None:
|
||||||
threshold = self.similarity_threshold
|
threshold = self.similarity_threshold
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for new_ent in new_entities:
|
for new_ent in new_entities:
|
||||||
matched = self.find_similar_entity(
|
matched = self.find_similar_entity(
|
||||||
project_id, new_ent["name"], new_ent.get("definition", ""), threshold = threshold
|
project_id, new_ent["name"], new_ent.get("definition", ""), threshold=threshold
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"new_entity": new_ent,
|
"new_entity": new_ent,
|
||||||
"matched_entity": None,
|
"matched_entity": None,
|
||||||
"similarity": 0.0,
|
"similarity": 0.0,
|
||||||
@@ -244,28 +244,28 @@ class EntityAligner:
|
|||||||
|
|
||||||
if matched:
|
if matched:
|
||||||
# 计算相似度
|
# 计算相似度
|
||||||
query_text = self.get_entity_text(new_ent["name"], new_ent.get("definition", ""))
|
query_text = self.get_entity_text(new_ent["name"], new_ent.get("definition", ""))
|
||||||
matched_text = self.get_entity_text(matched.name, matched.definition)
|
matched_text = self.get_entity_text(matched.name, matched.definition)
|
||||||
|
|
||||||
query_emb = self.get_embedding(query_text)
|
query_emb = self.get_embedding(query_text)
|
||||||
matched_emb = self.get_embedding(matched_text)
|
matched_emb = self.get_embedding(matched_text)
|
||||||
|
|
||||||
if query_emb and matched_emb:
|
if query_emb and matched_emb:
|
||||||
similarity = self.compute_similarity(query_emb, matched_emb)
|
similarity = self.compute_similarity(query_emb, matched_emb)
|
||||||
result["matched_entity"] = {
|
result["matched_entity"] = {
|
||||||
"id": matched.id,
|
"id": matched.id,
|
||||||
"name": matched.name,
|
"name": matched.name,
|
||||||
"type": matched.type,
|
"type": matched.type,
|
||||||
"definition": matched.definition,
|
"definition": matched.definition,
|
||||||
}
|
}
|
||||||
result["similarity"] = similarity
|
result["similarity"] = similarity
|
||||||
result["should_merge"] = similarity >= threshold
|
result["should_merge"] = similarity >= threshold
|
||||||
|
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def suggest_entity_aliases(self, entity_name: str, entity_definition: str = "") -> list[str]:
|
def suggest_entity_aliases(self, entity_name: str, entity_definition: str = "") -> list[str]:
|
||||||
"""
|
"""
|
||||||
使用 LLM 建议实体的别名
|
使用 LLM 建议实体的别名
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ class EntityAligner:
|
|||||||
if not KIMI_API_KEY:
|
if not KIMI_API_KEY:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
prompt = f"""为以下实体生成可能的别名或简称:
|
prompt = f"""为以下实体生成可能的别名或简称:
|
||||||
|
|
||||||
实体名称:{entity_name}
|
实体名称:{entity_name}
|
||||||
定义:{entity_definition}
|
定义:{entity_definition}
|
||||||
@@ -290,28 +290,28 @@ class EntityAligner:
|
|||||||
只返回 JSON,不要其他内容。"""
|
只返回 JSON,不要其他内容。"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
f"{KIMI_BASE_URL}/v1/chat/completions",
|
f"{KIMI_BASE_URL}/v1/chat/completions",
|
||||||
headers = {
|
headers={
|
||||||
"Authorization": f"Bearer {KIMI_API_KEY}",
|
"Authorization": f"Bearer {KIMI_API_KEY}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
json = {
|
json={
|
||||||
"model": "k2p5",
|
"model": "k2p5",
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
},
|
},
|
||||||
timeout = 30.0,
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
content = result["choices"][0]["message"]["content"]
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
if json_match:
|
if json_match:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
return data.get("aliases", [])
|
return data.get("aliases", [])
|
||||||
except (httpx.HTTPError, json.JSONDecodeError, KeyError) as e:
|
except (httpx.HTTPError, json.JSONDecodeError, KeyError) as e:
|
||||||
print(f"Alias suggestion failed: {e}")
|
print(f"Alias suggestion failed: {e}")
|
||||||
@@ -340,8 +340,8 @@ def simple_similarity(str1: str, str2: str) -> float:
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# 转换为小写
|
# 转换为小写
|
||||||
s1 = str1.lower()
|
s1 = str1.lower()
|
||||||
s2 = str2.lower()
|
s2 = str2.lower()
|
||||||
|
|
||||||
# 包含关系
|
# 包含关系
|
||||||
if s1 in s2 or s2 in s1:
|
if s1 in s2 or s2 in s1:
|
||||||
@@ -355,11 +355,11 @@ def simple_similarity(str1: str, str2: str) -> float:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 测试
|
# 测试
|
||||||
aligner = EntityAligner()
|
aligner = EntityAligner()
|
||||||
|
|
||||||
# 测试 embedding
|
# 测试 embedding
|
||||||
test_text = "Kubernetes 容器编排平台"
|
test_text = "Kubernetes 容器编排平台"
|
||||||
embedding = aligner.get_embedding(test_text)
|
embedding = aligner.get_embedding(test_text)
|
||||||
if embedding:
|
if embedding:
|
||||||
print(f"Embedding dimension: {len(embedding)}")
|
print(f"Embedding dimension: {len(embedding)}")
|
||||||
print(f"First 5 values: {embedding[:5]}")
|
print(f"First 5 values: {embedding[:5]}")
|
||||||
@@ -367,7 +367,7 @@ if __name__ == "__main__":
|
|||||||
print("Embedding API not available")
|
print("Embedding API not available")
|
||||||
|
|
||||||
# 测试相似度计算
|
# 测试相似度计算
|
||||||
emb1 = [1.0, 0.0, 0.0]
|
emb1 = [1.0, 0.0, 0.0]
|
||||||
emb2 = [0.9, 0.1, 0.0]
|
emb2 = [0.9, 0.1, 0.0]
|
||||||
sim = aligner.compute_similarity(emb1, emb2)
|
sim = aligner.compute_similarity(emb1, emb2)
|
||||||
print(f"Similarity: {sim:.4f}")
|
print(f"Similarity: {sim:.4f}")
|
||||||
|
|||||||
+105
-105
@@ -14,9 +14,9 @@ from typing import Any
|
|||||||
try:
|
try:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
PANDAS_AVAILABLE = True
|
PANDAS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PANDAS_AVAILABLE = False
|
PANDAS_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
@@ -32,9 +32,9 @@ try:
|
|||||||
TableStyle,
|
TableStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
REPORTLAB_AVAILABLE = True
|
REPORTLAB_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
REPORTLAB_AVAILABLE = False
|
REPORTLAB_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -71,8 +71,8 @@ class ExportTranscript:
|
|||||||
class ExportManager:
|
class ExportManager:
|
||||||
"""导出管理器 - 处理各种导出需求"""
|
"""导出管理器 - 处理各种导出需求"""
|
||||||
|
|
||||||
def __init__(self, db_manager = None) -> None:
|
def __init__(self, db_manager=None) -> None:
|
||||||
self.db = db_manager
|
self.db = db_manager
|
||||||
|
|
||||||
def export_knowledge_graph_svg(
|
def export_knowledge_graph_svg(
|
||||||
self, project_id: str, entities: list[ExportEntity], relations: list[ExportRelation]
|
self, project_id: str, entities: list[ExportEntity], relations: list[ExportRelation]
|
||||||
@@ -84,21 +84,21 @@ class ExportManager:
|
|||||||
SVG 字符串
|
SVG 字符串
|
||||||
"""
|
"""
|
||||||
# 计算布局参数
|
# 计算布局参数
|
||||||
width = 1200
|
width = 1200
|
||||||
height = 800
|
height = 800
|
||||||
center_x = width / 2
|
center_x = width / 2
|
||||||
center_y = height / 2
|
center_y = height / 2
|
||||||
radius = 300
|
radius = 300
|
||||||
|
|
||||||
# 按类型分组实体
|
# 按类型分组实体
|
||||||
entities_by_type = {}
|
entities_by_type = {}
|
||||||
for e in entities:
|
for e in entities:
|
||||||
if e.type not in entities_by_type:
|
if e.type not in entities_by_type:
|
||||||
entities_by_type[e.type] = []
|
entities_by_type[e.type] = []
|
||||||
entities_by_type[e.type].append(e)
|
entities_by_type[e.type].append(e)
|
||||||
|
|
||||||
# 颜色映射
|
# 颜色映射
|
||||||
type_colors = {
|
type_colors = {
|
||||||
"PERSON": "#FF6B6B",
|
"PERSON": "#FF6B6B",
|
||||||
"ORGANIZATION": "#4ECDC4",
|
"ORGANIZATION": "#4ECDC4",
|
||||||
"LOCATION": "#45B7D1",
|
"LOCATION": "#45B7D1",
|
||||||
@@ -110,17 +110,17 @@ class ExportManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 计算实体位置
|
# 计算实体位置
|
||||||
entity_positions = {}
|
entity_positions = {}
|
||||||
angle_step = 2 * 3.14159 / max(len(entities), 1)
|
angle_step = 2 * 3.14159 / max(len(entities), 1)
|
||||||
|
|
||||||
for i, entity in enumerate(entities):
|
for i, entity in enumerate(entities):
|
||||||
i * angle_step
|
i * angle_step
|
||||||
x = center_x + radius * 0.8 * (i % 3 - 1) * 150 + (i // 3) * 50
|
x = center_x + radius * 0.8 * (i % 3 - 1) * 150 + (i // 3) * 50
|
||||||
y = center_y + radius * 0.6 * ((i % 6) - 3) * 80
|
y = center_y + radius * 0.6 * ((i % 6) - 3) * 80
|
||||||
entity_positions[entity.id] = (x, y)
|
entity_positions[entity.id] = (x, y)
|
||||||
|
|
||||||
# 生成 SVG
|
# 生成 SVG
|
||||||
svg_parts = [
|
svg_parts = [
|
||||||
f'<svg xmlns = "http://www.w3.org/2000/svg" width = "{width}" height = "{height}" '
|
f'<svg xmlns = "http://www.w3.org/2000/svg" width = "{width}" height = "{height}" '
|
||||||
f'viewBox = "0 0 {width} {height}">',
|
f'viewBox = "0 0 {width} {height}">',
|
||||||
"<defs>",
|
"<defs>",
|
||||||
@@ -137,17 +137,17 @@ class ExportManager:
|
|||||||
# 绘制关系连线
|
# 绘制关系连线
|
||||||
for rel in relations:
|
for rel in relations:
|
||||||
if rel.source in entity_positions and rel.target in entity_positions:
|
if rel.source in entity_positions and rel.target in entity_positions:
|
||||||
x1, y1 = entity_positions[rel.source]
|
x1, y1 = entity_positions[rel.source]
|
||||||
x2, y2 = entity_positions[rel.target]
|
x2, y2 = entity_positions[rel.target]
|
||||||
|
|
||||||
# 计算箭头终点(避免覆盖节点)
|
# 计算箭头终点(避免覆盖节点)
|
||||||
dx = x2 - x1
|
dx = x2 - x1
|
||||||
dy = y2 - y1
|
dy = y2 - y1
|
||||||
dist = (dx**2 + dy**2) ** 0.5
|
dist = (dx**2 + dy**2) ** 0.5
|
||||||
if dist > 0:
|
if dist > 0:
|
||||||
offset = 40
|
offset = 40
|
||||||
x2 = x2 - dx * offset / dist
|
x2 = x2 - dx * offset / dist
|
||||||
y2 = y2 - dy * offset / dist
|
y2 = y2 - dy * offset / dist
|
||||||
|
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
f'<line x1 = "{x1}" y1 = "{y1}" x2 = "{x2}" y2 = "{y2}" '
|
f'<line x1 = "{x1}" y1 = "{y1}" x2 = "{x2}" y2 = "{y2}" '
|
||||||
@@ -155,8 +155,8 @@ class ExportManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 关系标签
|
# 关系标签
|
||||||
mid_x = (x1 + x2) / 2
|
mid_x = (x1 + x2) / 2
|
||||||
mid_y = (y1 + y2) / 2
|
mid_y = (y1 + y2) / 2
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
f'<rect x = "{mid_x - 30}" y = "{mid_y - 10}" width = "60" height = "20" '
|
f'<rect x = "{mid_x - 30}" y = "{mid_y - 10}" width = "60" height = "20" '
|
||||||
f'fill = "white" stroke = "#bdc3c7" rx = "3"/>'
|
f'fill = "white" stroke = "#bdc3c7" rx = "3"/>'
|
||||||
@@ -169,8 +169,8 @@ class ExportManager:
|
|||||||
# 绘制实体节点
|
# 绘制实体节点
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
if entity.id in entity_positions:
|
if entity.id in entity_positions:
|
||||||
x, y = entity_positions[entity.id]
|
x, y = entity_positions[entity.id]
|
||||||
color = type_colors.get(entity.type, type_colors["default"])
|
color = type_colors.get(entity.type, type_colors["default"])
|
||||||
|
|
||||||
# 节点圆圈
|
# 节点圆圈
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
@@ -190,11 +190,11 @@ class ExportManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 图例
|
# 图例
|
||||||
legend_x = width - 150
|
legend_x = width - 150
|
||||||
legend_y = 80
|
legend_y = 80
|
||||||
rect_x = legend_x - 10
|
rect_x = legend_x - 10
|
||||||
rect_y = legend_y - 20
|
rect_y = legend_y - 20
|
||||||
rect_height = len(type_colors) * 25 + 10
|
rect_height = len(type_colors) * 25 + 10
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
f'<rect x = "{rect_x}" y = "{rect_y}" width = "140" height = "{rect_height}" '
|
f'<rect x = "{rect_x}" y = "{rect_y}" width = "140" height = "{rect_height}" '
|
||||||
f'fill = "white" stroke = "#bdc3c7" rx = "5"/>'
|
f'fill = "white" stroke = "#bdc3c7" rx = "5"/>'
|
||||||
@@ -206,11 +206,11 @@ class ExportManager:
|
|||||||
|
|
||||||
for i, (etype, color) in enumerate(type_colors.items()):
|
for i, (etype, color) in enumerate(type_colors.items()):
|
||||||
if etype != "default":
|
if etype != "default":
|
||||||
y_pos = legend_y + 25 + i * 20
|
y_pos = legend_y + 25 + i * 20
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
f'<circle cx = "{legend_x + 10}" cy = "{y_pos}" r = "8" fill = "{color}"/>'
|
f'<circle cx = "{legend_x + 10}" cy = "{y_pos}" r = "8" fill = "{color}"/>'
|
||||||
)
|
)
|
||||||
text_y = y_pos + 4
|
text_y = y_pos + 4
|
||||||
svg_parts.append(
|
svg_parts.append(
|
||||||
f'<text x = "{legend_x + 25}" y = "{text_y}" font-size = "10" '
|
f'<text x = "{legend_x + 25}" y = "{text_y}" font-size = "10" '
|
||||||
f'fill = "#2c3e50">{etype}</text>'
|
f'fill = "#2c3e50">{etype}</text>'
|
||||||
@@ -231,12 +231,12 @@ class ExportManager:
|
|||||||
try:
|
try:
|
||||||
import cairosvg
|
import cairosvg
|
||||||
|
|
||||||
svg_content = self.export_knowledge_graph_svg(project_id, entities, relations)
|
svg_content = self.export_knowledge_graph_svg(project_id, entities, relations)
|
||||||
png_bytes = cairosvg.svg2png(bytestring = svg_content.encode("utf-8"))
|
png_bytes = cairosvg.svg2png(bytestring=svg_content.encode("utf-8"))
|
||||||
return png_bytes
|
return png_bytes
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# 如果没有 cairosvg,返回 SVG 的 base64
|
# 如果没有 cairosvg,返回 SVG 的 base64
|
||||||
svg_content = self.export_knowledge_graph_svg(project_id, entities, relations)
|
svg_content = self.export_knowledge_graph_svg(project_id, entities, relations)
|
||||||
return base64.b64encode(svg_content.encode("utf-8"))
|
return base64.b64encode(svg_content.encode("utf-8"))
|
||||||
|
|
||||||
def export_entities_excel(self, entities: list[ExportEntity]) -> bytes:
|
def export_entities_excel(self, entities: list[ExportEntity]) -> bytes:
|
||||||
@@ -250,9 +250,9 @@ class ExportManager:
|
|||||||
raise ImportError("pandas is required for Excel export")
|
raise ImportError("pandas is required for Excel export")
|
||||||
|
|
||||||
# 准备数据
|
# 准备数据
|
||||||
data = []
|
data = []
|
||||||
for e in entities:
|
for e in entities:
|
||||||
row = {
|
row = {
|
||||||
"ID": e.id,
|
"ID": e.id,
|
||||||
"名称": e.name,
|
"名称": e.name,
|
||||||
"类型": e.type,
|
"类型": e.type,
|
||||||
@@ -262,29 +262,29 @@ class ExportManager:
|
|||||||
}
|
}
|
||||||
# 添加属性
|
# 添加属性
|
||||||
for attr_name, attr_value in e.attributes.items():
|
for attr_name, attr_value in e.attributes.items():
|
||||||
row[f"属性:{attr_name}"] = attr_value
|
row[f"属性:{attr_name}"] = attr_value
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
# 写入 Excel
|
# 写入 Excel
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
with pd.ExcelWriter(output, engine = "openpyxl") as writer:
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||||
df.to_excel(writer, sheet_name = "实体列表", index = False)
|
df.to_excel(writer, sheet_name="实体列表", index=False)
|
||||||
|
|
||||||
# 调整列宽
|
# 调整列宽
|
||||||
worksheet = writer.sheets["实体列表"]
|
worksheet = writer.sheets["实体列表"]
|
||||||
for column in worksheet.columns:
|
for column in worksheet.columns:
|
||||||
max_length = 0
|
max_length = 0
|
||||||
column_letter = column[0].column_letter
|
column_letter = column[0].column_letter
|
||||||
for cell in column:
|
for cell in column:
|
||||||
try:
|
try:
|
||||||
if len(str(cell.value)) > max_length:
|
if len(str(cell.value)) > max_length:
|
||||||
max_length = len(str(cell.value))
|
max_length = len(str(cell.value))
|
||||||
except (AttributeError, TypeError, ValueError):
|
except (AttributeError, TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
adjusted_width = min(max_length + 2, 50)
|
adjusted_width = min(max_length + 2, 50)
|
||||||
worksheet.column_dimensions[column_letter].width = adjusted_width
|
worksheet.column_dimensions[column_letter].width = adjusted_width
|
||||||
|
|
||||||
return output.getvalue()
|
return output.getvalue()
|
||||||
|
|
||||||
@@ -295,24 +295,24 @@ class ExportManager:
|
|||||||
Returns:
|
Returns:
|
||||||
CSV 字符串
|
CSV 字符串
|
||||||
"""
|
"""
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
|
|
||||||
# 收集所有可能的属性列
|
# 收集所有可能的属性列
|
||||||
all_attrs = set()
|
all_attrs = set()
|
||||||
for e in entities:
|
for e in entities:
|
||||||
all_attrs.update(e.attributes.keys())
|
all_attrs.update(e.attributes.keys())
|
||||||
|
|
||||||
# 表头
|
# 表头
|
||||||
headers = ["ID", "名称", "类型", "定义", "别名", "提及次数"] + [
|
headers = ["ID", "名称", "类型", "定义", "别名", "提及次数"] + [
|
||||||
f"属性:{a}" for a in sorted(all_attrs)
|
f"属性:{a}" for a in sorted(all_attrs)
|
||||||
]
|
]
|
||||||
|
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
writer.writerow(headers)
|
writer.writerow(headers)
|
||||||
|
|
||||||
# 数据行
|
# 数据行
|
||||||
for e in entities:
|
for e in entities:
|
||||||
row = [e.id, e.name, e.type, e.definition, ", ".join(e.aliases), e.mention_count]
|
row = [e.id, e.name, e.type, e.definition, ", ".join(e.aliases), e.mention_count]
|
||||||
for attr in sorted(all_attrs):
|
for attr in sorted(all_attrs):
|
||||||
row.append(e.attributes.get(attr, ""))
|
row.append(e.attributes.get(attr, ""))
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
@@ -327,8 +327,8 @@ class ExportManager:
|
|||||||
CSV 字符串
|
CSV 字符串
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
writer.writerow(["ID", "源实体", "目标实体", "关系类型", "置信度", "证据"])
|
writer.writerow(["ID", "源实体", "目标实体", "关系类型", "置信度", "证据"])
|
||||||
|
|
||||||
for r in relations:
|
for r in relations:
|
||||||
@@ -345,7 +345,7 @@ class ExportManager:
|
|||||||
Returns:
|
Returns:
|
||||||
Markdown 字符串
|
Markdown 字符串
|
||||||
"""
|
"""
|
||||||
lines = [
|
lines = [
|
||||||
f"# {transcript.name}",
|
f"# {transcript.name}",
|
||||||
"",
|
"",
|
||||||
f"**类型**: {transcript.type}",
|
f"**类型**: {transcript.type}",
|
||||||
@@ -369,10 +369,10 @@ class ExportManager:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
for seg in transcript.segments:
|
for seg in transcript.segments:
|
||||||
speaker = seg.get("speaker", "Unknown")
|
speaker = seg.get("speaker", "Unknown")
|
||||||
start = seg.get("start", 0)
|
start = seg.get("start", 0)
|
||||||
end = seg.get("end", 0)
|
end = seg.get("end", 0)
|
||||||
text = seg.get("text", "")
|
text = seg.get("text", "")
|
||||||
lines.append(f"**[{start:.1f}s - {end:.1f}s] {speaker}**: {text}")
|
lines.append(f"**[{start:.1f}s - {end:.1f}s] {speaker}**: {text}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
@@ -387,12 +387,12 @@ class ExportManager:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
for mention in transcript.entity_mentions:
|
for mention in transcript.entity_mentions:
|
||||||
entity_id = mention.get("entity_id", "")
|
entity_id = mention.get("entity_id", "")
|
||||||
entity = entities_map.get(entity_id)
|
entity = entities_map.get(entity_id)
|
||||||
entity_name = entity.name if entity else mention.get("entity_name", "Unknown")
|
entity_name = entity.name if entity else mention.get("entity_name", "Unknown")
|
||||||
entity_type = entity.type if entity else "Unknown"
|
entity_type = entity.type if entity else "Unknown"
|
||||||
position = mention.get("position", "")
|
position = mention.get("position", "")
|
||||||
context = mention.get("context", "")[:50] + "..." if mention.get("context") else ""
|
context = mention.get("context", "")[:50] + "..." if mention.get("context") else ""
|
||||||
lines.append(f"| {entity_name} | {entity_type} | {position} | {context} |")
|
lines.append(f"| {entity_name} | {entity_type} | {position} | {context} |")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@@ -404,7 +404,7 @@ class ExportManager:
|
|||||||
entities: list[ExportEntity],
|
entities: list[ExportEntity],
|
||||||
relations: list[ExportRelation],
|
relations: list[ExportRelation],
|
||||||
transcripts: list[ExportTranscript],
|
transcripts: list[ExportTranscript],
|
||||||
summary: str = "",
|
summary: str = "",
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
导出项目报告为 PDF 格式
|
导出项目报告为 PDF 格式
|
||||||
@@ -415,29 +415,29 @@ class ExportManager:
|
|||||||
if not REPORTLAB_AVAILABLE:
|
if not REPORTLAB_AVAILABLE:
|
||||||
raise ImportError("reportlab is required for PDF export")
|
raise ImportError("reportlab is required for PDF export")
|
||||||
|
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
doc = SimpleDocTemplate(
|
doc = SimpleDocTemplate(
|
||||||
output, pagesize = A4, rightMargin = 72, leftMargin = 72, topMargin = 72, bottomMargin = 18
|
output, pagesize=A4, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=18
|
||||||
)
|
)
|
||||||
|
|
||||||
# 样式
|
# 样式
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
title_style = ParagraphStyle(
|
title_style = ParagraphStyle(
|
||||||
"CustomTitle",
|
"CustomTitle",
|
||||||
parent = styles["Heading1"],
|
parent=styles["Heading1"],
|
||||||
fontSize = 24,
|
fontSize=24,
|
||||||
spaceAfter = 30,
|
spaceAfter=30,
|
||||||
textColor = colors.HexColor("#2c3e50"),
|
textColor=colors.HexColor("#2c3e50"),
|
||||||
)
|
)
|
||||||
heading_style = ParagraphStyle(
|
heading_style = ParagraphStyle(
|
||||||
"CustomHeading",
|
"CustomHeading",
|
||||||
parent = styles["Heading2"],
|
parent=styles["Heading2"],
|
||||||
fontSize = 16,
|
fontSize=16,
|
||||||
spaceAfter = 12,
|
spaceAfter=12,
|
||||||
textColor = colors.HexColor("#34495e"),
|
textColor=colors.HexColor("#34495e"),
|
||||||
)
|
)
|
||||||
|
|
||||||
story = []
|
story = []
|
||||||
|
|
||||||
# 标题页
|
# 标题页
|
||||||
story.append(Paragraph("InsightFlow 项目报告", title_style))
|
story.append(Paragraph("InsightFlow 项目报告", title_style))
|
||||||
@@ -452,7 +452,7 @@ class ExportManager:
|
|||||||
|
|
||||||
# 统计概览
|
# 统计概览
|
||||||
story.append(Paragraph("项目概览", heading_style))
|
story.append(Paragraph("项目概览", heading_style))
|
||||||
stats_data = [
|
stats_data = [
|
||||||
["指标", "数值"],
|
["指标", "数值"],
|
||||||
["实体数量", str(len(entities))],
|
["实体数量", str(len(entities))],
|
||||||
["关系数量", str(len(relations))],
|
["关系数量", str(len(relations))],
|
||||||
@@ -460,14 +460,14 @@ class ExportManager:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# 按类型统计实体
|
# 按类型统计实体
|
||||||
type_counts = {}
|
type_counts = {}
|
||||||
for e in entities:
|
for e in entities:
|
||||||
type_counts[e.type] = type_counts.get(e.type, 0) + 1
|
type_counts[e.type] = type_counts.get(e.type, 0) + 1
|
||||||
|
|
||||||
for etype, count in sorted(type_counts.items()):
|
for etype, count in sorted(type_counts.items()):
|
||||||
stats_data.append([f"{etype} 实体", str(count)])
|
stats_data.append([f"{etype} 实体", str(count)])
|
||||||
|
|
||||||
stats_table = Table(stats_data, colWidths = [3 * inch, 2 * inch])
|
stats_table = Table(stats_data, colWidths=[3 * inch, 2 * inch])
|
||||||
stats_table.setStyle(
|
stats_table.setStyle(
|
||||||
TableStyle(
|
TableStyle(
|
||||||
[
|
[
|
||||||
@@ -496,8 +496,8 @@ class ExportManager:
|
|||||||
story.append(PageBreak())
|
story.append(PageBreak())
|
||||||
story.append(Paragraph("实体列表", heading_style))
|
story.append(Paragraph("实体列表", heading_style))
|
||||||
|
|
||||||
entity_data = [["名称", "类型", "提及次数", "定义"]]
|
entity_data = [["名称", "类型", "提及次数", "定义"]]
|
||||||
for e in sorted(entities, key = lambda x: x.mention_count, reverse = True)[
|
for e in sorted(entities, key=lambda x: x.mention_count, reverse=True)[
|
||||||
:50
|
:50
|
||||||
]: # 限制前50个
|
]: # 限制前50个
|
||||||
entity_data.append(
|
entity_data.append(
|
||||||
@@ -509,8 +509,8 @@ class ExportManager:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
entity_table = Table(
|
entity_table = Table(
|
||||||
entity_data, colWidths = [1.5 * inch, 1 * inch, 1 * inch, 2.5 * inch]
|
entity_data, colWidths=[1.5 * inch, 1 * inch, 1 * inch, 2.5 * inch]
|
||||||
)
|
)
|
||||||
entity_table.setStyle(
|
entity_table.setStyle(
|
||||||
TableStyle(
|
TableStyle(
|
||||||
@@ -534,12 +534,12 @@ class ExportManager:
|
|||||||
story.append(PageBreak())
|
story.append(PageBreak())
|
||||||
story.append(Paragraph("关系列表", heading_style))
|
story.append(Paragraph("关系列表", heading_style))
|
||||||
|
|
||||||
relation_data = [["源实体", "关系", "目标实体", "置信度"]]
|
relation_data = [["源实体", "关系", "目标实体", "置信度"]]
|
||||||
for r in relations[:100]: # 限制前100个
|
for r in relations[:100]: # 限制前100个
|
||||||
relation_data.append([r.source, r.relation_type, r.target, f"{r.confidence:.2f}"])
|
relation_data.append([r.source, r.relation_type, r.target, f"{r.confidence:.2f}"])
|
||||||
|
|
||||||
relation_table = Table(
|
relation_table = Table(
|
||||||
relation_data, colWidths = [2 * inch, 1.5 * inch, 2 * inch, 1 * inch]
|
relation_data, colWidths=[2 * inch, 1.5 * inch, 2 * inch, 1 * inch]
|
||||||
)
|
)
|
||||||
relation_table.setStyle(
|
relation_table.setStyle(
|
||||||
TableStyle(
|
TableStyle(
|
||||||
@@ -574,7 +574,7 @@ class ExportManager:
|
|||||||
Returns:
|
Returns:
|
||||||
JSON 字符串
|
JSON 字符串
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"project_name": project_name,
|
"project_name": project_name,
|
||||||
"export_time": datetime.now().isoformat(),
|
"export_time": datetime.now().isoformat(),
|
||||||
@@ -613,16 +613,16 @@ class ExportManager:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.dumps(data, ensure_ascii = False, indent = 2)
|
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
# 全局导出管理器实例
|
# 全局导出管理器实例
|
||||||
_export_manager = None
|
_export_manager = None
|
||||||
|
|
||||||
|
|
||||||
def get_export_manager(db_manager = None) -> None:
|
def get_export_manager(db_manager=None) -> None:
|
||||||
"""获取导出管理器实例"""
|
"""获取导出管理器实例"""
|
||||||
global _export_manager
|
global _export_manager
|
||||||
if _export_manager is None:
|
if _export_manager is None:
|
||||||
_export_manager = ExportManager(db_manager)
|
_export_manager = ExportManager(db_manager)
|
||||||
return _export_manager
|
return _export_manager
|
||||||
|
|||||||
+478
-478
File diff suppressed because it is too large
Load Diff
+124
-124
@@ -11,30 +11,30 @@ import uuid
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
UUID_LENGTH = 8 # UUID 截断长度
|
UUID_LENGTH = 8 # UUID 截断长度
|
||||||
|
|
||||||
# 尝试导入图像处理库
|
# 尝试导入图像处理库
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageEnhance, ImageFilter
|
from PIL import Image, ImageEnhance, ImageFilter
|
||||||
|
|
||||||
PIL_AVAILABLE = True
|
PIL_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PIL_AVAILABLE = False
|
PIL_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
CV2_AVAILABLE = True
|
CV2_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CV2_AVAILABLE = False
|
CV2_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pytesseract
|
import pytesseract
|
||||||
|
|
||||||
PYTESSERACT_AVAILABLE = True
|
PYTESSERACT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYTESSERACT_AVAILABLE = False
|
PYTESSERACT_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -44,7 +44,7 @@ class ImageEntity:
|
|||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
confidence: float
|
confidence: float
|
||||||
bbox: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
bbox: tuple[int, int, int, int] | None = None # (x, y, width, height)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -70,7 +70,7 @@ class ImageProcessingResult:
|
|||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
success: bool
|
success: bool
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -87,7 +87,7 @@ class ImageProcessor:
|
|||||||
"""图片处理器 - 处理各种类型图片"""
|
"""图片处理器 - 处理各种类型图片"""
|
||||||
|
|
||||||
# 图片类型定义
|
# 图片类型定义
|
||||||
IMAGE_TYPES = {
|
IMAGE_TYPES = {
|
||||||
"whiteboard": "白板",
|
"whiteboard": "白板",
|
||||||
"ppt": "PPT/演示文稿",
|
"ppt": "PPT/演示文稿",
|
||||||
"handwritten": "手写笔记",
|
"handwritten": "手写笔记",
|
||||||
@@ -96,17 +96,17 @@ class ImageProcessor:
|
|||||||
"other": "其他",
|
"other": "其他",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, temp_dir: str = None) -> None:
|
def __init__(self, temp_dir: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
初始化图片处理器
|
初始化图片处理器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
temp_dir: 临时文件目录
|
temp_dir: 临时文件目录
|
||||||
"""
|
"""
|
||||||
self.temp_dir = temp_dir or os.path.join(os.getcwd(), "temp", "images")
|
self.temp_dir = temp_dir or os.path.join(os.getcwd(), "temp", "images")
|
||||||
os.makedirs(self.temp_dir, exist_ok = True)
|
os.makedirs(self.temp_dir, exist_ok=True)
|
||||||
|
|
||||||
def preprocess_image(self, image, image_type: str = None) -> None:
|
def preprocess_image(self, image, image_type: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
预处理图片以提高OCR质量
|
预处理图片以提高OCR质量
|
||||||
|
|
||||||
@@ -123,25 +123,25 @@ class ImageProcessor:
|
|||||||
try:
|
try:
|
||||||
# 转换为RGB(如果是RGBA)
|
# 转换为RGB(如果是RGBA)
|
||||||
if image.mode == "RGBA":
|
if image.mode == "RGBA":
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
|
|
||||||
# 根据图片类型进行针对性处理
|
# 根据图片类型进行针对性处理
|
||||||
if image_type == "whiteboard":
|
if image_type == "whiteboard":
|
||||||
# 白板:增强对比度,去除背景
|
# 白板:增强对比度,去除背景
|
||||||
image = self._enhance_whiteboard(image)
|
image = self._enhance_whiteboard(image)
|
||||||
elif image_type == "handwritten":
|
elif image_type == "handwritten":
|
||||||
# 手写笔记:降噪,增强对比度
|
# 手写笔记:降噪,增强对比度
|
||||||
image = self._enhance_handwritten(image)
|
image = self._enhance_handwritten(image)
|
||||||
elif image_type == "screenshot":
|
elif image_type == "screenshot":
|
||||||
# 截图:轻微锐化
|
# 截图:轻微锐化
|
||||||
image = image.filter(ImageFilter.SHARPEN)
|
image = image.filter(ImageFilter.SHARPEN)
|
||||||
|
|
||||||
# 通用处理:调整大小(如果太大)
|
# 通用处理:调整大小(如果太大)
|
||||||
max_size = 4096
|
max_size = 4096
|
||||||
if max(image.size) > max_size:
|
if max(image.size) > max_size:
|
||||||
ratio = max_size / max(image.size)
|
ratio = max_size / max(image.size)
|
||||||
new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
|
new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
|
||||||
image = image.resize(new_size, Image.Resampling.LANCZOS)
|
image = image.resize(new_size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
return image
|
return image
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -151,33 +151,33 @@ class ImageProcessor:
|
|||||||
def _enhance_whiteboard(self, image) -> None:
|
def _enhance_whiteboard(self, image) -> None:
|
||||||
"""增强白板图片"""
|
"""增强白板图片"""
|
||||||
# 转换为灰度
|
# 转换为灰度
|
||||||
gray = image.convert("L")
|
gray = image.convert("L")
|
||||||
|
|
||||||
# 增强对比度
|
# 增强对比度
|
||||||
enhancer = ImageEnhance.Contrast(gray)
|
enhancer = ImageEnhance.Contrast(gray)
|
||||||
enhanced = enhancer.enhance(2.0)
|
enhanced = enhancer.enhance(2.0)
|
||||||
|
|
||||||
# 二值化
|
# 二值化
|
||||||
threshold = 128
|
threshold = 128
|
||||||
binary = enhanced.point(lambda x: 0 if x < threshold else 255, "1")
|
binary = enhanced.point(lambda x: 0 if x < threshold else 255, "1")
|
||||||
|
|
||||||
return binary.convert("L")
|
return binary.convert("L")
|
||||||
|
|
||||||
def _enhance_handwritten(self, image) -> None:
|
def _enhance_handwritten(self, image) -> None:
|
||||||
"""增强手写笔记图片"""
|
"""增强手写笔记图片"""
|
||||||
# 转换为灰度
|
# 转换为灰度
|
||||||
gray = image.convert("L")
|
gray = image.convert("L")
|
||||||
|
|
||||||
# 轻微降噪
|
# 轻微降噪
|
||||||
blurred = gray.filter(ImageFilter.GaussianBlur(radius = 1))
|
blurred = gray.filter(ImageFilter.GaussianBlur(radius=1))
|
||||||
|
|
||||||
# 增强对比度
|
# 增强对比度
|
||||||
enhancer = ImageEnhance.Contrast(blurred)
|
enhancer = ImageEnhance.Contrast(blurred)
|
||||||
enhanced = enhancer.enhance(1.5)
|
enhanced = enhancer.enhance(1.5)
|
||||||
|
|
||||||
return enhanced
|
return enhanced
|
||||||
|
|
||||||
def detect_image_type(self, image, ocr_text: str = "") -> str:
|
def detect_image_type(self, image, ocr_text: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
自动检测图片类型
|
自动检测图片类型
|
||||||
|
|
||||||
@@ -193,8 +193,8 @@ class ImageProcessor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 基于图片特征和OCR内容判断类型
|
# 基于图片特征和OCR内容判断类型
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
aspect_ratio = width / height
|
aspect_ratio = width / height
|
||||||
|
|
||||||
# 检测是否为PPT(通常是16:9或4:3)
|
# 检测是否为PPT(通常是16:9或4:3)
|
||||||
if 1.3 <= aspect_ratio <= 1.8:
|
if 1.3 <= aspect_ratio <= 1.8:
|
||||||
@@ -204,12 +204,12 @@ class ImageProcessor:
|
|||||||
|
|
||||||
# 检测是否为白板(大量手写文字,可能有箭头、框等)
|
# 检测是否为白板(大量手写文字,可能有箭头、框等)
|
||||||
if CV2_AVAILABLE:
|
if CV2_AVAILABLE:
|
||||||
img_array = np.array(image.convert("RGB"))
|
img_array = np.array(image.convert("RGB"))
|
||||||
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
|
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
|
||||||
|
|
||||||
# 检测边缘(白板通常有很多线条)
|
# 检测边缘(白板通常有很多线条)
|
||||||
edges = cv2.Canny(gray, 50, 150)
|
edges = cv2.Canny(gray, 50, 150)
|
||||||
edge_ratio = np.sum(edges > 0) / edges.size
|
edge_ratio = np.sum(edges > 0) / edges.size
|
||||||
|
|
||||||
# 如果边缘比例高,可能是白板
|
# 如果边缘比例高,可能是白板
|
||||||
if edge_ratio > 0.05 and len(ocr_text) > 50:
|
if edge_ratio > 0.05 and len(ocr_text) > 50:
|
||||||
@@ -236,7 +236,7 @@ class ImageProcessor:
|
|||||||
print(f"Image type detection error: {e}")
|
print(f"Image type detection error: {e}")
|
||||||
return "other"
|
return "other"
|
||||||
|
|
||||||
def perform_ocr(self, image, lang: str = "chi_sim+eng") -> tuple[str, float]:
|
def perform_ocr(self, image, lang: str = "chi_sim+eng") -> tuple[str, float]:
|
||||||
"""
|
"""
|
||||||
对图片进行OCR识别
|
对图片进行OCR识别
|
||||||
|
|
||||||
@@ -252,15 +252,15 @@ class ImageProcessor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 预处理图片
|
# 预处理图片
|
||||||
processed_image = self.preprocess_image(image)
|
processed_image = self.preprocess_image(image)
|
||||||
|
|
||||||
# 执行OCR
|
# 执行OCR
|
||||||
text = pytesseract.image_to_string(processed_image, lang = lang)
|
text = pytesseract.image_to_string(processed_image, lang=lang)
|
||||||
|
|
||||||
# 获取置信度
|
# 获取置信度
|
||||||
data = pytesseract.image_to_data(processed_image, output_type = pytesseract.Output.DICT)
|
data = pytesseract.image_to_data(processed_image, output_type=pytesseract.Output.DICT)
|
||||||
confidences = [int(c) for c in data["conf"] if int(c) > 0]
|
confidences = [int(c) for c in data["conf"] if int(c) > 0]
|
||||||
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
||||||
|
|
||||||
return text.strip(), avg_confidence / 100.0
|
return text.strip(), avg_confidence / 100.0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -277,26 +277,26 @@ class ImageProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
实体列表
|
实体列表
|
||||||
"""
|
"""
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
# 简单的实体提取规则(可以替换为LLM调用)
|
# 简单的实体提取规则(可以替换为LLM调用)
|
||||||
# 提取大写字母开头的词组(可能是专有名词)
|
# 提取大写字母开头的词组(可能是专有名词)
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# 项目名称(通常是大写或带引号)
|
# 项目名称(通常是大写或带引号)
|
||||||
project_pattern = r'["\']([^"\']+)["\']|([A-Z][a-zA-Z0-9]*(?:\s+[A-Z][a-zA-Z0-9]*)+)'
|
project_pattern = r'["\']([^"\']+)["\']|([A-Z][a-zA-Z0-9]*(?:\s+[A-Z][a-zA-Z0-9]*)+)'
|
||||||
for match in re.finditer(project_pattern, text):
|
for match in re.finditer(project_pattern, text):
|
||||||
name = match.group(1) or match.group(2)
|
name = match.group(1) or match.group(2)
|
||||||
if name and len(name) > 2:
|
if name and len(name) > 2:
|
||||||
entities.append(ImageEntity(name = name.strip(), type = "PROJECT", confidence = 0.7))
|
entities.append(ImageEntity(name=name.strip(), type="PROJECT", confidence=0.7))
|
||||||
|
|
||||||
# 人名(中文)
|
# 人名(中文)
|
||||||
name_pattern = r"([\u4e00-\u9fa5]{2, 4})(?:先生|女士|总|经理|工程师|老师)"
|
name_pattern = r"([\u4e00-\u9fa5]{2, 4})(?:先生|女士|总|经理|工程师|老师)"
|
||||||
for match in re.finditer(name_pattern, text):
|
for match in re.finditer(name_pattern, text):
|
||||||
entities.append(ImageEntity(name = match.group(1), type = "PERSON", confidence = 0.8))
|
entities.append(ImageEntity(name=match.group(1), type="PERSON", confidence=0.8))
|
||||||
|
|
||||||
# 技术术语
|
# 技术术语
|
||||||
tech_keywords = [
|
tech_keywords = [
|
||||||
"K8s",
|
"K8s",
|
||||||
"Kubernetes",
|
"Kubernetes",
|
||||||
"Docker",
|
"Docker",
|
||||||
@@ -314,13 +314,13 @@ class ImageProcessor:
|
|||||||
]
|
]
|
||||||
for keyword in tech_keywords:
|
for keyword in tech_keywords:
|
||||||
if keyword in text:
|
if keyword in text:
|
||||||
entities.append(ImageEntity(name = keyword, type = "TECH", confidence = 0.9))
|
entities.append(ImageEntity(name=keyword, type="TECH", confidence=0.9))
|
||||||
|
|
||||||
# 去重
|
# 去重
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_entities = []
|
unique_entities = []
|
||||||
for e in entities:
|
for e in entities:
|
||||||
key = (e.name.lower(), e.type)
|
key = (e.name.lower(), e.type)
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_entities.append(e)
|
unique_entities.append(e)
|
||||||
@@ -341,19 +341,19 @@ class ImageProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
图片描述
|
图片描述
|
||||||
"""
|
"""
|
||||||
type_name = self.IMAGE_TYPES.get(image_type, "图片")
|
type_name = self.IMAGE_TYPES.get(image_type, "图片")
|
||||||
|
|
||||||
description_parts = [f"这是一张{type_name}图片。"]
|
description_parts = [f"这是一张{type_name}图片。"]
|
||||||
|
|
||||||
if ocr_text:
|
if ocr_text:
|
||||||
# 提取前200字符作为摘要
|
# 提取前200字符作为摘要
|
||||||
text_preview = ocr_text[:200].replace("\n", " ")
|
text_preview = ocr_text[:200].replace("\n", " ")
|
||||||
if len(ocr_text) > 200:
|
if len(ocr_text) > 200:
|
||||||
text_preview += "..."
|
text_preview += "..."
|
||||||
description_parts.append(f"内容摘要:{text_preview}")
|
description_parts.append(f"内容摘要:{text_preview}")
|
||||||
|
|
||||||
if entities:
|
if entities:
|
||||||
entity_names = [e.name for e in entities[:5]] # 最多显示5个实体
|
entity_names = [e.name for e in entities[:5]] # 最多显示5个实体
|
||||||
description_parts.append(f"识别到的关键实体:{', '.join(entity_names)}")
|
description_parts.append(f"识别到的关键实体:{', '.join(entity_names)}")
|
||||||
|
|
||||||
return " ".join(description_parts)
|
return " ".join(description_parts)
|
||||||
@@ -361,9 +361,9 @@ class ImageProcessor:
|
|||||||
def process_image(
|
def process_image(
|
||||||
self,
|
self,
|
||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
filename: str = None,
|
filename: str = None,
|
||||||
image_id: str = None,
|
image_id: str = None,
|
||||||
detect_type: bool = True,
|
detect_type: bool = True,
|
||||||
) -> ImageProcessingResult:
|
) -> ImageProcessingResult:
|
||||||
"""
|
"""
|
||||||
处理单张图片
|
处理单张图片
|
||||||
@@ -377,73 +377,73 @@ class ImageProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
图片处理结果
|
图片处理结果
|
||||||
"""
|
"""
|
||||||
image_id = image_id or str(uuid.uuid4())[:UUID_LENGTH]
|
image_id = image_id or str(uuid.uuid4())[:UUID_LENGTH]
|
||||||
|
|
||||||
if not PIL_AVAILABLE:
|
if not PIL_AVAILABLE:
|
||||||
return ImageProcessingResult(
|
return ImageProcessingResult(
|
||||||
image_id = image_id,
|
image_id=image_id,
|
||||||
image_type = "other",
|
image_type="other",
|
||||||
ocr_text = "",
|
ocr_text="",
|
||||||
description = "PIL not available",
|
description="PIL not available",
|
||||||
entities = [],
|
entities=[],
|
||||||
relations = [],
|
relations=[],
|
||||||
width = 0,
|
width=0,
|
||||||
height = 0,
|
height=0,
|
||||||
success = False,
|
success=False,
|
||||||
error_message = "PIL library not available",
|
error_message="PIL library not available",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 加载图片
|
# 加载图片
|
||||||
image = Image.open(io.BytesIO(image_data))
|
image = Image.open(io.BytesIO(image_data))
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
|
|
||||||
# 执行OCR
|
# 执行OCR
|
||||||
ocr_text, ocr_confidence = self.perform_ocr(image)
|
ocr_text, ocr_confidence = self.perform_ocr(image)
|
||||||
|
|
||||||
# 检测图片类型
|
# 检测图片类型
|
||||||
image_type = "other"
|
image_type = "other"
|
||||||
if detect_type:
|
if detect_type:
|
||||||
image_type = self.detect_image_type(image, ocr_text)
|
image_type = self.detect_image_type(image, ocr_text)
|
||||||
|
|
||||||
# 提取实体
|
# 提取实体
|
||||||
entities = self.extract_entities_from_text(ocr_text)
|
entities = self.extract_entities_from_text(ocr_text)
|
||||||
|
|
||||||
# 生成描述
|
# 生成描述
|
||||||
description = self.generate_description(image_type, ocr_text, entities)
|
description = self.generate_description(image_type, ocr_text, entities)
|
||||||
|
|
||||||
# 提取关系(基于实体共现)
|
# 提取关系(基于实体共现)
|
||||||
relations = self._extract_relations(entities, ocr_text)
|
relations = self._extract_relations(entities, ocr_text)
|
||||||
|
|
||||||
# 保存图片文件(可选)
|
# 保存图片文件(可选)
|
||||||
if filename:
|
if filename:
|
||||||
save_path = os.path.join(self.temp_dir, f"{image_id}_{filename}")
|
save_path = os.path.join(self.temp_dir, f"{image_id}_{filename}")
|
||||||
image.save(save_path)
|
image.save(save_path)
|
||||||
|
|
||||||
return ImageProcessingResult(
|
return ImageProcessingResult(
|
||||||
image_id = image_id,
|
image_id=image_id,
|
||||||
image_type = image_type,
|
image_type=image_type,
|
||||||
ocr_text = ocr_text,
|
ocr_text=ocr_text,
|
||||||
description = description,
|
description=description,
|
||||||
entities = entities,
|
entities=entities,
|
||||||
relations = relations,
|
relations=relations,
|
||||||
width = width,
|
width=width,
|
||||||
height = height,
|
height=height,
|
||||||
success = True,
|
success=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ImageProcessingResult(
|
return ImageProcessingResult(
|
||||||
image_id = image_id,
|
image_id=image_id,
|
||||||
image_type = "other",
|
image_type="other",
|
||||||
ocr_text = "",
|
ocr_text="",
|
||||||
description = "",
|
description="",
|
||||||
entities = [],
|
entities=[],
|
||||||
relations = [],
|
relations=[],
|
||||||
width = 0,
|
width=0,
|
||||||
height = 0,
|
height=0,
|
||||||
success = False,
|
success=False,
|
||||||
error_message = str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extract_relations(self, entities: list[ImageEntity], text: str) -> list[ImageRelation]:
|
def _extract_relations(self, entities: list[ImageEntity], text: str) -> list[ImageRelation]:
|
||||||
@@ -457,16 +457,16 @@ class ImageProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
关系列表
|
关系列表
|
||||||
"""
|
"""
|
||||||
relations = []
|
relations = []
|
||||||
|
|
||||||
if len(entities) < 2:
|
if len(entities) < 2:
|
||||||
return relations
|
return relations
|
||||||
|
|
||||||
# 简单的关系提取:如果两个实体在同一句子中出现,则认为它们相关
|
# 简单的关系提取:如果两个实体在同一句子中出现,则认为它们相关
|
||||||
sentences = text.replace("。", ".").replace("!", "!").replace("?", "?").split(".")
|
sentences = text.replace("。", ".").replace("!", "!").replace("?", "?").split(".")
|
||||||
|
|
||||||
for sentence in sentences:
|
for sentence in sentences:
|
||||||
sentence_entities = []
|
sentence_entities = []
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
if entity.name in sentence:
|
if entity.name in sentence:
|
||||||
sentence_entities.append(entity)
|
sentence_entities.append(entity)
|
||||||
@@ -477,17 +477,17 @@ class ImageProcessor:
|
|||||||
for j in range(i + 1, len(sentence_entities)):
|
for j in range(i + 1, len(sentence_entities)):
|
||||||
relations.append(
|
relations.append(
|
||||||
ImageRelation(
|
ImageRelation(
|
||||||
source = sentence_entities[i].name,
|
source=sentence_entities[i].name,
|
||||||
target = sentence_entities[j].name,
|
target=sentence_entities[j].name,
|
||||||
relation_type = "related",
|
relation_type="related",
|
||||||
confidence = 0.5,
|
confidence=0.5,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return relations
|
return relations
|
||||||
|
|
||||||
def process_batch(
|
def process_batch(
|
||||||
self, images_data: list[tuple[bytes, str]], project_id: str = None
|
self, images_data: list[tuple[bytes, str]], project_id: str = None
|
||||||
) -> BatchProcessingResult:
|
) -> BatchProcessingResult:
|
||||||
"""
|
"""
|
||||||
批量处理图片
|
批量处理图片
|
||||||
@@ -499,12 +499,12 @@ class ImageProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
批量处理结果
|
批量处理结果
|
||||||
"""
|
"""
|
||||||
results = []
|
results = []
|
||||||
success_count = 0
|
success_count = 0
|
||||||
failed_count = 0
|
failed_count = 0
|
||||||
|
|
||||||
for image_data, filename in images_data:
|
for image_data, filename in images_data:
|
||||||
result = self.process_image(image_data, filename)
|
result = self.process_image(image_data, filename)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
@@ -513,10 +513,10 @@ class ImageProcessor:
|
|||||||
failed_count += 1
|
failed_count += 1
|
||||||
|
|
||||||
return BatchProcessingResult(
|
return BatchProcessingResult(
|
||||||
results = results,
|
results=results,
|
||||||
total_count = len(results),
|
total_count=len(results),
|
||||||
success_count = success_count,
|
success_count=success_count,
|
||||||
failed_count = failed_count,
|
failed_count=failed_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
def image_to_base64(self, image_data: bytes) -> str:
|
def image_to_base64(self, image_data: bytes) -> str:
|
||||||
@@ -531,7 +531,7 @@ class ImageProcessor:
|
|||||||
"""
|
"""
|
||||||
return base64.b64encode(image_data).decode("utf-8")
|
return base64.b64encode(image_data).decode("utf-8")
|
||||||
|
|
||||||
def get_image_thumbnail(self, image_data: bytes, size: tuple[int, int] = (200, 200)) -> bytes:
|
def get_image_thumbnail(self, image_data: bytes, size: tuple[int, int] = (200, 200)) -> bytes:
|
||||||
"""
|
"""
|
||||||
生成图片缩略图
|
生成图片缩略图
|
||||||
|
|
||||||
@@ -546,11 +546,11 @@ class ImageProcessor:
|
|||||||
return image_data
|
return image_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(io.BytesIO(image_data))
|
image = Image.open(io.BytesIO(image_data))
|
||||||
image.thumbnail(size, Image.Resampling.LANCZOS)
|
image.thumbnail(size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
image.save(buffer, format = "JPEG")
|
image.save(buffer, format="JPEG")
|
||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Thumbnail generation error: {e}")
|
print(f"Thumbnail generation error: {e}")
|
||||||
@@ -558,12 +558,12 @@ class ImageProcessor:
|
|||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_image_processor = None
|
_image_processor = None
|
||||||
|
|
||||||
|
|
||||||
def get_image_processor(temp_dir: str = None) -> ImageProcessor:
|
def get_image_processor(temp_dir: str = None) -> ImageProcessor:
|
||||||
"""获取图片处理器单例"""
|
"""获取图片处理器单例"""
|
||||||
global _image_processor
|
global _image_processor
|
||||||
if _image_processor is None:
|
if _image_processor is None:
|
||||||
_image_processor = ImageProcessor(temp_dir)
|
_image_processor = ImageProcessor(temp_dir)
|
||||||
return _image_processor
|
return _image_processor
|
||||||
|
|||||||
+9
-9
@@ -4,27 +4,27 @@
|
|||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
db_path = os.path.join(os.path.dirname(__file__), "insightflow.db")
|
db_path = os.path.join(os.path.dirname(__file__), "insightflow.db")
|
||||||
schema_path = os.path.join(os.path.dirname(__file__), "schema.sql")
|
schema_path = os.path.join(os.path.dirname(__file__), "schema.sql")
|
||||||
|
|
||||||
print(f"Database path: {db_path}")
|
print(f"Database path: {db_path}")
|
||||||
print(f"Schema path: {schema_path}")
|
print(f"Schema path: {schema_path}")
|
||||||
|
|
||||||
# Read schema
|
# Read schema
|
||||||
with open(schema_path) as f:
|
with open(schema_path) as f:
|
||||||
schema = f.read()
|
schema = f.read()
|
||||||
|
|
||||||
# Execute schema
|
# Execute schema
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Split schema by semicolons and execute each statement
|
# Split schema by semicolons and execute each statement
|
||||||
statements = schema.split(";")
|
statements = schema.split(";")
|
||||||
success_count = 0
|
success_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
|
||||||
for stmt in statements:
|
for stmt in statements:
|
||||||
stmt = stmt.strip()
|
stmt = stmt.strip()
|
||||||
if stmt:
|
if stmt:
|
||||||
try:
|
try:
|
||||||
cursor.execute(stmt)
|
cursor.execute(stmt)
|
||||||
|
|||||||
+126
-126
@@ -12,18 +12,18 @@ from enum import Enum
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
||||||
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
||||||
|
|
||||||
|
|
||||||
class ReasoningType(Enum):
|
class ReasoningType(Enum):
|
||||||
"""推理类型"""
|
"""推理类型"""
|
||||||
|
|
||||||
CAUSAL = "causal" # 因果推理
|
CAUSAL = "causal" # 因果推理
|
||||||
ASSOCIATIVE = "associative" # 关联推理
|
ASSOCIATIVE = "associative" # 关联推理
|
||||||
TEMPORAL = "temporal" # 时序推理
|
TEMPORAL = "temporal" # 时序推理
|
||||||
COMPARATIVE = "comparative" # 对比推理
|
COMPARATIVE = "comparative" # 对比推理
|
||||||
SUMMARY = "summary" # 总结推理
|
SUMMARY = "summary" # 总结推理
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -51,38 +51,38 @@ class InferencePath:
|
|||||||
class KnowledgeReasoner:
|
class KnowledgeReasoner:
|
||||||
"""知识推理引擎"""
|
"""知识推理引擎"""
|
||||||
|
|
||||||
def __init__(self, api_key: str = None, base_url: str = None) -> None:
|
def __init__(self, api_key: str = None, base_url: str = None) -> None:
|
||||||
self.api_key = api_key or KIMI_API_KEY
|
self.api_key = api_key or KIMI_API_KEY
|
||||||
self.base_url = base_url or KIMI_BASE_URL
|
self.base_url = base_url or KIMI_BASE_URL
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _call_llm(self, prompt: str, temperature: float = 0.3) -> str:
|
async def _call_llm(self, prompt: str, temperature: float = 0.3) -> str:
|
||||||
"""调用 LLM"""
|
"""调用 LLM"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("KIMI_API_KEY not set")
|
raise ValueError("KIMI_API_KEY not set")
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": "k2p5",
|
"model": "k2p5",
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{self.base_url}/v1/chat/completions",
|
f"{self.base_url}/v1/chat/completions",
|
||||||
headers = self.headers,
|
headers=self.headers,
|
||||||
json = payload,
|
json=payload,
|
||||||
timeout = 120.0,
|
timeout=120.0,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
return result["choices"][0]["message"]["content"]
|
return result["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
async def enhanced_qa(
|
async def enhanced_qa(
|
||||||
self, query: str, project_context: dict, graph_data: dict, reasoning_depth: str = "medium"
|
self, query: str, project_context: dict, graph_data: dict, reasoning_depth: str = "medium"
|
||||||
) -> ReasoningResult:
|
) -> ReasoningResult:
|
||||||
"""
|
"""
|
||||||
增强问答 - 结合图谱推理的问答
|
增强问答 - 结合图谱推理的问答
|
||||||
@@ -94,7 +94,7 @@ class KnowledgeReasoner:
|
|||||||
reasoning_depth: 推理深度 (shallow/medium/deep)
|
reasoning_depth: 推理深度 (shallow/medium/deep)
|
||||||
"""
|
"""
|
||||||
# 1. 分析问题类型
|
# 1. 分析问题类型
|
||||||
analysis = await self._analyze_question(query)
|
analysis = await self._analyze_question(query)
|
||||||
|
|
||||||
# 2. 根据问题类型选择推理策略
|
# 2. 根据问题类型选择推理策略
|
||||||
if analysis["type"] == "causal":
|
if analysis["type"] == "causal":
|
||||||
@@ -108,7 +108,7 @@ class KnowledgeReasoner:
|
|||||||
|
|
||||||
async def _analyze_question(self, query: str) -> dict:
|
async def _analyze_question(self, query: str) -> dict:
|
||||||
"""分析问题类型和意图"""
|
"""分析问题类型和意图"""
|
||||||
prompt = f"""分析以下问题的类型和意图:
|
prompt = f"""分析以下问题的类型和意图:
|
||||||
|
|
||||||
问题:{query}
|
问题:{query}
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ class KnowledgeReasoner:
|
|||||||
- factual: 事实类问题(是什么、有哪些)
|
- factual: 事实类问题(是什么、有哪些)
|
||||||
- opinion: 观点类问题(怎么看、态度、评价)"""
|
- opinion: 观点类问题(怎么看、态度、评价)"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.1)
|
content = await self._call_llm(prompt, temperature=0.1)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
return json.loads(json_match.group())
|
return json.loads(json_match.group())
|
||||||
@@ -144,10 +144,10 @@ class KnowledgeReasoner:
|
|||||||
"""因果推理 - 分析原因和影响"""
|
"""因果推理 - 分析原因和影响"""
|
||||||
|
|
||||||
# 构建因果分析提示
|
# 构建因果分析提示
|
||||||
entities_str = json.dumps(graph_data.get("entities", []), ensure_ascii = False, indent = 2)
|
entities_str = json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2)
|
||||||
relations_str = json.dumps(graph_data.get("relations", []), ensure_ascii = False, indent = 2)
|
relations_str = json.dumps(graph_data.get("relations", []), ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
prompt = f"""基于以下知识图谱进行因果推理分析:
|
prompt = f"""基于以下知识图谱进行因果推理分析:
|
||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
{query}
|
{query}
|
||||||
@@ -159,7 +159,7 @@ class KnowledgeReasoner:
|
|||||||
{relations_str[:2000]}
|
{relations_str[:2000]}
|
||||||
|
|
||||||
## 项目上下文
|
## 项目上下文
|
||||||
{json.dumps(project_context, ensure_ascii = False, indent = 2)[:1500]}
|
{json.dumps(project_context, ensure_ascii=False, indent=2)[:1500]}
|
||||||
|
|
||||||
请进行因果分析,返回 JSON 格式:
|
请进行因果分析,返回 JSON 格式:
|
||||||
{{
|
{{
|
||||||
@@ -172,31 +172,31 @@ class KnowledgeReasoner:
|
|||||||
"knowledge_gaps": ["缺失信息1"]
|
"knowledge_gaps": ["缺失信息1"]
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.3)
|
content = await self._call_llm(prompt, temperature=0.3)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
|
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = data.get("answer", ""),
|
answer=data.get("answer", ""),
|
||||||
reasoning_type = ReasoningType.CAUSAL,
|
reasoning_type=ReasoningType.CAUSAL,
|
||||||
confidence = data.get("confidence", 0.7),
|
confidence=data.get("confidence", 0.7),
|
||||||
evidence = [{"text": e} for e in data.get("evidence", [])],
|
evidence=[{"text": e} for e in data.get("evidence", [])],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = data.get("knowledge_gaps", []),
|
gaps=data.get("knowledge_gaps", []),
|
||||||
)
|
)
|
||||||
except (json.JSONDecodeError, KeyError):
|
except (json.JSONDecodeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = content,
|
answer=content,
|
||||||
reasoning_type = ReasoningType.CAUSAL,
|
reasoning_type=ReasoningType.CAUSAL,
|
||||||
confidence = 0.5,
|
confidence=0.5,
|
||||||
evidence = [],
|
evidence=[],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = ["无法完成因果推理"],
|
gaps=["无法完成因果推理"],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _comparative_reasoning(
|
async def _comparative_reasoning(
|
||||||
@@ -204,16 +204,16 @@ class KnowledgeReasoner:
|
|||||||
) -> ReasoningResult:
|
) -> ReasoningResult:
|
||||||
"""对比推理 - 比较实体间的异同"""
|
"""对比推理 - 比较实体间的异同"""
|
||||||
|
|
||||||
prompt = f"""基于以下知识图谱进行对比分析:
|
prompt = f"""基于以下知识图谱进行对比分析:
|
||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
{query}
|
{query}
|
||||||
|
|
||||||
## 实体
|
## 实体
|
||||||
{json.dumps(graph_data.get("entities", []), ensure_ascii = False, indent = 2)[:2000]}
|
{json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2)[:2000]}
|
||||||
|
|
||||||
## 关系
|
## 关系
|
||||||
{json.dumps(graph_data.get("relations", []), ensure_ascii = False, indent = 2)[:1500]}
|
{json.dumps(graph_data.get("relations", []), ensure_ascii=False, indent=2)[:1500]}
|
||||||
|
|
||||||
请进行对比分析,返回 JSON 格式:
|
请进行对比分析,返回 JSON 格式:
|
||||||
{{
|
{{
|
||||||
@@ -226,31 +226,31 @@ class KnowledgeReasoner:
|
|||||||
"knowledge_gaps": []
|
"knowledge_gaps": []
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.3)
|
content = await self._call_llm(prompt, temperature=0.3)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
|
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = data.get("answer", ""),
|
answer=data.get("answer", ""),
|
||||||
reasoning_type = ReasoningType.COMPARATIVE,
|
reasoning_type=ReasoningType.COMPARATIVE,
|
||||||
confidence = data.get("confidence", 0.7),
|
confidence=data.get("confidence", 0.7),
|
||||||
evidence = [{"text": e} for e in data.get("evidence", [])],
|
evidence=[{"text": e} for e in data.get("evidence", [])],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = data.get("knowledge_gaps", []),
|
gaps=data.get("knowledge_gaps", []),
|
||||||
)
|
)
|
||||||
except (json.JSONDecodeError, KeyError):
|
except (json.JSONDecodeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = content,
|
answer=content,
|
||||||
reasoning_type = ReasoningType.COMPARATIVE,
|
reasoning_type=ReasoningType.COMPARATIVE,
|
||||||
confidence = 0.5,
|
confidence=0.5,
|
||||||
evidence = [],
|
evidence=[],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = [],
|
gaps=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _temporal_reasoning(
|
async def _temporal_reasoning(
|
||||||
@@ -258,16 +258,16 @@ class KnowledgeReasoner:
|
|||||||
) -> ReasoningResult:
|
) -> ReasoningResult:
|
||||||
"""时序推理 - 分析时间线和演变"""
|
"""时序推理 - 分析时间线和演变"""
|
||||||
|
|
||||||
prompt = f"""基于以下知识图谱进行时序分析:
|
prompt = f"""基于以下知识图谱进行时序分析:
|
||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
{query}
|
{query}
|
||||||
|
|
||||||
## 项目时间线
|
## 项目时间线
|
||||||
{json.dumps(project_context.get("timeline", []), ensure_ascii = False, indent = 2)[:2000]}
|
{json.dumps(project_context.get("timeline", []), ensure_ascii=False, indent=2)[:2000]}
|
||||||
|
|
||||||
## 实体提及历史
|
## 实体提及历史
|
||||||
{json.dumps(graph_data.get("entities", []), ensure_ascii = False, indent = 2)[:1500]}
|
{json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2)[:1500]}
|
||||||
|
|
||||||
请进行时序分析,返回 JSON 格式:
|
请进行时序分析,返回 JSON 格式:
|
||||||
{{
|
{{
|
||||||
@@ -280,31 +280,31 @@ class KnowledgeReasoner:
|
|||||||
"knowledge_gaps": []
|
"knowledge_gaps": []
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.3)
|
content = await self._call_llm(prompt, temperature=0.3)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
|
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = data.get("answer", ""),
|
answer=data.get("answer", ""),
|
||||||
reasoning_type = ReasoningType.TEMPORAL,
|
reasoning_type=ReasoningType.TEMPORAL,
|
||||||
confidence = data.get("confidence", 0.7),
|
confidence=data.get("confidence", 0.7),
|
||||||
evidence = [{"text": e} for e in data.get("evidence", [])],
|
evidence=[{"text": e} for e in data.get("evidence", [])],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = data.get("knowledge_gaps", []),
|
gaps=data.get("knowledge_gaps", []),
|
||||||
)
|
)
|
||||||
except (json.JSONDecodeError, KeyError):
|
except (json.JSONDecodeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = content,
|
answer=content,
|
||||||
reasoning_type = ReasoningType.TEMPORAL,
|
reasoning_type=ReasoningType.TEMPORAL,
|
||||||
confidence = 0.5,
|
confidence=0.5,
|
||||||
evidence = [],
|
evidence=[],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = [],
|
gaps=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _associative_reasoning(
|
async def _associative_reasoning(
|
||||||
@@ -312,16 +312,16 @@ class KnowledgeReasoner:
|
|||||||
) -> ReasoningResult:
|
) -> ReasoningResult:
|
||||||
"""关联推理 - 发现实体间的隐含关联"""
|
"""关联推理 - 发现实体间的隐含关联"""
|
||||||
|
|
||||||
prompt = f"""基于以下知识图谱进行关联分析:
|
prompt = f"""基于以下知识图谱进行关联分析:
|
||||||
|
|
||||||
## 问题
|
## 问题
|
||||||
{query}
|
{query}
|
||||||
|
|
||||||
## 实体
|
## 实体
|
||||||
{json.dumps(graph_data.get("entities", [])[:20], ensure_ascii = False, indent = 2)}
|
{json.dumps(graph_data.get("entities", [])[:20], ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
## 关系
|
## 关系
|
||||||
{json.dumps(graph_data.get("relations", [])[:30], ensure_ascii = False, indent = 2)}
|
{json.dumps(graph_data.get("relations", [])[:30], ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
请进行关联推理,发现隐含联系,返回 JSON 格式:
|
请进行关联推理,发现隐含联系,返回 JSON 格式:
|
||||||
{{
|
{{
|
||||||
@@ -334,52 +334,52 @@ class KnowledgeReasoner:
|
|||||||
"knowledge_gaps": []
|
"knowledge_gaps": []
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.4)
|
content = await self._call_llm(prompt, temperature=0.4)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
|
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = data.get("answer", ""),
|
answer=data.get("answer", ""),
|
||||||
reasoning_type = ReasoningType.ASSOCIATIVE,
|
reasoning_type=ReasoningType.ASSOCIATIVE,
|
||||||
confidence = data.get("confidence", 0.7),
|
confidence=data.get("confidence", 0.7),
|
||||||
evidence = [{"text": e} for e in data.get("evidence", [])],
|
evidence=[{"text": e} for e in data.get("evidence", [])],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = data.get("knowledge_gaps", []),
|
gaps=data.get("knowledge_gaps", []),
|
||||||
)
|
)
|
||||||
except (json.JSONDecodeError, KeyError):
|
except (json.JSONDecodeError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ReasoningResult(
|
return ReasoningResult(
|
||||||
answer = content,
|
answer=content,
|
||||||
reasoning_type = ReasoningType.ASSOCIATIVE,
|
reasoning_type=ReasoningType.ASSOCIATIVE,
|
||||||
confidence = 0.5,
|
confidence=0.5,
|
||||||
evidence = [],
|
evidence=[],
|
||||||
related_entities = [],
|
related_entities=[],
|
||||||
gaps = [],
|
gaps=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
def find_inference_paths(
|
def find_inference_paths(
|
||||||
self, start_entity: str, end_entity: str, graph_data: dict, max_depth: int = 3
|
self, start_entity: str, end_entity: str, graph_data: dict, max_depth: int = 3
|
||||||
) -> list[InferencePath]:
|
) -> list[InferencePath]:
|
||||||
"""
|
"""
|
||||||
发现两个实体之间的推理路径
|
发现两个实体之间的推理路径
|
||||||
|
|
||||||
使用 BFS 在关系图中搜索路径
|
使用 BFS 在关系图中搜索路径
|
||||||
"""
|
"""
|
||||||
relations = graph_data.get("relations", [])
|
relations = graph_data.get("relations", [])
|
||||||
|
|
||||||
# 构建邻接表
|
# 构建邻接表
|
||||||
adj = {}
|
adj = {}
|
||||||
for r in relations:
|
for r in relations:
|
||||||
src = r.get("source_id") or r.get("source")
|
src = r.get("source_id") or r.get("source")
|
||||||
tgt = r.get("target_id") or r.get("target")
|
tgt = r.get("target_id") or r.get("target")
|
||||||
if src not in adj:
|
if src not in adj:
|
||||||
adj[src] = []
|
adj[src] = []
|
||||||
if tgt not in adj:
|
if tgt not in adj:
|
||||||
adj[tgt] = []
|
adj[tgt] = []
|
||||||
adj[src].append({"target": tgt, "relation": r.get("type", "related"), "data": r})
|
adj[src].append({"target": tgt, "relation": r.get("type", "related"), "data": r})
|
||||||
# 无向图也添加反向
|
# 无向图也添加反向
|
||||||
adj[tgt].append(
|
adj[tgt].append(
|
||||||
@@ -389,21 +389,21 @@ class KnowledgeReasoner:
|
|||||||
# BFS 搜索路径
|
# BFS 搜索路径
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
queue = deque([(start_entity, [{"entity": start_entity, "relation": None}])])
|
queue = deque([(start_entity, [{"entity": start_entity, "relation": None}])])
|
||||||
{start_entity}
|
{start_entity}
|
||||||
|
|
||||||
while queue and len(paths) < 5:
|
while queue and len(paths) < 5:
|
||||||
current, path = queue.popleft()
|
current, path = queue.popleft()
|
||||||
|
|
||||||
if current == end_entity and len(path) > 1:
|
if current == end_entity and len(path) > 1:
|
||||||
# 找到一条路径
|
# 找到一条路径
|
||||||
paths.append(
|
paths.append(
|
||||||
InferencePath(
|
InferencePath(
|
||||||
start_entity = start_entity,
|
start_entity=start_entity,
|
||||||
end_entity = end_entity,
|
end_entity=end_entity,
|
||||||
path = path,
|
path=path,
|
||||||
strength = self._calculate_path_strength(path),
|
strength=self._calculate_path_strength(path),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -412,9 +412,9 @@ class KnowledgeReasoner:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for neighbor in adj.get(current, []):
|
for neighbor in adj.get(current, []):
|
||||||
next_entity = neighbor["target"]
|
next_entity = neighbor["target"]
|
||||||
if next_entity not in [p["entity"] for p in path]: # 避免循环
|
if next_entity not in [p["entity"] for p in path]: # 避免循环
|
||||||
new_path = path + [
|
new_path = path + [
|
||||||
{
|
{
|
||||||
"entity": next_entity,
|
"entity": next_entity,
|
||||||
"relation": neighbor["relation"],
|
"relation": neighbor["relation"],
|
||||||
@@ -424,7 +424,7 @@ class KnowledgeReasoner:
|
|||||||
queue.append((next_entity, new_path))
|
queue.append((next_entity, new_path))
|
||||||
|
|
||||||
# 按强度排序
|
# 按强度排序
|
||||||
paths.sort(key = lambda p: p.strength, reverse = True)
|
paths.sort(key=lambda p: p.strength, reverse=True)
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
def _calculate_path_strength(self, path: list[dict]) -> float:
|
def _calculate_path_strength(self, path: list[dict]) -> float:
|
||||||
@@ -433,23 +433,23 @@ class KnowledgeReasoner:
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# 路径越短越强
|
# 路径越短越强
|
||||||
length_factor = 1.0 / len(path)
|
length_factor = 1.0 / len(path)
|
||||||
|
|
||||||
# 关系置信度
|
# 关系置信度
|
||||||
confidence_sum = 0
|
confidence_sum = 0
|
||||||
confidence_count = 0
|
confidence_count = 0
|
||||||
for node in path[1:]: # 跳过第一个节点
|
for node in path[1:]: # 跳过第一个节点
|
||||||
rel_data = node.get("relation_data", {})
|
rel_data = node.get("relation_data", {})
|
||||||
if "confidence" in rel_data:
|
if "confidence" in rel_data:
|
||||||
confidence_sum += rel_data["confidence"]
|
confidence_sum += rel_data["confidence"]
|
||||||
confidence_count += 1
|
confidence_count += 1
|
||||||
|
|
||||||
confidence_factor = (confidence_sum / confidence_count) if confidence_count > 0 else 0.5
|
confidence_factor = (confidence_sum / confidence_count) if confidence_count > 0 else 0.5
|
||||||
|
|
||||||
return length_factor * confidence_factor
|
return length_factor * confidence_factor
|
||||||
|
|
||||||
async def summarize_project(
|
async def summarize_project(
|
||||||
self, project_context: dict, graph_data: dict, summary_type: str = "comprehensive"
|
self, project_context: dict, graph_data: dict, summary_type: str = "comprehensive"
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
项目智能总结
|
项目智能总结
|
||||||
@@ -457,17 +457,17 @@ class KnowledgeReasoner:
|
|||||||
Args:
|
Args:
|
||||||
summary_type: comprehensive/executive/technical/risk
|
summary_type: comprehensive/executive/technical/risk
|
||||||
"""
|
"""
|
||||||
type_prompts = {
|
type_prompts = {
|
||||||
"comprehensive": "全面总结项目的所有方面",
|
"comprehensive": "全面总结项目的所有方面",
|
||||||
"executive": "高管摘要,关注关键决策和风险",
|
"executive": "高管摘要,关注关键决策和风险",
|
||||||
"technical": "技术总结,关注架构和技术栈",
|
"technical": "技术总结,关注架构和技术栈",
|
||||||
"risk": "风险分析,关注潜在问题和依赖",
|
"risk": "风险分析,关注潜在问题和依赖",
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt = f"""请对以下项目进行{type_prompts.get(summary_type, "全面总结")}:
|
prompt = f"""请对以下项目进行{type_prompts.get(summary_type, "全面总结")}:
|
||||||
|
|
||||||
## 项目信息
|
## 项目信息
|
||||||
{json.dumps(project_context, ensure_ascii = False, indent = 2)[:3000]}
|
{json.dumps(project_context, ensure_ascii=False, indent=2)[:3000]}
|
||||||
|
|
||||||
## 知识图谱
|
## 知识图谱
|
||||||
实体数: {len(graph_data.get("entities", []))}
|
实体数: {len(graph_data.get("entities", []))}
|
||||||
@@ -483,9 +483,9 @@ class KnowledgeReasoner:
|
|||||||
"confidence": 0.85
|
"confidence": 0.85
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
content = await self._call_llm(prompt, temperature = 0.3)
|
content = await self._call_llm(prompt, temperature=0.3)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
|
|
||||||
if json_match:
|
if json_match:
|
||||||
try:
|
try:
|
||||||
@@ -504,11 +504,11 @@ class KnowledgeReasoner:
|
|||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_reasoner = None
|
_reasoner = None
|
||||||
|
|
||||||
|
|
||||||
def get_knowledge_reasoner() -> KnowledgeReasoner:
|
def get_knowledge_reasoner() -> KnowledgeReasoner:
|
||||||
global _reasoner
|
global _reasoner
|
||||||
if _reasoner is None:
|
if _reasoner is None:
|
||||||
_reasoner = KnowledgeReasoner()
|
_reasoner = KnowledgeReasoner()
|
||||||
return _reasoner
|
return _reasoner
|
||||||
|
|||||||
+53
-53
@@ -12,8 +12,8 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "")
|
||||||
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -41,22 +41,22 @@ class RelationExtractionResult:
|
|||||||
class LLMClient:
|
class LLMClient:
|
||||||
"""Kimi API 客户端"""
|
"""Kimi API 客户端"""
|
||||||
|
|
||||||
def __init__(self, api_key: str = None, base_url: str = None) -> None:
|
def __init__(self, api_key: str = None, base_url: str = None) -> None:
|
||||||
self.api_key = api_key or KIMI_API_KEY
|
self.api_key = api_key or KIMI_API_KEY
|
||||||
self.base_url = base_url or KIMI_BASE_URL
|
self.base_url = base_url or KIMI_BASE_URL
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def chat(
|
async def chat(
|
||||||
self, messages: list[ChatMessage], temperature: float = 0.3, stream: bool = False
|
self, messages: list[ChatMessage], temperature: float = 0.3, stream: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
"""发送聊天请求"""
|
"""发送聊天请求"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("KIMI_API_KEY not set")
|
raise ValueError("KIMI_API_KEY not set")
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": "k2p5",
|
"model": "k2p5",
|
||||||
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
@@ -64,24 +64,24 @@ class LLMClient:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{self.base_url}/v1/chat/completions",
|
f"{self.base_url}/v1/chat/completions",
|
||||||
headers = self.headers,
|
headers=self.headers,
|
||||||
json = payload,
|
json=payload,
|
||||||
timeout = 120.0,
|
timeout=120.0,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
return result["choices"][0]["message"]["content"]
|
return result["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
async def chat_stream(
|
async def chat_stream(
|
||||||
self, messages: list[ChatMessage], temperature: float = 0.3
|
self, messages: list[ChatMessage], temperature: float = 0.3
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""流式聊天请求"""
|
"""流式聊天请求"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("KIMI_API_KEY not set")
|
raise ValueError("KIMI_API_KEY not set")
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": "k2p5",
|
"model": "k2p5",
|
||||||
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
@@ -92,19 +92,19 @@ class LLMClient:
|
|||||||
async with client.stream(
|
async with client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.base_url}/v1/chat/completions",
|
f"{self.base_url}/v1/chat/completions",
|
||||||
headers = self.headers,
|
headers=self.headers,
|
||||||
json = payload,
|
json=payload,
|
||||||
timeout = 120.0,
|
timeout=120.0,
|
||||||
) as response:
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
async for line in response.aiter_lines():
|
async for line in response.aiter_lines():
|
||||||
if line.startswith("data: "):
|
if line.startswith("data: "):
|
||||||
data = line[6:]
|
data = line[6:]
|
||||||
if data == "[DONE]":
|
if data == "[DONE]":
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
chunk = json.loads(data)
|
chunk = json.loads(data)
|
||||||
delta = chunk["choices"][0]["delta"]
|
delta = chunk["choices"][0]["delta"]
|
||||||
if "content" in delta:
|
if "content" in delta:
|
||||||
yield delta["content"]
|
yield delta["content"]
|
||||||
except (json.JSONDecodeError, KeyError, IndexError):
|
except (json.JSONDecodeError, KeyError, IndexError):
|
||||||
@@ -114,7 +114,7 @@ class LLMClient:
|
|||||||
self, text: str
|
self, text: str
|
||||||
) -> tuple[list[EntityExtractionResult], list[RelationExtractionResult]]:
|
) -> tuple[list[EntityExtractionResult], list[RelationExtractionResult]]:
|
||||||
"""提取实体和关系,带置信度分数"""
|
"""提取实体和关系,带置信度分数"""
|
||||||
prompt = f"""从以下会议文本中提取关键实体和它们之间的关系,以 JSON 格式返回:
|
prompt = f"""从以下会议文本中提取关键实体和它们之间的关系,以 JSON 格式返回:
|
||||||
|
|
||||||
文本:{text[:3000]}
|
文本:{text[:3000]}
|
||||||
|
|
||||||
@@ -139,30 +139,30 @@ class LLMClient:
|
|||||||
]
|
]
|
||||||
}}"""
|
}}"""
|
||||||
|
|
||||||
messages = [ChatMessage(role = "user", content = prompt)]
|
messages = [ChatMessage(role="user", content=prompt)]
|
||||||
content = await self.chat(messages, temperature = 0.1)
|
content = await self.chat(messages, temperature=0.1)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
if not json_match:
|
if not json_match:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_match.group())
|
data = json.loads(json_match.group())
|
||||||
entities = [
|
entities = [
|
||||||
EntityExtractionResult(
|
EntityExtractionResult(
|
||||||
name = e["name"],
|
name=e["name"],
|
||||||
type = e.get("type", "OTHER"),
|
type=e.get("type", "OTHER"),
|
||||||
definition = e.get("definition", ""),
|
definition=e.get("definition", ""),
|
||||||
confidence = e.get("confidence", 0.8),
|
confidence=e.get("confidence", 0.8),
|
||||||
)
|
)
|
||||||
for e in data.get("entities", [])
|
for e in data.get("entities", [])
|
||||||
]
|
]
|
||||||
relations = [
|
relations = [
|
||||||
RelationExtractionResult(
|
RelationExtractionResult(
|
||||||
source = r["source"],
|
source=r["source"],
|
||||||
target = r["target"],
|
target=r["target"],
|
||||||
type = r.get("type", "related"),
|
type=r.get("type", "related"),
|
||||||
confidence = r.get("confidence", 0.8),
|
confidence=r.get("confidence", 0.8),
|
||||||
)
|
)
|
||||||
for r in data.get("relations", [])
|
for r in data.get("relations", [])
|
||||||
]
|
]
|
||||||
@@ -173,10 +173,10 @@ class LLMClient:
|
|||||||
|
|
||||||
async def rag_query(self, query: str, context: str, project_context: dict) -> str:
|
async def rag_query(self, query: str, context: str, project_context: dict) -> str:
|
||||||
"""RAG 问答 - 基于项目上下文回答问题"""
|
"""RAG 问答 - 基于项目上下文回答问题"""
|
||||||
prompt = f"""你是一个专业的项目分析助手。基于以下项目信息回答问题:
|
prompt = f"""你是一个专业的项目分析助手。基于以下项目信息回答问题:
|
||||||
|
|
||||||
## 项目信息
|
## 项目信息
|
||||||
{json.dumps(project_context, ensure_ascii = False, indent = 2)}
|
{json.dumps(project_context, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
## 相关上下文
|
## 相关上下文
|
||||||
{context[:4000]}
|
{context[:4000]}
|
||||||
@@ -186,21 +186,21 @@ class LLMClient:
|
|||||||
|
|
||||||
请用中文回答,保持简洁专业。如果信息不足,请明确说明。"""
|
请用中文回答,保持简洁专业。如果信息不足,请明确说明。"""
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
ChatMessage(
|
ChatMessage(
|
||||||
role = "system", content = "你是一个专业的项目分析助手,擅长从会议记录中提取洞察。"
|
role="system", content="你是一个专业的项目分析助手,擅长从会议记录中提取洞察。"
|
||||||
),
|
),
|
||||||
ChatMessage(role = "user", content = prompt),
|
ChatMessage(role="user", content=prompt),
|
||||||
]
|
]
|
||||||
|
|
||||||
return await self.chat(messages, temperature = 0.3)
|
return await self.chat(messages, temperature=0.3)
|
||||||
|
|
||||||
async def agent_command(self, command: str, project_context: dict) -> dict:
|
async def agent_command(self, command: str, project_context: dict) -> dict:
|
||||||
"""Agent 指令解析 - 将自然语言指令转换为结构化操作"""
|
"""Agent 指令解析 - 将自然语言指令转换为结构化操作"""
|
||||||
prompt = f"""解析以下用户指令,转换为结构化操作:
|
prompt = f"""解析以下用户指令,转换为结构化操作:
|
||||||
|
|
||||||
## 项目信息
|
## 项目信息
|
||||||
{json.dumps(project_context, ensure_ascii = False, indent = 2)}
|
{json.dumps(project_context, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
## 用户指令
|
## 用户指令
|
||||||
{command}
|
{command}
|
||||||
@@ -221,10 +221,10 @@ class LLMClient:
|
|||||||
- create_relation: 创建关系,params 包含 source(源实体), target(目标实体), relation_type(关系类型)
|
- create_relation: 创建关系,params 包含 source(源实体), target(目标实体), relation_type(关系类型)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
messages = [ChatMessage(role = "user", content = prompt)]
|
messages = [ChatMessage(role="user", content=prompt)]
|
||||||
content = await self.chat(messages, temperature = 0.1)
|
content = await self.chat(messages, temperature=0.1)
|
||||||
|
|
||||||
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
json_match = re.search(r"\{{.*?\}}", content, re.DOTALL)
|
||||||
if not json_match:
|
if not json_match:
|
||||||
return {"intent": "unknown", "explanation": "无法解析指令"}
|
return {"intent": "unknown", "explanation": "无法解析指令"}
|
||||||
|
|
||||||
@@ -235,14 +235,14 @@ class LLMClient:
|
|||||||
|
|
||||||
async def analyze_entity_evolution(self, entity_name: str, mentions: list[dict]) -> str:
|
async def analyze_entity_evolution(self, entity_name: str, mentions: list[dict]) -> str:
|
||||||
"""分析实体在项目中的演变/态度变化"""
|
"""分析实体在项目中的演变/态度变化"""
|
||||||
mentions_text = "\n".join(
|
mentions_text = "\n".join(
|
||||||
[
|
[
|
||||||
f"[{m.get('created_at', '未知时间')}] {m.get('text_snippet', '')}"
|
f"[{m.get('created_at', '未知时间')}] {m.get('text_snippet', '')}"
|
||||||
for m in mentions[:20]
|
for m in mentions[:20]
|
||||||
] # 限制数量
|
] # 限制数量
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = f"""分析实体 "{entity_name}" 在项目中的演变和态度变化:
|
prompt = f"""分析实体 "{entity_name}" 在项目中的演变和态度变化:
|
||||||
|
|
||||||
## 提及记录
|
## 提及记录
|
||||||
{mentions_text}
|
{mentions_text}
|
||||||
@@ -255,16 +255,16 @@ class LLMClient:
|
|||||||
|
|
||||||
用中文回答,结构清晰。"""
|
用中文回答,结构清晰。"""
|
||||||
|
|
||||||
messages = [ChatMessage(role = "user", content = prompt)]
|
messages = [ChatMessage(role="user", content=prompt)]
|
||||||
return await self.chat(messages, temperature = 0.3)
|
return await self.chat(messages, temperature=0.3)
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_llm_client = None
|
_llm_client = None
|
||||||
|
|
||||||
|
|
||||||
def get_llm_client() -> LLMClient:
|
def get_llm_client() -> LLMClient:
|
||||||
global _llm_client
|
global _llm_client
|
||||||
if _llm_client is None:
|
if _llm_client is None:
|
||||||
_llm_client = LLMClient()
|
_llm_client = LLMClient()
|
||||||
return _llm_client
|
return _llm_client
|
||||||
|
|||||||
+305
-305
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1166,7 +1166,7 @@ async def create_manual_entity(
|
|||||||
start_pos=entity.start_pos,
|
start_pos=entity.start_pos,
|
||||||
end_pos=entity.end_pos,
|
end_pos=entity.end_pos,
|
||||||
text_snippet=text[
|
text_snippet=text[
|
||||||
max(0, entity.start_pos - 20) : min(len(text), entity.end_pos + 20)
|
max(0, entity.start_pos - 20): min(len(text), entity.end_pos + 20)
|
||||||
],
|
],
|
||||||
confidence=1.0,
|
confidence=1.0,
|
||||||
)
|
)
|
||||||
@@ -1408,7 +1408,7 @@ async def upload_audio(project_id: str, file: UploadFile = File(...), _=Depends(
|
|||||||
start_pos=pos,
|
start_pos=pos,
|
||||||
end_pos=pos + len(name),
|
end_pos=pos + len(name),
|
||||||
text_snippet=full_text[
|
text_snippet=full_text[
|
||||||
max(0, pos - 20) : min(len(full_text), pos + len(name) + 20)
|
max(0, pos - 20): min(len(full_text), pos + len(name) + 20)
|
||||||
],
|
],
|
||||||
confidence=1.0,
|
confidence=1.0,
|
||||||
)
|
)
|
||||||
@@ -1534,7 +1534,7 @@ async def upload_document(project_id: str, file: UploadFile = File(...), _=Depen
|
|||||||
start_pos=pos,
|
start_pos=pos,
|
||||||
end_pos=pos + len(name),
|
end_pos=pos + len(name),
|
||||||
text_snippet=full_text[
|
text_snippet=full_text[
|
||||||
max(0, pos - 20) : min(len(full_text), pos + len(name) + 20)
|
max(0, pos - 20): min(len(full_text), pos + len(name) + 20)
|
||||||
],
|
],
|
||||||
confidence=1.0,
|
confidence=1.0,
|
||||||
)
|
)
|
||||||
@@ -3804,10 +3804,10 @@ async def system_status():
|
|||||||
# ==================== Phase 7: Workflow Automation Endpoints ====================
|
# ==================== Phase 7: Workflow Automation Endpoints ====================
|
||||||
|
|
||||||
# Workflow Manager singleton
|
# Workflow Manager singleton
|
||||||
_workflow_manager: "WorkflowManager | None" = None
|
_workflow_manager: Any = None
|
||||||
|
|
||||||
|
|
||||||
def get_workflow_manager_instance() -> "WorkflowManager | None":
|
def get_workflow_manager_instance() -> Any:
|
||||||
global _workflow_manager
|
global _workflow_manager
|
||||||
if _workflow_manager is None and WORKFLOW_AVAILABLE and DB_AVAILABLE:
|
if _workflow_manager is None and WORKFLOW_AVAILABLE and DB_AVAILABLE:
|
||||||
from workflow_manager import WorkflowManager
|
from workflow_manager import WorkflowManager
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ from dataclasses import dataclass
|
|||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
UUID_LENGTH = 8 # UUID 截断长度
|
UUID_LENGTH = 8 # UUID 截断长度
|
||||||
|
|
||||||
# 尝试导入embedding库
|
# 尝试导入embedding库
|
||||||
try:
|
try:
|
||||||
NUMPY_AVAILABLE = True
|
NUMPY_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
NUMPY_AVAILABLE = False
|
NUMPY_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -30,11 +30,11 @@ class MultimodalEntity:
|
|||||||
source_id: str
|
source_id: str
|
||||||
mention_context: str
|
mention_context: str
|
||||||
confidence: float
|
confidence: float
|
||||||
modality_features: dict = None # 模态特定特征
|
modality_features: dict = None # 模态特定特征
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.modality_features is None:
|
if self.modality_features is None:
|
||||||
self.modality_features = {}
|
self.modality_features = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -78,7 +78,7 @@ class MultimodalEntityLinker:
|
|||||||
"""多模态实体关联器 - 跨模态实体对齐和知识融合"""
|
"""多模态实体关联器 - 跨模态实体对齐和知识融合"""
|
||||||
|
|
||||||
# 关联类型
|
# 关联类型
|
||||||
LINK_TYPES = {
|
LINK_TYPES = {
|
||||||
"same_as": "同一实体",
|
"same_as": "同一实体",
|
||||||
"related_to": "相关实体",
|
"related_to": "相关实体",
|
||||||
"part_of": "组成部分",
|
"part_of": "组成部分",
|
||||||
@@ -86,16 +86,16 @@ class MultimodalEntityLinker:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 模态类型
|
# 模态类型
|
||||||
MODALITIES = ["audio", "video", "image", "document"]
|
MODALITIES = ["audio", "video", "image", "document"]
|
||||||
|
|
||||||
def __init__(self, similarity_threshold: float = 0.85) -> None:
|
def __init__(self, similarity_threshold: float = 0.85) -> None:
|
||||||
"""
|
"""
|
||||||
初始化多模态实体关联器
|
初始化多模态实体关联器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
similarity_threshold: 相似度阈值
|
similarity_threshold: 相似度阈值
|
||||||
"""
|
"""
|
||||||
self.similarity_threshold = similarity_threshold
|
self.similarity_threshold = similarity_threshold
|
||||||
|
|
||||||
def calculate_string_similarity(self, s1: str, s2: str) -> float:
|
def calculate_string_similarity(self, s1: str, s2: str) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -111,7 +111,7 @@ class MultimodalEntityLinker:
|
|||||||
if not s1 or not s2:
|
if not s1 or not s2:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
s1, s2 = s1.lower().strip(), s2.lower().strip()
|
s1, s2 = s1.lower().strip(), s2.lower().strip()
|
||||||
|
|
||||||
# 完全匹配
|
# 完全匹配
|
||||||
if s1 == s2:
|
if s1 == s2:
|
||||||
@@ -136,7 +136,7 @@ class MultimodalEntityLinker:
|
|||||||
(相似度, 匹配类型)
|
(相似度, 匹配类型)
|
||||||
"""
|
"""
|
||||||
# 名称相似度
|
# 名称相似度
|
||||||
name_sim = self.calculate_string_similarity(
|
name_sim = self.calculate_string_similarity(
|
||||||
entity1.get("name", ""), entity2.get("name", "")
|
entity1.get("name", ""), entity2.get("name", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ class MultimodalEntityLinker:
|
|||||||
return 1.0, "exact"
|
return 1.0, "exact"
|
||||||
|
|
||||||
# 检查别名
|
# 检查别名
|
||||||
aliases1 = set(a.lower() for a in entity1.get("aliases", []))
|
aliases1 = set(a.lower() for a in entity1.get("aliases", []))
|
||||||
aliases2 = set(a.lower() for a in entity2.get("aliases", []))
|
aliases2 = set(a.lower() for a in entity2.get("aliases", []))
|
||||||
|
|
||||||
if aliases1 & aliases2: # 有共同别名
|
if aliases1 & aliases2: # 有共同别名
|
||||||
return 0.95, "alias_match"
|
return 0.95, "alias_match"
|
||||||
@@ -157,12 +157,12 @@ class MultimodalEntityLinker:
|
|||||||
return 0.95, "alias_match"
|
return 0.95, "alias_match"
|
||||||
|
|
||||||
# 定义相似度
|
# 定义相似度
|
||||||
def_sim = self.calculate_string_similarity(
|
def_sim = self.calculate_string_similarity(
|
||||||
entity1.get("definition", ""), entity2.get("definition", "")
|
entity1.get("definition", ""), entity2.get("definition", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 综合相似度
|
# 综合相似度
|
||||||
combined_sim = name_sim * 0.7 + def_sim * 0.3
|
combined_sim = name_sim * 0.7 + def_sim * 0.3
|
||||||
|
|
||||||
if combined_sim >= self.similarity_threshold:
|
if combined_sim >= self.similarity_threshold:
|
||||||
return combined_sim, "fuzzy"
|
return combined_sim, "fuzzy"
|
||||||
@@ -170,7 +170,7 @@ class MultimodalEntityLinker:
|
|||||||
return combined_sim, "none"
|
return combined_sim, "none"
|
||||||
|
|
||||||
def find_matching_entity(
|
def find_matching_entity(
|
||||||
self, query_entity: dict, candidate_entities: list[dict], exclude_ids: set[str] = None
|
self, query_entity: dict, candidate_entities: list[dict], exclude_ids: set[str] = None
|
||||||
) -> AlignmentResult | None:
|
) -> AlignmentResult | None:
|
||||||
"""
|
"""
|
||||||
在候选实体中查找匹配的实体
|
在候选实体中查找匹配的实体
|
||||||
@@ -183,28 +183,28 @@ class MultimodalEntityLinker:
|
|||||||
Returns:
|
Returns:
|
||||||
对齐结果
|
对齐结果
|
||||||
"""
|
"""
|
||||||
exclude_ids = exclude_ids or set()
|
exclude_ids = exclude_ids or set()
|
||||||
best_match = None
|
best_match = None
|
||||||
best_similarity = 0.0
|
best_similarity = 0.0
|
||||||
|
|
||||||
for candidate in candidate_entities:
|
for candidate in candidate_entities:
|
||||||
if candidate.get("id") in exclude_ids:
|
if candidate.get("id") in exclude_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
similarity, match_type = self.calculate_entity_similarity(query_entity, candidate)
|
similarity, match_type = self.calculate_entity_similarity(query_entity, candidate)
|
||||||
|
|
||||||
if similarity > best_similarity and similarity >= self.similarity_threshold:
|
if similarity > best_similarity and similarity >= self.similarity_threshold:
|
||||||
best_similarity = similarity
|
best_similarity = similarity
|
||||||
best_match = candidate
|
best_match = candidate
|
||||||
best_match_type = match_type
|
best_match_type = match_type
|
||||||
|
|
||||||
if best_match:
|
if best_match:
|
||||||
return AlignmentResult(
|
return AlignmentResult(
|
||||||
entity_id = query_entity.get("id"),
|
entity_id=query_entity.get("id"),
|
||||||
matched_entity_id = best_match.get("id"),
|
matched_entity_id=best_match.get("id"),
|
||||||
similarity = best_similarity,
|
similarity=best_similarity,
|
||||||
match_type = best_match_type,
|
match_type=best_match_type,
|
||||||
confidence = best_similarity,
|
confidence=best_similarity,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -230,10 +230,10 @@ class MultimodalEntityLinker:
|
|||||||
Returns:
|
Returns:
|
||||||
实体关联列表
|
实体关联列表
|
||||||
"""
|
"""
|
||||||
links = []
|
links = []
|
||||||
|
|
||||||
# 合并所有实体
|
# 合并所有实体
|
||||||
all_entities = {
|
all_entities = {
|
||||||
"audio": audio_entities,
|
"audio": audio_entities,
|
||||||
"video": video_entities,
|
"video": video_entities,
|
||||||
"image": image_entities,
|
"image": image_entities,
|
||||||
@@ -246,24 +246,24 @@ class MultimodalEntityLinker:
|
|||||||
if mod1 >= mod2: # 避免重复比较
|
if mod1 >= mod2: # 避免重复比较
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entities1 = all_entities.get(mod1, [])
|
entities1 = all_entities.get(mod1, [])
|
||||||
entities2 = all_entities.get(mod2, [])
|
entities2 = all_entities.get(mod2, [])
|
||||||
|
|
||||||
for ent1 in entities1:
|
for ent1 in entities1:
|
||||||
# 在另一个模态中查找匹配
|
# 在另一个模态中查找匹配
|
||||||
result = self.find_matching_entity(ent1, entities2)
|
result = self.find_matching_entity(ent1, entities2)
|
||||||
|
|
||||||
if result and result.matched_entity_id:
|
if result and result.matched_entity_id:
|
||||||
link = EntityLink(
|
link = EntityLink(
|
||||||
id = str(uuid.uuid4())[:UUID_LENGTH],
|
id=str(uuid.uuid4())[:UUID_LENGTH],
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
source_entity_id = ent1.get("id"),
|
source_entity_id=ent1.get("id"),
|
||||||
target_entity_id = result.matched_entity_id,
|
target_entity_id=result.matched_entity_id,
|
||||||
link_type = "same_as" if result.similarity > 0.95 else "related_to",
|
link_type="same_as" if result.similarity > 0.95 else "related_to",
|
||||||
source_modality = mod1,
|
source_modality=mod1,
|
||||||
target_modality = mod2,
|
target_modality=mod2,
|
||||||
confidence = result.confidence,
|
confidence=result.confidence,
|
||||||
evidence = f"Cross-modal alignment: {result.match_type}",
|
evidence=f"Cross-modal alignment: {result.match_type}",
|
||||||
)
|
)
|
||||||
links.append(link)
|
links.append(link)
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ class MultimodalEntityLinker:
|
|||||||
融合结果
|
融合结果
|
||||||
"""
|
"""
|
||||||
# 收集所有属性
|
# 收集所有属性
|
||||||
fused_properties = {
|
fused_properties = {
|
||||||
"names": set(),
|
"names": set(),
|
||||||
"definitions": [],
|
"definitions": [],
|
||||||
"aliases": set(),
|
"aliases": set(),
|
||||||
@@ -293,7 +293,7 @@ class MultimodalEntityLinker:
|
|||||||
"contexts": [],
|
"contexts": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
merged_ids = []
|
merged_ids = []
|
||||||
|
|
||||||
for entity in linked_entities:
|
for entity in linked_entities:
|
||||||
merged_ids.append(entity.get("id"))
|
merged_ids.append(entity.get("id"))
|
||||||
@@ -318,21 +318,21 @@ class MultimodalEntityLinker:
|
|||||||
fused_properties["contexts"].append(mention.get("mention_context"))
|
fused_properties["contexts"].append(mention.get("mention_context"))
|
||||||
|
|
||||||
# 选择最佳定义(最长的那个)
|
# 选择最佳定义(最长的那个)
|
||||||
best_definition = (
|
best_definition = (
|
||||||
max(fused_properties["definitions"], key = len) if fused_properties["definitions"] else ""
|
max(fused_properties["definitions"], key=len) if fused_properties["definitions"] else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# 选择最佳名称(最常见的那个)
|
# 选择最佳名称(最常见的那个)
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
name_counts = Counter(fused_properties["names"])
|
name_counts = Counter(fused_properties["names"])
|
||||||
best_name = name_counts.most_common(1)[0][0] if name_counts else ""
|
best_name = name_counts.most_common(1)[0][0] if name_counts else ""
|
||||||
|
|
||||||
# 构建融合结果
|
# 构建融合结果
|
||||||
return FusionResult(
|
return FusionResult(
|
||||||
canonical_entity_id = entity_id,
|
canonical_entity_id=entity_id,
|
||||||
merged_entity_ids = merged_ids,
|
merged_entity_ids=merged_ids,
|
||||||
fused_properties = {
|
fused_properties={
|
||||||
"name": best_name,
|
"name": best_name,
|
||||||
"definition": best_definition,
|
"definition": best_definition,
|
||||||
"aliases": list(fused_properties["aliases"]),
|
"aliases": list(fused_properties["aliases"]),
|
||||||
@@ -340,8 +340,8 @@ class MultimodalEntityLinker:
|
|||||||
"modalities": list(fused_properties["modalities"]),
|
"modalities": list(fused_properties["modalities"]),
|
||||||
"contexts": fused_properties["contexts"][:10], # 最多10个上下文
|
"contexts": fused_properties["contexts"][:10], # 最多10个上下文
|
||||||
},
|
},
|
||||||
source_modalities = list(fused_properties["modalities"]),
|
source_modalities=list(fused_properties["modalities"]),
|
||||||
confidence = min(1.0, len(linked_entities) * 0.2 + 0.5),
|
confidence=min(1.0, len(linked_entities) * 0.2 + 0.5),
|
||||||
)
|
)
|
||||||
|
|
||||||
def detect_entity_conflicts(self, entities: list[dict]) -> list[dict]:
|
def detect_entity_conflicts(self, entities: list[dict]) -> list[dict]:
|
||||||
@@ -354,30 +354,30 @@ class MultimodalEntityLinker:
|
|||||||
Returns:
|
Returns:
|
||||||
冲突列表
|
冲突列表
|
||||||
"""
|
"""
|
||||||
conflicts = []
|
conflicts = []
|
||||||
|
|
||||||
# 按名称分组
|
# 按名称分组
|
||||||
name_groups = {}
|
name_groups = {}
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
name = entity.get("name", "").lower()
|
name = entity.get("name", "").lower()
|
||||||
if name:
|
if name:
|
||||||
if name not in name_groups:
|
if name not in name_groups:
|
||||||
name_groups[name] = []
|
name_groups[name] = []
|
||||||
name_groups[name].append(entity)
|
name_groups[name].append(entity)
|
||||||
|
|
||||||
# 检测同名但定义不同的实体
|
# 检测同名但定义不同的实体
|
||||||
for name, group in name_groups.items():
|
for name, group in name_groups.items():
|
||||||
if len(group) > 1:
|
if len(group) > 1:
|
||||||
# 检查定义是否相似
|
# 检查定义是否相似
|
||||||
definitions = [e.get("definition", "") for e in group if e.get("definition")]
|
definitions = [e.get("definition", "") for e in group if e.get("definition")]
|
||||||
|
|
||||||
if len(definitions) > 1:
|
if len(definitions) > 1:
|
||||||
# 计算定义之间的相似度
|
# 计算定义之间的相似度
|
||||||
sim_matrix = []
|
sim_matrix = []
|
||||||
for i, d1 in enumerate(definitions):
|
for i, d1 in enumerate(definitions):
|
||||||
for j, d2 in enumerate(definitions):
|
for j, d2 in enumerate(definitions):
|
||||||
if i < j:
|
if i < j:
|
||||||
sim = self.calculate_string_similarity(d1, d2)
|
sim = self.calculate_string_similarity(d1, d2)
|
||||||
sim_matrix.append(sim)
|
sim_matrix.append(sim)
|
||||||
|
|
||||||
# 如果定义相似度都很低,可能是冲突
|
# 如果定义相似度都很低,可能是冲突
|
||||||
@@ -394,7 +394,7 @@ class MultimodalEntityLinker:
|
|||||||
return conflicts
|
return conflicts
|
||||||
|
|
||||||
def suggest_entity_merges(
|
def suggest_entity_merges(
|
||||||
self, entities: list[dict], existing_links: list[EntityLink] = None
|
self, entities: list[dict], existing_links: list[EntityLink] = None
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
建议实体合并
|
建议实体合并
|
||||||
@@ -406,13 +406,13 @@ class MultimodalEntityLinker:
|
|||||||
Returns:
|
Returns:
|
||||||
合并建议列表
|
合并建议列表
|
||||||
"""
|
"""
|
||||||
suggestions = []
|
suggestions = []
|
||||||
existing_pairs = set()
|
existing_pairs = set()
|
||||||
|
|
||||||
# 记录已有的关联
|
# 记录已有的关联
|
||||||
if existing_links:
|
if existing_links:
|
||||||
for link in existing_links:
|
for link in existing_links:
|
||||||
pair = tuple(sorted([link.source_entity_id, link.target_entity_id]))
|
pair = tuple(sorted([link.source_entity_id, link.target_entity_id]))
|
||||||
existing_pairs.add(pair)
|
existing_pairs.add(pair)
|
||||||
|
|
||||||
# 检查所有实体对
|
# 检查所有实体对
|
||||||
@@ -422,12 +422,12 @@ class MultimodalEntityLinker:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查是否已有关联
|
# 检查是否已有关联
|
||||||
pair = tuple(sorted([ent1.get("id"), ent2.get("id")]))
|
pair = tuple(sorted([ent1.get("id"), ent2.get("id")]))
|
||||||
if pair in existing_pairs:
|
if pair in existing_pairs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 计算相似度
|
# 计算相似度
|
||||||
similarity, match_type = self.calculate_entity_similarity(ent1, ent2)
|
similarity, match_type = self.calculate_entity_similarity(ent1, ent2)
|
||||||
|
|
||||||
if similarity >= self.similarity_threshold:
|
if similarity >= self.similarity_threshold:
|
||||||
suggestions.append(
|
suggestions.append(
|
||||||
@@ -441,7 +441,7 @@ class MultimodalEntityLinker:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 按相似度排序
|
# 按相似度排序
|
||||||
suggestions.sort(key = lambda x: x["similarity"], reverse = True)
|
suggestions.sort(key=lambda x: x["similarity"], reverse=True)
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
@@ -451,8 +451,8 @@ class MultimodalEntityLinker:
|
|||||||
entity_id: str,
|
entity_id: str,
|
||||||
source_type: str,
|
source_type: str,
|
||||||
source_id: str,
|
source_id: str,
|
||||||
mention_context: str = "",
|
mention_context: str = "",
|
||||||
confidence: float = 1.0,
|
confidence: float = 1.0,
|
||||||
) -> MultimodalEntity:
|
) -> MultimodalEntity:
|
||||||
"""
|
"""
|
||||||
创建多模态实体记录
|
创建多模态实体记录
|
||||||
@@ -469,14 +469,14 @@ class MultimodalEntityLinker:
|
|||||||
多模态实体记录
|
多模态实体记录
|
||||||
"""
|
"""
|
||||||
return MultimodalEntity(
|
return MultimodalEntity(
|
||||||
id = str(uuid.uuid4())[:UUID_LENGTH],
|
id=str(uuid.uuid4())[:UUID_LENGTH],
|
||||||
entity_id = entity_id,
|
entity_id=entity_id,
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
name = "", # 将在后续填充
|
name="", # 将在后续填充
|
||||||
source_type = source_type,
|
source_type=source_type,
|
||||||
source_id = source_id,
|
source_id=source_id,
|
||||||
mention_context = mention_context,
|
mention_context=mention_context,
|
||||||
confidence = confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
|
|
||||||
def analyze_modality_distribution(self, multimodal_entities: list[MultimodalEntity]) -> dict:
|
def analyze_modality_distribution(self, multimodal_entities: list[MultimodalEntity]) -> dict:
|
||||||
@@ -489,7 +489,7 @@ class MultimodalEntityLinker:
|
|||||||
Returns:
|
Returns:
|
||||||
模态分布统计
|
模态分布统计
|
||||||
"""
|
"""
|
||||||
distribution = {mod: 0 for mod in self.MODALITIES}
|
distribution = {mod: 0 for mod in self.MODALITIES}
|
||||||
|
|
||||||
# 统计每个模态的实体数
|
# 统计每个模态的实体数
|
||||||
for me in multimodal_entities:
|
for me in multimodal_entities:
|
||||||
@@ -497,13 +497,13 @@ class MultimodalEntityLinker:
|
|||||||
distribution[me.source_type] += 1
|
distribution[me.source_type] += 1
|
||||||
|
|
||||||
# 统计跨模态实体
|
# 统计跨模态实体
|
||||||
entity_modalities = {}
|
entity_modalities = {}
|
||||||
for me in multimodal_entities:
|
for me in multimodal_entities:
|
||||||
if me.entity_id not in entity_modalities:
|
if me.entity_id not in entity_modalities:
|
||||||
entity_modalities[me.entity_id] = set()
|
entity_modalities[me.entity_id] = set()
|
||||||
entity_modalities[me.entity_id].add(me.source_type)
|
entity_modalities[me.entity_id].add(me.source_type)
|
||||||
|
|
||||||
cross_modal_count = sum(1 for mods in entity_modalities.values() if len(mods) > 1)
|
cross_modal_count = sum(1 for mods in entity_modalities.values() if len(mods) > 1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"modality_distribution": distribution,
|
"modality_distribution": distribution,
|
||||||
@@ -517,12 +517,12 @@ class MultimodalEntityLinker:
|
|||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_multimodal_entity_linker = None
|
_multimodal_entity_linker = None
|
||||||
|
|
||||||
|
|
||||||
def get_multimodal_entity_linker(similarity_threshold: float = 0.85) -> MultimodalEntityLinker:
|
def get_multimodal_entity_linker(similarity_threshold: float = 0.85) -> MultimodalEntityLinker:
|
||||||
"""获取多模态实体关联器单例"""
|
"""获取多模态实体关联器单例"""
|
||||||
global _multimodal_entity_linker
|
global _multimodal_entity_linker
|
||||||
if _multimodal_entity_linker is None:
|
if _multimodal_entity_linker is None:
|
||||||
_multimodal_entity_linker = MultimodalEntityLinker(similarity_threshold)
|
_multimodal_entity_linker = MultimodalEntityLinker(similarity_threshold)
|
||||||
return _multimodal_entity_linker
|
return _multimodal_entity_linker
|
||||||
|
|||||||
+110
-110
@@ -13,30 +13,30 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
UUID_LENGTH = 8 # UUID 截断长度
|
UUID_LENGTH = 8 # UUID 截断长度
|
||||||
|
|
||||||
# 尝试导入OCR库
|
# 尝试导入OCR库
|
||||||
try:
|
try:
|
||||||
import pytesseract
|
import pytesseract
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
PYTESSERACT_AVAILABLE = True
|
PYTESSERACT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYTESSERACT_AVAILABLE = False
|
PYTESSERACT_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
CV2_AVAILABLE = True
|
CV2_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CV2_AVAILABLE = False
|
CV2_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
|
|
||||||
FFMPEG_AVAILABLE = True
|
FFMPEG_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
FFMPEG_AVAILABLE = False
|
FFMPEG_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -48,13 +48,13 @@ class VideoFrame:
|
|||||||
frame_number: int
|
frame_number: int
|
||||||
timestamp: float
|
timestamp: float
|
||||||
frame_path: str
|
frame_path: str
|
||||||
ocr_text: str = ""
|
ocr_text: str = ""
|
||||||
ocr_confidence: float = 0.0
|
ocr_confidence: float = 0.0
|
||||||
entities_detected: list[dict] = None
|
entities_detected: list[dict] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.entities_detected is None:
|
if self.entities_detected is None:
|
||||||
self.entities_detected = []
|
self.entities_detected = []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -65,20 +65,20 @@ class VideoInfo:
|
|||||||
project_id: str
|
project_id: str
|
||||||
filename: str
|
filename: str
|
||||||
file_path: str
|
file_path: str
|
||||||
duration: float = 0.0
|
duration: float = 0.0
|
||||||
width: int = 0
|
width: int = 0
|
||||||
height: int = 0
|
height: int = 0
|
||||||
fps: float = 0.0
|
fps: float = 0.0
|
||||||
audio_extracted: bool = False
|
audio_extracted: bool = False
|
||||||
audio_path: str = ""
|
audio_path: str = ""
|
||||||
transcript_id: str = ""
|
transcript_id: str = ""
|
||||||
status: str = "pending"
|
status: str = "pending"
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
metadata: dict = None
|
metadata: dict = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.metadata is None:
|
if self.metadata is None:
|
||||||
self.metadata = {}
|
self.metadata = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -91,13 +91,13 @@ class VideoProcessingResult:
|
|||||||
ocr_results: list[dict]
|
ocr_results: list[dict]
|
||||||
full_text: str # 整合的文本(音频转录 + OCR文本)
|
full_text: str # 整合的文本(音频转录 + OCR文本)
|
||||||
success: bool
|
success: bool
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
|
|
||||||
|
|
||||||
class MultimodalProcessor:
|
class MultimodalProcessor:
|
||||||
"""多模态处理器 - 处理视频文件"""
|
"""多模态处理器 - 处理视频文件"""
|
||||||
|
|
||||||
def __init__(self, temp_dir: str = None, frame_interval: int = 5) -> None:
|
def __init__(self, temp_dir: str = None, frame_interval: int = 5) -> None:
|
||||||
"""
|
"""
|
||||||
初始化多模态处理器
|
初始化多模态处理器
|
||||||
|
|
||||||
@@ -105,16 +105,16 @@ class MultimodalProcessor:
|
|||||||
temp_dir: 临时文件目录
|
temp_dir: 临时文件目录
|
||||||
frame_interval: 关键帧提取间隔(秒)
|
frame_interval: 关键帧提取间隔(秒)
|
||||||
"""
|
"""
|
||||||
self.temp_dir = temp_dir or tempfile.gettempdir()
|
self.temp_dir = temp_dir or tempfile.gettempdir()
|
||||||
self.frame_interval = frame_interval
|
self.frame_interval = frame_interval
|
||||||
self.video_dir = os.path.join(self.temp_dir, "videos")
|
self.video_dir = os.path.join(self.temp_dir, "videos")
|
||||||
self.frames_dir = os.path.join(self.temp_dir, "frames")
|
self.frames_dir = os.path.join(self.temp_dir, "frames")
|
||||||
self.audio_dir = os.path.join(self.temp_dir, "audio")
|
self.audio_dir = os.path.join(self.temp_dir, "audio")
|
||||||
|
|
||||||
# 创建目录
|
# 创建目录
|
||||||
os.makedirs(self.video_dir, exist_ok = True)
|
os.makedirs(self.video_dir, exist_ok=True)
|
||||||
os.makedirs(self.frames_dir, exist_ok = True)
|
os.makedirs(self.frames_dir, exist_ok=True)
|
||||||
os.makedirs(self.audio_dir, exist_ok = True)
|
os.makedirs(self.audio_dir, exist_ok=True)
|
||||||
|
|
||||||
def extract_video_info(self, video_path: str) -> dict:
|
def extract_video_info(self, video_path: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -128,11 +128,11 @@ class MultimodalProcessor:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if FFMPEG_AVAILABLE:
|
if FFMPEG_AVAILABLE:
|
||||||
probe = ffmpeg.probe(video_path)
|
probe = ffmpeg.probe(video_path)
|
||||||
video_stream = next(
|
video_stream = next(
|
||||||
(s for s in probe["streams"] if s["codec_type"] == "video"), None
|
(s for s in probe["streams"] if s["codec_type"] == "video"), None
|
||||||
)
|
)
|
||||||
audio_stream = next(
|
audio_stream = next(
|
||||||
(s for s in probe["streams"] if s["codec_type"] == "audio"), None
|
(s for s in probe["streams"] if s["codec_type"] == "audio"), None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class MultimodalProcessor:
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# 使用 ffprobe 命令行
|
# 使用 ffprobe 命令行
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
@@ -159,9 +159,9 @@ class MultimodalProcessor:
|
|||||||
"json",
|
"json",
|
||||||
video_path,
|
video_path,
|
||||||
]
|
]
|
||||||
result = subprocess.run(cmd, capture_output = True, text = True)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
return {
|
return {
|
||||||
"duration": float(data["format"].get("duration", 0)),
|
"duration": float(data["format"].get("duration", 0)),
|
||||||
"width": int(data["streams"][0].get("width", 0)) if data["streams"] else 0,
|
"width": int(data["streams"][0].get("width", 0)) if data["streams"] else 0,
|
||||||
@@ -177,7 +177,7 @@ class MultimodalProcessor:
|
|||||||
|
|
||||||
return {"duration": 0, "width": 0, "height": 0, "fps": 0, "has_audio": False, "bitrate": 0}
|
return {"duration": 0, "width": 0, "height": 0, "fps": 0, "has_audio": False, "bitrate": 0}
|
||||||
|
|
||||||
def extract_audio(self, video_path: str, output_path: str = None) -> str:
|
def extract_audio(self, video_path: str, output_path: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
从视频中提取音频
|
从视频中提取音频
|
||||||
|
|
||||||
@@ -189,20 +189,20 @@ class MultimodalProcessor:
|
|||||||
提取的音频文件路径
|
提取的音频文件路径
|
||||||
"""
|
"""
|
||||||
if output_path is None:
|
if output_path is None:
|
||||||
video_name = Path(video_path).stem
|
video_name = Path(video_path).stem
|
||||||
output_path = os.path.join(self.audio_dir, f"{video_name}.wav")
|
output_path = os.path.join(self.audio_dir, f"{video_name}.wav")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if FFMPEG_AVAILABLE:
|
if FFMPEG_AVAILABLE:
|
||||||
(
|
(
|
||||||
ffmpeg.input(video_path)
|
ffmpeg.input(video_path)
|
||||||
.output(output_path, ac = 1, ar = 16000, vn = None)
|
.output(output_path, ac=1, ar=16000, vn=None)
|
||||||
.overwrite_output()
|
.overwrite_output()
|
||||||
.run(quiet = True)
|
.run(quiet=True)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 使用命令行 ffmpeg
|
# 使用命令行 ffmpeg
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-i",
|
"-i",
|
||||||
video_path,
|
video_path,
|
||||||
@@ -216,14 +216,14 @@ class MultimodalProcessor:
|
|||||||
"-y",
|
"-y",
|
||||||
output_path,
|
output_path,
|
||||||
]
|
]
|
||||||
subprocess.run(cmd, check = True, capture_output = True)
|
subprocess.run(cmd, check=True, capture_output=True)
|
||||||
|
|
||||||
return output_path
|
return output_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error extracting audio: {e}")
|
print(f"Error extracting audio: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def extract_keyframes(self, video_path: str, video_id: str, interval: int = None) -> list[str]:
|
def extract_keyframes(self, video_path: str, video_id: str, interval: int = None) -> list[str]:
|
||||||
"""
|
"""
|
||||||
从视频中提取关键帧
|
从视频中提取关键帧
|
||||||
|
|
||||||
@@ -235,31 +235,31 @@ class MultimodalProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
提取的帧文件路径列表
|
提取的帧文件路径列表
|
||||||
"""
|
"""
|
||||||
interval = interval or self.frame_interval
|
interval = interval or self.frame_interval
|
||||||
frame_paths = []
|
frame_paths = []
|
||||||
|
|
||||||
# 创建帧存储目录
|
# 创建帧存储目录
|
||||||
video_frames_dir = os.path.join(self.frames_dir, video_id)
|
video_frames_dir = os.path.join(self.frames_dir, video_id)
|
||||||
os.makedirs(video_frames_dir, exist_ok = True)
|
os.makedirs(video_frames_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if CV2_AVAILABLE:
|
if CV2_AVAILABLE:
|
||||||
# 使用 OpenCV 提取帧
|
# 使用 OpenCV 提取帧
|
||||||
cap = cv2.VideoCapture(video_path)
|
cap = cv2.VideoCapture(video_path)
|
||||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
|
||||||
frame_interval_frames = int(fps * interval)
|
frame_interval_frames = int(fps * interval)
|
||||||
frame_number = 0
|
frame_number = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
ret, frame = cap.read()
|
ret, frame = cap.read()
|
||||||
if not ret:
|
if not ret:
|
||||||
break
|
break
|
||||||
|
|
||||||
if frame_number % frame_interval_frames == 0:
|
if frame_number % frame_interval_frames == 0:
|
||||||
timestamp = frame_number / fps
|
timestamp = frame_number / fps
|
||||||
frame_path = os.path.join(
|
frame_path = os.path.join(
|
||||||
video_frames_dir, f"frame_{frame_number:06d}_{timestamp:.2f}.jpg"
|
video_frames_dir, f"frame_{frame_number:06d}_{timestamp:.2f}.jpg"
|
||||||
)
|
)
|
||||||
cv2.imwrite(frame_path, frame)
|
cv2.imwrite(frame_path, frame)
|
||||||
@@ -271,9 +271,9 @@ class MultimodalProcessor:
|
|||||||
else:
|
else:
|
||||||
# 使用 ffmpeg 命令行提取帧
|
# 使用 ffmpeg 命令行提取帧
|
||||||
Path(video_path).stem
|
Path(video_path).stem
|
||||||
output_pattern = os.path.join(video_frames_dir, "frame_%06d_%t.jpg")
|
output_pattern = os.path.join(video_frames_dir, "frame_%06d_%t.jpg")
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-i",
|
"-i",
|
||||||
video_path,
|
video_path,
|
||||||
@@ -284,10 +284,10 @@ class MultimodalProcessor:
|
|||||||
"-y",
|
"-y",
|
||||||
output_pattern,
|
output_pattern,
|
||||||
]
|
]
|
||||||
subprocess.run(cmd, check = True, capture_output = True)
|
subprocess.run(cmd, check=True, capture_output=True)
|
||||||
|
|
||||||
# 获取生成的帧文件列表
|
# 获取生成的帧文件列表
|
||||||
frame_paths = sorted(
|
frame_paths = sorted(
|
||||||
[
|
[
|
||||||
os.path.join(video_frames_dir, f)
|
os.path.join(video_frames_dir, f)
|
||||||
for f in os.listdir(video_frames_dir)
|
for f in os.listdir(video_frames_dir)
|
||||||
@@ -313,19 +313,19 @@ class MultimodalProcessor:
|
|||||||
return "", 0.0
|
return "", 0.0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(image_path)
|
image = Image.open(image_path)
|
||||||
|
|
||||||
# 预处理:转换为灰度图
|
# 预处理:转换为灰度图
|
||||||
if image.mode != "L":
|
if image.mode != "L":
|
||||||
image = image.convert("L")
|
image = image.convert("L")
|
||||||
|
|
||||||
# 使用 pytesseract 进行 OCR
|
# 使用 pytesseract 进行 OCR
|
||||||
text = pytesseract.image_to_string(image, lang = "chi_sim+eng")
|
text = pytesseract.image_to_string(image, lang="chi_sim+eng")
|
||||||
|
|
||||||
# 获取置信度数据
|
# 获取置信度数据
|
||||||
data = pytesseract.image_to_data(image, output_type = pytesseract.Output.DICT)
|
data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)
|
||||||
confidences = [int(c) for c in data["conf"] if int(c) > 0]
|
confidences = [int(c) for c in data["conf"] if int(c) > 0]
|
||||||
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
||||||
|
|
||||||
return text.strip(), avg_confidence / 100.0
|
return text.strip(), avg_confidence / 100.0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -333,7 +333,7 @@ class MultimodalProcessor:
|
|||||||
return "", 0.0
|
return "", 0.0
|
||||||
|
|
||||||
def process_video(
|
def process_video(
|
||||||
self, video_data: bytes, filename: str, project_id: str, video_id: str = None
|
self, video_data: bytes, filename: str, project_id: str, video_id: str = None
|
||||||
) -> VideoProcessingResult:
|
) -> VideoProcessingResult:
|
||||||
"""
|
"""
|
||||||
处理视频文件:提取音频、关键帧、OCR
|
处理视频文件:提取音频、关键帧、OCR
|
||||||
@@ -347,48 +347,48 @@ class MultimodalProcessor:
|
|||||||
Returns:
|
Returns:
|
||||||
视频处理结果
|
视频处理结果
|
||||||
"""
|
"""
|
||||||
video_id = video_id or str(uuid.uuid4())[:UUID_LENGTH]
|
video_id = video_id or str(uuid.uuid4())[:UUID_LENGTH]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 保存视频文件
|
# 保存视频文件
|
||||||
video_path = os.path.join(self.video_dir, f"{video_id}_{filename}")
|
video_path = os.path.join(self.video_dir, f"{video_id}_{filename}")
|
||||||
with open(video_path, "wb") as f:
|
with open(video_path, "wb") as f:
|
||||||
f.write(video_data)
|
f.write(video_data)
|
||||||
|
|
||||||
# 提取视频信息
|
# 提取视频信息
|
||||||
video_info = self.extract_video_info(video_path)
|
video_info = self.extract_video_info(video_path)
|
||||||
|
|
||||||
# 提取音频
|
# 提取音频
|
||||||
audio_path = ""
|
audio_path = ""
|
||||||
if video_info["has_audio"]:
|
if video_info["has_audio"]:
|
||||||
audio_path = self.extract_audio(video_path)
|
audio_path = self.extract_audio(video_path)
|
||||||
|
|
||||||
# 提取关键帧
|
# 提取关键帧
|
||||||
frame_paths = self.extract_keyframes(video_path, video_id)
|
frame_paths = self.extract_keyframes(video_path, video_id)
|
||||||
|
|
||||||
# 对关键帧进行 OCR
|
# 对关键帧进行 OCR
|
||||||
frames = []
|
frames = []
|
||||||
ocr_results = []
|
ocr_results = []
|
||||||
all_ocr_text = []
|
all_ocr_text = []
|
||||||
|
|
||||||
for i, frame_path in enumerate(frame_paths):
|
for i, frame_path in enumerate(frame_paths):
|
||||||
# 解析帧信息
|
# 解析帧信息
|
||||||
frame_name = os.path.basename(frame_path)
|
frame_name = os.path.basename(frame_path)
|
||||||
parts = frame_name.replace(".jpg", "").split("_")
|
parts = frame_name.replace(".jpg", "").split("_")
|
||||||
frame_number = int(parts[1]) if len(parts) > 1 else i
|
frame_number = int(parts[1]) if len(parts) > 1 else i
|
||||||
timestamp = float(parts[2]) if len(parts) > 2 else i * self.frame_interval
|
timestamp = float(parts[2]) if len(parts) > 2 else i * self.frame_interval
|
||||||
|
|
||||||
# OCR 识别
|
# OCR 识别
|
||||||
ocr_text, confidence = self.perform_ocr(frame_path)
|
ocr_text, confidence = self.perform_ocr(frame_path)
|
||||||
|
|
||||||
frame = VideoFrame(
|
frame = VideoFrame(
|
||||||
id = str(uuid.uuid4())[:UUID_LENGTH],
|
id=str(uuid.uuid4())[:UUID_LENGTH],
|
||||||
video_id = video_id,
|
video_id=video_id,
|
||||||
frame_number = frame_number,
|
frame_number=frame_number,
|
||||||
timestamp = timestamp,
|
timestamp=timestamp,
|
||||||
frame_path = frame_path,
|
frame_path=frame_path,
|
||||||
ocr_text = ocr_text,
|
ocr_text=ocr_text,
|
||||||
ocr_confidence = confidence,
|
ocr_confidence=confidence,
|
||||||
)
|
)
|
||||||
frames.append(frame)
|
frames.append(frame)
|
||||||
|
|
||||||
@@ -404,29 +404,29 @@ class MultimodalProcessor:
|
|||||||
all_ocr_text.append(ocr_text)
|
all_ocr_text.append(ocr_text)
|
||||||
|
|
||||||
# 整合所有 OCR 文本
|
# 整合所有 OCR 文本
|
||||||
full_ocr_text = "\n\n".join(all_ocr_text)
|
full_ocr_text = "\n\n".join(all_ocr_text)
|
||||||
|
|
||||||
return VideoProcessingResult(
|
return VideoProcessingResult(
|
||||||
video_id = video_id,
|
video_id=video_id,
|
||||||
audio_path = audio_path,
|
audio_path=audio_path,
|
||||||
frames = frames,
|
frames=frames,
|
||||||
ocr_results = ocr_results,
|
ocr_results=ocr_results,
|
||||||
full_text = full_ocr_text,
|
full_text=full_ocr_text,
|
||||||
success = True,
|
success=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return VideoProcessingResult(
|
return VideoProcessingResult(
|
||||||
video_id = video_id,
|
video_id=video_id,
|
||||||
audio_path = "",
|
audio_path="",
|
||||||
frames = [],
|
frames=[],
|
||||||
ocr_results = [],
|
ocr_results=[],
|
||||||
full_text = "",
|
full_text="",
|
||||||
success = False,
|
success=False,
|
||||||
error_message = str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
def cleanup(self, video_id: str = None) -> None:
|
def cleanup(self, video_id: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
清理临时文件
|
清理临时文件
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ class MultimodalProcessor:
|
|||||||
if video_id:
|
if video_id:
|
||||||
# 清理特定视频的文件
|
# 清理特定视频的文件
|
||||||
for dir_path in [self.video_dir, self.frames_dir, self.audio_dir]:
|
for dir_path in [self.video_dir, self.frames_dir, self.audio_dir]:
|
||||||
target_dir = (
|
target_dir = (
|
||||||
os.path.join(dir_path, video_id) if dir_path == self.frames_dir else dir_path
|
os.path.join(dir_path, video_id) if dir_path == self.frames_dir else dir_path
|
||||||
)
|
)
|
||||||
if os.path.exists(target_dir):
|
if os.path.exists(target_dir):
|
||||||
@@ -450,16 +450,16 @@ class MultimodalProcessor:
|
|||||||
for dir_path in [self.video_dir, self.frames_dir, self.audio_dir]:
|
for dir_path in [self.video_dir, self.frames_dir, self.audio_dir]:
|
||||||
if os.path.exists(dir_path):
|
if os.path.exists(dir_path):
|
||||||
shutil.rmtree(dir_path)
|
shutil.rmtree(dir_path)
|
||||||
os.makedirs(dir_path, exist_ok = True)
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
# Singleton instance
|
# Singleton instance
|
||||||
_multimodal_processor = None
|
_multimodal_processor = None
|
||||||
|
|
||||||
|
|
||||||
def get_multimodal_processor(temp_dir: str = None, frame_interval: int = 5) -> MultimodalProcessor:
|
def get_multimodal_processor(temp_dir: str = None, frame_interval: int = 5) -> MultimodalProcessor:
|
||||||
"""获取多模态处理器单例"""
|
"""获取多模态处理器单例"""
|
||||||
global _multimodal_processor
|
global _multimodal_processor
|
||||||
if _multimodal_processor is None:
|
if _multimodal_processor is None:
|
||||||
_multimodal_processor = MultimodalProcessor(temp_dir, frame_interval)
|
_multimodal_processor = MultimodalProcessor(temp_dir, frame_interval)
|
||||||
return _multimodal_processor
|
return _multimodal_processor
|
||||||
|
|||||||
+183
-183
@@ -10,20 +10,20 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Neo4j 连接配置
|
# Neo4j 连接配置
|
||||||
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
||||||
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "password")
|
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "password")
|
||||||
|
|
||||||
# 延迟导入,避免未安装时出错
|
# 延迟导入,避免未安装时出错
|
||||||
try:
|
try:
|
||||||
from neo4j import Driver, GraphDatabase
|
from neo4j import Driver, GraphDatabase
|
||||||
|
|
||||||
NEO4J_AVAILABLE = True
|
NEO4J_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
NEO4J_AVAILABLE = False
|
NEO4J_AVAILABLE = False
|
||||||
logger.warning("Neo4j driver not installed. Neo4j features will be disabled.")
|
logger.warning("Neo4j driver not installed. Neo4j features will be disabled.")
|
||||||
|
|
||||||
|
|
||||||
@@ -35,15 +35,15 @@ class GraphEntity:
|
|||||||
project_id: str
|
project_id: str
|
||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
definition: str = ""
|
definition: str = ""
|
||||||
aliases: list[str] = None
|
aliases: list[str] = None
|
||||||
properties: dict = None
|
properties: dict = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.aliases is None:
|
if self.aliases is None:
|
||||||
self.aliases = []
|
self.aliases = []
|
||||||
if self.properties is None:
|
if self.properties is None:
|
||||||
self.properties = {}
|
self.properties = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -54,12 +54,12 @@ class GraphRelation:
|
|||||||
source_id: str
|
source_id: str
|
||||||
target_id: str
|
target_id: str
|
||||||
relation_type: str
|
relation_type: str
|
||||||
evidence: str = ""
|
evidence: str = ""
|
||||||
properties: dict = None
|
properties: dict = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.properties is None:
|
if self.properties is None:
|
||||||
self.properties = {}
|
self.properties = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -69,7 +69,7 @@ class PathResult:
|
|||||||
nodes: list[dict]
|
nodes: list[dict]
|
||||||
relationships: list[dict]
|
relationships: list[dict]
|
||||||
length: int
|
length: int
|
||||||
total_weight: float = 0.0
|
total_weight: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -79,7 +79,7 @@ class CommunityResult:
|
|||||||
community_id: int
|
community_id: int
|
||||||
nodes: list[dict]
|
nodes: list[dict]
|
||||||
size: int
|
size: int
|
||||||
density: float = 0.0
|
density: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -89,17 +89,17 @@ class CentralityResult:
|
|||||||
entity_id: str
|
entity_id: str
|
||||||
entity_name: str
|
entity_name: str
|
||||||
score: float
|
score: float
|
||||||
rank: int = 0
|
rank: int = 0
|
||||||
|
|
||||||
|
|
||||||
class Neo4jManager:
|
class Neo4jManager:
|
||||||
"""Neo4j 图数据库管理器"""
|
"""Neo4j 图数据库管理器"""
|
||||||
|
|
||||||
def __init__(self, uri: str = None, user: str = None, password: str = None) -> None:
|
def __init__(self, uri: str = None, user: str = None, password: str = None) -> None:
|
||||||
self.uri = uri or NEO4J_URI
|
self.uri = uri or NEO4J_URI
|
||||||
self.user = user or NEO4J_USER
|
self.user = user or NEO4J_USER
|
||||||
self.password = password or NEO4J_PASSWORD
|
self.password = password or NEO4J_PASSWORD
|
||||||
self._driver: Driver | None = None
|
self._driver: Driver | None = None
|
||||||
|
|
||||||
if not NEO4J_AVAILABLE:
|
if not NEO4J_AVAILABLE:
|
||||||
logger.error("Neo4j driver not available. Please install: pip install neo4j")
|
logger.error("Neo4j driver not available. Please install: pip install neo4j")
|
||||||
@@ -113,13 +113,13 @@ class Neo4jManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._driver = GraphDatabase.driver(self.uri, auth = (self.user, self.password))
|
self._driver = GraphDatabase.driver(self.uri, auth=(self.user, self.password))
|
||||||
# 验证连接
|
# 验证连接
|
||||||
self._driver.verify_connectivity()
|
self._driver.verify_connectivity()
|
||||||
logger.info(f"Connected to Neo4j at {self.uri}")
|
logger.info(f"Connected to Neo4j at {self.uri}")
|
||||||
except (RuntimeError, ValueError, TypeError) as e:
|
except (RuntimeError, ValueError, TypeError) as e:
|
||||||
logger.error(f"Failed to connect to Neo4j: {e}")
|
logger.error(f"Failed to connect to Neo4j: {e}")
|
||||||
self._driver = None
|
self._driver = None
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""关闭连接"""
|
"""关闭连接"""
|
||||||
@@ -179,7 +179,7 @@ class Neo4jManager:
|
|||||||
# ==================== 数据同步 ====================
|
# ==================== 数据同步 ====================
|
||||||
|
|
||||||
def sync_project(
|
def sync_project(
|
||||||
self, project_id: str, project_name: str, project_description: str = ""
|
self, project_id: str, project_name: str, project_description: str = ""
|
||||||
) -> None:
|
) -> None:
|
||||||
"""同步项目节点到 Neo4j"""
|
"""同步项目节点到 Neo4j"""
|
||||||
if not self._driver:
|
if not self._driver:
|
||||||
@@ -193,9 +193,9 @@ class Neo4jManager:
|
|||||||
p.description = $description,
|
p.description = $description,
|
||||||
p.updated_at = datetime()
|
p.updated_at = datetime()
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
name = project_name,
|
name=project_name,
|
||||||
description = project_description,
|
description=project_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_entity(self, entity: GraphEntity) -> None:
|
def sync_entity(self, entity: GraphEntity) -> None:
|
||||||
@@ -218,13 +218,13 @@ class Neo4jManager:
|
|||||||
MATCH (p:Project {id: $project_id})
|
MATCH (p:Project {id: $project_id})
|
||||||
MERGE (e)-[:BELONGS_TO]->(p)
|
MERGE (e)-[:BELONGS_TO]->(p)
|
||||||
""",
|
""",
|
||||||
id = entity.id,
|
id=entity.id,
|
||||||
project_id = entity.project_id,
|
project_id=entity.project_id,
|
||||||
name = entity.name,
|
name=entity.name,
|
||||||
type = entity.type,
|
type=entity.type,
|
||||||
definition = entity.definition,
|
definition=entity.definition,
|
||||||
aliases = json.dumps(entity.aliases),
|
aliases=json.dumps(entity.aliases),
|
||||||
properties = json.dumps(entity.properties),
|
properties=json.dumps(entity.properties),
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_entities_batch(self, entities: list[GraphEntity]) -> None:
|
def sync_entities_batch(self, entities: list[GraphEntity]) -> None:
|
||||||
@@ -234,7 +234,7 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
# 使用 UNWIND 批量处理
|
# 使用 UNWIND 批量处理
|
||||||
entities_data = [
|
entities_data = [
|
||||||
{
|
{
|
||||||
"id": e.id,
|
"id": e.id,
|
||||||
"project_id": e.project_id,
|
"project_id": e.project_id,
|
||||||
@@ -261,7 +261,7 @@ class Neo4jManager:
|
|||||||
MATCH (p:Project {id: entity.project_id})
|
MATCH (p:Project {id: entity.project_id})
|
||||||
MERGE (e)-[:BELONGS_TO]->(p)
|
MERGE (e)-[:BELONGS_TO]->(p)
|
||||||
""",
|
""",
|
||||||
entities = entities_data,
|
entities=entities_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_relation(self, relation: GraphRelation) -> None:
|
def sync_relation(self, relation: GraphRelation) -> None:
|
||||||
@@ -280,12 +280,12 @@ class Neo4jManager:
|
|||||||
r.properties = $properties,
|
r.properties = $properties,
|
||||||
r.updated_at = datetime()
|
r.updated_at = datetime()
|
||||||
""",
|
""",
|
||||||
id = relation.id,
|
id=relation.id,
|
||||||
source_id = relation.source_id,
|
source_id=relation.source_id,
|
||||||
target_id = relation.target_id,
|
target_id=relation.target_id,
|
||||||
relation_type = relation.relation_type,
|
relation_type=relation.relation_type,
|
||||||
evidence = relation.evidence,
|
evidence=relation.evidence,
|
||||||
properties = json.dumps(relation.properties),
|
properties=json.dumps(relation.properties),
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_relations_batch(self, relations: list[GraphRelation]) -> None:
|
def sync_relations_batch(self, relations: list[GraphRelation]) -> None:
|
||||||
@@ -294,7 +294,7 @@ class Neo4jManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
relations_data = [
|
relations_data = [
|
||||||
{
|
{
|
||||||
"id": r.id,
|
"id": r.id,
|
||||||
"source_id": r.source_id,
|
"source_id": r.source_id,
|
||||||
@@ -317,7 +317,7 @@ class Neo4jManager:
|
|||||||
r.properties = rel.properties,
|
r.properties = rel.properties,
|
||||||
r.updated_at = datetime()
|
r.updated_at = datetime()
|
||||||
""",
|
""",
|
||||||
relations = relations_data,
|
relations=relations_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_entity(self, entity_id: str) -> None:
|
def delete_entity(self, entity_id: str) -> None:
|
||||||
@@ -331,7 +331,7 @@ class Neo4jManager:
|
|||||||
MATCH (e:Entity {id: $id})
|
MATCH (e:Entity {id: $id})
|
||||||
DETACH DELETE e
|
DETACH DELETE e
|
||||||
""",
|
""",
|
||||||
id = entity_id,
|
id=entity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_project(self, project_id: str) -> None:
|
def delete_project(self, project_id: str) -> None:
|
||||||
@@ -346,13 +346,13 @@ class Neo4jManager:
|
|||||||
OPTIONAL MATCH (e:Entity)-[:BELONGS_TO]->(p)
|
OPTIONAL MATCH (e:Entity)-[:BELONGS_TO]->(p)
|
||||||
DETACH DELETE e, p
|
DETACH DELETE e, p
|
||||||
""",
|
""",
|
||||||
id = project_id,
|
id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ==================== 复杂图查询 ====================
|
# ==================== 复杂图查询 ====================
|
||||||
|
|
||||||
def find_shortest_path(
|
def find_shortest_path(
|
||||||
self, source_id: str, target_id: str, max_depth: int = 10
|
self, source_id: str, target_id: str, max_depth: int = 10
|
||||||
) -> PathResult | None:
|
) -> PathResult | None:
|
||||||
"""
|
"""
|
||||||
查找两个实体之间的最短路径
|
查找两个实体之间的最短路径
|
||||||
@@ -369,31 +369,31 @@ class Neo4jManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH path = shortestPath(
|
MATCH path = shortestPath(
|
||||||
(source:Entity {id: $source_id})-[*1..$max_depth]-(target:Entity {id: $target_id})
|
(source:Entity {id: $source_id})-[*1..$max_depth]-(target:Entity {id: $target_id})
|
||||||
)
|
)
|
||||||
RETURN path
|
RETURN path
|
||||||
""",
|
""",
|
||||||
source_id = source_id,
|
source_id=source_id,
|
||||||
target_id = target_id,
|
target_id=target_id,
|
||||||
max_depth = max_depth,
|
max_depth=max_depth,
|
||||||
)
|
)
|
||||||
|
|
||||||
record = result.single()
|
record = result.single()
|
||||||
if not record:
|
if not record:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path = record["path"]
|
path = record["path"]
|
||||||
|
|
||||||
# 提取节点和关系
|
# 提取节点和关系
|
||||||
nodes = [
|
nodes = [
|
||||||
{"id": node["id"], "name": node["name"], "type": node["type"]}
|
{"id": node["id"], "name": node["name"], "type": node["type"]}
|
||||||
for node in path.nodes
|
for node in path.nodes
|
||||||
]
|
]
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
{
|
{
|
||||||
"source": rel.start_node["id"],
|
"source": rel.start_node["id"],
|
||||||
"target": rel.end_node["id"],
|
"target": rel.end_node["id"],
|
||||||
@@ -404,11 +404,11 @@ class Neo4jManager:
|
|||||||
]
|
]
|
||||||
|
|
||||||
return PathResult(
|
return PathResult(
|
||||||
nodes = nodes, relationships = relationships, length = len(path.relationships)
|
nodes=nodes, relationships=relationships, length=len(path.relationships)
|
||||||
)
|
)
|
||||||
|
|
||||||
def find_all_paths(
|
def find_all_paths(
|
||||||
self, source_id: str, target_id: str, max_depth: int = 5, limit: int = 10
|
self, source_id: str, target_id: str, max_depth: int = 5, limit: int = 10
|
||||||
) -> list[PathResult]:
|
) -> list[PathResult]:
|
||||||
"""
|
"""
|
||||||
查找两个实体之间的所有路径
|
查找两个实体之间的所有路径
|
||||||
@@ -426,29 +426,29 @@ class Neo4jManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH path = (source:Entity {id: $source_id})-[*1..$max_depth]-(target:Entity {id: $target_id})
|
MATCH path = (source:Entity {id: $source_id})-[*1..$max_depth]-(target:Entity {id: $target_id})
|
||||||
WHERE source <> target
|
WHERE source <> target
|
||||||
RETURN path
|
RETURN path
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
""",
|
""",
|
||||||
source_id = source_id,
|
source_id=source_id,
|
||||||
target_id = target_id,
|
target_id=target_id,
|
||||||
max_depth = max_depth,
|
max_depth=max_depth,
|
||||||
limit = limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
for record in result:
|
for record in result:
|
||||||
path = record["path"]
|
path = record["path"]
|
||||||
|
|
||||||
nodes = [
|
nodes = [
|
||||||
{"id": node["id"], "name": node["name"], "type": node["type"]}
|
{"id": node["id"], "name": node["name"], "type": node["type"]}
|
||||||
for node in path.nodes
|
for node in path.nodes
|
||||||
]
|
]
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
{
|
{
|
||||||
"source": rel.start_node["id"],
|
"source": rel.start_node["id"],
|
||||||
"target": rel.end_node["id"],
|
"target": rel.end_node["id"],
|
||||||
@@ -460,14 +460,14 @@ class Neo4jManager:
|
|||||||
|
|
||||||
paths.append(
|
paths.append(
|
||||||
PathResult(
|
PathResult(
|
||||||
nodes = nodes, relationships = relationships, length = len(path.relationships)
|
nodes=nodes, relationships=relationships, length=len(path.relationships)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
def find_neighbors(
|
def find_neighbors(
|
||||||
self, entity_id: str, relation_type: str = None, limit: int = 50
|
self, entity_id: str, relation_type: str = None, limit: int = 50
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
查找实体的邻居节点
|
查找实体的邻居节点
|
||||||
@@ -485,30 +485,30 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
if relation_type:
|
if relation_type:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity {id: $entity_id})-[r:RELATES_TO {relation_type: $relation_type}]-(neighbor:Entity)
|
MATCH (e:Entity {id: $entity_id})-[r:RELATES_TO {relation_type: $relation_type}]-(neighbor:Entity)
|
||||||
RETURN neighbor, r.relation_type as rel_type, r.evidence as evidence
|
RETURN neighbor, r.relation_type as rel_type, r.evidence as evidence
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
""",
|
""",
|
||||||
entity_id = entity_id,
|
entity_id=entity_id,
|
||||||
relation_type = relation_type,
|
relation_type=relation_type,
|
||||||
limit = limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity {id: $entity_id})-[r:RELATES_TO]-(neighbor:Entity)
|
MATCH (e:Entity {id: $entity_id})-[r:RELATES_TO]-(neighbor:Entity)
|
||||||
RETURN neighbor, r.relation_type as rel_type, r.evidence as evidence
|
RETURN neighbor, r.relation_type as rel_type, r.evidence as evidence
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
""",
|
""",
|
||||||
entity_id = entity_id,
|
entity_id=entity_id,
|
||||||
limit = limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
neighbors = []
|
neighbors = []
|
||||||
for record in result:
|
for record in result:
|
||||||
node = record["neighbor"]
|
node = record["neighbor"]
|
||||||
neighbors.append(
|
neighbors.append(
|
||||||
{
|
{
|
||||||
"id": node["id"],
|
"id": node["id"],
|
||||||
@@ -536,13 +536,13 @@ class Neo4jManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e1:Entity {id: $id1})-[:RELATES_TO]-(common:Entity)-[:RELATES_TO]-(e2:Entity {id: $id2})
|
MATCH (e1:Entity {id: $id1})-[:RELATES_TO]-(common:Entity)-[:RELATES_TO]-(e2:Entity {id: $id2})
|
||||||
RETURN DISTINCT common
|
RETURN DISTINCT common
|
||||||
""",
|
""",
|
||||||
id1 = entity_id1,
|
id1=entity_id1,
|
||||||
id2 = entity_id2,
|
id2=entity_id2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -556,7 +556,7 @@ class Neo4jManager:
|
|||||||
|
|
||||||
# ==================== 图算法分析 ====================
|
# ==================== 图算法分析 ====================
|
||||||
|
|
||||||
def calculate_pagerank(self, project_id: str, top_n: int = 20) -> list[CentralityResult]:
|
def calculate_pagerank(self, project_id: str, top_n: int = 20) -> list[CentralityResult]:
|
||||||
"""
|
"""
|
||||||
计算 PageRank 中心性
|
计算 PageRank 中心性
|
||||||
|
|
||||||
@@ -571,7 +571,7 @@ class Neo4jManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
CALL gds.graph.exists('project-graph-$project_id') YIELD exists
|
CALL gds.graph.exists('project-graph-$project_id') YIELD exists
|
||||||
WITH exists
|
WITH exists
|
||||||
@@ -581,7 +581,7 @@ class Neo4jManager:
|
|||||||
{}
|
{}
|
||||||
) YIELD value RETURN value
|
) YIELD value RETURN value
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建临时图
|
# 创建临时图
|
||||||
@@ -601,11 +601,11 @@ class Neo4jManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 运行 PageRank
|
# 运行 PageRank
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
CALL gds.pageRank.stream('project-graph-$project_id')
|
CALL gds.pageRank.stream('project-graph-$project_id')
|
||||||
YIELD nodeId, score
|
YIELD nodeId, score
|
||||||
@@ -615,19 +615,19 @@ class Neo4jManager:
|
|||||||
ORDER BY score DESC
|
ORDER BY score DESC
|
||||||
LIMIT $top_n
|
LIMIT $top_n
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
top_n = top_n,
|
top_n=top_n,
|
||||||
)
|
)
|
||||||
|
|
||||||
rankings = []
|
rankings = []
|
||||||
rank = 1
|
rank = 1
|
||||||
for record in result:
|
for record in result:
|
||||||
rankings.append(
|
rankings.append(
|
||||||
CentralityResult(
|
CentralityResult(
|
||||||
entity_id = record["entity_id"],
|
entity_id=record["entity_id"],
|
||||||
entity_name = record["entity_name"],
|
entity_name=record["entity_name"],
|
||||||
score = record["score"],
|
score=record["score"],
|
||||||
rank = rank,
|
rank=rank,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rank += 1
|
rank += 1
|
||||||
@@ -637,12 +637,12 @@ class Neo4jManager:
|
|||||||
"""
|
"""
|
||||||
CALL gds.graph.drop('project-graph-$project_id')
|
CALL gds.graph.drop('project-graph-$project_id')
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return rankings
|
return rankings
|
||||||
|
|
||||||
def calculate_betweenness(self, project_id: str, top_n: int = 20) -> list[CentralityResult]:
|
def calculate_betweenness(self, project_id: str, top_n: int = 20) -> list[CentralityResult]:
|
||||||
"""
|
"""
|
||||||
计算 Betweenness 中心性(桥梁作用)
|
计算 Betweenness 中心性(桥梁作用)
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
# 使用 APOC 的 betweenness 计算(如果没有 GDS)
|
# 使用 APOC 的 betweenness 计算(如果没有 GDS)
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
||||||
@@ -667,19 +667,19 @@ class Neo4jManager:
|
|||||||
LIMIT $top_n
|
LIMIT $top_n
|
||||||
RETURN e.id as entity_id, e.name as entity_name, degree as score
|
RETURN e.id as entity_id, e.name as entity_name, degree as score
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
top_n = top_n,
|
top_n=top_n,
|
||||||
)
|
)
|
||||||
|
|
||||||
rankings = []
|
rankings = []
|
||||||
rank = 1
|
rank = 1
|
||||||
for record in result:
|
for record in result:
|
||||||
rankings.append(
|
rankings.append(
|
||||||
CentralityResult(
|
CentralityResult(
|
||||||
entity_id = record["entity_id"],
|
entity_id=record["entity_id"],
|
||||||
entity_name = record["entity_name"],
|
entity_name=record["entity_name"],
|
||||||
score = float(record["score"]),
|
score=float(record["score"]),
|
||||||
rank = rank,
|
rank=rank,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rank += 1
|
rank += 1
|
||||||
@@ -701,7 +701,7 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
# 简单的社区检测:基于连通分量
|
# 简单的社区检测:基于连通分量
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)-[:BELONGS_TO]->(p)
|
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)-[:BELONGS_TO]->(p)
|
||||||
@@ -710,25 +710,25 @@ class Neo4jManager:
|
|||||||
connections, size(connections) as connection_count
|
connections, size(connections) as connection_count
|
||||||
ORDER BY connection_count DESC
|
ORDER BY connection_count DESC
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 手动分组(基于连通性)
|
# 手动分组(基于连通性)
|
||||||
communities = {}
|
communities = {}
|
||||||
for record in result:
|
for record in result:
|
||||||
entity_id = record["entity_id"]
|
entity_id = record["entity_id"]
|
||||||
connections = record["connections"]
|
connections = record["connections"]
|
||||||
|
|
||||||
# 找到所属的社区
|
# 找到所属的社区
|
||||||
found_community = None
|
found_community = None
|
||||||
for comm_id, comm_data in communities.items():
|
for comm_id, comm_data in communities.items():
|
||||||
if any(conn in comm_data["member_ids"] for conn in connections):
|
if any(conn in comm_data["member_ids"] for conn in connections):
|
||||||
found_community = comm_id
|
found_community = comm_id
|
||||||
break
|
break
|
||||||
|
|
||||||
if found_community is None:
|
if found_community is None:
|
||||||
found_community = len(communities)
|
found_community = len(communities)
|
||||||
communities[found_community] = {"member_ids": set(), "nodes": []}
|
communities[found_community] = {"member_ids": set(), "nodes": []}
|
||||||
|
|
||||||
communities[found_community]["member_ids"].add(entity_id)
|
communities[found_community]["member_ids"].add(entity_id)
|
||||||
communities[found_community]["nodes"].append(
|
communities[found_community]["nodes"].append(
|
||||||
@@ -741,27 +741,27 @@ class Neo4jManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 构建结果
|
# 构建结果
|
||||||
results = []
|
results = []
|
||||||
for comm_id, comm_data in communities.items():
|
for comm_id, comm_data in communities.items():
|
||||||
nodes = comm_data["nodes"]
|
nodes = comm_data["nodes"]
|
||||||
size = len(nodes)
|
size = len(nodes)
|
||||||
# 计算密度(简化版)
|
# 计算密度(简化版)
|
||||||
max_edges = size * (size - 1) / 2 if size > 1 else 1
|
max_edges = size * (size - 1) / 2 if size > 1 else 1
|
||||||
actual_edges = sum(n["connections"] for n in nodes) / 2
|
actual_edges = sum(n["connections"] for n in nodes) / 2
|
||||||
density = actual_edges / max_edges if max_edges > 0 else 0
|
density = actual_edges / max_edges if max_edges > 0 else 0
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
CommunityResult(
|
CommunityResult(
|
||||||
community_id = comm_id, nodes = nodes, size = size, density = min(density, 1.0)
|
community_id=comm_id, nodes=nodes, size=size, density=min(density, 1.0)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 按大小排序
|
# 按大小排序
|
||||||
results.sort(key = lambda x: x.size, reverse = True)
|
results.sort(key=lambda x: x.size, reverse=True)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def find_central_entities(
|
def find_central_entities(
|
||||||
self, project_id: str, metric: str = "degree"
|
self, project_id: str, metric: str = "degree"
|
||||||
) -> list[CentralityResult]:
|
) -> list[CentralityResult]:
|
||||||
"""
|
"""
|
||||||
查找中心实体
|
查找中心实体
|
||||||
@@ -778,7 +778,7 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
if metric == "degree":
|
if metric == "degree":
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
||||||
@@ -787,11 +787,11 @@ class Neo4jManager:
|
|||||||
ORDER BY degree DESC
|
ORDER BY degree DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 默认使用度中心性
|
# 默认使用度中心性
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
OPTIONAL MATCH (e)-[:RELATES_TO]-(other:Entity)
|
||||||
@@ -800,18 +800,18 @@ class Neo4jManager:
|
|||||||
ORDER BY degree DESC
|
ORDER BY degree DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
rankings = []
|
rankings = []
|
||||||
rank = 1
|
rank = 1
|
||||||
for record in result:
|
for record in result:
|
||||||
rankings.append(
|
rankings.append(
|
||||||
CentralityResult(
|
CentralityResult(
|
||||||
entity_id = record["entity_id"],
|
entity_id=record["entity_id"],
|
||||||
entity_name = record["entity_name"],
|
entity_name=record["entity_name"],
|
||||||
score = float(record["score"]),
|
score=float(record["score"]),
|
||||||
rank = rank,
|
rank=rank,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
rank += 1
|
rank += 1
|
||||||
@@ -835,49 +835,49 @@ class Neo4jManager:
|
|||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
# 实体数量
|
# 实体数量
|
||||||
entity_count = session.run(
|
entity_count = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
RETURN count(e) as count
|
RETURN count(e) as count
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
).single()["count"]
|
).single()["count"]
|
||||||
|
|
||||||
# 关系数量
|
# 关系数量
|
||||||
relation_count = session.run(
|
relation_count = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
MATCH (e)-[r:RELATES_TO]-()
|
MATCH (e)-[r:RELATES_TO]-()
|
||||||
RETURN count(r) as count
|
RETURN count(r) as count
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
).single()["count"]
|
).single()["count"]
|
||||||
|
|
||||||
# 实体类型分布
|
# 实体类型分布
|
||||||
type_distribution = session.run(
|
type_distribution = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
RETURN e.type as type, count(e) as count
|
RETURN e.type as type, count(e) as count
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
types = {record["type"]: record["count"] for record in type_distribution}
|
types = {record["type"]: record["count"] for record in type_distribution}
|
||||||
|
|
||||||
# 平均度
|
# 平均度
|
||||||
avg_degree = session.run(
|
avg_degree = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
OPTIONAL MATCH (e)-[:RELATES_TO]-(other)
|
OPTIONAL MATCH (e)-[:RELATES_TO]-(other)
|
||||||
WITH e, count(other) as degree
|
WITH e, count(other) as degree
|
||||||
RETURN avg(degree) as avg_degree
|
RETURN avg(degree) as avg_degree
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
).single()["avg_degree"]
|
).single()["avg_degree"]
|
||||||
|
|
||||||
# 关系类型分布
|
# 关系类型分布
|
||||||
rel_types = session.run(
|
rel_types = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
MATCH (e:Entity)-[:BELONGS_TO]->(p:Project {id: $project_id})
|
||||||
MATCH (e)-[r:RELATES_TO]-()
|
MATCH (e)-[r:RELATES_TO]-()
|
||||||
@@ -885,10 +885,10 @@ class Neo4jManager:
|
|||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
""",
|
""",
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
relation_types = {record["type"]: record["count"] for record in rel_types}
|
relation_types = {record["type"]: record["count"] for record in rel_types}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"entity_count": entity_count,
|
"entity_count": entity_count,
|
||||||
@@ -901,7 +901,7 @@ class Neo4jManager:
|
|||||||
else 0,
|
else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_subgraph(self, entity_ids: list[str], depth: int = 1) -> dict:
|
def get_subgraph(self, entity_ids: list[str], depth: int = 1) -> dict:
|
||||||
"""
|
"""
|
||||||
获取指定实体的子图
|
获取指定实体的子图
|
||||||
|
|
||||||
@@ -916,7 +916,7 @@ class Neo4jManager:
|
|||||||
return {"nodes": [], "relationships": []}
|
return {"nodes": [], "relationships": []}
|
||||||
|
|
||||||
with self._driver.session() as session:
|
with self._driver.session() as session:
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (e:Entity)
|
MATCH (e:Entity)
|
||||||
WHERE e.id IN $entity_ids
|
WHERE e.id IN $entity_ids
|
||||||
@@ -927,14 +927,14 @@ class Neo4jManager:
|
|||||||
}) YIELD node
|
}) YIELD node
|
||||||
RETURN DISTINCT node
|
RETURN DISTINCT node
|
||||||
""",
|
""",
|
||||||
entity_ids = entity_ids,
|
entity_ids=entity_ids,
|
||||||
depth = depth,
|
depth=depth,
|
||||||
)
|
)
|
||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
node_ids = set()
|
node_ids = set()
|
||||||
for record in result:
|
for record in result:
|
||||||
node = record["node"]
|
node = record["node"]
|
||||||
node_ids.add(node["id"])
|
node_ids.add(node["id"])
|
||||||
nodes.append(
|
nodes.append(
|
||||||
{
|
{
|
||||||
@@ -946,17 +946,17 @@ class Neo4jManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 获取这些节点之间的关系
|
# 获取这些节点之间的关系
|
||||||
result = session.run(
|
result = session.run(
|
||||||
"""
|
"""
|
||||||
MATCH (source:Entity)-[r:RELATES_TO]->(target:Entity)
|
MATCH (source:Entity)-[r:RELATES_TO]->(target:Entity)
|
||||||
WHERE source.id IN $node_ids AND target.id IN $node_ids
|
WHERE source.id IN $node_ids AND target.id IN $node_ids
|
||||||
RETURN source.id as source_id, target.id as target_id,
|
RETURN source.id as source_id, target.id as target_id,
|
||||||
r.relation_type as type, r.evidence as evidence
|
r.relation_type as type, r.evidence as evidence
|
||||||
""",
|
""",
|
||||||
node_ids = list(node_ids),
|
node_ids=list(node_ids),
|
||||||
)
|
)
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
{
|
{
|
||||||
"source": record["source_id"],
|
"source": record["source_id"],
|
||||||
"target": record["target_id"],
|
"target": record["target_id"],
|
||||||
@@ -970,14 +970,14 @@ class Neo4jManager:
|
|||||||
|
|
||||||
|
|
||||||
# 全局单例
|
# 全局单例
|
||||||
_neo4j_manager = None
|
_neo4j_manager = None
|
||||||
|
|
||||||
|
|
||||||
def get_neo4j_manager() -> Neo4jManager:
|
def get_neo4j_manager() -> Neo4jManager:
|
||||||
"""获取 Neo4j 管理器单例"""
|
"""获取 Neo4j 管理器单例"""
|
||||||
global _neo4j_manager
|
global _neo4j_manager
|
||||||
if _neo4j_manager is None:
|
if _neo4j_manager is None:
|
||||||
_neo4j_manager = Neo4jManager()
|
_neo4j_manager = Neo4jManager()
|
||||||
return _neo4j_manager
|
return _neo4j_manager
|
||||||
|
|
||||||
|
|
||||||
@@ -986,7 +986,7 @@ def close_neo4j_manager() -> None:
|
|||||||
global _neo4j_manager
|
global _neo4j_manager
|
||||||
if _neo4j_manager:
|
if _neo4j_manager:
|
||||||
_neo4j_manager.close()
|
_neo4j_manager.close()
|
||||||
_neo4j_manager = None
|
_neo4j_manager = None
|
||||||
|
|
||||||
|
|
||||||
# 便捷函数
|
# 便捷函数
|
||||||
@@ -1004,7 +1004,7 @@ def sync_project_to_neo4j(
|
|||||||
entities: 实体列表(字典格式)
|
entities: 实体列表(字典格式)
|
||||||
relations: 关系列表(字典格式)
|
relations: 关系列表(字典格式)
|
||||||
"""
|
"""
|
||||||
manager = get_neo4j_manager()
|
manager = get_neo4j_manager()
|
||||||
if not manager.is_connected():
|
if not manager.is_connected():
|
||||||
logger.warning("Neo4j not connected, skipping sync")
|
logger.warning("Neo4j not connected, skipping sync")
|
||||||
return
|
return
|
||||||
@@ -1013,29 +1013,29 @@ def sync_project_to_neo4j(
|
|||||||
manager.sync_project(project_id, project_name)
|
manager.sync_project(project_id, project_name)
|
||||||
|
|
||||||
# 同步实体
|
# 同步实体
|
||||||
graph_entities = [
|
graph_entities = [
|
||||||
GraphEntity(
|
GraphEntity(
|
||||||
id = e["id"],
|
id=e["id"],
|
||||||
project_id = project_id,
|
project_id=project_id,
|
||||||
name = e["name"],
|
name=e["name"],
|
||||||
type = e.get("type", "unknown"),
|
type=e.get("type", "unknown"),
|
||||||
definition = e.get("definition", ""),
|
definition=e.get("definition", ""),
|
||||||
aliases = e.get("aliases", []),
|
aliases=e.get("aliases", []),
|
||||||
properties = e.get("properties", {}),
|
properties=e.get("properties", {}),
|
||||||
)
|
)
|
||||||
for e in entities
|
for e in entities
|
||||||
]
|
]
|
||||||
manager.sync_entities_batch(graph_entities)
|
manager.sync_entities_batch(graph_entities)
|
||||||
|
|
||||||
# 同步关系
|
# 同步关系
|
||||||
graph_relations = [
|
graph_relations = [
|
||||||
GraphRelation(
|
GraphRelation(
|
||||||
id = r["id"],
|
id=r["id"],
|
||||||
source_id = r["source_entity_id"],
|
source_id=r["source_entity_id"],
|
||||||
target_id = r["target_entity_id"],
|
target_id=r["target_entity_id"],
|
||||||
relation_type = r["relation_type"],
|
relation_type=r["relation_type"],
|
||||||
evidence = r.get("evidence", ""),
|
evidence=r.get("evidence", ""),
|
||||||
properties = r.get("properties", {}),
|
properties=r.get("properties", {}),
|
||||||
)
|
)
|
||||||
for r in relations
|
for r in relations
|
||||||
]
|
]
|
||||||
@@ -1048,9 +1048,9 @@ def sync_project_to_neo4j(
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 测试代码
|
# 测试代码
|
||||||
logging.basicConfig(level = logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
manager = Neo4jManager()
|
manager = Neo4jManager()
|
||||||
|
|
||||||
if manager.is_connected():
|
if manager.is_connected():
|
||||||
print("✅ Connected to Neo4j")
|
print("✅ Connected to Neo4j")
|
||||||
@@ -1064,18 +1064,18 @@ if __name__ == "__main__":
|
|||||||
print("✅ Project synced")
|
print("✅ Project synced")
|
||||||
|
|
||||||
# 测试实体
|
# 测试实体
|
||||||
test_entity = GraphEntity(
|
test_entity = GraphEntity(
|
||||||
id = "test-entity-1",
|
id="test-entity-1",
|
||||||
project_id = "test-project",
|
project_id="test-project",
|
||||||
name = "Test Entity",
|
name="Test Entity",
|
||||||
type = "Person",
|
type="Person",
|
||||||
definition = "A test entity",
|
definition="A test entity",
|
||||||
)
|
)
|
||||||
manager.sync_entity(test_entity)
|
manager.sync_entity(test_entity)
|
||||||
print("✅ Entity synced")
|
print("✅ Entity synced")
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计
|
||||||
stats = manager.get_graph_stats("test-project")
|
stats = manager.get_graph_stats("test-project")
|
||||||
print(f"📊 Graph stats: {stats}")
|
print(f"📊 Graph stats: {stats}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
+794
-794
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -12,29 +12,29 @@ import oss2
|
|||||||
|
|
||||||
class OSSUploader:
|
class OSSUploader:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.access_key = os.getenv("ALI_ACCESS_KEY")
|
self.access_key = os.getenv("ALI_ACCESS_KEY")
|
||||||
self.secret_key = os.getenv("ALI_SECRET_KEY")
|
self.secret_key = os.getenv("ALI_SECRET_KEY")
|
||||||
self.bucket_name = os.getenv("OSS_BUCKET", "insightflow-audio")
|
self.bucket_name = os.getenv("OSS_BUCKET", "insightflow-audio")
|
||||||
self.region = os.getenv("OSS_REGION", "oss-cn-hangzhou.aliyuncs.com")
|
self.region = os.getenv("OSS_REGION", "oss-cn-hangzhou.aliyuncs.com")
|
||||||
self.endpoint = f"https://{self.region}"
|
self.endpoint = f"https://{self.region}"
|
||||||
|
|
||||||
if not self.access_key or not self.secret_key:
|
if not self.access_key or not self.secret_key:
|
||||||
raise ValueError("ALI_ACCESS_KEY and ALI_SECRET_KEY must be set")
|
raise ValueError("ALI_ACCESS_KEY and ALI_SECRET_KEY must be set")
|
||||||
|
|
||||||
self.auth = oss2.Auth(self.access_key, self.secret_key)
|
self.auth = oss2.Auth(self.access_key, self.secret_key)
|
||||||
self.bucket = oss2.Bucket(self.auth, self.endpoint, self.bucket_name)
|
self.bucket = oss2.Bucket(self.auth, self.endpoint, self.bucket_name)
|
||||||
|
|
||||||
def upload_audio(self, audio_data: bytes, filename: str) -> tuple:
|
def upload_audio(self, audio_data: bytes, filename: str) -> tuple:
|
||||||
"""上传音频到 OSS,返回 (URL, object_name)"""
|
"""上传音频到 OSS,返回 (URL, object_name)"""
|
||||||
# 生成唯一文件名
|
# 生成唯一文件名
|
||||||
ext = os.path.splitext(filename)[1] or ".wav"
|
ext = os.path.splitext(filename)[1] or ".wav"
|
||||||
object_name = f"audio/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}{ext}"
|
object_name = f"audio/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}{ext}"
|
||||||
|
|
||||||
# 上传文件
|
# 上传文件
|
||||||
self.bucket.put_object(object_name, audio_data)
|
self.bucket.put_object(object_name, audio_data)
|
||||||
|
|
||||||
# 生成临时访问 URL (1小时有效)
|
# 生成临时访问 URL (1小时有效)
|
||||||
url = self.bucket.sign_url("GET", object_name, 3600)
|
url = self.bucket.sign_url("GET", object_name, 3600)
|
||||||
return url, object_name
|
return url, object_name
|
||||||
|
|
||||||
def delete_object(self, object_name: str) -> None:
|
def delete_object(self, object_name: str) -> None:
|
||||||
@@ -43,11 +43,11 @@ class OSSUploader:
|
|||||||
|
|
||||||
|
|
||||||
# 单例
|
# 单例
|
||||||
_oss_uploader = None
|
_oss_uploader = None
|
||||||
|
|
||||||
|
|
||||||
def get_oss_uploader() -> OSSUploader:
|
def get_oss_uploader() -> OSSUploader:
|
||||||
global _oss_uploader
|
global _oss_uploader
|
||||||
if _oss_uploader is None:
|
if _oss_uploader is None:
|
||||||
_oss_uploader = OSSUploader()
|
_oss_uploader = OSSUploader()
|
||||||
return _oss_uploader
|
return _oss_uploader
|
||||||
|
|||||||
+306
-306
File diff suppressed because it is too large
Load Diff
+382
-383
File diff suppressed because it is too large
Load Diff
+55
-55
@@ -17,9 +17,9 @@ from functools import wraps
|
|||||||
class RateLimitConfig:
|
class RateLimitConfig:
|
||||||
"""限流配置"""
|
"""限流配置"""
|
||||||
|
|
||||||
requests_per_minute: int = 60
|
requests_per_minute: int = 60
|
||||||
burst_size: int = 10 # 突发请求数
|
burst_size: int = 10 # 突发请求数
|
||||||
window_size: int = 60 # 窗口大小(秒)
|
window_size: int = 60 # 窗口大小(秒)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -35,16 +35,16 @@ class RateLimitInfo:
|
|||||||
class SlidingWindowCounter:
|
class SlidingWindowCounter:
|
||||||
"""滑动窗口计数器"""
|
"""滑动窗口计数器"""
|
||||||
|
|
||||||
def __init__(self, window_size: int = 60) -> None:
|
def __init__(self, window_size: int = 60) -> None:
|
||||||
self.window_size = window_size
|
self.window_size = window_size
|
||||||
self.requests: dict[int, int] = defaultdict(int) # 秒级计数
|
self.requests: dict[int, int] = defaultdict(int) # 秒级计数
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._cleanup_lock = asyncio.Lock()
|
self._cleanup_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def add_request(self) -> int:
|
async def add_request(self) -> int:
|
||||||
"""添加请求,返回当前窗口内的请求数"""
|
"""添加请求,返回当前窗口内的请求数"""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
self.requests[now] += 1
|
self.requests[now] += 1
|
||||||
self._cleanup_old(now)
|
self._cleanup_old(now)
|
||||||
return sum(self.requests.values())
|
return sum(self.requests.values())
|
||||||
@@ -52,14 +52,14 @@ class SlidingWindowCounter:
|
|||||||
async def get_count(self) -> int:
|
async def get_count(self) -> int:
|
||||||
"""获取当前窗口内的请求数"""
|
"""获取当前窗口内的请求数"""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
self._cleanup_old(now)
|
self._cleanup_old(now)
|
||||||
return sum(self.requests.values())
|
return sum(self.requests.values())
|
||||||
|
|
||||||
def _cleanup_old(self, now: int) -> None:
|
def _cleanup_old(self, now: int) -> None:
|
||||||
"""清理过期的请求记录 - 使用独立锁避免竞态条件"""
|
"""清理过期的请求记录 - 使用独立锁避免竞态条件"""
|
||||||
cutoff = now - self.window_size
|
cutoff = now - self.window_size
|
||||||
old_keys = [k for k in list(self.requests.keys()) if k < cutoff]
|
old_keys = [k for k in list(self.requests.keys()) if k < cutoff]
|
||||||
for k in old_keys:
|
for k in old_keys:
|
||||||
self.requests.pop(k, None)
|
self.requests.pop(k, None)
|
||||||
|
|
||||||
@@ -69,13 +69,13 @@ class RateLimiter:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# key -> SlidingWindowCounter
|
# key -> SlidingWindowCounter
|
||||||
self.counters: dict[str, SlidingWindowCounter] = {}
|
self.counters: dict[str, SlidingWindowCounter] = {}
|
||||||
# key -> RateLimitConfig
|
# key -> RateLimitConfig
|
||||||
self.configs: dict[str, RateLimitConfig] = {}
|
self.configs: dict[str, RateLimitConfig] = {}
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._cleanup_lock = asyncio.Lock()
|
self._cleanup_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def is_allowed(self, key: str, config: RateLimitConfig | None = None) -> RateLimitInfo:
|
async def is_allowed(self, key: str, config: RateLimitConfig | None = None) -> RateLimitInfo:
|
||||||
"""
|
"""
|
||||||
检查是否允许请求
|
检查是否允许请求
|
||||||
|
|
||||||
@@ -87,70 +87,70 @@ class RateLimiter:
|
|||||||
RateLimitInfo
|
RateLimitInfo
|
||||||
"""
|
"""
|
||||||
if config is None:
|
if config is None:
|
||||||
config = RateLimitConfig()
|
config = RateLimitConfig()
|
||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if key not in self.counters:
|
if key not in self.counters:
|
||||||
self.counters[key] = SlidingWindowCounter(config.window_size)
|
self.counters[key] = SlidingWindowCounter(config.window_size)
|
||||||
self.configs[key] = config
|
self.configs[key] = config
|
||||||
|
|
||||||
counter = self.counters[key]
|
counter = self.counters[key]
|
||||||
stored_config = self.configs.get(key, config)
|
stored_config = self.configs.get(key, config)
|
||||||
|
|
||||||
# 获取当前计数
|
# 获取当前计数
|
||||||
current_count = await counter.get_count()
|
current_count = await counter.get_count()
|
||||||
|
|
||||||
# 计算剩余配额
|
# 计算剩余配额
|
||||||
remaining = max(0, stored_config.requests_per_minute - current_count)
|
remaining = max(0, stored_config.requests_per_minute - current_count)
|
||||||
|
|
||||||
# 计算重置时间
|
# 计算重置时间
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
reset_time = now + stored_config.window_size
|
reset_time = now + stored_config.window_size
|
||||||
|
|
||||||
# 检查是否超过限制
|
# 检查是否超过限制
|
||||||
if current_count >= stored_config.requests_per_minute:
|
if current_count >= stored_config.requests_per_minute:
|
||||||
return RateLimitInfo(
|
return RateLimitInfo(
|
||||||
allowed = False,
|
allowed=False,
|
||||||
remaining = 0,
|
remaining=0,
|
||||||
reset_time = reset_time,
|
reset_time=reset_time,
|
||||||
retry_after = stored_config.window_size,
|
retry_after=stored_config.window_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 允许请求,增加计数
|
# 允许请求,增加计数
|
||||||
await counter.add_request()
|
await counter.add_request()
|
||||||
|
|
||||||
return RateLimitInfo(
|
return RateLimitInfo(
|
||||||
allowed = True, remaining = remaining - 1, reset_time = reset_time, retry_after = 0
|
allowed=True, remaining=remaining - 1, reset_time=reset_time, retry_after=0
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_limit_info(self, key: str) -> RateLimitInfo:
|
async def get_limit_info(self, key: str) -> RateLimitInfo:
|
||||||
"""获取限流信息(不增加计数)"""
|
"""获取限流信息(不增加计数)"""
|
||||||
if key not in self.counters:
|
if key not in self.counters:
|
||||||
config = RateLimitConfig()
|
config = RateLimitConfig()
|
||||||
return RateLimitInfo(
|
return RateLimitInfo(
|
||||||
allowed = True,
|
allowed=True,
|
||||||
remaining = config.requests_per_minute,
|
remaining=config.requests_per_minute,
|
||||||
reset_time = int(time.time()) + config.window_size,
|
reset_time=int(time.time()) + config.window_size,
|
||||||
retry_after = 0,
|
retry_after=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
counter = self.counters[key]
|
counter = self.counters[key]
|
||||||
config = self.configs.get(key, RateLimitConfig())
|
config = self.configs.get(key, RateLimitConfig())
|
||||||
|
|
||||||
current_count = await counter.get_count()
|
current_count = await counter.get_count()
|
||||||
remaining = max(0, config.requests_per_minute - current_count)
|
remaining = max(0, config.requests_per_minute - current_count)
|
||||||
reset_time = int(time.time()) + config.window_size
|
reset_time = int(time.time()) + config.window_size
|
||||||
|
|
||||||
return RateLimitInfo(
|
return RateLimitInfo(
|
||||||
allowed = current_count < config.requests_per_minute,
|
allowed=current_count < config.requests_per_minute,
|
||||||
remaining = remaining,
|
remaining=remaining,
|
||||||
reset_time = reset_time,
|
reset_time=reset_time,
|
||||||
retry_after = max(0, config.window_size)
|
retry_after=max(0, config.window_size)
|
||||||
if current_count >= config.requests_per_minute
|
if current_count >= config.requests_per_minute
|
||||||
else 0,
|
else 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset(self, key: str | None = None) -> None:
|
def reset(self, key: str | None = None) -> None:
|
||||||
"""重置限流计数器"""
|
"""重置限流计数器"""
|
||||||
if key:
|
if key:
|
||||||
self.counters.pop(key, None)
|
self.counters.pop(key, None)
|
||||||
@@ -161,21 +161,21 @@ class RateLimiter:
|
|||||||
|
|
||||||
|
|
||||||
# 全局限流器实例
|
# 全局限流器实例
|
||||||
_rate_limiter: RateLimiter | None = None
|
_rate_limiter: RateLimiter | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_rate_limiter() -> RateLimiter:
|
def get_rate_limiter() -> RateLimiter:
|
||||||
"""获取限流器实例"""
|
"""获取限流器实例"""
|
||||||
global _rate_limiter
|
global _rate_limiter
|
||||||
if _rate_limiter is None:
|
if _rate_limiter is None:
|
||||||
_rate_limiter = RateLimiter()
|
_rate_limiter = RateLimiter()
|
||||||
return _rate_limiter
|
return _rate_limiter
|
||||||
|
|
||||||
|
|
||||||
# 限流装饰器(用于函数级别限流)
|
# 限流装饰器(用于函数级别限流)
|
||||||
|
|
||||||
|
|
||||||
def rate_limit(requests_per_minute: int = 60, key_func: Callable | None = None) -> None:
|
def rate_limit(requests_per_minute: int = 60, key_func: Callable | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
限流装饰器
|
限流装饰器
|
||||||
|
|
||||||
@@ -185,13 +185,13 @@ def rate_limit(requests_per_minute: int = 60, key_func: Callable | None = None
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func) -> None:
|
def decorator(func) -> None:
|
||||||
limiter = get_rate_limiter()
|
limiter = get_rate_limiter()
|
||||||
config = RateLimitConfig(requests_per_minute = requests_per_minute)
|
config = RateLimitConfig(requests_per_minute=requests_per_minute)
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def async_wrapper(*args, **kwargs) -> None:
|
async def async_wrapper(*args, **kwargs) -> None:
|
||||||
key = key_func(*args, **kwargs) if key_func else func.__name__
|
key = key_func(*args, **kwargs) if key_func else func.__name__
|
||||||
info = await limiter.is_allowed(key, config)
|
info = await limiter.is_allowed(key, config)
|
||||||
|
|
||||||
if not info.allowed:
|
if not info.allowed:
|
||||||
raise RateLimitExceeded(
|
raise RateLimitExceeded(
|
||||||
@@ -202,9 +202,9 @@ def rate_limit(requests_per_minute: int = 60, key_func: Callable | None = None
|
|||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def sync_wrapper(*args, **kwargs) -> None:
|
def sync_wrapper(*args, **kwargs) -> None:
|
||||||
key = key_func(*args, **kwargs) if key_func else func.__name__
|
key = key_func(*args, **kwargs) if key_func else func.__name__
|
||||||
# 同步版本使用 asyncio.run
|
# 同步版本使用 asyncio.run
|
||||||
info = asyncio.run(limiter.is_allowed(key, config))
|
info = asyncio.run(limiter.is_allowed(key, config))
|
||||||
|
|
||||||
if not info.allowed:
|
if not info.allowed:
|
||||||
raise RateLimitExceeded(
|
raise RateLimitExceeded(
|
||||||
|
|||||||
+405
-405
File diff suppressed because it is too large
Load Diff
+373
-373
File diff suppressed because it is too large
Load Diff
+414
-414
File diff suppressed because it is too large
Load Diff
+291
-291
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -42,7 +42,7 @@ except ImportError as e:
|
|||||||
print("\n2. 测试模块初始化...")
|
print("\n2. 测试模块初始化...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processor = get_multimodal_processor()
|
processor = get_multimodal_processor()
|
||||||
print(" ✓ MultimodalProcessor 初始化成功")
|
print(" ✓ MultimodalProcessor 初始化成功")
|
||||||
print(f" - 临时目录: {processor.temp_dir}")
|
print(f" - 临时目录: {processor.temp_dir}")
|
||||||
print(f" - 帧提取间隔: {processor.frame_interval}秒")
|
print(f" - 帧提取间隔: {processor.frame_interval}秒")
|
||||||
@@ -50,14 +50,14 @@ except Exception as e:
|
|||||||
print(f" ✗ MultimodalProcessor 初始化失败: {e}")
|
print(f" ✗ MultimodalProcessor 初始化失败: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
img_processor = get_image_processor()
|
img_processor = get_image_processor()
|
||||||
print(" ✓ ImageProcessor 初始化成功")
|
print(" ✓ ImageProcessor 初始化成功")
|
||||||
print(f" - 临时目录: {img_processor.temp_dir}")
|
print(f" - 临时目录: {img_processor.temp_dir}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ✗ ImageProcessor 初始化失败: {e}")
|
print(f" ✗ ImageProcessor 初始化失败: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
linker = get_multimodal_entity_linker()
|
linker = get_multimodal_entity_linker()
|
||||||
print(" ✓ MultimodalEntityLinker 初始化成功")
|
print(" ✓ MultimodalEntityLinker 初始化成功")
|
||||||
print(f" - 相似度阈值: {linker.similarity_threshold}")
|
print(f" - 相似度阈值: {linker.similarity_threshold}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -67,20 +67,20 @@ except Exception as e:
|
|||||||
print("\n3. 测试实体关联功能...")
|
print("\n3. 测试实体关联功能...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
linker = get_multimodal_entity_linker()
|
linker = get_multimodal_entity_linker()
|
||||||
|
|
||||||
# 测试字符串相似度
|
# 测试字符串相似度
|
||||||
sim = linker.calculate_string_similarity("Project Alpha", "Project Alpha")
|
sim = linker.calculate_string_similarity("Project Alpha", "Project Alpha")
|
||||||
assert sim == 1.0, "完全匹配应该返回1.0"
|
assert sim == 1.0, "完全匹配应该返回1.0"
|
||||||
print(f" ✓ 字符串相似度计算正常 (完全匹配: {sim})")
|
print(f" ✓ 字符串相似度计算正常 (完全匹配: {sim})")
|
||||||
|
|
||||||
sim = linker.calculate_string_similarity("K8s", "Kubernetes")
|
sim = linker.calculate_string_similarity("K8s", "Kubernetes")
|
||||||
print(f" ✓ 字符串相似度计算正常 (不同字符串: {sim:.2f})")
|
print(f" ✓ 字符串相似度计算正常 (不同字符串: {sim:.2f})")
|
||||||
|
|
||||||
# 测试实体相似度
|
# 测试实体相似度
|
||||||
entity1 = {"name": "Project Alpha", "type": "PROJECT", "definition": "核心项目"}
|
entity1 = {"name": "Project Alpha", "type": "PROJECT", "definition": "核心项目"}
|
||||||
entity2 = {"name": "Project Alpha", "type": "PROJECT", "definition": "主要项目"}
|
entity2 = {"name": "Project Alpha", "type": "PROJECT", "definition": "主要项目"}
|
||||||
sim, match_type = linker.calculate_entity_similarity(entity1, entity2)
|
sim, match_type = linker.calculate_entity_similarity(entity1, entity2)
|
||||||
print(f" ✓ 实体相似度计算正常 (相似度: {sim:.2f}, 类型: {match_type})")
|
print(f" ✓ 实体相似度计算正常 (相似度: {sim:.2f}, 类型: {match_type})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -90,7 +90,7 @@ except Exception as e:
|
|||||||
print("\n4. 测试图片处理器功能...")
|
print("\n4. 测试图片处理器功能...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processor = get_image_processor()
|
processor = get_image_processor()
|
||||||
|
|
||||||
# 测试图片类型检测(使用模拟数据)
|
# 测试图片类型检测(使用模拟数据)
|
||||||
print(f" ✓ 支持的图片类型: {list(processor.IMAGE_TYPES.keys())}")
|
print(f" ✓ 支持的图片类型: {list(processor.IMAGE_TYPES.keys())}")
|
||||||
@@ -103,7 +103,7 @@ except Exception as e:
|
|||||||
print("\n5. 测试视频处理器配置...")
|
print("\n5. 测试视频处理器配置...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processor = get_multimodal_processor()
|
processor = get_multimodal_processor()
|
||||||
|
|
||||||
print(f" ✓ 视频目录: {processor.video_dir}")
|
print(f" ✓ 视频目录: {processor.video_dir}")
|
||||||
print(f" ✓ 帧目录: {processor.frames_dir}")
|
print(f" ✓ 帧目录: {processor.frames_dir}")
|
||||||
@@ -129,11 +129,11 @@ print("\n6. 测试数据库多模态方法...")
|
|||||||
try:
|
try:
|
||||||
from db_manager import get_db_manager
|
from db_manager import get_db_manager
|
||||||
|
|
||||||
db = get_db_manager()
|
db = get_db_manager()
|
||||||
|
|
||||||
# 检查多模态表是否存在
|
# 检查多模态表是否存在
|
||||||
conn = db.get_conn()
|
conn = db.get_conn()
|
||||||
tables = ["videos", "video_frames", "images", "multimodal_mentions", "multimodal_entity_links"]
|
tables = ["videos", "video_frames", "images", "multimodal_mentions", "multimodal_entity_links"]
|
||||||
|
|
||||||
for table in tables:
|
for table in tables:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ def test_fulltext_search() -> None:
|
|||||||
print("测试全文搜索 (FullTextSearch)")
|
print("测试全文搜索 (FullTextSearch)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
search = FullTextSearch()
|
search = FullTextSearch()
|
||||||
|
|
||||||
# 测试索引创建
|
# 测试索引创建
|
||||||
print("\n1. 测试索引创建...")
|
print("\n1. 测试索引创建...")
|
||||||
success = search.index_content(
|
success = search.index_content(
|
||||||
content_id = "test_entity_1",
|
content_id="test_entity_1",
|
||||||
content_type = "entity",
|
content_type="entity",
|
||||||
project_id = "test_project",
|
project_id="test_project",
|
||||||
text = "这是一个测试实体,用于验证全文搜索功能。支持关键词高亮显示。",
|
text="这是一个测试实体,用于验证全文搜索功能。支持关键词高亮显示。",
|
||||||
)
|
)
|
||||||
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
||||||
|
|
||||||
# 测试搜索
|
# 测试搜索
|
||||||
print("\n2. 测试关键词搜索...")
|
print("\n2. 测试关键词搜索...")
|
||||||
results = search.search("测试", project_id = "test_project")
|
results = search.search("测试", project_id="test_project")
|
||||||
print(f" 搜索结果数量: {len(results)}")
|
print(f" 搜索结果数量: {len(results)}")
|
||||||
if results:
|
if results:
|
||||||
print(f" 第一个结果: {results[0].content[:50]}...")
|
print(f" 第一个结果: {results[0].content[:50]}...")
|
||||||
@@ -49,15 +49,15 @@ def test_fulltext_search() -> None:
|
|||||||
|
|
||||||
# 测试布尔搜索
|
# 测试布尔搜索
|
||||||
print("\n3. 测试布尔搜索...")
|
print("\n3. 测试布尔搜索...")
|
||||||
results = search.search("测试 AND 全文", project_id = "test_project")
|
results = search.search("测试 AND 全文", project_id="test_project")
|
||||||
print(f" AND 搜索结果: {len(results)}")
|
print(f" AND 搜索结果: {len(results)}")
|
||||||
|
|
||||||
results = search.search("测试 OR 关键词", project_id = "test_project")
|
results = search.search("测试 OR 关键词", project_id="test_project")
|
||||||
print(f" OR 搜索结果: {len(results)}")
|
print(f" OR 搜索结果: {len(results)}")
|
||||||
|
|
||||||
# 测试高亮
|
# 测试高亮
|
||||||
print("\n4. 测试文本高亮...")
|
print("\n4. 测试文本高亮...")
|
||||||
highlighted = search.highlight_text("这是一个测试实体,用于验证全文搜索功能。", "测试 全文")
|
highlighted = search.highlight_text("这是一个测试实体,用于验证全文搜索功能。", "测试 全文")
|
||||||
print(f" 高亮结果: {highlighted}")
|
print(f" 高亮结果: {highlighted}")
|
||||||
|
|
||||||
print("\n✓ 全文搜索测试完成")
|
print("\n✓ 全文搜索测试完成")
|
||||||
@@ -70,7 +70,7 @@ def test_semantic_search() -> None:
|
|||||||
print("测试语义搜索 (SemanticSearch)")
|
print("测试语义搜索 (SemanticSearch)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
semantic = SemanticSearch()
|
semantic = SemanticSearch()
|
||||||
|
|
||||||
# 检查可用性
|
# 检查可用性
|
||||||
print(f"\n1. 语义搜索可用性: {'✓ 可用' if semantic.is_available() else '✗ 不可用'}")
|
print(f"\n1. 语义搜索可用性: {'✓ 可用' if semantic.is_available() else '✗ 不可用'}")
|
||||||
@@ -81,18 +81,18 @@ def test_semantic_search() -> None:
|
|||||||
|
|
||||||
# 测试 embedding 生成
|
# 测试 embedding 生成
|
||||||
print("\n2. 测试 embedding 生成...")
|
print("\n2. 测试 embedding 生成...")
|
||||||
embedding = semantic.generate_embedding("这是一个测试句子")
|
embedding = semantic.generate_embedding("这是一个测试句子")
|
||||||
if embedding:
|
if embedding:
|
||||||
print(f" Embedding 维度: {len(embedding)}")
|
print(f" Embedding 维度: {len(embedding)}")
|
||||||
print(f" 前5个值: {embedding[:5]}")
|
print(f" 前5个值: {embedding[:5]}")
|
||||||
|
|
||||||
# 测试索引
|
# 测试索引
|
||||||
print("\n3. 测试语义索引...")
|
print("\n3. 测试语义索引...")
|
||||||
success = semantic.index_embedding(
|
success = semantic.index_embedding(
|
||||||
content_id = "test_content_1",
|
content_id="test_content_1",
|
||||||
content_type = "transcript",
|
content_type="transcript",
|
||||||
project_id = "test_project",
|
project_id="test_project",
|
||||||
text = "这是用于语义搜索测试的文本内容。",
|
text="这是用于语义搜索测试的文本内容。",
|
||||||
)
|
)
|
||||||
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
print(f" 索引创建: {'✓ 成功' if success else '✗ 失败'}")
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ def test_entity_path_discovery() -> None:
|
|||||||
print("测试实体路径发现 (EntityPathDiscovery)")
|
print("测试实体路径发现 (EntityPathDiscovery)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
discovery = EntityPathDiscovery()
|
discovery = EntityPathDiscovery()
|
||||||
|
|
||||||
print("\n1. 测试路径发现初始化...")
|
print("\n1. 测试路径发现初始化...")
|
||||||
print(f" 数据库路径: {discovery.db_path}")
|
print(f" 数据库路径: {discovery.db_path}")
|
||||||
@@ -125,7 +125,7 @@ def test_knowledge_gap_detection() -> None:
|
|||||||
print("测试知识缺口识别 (KnowledgeGapDetection)")
|
print("测试知识缺口识别 (KnowledgeGapDetection)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
detection = KnowledgeGapDetection()
|
detection = KnowledgeGapDetection()
|
||||||
|
|
||||||
print("\n1. 测试缺口检测初始化...")
|
print("\n1. 测试缺口检测初始化...")
|
||||||
print(f" 数据库路径: {detection.db_path}")
|
print(f" 数据库路径: {detection.db_path}")
|
||||||
@@ -144,26 +144,26 @@ def test_cache_manager() -> None:
|
|||||||
print("测试缓存管理器 (CacheManager)")
|
print("测试缓存管理器 (CacheManager)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
cache = CacheManager()
|
cache = CacheManager()
|
||||||
|
|
||||||
print(f"\n1. 缓存后端: {'Redis' if cache.use_redis else '内存 LRU'}")
|
print(f"\n1. 缓存后端: {'Redis' if cache.use_redis else '内存 LRU'}")
|
||||||
|
|
||||||
print("\n2. 测试缓存操作...")
|
print("\n2. 测试缓存操作...")
|
||||||
# 设置缓存
|
# 设置缓存
|
||||||
cache.set("test_key_1", {"name": "测试数据", "value": 123}, ttl = 60)
|
cache.set("test_key_1", {"name": "测试数据", "value": 123}, ttl=60)
|
||||||
print(" ✓ 设置缓存 test_key_1")
|
print(" ✓ 设置缓存 test_key_1")
|
||||||
|
|
||||||
# 获取缓存
|
# 获取缓存
|
||||||
_ = cache.get("test_key_1")
|
_ = cache.get("test_key_1")
|
||||||
print(" ✓ 获取缓存: {value}")
|
print(" ✓ 获取缓存: {value}")
|
||||||
|
|
||||||
# 批量操作
|
# 批量操作
|
||||||
cache.set_many(
|
cache.set_many(
|
||||||
{"batch_key_1": "value1", "batch_key_2": "value2", "batch_key_3": "value3"}, ttl = 60
|
{"batch_key_1": "value1", "batch_key_2": "value2", "batch_key_3": "value3"}, ttl=60
|
||||||
)
|
)
|
||||||
print(" ✓ 批量设置缓存")
|
print(" ✓ 批量设置缓存")
|
||||||
|
|
||||||
_ = cache.get_many(["batch_key_1", "batch_key_2", "batch_key_3"])
|
_ = cache.get_many(["batch_key_1", "batch_key_2", "batch_key_3"])
|
||||||
print(" ✓ 批量获取缓存: {len(values)} 个")
|
print(" ✓ 批量获取缓存: {len(values)} 个")
|
||||||
|
|
||||||
# 删除缓存
|
# 删除缓存
|
||||||
@@ -171,7 +171,7 @@ def test_cache_manager() -> None:
|
|||||||
print(" ✓ 删除缓存 test_key_1")
|
print(" ✓ 删除缓存 test_key_1")
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计
|
||||||
stats = cache.get_stats()
|
stats = cache.get_stats()
|
||||||
print("\n3. 缓存统计:")
|
print("\n3. 缓存统计:")
|
||||||
print(f" 总请求数: {stats['total_requests']}")
|
print(f" 总请求数: {stats['total_requests']}")
|
||||||
print(f" 命中数: {stats['hits']}")
|
print(f" 命中数: {stats['hits']}")
|
||||||
@@ -192,7 +192,7 @@ def test_task_queue() -> None:
|
|||||||
print("测试任务队列 (TaskQueue)")
|
print("测试任务队列 (TaskQueue)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
queue = TaskQueue()
|
queue = TaskQueue()
|
||||||
|
|
||||||
print(f"\n1. 任务队列可用性: {'✓ 可用' if queue.is_available() else '✗ 不可用'}")
|
print(f"\n1. 任务队列可用性: {'✓ 可用' if queue.is_available() else '✗ 不可用'}")
|
||||||
print(f" 后端: {'Celery' if queue.use_celery else '内存'}")
|
print(f" 后端: {'Celery' if queue.use_celery else '内存'}")
|
||||||
@@ -207,18 +207,18 @@ def test_task_queue() -> None:
|
|||||||
queue.register_handler("test_task", test_task_handler)
|
queue.register_handler("test_task", test_task_handler)
|
||||||
|
|
||||||
# 提交任务
|
# 提交任务
|
||||||
task_id = queue.submit(
|
task_id = queue.submit(
|
||||||
task_type = "test_task", payload = {"test": "data", "timestamp": time.time()}
|
task_type="test_task", payload={"test": "data", "timestamp": time.time()}
|
||||||
)
|
)
|
||||||
print(" ✓ 提交任务: {task_id}")
|
print(" ✓ 提交任务: {task_id}")
|
||||||
|
|
||||||
# 获取任务状态
|
# 获取任务状态
|
||||||
task_info = queue.get_status(task_id)
|
task_info = queue.get_status(task_id)
|
||||||
if task_info:
|
if task_info:
|
||||||
print(" ✓ 任务状态: {task_info.status}")
|
print(" ✓ 任务状态: {task_info.status}")
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计
|
||||||
stats = queue.get_stats()
|
stats = queue.get_stats()
|
||||||
print("\n3. 任务队列统计:")
|
print("\n3. 任务队列统计:")
|
||||||
print(f" 后端: {stats['backend']}")
|
print(f" 后端: {stats['backend']}")
|
||||||
print(f" 按状态统计: {stats.get('by_status', {})}")
|
print(f" 按状态统计: {stats.get('by_status', {})}")
|
||||||
@@ -233,32 +233,32 @@ def test_performance_monitor() -> None:
|
|||||||
print("测试性能监控 (PerformanceMonitor)")
|
print("测试性能监控 (PerformanceMonitor)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
monitor = PerformanceMonitor()
|
monitor = PerformanceMonitor()
|
||||||
|
|
||||||
print("\n1. 测试指标记录...")
|
print("\n1. 测试指标记录...")
|
||||||
|
|
||||||
# 记录一些测试指标
|
# 记录一些测试指标
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
monitor.record_metric(
|
monitor.record_metric(
|
||||||
metric_type = "api_response",
|
metric_type="api_response",
|
||||||
duration_ms = 50 + i * 10,
|
duration_ms=50 + i * 10,
|
||||||
endpoint = "/api/v1/test",
|
endpoint="/api/v1/test",
|
||||||
metadata = {"test": True},
|
metadata={"test": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
monitor.record_metric(
|
monitor.record_metric(
|
||||||
metric_type = "db_query",
|
metric_type="db_query",
|
||||||
duration_ms = 20 + i * 5,
|
duration_ms=20 + i * 5,
|
||||||
endpoint = "SELECT test",
|
endpoint="SELECT test",
|
||||||
metadata = {"test": True},
|
metadata={"test": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
print(" ✓ 记录了 8 个测试指标")
|
print(" ✓ 记录了 8 个测试指标")
|
||||||
|
|
||||||
# 获取统计
|
# 获取统计
|
||||||
print("\n2. 获取性能统计...")
|
print("\n2. 获取性能统计...")
|
||||||
stats = monitor.get_stats(hours = 1)
|
stats = monitor.get_stats(hours=1)
|
||||||
print(f" 总请求数: {stats['overall']['total_requests']}")
|
print(f" 总请求数: {stats['overall']['total_requests']}")
|
||||||
print(f" 平均响应时间: {stats['overall']['avg_duration_ms']} ms")
|
print(f" 平均响应时间: {stats['overall']['avg_duration_ms']} ms")
|
||||||
print(f" 最大响应时间: {stats['overall']['max_duration_ms']} ms")
|
print(f" 最大响应时间: {stats['overall']['max_duration_ms']} ms")
|
||||||
@@ -280,13 +280,13 @@ def test_search_manager() -> None:
|
|||||||
print("测试搜索管理器 (SearchManager)")
|
print("测试搜索管理器 (SearchManager)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_search_manager()
|
manager = get_search_manager()
|
||||||
|
|
||||||
print("\n1. 搜索管理器初始化...")
|
print("\n1. 搜索管理器初始化...")
|
||||||
print(" ✓ 搜索管理器已初始化")
|
print(" ✓ 搜索管理器已初始化")
|
||||||
|
|
||||||
print("\n2. 获取搜索统计...")
|
print("\n2. 获取搜索统计...")
|
||||||
stats = manager.get_search_stats()
|
stats = manager.get_search_stats()
|
||||||
print(f" 全文索引数: {stats['fulltext_indexed']}")
|
print(f" 全文索引数: {stats['fulltext_indexed']}")
|
||||||
print(f" 语义索引数: {stats['semantic_indexed']}")
|
print(f" 语义索引数: {stats['semantic_indexed']}")
|
||||||
print(f" 语义搜索可用: {stats['semantic_search_available']}")
|
print(f" 语义搜索可用: {stats['semantic_search_available']}")
|
||||||
@@ -301,18 +301,18 @@ def test_performance_manager() -> None:
|
|||||||
print("测试性能管理器 (PerformanceManager)")
|
print("测试性能管理器 (PerformanceManager)")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_performance_manager()
|
manager = get_performance_manager()
|
||||||
|
|
||||||
print("\n1. 性能管理器初始化...")
|
print("\n1. 性能管理器初始化...")
|
||||||
print(" ✓ 性能管理器已初始化")
|
print(" ✓ 性能管理器已初始化")
|
||||||
|
|
||||||
print("\n2. 获取系统健康状态...")
|
print("\n2. 获取系统健康状态...")
|
||||||
health = manager.get_health_status()
|
health = manager.get_health_status()
|
||||||
print(f" 缓存后端: {health['cache']['backend']}")
|
print(f" 缓存后端: {health['cache']['backend']}")
|
||||||
print(f" 任务队列后端: {health['task_queue']['backend']}")
|
print(f" 任务队列后端: {health['task_queue']['backend']}")
|
||||||
|
|
||||||
print("\n3. 获取完整统计...")
|
print("\n3. 获取完整统计...")
|
||||||
stats = manager.get_full_stats()
|
stats = manager.get_full_stats()
|
||||||
print(f" 缓存统计: {stats['cache']['total_requests']} 请求")
|
print(f" 缓存统计: {stats['cache']['total_requests']} 请求")
|
||||||
print(f" 任务队列统计: {stats['task_queue']}")
|
print(f" 任务队列统计: {stats['task_queue']}")
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ def run_all_tests() -> None:
|
|||||||
print("高级搜索与发现 + 性能优化与扩展")
|
print("高级搜索与发现 + 性能优化与扩展")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# 搜索模块测试
|
# 搜索模块测试
|
||||||
try:
|
try:
|
||||||
@@ -390,11 +390,11 @@ def run_all_tests() -> None:
|
|||||||
print("测试汇总")
|
print("测试汇总")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
passed = sum(1 for _, result in results if result)
|
passed = sum(1 for _, result in results if result)
|
||||||
total = len(results)
|
total = len(results)
|
||||||
|
|
||||||
for name, result in results:
|
for name, result in results:
|
||||||
status = "✓ 通过" if result else "✗ 失败"
|
status = "✓ 通过" if result else "✗ 失败"
|
||||||
print(f" {status} - {name}")
|
print(f" {status} - {name}")
|
||||||
|
|
||||||
print(f"\n总计: {passed}/{total} 测试通过")
|
print(f"\n总计: {passed}/{total} 测试通过")
|
||||||
@@ -408,5 +408,5 @@ def run_all_tests() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = run_all_tests()
|
success = run_all_tests()
|
||||||
sys.exit(0 if success else 1)
|
sys.exit(0 if success else 1)
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ def test_tenant_management() -> None:
|
|||||||
print("测试 1: 租户管理")
|
print("测试 1: 租户管理")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 1. 创建租户
|
# 1. 创建租户
|
||||||
print("\n1.1 创建租户...")
|
print("\n1.1 创建租户...")
|
||||||
tenant = manager.create_tenant(
|
tenant = manager.create_tenant(
|
||||||
name = "Test Company", owner_id = "user_001", tier = "pro", description = "A test company tenant"
|
name="Test Company", owner_id="user_001", tier="pro", description="A test company tenant"
|
||||||
)
|
)
|
||||||
print(f"✅ 租户创建成功: {tenant.id}")
|
print(f"✅ 租户创建成功: {tenant.id}")
|
||||||
print(f" - 名称: {tenant.name}")
|
print(f" - 名称: {tenant.name}")
|
||||||
@@ -40,27 +40,27 @@ def test_tenant_management() -> None:
|
|||||||
|
|
||||||
# 2. 获取租户
|
# 2. 获取租户
|
||||||
print("\n1.2 获取租户信息...")
|
print("\n1.2 获取租户信息...")
|
||||||
fetched = manager.get_tenant(tenant.id)
|
fetched = manager.get_tenant(tenant.id)
|
||||||
assert fetched is not None, "获取租户失败"
|
assert fetched is not None, "获取租户失败"
|
||||||
print(f"✅ 获取租户成功: {fetched.name}")
|
print(f"✅ 获取租户成功: {fetched.name}")
|
||||||
|
|
||||||
# 3. 通过 slug 获取
|
# 3. 通过 slug 获取
|
||||||
print("\n1.3 通过 slug 获取租户...")
|
print("\n1.3 通过 slug 获取租户...")
|
||||||
by_slug = manager.get_tenant_by_slug(tenant.slug)
|
by_slug = manager.get_tenant_by_slug(tenant.slug)
|
||||||
assert by_slug is not None, "通过 slug 获取失败"
|
assert by_slug is not None, "通过 slug 获取失败"
|
||||||
print(f"✅ 通过 slug 获取成功: {by_slug.name}")
|
print(f"✅ 通过 slug 获取成功: {by_slug.name}")
|
||||||
|
|
||||||
# 4. 更新租户
|
# 4. 更新租户
|
||||||
print("\n1.4 更新租户信息...")
|
print("\n1.4 更新租户信息...")
|
||||||
updated = manager.update_tenant(
|
updated = manager.update_tenant(
|
||||||
tenant_id = tenant.id, name = "Test Company Updated", tier = "enterprise"
|
tenant_id=tenant.id, name="Test Company Updated", tier="enterprise"
|
||||||
)
|
)
|
||||||
assert updated is not None, "更新租户失败"
|
assert updated is not None, "更新租户失败"
|
||||||
print(f"✅ 租户更新成功: {updated.name}, 层级: {updated.tier}")
|
print(f"✅ 租户更新成功: {updated.name}, 层级: {updated.tier}")
|
||||||
|
|
||||||
# 5. 列出租户
|
# 5. 列出租户
|
||||||
print("\n1.5 列出租户...")
|
print("\n1.5 列出租户...")
|
||||||
tenants = manager.list_tenants(limit = 10)
|
tenants = manager.list_tenants(limit=10)
|
||||||
print(f"✅ 找到 {len(tenants)} 个租户")
|
print(f"✅ 找到 {len(tenants)} 个租户")
|
||||||
|
|
||||||
return tenant.id
|
return tenant.id
|
||||||
@@ -72,11 +72,11 @@ def test_domain_management(tenant_id: str) -> None:
|
|||||||
print("测试 2: 域名管理")
|
print("测试 2: 域名管理")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 1. 添加域名
|
# 1. 添加域名
|
||||||
print("\n2.1 添加自定义域名...")
|
print("\n2.1 添加自定义域名...")
|
||||||
domain = manager.add_domain(tenant_id = tenant_id, domain = "test.example.com", is_primary = True)
|
domain = manager.add_domain(tenant_id=tenant_id, domain="test.example.com", is_primary=True)
|
||||||
print(f"✅ 域名添加成功: {domain.domain}")
|
print(f"✅ 域名添加成功: {domain.domain}")
|
||||||
print(f" - ID: {domain.id}")
|
print(f" - ID: {domain.id}")
|
||||||
print(f" - 状态: {domain.status}")
|
print(f" - 状态: {domain.status}")
|
||||||
@@ -84,19 +84,19 @@ def test_domain_management(tenant_id: str) -> None:
|
|||||||
|
|
||||||
# 2. 获取验证指导
|
# 2. 获取验证指导
|
||||||
print("\n2.2 获取域名验证指导...")
|
print("\n2.2 获取域名验证指导...")
|
||||||
instructions = manager.get_domain_verification_instructions(domain.id)
|
instructions = manager.get_domain_verification_instructions(domain.id)
|
||||||
print("✅ 验证指导:")
|
print("✅ 验证指导:")
|
||||||
print(f" - DNS 记录: {instructions['dns_record']}")
|
print(f" - DNS 记录: {instructions['dns_record']}")
|
||||||
print(f" - 文件验证: {instructions['file_verification']}")
|
print(f" - 文件验证: {instructions['file_verification']}")
|
||||||
|
|
||||||
# 3. 验证域名
|
# 3. 验证域名
|
||||||
print("\n2.3 验证域名...")
|
print("\n2.3 验证域名...")
|
||||||
verified = manager.verify_domain(tenant_id, domain.id)
|
verified = manager.verify_domain(tenant_id, domain.id)
|
||||||
print(f"✅ 域名验证结果: {verified}")
|
print(f"✅ 域名验证结果: {verified}")
|
||||||
|
|
||||||
# 4. 通过域名获取租户
|
# 4. 通过域名获取租户
|
||||||
print("\n2.4 通过域名获取租户...")
|
print("\n2.4 通过域名获取租户...")
|
||||||
by_domain = manager.get_tenant_by_domain("test.example.com")
|
by_domain = manager.get_tenant_by_domain("test.example.com")
|
||||||
if by_domain:
|
if by_domain:
|
||||||
print(f"✅ 通过域名获取租户成功: {by_domain.name}")
|
print(f"✅ 通过域名获取租户成功: {by_domain.name}")
|
||||||
else:
|
else:
|
||||||
@@ -104,7 +104,7 @@ def test_domain_management(tenant_id: str) -> None:
|
|||||||
|
|
||||||
# 5. 列出域名
|
# 5. 列出域名
|
||||||
print("\n2.5 列出所有域名...")
|
print("\n2.5 列出所有域名...")
|
||||||
domains = manager.list_domains(tenant_id)
|
domains = manager.list_domains(tenant_id)
|
||||||
print(f"✅ 找到 {len(domains)} 个域名")
|
print(f"✅ 找到 {len(domains)} 个域名")
|
||||||
for d in domains:
|
for d in domains:
|
||||||
print(f" - {d.domain} ({d.status})")
|
print(f" - {d.domain} ({d.status})")
|
||||||
@@ -118,19 +118,19 @@ def test_branding_management(tenant_id: str) -> None:
|
|||||||
print("测试 3: 品牌白标")
|
print("测试 3: 品牌白标")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 1. 更新品牌配置
|
# 1. 更新品牌配置
|
||||||
print("\n3.1 更新品牌配置...")
|
print("\n3.1 更新品牌配置...")
|
||||||
branding = manager.update_branding(
|
branding = manager.update_branding(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
logo_url = "https://example.com/logo.png",
|
logo_url="https://example.com/logo.png",
|
||||||
favicon_url = "https://example.com/favicon.ico",
|
favicon_url="https://example.com/favicon.ico",
|
||||||
primary_color = "#1890ff",
|
primary_color="#1890ff",
|
||||||
secondary_color = "#52c41a",
|
secondary_color="#52c41a",
|
||||||
custom_css = ".header { background: #1890ff; }",
|
custom_css=".header { background: #1890ff; }",
|
||||||
custom_js = "console.log('Custom JS loaded');",
|
custom_js="console.log('Custom JS loaded');",
|
||||||
login_page_bg = "https://example.com/bg.jpg",
|
login_page_bg="https://example.com/bg.jpg",
|
||||||
)
|
)
|
||||||
print("✅ 品牌配置更新成功")
|
print("✅ 品牌配置更新成功")
|
||||||
print(f" - Logo: {branding.logo_url}")
|
print(f" - Logo: {branding.logo_url}")
|
||||||
@@ -139,13 +139,13 @@ def test_branding_management(tenant_id: str) -> None:
|
|||||||
|
|
||||||
# 2. 获取品牌配置
|
# 2. 获取品牌配置
|
||||||
print("\n3.2 获取品牌配置...")
|
print("\n3.2 获取品牌配置...")
|
||||||
fetched = manager.get_branding(tenant_id)
|
fetched = manager.get_branding(tenant_id)
|
||||||
assert fetched is not None, "获取品牌配置失败"
|
assert fetched is not None, "获取品牌配置失败"
|
||||||
print("✅ 获取品牌配置成功")
|
print("✅ 获取品牌配置成功")
|
||||||
|
|
||||||
# 3. 生成品牌 CSS
|
# 3. 生成品牌 CSS
|
||||||
print("\n3.3 生成品牌 CSS...")
|
print("\n3.3 生成品牌 CSS...")
|
||||||
css = manager.get_branding_css(tenant_id)
|
css = manager.get_branding_css(tenant_id)
|
||||||
print(f"✅ 生成 CSS 成功 ({len(css)} 字符)")
|
print(f"✅ 生成 CSS 成功 ({len(css)} 字符)")
|
||||||
print(f" CSS 预览:\n{css[:200]}...")
|
print(f" CSS 预览:\n{css[:200]}...")
|
||||||
|
|
||||||
@@ -158,48 +158,48 @@ def test_member_management(tenant_id: str) -> None:
|
|||||||
print("测试 4: 成员管理")
|
print("测试 4: 成员管理")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 1. 邀请成员
|
# 1. 邀请成员
|
||||||
print("\n4.1 邀请成员...")
|
print("\n4.1 邀请成员...")
|
||||||
member1 = manager.invite_member(
|
member1 = manager.invite_member(
|
||||||
tenant_id = tenant_id, email = "admin@test.com", role = "admin", invited_by = "user_001"
|
tenant_id=tenant_id, email="admin@test.com", role="admin", invited_by="user_001"
|
||||||
)
|
)
|
||||||
print(f"✅ 成员邀请成功: {member1.email}")
|
print(f"✅ 成员邀请成功: {member1.email}")
|
||||||
print(f" - ID: {member1.id}")
|
print(f" - ID: {member1.id}")
|
||||||
print(f" - 角色: {member1.role}")
|
print(f" - 角色: {member1.role}")
|
||||||
print(f" - 权限: {member1.permissions}")
|
print(f" - 权限: {member1.permissions}")
|
||||||
|
|
||||||
member2 = manager.invite_member(
|
member2 = manager.invite_member(
|
||||||
tenant_id = tenant_id, email = "member@test.com", role = "member", invited_by = "user_001"
|
tenant_id=tenant_id, email="member@test.com", role="member", invited_by="user_001"
|
||||||
)
|
)
|
||||||
print(f"✅ 成员邀请成功: {member2.email}")
|
print(f"✅ 成员邀请成功: {member2.email}")
|
||||||
|
|
||||||
# 2. 接受邀请
|
# 2. 接受邀请
|
||||||
print("\n4.2 接受邀请...")
|
print("\n4.2 接受邀请...")
|
||||||
accepted = manager.accept_invitation(member1.id, "user_002")
|
accepted = manager.accept_invitation(member1.id, "user_002")
|
||||||
print(f"✅ 邀请接受结果: {accepted}")
|
print(f"✅ 邀请接受结果: {accepted}")
|
||||||
|
|
||||||
# 3. 列出成员
|
# 3. 列出成员
|
||||||
print("\n4.3 列出所有成员...")
|
print("\n4.3 列出所有成员...")
|
||||||
members = manager.list_members(tenant_id)
|
members = manager.list_members(tenant_id)
|
||||||
print(f"✅ 找到 {len(members)} 个成员")
|
print(f"✅ 找到 {len(members)} 个成员")
|
||||||
for m in members:
|
for m in members:
|
||||||
print(f" - {m.email} ({m.role}) - {m.status}")
|
print(f" - {m.email} ({m.role}) - {m.status}")
|
||||||
|
|
||||||
# 4. 检查权限
|
# 4. 检查权限
|
||||||
print("\n4.4 检查权限...")
|
print("\n4.4 检查权限...")
|
||||||
can_manage = manager.check_permission(tenant_id, "user_002", "project", "create")
|
can_manage = manager.check_permission(tenant_id, "user_002", "project", "create")
|
||||||
print(f"✅ user_002 可以创建项目: {can_manage}")
|
print(f"✅ user_002 可以创建项目: {can_manage}")
|
||||||
|
|
||||||
# 5. 更新成员角色
|
# 5. 更新成员角色
|
||||||
print("\n4.5 更新成员角色...")
|
print("\n4.5 更新成员角色...")
|
||||||
updated = manager.update_member_role(tenant_id, member2.id, "viewer")
|
updated = manager.update_member_role(tenant_id, member2.id, "viewer")
|
||||||
print(f"✅ 角色更新结果: {updated}")
|
print(f"✅ 角色更新结果: {updated}")
|
||||||
|
|
||||||
# 6. 获取用户所属租户
|
# 6. 获取用户所属租户
|
||||||
print("\n4.6 获取用户所属租户...")
|
print("\n4.6 获取用户所属租户...")
|
||||||
user_tenants = manager.get_user_tenants("user_002")
|
user_tenants = manager.get_user_tenants("user_002")
|
||||||
print(f"✅ user_002 属于 {len(user_tenants)} 个租户")
|
print(f"✅ user_002 属于 {len(user_tenants)} 个租户")
|
||||||
for t in user_tenants:
|
for t in user_tenants:
|
||||||
print(f" - {t['name']} ({t['member_role']})")
|
print(f" - {t['name']} ({t['member_role']})")
|
||||||
@@ -213,24 +213,24 @@ def test_usage_tracking(tenant_id: str) -> None:
|
|||||||
print("测试 5: 资源使用统计")
|
print("测试 5: 资源使用统计")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 1. 记录使用
|
# 1. 记录使用
|
||||||
print("\n5.1 记录资源使用...")
|
print("\n5.1 记录资源使用...")
|
||||||
manager.record_usage(
|
manager.record_usage(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
storage_bytes = 1024 * 1024 * 50, # 50MB
|
storage_bytes=1024 * 1024 * 50, # 50MB
|
||||||
transcription_seconds = 600, # 10分钟
|
transcription_seconds=600, # 10分钟
|
||||||
api_calls = 100,
|
api_calls=100,
|
||||||
projects_count = 5,
|
projects_count=5,
|
||||||
entities_count = 50,
|
entities_count=50,
|
||||||
members_count = 3,
|
members_count=3,
|
||||||
)
|
)
|
||||||
print("✅ 资源使用记录成功")
|
print("✅ 资源使用记录成功")
|
||||||
|
|
||||||
# 2. 获取使用统计
|
# 2. 获取使用统计
|
||||||
print("\n5.2 获取使用统计...")
|
print("\n5.2 获取使用统计...")
|
||||||
stats = manager.get_usage_stats(tenant_id)
|
stats = manager.get_usage_stats(tenant_id)
|
||||||
print("✅ 使用统计:")
|
print("✅ 使用统计:")
|
||||||
print(f" - 存储: {stats['storage_mb']:.2f} MB")
|
print(f" - 存储: {stats['storage_mb']:.2f} MB")
|
||||||
print(f" - 转录: {stats['transcription_minutes']:.2f} 分钟")
|
print(f" - 转录: {stats['transcription_minutes']:.2f} 分钟")
|
||||||
@@ -243,7 +243,7 @@ def test_usage_tracking(tenant_id: str) -> None:
|
|||||||
# 3. 检查资源限制
|
# 3. 检查资源限制
|
||||||
print("\n5.3 检查资源限制...")
|
print("\n5.3 检查资源限制...")
|
||||||
for resource in ["storage", "transcription", "api_calls", "projects", "entities", "members"]:
|
for resource in ["storage", "transcription", "api_calls", "projects", "entities", "members"]:
|
||||||
allowed, current, limit = manager.check_resource_limit(tenant_id, resource)
|
allowed, current, limit = manager.check_resource_limit(tenant_id, resource)
|
||||||
print(f" - {resource}: {current}/{limit} ({'✅' if allowed else '❌'})")
|
print(f" - {resource}: {current}/{limit} ({'✅' if allowed else '❌'})")
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
@@ -255,7 +255,7 @@ def cleanup(tenant_id: str, domain_id: str, member_ids: list) -> None:
|
|||||||
print("清理测试数据")
|
print("清理测试数据")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
manager = get_tenant_manager()
|
manager = get_tenant_manager()
|
||||||
|
|
||||||
# 移除成员
|
# 移除成员
|
||||||
for member_id in member_ids:
|
for member_id in member_ids:
|
||||||
@@ -279,17 +279,17 @@ def main() -> None:
|
|||||||
print("InsightFlow Phase 8 Task 1 - 多租户 SaaS 架构测试")
|
print("InsightFlow Phase 8 Task 1 - 多租户 SaaS 架构测试")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
tenant_id = None
|
tenant_id = None
|
||||||
domain_id = None
|
domain_id = None
|
||||||
member_ids = []
|
member_ids = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 运行所有测试
|
# 运行所有测试
|
||||||
tenant_id = test_tenant_management()
|
tenant_id = test_tenant_management()
|
||||||
domain_id = test_domain_management(tenant_id)
|
domain_id = test_domain_management(tenant_id)
|
||||||
test_branding_management(tenant_id)
|
test_branding_management(tenant_id)
|
||||||
m1, m2 = test_member_management(tenant_id)
|
m1, m2 = test_member_management(tenant_id)
|
||||||
member_ids = [m1, m2]
|
member_ids = [m1, m2]
|
||||||
test_usage_tracking(tenant_id)
|
test_usage_tracking(tenant_id)
|
||||||
|
|
||||||
print("\n" + " = " * 60)
|
print("\n" + " = " * 60)
|
||||||
|
|||||||
@@ -19,24 +19,24 @@ def test_subscription_manager() -> None:
|
|||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
# 使用临时文件数据库进行测试
|
# 使用临时文件数据库进行测试
|
||||||
db_path = tempfile.mktemp(suffix = ".db")
|
db_path = tempfile.mktemp(suffix=".db")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = SubscriptionManager(db_path = db_path)
|
manager = SubscriptionManager(db_path=db_path)
|
||||||
|
|
||||||
print("\n1. 测试订阅计划管理")
|
print("\n1. 测试订阅计划管理")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 获取默认计划
|
# 获取默认计划
|
||||||
plans = manager.list_plans()
|
plans = manager.list_plans()
|
||||||
print(f"✓ 默认计划数量: {len(plans)}")
|
print(f"✓ 默认计划数量: {len(plans)}")
|
||||||
for plan in plans:
|
for plan in plans:
|
||||||
print(f" - {plan.name} ({plan.tier}): ¥{plan.price_monthly}/月")
|
print(f" - {plan.name} ({plan.tier}): ¥{plan.price_monthly}/月")
|
||||||
|
|
||||||
# 通过 tier 获取计划
|
# 通过 tier 获取计划
|
||||||
free_plan = manager.get_plan_by_tier("free")
|
free_plan = manager.get_plan_by_tier("free")
|
||||||
pro_plan = manager.get_plan_by_tier("pro")
|
pro_plan = manager.get_plan_by_tier("pro")
|
||||||
enterprise_plan = manager.get_plan_by_tier("enterprise")
|
enterprise_plan = manager.get_plan_by_tier("enterprise")
|
||||||
|
|
||||||
assert free_plan is not None, "Free 计划应该存在"
|
assert free_plan is not None, "Free 计划应该存在"
|
||||||
assert pro_plan is not None, "Pro 计划应该存在"
|
assert pro_plan is not None, "Pro 计划应该存在"
|
||||||
@@ -49,14 +49,14 @@ def test_subscription_manager() -> None:
|
|||||||
print("\n2. 测试订阅管理")
|
print("\n2. 测试订阅管理")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
tenant_id = "test-tenant-001"
|
tenant_id = "test-tenant-001"
|
||||||
|
|
||||||
# 创建订阅
|
# 创建订阅
|
||||||
subscription = manager.create_subscription(
|
subscription = manager.create_subscription(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
plan_id = pro_plan.id,
|
plan_id=pro_plan.id,
|
||||||
payment_provider = PaymentProvider.STRIPE.value,
|
payment_provider=PaymentProvider.STRIPE.value,
|
||||||
trial_days = 14,
|
trial_days=14,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"✓ 创建订阅: {subscription.id}")
|
print(f"✓ 创建订阅: {subscription.id}")
|
||||||
@@ -66,7 +66,7 @@ def test_subscription_manager() -> None:
|
|||||||
print(f" - 试用结束: {subscription.trial_end}")
|
print(f" - 试用结束: {subscription.trial_end}")
|
||||||
|
|
||||||
# 获取租户订阅
|
# 获取租户订阅
|
||||||
tenant_sub = manager.get_tenant_subscription(tenant_id)
|
tenant_sub = manager.get_tenant_subscription(tenant_id)
|
||||||
assert tenant_sub is not None, "应该能获取到租户订阅"
|
assert tenant_sub is not None, "应该能获取到租户订阅"
|
||||||
print(f"✓ 获取租户订阅: {tenant_sub.id}")
|
print(f"✓ 获取租户订阅: {tenant_sub.id}")
|
||||||
|
|
||||||
@@ -74,27 +74,27 @@ def test_subscription_manager() -> None:
|
|||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 记录转录用量
|
# 记录转录用量
|
||||||
usage1 = manager.record_usage(
|
usage1 = manager.record_usage(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
resource_type = "transcription",
|
resource_type="transcription",
|
||||||
quantity = 120,
|
quantity=120,
|
||||||
unit = "minute",
|
unit="minute",
|
||||||
description = "会议转录",
|
description="会议转录",
|
||||||
)
|
)
|
||||||
print(f"✓ 记录转录用量: {usage1.quantity} {usage1.unit}, 费用: ¥{usage1.cost:.2f}")
|
print(f"✓ 记录转录用量: {usage1.quantity} {usage1.unit}, 费用: ¥{usage1.cost:.2f}")
|
||||||
|
|
||||||
# 记录存储用量
|
# 记录存储用量
|
||||||
usage2 = manager.record_usage(
|
usage2 = manager.record_usage(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
resource_type = "storage",
|
resource_type="storage",
|
||||||
quantity = 2.5,
|
quantity=2.5,
|
||||||
unit = "gb",
|
unit="gb",
|
||||||
description = "文件存储",
|
description="文件存储",
|
||||||
)
|
)
|
||||||
print(f"✓ 记录存储用量: {usage2.quantity} {usage2.unit}, 费用: ¥{usage2.cost:.2f}")
|
print(f"✓ 记录存储用量: {usage2.quantity} {usage2.unit}, 费用: ¥{usage2.cost:.2f}")
|
||||||
|
|
||||||
# 获取用量汇总
|
# 获取用量汇总
|
||||||
summary = manager.get_usage_summary(tenant_id)
|
summary = manager.get_usage_summary(tenant_id)
|
||||||
print("✓ 用量汇总:")
|
print("✓ 用量汇总:")
|
||||||
print(f" - 总费用: ¥{summary['total_cost']:.2f}")
|
print(f" - 总费用: ¥{summary['total_cost']:.2f}")
|
||||||
for resource, data in summary["breakdown"].items():
|
for resource, data in summary["breakdown"].items():
|
||||||
@@ -104,12 +104,12 @@ def test_subscription_manager() -> None:
|
|||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 创建支付
|
# 创建支付
|
||||||
payment = manager.create_payment(
|
payment = manager.create_payment(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
amount = 99.0,
|
amount=99.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
provider = PaymentProvider.ALIPAY.value,
|
provider=PaymentProvider.ALIPAY.value,
|
||||||
payment_method = "qrcode",
|
payment_method="qrcode",
|
||||||
)
|
)
|
||||||
print(f"✓ 创建支付: {payment.id}")
|
print(f"✓ 创建支付: {payment.id}")
|
||||||
print(f" - 金额: ¥{payment.amount}")
|
print(f" - 金额: ¥{payment.amount}")
|
||||||
@@ -117,22 +117,22 @@ def test_subscription_manager() -> None:
|
|||||||
print(f" - 状态: {payment.status}")
|
print(f" - 状态: {payment.status}")
|
||||||
|
|
||||||
# 确认支付
|
# 确认支付
|
||||||
confirmed = manager.confirm_payment(payment.id, "alipay_123456")
|
confirmed = manager.confirm_payment(payment.id, "alipay_123456")
|
||||||
print(f"✓ 确认支付完成: {confirmed.status}")
|
print(f"✓ 确认支付完成: {confirmed.status}")
|
||||||
|
|
||||||
# 列出支付记录
|
# 列出支付记录
|
||||||
payments = manager.list_payments(tenant_id)
|
payments = manager.list_payments(tenant_id)
|
||||||
print(f"✓ 支付记录数量: {len(payments)}")
|
print(f"✓ 支付记录数量: {len(payments)}")
|
||||||
|
|
||||||
print("\n5. 测试发票管理")
|
print("\n5. 测试发票管理")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 列出发票
|
# 列出发票
|
||||||
invoices = manager.list_invoices(tenant_id)
|
invoices = manager.list_invoices(tenant_id)
|
||||||
print(f"✓ 发票数量: {len(invoices)}")
|
print(f"✓ 发票数量: {len(invoices)}")
|
||||||
|
|
||||||
if invoices:
|
if invoices:
|
||||||
invoice = invoices[0]
|
invoice = invoices[0]
|
||||||
print(f" - 发票号: {invoice.invoice_number}")
|
print(f" - 发票号: {invoice.invoice_number}")
|
||||||
print(f" - 金额: ¥{invoice.amount_due}")
|
print(f" - 金额: ¥{invoice.amount_due}")
|
||||||
print(f" - 状态: {invoice.status}")
|
print(f" - 状态: {invoice.status}")
|
||||||
@@ -141,12 +141,12 @@ def test_subscription_manager() -> None:
|
|||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 申请退款
|
# 申请退款
|
||||||
refund = manager.request_refund(
|
refund = manager.request_refund(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
payment_id = payment.id,
|
payment_id=payment.id,
|
||||||
amount = 50.0,
|
amount=50.0,
|
||||||
reason = "服务不满意",
|
reason="服务不满意",
|
||||||
requested_by = "user_001",
|
requested_by="user_001",
|
||||||
)
|
)
|
||||||
print(f"✓ 申请退款: {refund.id}")
|
print(f"✓ 申请退款: {refund.id}")
|
||||||
print(f" - 金额: ¥{refund.amount}")
|
print(f" - 金额: ¥{refund.amount}")
|
||||||
@@ -154,21 +154,21 @@ def test_subscription_manager() -> None:
|
|||||||
print(f" - 状态: {refund.status}")
|
print(f" - 状态: {refund.status}")
|
||||||
|
|
||||||
# 批准退款
|
# 批准退款
|
||||||
approved = manager.approve_refund(refund.id, "admin_001")
|
approved = manager.approve_refund(refund.id, "admin_001")
|
||||||
print(f"✓ 批准退款: {approved.status}")
|
print(f"✓ 批准退款: {approved.status}")
|
||||||
|
|
||||||
# 完成退款
|
# 完成退款
|
||||||
completed = manager.complete_refund(refund.id, "refund_123456")
|
completed = manager.complete_refund(refund.id, "refund_123456")
|
||||||
print(f"✓ 完成退款: {completed.status}")
|
print(f"✓ 完成退款: {completed.status}")
|
||||||
|
|
||||||
# 列出退款记录
|
# 列出退款记录
|
||||||
refunds = manager.list_refunds(tenant_id)
|
refunds = manager.list_refunds(tenant_id)
|
||||||
print(f"✓ 退款记录数量: {len(refunds)}")
|
print(f"✓ 退款记录数量: {len(refunds)}")
|
||||||
|
|
||||||
print("\n7. 测试账单历史")
|
print("\n7. 测试账单历史")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
history = manager.get_billing_history(tenant_id)
|
history = manager.get_billing_history(tenant_id)
|
||||||
print(f"✓ 账单历史记录数量: {len(history)}")
|
print(f"✓ 账单历史记录数量: {len(history)}")
|
||||||
for h in history:
|
for h in history:
|
||||||
print(f" - [{h.type}] {h.description}: ¥{h.amount}")
|
print(f" - [{h.type}] {h.description}: ¥{h.amount}")
|
||||||
@@ -177,24 +177,24 @@ def test_subscription_manager() -> None:
|
|||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# Stripe Checkout
|
# Stripe Checkout
|
||||||
stripe_session = manager.create_stripe_checkout_session(
|
stripe_session = manager.create_stripe_checkout_session(
|
||||||
tenant_id = tenant_id,
|
tenant_id=tenant_id,
|
||||||
plan_id = enterprise_plan.id,
|
plan_id=enterprise_plan.id,
|
||||||
success_url = "https://example.com/success",
|
success_url="https://example.com/success",
|
||||||
cancel_url = "https://example.com/cancel",
|
cancel_url="https://example.com/cancel",
|
||||||
)
|
)
|
||||||
print(f"✓ Stripe Checkout 会话: {stripe_session['session_id']}")
|
print(f"✓ Stripe Checkout 会话: {stripe_session['session_id']}")
|
||||||
|
|
||||||
# 支付宝订单
|
# 支付宝订单
|
||||||
alipay_order = manager.create_alipay_order(tenant_id = tenant_id, plan_id = pro_plan.id)
|
alipay_order = manager.create_alipay_order(tenant_id=tenant_id, plan_id=pro_plan.id)
|
||||||
print(f"✓ 支付宝订单: {alipay_order['order_id']}")
|
print(f"✓ 支付宝订单: {alipay_order['order_id']}")
|
||||||
|
|
||||||
# 微信支付订单
|
# 微信支付订单
|
||||||
wechat_order = manager.create_wechat_order(tenant_id = tenant_id, plan_id = pro_plan.id)
|
wechat_order = manager.create_wechat_order(tenant_id=tenant_id, plan_id=pro_plan.id)
|
||||||
print(f"✓ 微信支付订单: {wechat_order['order_id']}")
|
print(f"✓ 微信支付订单: {wechat_order['order_id']}")
|
||||||
|
|
||||||
# Webhook 处理
|
# Webhook 处理
|
||||||
webhook_result = manager.handle_webhook(
|
webhook_result = manager.handle_webhook(
|
||||||
"stripe",
|
"stripe",
|
||||||
{"event_type": "checkout.session.completed", "data": {"object": {"id": "cs_test"}}},
|
{"event_type": "checkout.session.completed", "data": {"object": {"id": "cs_test"}}},
|
||||||
)
|
)
|
||||||
@@ -204,13 +204,13 @@ def test_subscription_manager() -> None:
|
|||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
# 更改计划
|
# 更改计划
|
||||||
changed = manager.change_plan(
|
changed = manager.change_plan(
|
||||||
subscription_id = subscription.id, new_plan_id = enterprise_plan.id
|
subscription_id=subscription.id, new_plan_id=enterprise_plan.id
|
||||||
)
|
)
|
||||||
print(f"✓ 更改计划: {changed.plan_id} (Enterprise)")
|
print(f"✓ 更改计划: {changed.plan_id} (Enterprise)")
|
||||||
|
|
||||||
# 取消订阅
|
# 取消订阅
|
||||||
cancelled = manager.cancel_subscription(subscription_id = subscription.id, at_period_end = True)
|
cancelled = manager.cancel_subscription(subscription_id=subscription.id, at_period_end=True)
|
||||||
print(f"✓ 取消订阅: {cancelled.status}")
|
print(f"✓ 取消订阅: {cancelled.status}")
|
||||||
print(f" - 周期结束时取消: {cancelled.cancel_at_period_end}")
|
print(f" - 周期结束时取消: {cancelled.cancel_at_period_end}")
|
||||||
|
|
||||||
|
|||||||
@@ -18,27 +18,27 @@ def test_custom_model() -> None:
|
|||||||
"""测试自定义模型功能"""
|
"""测试自定义模型功能"""
|
||||||
print("\n=== 测试自定义模型 ===")
|
print("\n=== 测试自定义模型 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 1. 创建自定义模型
|
# 1. 创建自定义模型
|
||||||
print("1. 创建自定义模型...")
|
print("1. 创建自定义模型...")
|
||||||
model = manager.create_custom_model(
|
model = manager.create_custom_model(
|
||||||
tenant_id = "tenant_001",
|
tenant_id="tenant_001",
|
||||||
name = "领域实体识别模型",
|
name="领域实体识别模型",
|
||||||
description = "用于识别医疗领域实体的自定义模型",
|
description="用于识别医疗领域实体的自定义模型",
|
||||||
model_type = ModelType.CUSTOM_NER,
|
model_type=ModelType.CUSTOM_NER,
|
||||||
training_data = {
|
training_data={
|
||||||
"entity_types": ["DISEASE", "SYMPTOM", "DRUG", "TREATMENT"],
|
"entity_types": ["DISEASE", "SYMPTOM", "DRUG", "TREATMENT"],
|
||||||
"domain": "medical",
|
"domain": "medical",
|
||||||
},
|
},
|
||||||
hyperparameters = {"epochs": 15, "learning_rate": 0.001, "batch_size": 32},
|
hyperparameters={"epochs": 15, "learning_rate": 0.001, "batch_size": 32},
|
||||||
created_by = "user_001",
|
created_by="user_001",
|
||||||
)
|
)
|
||||||
print(f" 创建成功: {model.id}, 状态: {model.status.value}")
|
print(f" 创建成功: {model.id}, 状态: {model.status.value}")
|
||||||
|
|
||||||
# 2. 添加训练样本
|
# 2. 添加训练样本
|
||||||
print("2. 添加训练样本...")
|
print("2. 添加训练样本...")
|
||||||
samples = [
|
samples = [
|
||||||
{
|
{
|
||||||
"text": "患者张三患有高血压,正在服用降压药治疗。",
|
"text": "患者张三患有高血压,正在服用降压药治疗。",
|
||||||
"entities": [
|
"entities": [
|
||||||
@@ -66,22 +66,22 @@ def test_custom_model() -> None:
|
|||||||
]
|
]
|
||||||
|
|
||||||
for sample_data in samples:
|
for sample_data in samples:
|
||||||
sample = manager.add_training_sample(
|
sample = manager.add_training_sample(
|
||||||
model_id = model.id,
|
model_id=model.id,
|
||||||
text = sample_data["text"],
|
text=sample_data["text"],
|
||||||
entities = sample_data["entities"],
|
entities=sample_data["entities"],
|
||||||
metadata = {"source": "manual"},
|
metadata={"source": "manual"},
|
||||||
)
|
)
|
||||||
print(f" 添加样本: {sample.id}")
|
print(f" 添加样本: {sample.id}")
|
||||||
|
|
||||||
# 3. 获取训练样本
|
# 3. 获取训练样本
|
||||||
print("3. 获取训练样本...")
|
print("3. 获取训练样本...")
|
||||||
all_samples = manager.get_training_samples(model.id)
|
all_samples = manager.get_training_samples(model.id)
|
||||||
print(f" 共有 {len(all_samples)} 个训练样本")
|
print(f" 共有 {len(all_samples)} 个训练样本")
|
||||||
|
|
||||||
# 4. 列出自定义模型
|
# 4. 列出自定义模型
|
||||||
print("4. 列出自定义模型...")
|
print("4. 列出自定义模型...")
|
||||||
models = manager.list_custom_models(tenant_id = "tenant_001")
|
models = manager.list_custom_models(tenant_id="tenant_001")
|
||||||
print(f" 找到 {len(models)} 个模型")
|
print(f" 找到 {len(models)} 个模型")
|
||||||
for m in models:
|
for m in models:
|
||||||
print(f" - {m.name} ({m.model_type.value}): {m.status.value}")
|
print(f" - {m.name} ({m.model_type.value}): {m.status.value}")
|
||||||
@@ -93,12 +93,12 @@ async def test_train_and_predict(model_id: str) -> None:
|
|||||||
"""测试训练和预测"""
|
"""测试训练和预测"""
|
||||||
print("\n=== 测试模型训练和预测 ===")
|
print("\n=== 测试模型训练和预测 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 1. 训练模型
|
# 1. 训练模型
|
||||||
print("1. 训练模型...")
|
print("1. 训练模型...")
|
||||||
try:
|
try:
|
||||||
trained_model = await manager.train_custom_model(model_id)
|
trained_model = await manager.train_custom_model(model_id)
|
||||||
print(f" 训练完成: {trained_model.status.value}")
|
print(f" 训练完成: {trained_model.status.value}")
|
||||||
print(f" 指标: {trained_model.metrics}")
|
print(f" 指标: {trained_model.metrics}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -107,9 +107,9 @@ async def test_train_and_predict(model_id: str) -> None:
|
|||||||
|
|
||||||
# 2. 使用模型预测
|
# 2. 使用模型预测
|
||||||
print("2. 使用模型预测...")
|
print("2. 使用模型预测...")
|
||||||
test_text = "赵六患有糖尿病,正在使用胰岛素治疗。"
|
test_text = "赵六患有糖尿病,正在使用胰岛素治疗。"
|
||||||
try:
|
try:
|
||||||
entities = await manager.predict_with_custom_model(model_id, test_text)
|
entities = await manager.predict_with_custom_model(model_id, test_text)
|
||||||
print(f" 输入: {test_text}")
|
print(f" 输入: {test_text}")
|
||||||
print(f" 预测实体: {entities}")
|
print(f" 预测实体: {entities}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -120,37 +120,37 @@ def test_prediction_models() -> None:
|
|||||||
"""测试预测模型"""
|
"""测试预测模型"""
|
||||||
print("\n=== 测试预测模型 ===")
|
print("\n=== 测试预测模型 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 1. 创建趋势预测模型
|
# 1. 创建趋势预测模型
|
||||||
print("1. 创建趋势预测模型...")
|
print("1. 创建趋势预测模型...")
|
||||||
trend_model = manager.create_prediction_model(
|
trend_model = manager.create_prediction_model(
|
||||||
tenant_id = "tenant_001",
|
tenant_id="tenant_001",
|
||||||
project_id = "project_001",
|
project_id="project_001",
|
||||||
name = "实体数量趋势预测",
|
name="实体数量趋势预测",
|
||||||
prediction_type = PredictionType.TREND,
|
prediction_type=PredictionType.TREND,
|
||||||
target_entity_type = "PERSON",
|
target_entity_type="PERSON",
|
||||||
features = ["entity_count", "time_period", "document_count"],
|
features=["entity_count", "time_period", "document_count"],
|
||||||
model_config = {"algorithm": "linear_regression", "window_size": 7},
|
model_config={"algorithm": "linear_regression", "window_size": 7},
|
||||||
)
|
)
|
||||||
print(f" 创建成功: {trend_model.id}")
|
print(f" 创建成功: {trend_model.id}")
|
||||||
|
|
||||||
# 2. 创建异常检测模型
|
# 2. 创建异常检测模型
|
||||||
print("2. 创建异常检测模型...")
|
print("2. 创建异常检测模型...")
|
||||||
anomaly_model = manager.create_prediction_model(
|
anomaly_model = manager.create_prediction_model(
|
||||||
tenant_id = "tenant_001",
|
tenant_id="tenant_001",
|
||||||
project_id = "project_001",
|
project_id="project_001",
|
||||||
name = "实体增长异常检测",
|
name="实体增长异常检测",
|
||||||
prediction_type = PredictionType.ANOMALY,
|
prediction_type=PredictionType.ANOMALY,
|
||||||
target_entity_type = None,
|
target_entity_type=None,
|
||||||
features = ["daily_growth", "weekly_growth"],
|
features=["daily_growth", "weekly_growth"],
|
||||||
model_config = {"threshold": 2.5, "sensitivity": "medium"},
|
model_config={"threshold": 2.5, "sensitivity": "medium"},
|
||||||
)
|
)
|
||||||
print(f" 创建成功: {anomaly_model.id}")
|
print(f" 创建成功: {anomaly_model.id}")
|
||||||
|
|
||||||
# 3. 列出预测模型
|
# 3. 列出预测模型
|
||||||
print("3. 列出预测模型...")
|
print("3. 列出预测模型...")
|
||||||
models = manager.list_prediction_models(tenant_id = "tenant_001")
|
models = manager.list_prediction_models(tenant_id="tenant_001")
|
||||||
print(f" 找到 {len(models)} 个预测模型")
|
print(f" 找到 {len(models)} 个预测模型")
|
||||||
for m in models:
|
for m in models:
|
||||||
print(f" - {m.name} ({m.prediction_type.value})")
|
print(f" - {m.name} ({m.prediction_type.value})")
|
||||||
@@ -162,11 +162,11 @@ async def test_predictions(trend_model_id: str, anomaly_model_id: str) -> None:
|
|||||||
"""测试预测功能"""
|
"""测试预测功能"""
|
||||||
print("\n=== 测试预测功能 ===")
|
print("\n=== 测试预测功能 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 1. 训练趋势预测模型
|
# 1. 训练趋势预测模型
|
||||||
print("1. 训练趋势预测模型...")
|
print("1. 训练趋势预测模型...")
|
||||||
historical_data = [
|
historical_data = [
|
||||||
{"date": "2024-01-01", "value": 10},
|
{"date": "2024-01-01", "value": 10},
|
||||||
{"date": "2024-01-02", "value": 12},
|
{"date": "2024-01-02", "value": 12},
|
||||||
{"date": "2024-01-03", "value": 15},
|
{"date": "2024-01-03", "value": 15},
|
||||||
@@ -175,19 +175,19 @@ async def test_predictions(trend_model_id: str, anomaly_model_id: str) -> None:
|
|||||||
{"date": "2024-01-06", "value": 20},
|
{"date": "2024-01-06", "value": 20},
|
||||||
{"date": "2024-01-07", "value": 22},
|
{"date": "2024-01-07", "value": 22},
|
||||||
]
|
]
|
||||||
trained = await manager.train_prediction_model(trend_model_id, historical_data)
|
trained = await manager.train_prediction_model(trend_model_id, historical_data)
|
||||||
print(f" 训练完成,准确率: {trained.accuracy}")
|
print(f" 训练完成,准确率: {trained.accuracy}")
|
||||||
|
|
||||||
# 2. 趋势预测
|
# 2. 趋势预测
|
||||||
print("2. 趋势预测...")
|
print("2. 趋势预测...")
|
||||||
trend_result = await manager.predict(
|
trend_result = await manager.predict(
|
||||||
trend_model_id, {"historical_values": [10, 12, 15, 14, 18, 20, 22]}
|
trend_model_id, {"historical_values": [10, 12, 15, 14, 18, 20, 22]}
|
||||||
)
|
)
|
||||||
print(f" 预测结果: {trend_result.prediction_data}")
|
print(f" 预测结果: {trend_result.prediction_data}")
|
||||||
|
|
||||||
# 3. 异常检测
|
# 3. 异常检测
|
||||||
print("3. 异常检测...")
|
print("3. 异常检测...")
|
||||||
anomaly_result = await manager.predict(
|
anomaly_result = await manager.predict(
|
||||||
anomaly_model_id, {"value": 50, "historical_values": [10, 12, 11, 13, 12, 14, 13]}
|
anomaly_model_id, {"value": 50, "historical_values": [10, 12, 11, 13, 12, 14, 13]}
|
||||||
)
|
)
|
||||||
print(f" 检测结果: {anomaly_result.prediction_data}")
|
print(f" 检测结果: {anomaly_result.prediction_data}")
|
||||||
@@ -197,27 +197,27 @@ def test_kg_rag() -> None:
|
|||||||
"""测试知识图谱 RAG"""
|
"""测试知识图谱 RAG"""
|
||||||
print("\n=== 测试知识图谱 RAG ===")
|
print("\n=== 测试知识图谱 RAG ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 创建 RAG 配置
|
# 创建 RAG 配置
|
||||||
print("1. 创建知识图谱 RAG 配置...")
|
print("1. 创建知识图谱 RAG 配置...")
|
||||||
rag = manager.create_kg_rag(
|
rag = manager.create_kg_rag(
|
||||||
tenant_id = "tenant_001",
|
tenant_id="tenant_001",
|
||||||
project_id = "project_001",
|
project_id="project_001",
|
||||||
name = "项目知识问答",
|
name="项目知识问答",
|
||||||
description = "基于项目知识图谱的智能问答",
|
description="基于项目知识图谱的智能问答",
|
||||||
kg_config = {
|
kg_config={
|
||||||
"entity_types": ["PERSON", "ORG", "PROJECT", "TECH"],
|
"entity_types": ["PERSON", "ORG", "PROJECT", "TECH"],
|
||||||
"relation_types": ["works_with", "belongs_to", "depends_on"],
|
"relation_types": ["works_with", "belongs_to", "depends_on"],
|
||||||
},
|
},
|
||||||
retrieval_config = {"top_k": 5, "similarity_threshold": 0.7, "expand_relations": True},
|
retrieval_config={"top_k": 5, "similarity_threshold": 0.7, "expand_relations": True},
|
||||||
generation_config = {"temperature": 0.3, "max_tokens": 1000, "include_sources": True},
|
generation_config={"temperature": 0.3, "max_tokens": 1000, "include_sources": True},
|
||||||
)
|
)
|
||||||
print(f" 创建成功: {rag.id}")
|
print(f" 创建成功: {rag.id}")
|
||||||
|
|
||||||
# 列出 RAG 配置
|
# 列出 RAG 配置
|
||||||
print("2. 列出 RAG 配置...")
|
print("2. 列出 RAG 配置...")
|
||||||
rags = manager.list_kg_rags(tenant_id = "tenant_001")
|
rags = manager.list_kg_rags(tenant_id="tenant_001")
|
||||||
print(f" 找到 {len(rags)} 个配置")
|
print(f" 找到 {len(rags)} 个配置")
|
||||||
|
|
||||||
return rag.id
|
return rag.id
|
||||||
@@ -227,10 +227,10 @@ async def test_kg_rag_query(rag_id: str) -> None:
|
|||||||
"""测试 RAG 查询"""
|
"""测试 RAG 查询"""
|
||||||
print("\n=== 测试知识图谱 RAG 查询 ===")
|
print("\n=== 测试知识图谱 RAG 查询 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 模拟项目实体和关系
|
# 模拟项目实体和关系
|
||||||
project_entities = [
|
project_entities = [
|
||||||
{"id": "e1", "name": "张三", "type": "PERSON", "definition": "项目经理"},
|
{"id": "e1", "name": "张三", "type": "PERSON", "definition": "项目经理"},
|
||||||
{"id": "e2", "name": "李四", "type": "PERSON", "definition": "技术负责人"},
|
{"id": "e2", "name": "李四", "type": "PERSON", "definition": "技术负责人"},
|
||||||
{"id": "e3", "name": "Project Alpha", "type": "PROJECT", "definition": "核心产品项目"},
|
{"id": "e3", "name": "Project Alpha", "type": "PROJECT", "definition": "核心产品项目"},
|
||||||
@@ -238,7 +238,7 @@ async def test_kg_rag_query(rag_id: str) -> None:
|
|||||||
{"id": "e5", "name": "TechCorp", "type": "ORG", "definition": "科技公司"},
|
{"id": "e5", "name": "TechCorp", "type": "ORG", "definition": "科技公司"},
|
||||||
]
|
]
|
||||||
|
|
||||||
project_relations = [
|
project_relations = [
|
||||||
{
|
{
|
||||||
"source_entity_id": "e1",
|
"source_entity_id": "e1",
|
||||||
"target_entity_id": "e3",
|
"target_entity_id": "e3",
|
||||||
@@ -275,14 +275,14 @@ async def test_kg_rag_query(rag_id: str) -> None:
|
|||||||
|
|
||||||
# 执行查询
|
# 执行查询
|
||||||
print("1. 执行 RAG 查询...")
|
print("1. 执行 RAG 查询...")
|
||||||
query_text = "Project Alpha 项目有哪些人参与?使用了什么技术?"
|
query_text = "Project Alpha 项目有哪些人参与?使用了什么技术?"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await manager.query_kg_rag(
|
result = await manager.query_kg_rag(
|
||||||
rag_id = rag_id,
|
rag_id=rag_id,
|
||||||
query = query_text,
|
query=query_text,
|
||||||
project_entities = project_entities,
|
project_entities=project_entities,
|
||||||
project_relations = project_relations,
|
project_relations=project_relations,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f" 查询: {result.query}")
|
print(f" 查询: {result.query}")
|
||||||
@@ -298,10 +298,10 @@ async def test_smart_summary() -> None:
|
|||||||
"""测试智能摘要"""
|
"""测试智能摘要"""
|
||||||
print("\n=== 测试智能摘要 ===")
|
print("\n=== 测试智能摘要 ===")
|
||||||
|
|
||||||
manager = get_ai_manager()
|
manager = get_ai_manager()
|
||||||
|
|
||||||
# 模拟转录文本
|
# 模拟转录文本
|
||||||
transcript_text = """
|
transcript_text = """
|
||||||
今天的会议主要讨论了 Project Alpha 的进展情况。张三作为项目经理,
|
今天的会议主要讨论了 Project Alpha 的进展情况。张三作为项目经理,
|
||||||
汇报了当前的项目进度,表示已经完成了 80% 的开发工作。李四提出了
|
汇报了当前的项目进度,表示已经完成了 80% 的开发工作。李四提出了
|
||||||
一些关于 Kubernetes 部署的问题,建议我们采用新的部署策略。
|
一些关于 Kubernetes 部署的问题,建议我们采用新的部署策略。
|
||||||
@@ -309,7 +309,7 @@ async def test_smart_summary() -> None:
|
|||||||
大家一致认为项目进展顺利,预计可以按时交付。
|
大家一致认为项目进展顺利,预计可以按时交付。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content_data = {
|
content_data = {
|
||||||
"text": transcript_text,
|
"text": transcript_text,
|
||||||
"entities": [
|
"entities": [
|
||||||
{"name": "张三", "type": "PERSON"},
|
{"name": "张三", "type": "PERSON"},
|
||||||
@@ -320,18 +320,18 @@ async def test_smart_summary() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 生成不同类型的摘要
|
# 生成不同类型的摘要
|
||||||
summary_types = ["extractive", "abstractive", "key_points"]
|
summary_types = ["extractive", "abstractive", "key_points"]
|
||||||
|
|
||||||
for summary_type in summary_types:
|
for summary_type in summary_types:
|
||||||
print(f"1. 生成 {summary_type} 类型摘要...")
|
print(f"1. 生成 {summary_type} 类型摘要...")
|
||||||
try:
|
try:
|
||||||
summary = await manager.generate_smart_summary(
|
summary = await manager.generate_smart_summary(
|
||||||
tenant_id = "tenant_001",
|
tenant_id="tenant_001",
|
||||||
project_id = "project_001",
|
project_id="project_001",
|
||||||
source_type = "transcript",
|
source_type="transcript",
|
||||||
source_id = "transcript_001",
|
source_id="transcript_001",
|
||||||
summary_type = summary_type,
|
summary_type=summary_type,
|
||||||
content_data = content_data,
|
content_data=content_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f" 摘要类型: {summary.summary_type}")
|
print(f" 摘要类型: {summary.summary_type}")
|
||||||
@@ -350,19 +350,19 @@ async def main() -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 测试自定义模型
|
# 测试自定义模型
|
||||||
model_id = test_custom_model()
|
model_id = test_custom_model()
|
||||||
|
|
||||||
# 测试训练和预测
|
# 测试训练和预测
|
||||||
await test_train_and_predict(model_id)
|
await test_train_and_predict(model_id)
|
||||||
|
|
||||||
# 测试预测模型
|
# 测试预测模型
|
||||||
trend_model_id, anomaly_model_id = test_prediction_models()
|
trend_model_id, anomaly_model_id = test_prediction_models()
|
||||||
|
|
||||||
# 测试预测功能
|
# 测试预测功能
|
||||||
await test_predictions(trend_model_id, anomaly_model_id)
|
await test_predictions(trend_model_id, anomaly_model_id)
|
||||||
|
|
||||||
# 测试知识图谱 RAG
|
# 测试知识图谱 RAG
|
||||||
rag_id = test_kg_rag()
|
rag_id = test_kg_rag()
|
||||||
|
|
||||||
# 测试 RAG 查询
|
# 测试 RAG 查询
|
||||||
await test_kg_rag_query(rag_id)
|
await test_kg_rag_query(rag_id)
|
||||||
|
|||||||
+162
-162
@@ -28,7 +28,7 @@ from growth_manager import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 添加 backend 目录到路径
|
# 添加 backend 目录到路径
|
||||||
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
if backend_dir not in sys.path:
|
if backend_dir not in sys.path:
|
||||||
sys.path.insert(0, backend_dir)
|
sys.path.insert(0, backend_dir)
|
||||||
|
|
||||||
@@ -37,14 +37,14 @@ class TestGrowthManager:
|
|||||||
"""测试 Growth Manager 功能"""
|
"""测试 Growth Manager 功能"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.manager = GrowthManager()
|
self.manager = GrowthManager()
|
||||||
self.test_tenant_id = "test_tenant_001"
|
self.test_tenant_id = "test_tenant_001"
|
||||||
self.test_user_id = "test_user_001"
|
self.test_user_id = "test_user_001"
|
||||||
self.test_results = []
|
self.test_results = []
|
||||||
|
|
||||||
def log(self, message: str, success: bool = True) -> None:
|
def log(self, message: str, success: bool = True) -> None:
|
||||||
"""记录测试结果"""
|
"""记录测试结果"""
|
||||||
status = "✅" if success else "❌"
|
status = "✅" if success else "❌"
|
||||||
print(f"{status} {message}")
|
print(f"{status} {message}")
|
||||||
self.test_results.append((message, success))
|
self.test_results.append((message, success))
|
||||||
|
|
||||||
@@ -55,16 +55,16 @@ class TestGrowthManager:
|
|||||||
print("\n📊 测试事件追踪...")
|
print("\n📊 测试事件追踪...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = await self.manager.track_event(
|
event = await self.manager.track_event(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
user_id = self.test_user_id,
|
user_id=self.test_user_id,
|
||||||
event_type = EventType.PAGE_VIEW,
|
event_type=EventType.PAGE_VIEW,
|
||||||
event_name = "dashboard_view",
|
event_name="dashboard_view",
|
||||||
properties = {"page": "/dashboard", "duration": 120},
|
properties={"page": "/dashboard", "duration": 120},
|
||||||
session_id = "session_001",
|
session_id="session_001",
|
||||||
device_info = {"browser": "Chrome", "os": "MacOS"},
|
device_info={"browser": "Chrome", "os": "MacOS"},
|
||||||
referrer = "https://google.com",
|
referrer="https://google.com",
|
||||||
utm_params = {"source": "google", "medium": "organic", "campaign": "summer"},
|
utm_params={"source": "google", "medium": "organic", "campaign": "summer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert event.id is not None
|
assert event.id is not None
|
||||||
@@ -74,7 +74,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"事件追踪成功: {event.id}")
|
self.log(f"事件追踪成功: {event.id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"事件追踪失败: {e}", success = False)
|
self.log(f"事件追踪失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def test_track_multiple_events(self) -> None:
|
async def test_track_multiple_events(self) -> None:
|
||||||
@@ -82,7 +82,7 @@ class TestGrowthManager:
|
|||||||
print("\n📊 测试追踪多个事件...")
|
print("\n📊 测试追踪多个事件...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
events = [
|
events = [
|
||||||
(EventType.FEATURE_USE, "entity_extraction", {"entity_count": 5}),
|
(EventType.FEATURE_USE, "entity_extraction", {"entity_count": 5}),
|
||||||
(EventType.FEATURE_USE, "relation_discovery", {"relation_count": 3}),
|
(EventType.FEATURE_USE, "relation_discovery", {"relation_count": 3}),
|
||||||
(EventType.CONVERSION, "upgrade_click", {"plan": "pro"}),
|
(EventType.CONVERSION, "upgrade_click", {"plan": "pro"}),
|
||||||
@@ -91,17 +91,17 @@ class TestGrowthManager:
|
|||||||
|
|
||||||
for event_type, event_name, props in events:
|
for event_type, event_name, props in events:
|
||||||
await self.manager.track_event(
|
await self.manager.track_event(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
user_id = self.test_user_id,
|
user_id=self.test_user_id,
|
||||||
event_type = event_type,
|
event_type=event_type,
|
||||||
event_name = event_name,
|
event_name=event_name,
|
||||||
properties = props,
|
properties=props,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"成功追踪 {len(events)} 个事件")
|
self.log(f"成功追踪 {len(events)} 个事件")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"批量事件追踪失败: {e}", success = False)
|
self.log(f"批量事件追踪失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_get_user_profile(self) -> None:
|
def test_get_user_profile(self) -> None:
|
||||||
@@ -109,7 +109,7 @@ class TestGrowthManager:
|
|||||||
print("\n👤 测试用户画像...")
|
print("\n👤 测试用户画像...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile = self.manager.get_user_profile(self.test_tenant_id, self.test_user_id)
|
profile = self.manager.get_user_profile(self.test_tenant_id, self.test_user_id)
|
||||||
|
|
||||||
if profile:
|
if profile:
|
||||||
assert profile.user_id == self.test_user_id
|
assert profile.user_id == self.test_user_id
|
||||||
@@ -120,7 +120,7 @@ class TestGrowthManager:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"获取用户画像失败: {e}", success = False)
|
self.log(f"获取用户画像失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_get_analytics_summary(self) -> None:
|
def test_get_analytics_summary(self) -> None:
|
||||||
@@ -128,10 +128,10 @@ class TestGrowthManager:
|
|||||||
print("\n📈 测试分析汇总...")
|
print("\n📈 测试分析汇总...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
summary = self.manager.get_user_analytics_summary(
|
summary = self.manager.get_user_analytics_summary(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
start_date = datetime.now() - timedelta(days = 7),
|
start_date=datetime.now() - timedelta(days=7),
|
||||||
end_date = datetime.now(),
|
end_date=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "unique_users" in summary
|
assert "unique_users" in summary
|
||||||
@@ -141,7 +141,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"分析汇总: {summary['unique_users']} 用户, {summary['total_events']} 事件")
|
self.log(f"分析汇总: {summary['unique_users']} 用户, {summary['total_events']} 事件")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"获取分析汇总失败: {e}", success = False)
|
self.log(f"获取分析汇总失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_create_funnel(self) -> None:
|
def test_create_funnel(self) -> None:
|
||||||
@@ -149,17 +149,17 @@ class TestGrowthManager:
|
|||||||
print("\n🎯 测试创建转化漏斗...")
|
print("\n🎯 测试创建转化漏斗...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
funnel = self.manager.create_funnel(
|
funnel = self.manager.create_funnel(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "用户注册转化漏斗",
|
name="用户注册转化漏斗",
|
||||||
description = "从访问到完成注册的转化流程",
|
description="从访问到完成注册的转化流程",
|
||||||
steps = [
|
steps=[
|
||||||
{"name": "访问首页", "event_name": "page_view_home"},
|
{"name": "访问首页", "event_name": "page_view_home"},
|
||||||
{"name": "点击注册", "event_name": "signup_click"},
|
{"name": "点击注册", "event_name": "signup_click"},
|
||||||
{"name": "填写信息", "event_name": "signup_form_fill"},
|
{"name": "填写信息", "event_name": "signup_form_fill"},
|
||||||
{"name": "完成注册", "event_name": "signup_complete"},
|
{"name": "完成注册", "event_name": "signup_complete"},
|
||||||
],
|
],
|
||||||
created_by = "test",
|
created_by="test",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert funnel.id is not None
|
assert funnel.id is not None
|
||||||
@@ -168,7 +168,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"漏斗创建成功: {funnel.id}")
|
self.log(f"漏斗创建成功: {funnel.id}")
|
||||||
return funnel.id
|
return funnel.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建漏斗失败: {e}", success = False)
|
self.log(f"创建漏斗失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_analyze_funnel(self, funnel_id: str) -> None:
|
def test_analyze_funnel(self, funnel_id: str) -> None:
|
||||||
@@ -180,10 +180,10 @@ class TestGrowthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
analysis = self.manager.analyze_funnel(
|
analysis = self.manager.analyze_funnel(
|
||||||
funnel_id = funnel_id,
|
funnel_id=funnel_id,
|
||||||
period_start = datetime.now() - timedelta(days = 30),
|
period_start=datetime.now() - timedelta(days=30),
|
||||||
period_end = datetime.now(),
|
period_end=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if analysis:
|
if analysis:
|
||||||
@@ -194,7 +194,7 @@ class TestGrowthManager:
|
|||||||
self.log("漏斗分析返回空结果")
|
self.log("漏斗分析返回空结果")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"漏斗分析失败: {e}", success = False)
|
self.log(f"漏斗分析失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_calculate_retention(self) -> None:
|
def test_calculate_retention(self) -> None:
|
||||||
@@ -202,10 +202,10 @@ class TestGrowthManager:
|
|||||||
print("\n🔄 测试留存率计算...")
|
print("\n🔄 测试留存率计算...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
retention = self.manager.calculate_retention(
|
retention = self.manager.calculate_retention(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
cohort_date = datetime.now() - timedelta(days = 7),
|
cohort_date=datetime.now() - timedelta(days=7),
|
||||||
periods = [1, 3, 7],
|
periods=[1, 3, 7],
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "cohort_date" in retention
|
assert "cohort_date" in retention
|
||||||
@@ -214,7 +214,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"留存率计算完成: 同期群 {retention['cohort_size']} 用户")
|
self.log(f"留存率计算完成: 同期群 {retention['cohort_size']} 用户")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"留存率计算失败: {e}", success = False)
|
self.log(f"留存率计算失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 测试 A/B 测试框架 ====================
|
# ==================== 测试 A/B 测试框架 ====================
|
||||||
@@ -224,24 +224,24 @@ class TestGrowthManager:
|
|||||||
print("\n🧪 测试创建 A/B 测试实验...")
|
print("\n🧪 测试创建 A/B 测试实验...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
experiment = self.manager.create_experiment(
|
experiment = self.manager.create_experiment(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "首页按钮颜色测试",
|
name="首页按钮颜色测试",
|
||||||
description = "测试不同按钮颜色对转化率的影响",
|
description="测试不同按钮颜色对转化率的影响",
|
||||||
hypothesis = "蓝色按钮比红色按钮有更高的点击率",
|
hypothesis="蓝色按钮比红色按钮有更高的点击率",
|
||||||
variants = [
|
variants=[
|
||||||
{"id": "control", "name": "红色按钮", "is_control": True},
|
{"id": "control", "name": "红色按钮", "is_control": True},
|
||||||
{"id": "variant_a", "name": "蓝色按钮", "is_control": False},
|
{"id": "variant_a", "name": "蓝色按钮", "is_control": False},
|
||||||
{"id": "variant_b", "name": "绿色按钮", "is_control": False},
|
{"id": "variant_b", "name": "绿色按钮", "is_control": False},
|
||||||
],
|
],
|
||||||
traffic_allocation = TrafficAllocationType.RANDOM,
|
traffic_allocation=TrafficAllocationType.RANDOM,
|
||||||
traffic_split = {"control": 0.34, "variant_a": 0.33, "variant_b": 0.33},
|
traffic_split={"control": 0.34, "variant_a": 0.33, "variant_b": 0.33},
|
||||||
target_audience = {"conditions": []},
|
target_audience={"conditions": []},
|
||||||
primary_metric = "button_click_rate",
|
primary_metric="button_click_rate",
|
||||||
secondary_metrics = ["conversion_rate", "bounce_rate"],
|
secondary_metrics=["conversion_rate", "bounce_rate"],
|
||||||
min_sample_size = 100,
|
min_sample_size=100,
|
||||||
confidence_level = 0.95,
|
confidence_level=0.95,
|
||||||
created_by = "test",
|
created_by="test",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert experiment.id is not None
|
assert experiment.id is not None
|
||||||
@@ -250,7 +250,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"实验创建成功: {experiment.id}")
|
self.log(f"实验创建成功: {experiment.id}")
|
||||||
return experiment.id
|
return experiment.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建实验失败: {e}", success = False)
|
self.log(f"创建实验失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_list_experiments(self) -> None:
|
def test_list_experiments(self) -> None:
|
||||||
@@ -258,12 +258,12 @@ class TestGrowthManager:
|
|||||||
print("\n📋 测试列出实验...")
|
print("\n📋 测试列出实验...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
experiments = self.manager.list_experiments(self.test_tenant_id)
|
experiments = self.manager.list_experiments(self.test_tenant_id)
|
||||||
|
|
||||||
self.log(f"列出 {len(experiments)} 个实验")
|
self.log(f"列出 {len(experiments)} 个实验")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"列出实验失败: {e}", success = False)
|
self.log(f"列出实验失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_assign_variant(self, experiment_id: str) -> None:
|
def test_assign_variant(self, experiment_id: str) -> None:
|
||||||
@@ -279,23 +279,23 @@ class TestGrowthManager:
|
|||||||
self.manager.start_experiment(experiment_id)
|
self.manager.start_experiment(experiment_id)
|
||||||
|
|
||||||
# 测试多个用户的变体分配
|
# 测试多个用户的变体分配
|
||||||
test_users = ["user_001", "user_002", "user_003", "user_004", "user_005"]
|
test_users = ["user_001", "user_002", "user_003", "user_004", "user_005"]
|
||||||
assignments = {}
|
assignments = {}
|
||||||
|
|
||||||
for user_id in test_users:
|
for user_id in test_users:
|
||||||
variant_id = self.manager.assign_variant(
|
variant_id = self.manager.assign_variant(
|
||||||
experiment_id = experiment_id,
|
experiment_id=experiment_id,
|
||||||
user_id = user_id,
|
user_id=user_id,
|
||||||
user_attributes = {"user_id": user_id, "segment": "new"},
|
user_attributes={"user_id": user_id, "segment": "new"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if variant_id:
|
if variant_id:
|
||||||
assignments[user_id] = variant_id
|
assignments[user_id] = variant_id
|
||||||
|
|
||||||
self.log(f"变体分配完成: {len(assignments)} 个用户")
|
self.log(f"变体分配完成: {len(assignments)} 个用户")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"变体分配失败: {e}", success = False)
|
self.log(f"变体分配失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_record_experiment_metric(self, experiment_id: str) -> None:
|
def test_record_experiment_metric(self, experiment_id: str) -> None:
|
||||||
@@ -308,7 +308,7 @@ class TestGrowthManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 模拟记录一些指标
|
# 模拟记录一些指标
|
||||||
test_data = [
|
test_data = [
|
||||||
("user_001", "control", 1),
|
("user_001", "control", 1),
|
||||||
("user_002", "variant_a", 1),
|
("user_002", "variant_a", 1),
|
||||||
("user_003", "variant_b", 0),
|
("user_003", "variant_b", 0),
|
||||||
@@ -318,17 +318,17 @@ class TestGrowthManager:
|
|||||||
|
|
||||||
for user_id, variant_id, value in test_data:
|
for user_id, variant_id, value in test_data:
|
||||||
self.manager.record_experiment_metric(
|
self.manager.record_experiment_metric(
|
||||||
experiment_id = experiment_id,
|
experiment_id=experiment_id,
|
||||||
variant_id = variant_id,
|
variant_id=variant_id,
|
||||||
user_id = user_id,
|
user_id=user_id,
|
||||||
metric_name = "button_click_rate",
|
metric_name="button_click_rate",
|
||||||
metric_value = value,
|
metric_value=value,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"成功记录 {len(test_data)} 条指标")
|
self.log(f"成功记录 {len(test_data)} 条指标")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"记录指标失败: {e}", success = False)
|
self.log(f"记录指标失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_analyze_experiment(self, experiment_id: str) -> None:
|
def test_analyze_experiment(self, experiment_id: str) -> None:
|
||||||
@@ -340,16 +340,16 @@ class TestGrowthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.manager.analyze_experiment(experiment_id)
|
result = self.manager.analyze_experiment(experiment_id)
|
||||||
|
|
||||||
if "error" not in result:
|
if "error" not in result:
|
||||||
self.log(f"实验分析完成: {len(result.get('variant_results', {}))} 个变体")
|
self.log(f"实验分析完成: {len(result.get('variant_results', {}))} 个变体")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.log(f"实验分析返回错误: {result['error']}", success = False)
|
self.log(f"实验分析返回错误: {result['error']}", success=False)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"实验分析失败: {e}", success = False)
|
self.log(f"实验分析失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 测试邮件营销 ====================
|
# ==================== 测试邮件营销 ====================
|
||||||
@@ -359,12 +359,12 @@ class TestGrowthManager:
|
|||||||
print("\n📧 测试创建邮件模板...")
|
print("\n📧 测试创建邮件模板...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = self.manager.create_email_template(
|
template = self.manager.create_email_template(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "欢迎邮件",
|
name="欢迎邮件",
|
||||||
template_type = EmailTemplateType.WELCOME,
|
template_type=EmailTemplateType.WELCOME,
|
||||||
subject = "欢迎加入 InsightFlow!",
|
subject="欢迎加入 InsightFlow!",
|
||||||
html_content = """
|
html_content="""
|
||||||
<h1>欢迎,{{user_name}}!</h1>
|
<h1>欢迎,{{user_name}}!</h1>
|
||||||
<p>感谢您注册 InsightFlow。我们很高兴您能加入我们!</p>
|
<p>感谢您注册 InsightFlow。我们很高兴您能加入我们!</p>
|
||||||
<p>您的账户已创建,可以开始使用以下功能:</p>
|
<p>您的账户已创建,可以开始使用以下功能:</p>
|
||||||
@@ -375,8 +375,8 @@ class TestGrowthManager:
|
|||||||
</ul>
|
</ul>
|
||||||
<p><a href = "{{dashboard_url}}">立即开始使用</a></p>
|
<p><a href = "{{dashboard_url}}">立即开始使用</a></p>
|
||||||
""",
|
""",
|
||||||
from_name = "InsightFlow 团队",
|
from_name="InsightFlow 团队",
|
||||||
from_email = "welcome@insightflow.io",
|
from_email="welcome@insightflow.io",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert template.id is not None
|
assert template.id is not None
|
||||||
@@ -385,7 +385,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"邮件模板创建成功: {template.id}")
|
self.log(f"邮件模板创建成功: {template.id}")
|
||||||
return template.id
|
return template.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建邮件模板失败: {e}", success = False)
|
self.log(f"创建邮件模板失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_list_email_templates(self) -> None:
|
def test_list_email_templates(self) -> None:
|
||||||
@@ -393,12 +393,12 @@ class TestGrowthManager:
|
|||||||
print("\n📧 测试列出邮件模板...")
|
print("\n📧 测试列出邮件模板...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
templates = self.manager.list_email_templates(self.test_tenant_id)
|
templates = self.manager.list_email_templates(self.test_tenant_id)
|
||||||
|
|
||||||
self.log(f"列出 {len(templates)} 个邮件模板")
|
self.log(f"列出 {len(templates)} 个邮件模板")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"列出邮件模板失败: {e}", success = False)
|
self.log(f"列出邮件模板失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_render_template(self, template_id: str) -> None:
|
def test_render_template(self, template_id: str) -> None:
|
||||||
@@ -410,9 +410,9 @@ class TestGrowthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rendered = self.manager.render_template(
|
rendered = self.manager.render_template(
|
||||||
template_id = template_id,
|
template_id=template_id,
|
||||||
variables = {
|
variables={
|
||||||
"user_name": "张三",
|
"user_name": "张三",
|
||||||
"dashboard_url": "https://app.insightflow.io/dashboard",
|
"dashboard_url": "https://app.insightflow.io/dashboard",
|
||||||
},
|
},
|
||||||
@@ -424,10 +424,10 @@ class TestGrowthManager:
|
|||||||
self.log(f"模板渲染成功: {rendered['subject']}")
|
self.log(f"模板渲染成功: {rendered['subject']}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.log("模板渲染返回空结果", success = False)
|
self.log("模板渲染返回空结果", success=False)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"模板渲染失败: {e}", success = False)
|
self.log(f"模板渲染失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_create_email_campaign(self, template_id: str) -> None:
|
def test_create_email_campaign(self, template_id: str) -> None:
|
||||||
@@ -439,11 +439,11 @@ class TestGrowthManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
campaign = self.manager.create_email_campaign(
|
campaign = self.manager.create_email_campaign(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "新用户欢迎活动",
|
name="新用户欢迎活动",
|
||||||
template_id = template_id,
|
template_id=template_id,
|
||||||
recipient_list = [
|
recipient_list=[
|
||||||
{"user_id": "user_001", "email": "user1@example.com"},
|
{"user_id": "user_001", "email": "user1@example.com"},
|
||||||
{"user_id": "user_002", "email": "user2@example.com"},
|
{"user_id": "user_002", "email": "user2@example.com"},
|
||||||
{"user_id": "user_003", "email": "user3@example.com"},
|
{"user_id": "user_003", "email": "user3@example.com"},
|
||||||
@@ -456,7 +456,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"营销活动创建成功: {campaign.id}, {campaign.recipient_count} 收件人")
|
self.log(f"营销活动创建成功: {campaign.id}, {campaign.recipient_count} 收件人")
|
||||||
return campaign.id
|
return campaign.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建营销活动失败: {e}", success = False)
|
self.log(f"创建营销活动失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_create_automation_workflow(self) -> None:
|
def test_create_automation_workflow(self) -> None:
|
||||||
@@ -464,13 +464,13 @@ class TestGrowthManager:
|
|||||||
print("\n🤖 测试创建自动化工作流...")
|
print("\n🤖 测试创建自动化工作流...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
workflow = self.manager.create_automation_workflow(
|
workflow = self.manager.create_automation_workflow(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "新用户欢迎序列",
|
name="新用户欢迎序列",
|
||||||
description = "用户注册后自动发送欢迎邮件序列",
|
description="用户注册后自动发送欢迎邮件序列",
|
||||||
trigger_type = WorkflowTriggerType.USER_SIGNUP,
|
trigger_type=WorkflowTriggerType.USER_SIGNUP,
|
||||||
trigger_conditions = {"event": "user_signup"},
|
trigger_conditions={"event": "user_signup"},
|
||||||
actions = [
|
actions=[
|
||||||
{"type": "send_email", "template_type": "welcome", "delay_hours": 0},
|
{"type": "send_email", "template_type": "welcome", "delay_hours": 0},
|
||||||
{"type": "send_email", "template_type": "onboarding", "delay_hours": 24},
|
{"type": "send_email", "template_type": "onboarding", "delay_hours": 24},
|
||||||
{"type": "send_email", "template_type": "feature_tips", "delay_hours": 72},
|
{"type": "send_email", "template_type": "feature_tips", "delay_hours": 72},
|
||||||
@@ -483,7 +483,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"自动化工作流创建成功: {workflow.id}")
|
self.log(f"自动化工作流创建成功: {workflow.id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建工作流失败: {e}", success = False)
|
self.log(f"创建工作流失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 测试推荐系统 ====================
|
# ==================== 测试推荐系统 ====================
|
||||||
@@ -493,17 +493,17 @@ class TestGrowthManager:
|
|||||||
print("\n🎁 测试创建推荐计划...")
|
print("\n🎁 测试创建推荐计划...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
program = self.manager.create_referral_program(
|
program = self.manager.create_referral_program(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "邀请好友奖励计划",
|
name="邀请好友奖励计划",
|
||||||
description = "邀请好友注册,双方获得积分奖励",
|
description="邀请好友注册,双方获得积分奖励",
|
||||||
referrer_reward_type = "credit",
|
referrer_reward_type="credit",
|
||||||
referrer_reward_value = 100.0,
|
referrer_reward_value=100.0,
|
||||||
referee_reward_type = "credit",
|
referee_reward_type="credit",
|
||||||
referee_reward_value = 50.0,
|
referee_reward_value=50.0,
|
||||||
max_referrals_per_user = 10,
|
max_referrals_per_user=10,
|
||||||
referral_code_length = 8,
|
referral_code_length=8,
|
||||||
expiry_days = 30,
|
expiry_days=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert program.id is not None
|
assert program.id is not None
|
||||||
@@ -512,7 +512,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"推荐计划创建成功: {program.id}")
|
self.log(f"推荐计划创建成功: {program.id}")
|
||||||
return program.id
|
return program.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建推荐计划失败: {e}", success = False)
|
self.log(f"创建推荐计划失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_generate_referral_code(self, program_id: str) -> None:
|
def test_generate_referral_code(self, program_id: str) -> None:
|
||||||
@@ -524,8 +524,8 @@ class TestGrowthManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
referral = self.manager.generate_referral_code(
|
referral = self.manager.generate_referral_code(
|
||||||
program_id = program_id, referrer_id = "referrer_user_001"
|
program_id=program_id, referrer_id="referrer_user_001"
|
||||||
)
|
)
|
||||||
|
|
||||||
if referral:
|
if referral:
|
||||||
@@ -535,10 +535,10 @@ class TestGrowthManager:
|
|||||||
self.log(f"推荐码生成成功: {referral.referral_code}")
|
self.log(f"推荐码生成成功: {referral.referral_code}")
|
||||||
return referral.referral_code
|
return referral.referral_code
|
||||||
else:
|
else:
|
||||||
self.log("生成推荐码返回空结果", success = False)
|
self.log("生成推荐码返回空结果", success=False)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"生成推荐码失败: {e}", success = False)
|
self.log(f"生成推荐码失败: {e}", success=False)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_apply_referral_code(self, referral_code: str) -> None:
|
def test_apply_referral_code(self, referral_code: str) -> None:
|
||||||
@@ -550,18 +550,18 @@ class TestGrowthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = self.manager.apply_referral_code(
|
success = self.manager.apply_referral_code(
|
||||||
referral_code = referral_code, referee_id = "new_user_001"
|
referral_code=referral_code, referee_id="new_user_001"
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.log(f"推荐码应用成功: {referral_code}")
|
self.log(f"推荐码应用成功: {referral_code}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.log("推荐码应用失败", success = False)
|
self.log("推荐码应用失败", success=False)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"应用推荐码失败: {e}", success = False)
|
self.log(f"应用推荐码失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_get_referral_stats(self, program_id: str) -> None:
|
def test_get_referral_stats(self, program_id: str) -> None:
|
||||||
@@ -573,7 +573,7 @@ class TestGrowthManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats = self.manager.get_referral_stats(program_id)
|
stats = self.manager.get_referral_stats(program_id)
|
||||||
|
|
||||||
assert "total_referrals" in stats
|
assert "total_referrals" in stats
|
||||||
assert "conversion_rate" in stats
|
assert "conversion_rate" in stats
|
||||||
@@ -583,7 +583,7 @@ class TestGrowthManager:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"获取推荐统计失败: {e}", success = False)
|
self.log(f"获取推荐统计失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_create_team_incentive(self) -> None:
|
def test_create_team_incentive(self) -> None:
|
||||||
@@ -591,16 +591,16 @@ class TestGrowthManager:
|
|||||||
print("\n🏆 测试创建团队升级激励...")
|
print("\n🏆 测试创建团队升级激励...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
incentive = self.manager.create_team_incentive(
|
incentive = self.manager.create_team_incentive(
|
||||||
tenant_id = self.test_tenant_id,
|
tenant_id=self.test_tenant_id,
|
||||||
name = "团队升级奖励",
|
name="团队升级奖励",
|
||||||
description = "团队规模达到5人升级到 Pro 计划可获得折扣",
|
description="团队规模达到5人升级到 Pro 计划可获得折扣",
|
||||||
target_tier = "pro",
|
target_tier="pro",
|
||||||
min_team_size = 5,
|
min_team_size=5,
|
||||||
incentive_type = "discount",
|
incentive_type="discount",
|
||||||
incentive_value = 20.0, # 20% 折扣
|
incentive_value=20.0, # 20% 折扣
|
||||||
valid_from = datetime.now(),
|
valid_from=datetime.now(),
|
||||||
valid_until = datetime.now() + timedelta(days = 90),
|
valid_until=datetime.now() + timedelta(days=90),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert incentive.id is not None
|
assert incentive.id is not None
|
||||||
@@ -609,7 +609,7 @@ class TestGrowthManager:
|
|||||||
self.log(f"团队激励创建成功: {incentive.id}")
|
self.log(f"团队激励创建成功: {incentive.id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"创建团队激励失败: {e}", success = False)
|
self.log(f"创建团队激励失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def test_check_team_incentive_eligibility(self) -> None:
|
def test_check_team_incentive_eligibility(self) -> None:
|
||||||
@@ -617,14 +617,14 @@ class TestGrowthManager:
|
|||||||
print("\n🔍 测试检查团队激励资格...")
|
print("\n🔍 测试检查团队激励资格...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
incentives = self.manager.check_team_incentive_eligibility(
|
incentives = self.manager.check_team_incentive_eligibility(
|
||||||
tenant_id = self.test_tenant_id, current_tier = "free", team_size = 5
|
tenant_id=self.test_tenant_id, current_tier="free", team_size=5
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"找到 {len(incentives)} 个符合条件的激励")
|
self.log(f"找到 {len(incentives)} 个符合条件的激励")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"检查激励资格失败: {e}", success = False)
|
self.log(f"检查激励资格失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 测试实时仪表板 ====================
|
# ==================== 测试实时仪表板 ====================
|
||||||
@@ -634,19 +634,19 @@ class TestGrowthManager:
|
|||||||
print("\n📺 测试实时分析仪表板...")
|
print("\n📺 测试实时分析仪表板...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dashboard = self.manager.get_realtime_dashboard(self.test_tenant_id)
|
dashboard = self.manager.get_realtime_dashboard(self.test_tenant_id)
|
||||||
|
|
||||||
assert "today" in dashboard
|
assert "today" in dashboard
|
||||||
assert "recent_events" in dashboard
|
assert "recent_events" in dashboard
|
||||||
assert "top_features" in dashboard
|
assert "top_features" in dashboard
|
||||||
|
|
||||||
today = dashboard["today"]
|
today = dashboard["today"]
|
||||||
self.log(
|
self.log(
|
||||||
f"实时仪表板: 今日 {today['active_users']} 活跃用户, {today['total_events']} 事件"
|
f"实时仪表板: 今日 {today['active_users']} 活跃用户, {today['total_events']} 事件"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"获取实时仪表板失败: {e}", success = False)
|
self.log(f"获取实时仪表板失败: {e}", success=False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ==================== 运行所有测试 ====================
|
# ==================== 运行所有测试 ====================
|
||||||
@@ -666,7 +666,7 @@ class TestGrowthManager:
|
|||||||
await self.test_track_multiple_events()
|
await self.test_track_multiple_events()
|
||||||
self.test_get_user_profile()
|
self.test_get_user_profile()
|
||||||
self.test_get_analytics_summary()
|
self.test_get_analytics_summary()
|
||||||
funnel_id = self.test_create_funnel()
|
funnel_id = self.test_create_funnel()
|
||||||
self.test_analyze_funnel(funnel_id)
|
self.test_analyze_funnel(funnel_id)
|
||||||
self.test_calculate_retention()
|
self.test_calculate_retention()
|
||||||
|
|
||||||
@@ -675,7 +675,7 @@ class TestGrowthManager:
|
|||||||
print("🧪 模块 2: A/B 测试框架")
|
print("🧪 模块 2: A/B 测试框架")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
experiment_id = self.test_create_experiment()
|
experiment_id = self.test_create_experiment()
|
||||||
self.test_list_experiments()
|
self.test_list_experiments()
|
||||||
self.test_assign_variant(experiment_id)
|
self.test_assign_variant(experiment_id)
|
||||||
self.test_record_experiment_metric(experiment_id)
|
self.test_record_experiment_metric(experiment_id)
|
||||||
@@ -686,7 +686,7 @@ class TestGrowthManager:
|
|||||||
print("📧 模块 3: 邮件营销自动化")
|
print("📧 模块 3: 邮件营销自动化")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
template_id = self.test_create_email_template()
|
template_id = self.test_create_email_template()
|
||||||
self.test_list_email_templates()
|
self.test_list_email_templates()
|
||||||
self.test_render_template(template_id)
|
self.test_render_template(template_id)
|
||||||
self.test_create_email_campaign(template_id)
|
self.test_create_email_campaign(template_id)
|
||||||
@@ -697,8 +697,8 @@ class TestGrowthManager:
|
|||||||
print("🎁 模块 4: 推荐系统")
|
print("🎁 模块 4: 推荐系统")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
program_id = self.test_create_referral_program()
|
program_id = self.test_create_referral_program()
|
||||||
referral_code = self.test_generate_referral_code(program_id)
|
referral_code = self.test_generate_referral_code(program_id)
|
||||||
self.test_apply_referral_code(referral_code)
|
self.test_apply_referral_code(referral_code)
|
||||||
self.test_get_referral_stats(program_id)
|
self.test_get_referral_stats(program_id)
|
||||||
self.test_create_team_incentive()
|
self.test_create_team_incentive()
|
||||||
@@ -716,9 +716,9 @@ class TestGrowthManager:
|
|||||||
print("📋 测试总结")
|
print("📋 测试总结")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
total_tests = len(self.test_results)
|
total_tests = len(self.test_results)
|
||||||
passed_tests = sum(1 for _, success in self.test_results if success)
|
passed_tests = sum(1 for _, success in self.test_results if success)
|
||||||
failed_tests = total_tests - passed_tests
|
failed_tests = total_tests - passed_tests
|
||||||
|
|
||||||
print(f"总测试数: {total_tests}")
|
print(f"总测试数: {total_tests}")
|
||||||
print(f"通过: {passed_tests} ✅")
|
print(f"通过: {passed_tests} ✅")
|
||||||
@@ -738,7 +738,7 @@ class TestGrowthManager:
|
|||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
tester = TestGrowthManager()
|
tester = TestGrowthManager()
|
||||||
await tester.run_all_tests()
|
await tester.run_all_tests()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+237
-237
@@ -25,7 +25,7 @@ from developer_ecosystem_manager import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add backend directory to path
|
# Add backend directory to path
|
||||||
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
if backend_dir not in sys.path:
|
if backend_dir not in sys.path:
|
||||||
sys.path.insert(0, backend_dir)
|
sys.path.insert(0, backend_dir)
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ class TestDeveloperEcosystem:
|
|||||||
"""开发者生态系统测试类"""
|
"""开发者生态系统测试类"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.manager = DeveloperEcosystemManager()
|
self.manager = DeveloperEcosystemManager()
|
||||||
self.test_results = []
|
self.test_results = []
|
||||||
self.created_ids = {
|
self.created_ids = {
|
||||||
"sdk": [],
|
"sdk": [],
|
||||||
"template": [],
|
"template": [],
|
||||||
"plugin": [],
|
"plugin": [],
|
||||||
@@ -45,9 +45,9 @@ class TestDeveloperEcosystem:
|
|||||||
"portal_config": [],
|
"portal_config": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
def log(self, message: str, success: bool = True) -> None:
|
def log(self, message: str, success: bool = True) -> None:
|
||||||
"""记录测试结果"""
|
"""记录测试结果"""
|
||||||
status = "✅" if success else "❌"
|
status = "✅" if success else "❌"
|
||||||
print(f"{status} {message}")
|
print(f"{status} {message}")
|
||||||
self.test_results.append(
|
self.test_results.append(
|
||||||
{"message": message, "success": success, "timestamp": datetime.now().isoformat()}
|
{"message": message, "success": success, "timestamp": datetime.now().isoformat()}
|
||||||
@@ -122,429 +122,429 @@ class TestDeveloperEcosystem:
|
|||||||
def test_sdk_create(self) -> None:
|
def test_sdk_create(self) -> None:
|
||||||
"""测试创建 SDK"""
|
"""测试创建 SDK"""
|
||||||
try:
|
try:
|
||||||
sdk = self.manager.create_sdk_release(
|
sdk = self.manager.create_sdk_release(
|
||||||
name = "InsightFlow Python SDK",
|
name="InsightFlow Python SDK",
|
||||||
language = SDKLanguage.PYTHON,
|
language=SDKLanguage.PYTHON,
|
||||||
version = "1.0.0",
|
version="1.0.0",
|
||||||
description = "Python SDK for InsightFlow API",
|
description="Python SDK for InsightFlow API",
|
||||||
changelog = "Initial release",
|
changelog="Initial release",
|
||||||
download_url = "https://pypi.org/insightflow/1.0.0",
|
download_url="https://pypi.org/insightflow/1.0.0",
|
||||||
documentation_url = "https://docs.insightflow.io/python",
|
documentation_url="https://docs.insightflow.io/python",
|
||||||
repository_url = "https://github.com/insightflow/python-sdk",
|
repository_url="https://github.com/insightflow/python-sdk",
|
||||||
package_name = "insightflow",
|
package_name="insightflow",
|
||||||
min_platform_version = "1.0.0",
|
min_platform_version="1.0.0",
|
||||||
dependencies = [{"name": "requests", "version": ">= 2.0"}],
|
dependencies=[{"name": "requests", "version": ">= 2.0"}],
|
||||||
file_size = 1024000,
|
file_size=1024000,
|
||||||
checksum = "abc123",
|
checksum="abc123",
|
||||||
created_by = "test_user",
|
created_by="test_user",
|
||||||
)
|
)
|
||||||
self.created_ids["sdk"].append(sdk.id)
|
self.created_ids["sdk"].append(sdk.id)
|
||||||
self.log(f"Created SDK: {sdk.name} ({sdk.id})")
|
self.log(f"Created SDK: {sdk.name} ({sdk.id})")
|
||||||
|
|
||||||
# Create JavaScript SDK
|
# Create JavaScript SDK
|
||||||
sdk_js = self.manager.create_sdk_release(
|
sdk_js = self.manager.create_sdk_release(
|
||||||
name = "InsightFlow JavaScript SDK",
|
name="InsightFlow JavaScript SDK",
|
||||||
language = SDKLanguage.JAVASCRIPT,
|
language=SDKLanguage.JAVASCRIPT,
|
||||||
version = "1.0.0",
|
version="1.0.0",
|
||||||
description = "JavaScript SDK for InsightFlow API",
|
description="JavaScript SDK for InsightFlow API",
|
||||||
changelog = "Initial release",
|
changelog="Initial release",
|
||||||
download_url = "https://npmjs.com/insightflow/1.0.0",
|
download_url="https://npmjs.com/insightflow/1.0.0",
|
||||||
documentation_url = "https://docs.insightflow.io/js",
|
documentation_url="https://docs.insightflow.io/js",
|
||||||
repository_url = "https://github.com/insightflow/js-sdk",
|
repository_url="https://github.com/insightflow/js-sdk",
|
||||||
package_name = "@insightflow/sdk",
|
package_name="@insightflow/sdk",
|
||||||
min_platform_version = "1.0.0",
|
min_platform_version="1.0.0",
|
||||||
dependencies = [{"name": "axios", "version": ">= 0.21"}],
|
dependencies=[{"name": "axios", "version": ">= 0.21"}],
|
||||||
file_size = 512000,
|
file_size=512000,
|
||||||
checksum = "def456",
|
checksum="def456",
|
||||||
created_by = "test_user",
|
created_by="test_user",
|
||||||
)
|
)
|
||||||
self.created_ids["sdk"].append(sdk_js.id)
|
self.created_ids["sdk"].append(sdk_js.id)
|
||||||
self.log(f"Created SDK: {sdk_js.name} ({sdk_js.id})")
|
self.log(f"Created SDK: {sdk_js.name} ({sdk_js.id})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create SDK: {str(e)}", success = False)
|
self.log(f"Failed to create SDK: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_sdk_list(self) -> None:
|
def test_sdk_list(self) -> None:
|
||||||
"""测试列出 SDK"""
|
"""测试列出 SDK"""
|
||||||
try:
|
try:
|
||||||
sdks = self.manager.list_sdk_releases()
|
sdks = self.manager.list_sdk_releases()
|
||||||
self.log(f"Listed {len(sdks)} SDKs")
|
self.log(f"Listed {len(sdks)} SDKs")
|
||||||
|
|
||||||
# Test filter by language
|
# Test filter by language
|
||||||
python_sdks = self.manager.list_sdk_releases(language = SDKLanguage.PYTHON)
|
python_sdks = self.manager.list_sdk_releases(language=SDKLanguage.PYTHON)
|
||||||
self.log(f"Found {len(python_sdks)} Python SDKs")
|
self.log(f"Found {len(python_sdks)} Python SDKs")
|
||||||
|
|
||||||
# Test search
|
# Test search
|
||||||
search_results = self.manager.list_sdk_releases(search = "Python")
|
search_results = self.manager.list_sdk_releases(search="Python")
|
||||||
self.log(f"Search found {len(search_results)} SDKs")
|
self.log(f"Search found {len(search_results)} SDKs")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to list SDKs: {str(e)}", success = False)
|
self.log(f"Failed to list SDKs: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_sdk_get(self) -> None:
|
def test_sdk_get(self) -> None:
|
||||||
"""测试获取 SDK 详情"""
|
"""测试获取 SDK 详情"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["sdk"]:
|
if self.created_ids["sdk"]:
|
||||||
sdk = self.manager.get_sdk_release(self.created_ids["sdk"][0])
|
sdk = self.manager.get_sdk_release(self.created_ids["sdk"][0])
|
||||||
if sdk:
|
if sdk:
|
||||||
self.log(f"Retrieved SDK: {sdk.name}")
|
self.log(f"Retrieved SDK: {sdk.name}")
|
||||||
else:
|
else:
|
||||||
self.log("SDK not found", success = False)
|
self.log("SDK not found", success=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get SDK: {str(e)}", success = False)
|
self.log(f"Failed to get SDK: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_sdk_update(self) -> None:
|
def test_sdk_update(self) -> None:
|
||||||
"""测试更新 SDK"""
|
"""测试更新 SDK"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["sdk"]:
|
if self.created_ids["sdk"]:
|
||||||
sdk = self.manager.update_sdk_release(
|
sdk = self.manager.update_sdk_release(
|
||||||
self.created_ids["sdk"][0], description = "Updated description"
|
self.created_ids["sdk"][0], description="Updated description"
|
||||||
)
|
)
|
||||||
if sdk:
|
if sdk:
|
||||||
self.log(f"Updated SDK: {sdk.name}")
|
self.log(f"Updated SDK: {sdk.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to update SDK: {str(e)}", success = False)
|
self.log(f"Failed to update SDK: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_sdk_publish(self) -> None:
|
def test_sdk_publish(self) -> None:
|
||||||
"""测试发布 SDK"""
|
"""测试发布 SDK"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["sdk"]:
|
if self.created_ids["sdk"]:
|
||||||
sdk = self.manager.publish_sdk_release(self.created_ids["sdk"][0])
|
sdk = self.manager.publish_sdk_release(self.created_ids["sdk"][0])
|
||||||
if sdk:
|
if sdk:
|
||||||
self.log(f"Published SDK: {sdk.name} (status: {sdk.status.value})")
|
self.log(f"Published SDK: {sdk.name} (status: {sdk.status.value})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to publish SDK: {str(e)}", success = False)
|
self.log(f"Failed to publish SDK: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_sdk_version_add(self) -> None:
|
def test_sdk_version_add(self) -> None:
|
||||||
"""测试添加 SDK 版本"""
|
"""测试添加 SDK 版本"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["sdk"]:
|
if self.created_ids["sdk"]:
|
||||||
version = self.manager.add_sdk_version(
|
version = self.manager.add_sdk_version(
|
||||||
sdk_id = self.created_ids["sdk"][0],
|
sdk_id=self.created_ids["sdk"][0],
|
||||||
version = "1.1.0",
|
version="1.1.0",
|
||||||
is_lts = True,
|
is_lts=True,
|
||||||
release_notes = "Bug fixes and improvements",
|
release_notes="Bug fixes and improvements",
|
||||||
download_url = "https://pypi.org/insightflow/1.1.0",
|
download_url="https://pypi.org/insightflow/1.1.0",
|
||||||
checksum = "xyz789",
|
checksum="xyz789",
|
||||||
file_size = 1100000,
|
file_size=1100000,
|
||||||
)
|
)
|
||||||
self.log(f"Added SDK version: {version.version}")
|
self.log(f"Added SDK version: {version.version}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to add SDK version: {str(e)}", success = False)
|
self.log(f"Failed to add SDK version: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_create(self) -> None:
|
def test_template_create(self) -> None:
|
||||||
"""测试创建模板"""
|
"""测试创建模板"""
|
||||||
try:
|
try:
|
||||||
template = self.manager.create_template(
|
template = self.manager.create_template(
|
||||||
name = "医疗行业实体识别模板",
|
name="医疗行业实体识别模板",
|
||||||
description = "专门针对医疗行业的实体识别模板,支持疾病、药物、症状等实体",
|
description="专门针对医疗行业的实体识别模板,支持疾病、药物、症状等实体",
|
||||||
category = TemplateCategory.MEDICAL,
|
category=TemplateCategory.MEDICAL,
|
||||||
subcategory = "entity_recognition",
|
subcategory="entity_recognition",
|
||||||
tags = ["medical", "healthcare", "ner"],
|
tags=["medical", "healthcare", "ner"],
|
||||||
author_id = "dev_001",
|
author_id="dev_001",
|
||||||
author_name = "Medical AI Lab",
|
author_name="Medical AI Lab",
|
||||||
price = 99.0,
|
price=99.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
preview_image_url = "https://cdn.insightflow.io/templates/medical.png",
|
preview_image_url="https://cdn.insightflow.io/templates/medical.png",
|
||||||
demo_url = "https://demo.insightflow.io/medical",
|
demo_url="https://demo.insightflow.io/medical",
|
||||||
documentation_url = "https://docs.insightflow.io/templates/medical",
|
documentation_url="https://docs.insightflow.io/templates/medical",
|
||||||
download_url = "https://cdn.insightflow.io/templates/medical.zip",
|
download_url="https://cdn.insightflow.io/templates/medical.zip",
|
||||||
version = "1.0.0",
|
version="1.0.0",
|
||||||
min_platform_version = "2.0.0",
|
min_platform_version="2.0.0",
|
||||||
file_size = 5242880,
|
file_size=5242880,
|
||||||
checksum = "tpl123",
|
checksum="tpl123",
|
||||||
)
|
)
|
||||||
self.created_ids["template"].append(template.id)
|
self.created_ids["template"].append(template.id)
|
||||||
self.log(f"Created template: {template.name} ({template.id})")
|
self.log(f"Created template: {template.name} ({template.id})")
|
||||||
|
|
||||||
# Create free template
|
# Create free template
|
||||||
template_free = self.manager.create_template(
|
template_free = self.manager.create_template(
|
||||||
name = "通用实体识别模板",
|
name="通用实体识别模板",
|
||||||
description = "适用于一般场景的实体识别模板",
|
description="适用于一般场景的实体识别模板",
|
||||||
category = TemplateCategory.GENERAL,
|
category=TemplateCategory.GENERAL,
|
||||||
subcategory = None,
|
subcategory=None,
|
||||||
tags = ["general", "ner", "basic"],
|
tags=["general", "ner", "basic"],
|
||||||
author_id = "dev_002",
|
author_id="dev_002",
|
||||||
author_name = "InsightFlow Team",
|
author_name="InsightFlow Team",
|
||||||
price = 0.0,
|
price=0.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
)
|
)
|
||||||
self.created_ids["template"].append(template_free.id)
|
self.created_ids["template"].append(template_free.id)
|
||||||
self.log(f"Created free template: {template_free.name}")
|
self.log(f"Created free template: {template_free.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create template: {str(e)}", success = False)
|
self.log(f"Failed to create template: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_list(self) -> None:
|
def test_template_list(self) -> None:
|
||||||
"""测试列出模板"""
|
"""测试列出模板"""
|
||||||
try:
|
try:
|
||||||
templates = self.manager.list_templates()
|
templates = self.manager.list_templates()
|
||||||
self.log(f"Listed {len(templates)} templates")
|
self.log(f"Listed {len(templates)} templates")
|
||||||
|
|
||||||
# Filter by category
|
# Filter by category
|
||||||
medical_templates = self.manager.list_templates(category = TemplateCategory.MEDICAL)
|
medical_templates = self.manager.list_templates(category=TemplateCategory.MEDICAL)
|
||||||
self.log(f"Found {len(medical_templates)} medical templates")
|
self.log(f"Found {len(medical_templates)} medical templates")
|
||||||
|
|
||||||
# Filter by price
|
# Filter by price
|
||||||
free_templates = self.manager.list_templates(max_price = 0)
|
free_templates = self.manager.list_templates(max_price=0)
|
||||||
self.log(f"Found {len(free_templates)} free templates")
|
self.log(f"Found {len(free_templates)} free templates")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to list templates: {str(e)}", success = False)
|
self.log(f"Failed to list templates: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_get(self) -> None:
|
def test_template_get(self) -> None:
|
||||||
"""测试获取模板详情"""
|
"""测试获取模板详情"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["template"]:
|
if self.created_ids["template"]:
|
||||||
template = self.manager.get_template(self.created_ids["template"][0])
|
template = self.manager.get_template(self.created_ids["template"][0])
|
||||||
if template:
|
if template:
|
||||||
self.log(f"Retrieved template: {template.name}")
|
self.log(f"Retrieved template: {template.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get template: {str(e)}", success = False)
|
self.log(f"Failed to get template: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_approve(self) -> None:
|
def test_template_approve(self) -> None:
|
||||||
"""测试审核通过模板"""
|
"""测试审核通过模板"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["template"]:
|
if self.created_ids["template"]:
|
||||||
template = self.manager.approve_template(
|
template = self.manager.approve_template(
|
||||||
self.created_ids["template"][0], reviewed_by = "admin_001"
|
self.created_ids["template"][0], reviewed_by="admin_001"
|
||||||
)
|
)
|
||||||
if template:
|
if template:
|
||||||
self.log(f"Approved template: {template.name}")
|
self.log(f"Approved template: {template.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to approve template: {str(e)}", success = False)
|
self.log(f"Failed to approve template: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_publish(self) -> None:
|
def test_template_publish(self) -> None:
|
||||||
"""测试发布模板"""
|
"""测试发布模板"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["template"]:
|
if self.created_ids["template"]:
|
||||||
template = self.manager.publish_template(self.created_ids["template"][0])
|
template = self.manager.publish_template(self.created_ids["template"][0])
|
||||||
if template:
|
if template:
|
||||||
self.log(f"Published template: {template.name}")
|
self.log(f"Published template: {template.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to publish template: {str(e)}", success = False)
|
self.log(f"Failed to publish template: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_template_review(self) -> None:
|
def test_template_review(self) -> None:
|
||||||
"""测试添加模板评价"""
|
"""测试添加模板评价"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["template"]:
|
if self.created_ids["template"]:
|
||||||
review = self.manager.add_template_review(
|
review = self.manager.add_template_review(
|
||||||
template_id = self.created_ids["template"][0],
|
template_id=self.created_ids["template"][0],
|
||||||
user_id = "user_001",
|
user_id="user_001",
|
||||||
user_name = "Test User",
|
user_name="Test User",
|
||||||
rating = 5,
|
rating=5,
|
||||||
comment = "Great template! Very accurate for medical entities.",
|
comment="Great template! Very accurate for medical entities.",
|
||||||
is_verified_purchase = True,
|
is_verified_purchase=True,
|
||||||
)
|
)
|
||||||
self.log(f"Added template review: {review.rating} stars")
|
self.log(f"Added template review: {review.rating} stars")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to add template review: {str(e)}", success = False)
|
self.log(f"Failed to add template review: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_create(self) -> None:
|
def test_plugin_create(self) -> None:
|
||||||
"""测试创建插件"""
|
"""测试创建插件"""
|
||||||
try:
|
try:
|
||||||
plugin = self.manager.create_plugin(
|
plugin = self.manager.create_plugin(
|
||||||
name = "飞书机器人集成插件",
|
name="飞书机器人集成插件",
|
||||||
description = "将 InsightFlow 与飞书机器人集成,实现自动通知",
|
description="将 InsightFlow 与飞书机器人集成,实现自动通知",
|
||||||
category = PluginCategory.INTEGRATION,
|
category=PluginCategory.INTEGRATION,
|
||||||
tags = ["feishu", "bot", "integration", "notification"],
|
tags=["feishu", "bot", "integration", "notification"],
|
||||||
author_id = "dev_003",
|
author_id="dev_003",
|
||||||
author_name = "Integration Team",
|
author_name="Integration Team",
|
||||||
price = 49.0,
|
price=49.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
pricing_model = "paid",
|
pricing_model="paid",
|
||||||
preview_image_url = "https://cdn.insightflow.io/plugins/feishu.png",
|
preview_image_url="https://cdn.insightflow.io/plugins/feishu.png",
|
||||||
demo_url = "https://demo.insightflow.io/feishu",
|
demo_url="https://demo.insightflow.io/feishu",
|
||||||
documentation_url = "https://docs.insightflow.io/plugins/feishu",
|
documentation_url="https://docs.insightflow.io/plugins/feishu",
|
||||||
repository_url = "https://github.com/insightflow/feishu-plugin",
|
repository_url="https://github.com/insightflow/feishu-plugin",
|
||||||
download_url = "https://cdn.insightflow.io/plugins/feishu.zip",
|
download_url="https://cdn.insightflow.io/plugins/feishu.zip",
|
||||||
webhook_url = "https://api.insightflow.io/webhooks/feishu",
|
webhook_url="https://api.insightflow.io/webhooks/feishu",
|
||||||
permissions = ["read:projects", "write:notifications"],
|
permissions=["read:projects", "write:notifications"],
|
||||||
version = "1.0.0",
|
version="1.0.0",
|
||||||
min_platform_version = "2.0.0",
|
min_platform_version="2.0.0",
|
||||||
file_size = 1048576,
|
file_size=1048576,
|
||||||
checksum = "plg123",
|
checksum="plg123",
|
||||||
)
|
)
|
||||||
self.created_ids["plugin"].append(plugin.id)
|
self.created_ids["plugin"].append(plugin.id)
|
||||||
self.log(f"Created plugin: {plugin.name} ({plugin.id})")
|
self.log(f"Created plugin: {plugin.name} ({plugin.id})")
|
||||||
|
|
||||||
# Create free plugin
|
# Create free plugin
|
||||||
plugin_free = self.manager.create_plugin(
|
plugin_free = self.manager.create_plugin(
|
||||||
name = "数据导出插件",
|
name="数据导出插件",
|
||||||
description = "支持多种格式的数据导出",
|
description="支持多种格式的数据导出",
|
||||||
category = PluginCategory.ANALYSIS,
|
category=PluginCategory.ANALYSIS,
|
||||||
tags = ["export", "data", "csv", "json"],
|
tags=["export", "data", "csv", "json"],
|
||||||
author_id = "dev_004",
|
author_id="dev_004",
|
||||||
author_name = "Data Team",
|
author_name="Data Team",
|
||||||
price = 0.0,
|
price=0.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
pricing_model = "free",
|
pricing_model="free",
|
||||||
)
|
)
|
||||||
self.created_ids["plugin"].append(plugin_free.id)
|
self.created_ids["plugin"].append(plugin_free.id)
|
||||||
self.log(f"Created free plugin: {plugin_free.name}")
|
self.log(f"Created free plugin: {plugin_free.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create plugin: {str(e)}", success = False)
|
self.log(f"Failed to create plugin: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_list(self) -> None:
|
def test_plugin_list(self) -> None:
|
||||||
"""测试列出插件"""
|
"""测试列出插件"""
|
||||||
try:
|
try:
|
||||||
plugins = self.manager.list_plugins()
|
plugins = self.manager.list_plugins()
|
||||||
self.log(f"Listed {len(plugins)} plugins")
|
self.log(f"Listed {len(plugins)} plugins")
|
||||||
|
|
||||||
# Filter by category
|
# Filter by category
|
||||||
integration_plugins = self.manager.list_plugins(category = PluginCategory.INTEGRATION)
|
integration_plugins = self.manager.list_plugins(category=PluginCategory.INTEGRATION)
|
||||||
self.log(f"Found {len(integration_plugins)} integration plugins")
|
self.log(f"Found {len(integration_plugins)} integration plugins")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to list plugins: {str(e)}", success = False)
|
self.log(f"Failed to list plugins: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_get(self) -> None:
|
def test_plugin_get(self) -> None:
|
||||||
"""测试获取插件详情"""
|
"""测试获取插件详情"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["plugin"]:
|
if self.created_ids["plugin"]:
|
||||||
plugin = self.manager.get_plugin(self.created_ids["plugin"][0])
|
plugin = self.manager.get_plugin(self.created_ids["plugin"][0])
|
||||||
if plugin:
|
if plugin:
|
||||||
self.log(f"Retrieved plugin: {plugin.name}")
|
self.log(f"Retrieved plugin: {plugin.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get plugin: {str(e)}", success = False)
|
self.log(f"Failed to get plugin: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_review(self) -> None:
|
def test_plugin_review(self) -> None:
|
||||||
"""测试审核插件"""
|
"""测试审核插件"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["plugin"]:
|
if self.created_ids["plugin"]:
|
||||||
plugin = self.manager.review_plugin(
|
plugin = self.manager.review_plugin(
|
||||||
self.created_ids["plugin"][0],
|
self.created_ids["plugin"][0],
|
||||||
reviewed_by = "admin_001",
|
reviewed_by="admin_001",
|
||||||
status = PluginStatus.APPROVED,
|
status=PluginStatus.APPROVED,
|
||||||
notes = "Code review passed",
|
notes="Code review passed",
|
||||||
)
|
)
|
||||||
if plugin:
|
if plugin:
|
||||||
self.log(f"Reviewed plugin: {plugin.name} ({plugin.status.value})")
|
self.log(f"Reviewed plugin: {plugin.name} ({plugin.status.value})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to review plugin: {str(e)}", success = False)
|
self.log(f"Failed to review plugin: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_publish(self) -> None:
|
def test_plugin_publish(self) -> None:
|
||||||
"""测试发布插件"""
|
"""测试发布插件"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["plugin"]:
|
if self.created_ids["plugin"]:
|
||||||
plugin = self.manager.publish_plugin(self.created_ids["plugin"][0])
|
plugin = self.manager.publish_plugin(self.created_ids["plugin"][0])
|
||||||
if plugin:
|
if plugin:
|
||||||
self.log(f"Published plugin: {plugin.name}")
|
self.log(f"Published plugin: {plugin.name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to publish plugin: {str(e)}", success = False)
|
self.log(f"Failed to publish plugin: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_plugin_review_add(self) -> None:
|
def test_plugin_review_add(self) -> None:
|
||||||
"""测试添加插件评价"""
|
"""测试添加插件评价"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["plugin"]:
|
if self.created_ids["plugin"]:
|
||||||
review = self.manager.add_plugin_review(
|
review = self.manager.add_plugin_review(
|
||||||
plugin_id = self.created_ids["plugin"][0],
|
plugin_id=self.created_ids["plugin"][0],
|
||||||
user_id = "user_002",
|
user_id="user_002",
|
||||||
user_name = "Plugin User",
|
user_name="Plugin User",
|
||||||
rating = 4,
|
rating=4,
|
||||||
comment = "Works great with Feishu!",
|
comment="Works great with Feishu!",
|
||||||
is_verified_purchase = True,
|
is_verified_purchase=True,
|
||||||
)
|
)
|
||||||
self.log(f"Added plugin review: {review.rating} stars")
|
self.log(f"Added plugin review: {review.rating} stars")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to add plugin review: {str(e)}", success = False)
|
self.log(f"Failed to add plugin review: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_developer_profile_create(self) -> None:
|
def test_developer_profile_create(self) -> None:
|
||||||
"""测试创建开发者档案"""
|
"""测试创建开发者档案"""
|
||||||
try:
|
try:
|
||||||
# Generate unique user IDs
|
# Generate unique user IDs
|
||||||
unique_id = uuid.uuid4().hex[:8]
|
unique_id = uuid.uuid4().hex[:8]
|
||||||
|
|
||||||
profile = self.manager.create_developer_profile(
|
profile = self.manager.create_developer_profile(
|
||||||
user_id = f"user_dev_{unique_id}_001",
|
user_id=f"user_dev_{unique_id}_001",
|
||||||
display_name = "张三",
|
display_name="张三",
|
||||||
email = f"zhangsan_{unique_id}@example.com",
|
email=f"zhangsan_{unique_id}@example.com",
|
||||||
bio = "专注于医疗AI和自然语言处理",
|
bio="专注于医疗AI和自然语言处理",
|
||||||
website = "https://zhangsan.dev",
|
website="https://zhangsan.dev",
|
||||||
github_url = "https://github.com/zhangsan",
|
github_url="https://github.com/zhangsan",
|
||||||
avatar_url = "https://cdn.example.com/avatars/zhangsan.png",
|
avatar_url="https://cdn.example.com/avatars/zhangsan.png",
|
||||||
)
|
)
|
||||||
self.created_ids["developer"].append(profile.id)
|
self.created_ids["developer"].append(profile.id)
|
||||||
self.log(f"Created developer profile: {profile.display_name} ({profile.id})")
|
self.log(f"Created developer profile: {profile.display_name} ({profile.id})")
|
||||||
|
|
||||||
# Create another developer
|
# Create another developer
|
||||||
profile2 = self.manager.create_developer_profile(
|
profile2 = self.manager.create_developer_profile(
|
||||||
user_id = f"user_dev_{unique_id}_002",
|
user_id=f"user_dev_{unique_id}_002",
|
||||||
display_name = "李四",
|
display_name="李四",
|
||||||
email = f"lisi_{unique_id}@example.com",
|
email=f"lisi_{unique_id}@example.com",
|
||||||
bio = "全栈开发者,热爱开源",
|
bio="全栈开发者,热爱开源",
|
||||||
)
|
)
|
||||||
self.created_ids["developer"].append(profile2.id)
|
self.created_ids["developer"].append(profile2.id)
|
||||||
self.log(f"Created developer profile: {profile2.display_name}")
|
self.log(f"Created developer profile: {profile2.display_name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create developer profile: {str(e)}", success = False)
|
self.log(f"Failed to create developer profile: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_developer_profile_get(self) -> None:
|
def test_developer_profile_get(self) -> None:
|
||||||
"""测试获取开发者档案"""
|
"""测试获取开发者档案"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["developer"]:
|
if self.created_ids["developer"]:
|
||||||
profile = self.manager.get_developer_profile(self.created_ids["developer"][0])
|
profile = self.manager.get_developer_profile(self.created_ids["developer"][0])
|
||||||
if profile:
|
if profile:
|
||||||
self.log(f"Retrieved developer profile: {profile.display_name}")
|
self.log(f"Retrieved developer profile: {profile.display_name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get developer profile: {str(e)}", success = False)
|
self.log(f"Failed to get developer profile: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_developer_verify(self) -> None:
|
def test_developer_verify(self) -> None:
|
||||||
"""测试验证开发者"""
|
"""测试验证开发者"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["developer"]:
|
if self.created_ids["developer"]:
|
||||||
profile = self.manager.verify_developer(
|
profile = self.manager.verify_developer(
|
||||||
self.created_ids["developer"][0], DeveloperStatus.VERIFIED
|
self.created_ids["developer"][0], DeveloperStatus.VERIFIED
|
||||||
)
|
)
|
||||||
if profile:
|
if profile:
|
||||||
self.log(f"Verified developer: {profile.display_name} ({profile.status.value})")
|
self.log(f"Verified developer: {profile.display_name} ({profile.status.value})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to verify developer: {str(e)}", success = False)
|
self.log(f"Failed to verify developer: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_developer_stats_update(self) -> None:
|
def test_developer_stats_update(self) -> None:
|
||||||
"""测试更新开发者统计"""
|
"""测试更新开发者统计"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["developer"]:
|
if self.created_ids["developer"]:
|
||||||
self.manager.update_developer_stats(self.created_ids["developer"][0])
|
self.manager.update_developer_stats(self.created_ids["developer"][0])
|
||||||
profile = self.manager.get_developer_profile(self.created_ids["developer"][0])
|
profile = self.manager.get_developer_profile(self.created_ids["developer"][0])
|
||||||
self.log(
|
self.log(
|
||||||
f"Updated developer stats: {profile.plugin_count} plugins, {profile.template_count} templates"
|
f"Updated developer stats: {profile.plugin_count} plugins, {profile.template_count} templates"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to update developer stats: {str(e)}", success = False)
|
self.log(f"Failed to update developer stats: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_code_example_create(self) -> None:
|
def test_code_example_create(self) -> None:
|
||||||
"""测试创建代码示例"""
|
"""测试创建代码示例"""
|
||||||
try:
|
try:
|
||||||
example = self.manager.create_code_example(
|
example = self.manager.create_code_example(
|
||||||
title = "使用 Python SDK 创建项目",
|
title="使用 Python SDK 创建项目",
|
||||||
description = "演示如何使用 Python SDK 创建新项目",
|
description="演示如何使用 Python SDK 创建新项目",
|
||||||
language = "python",
|
language="python",
|
||||||
category = "quickstart",
|
category="quickstart",
|
||||||
code = """from insightflow import Client
|
code="""from insightflow import Client
|
||||||
|
|
||||||
client = Client(api_key = "your_api_key")
|
client = Client(api_key = "your_api_key")
|
||||||
project = client.projects.create(name = "My Project")
|
project = client.projects.create(name = "My Project")
|
||||||
print(f"Created project: {project.id}")
|
print(f"Created project: {project.id}")
|
||||||
""",
|
""",
|
||||||
explanation = "首先导入 Client 类,然后使用 API Key 初始化客户端,最后调用 create 方法创建项目。",
|
explanation="首先导入 Client 类,然后使用 API Key 初始化客户端,最后调用 create 方法创建项目。",
|
||||||
tags = ["python", "quickstart", "projects"],
|
tags=["python", "quickstart", "projects"],
|
||||||
author_id = "dev_001",
|
author_id="dev_001",
|
||||||
author_name = "InsightFlow Team",
|
author_name="InsightFlow Team",
|
||||||
api_endpoints = ["/api/v1/projects"],
|
api_endpoints=["/api/v1/projects"],
|
||||||
)
|
)
|
||||||
self.created_ids["code_example"].append(example.id)
|
self.created_ids["code_example"].append(example.id)
|
||||||
self.log(f"Created code example: {example.title}")
|
self.log(f"Created code example: {example.title}")
|
||||||
|
|
||||||
# Create JavaScript example
|
# Create JavaScript example
|
||||||
example_js = self.manager.create_code_example(
|
example_js = self.manager.create_code_example(
|
||||||
title = "使用 JavaScript SDK 上传文件",
|
title="使用 JavaScript SDK 上传文件",
|
||||||
description = "演示如何使用 JavaScript SDK 上传音频文件",
|
description="演示如何使用 JavaScript SDK 上传音频文件",
|
||||||
language = "javascript",
|
language="javascript",
|
||||||
category = "upload",
|
category="upload",
|
||||||
code = """const { Client } = require('insightflow');
|
code="""const { Client } = require('insightflow');
|
||||||
|
|
||||||
const client = new Client({ apiKey: 'your_api_key' });
|
const client = new Client({ apiKey: 'your_api_key' });
|
||||||
const result = await client.uploads.create({
|
const result = await client.uploads.create({
|
||||||
@@ -553,104 +553,104 @@ const result = await client.uploads.create({
|
|||||||
});
|
});
|
||||||
console.log('Upload complete:', result.id);
|
console.log('Upload complete:', result.id);
|
||||||
""",
|
""",
|
||||||
explanation = "使用 JavaScript SDK 上传文件到 InsightFlow",
|
explanation="使用 JavaScript SDK 上传文件到 InsightFlow",
|
||||||
tags = ["javascript", "upload", "audio"],
|
tags=["javascript", "upload", "audio"],
|
||||||
author_id = "dev_002",
|
author_id="dev_002",
|
||||||
author_name = "JS Team",
|
author_name="JS Team",
|
||||||
)
|
)
|
||||||
self.created_ids["code_example"].append(example_js.id)
|
self.created_ids["code_example"].append(example_js.id)
|
||||||
self.log(f"Created code example: {example_js.title}")
|
self.log(f"Created code example: {example_js.title}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create code example: {str(e)}", success = False)
|
self.log(f"Failed to create code example: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_code_example_list(self) -> None:
|
def test_code_example_list(self) -> None:
|
||||||
"""测试列出代码示例"""
|
"""测试列出代码示例"""
|
||||||
try:
|
try:
|
||||||
examples = self.manager.list_code_examples()
|
examples = self.manager.list_code_examples()
|
||||||
self.log(f"Listed {len(examples)} code examples")
|
self.log(f"Listed {len(examples)} code examples")
|
||||||
|
|
||||||
# Filter by language
|
# Filter by language
|
||||||
python_examples = self.manager.list_code_examples(language = "python")
|
python_examples = self.manager.list_code_examples(language="python")
|
||||||
self.log(f"Found {len(python_examples)} Python examples")
|
self.log(f"Found {len(python_examples)} Python examples")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to list code examples: {str(e)}", success = False)
|
self.log(f"Failed to list code examples: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_code_example_get(self) -> None:
|
def test_code_example_get(self) -> None:
|
||||||
"""测试获取代码示例详情"""
|
"""测试获取代码示例详情"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["code_example"]:
|
if self.created_ids["code_example"]:
|
||||||
example = self.manager.get_code_example(self.created_ids["code_example"][0])
|
example = self.manager.get_code_example(self.created_ids["code_example"][0])
|
||||||
if example:
|
if example:
|
||||||
self.log(
|
self.log(
|
||||||
f"Retrieved code example: {example.title} (views: {example.view_count})"
|
f"Retrieved code example: {example.title} (views: {example.view_count})"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get code example: {str(e)}", success = False)
|
self.log(f"Failed to get code example: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_portal_config_create(self) -> None:
|
def test_portal_config_create(self) -> None:
|
||||||
"""测试创建开发者门户配置"""
|
"""测试创建开发者门户配置"""
|
||||||
try:
|
try:
|
||||||
config = self.manager.create_portal_config(
|
config = self.manager.create_portal_config(
|
||||||
name = "InsightFlow Developer Portal",
|
name="InsightFlow Developer Portal",
|
||||||
description = "开发者门户 - SDK、API 文档和示例代码",
|
description="开发者门户 - SDK、API 文档和示例代码",
|
||||||
theme = "default",
|
theme="default",
|
||||||
primary_color = "#1890ff",
|
primary_color="#1890ff",
|
||||||
secondary_color = "#52c41a",
|
secondary_color="#52c41a",
|
||||||
support_email = "developers@insightflow.io",
|
support_email="developers@insightflow.io",
|
||||||
support_url = "https://support.insightflow.io",
|
support_url="https://support.insightflow.io",
|
||||||
github_url = "https://github.com/insightflow",
|
github_url="https://github.com/insightflow",
|
||||||
discord_url = "https://discord.gg/insightflow",
|
discord_url="https://discord.gg/insightflow",
|
||||||
api_base_url = "https://api.insightflow.io/v1",
|
api_base_url="https://api.insightflow.io/v1",
|
||||||
)
|
)
|
||||||
self.created_ids["portal_config"].append(config.id)
|
self.created_ids["portal_config"].append(config.id)
|
||||||
self.log(f"Created portal config: {config.name}")
|
self.log(f"Created portal config: {config.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to create portal config: {str(e)}", success = False)
|
self.log(f"Failed to create portal config: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_portal_config_get(self) -> None:
|
def test_portal_config_get(self) -> None:
|
||||||
"""测试获取开发者门户配置"""
|
"""测试获取开发者门户配置"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["portal_config"]:
|
if self.created_ids["portal_config"]:
|
||||||
config = self.manager.get_portal_config(self.created_ids["portal_config"][0])
|
config = self.manager.get_portal_config(self.created_ids["portal_config"][0])
|
||||||
if config:
|
if config:
|
||||||
self.log(f"Retrieved portal config: {config.name}")
|
self.log(f"Retrieved portal config: {config.name}")
|
||||||
|
|
||||||
# Test active config
|
# Test active config
|
||||||
active_config = self.manager.get_active_portal_config()
|
active_config = self.manager.get_active_portal_config()
|
||||||
if active_config:
|
if active_config:
|
||||||
self.log(f"Active portal config: {active_config.name}")
|
self.log(f"Active portal config: {active_config.name}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get portal config: {str(e)}", success = False)
|
self.log(f"Failed to get portal config: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_revenue_record(self) -> None:
|
def test_revenue_record(self) -> None:
|
||||||
"""测试记录开发者收益"""
|
"""测试记录开发者收益"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["developer"] and self.created_ids["plugin"]:
|
if self.created_ids["developer"] and self.created_ids["plugin"]:
|
||||||
revenue = self.manager.record_revenue(
|
revenue = self.manager.record_revenue(
|
||||||
developer_id = self.created_ids["developer"][0],
|
developer_id=self.created_ids["developer"][0],
|
||||||
item_type = "plugin",
|
item_type="plugin",
|
||||||
item_id = self.created_ids["plugin"][0],
|
item_id=self.created_ids["plugin"][0],
|
||||||
item_name = "飞书机器人集成插件",
|
item_name="飞书机器人集成插件",
|
||||||
sale_amount = 49.0,
|
sale_amount=49.0,
|
||||||
currency = "CNY",
|
currency="CNY",
|
||||||
buyer_id = "user_buyer_001",
|
buyer_id="user_buyer_001",
|
||||||
transaction_id = "txn_123456",
|
transaction_id="txn_123456",
|
||||||
)
|
)
|
||||||
self.log(f"Recorded revenue: {revenue.sale_amount} {revenue.currency}")
|
self.log(f"Recorded revenue: {revenue.sale_amount} {revenue.currency}")
|
||||||
self.log(f" - Platform fee: {revenue.platform_fee}")
|
self.log(f" - Platform fee: {revenue.platform_fee}")
|
||||||
self.log(f" - Developer earnings: {revenue.developer_earnings}")
|
self.log(f" - Developer earnings: {revenue.developer_earnings}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to record revenue: {str(e)}", success = False)
|
self.log(f"Failed to record revenue: {str(e)}", success=False)
|
||||||
|
|
||||||
def test_revenue_summary(self) -> None:
|
def test_revenue_summary(self) -> None:
|
||||||
"""测试获取开发者收益汇总"""
|
"""测试获取开发者收益汇总"""
|
||||||
try:
|
try:
|
||||||
if self.created_ids["developer"]:
|
if self.created_ids["developer"]:
|
||||||
summary = self.manager.get_developer_revenue_summary(
|
summary = self.manager.get_developer_revenue_summary(
|
||||||
self.created_ids["developer"][0]
|
self.created_ids["developer"][0]
|
||||||
)
|
)
|
||||||
self.log("Revenue summary for developer:")
|
self.log("Revenue summary for developer:")
|
||||||
@@ -659,7 +659,7 @@ console.log('Upload complete:', result.id);
|
|||||||
self.log(f" - Total earnings: {summary['total_earnings']}")
|
self.log(f" - Total earnings: {summary['total_earnings']}")
|
||||||
self.log(f" - Transaction count: {summary['transaction_count']}")
|
self.log(f" - Transaction count: {summary['transaction_count']}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failed to get revenue summary: {str(e)}", success = False)
|
self.log(f"Failed to get revenue summary: {str(e)}", success=False)
|
||||||
|
|
||||||
def print_summary(self) -> None:
|
def print_summary(self) -> None:
|
||||||
"""打印测试摘要"""
|
"""打印测试摘要"""
|
||||||
@@ -667,9 +667,9 @@ console.log('Upload complete:', result.id);
|
|||||||
print("Test Summary")
|
print("Test Summary")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
total = len(self.test_results)
|
total = len(self.test_results)
|
||||||
passed = sum(1 for r in self.test_results if r["success"])
|
passed = sum(1 for r in self.test_results if r["success"])
|
||||||
failed = total - passed
|
failed = total - passed
|
||||||
|
|
||||||
print(f"Total tests: {total}")
|
print(f"Total tests: {total}")
|
||||||
print(f"Passed: {passed} ✅")
|
print(f"Passed: {passed} ✅")
|
||||||
@@ -691,7 +691,7 @@ console.log('Upload complete:', result.id);
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
test = TestDeveloperEcosystem()
|
test = TestDeveloperEcosystem()
|
||||||
test.run_all_tests()
|
test.run_all_tests()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+214
-214
@@ -26,7 +26,7 @@ from ops_manager import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add backend directory to path
|
# Add backend directory to path
|
||||||
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
backend_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
if backend_dir not in sys.path:
|
if backend_dir not in sys.path:
|
||||||
sys.path.insert(0, backend_dir)
|
sys.path.insert(0, backend_dir)
|
||||||
|
|
||||||
@@ -35,13 +35,13 @@ class TestOpsManager:
|
|||||||
"""测试运维与监控管理器"""
|
"""测试运维与监控管理器"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.manager = get_ops_manager()
|
self.manager = get_ops_manager()
|
||||||
self.tenant_id = "test_tenant_001"
|
self.tenant_id = "test_tenant_001"
|
||||||
self.test_results = []
|
self.test_results = []
|
||||||
|
|
||||||
def log(self, message: str, success: bool = True) -> None:
|
def log(self, message: str, success: bool = True) -> None:
|
||||||
"""记录测试结果"""
|
"""记录测试结果"""
|
||||||
status = "✅" if success else "❌"
|
status = "✅" if success else "❌"
|
||||||
print(f"{status} {message}")
|
print(f"{status} {message}")
|
||||||
self.test_results.append((message, success))
|
self.test_results.append((message, success))
|
||||||
|
|
||||||
@@ -79,57 +79,57 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建阈值告警规则
|
# 创建阈值告警规则
|
||||||
rule1 = self.manager.create_alert_rule(
|
rule1 = self.manager.create_alert_rule(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "CPU 使用率告警",
|
name="CPU 使用率告警",
|
||||||
description = "当 CPU 使用率超过 80% 时触发告警",
|
description="当 CPU 使用率超过 80% 时触发告警",
|
||||||
rule_type = AlertRuleType.THRESHOLD,
|
rule_type=AlertRuleType.THRESHOLD,
|
||||||
severity = AlertSeverity.P1,
|
severity=AlertSeverity.P1,
|
||||||
metric = "cpu_usage_percent",
|
metric="cpu_usage_percent",
|
||||||
condition = ">",
|
condition=">",
|
||||||
threshold = 80.0,
|
threshold=80.0,
|
||||||
duration = 300,
|
duration=300,
|
||||||
evaluation_interval = 60,
|
evaluation_interval=60,
|
||||||
channels = [],
|
channels=[],
|
||||||
labels = {"service": "api", "team": "platform"},
|
labels={"service": "api", "team": "platform"},
|
||||||
annotations = {"summary": "CPU 使用率过高", "runbook": "https://wiki/runbooks/cpu"},
|
annotations={"summary": "CPU 使用率过高", "runbook": "https://wiki/runbooks/cpu"},
|
||||||
created_by = "test_user",
|
created_by="test_user",
|
||||||
)
|
)
|
||||||
self.log(f"Created alert rule: {rule1.name} (ID: {rule1.id})")
|
self.log(f"Created alert rule: {rule1.name} (ID: {rule1.id})")
|
||||||
|
|
||||||
# 创建异常检测告警规则
|
# 创建异常检测告警规则
|
||||||
rule2 = self.manager.create_alert_rule(
|
rule2 = self.manager.create_alert_rule(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "内存异常检测",
|
name="内存异常检测",
|
||||||
description = "检测内存使用异常",
|
description="检测内存使用异常",
|
||||||
rule_type = AlertRuleType.ANOMALY,
|
rule_type=AlertRuleType.ANOMALY,
|
||||||
severity = AlertSeverity.P2,
|
severity=AlertSeverity.P2,
|
||||||
metric = "memory_usage_percent",
|
metric="memory_usage_percent",
|
||||||
condition = ">",
|
condition=">",
|
||||||
threshold = 0.0,
|
threshold=0.0,
|
||||||
duration = 600,
|
duration=600,
|
||||||
evaluation_interval = 300,
|
evaluation_interval=300,
|
||||||
channels = [],
|
channels=[],
|
||||||
labels = {"service": "database"},
|
labels={"service": "database"},
|
||||||
annotations = {},
|
annotations={},
|
||||||
created_by = "test_user",
|
created_by="test_user",
|
||||||
)
|
)
|
||||||
self.log(f"Created anomaly alert rule: {rule2.name} (ID: {rule2.id})")
|
self.log(f"Created anomaly alert rule: {rule2.name} (ID: {rule2.id})")
|
||||||
|
|
||||||
# 获取告警规则
|
# 获取告警规则
|
||||||
fetched_rule = self.manager.get_alert_rule(rule1.id)
|
fetched_rule = self.manager.get_alert_rule(rule1.id)
|
||||||
assert fetched_rule is not None
|
assert fetched_rule is not None
|
||||||
assert fetched_rule.name == rule1.name
|
assert fetched_rule.name == rule1.name
|
||||||
self.log(f"Fetched alert rule: {fetched_rule.name}")
|
self.log(f"Fetched alert rule: {fetched_rule.name}")
|
||||||
|
|
||||||
# 列出租户的所有告警规则
|
# 列出租户的所有告警规则
|
||||||
rules = self.manager.list_alert_rules(self.tenant_id)
|
rules = self.manager.list_alert_rules(self.tenant_id)
|
||||||
assert len(rules) >= 2
|
assert len(rules) >= 2
|
||||||
self.log(f"Listed {len(rules)} alert rules for tenant")
|
self.log(f"Listed {len(rules)} alert rules for tenant")
|
||||||
|
|
||||||
# 更新告警规则
|
# 更新告警规则
|
||||||
updated_rule = self.manager.update_alert_rule(
|
updated_rule = self.manager.update_alert_rule(
|
||||||
rule1.id, threshold = 85.0, description = "更新后的描述"
|
rule1.id, threshold=85.0, description="更新后的描述"
|
||||||
)
|
)
|
||||||
assert updated_rule.threshold == 85.0
|
assert updated_rule.threshold == 85.0
|
||||||
self.log(f"Updated alert rule threshold to {updated_rule.threshold}")
|
self.log(f"Updated alert rule threshold to {updated_rule.threshold}")
|
||||||
@@ -140,7 +140,7 @@ class TestOpsManager:
|
|||||||
self.log("Deleted test alert rules")
|
self.log("Deleted test alert rules")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Alert rules test failed: {e}", success = False)
|
self.log(f"Alert rules test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_alert_channels(self) -> None:
|
def test_alert_channels(self) -> None:
|
||||||
"""测试告警渠道管理"""
|
"""测试告警渠道管理"""
|
||||||
@@ -148,49 +148,49 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建飞书告警渠道
|
# 创建飞书告警渠道
|
||||||
channel1 = self.manager.create_alert_channel(
|
channel1 = self.manager.create_alert_channel(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "飞书告警",
|
name="飞书告警",
|
||||||
channel_type = AlertChannelType.FEISHU,
|
channel_type=AlertChannelType.FEISHU,
|
||||||
config = {
|
config={
|
||||||
"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/test",
|
"webhook_url": "https://open.feishu.cn/open-apis/bot/v2/hook/test",
|
||||||
"secret": "test_secret",
|
"secret": "test_secret",
|
||||||
},
|
},
|
||||||
severity_filter = ["p0", "p1"],
|
severity_filter=["p0", "p1"],
|
||||||
)
|
)
|
||||||
self.log(f"Created Feishu channel: {channel1.name} (ID: {channel1.id})")
|
self.log(f"Created Feishu channel: {channel1.name} (ID: {channel1.id})")
|
||||||
|
|
||||||
# 创建钉钉告警渠道
|
# 创建钉钉告警渠道
|
||||||
channel2 = self.manager.create_alert_channel(
|
channel2 = self.manager.create_alert_channel(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "钉钉告警",
|
name="钉钉告警",
|
||||||
channel_type = AlertChannelType.DINGTALK,
|
channel_type=AlertChannelType.DINGTALK,
|
||||||
config = {
|
config={
|
||||||
"webhook_url": "https://oapi.dingtalk.com/robot/send?access_token = test",
|
"webhook_url": "https://oapi.dingtalk.com/robot/send?access_token = test",
|
||||||
"secret": "test_secret",
|
"secret": "test_secret",
|
||||||
},
|
},
|
||||||
severity_filter = ["p0", "p1", "p2"],
|
severity_filter=["p0", "p1", "p2"],
|
||||||
)
|
)
|
||||||
self.log(f"Created DingTalk channel: {channel2.name} (ID: {channel2.id})")
|
self.log(f"Created DingTalk channel: {channel2.name} (ID: {channel2.id})")
|
||||||
|
|
||||||
# 创建 Slack 告警渠道
|
# 创建 Slack 告警渠道
|
||||||
channel3 = self.manager.create_alert_channel(
|
channel3 = self.manager.create_alert_channel(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "Slack 告警",
|
name="Slack 告警",
|
||||||
channel_type = AlertChannelType.SLACK,
|
channel_type=AlertChannelType.SLACK,
|
||||||
config = {"webhook_url": "https://hooks.slack.com/services/test"},
|
config={"webhook_url": "https://hooks.slack.com/services/test"},
|
||||||
severity_filter = ["p0", "p1", "p2", "p3"],
|
severity_filter=["p0", "p1", "p2", "p3"],
|
||||||
)
|
)
|
||||||
self.log(f"Created Slack channel: {channel3.name} (ID: {channel3.id})")
|
self.log(f"Created Slack channel: {channel3.name} (ID: {channel3.id})")
|
||||||
|
|
||||||
# 获取告警渠道
|
# 获取告警渠道
|
||||||
fetched_channel = self.manager.get_alert_channel(channel1.id)
|
fetched_channel = self.manager.get_alert_channel(channel1.id)
|
||||||
assert fetched_channel is not None
|
assert fetched_channel is not None
|
||||||
assert fetched_channel.name == channel1.name
|
assert fetched_channel.name == channel1.name
|
||||||
self.log(f"Fetched alert channel: {fetched_channel.name}")
|
self.log(f"Fetched alert channel: {fetched_channel.name}")
|
||||||
|
|
||||||
# 列出租户的所有告警渠道
|
# 列出租户的所有告警渠道
|
||||||
channels = self.manager.list_alert_channels(self.tenant_id)
|
channels = self.manager.list_alert_channels(self.tenant_id)
|
||||||
assert len(channels) >= 3
|
assert len(channels) >= 3
|
||||||
self.log(f"Listed {len(channels)} alert channels for tenant")
|
self.log(f"Listed {len(channels)} alert channels for tenant")
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class TestOpsManager:
|
|||||||
self.log("Deleted test alert channels")
|
self.log("Deleted test alert channels")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Alert channels test failed: {e}", success = False)
|
self.log(f"Alert channels test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_alerts(self) -> None:
|
def test_alerts(self) -> None:
|
||||||
"""测试告警管理"""
|
"""测试告警管理"""
|
||||||
@@ -211,61 +211,61 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建告警规则
|
# 创建告警规则
|
||||||
rule = self.manager.create_alert_rule(
|
rule = self.manager.create_alert_rule(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "测试告警规则",
|
name="测试告警规则",
|
||||||
description = "用于测试的告警规则",
|
description="用于测试的告警规则",
|
||||||
rule_type = AlertRuleType.THRESHOLD,
|
rule_type=AlertRuleType.THRESHOLD,
|
||||||
severity = AlertSeverity.P1,
|
severity=AlertSeverity.P1,
|
||||||
metric = "test_metric",
|
metric="test_metric",
|
||||||
condition = ">",
|
condition=">",
|
||||||
threshold = 100.0,
|
threshold=100.0,
|
||||||
duration = 60,
|
duration=60,
|
||||||
evaluation_interval = 60,
|
evaluation_interval=60,
|
||||||
channels = [],
|
channels=[],
|
||||||
labels = {},
|
labels={},
|
||||||
annotations = {},
|
annotations={},
|
||||||
created_by = "test_user",
|
created_by="test_user",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 记录资源指标
|
# 记录资源指标
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
self.manager.record_resource_metric(
|
self.manager.record_resource_metric(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
resource_type = ResourceType.CPU,
|
resource_type=ResourceType.CPU,
|
||||||
resource_id = "server-001",
|
resource_id="server-001",
|
||||||
metric_name = "test_metric",
|
metric_name="test_metric",
|
||||||
metric_value = 110.0 + i,
|
metric_value=110.0 + i,
|
||||||
unit = "percent",
|
unit="percent",
|
||||||
metadata = {"region": "cn-north-1"},
|
metadata={"region": "cn-north-1"},
|
||||||
)
|
)
|
||||||
self.log("Recorded 10 resource metrics")
|
self.log("Recorded 10 resource metrics")
|
||||||
|
|
||||||
# 手动创建告警
|
# 手动创建告警
|
||||||
from ops_manager import Alert
|
from ops_manager import Alert
|
||||||
|
|
||||||
alert_id = f"test_alert_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
alert_id = f"test_alert_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
alert = Alert(
|
alert = Alert(
|
||||||
id = alert_id,
|
id=alert_id,
|
||||||
rule_id = rule.id,
|
rule_id=rule.id,
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
severity = AlertSeverity.P1,
|
severity=AlertSeverity.P1,
|
||||||
status = AlertStatus.FIRING,
|
status=AlertStatus.FIRING,
|
||||||
title = "测试告警",
|
title="测试告警",
|
||||||
description = "这是一条测试告警",
|
description="这是一条测试告警",
|
||||||
metric = "test_metric",
|
metric="test_metric",
|
||||||
value = 120.0,
|
value=120.0,
|
||||||
threshold = 100.0,
|
threshold=100.0,
|
||||||
labels = {"test": "true"},
|
labels={"test": "true"},
|
||||||
annotations = {},
|
annotations={},
|
||||||
started_at = now,
|
started_at=now,
|
||||||
resolved_at = None,
|
resolved_at=None,
|
||||||
acknowledged_by = None,
|
acknowledged_by=None,
|
||||||
acknowledged_at = None,
|
acknowledged_at=None,
|
||||||
notification_sent = {},
|
notification_sent={},
|
||||||
suppression_count = 0,
|
suppression_count=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.manager._get_db() as conn:
|
with self.manager._get_db() as conn:
|
||||||
@@ -299,20 +299,20 @@ class TestOpsManager:
|
|||||||
self.log(f"Created test alert: {alert.id}")
|
self.log(f"Created test alert: {alert.id}")
|
||||||
|
|
||||||
# 列出租户的告警
|
# 列出租户的告警
|
||||||
alerts = self.manager.list_alerts(self.tenant_id)
|
alerts = self.manager.list_alerts(self.tenant_id)
|
||||||
assert len(alerts) >= 1
|
assert len(alerts) >= 1
|
||||||
self.log(f"Listed {len(alerts)} alerts for tenant")
|
self.log(f"Listed {len(alerts)} alerts for tenant")
|
||||||
|
|
||||||
# 确认告警
|
# 确认告警
|
||||||
self.manager.acknowledge_alert(alert_id, "test_user")
|
self.manager.acknowledge_alert(alert_id, "test_user")
|
||||||
fetched_alert = self.manager.get_alert(alert_id)
|
fetched_alert = self.manager.get_alert(alert_id)
|
||||||
assert fetched_alert.status == AlertStatus.ACKNOWLEDGED
|
assert fetched_alert.status == AlertStatus.ACKNOWLEDGED
|
||||||
assert fetched_alert.acknowledged_by == "test_user"
|
assert fetched_alert.acknowledged_by == "test_user"
|
||||||
self.log(f"Acknowledged alert: {alert_id}")
|
self.log(f"Acknowledged alert: {alert_id}")
|
||||||
|
|
||||||
# 解决告警
|
# 解决告警
|
||||||
self.manager.resolve_alert(alert_id)
|
self.manager.resolve_alert(alert_id)
|
||||||
fetched_alert = self.manager.get_alert(alert_id)
|
fetched_alert = self.manager.get_alert(alert_id)
|
||||||
assert fetched_alert.status == AlertStatus.RESOLVED
|
assert fetched_alert.status == AlertStatus.RESOLVED
|
||||||
assert fetched_alert.resolved_at is not None
|
assert fetched_alert.resolved_at is not None
|
||||||
self.log(f"Resolved alert: {alert_id}")
|
self.log(f"Resolved alert: {alert_id}")
|
||||||
@@ -326,7 +326,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up test data")
|
self.log("Cleaned up test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Alerts test failed: {e}", success = False)
|
self.log(f"Alerts test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_capacity_planning(self) -> None:
|
def test_capacity_planning(self) -> None:
|
||||||
"""测试容量规划"""
|
"""测试容量规划"""
|
||||||
@@ -334,9 +334,9 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 记录历史指标数据
|
# 记录历史指标数据
|
||||||
base_time = datetime.now() - timedelta(days = 30)
|
base_time = datetime.now() - timedelta(days=30)
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
timestamp = (base_time + timedelta(days = i)).isoformat()
|
timestamp = (base_time + timedelta(days=i)).isoformat()
|
||||||
with self.manager._get_db() as conn:
|
with self.manager._get_db() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -360,13 +360,13 @@ class TestOpsManager:
|
|||||||
self.log("Recorded 30 days of historical metrics")
|
self.log("Recorded 30 days of historical metrics")
|
||||||
|
|
||||||
# 创建容量规划
|
# 创建容量规划
|
||||||
prediction_date = (datetime.now() + timedelta(days = 30)).strftime("%Y-%m-%d")
|
prediction_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
plan = self.manager.create_capacity_plan(
|
plan = self.manager.create_capacity_plan(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
resource_type = ResourceType.CPU,
|
resource_type=ResourceType.CPU,
|
||||||
current_capacity = 100.0,
|
current_capacity=100.0,
|
||||||
prediction_date = prediction_date,
|
prediction_date=prediction_date,
|
||||||
confidence = 0.85,
|
confidence=0.85,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"Created capacity plan: {plan.id}")
|
self.log(f"Created capacity plan: {plan.id}")
|
||||||
@@ -375,7 +375,7 @@ class TestOpsManager:
|
|||||||
self.log(f" Recommended action: {plan.recommended_action}")
|
self.log(f" Recommended action: {plan.recommended_action}")
|
||||||
|
|
||||||
# 获取容量规划列表
|
# 获取容量规划列表
|
||||||
plans = self.manager.get_capacity_plans(self.tenant_id)
|
plans = self.manager.get_capacity_plans(self.tenant_id)
|
||||||
assert len(plans) >= 1
|
assert len(plans) >= 1
|
||||||
self.log(f"Listed {len(plans)} capacity plans")
|
self.log(f"Listed {len(plans)} capacity plans")
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up capacity planning test data")
|
self.log("Cleaned up capacity planning test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Capacity planning test failed: {e}", success = False)
|
self.log(f"Capacity planning test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_auto_scaling(self) -> None:
|
def test_auto_scaling(self) -> None:
|
||||||
"""测试自动扩缩容"""
|
"""测试自动扩缩容"""
|
||||||
@@ -395,18 +395,18 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建自动扩缩容策略
|
# 创建自动扩缩容策略
|
||||||
policy = self.manager.create_auto_scaling_policy(
|
policy = self.manager.create_auto_scaling_policy(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "API 服务自动扩缩容",
|
name="API 服务自动扩缩容",
|
||||||
resource_type = ResourceType.CPU,
|
resource_type=ResourceType.CPU,
|
||||||
min_instances = 2,
|
min_instances=2,
|
||||||
max_instances = 10,
|
max_instances=10,
|
||||||
target_utilization = 0.7,
|
target_utilization=0.7,
|
||||||
scale_up_threshold = 0.8,
|
scale_up_threshold=0.8,
|
||||||
scale_down_threshold = 0.3,
|
scale_down_threshold=0.3,
|
||||||
scale_up_step = 2,
|
scale_up_step=2,
|
||||||
scale_down_step = 1,
|
scale_down_step=1,
|
||||||
cooldown_period = 300,
|
cooldown_period=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"Created auto scaling policy: {policy.name} (ID: {policy.id})")
|
self.log(f"Created auto scaling policy: {policy.name} (ID: {policy.id})")
|
||||||
@@ -415,13 +415,13 @@ class TestOpsManager:
|
|||||||
self.log(f" Target utilization: {policy.target_utilization}")
|
self.log(f" Target utilization: {policy.target_utilization}")
|
||||||
|
|
||||||
# 获取策略列表
|
# 获取策略列表
|
||||||
policies = self.manager.list_auto_scaling_policies(self.tenant_id)
|
policies = self.manager.list_auto_scaling_policies(self.tenant_id)
|
||||||
assert len(policies) >= 1
|
assert len(policies) >= 1
|
||||||
self.log(f"Listed {len(policies)} auto scaling policies")
|
self.log(f"Listed {len(policies)} auto scaling policies")
|
||||||
|
|
||||||
# 模拟扩缩容评估
|
# 模拟扩缩容评估
|
||||||
event = self.manager.evaluate_scaling_policy(
|
event = self.manager.evaluate_scaling_policy(
|
||||||
policy_id = policy.id, current_instances = 3, current_utilization = 0.85
|
policy_id=policy.id, current_instances=3, current_utilization=0.85
|
||||||
)
|
)
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
@@ -432,7 +432,7 @@ class TestOpsManager:
|
|||||||
self.log("No scaling action needed")
|
self.log("No scaling action needed")
|
||||||
|
|
||||||
# 获取扩缩容事件列表
|
# 获取扩缩容事件列表
|
||||||
events = self.manager.list_scaling_events(self.tenant_id)
|
events = self.manager.list_scaling_events(self.tenant_id)
|
||||||
self.log(f"Listed {len(events)} scaling events")
|
self.log(f"Listed {len(events)} scaling events")
|
||||||
|
|
||||||
# 清理
|
# 清理
|
||||||
@@ -445,7 +445,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up auto scaling test data")
|
self.log("Cleaned up auto scaling test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Auto scaling test failed: {e}", success = False)
|
self.log(f"Auto scaling test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_health_checks(self) -> None:
|
def test_health_checks(self) -> None:
|
||||||
"""测试健康检查"""
|
"""测试健康检查"""
|
||||||
@@ -453,41 +453,41 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建 HTTP 健康检查
|
# 创建 HTTP 健康检查
|
||||||
check1 = self.manager.create_health_check(
|
check1 = self.manager.create_health_check(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "API 服务健康检查",
|
name="API 服务健康检查",
|
||||||
target_type = "service",
|
target_type="service",
|
||||||
target_id = "api-service",
|
target_id="api-service",
|
||||||
check_type = "http",
|
check_type="http",
|
||||||
check_config = {"url": "https://api.insightflow.io/health", "expected_status": 200},
|
check_config={"url": "https://api.insightflow.io/health", "expected_status": 200},
|
||||||
interval = 60,
|
interval=60,
|
||||||
timeout = 10,
|
timeout=10,
|
||||||
retry_count = 3,
|
retry_count=3,
|
||||||
)
|
)
|
||||||
self.log(f"Created HTTP health check: {check1.name} (ID: {check1.id})")
|
self.log(f"Created HTTP health check: {check1.name} (ID: {check1.id})")
|
||||||
|
|
||||||
# 创建 TCP 健康检查
|
# 创建 TCP 健康检查
|
||||||
check2 = self.manager.create_health_check(
|
check2 = self.manager.create_health_check(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "数据库健康检查",
|
name="数据库健康检查",
|
||||||
target_type = "database",
|
target_type="database",
|
||||||
target_id = "postgres-001",
|
target_id="postgres-001",
|
||||||
check_type = "tcp",
|
check_type="tcp",
|
||||||
check_config = {"host": "db.insightflow.io", "port": 5432},
|
check_config={"host": "db.insightflow.io", "port": 5432},
|
||||||
interval = 30,
|
interval=30,
|
||||||
timeout = 5,
|
timeout=5,
|
||||||
retry_count = 2,
|
retry_count=2,
|
||||||
)
|
)
|
||||||
self.log(f"Created TCP health check: {check2.name} (ID: {check2.id})")
|
self.log(f"Created TCP health check: {check2.name} (ID: {check2.id})")
|
||||||
|
|
||||||
# 获取健康检查列表
|
# 获取健康检查列表
|
||||||
checks = self.manager.list_health_checks(self.tenant_id)
|
checks = self.manager.list_health_checks(self.tenant_id)
|
||||||
assert len(checks) >= 2
|
assert len(checks) >= 2
|
||||||
self.log(f"Listed {len(checks)} health checks")
|
self.log(f"Listed {len(checks)} health checks")
|
||||||
|
|
||||||
# 执行健康检查(异步)
|
# 执行健康检查(异步)
|
||||||
async def run_health_check() -> None:
|
async def run_health_check() -> None:
|
||||||
result = await self.manager.execute_health_check(check1.id)
|
result = await self.manager.execute_health_check(check1.id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 由于健康检查需要网络,这里只验证方法存在
|
# 由于健康检查需要网络,这里只验证方法存在
|
||||||
@@ -500,7 +500,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up health check test data")
|
self.log("Cleaned up health check test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Health checks test failed: {e}", success = False)
|
self.log(f"Health checks test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_failover(self) -> None:
|
def test_failover(self) -> None:
|
||||||
"""测试故障转移"""
|
"""测试故障转移"""
|
||||||
@@ -508,15 +508,15 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建故障转移配置
|
# 创建故障转移配置
|
||||||
config = self.manager.create_failover_config(
|
config = self.manager.create_failover_config(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "主备数据中心故障转移",
|
name="主备数据中心故障转移",
|
||||||
primary_region = "cn-north-1",
|
primary_region="cn-north-1",
|
||||||
secondary_regions = ["cn-south-1", "cn-east-1"],
|
secondary_regions=["cn-south-1", "cn-east-1"],
|
||||||
failover_trigger = "health_check_failed",
|
failover_trigger="health_check_failed",
|
||||||
auto_failover = False,
|
auto_failover=False,
|
||||||
failover_timeout = 300,
|
failover_timeout=300,
|
||||||
health_check_id = None,
|
health_check_id=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"Created failover config: {config.name} (ID: {config.id})")
|
self.log(f"Created failover config: {config.name} (ID: {config.id})")
|
||||||
@@ -524,13 +524,13 @@ class TestOpsManager:
|
|||||||
self.log(f" Secondary regions: {config.secondary_regions}")
|
self.log(f" Secondary regions: {config.secondary_regions}")
|
||||||
|
|
||||||
# 获取故障转移配置列表
|
# 获取故障转移配置列表
|
||||||
configs = self.manager.list_failover_configs(self.tenant_id)
|
configs = self.manager.list_failover_configs(self.tenant_id)
|
||||||
assert len(configs) >= 1
|
assert len(configs) >= 1
|
||||||
self.log(f"Listed {len(configs)} failover configs")
|
self.log(f"Listed {len(configs)} failover configs")
|
||||||
|
|
||||||
# 发起故障转移
|
# 发起故障转移
|
||||||
event = self.manager.initiate_failover(
|
event = self.manager.initiate_failover(
|
||||||
config_id = config.id, reason = "Primary region health check failed"
|
config_id=config.id, reason="Primary region health check failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
@@ -540,12 +540,12 @@ class TestOpsManager:
|
|||||||
|
|
||||||
# 更新故障转移状态
|
# 更新故障转移状态
|
||||||
self.manager.update_failover_status(event.id, "completed")
|
self.manager.update_failover_status(event.id, "completed")
|
||||||
updated_event = self.manager.get_failover_event(event.id)
|
updated_event = self.manager.get_failover_event(event.id)
|
||||||
assert updated_event.status == "completed"
|
assert updated_event.status == "completed"
|
||||||
self.log("Failover completed")
|
self.log("Failover completed")
|
||||||
|
|
||||||
# 获取故障转移事件列表
|
# 获取故障转移事件列表
|
||||||
events = self.manager.list_failover_events(self.tenant_id)
|
events = self.manager.list_failover_events(self.tenant_id)
|
||||||
self.log(f"Listed {len(events)} failover events")
|
self.log(f"Listed {len(events)} failover events")
|
||||||
|
|
||||||
# 清理
|
# 清理
|
||||||
@@ -556,7 +556,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up failover test data")
|
self.log("Cleaned up failover test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Failover test failed: {e}", success = False)
|
self.log(f"Failover test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_backup(self) -> None:
|
def test_backup(self) -> None:
|
||||||
"""测试备份与恢复"""
|
"""测试备份与恢复"""
|
||||||
@@ -564,17 +564,17 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 创建备份任务
|
# 创建备份任务
|
||||||
job = self.manager.create_backup_job(
|
job = self.manager.create_backup_job(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
name = "每日数据库备份",
|
name="每日数据库备份",
|
||||||
backup_type = "full",
|
backup_type="full",
|
||||||
target_type = "database",
|
target_type="database",
|
||||||
target_id = "postgres-main",
|
target_id="postgres-main",
|
||||||
schedule = "0 2 * * *", # 每天凌晨2点
|
schedule="0 2 * * *", # 每天凌晨2点
|
||||||
retention_days = 30,
|
retention_days=30,
|
||||||
encryption_enabled = True,
|
encryption_enabled=True,
|
||||||
compression_enabled = True,
|
compression_enabled=True,
|
||||||
storage_location = "s3://insightflow-backups/",
|
storage_location="s3://insightflow-backups/",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"Created backup job: {job.name} (ID: {job.id})")
|
self.log(f"Created backup job: {job.name} (ID: {job.id})")
|
||||||
@@ -582,12 +582,12 @@ class TestOpsManager:
|
|||||||
self.log(f" Retention: {job.retention_days} days")
|
self.log(f" Retention: {job.retention_days} days")
|
||||||
|
|
||||||
# 获取备份任务列表
|
# 获取备份任务列表
|
||||||
jobs = self.manager.list_backup_jobs(self.tenant_id)
|
jobs = self.manager.list_backup_jobs(self.tenant_id)
|
||||||
assert len(jobs) >= 1
|
assert len(jobs) >= 1
|
||||||
self.log(f"Listed {len(jobs)} backup jobs")
|
self.log(f"Listed {len(jobs)} backup jobs")
|
||||||
|
|
||||||
# 执行备份
|
# 执行备份
|
||||||
record = self.manager.execute_backup(job.id)
|
record = self.manager.execute_backup(job.id)
|
||||||
|
|
||||||
if record:
|
if record:
|
||||||
self.log(f"Executed backup: {record.id}")
|
self.log(f"Executed backup: {record.id}")
|
||||||
@@ -595,11 +595,11 @@ class TestOpsManager:
|
|||||||
self.log(f" Storage: {record.storage_path}")
|
self.log(f" Storage: {record.storage_path}")
|
||||||
|
|
||||||
# 获取备份记录列表
|
# 获取备份记录列表
|
||||||
records = self.manager.list_backup_records(self.tenant_id)
|
records = self.manager.list_backup_records(self.tenant_id)
|
||||||
self.log(f"Listed {len(records)} backup records")
|
self.log(f"Listed {len(records)} backup records")
|
||||||
|
|
||||||
# 测试恢复(模拟)
|
# 测试恢复(模拟)
|
||||||
restore_result = self.manager.restore_from_backup(record.id)
|
restore_result = self.manager.restore_from_backup(record.id)
|
||||||
self.log(f"Restore test result: {restore_result}")
|
self.log(f"Restore test result: {restore_result}")
|
||||||
|
|
||||||
# 清理
|
# 清理
|
||||||
@@ -610,7 +610,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up backup test data")
|
self.log("Cleaned up backup test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Backup test failed: {e}", success = False)
|
self.log(f"Backup test failed: {e}", success=False)
|
||||||
|
|
||||||
def test_cost_optimization(self) -> None:
|
def test_cost_optimization(self) -> None:
|
||||||
"""测试成本优化"""
|
"""测试成本优化"""
|
||||||
@@ -618,27 +618,27 @@ class TestOpsManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 记录资源利用率数据
|
# 记录资源利用率数据
|
||||||
report_date = datetime.now().strftime("%Y-%m-%d")
|
report_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
self.manager.record_resource_utilization(
|
self.manager.record_resource_utilization(
|
||||||
tenant_id = self.tenant_id,
|
tenant_id=self.tenant_id,
|
||||||
resource_type = ResourceType.CPU,
|
resource_type=ResourceType.CPU,
|
||||||
resource_id = f"server-{i:03d}",
|
resource_id=f"server-{i:03d}",
|
||||||
utilization_rate = 0.05 + random.random() * 0.1, # 低利用率
|
utilization_rate=0.05 + random.random() * 0.1, # 低利用率
|
||||||
peak_utilization = 0.15,
|
peak_utilization=0.15,
|
||||||
avg_utilization = 0.08,
|
avg_utilization=0.08,
|
||||||
idle_time_percent = 0.85,
|
idle_time_percent=0.85,
|
||||||
report_date = report_date,
|
report_date=report_date,
|
||||||
recommendations = ["Consider downsizing this resource"],
|
recommendations=["Consider downsizing this resource"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log("Recorded 5 resource utilization records")
|
self.log("Recorded 5 resource utilization records")
|
||||||
|
|
||||||
# 生成成本报告
|
# 生成成本报告
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
report = self.manager.generate_cost_report(
|
report = self.manager.generate_cost_report(
|
||||||
tenant_id = self.tenant_id, year = now.year, month = now.month
|
tenant_id=self.tenant_id, year=now.year, month=now.month
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(f"Generated cost report: {report.id}")
|
self.log(f"Generated cost report: {report.id}")
|
||||||
@@ -647,11 +647,11 @@ class TestOpsManager:
|
|||||||
self.log(f" Anomalies detected: {len(report.anomalies)}")
|
self.log(f" Anomalies detected: {len(report.anomalies)}")
|
||||||
|
|
||||||
# 检测闲置资源
|
# 检测闲置资源
|
||||||
idle_resources = self.manager.detect_idle_resources(self.tenant_id)
|
idle_resources = self.manager.detect_idle_resources(self.tenant_id)
|
||||||
self.log(f"Detected {len(idle_resources)} idle resources")
|
self.log(f"Detected {len(idle_resources)} idle resources")
|
||||||
|
|
||||||
# 获取闲置资源列表
|
# 获取闲置资源列表
|
||||||
idle_list = self.manager.get_idle_resources(self.tenant_id)
|
idle_list = self.manager.get_idle_resources(self.tenant_id)
|
||||||
for resource in idle_list:
|
for resource in idle_list:
|
||||||
self.log(
|
self.log(
|
||||||
f" Idle resource: {resource.resource_name} (est. cost: {
|
f" Idle resource: {resource.resource_name} (est. cost: {
|
||||||
@@ -660,7 +660,7 @@ class TestOpsManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 生成成本优化建议
|
# 生成成本优化建议
|
||||||
suggestions = self.manager.generate_cost_optimization_suggestions(self.tenant_id)
|
suggestions = self.manager.generate_cost_optimization_suggestions(self.tenant_id)
|
||||||
self.log(f"Generated {len(suggestions)} cost optimization suggestions")
|
self.log(f"Generated {len(suggestions)} cost optimization suggestions")
|
||||||
|
|
||||||
for suggestion in suggestions:
|
for suggestion in suggestions:
|
||||||
@@ -672,12 +672,12 @@ class TestOpsManager:
|
|||||||
self.log(f" Difficulty: {suggestion.difficulty}")
|
self.log(f" Difficulty: {suggestion.difficulty}")
|
||||||
|
|
||||||
# 获取优化建议列表
|
# 获取优化建议列表
|
||||||
all_suggestions = self.manager.get_cost_optimization_suggestions(self.tenant_id)
|
all_suggestions = self.manager.get_cost_optimization_suggestions(self.tenant_id)
|
||||||
self.log(f"Listed {len(all_suggestions)} optimization suggestions")
|
self.log(f"Listed {len(all_suggestions)} optimization suggestions")
|
||||||
|
|
||||||
# 应用优化建议
|
# 应用优化建议
|
||||||
if all_suggestions:
|
if all_suggestions:
|
||||||
applied = self.manager.apply_cost_optimization_suggestion(all_suggestions[0].id)
|
applied = self.manager.apply_cost_optimization_suggestion(all_suggestions[0].id)
|
||||||
if applied:
|
if applied:
|
||||||
self.log(f"Applied optimization suggestion: {applied.title}")
|
self.log(f"Applied optimization suggestion: {applied.title}")
|
||||||
assert applied.is_applied
|
assert applied.is_applied
|
||||||
@@ -698,7 +698,7 @@ class TestOpsManager:
|
|||||||
self.log("Cleaned up cost optimization test data")
|
self.log("Cleaned up cost optimization test data")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Cost optimization test failed: {e}", success = False)
|
self.log(f"Cost optimization test failed: {e}", success=False)
|
||||||
|
|
||||||
def print_summary(self) -> None:
|
def print_summary(self) -> None:
|
||||||
"""打印测试总结"""
|
"""打印测试总结"""
|
||||||
@@ -706,9 +706,9 @@ class TestOpsManager:
|
|||||||
print("Test Summary")
|
print("Test Summary")
|
||||||
print(" = " * 60)
|
print(" = " * 60)
|
||||||
|
|
||||||
total = len(self.test_results)
|
total = len(self.test_results)
|
||||||
passed = sum(1 for _, success in self.test_results if success)
|
passed = sum(1 for _, success in self.test_results if success)
|
||||||
failed = total - passed
|
failed = total - passed
|
||||||
|
|
||||||
print(f"Total tests: {total}")
|
print(f"Total tests: {total}")
|
||||||
print(f"Passed: {passed} ✅")
|
print(f"Passed: {passed} ✅")
|
||||||
@@ -725,7 +725,7 @@ class TestOpsManager:
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
test = TestOpsManager()
|
test = TestOpsManager()
|
||||||
test.run_all_tests()
|
test.run_all_tests()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+34
-31
@@ -11,18 +11,18 @@ from typing import Any
|
|||||||
|
|
||||||
class TingwuClient:
|
class TingwuClient:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.access_key = os.getenv("ALI_ACCESS_KEY", "")
|
self.access_key = os.getenv("ALI_ACCESS_KEY", "")
|
||||||
self.secret_key = os.getenv("ALI_SECRET_KEY", "")
|
self.secret_key = os.getenv("ALI_SECRET_KEY", "")
|
||||||
self.endpoint = "https://tingwu.cn-beijing.aliyuncs.com"
|
self.endpoint = "https://tingwu.cn-beijing.aliyuncs.com"
|
||||||
|
|
||||||
if not self.access_key or not self.secret_key:
|
if not self.access_key or not self.secret_key:
|
||||||
raise ValueError("ALI_ACCESS_KEY and ALI_SECRET_KEY required")
|
raise ValueError("ALI_ACCESS_KEY and ALI_SECRET_KEY required")
|
||||||
|
|
||||||
def _sign_request(
|
def _sign_request(
|
||||||
self, method: str, uri: str, query: str = "", body: str = ""
|
self, method: str, uri: str, query: str = "", body: str = ""
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""阿里云签名 V3"""
|
"""阿里云签名 V3"""
|
||||||
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
# 简化签名,实际生产需要完整实现
|
# 简化签名,实际生产需要完整实现
|
||||||
# 这里使用基础认证头
|
# 这里使用基础认证头
|
||||||
@@ -34,7 +34,7 @@ class TingwuClient:
|
|||||||
"Authorization": f"ACS3-HMAC-SHA256 Credential = {self.access_key}/acs/tingwu/cn-beijing",
|
"Authorization": f"ACS3-HMAC-SHA256 Credential = {self.access_key}/acs/tingwu/cn-beijing",
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_task(self, audio_url: str, language: str = "zh") -> str:
|
def create_task(self, audio_url: str, language: str = "zh") -> str:
|
||||||
"""创建听悟任务"""
|
"""创建听悟任务"""
|
||||||
try:
|
try:
|
||||||
# 导入移到文件顶部会导致循环导入,保持在这里
|
# 导入移到文件顶部会导致循环导入,保持在这里
|
||||||
@@ -42,23 +42,23 @@ class TingwuClient:
|
|||||||
from alibabacloud_tingwu20230930 import models as tingwu_models
|
from alibabacloud_tingwu20230930 import models as tingwu_models
|
||||||
from alibabacloud_tingwu20230930.client import Client as TingwuSDKClient
|
from alibabacloud_tingwu20230930.client import Client as TingwuSDKClient
|
||||||
|
|
||||||
config = open_api_models.Config(
|
config = open_api_models.Config(
|
||||||
access_key_id = self.access_key, access_key_secret = self.secret_key
|
access_key_id=self.access_key, access_key_secret=self.secret_key
|
||||||
)
|
)
|
||||||
config.endpoint = "tingwu.cn-beijing.aliyuncs.com"
|
config.endpoint = "tingwu.cn-beijing.aliyuncs.com"
|
||||||
client = TingwuSDKClient(config)
|
client = TingwuSDKClient(config)
|
||||||
|
|
||||||
request = tingwu_models.CreateTaskRequest(
|
request = tingwu_models.CreateTaskRequest(
|
||||||
type = "offline",
|
type="offline",
|
||||||
input = tingwu_models.Input(source = "OSS", file_url = audio_url),
|
input=tingwu_models.Input(source="OSS", file_url=audio_url),
|
||||||
parameters = tingwu_models.Parameters(
|
parameters=tingwu_models.Parameters(
|
||||||
transcription = tingwu_models.Transcription(
|
transcription=tingwu_models.Transcription(
|
||||||
diarization_enabled = True, sentence_max_length = 20
|
diarization_enabled=True, sentence_max_length=20
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.create_task(request)
|
response = client.create_task(request)
|
||||||
if response.body.code == "0":
|
if response.body.code == "0":
|
||||||
return response.body.data.task_id
|
return response.body.data.task_id
|
||||||
else:
|
else:
|
||||||
@@ -73,26 +73,29 @@ class TingwuClient:
|
|||||||
return f"mock_task_{int(time.time())}"
|
return f"mock_task_{int(time.time())}"
|
||||||
|
|
||||||
def get_task_result(
|
def get_task_result(
|
||||||
self, task_id: str, max_retries: int = 60, interval: int = 5
|
self, task_id: str, max_retries: int = 60, interval: int = 5
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取任务结果"""
|
"""获取任务结果"""
|
||||||
try:
|
try:
|
||||||
# 导入移到文件顶部会导致循环导入,保持在这里
|
# 导入移到文件顶部会导致循环导入,保持在这里
|
||||||
|
from alibabacloud_tingwu20230930 import models as tingwu_models
|
||||||
|
from alibabacloud_tingwu20230930.client import Client as TingwuSDKClient
|
||||||
|
from alibabacloud_openapi_util import models as open_api_models
|
||||||
|
|
||||||
config = open_api_models.Config(
|
config = open_api_models.Config(
|
||||||
access_key_id = self.access_key, access_key_secret = self.secret_key
|
access_key_id=self.access_key, access_key_secret=self.secret_key
|
||||||
)
|
)
|
||||||
config.endpoint = "tingwu.cn-beijing.aliyuncs.com"
|
config.endpoint = "tingwu.cn-beijing.aliyuncs.com"
|
||||||
client = TingwuSDKClient(config)
|
client = TingwuSDKClient(config)
|
||||||
|
|
||||||
for i in range(max_retries):
|
for i in range(max_retries):
|
||||||
request = tingwu_models.GetTaskInfoRequest()
|
request = tingwu_models.GetTaskInfoRequest()
|
||||||
response = client.get_task_info(task_id, request)
|
response = client.get_task_info(task_id, request)
|
||||||
|
|
||||||
if response.body.code != "0":
|
if response.body.code != "0":
|
||||||
raise RuntimeError(f"Query failed: {response.body.message}")
|
raise RuntimeError(f"Query failed: {response.body.message}")
|
||||||
|
|
||||||
status = response.body.data.task_status
|
status = response.body.data.task_status
|
||||||
|
|
||||||
if status == "SUCCESS":
|
if status == "SUCCESS":
|
||||||
return self._parse_result(response.body.data)
|
return self._parse_result(response.body.data)
|
||||||
@@ -113,11 +116,11 @@ class TingwuClient:
|
|||||||
|
|
||||||
def _parse_result(self, data) -> dict[str, Any]:
|
def _parse_result(self, data) -> dict[str, Any]:
|
||||||
"""解析结果"""
|
"""解析结果"""
|
||||||
result = data.result
|
result = data.result
|
||||||
transcription = result.transcription
|
transcription = result.transcription
|
||||||
|
|
||||||
full_text = ""
|
full_text = ""
|
||||||
segments = []
|
segments = []
|
||||||
|
|
||||||
if transcription.paragraphs:
|
if transcription.paragraphs:
|
||||||
for para in transcription.paragraphs:
|
for para in transcription.paragraphs:
|
||||||
@@ -150,8 +153,8 @@ class TingwuClient:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def transcribe(self, audio_url: str, language: str = "zh") -> dict[str, Any]:
|
def transcribe(self, audio_url: str, language: str = "zh") -> dict[str, Any]:
|
||||||
"""一键转录"""
|
"""一键转录"""
|
||||||
task_id = self.create_task(audio_url, language)
|
task_id = self.create_task(audio_url, language)
|
||||||
print(f"Tingwu task: {task_id}")
|
print(f"Tingwu task: {task_id}")
|
||||||
return self.get_task_result(task_id)
|
return self.get_task_result(task_id)
|
||||||
|
|||||||
+339
-340
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user