Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/17547916945_personalknowledgemanagementsystem.json

1 line
1.7 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "17547916945/personalknowledgemanagementsystem", "files": {"app.py": {"129d1dc95af3908ad0ccea240938abcab4396b42": {"t": 1785745984.2198927, "n": 150627, "h": "8f303754c72b652f", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识节点表添加mastery_level字段(掌握等级:0=待学习, 1=学习中, 2=已掌握)\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN mastery_level INTEGER DEFAULT 0')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识点学习记录表 - 记录用户对每个知识点的学习行为\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_learning (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n total_study_time INTEGER DEFAULT 0, -- 累计学习时长(秒)\n study_count INTEGER DEFAULT 0, -- 学习次数\n last_study_time TIMESTAMP, -- 最后学习时间\n test_score REAL, -- 最近测试分数(0-100)\n test_count INTEGER DEFAULT 0, -- 测试次数\n last_test_time TIMESTAMP, -- 最后测试时间\n mastery_level INTEGER DEFAULT 0, -- 掌握等级:0=待学习, 1=学习中, 2=已掌握\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id),\n UNIQUE(user_id, knowledge_node_id)\n )\n ''')\n \n # 知识点学习历史表 - 记录每次学习的详细信息\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_study_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n action_type TEXT NOT NULL, -- study(学习)、test(测试)\n duration INTEGER, -- 本次学习时长(秒)\n score REAL, -- 本次测试分数(仅测试时有值)\n details TEXT, -- JSON格式存储详细信息\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户手动管理的知识点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_manual_knowledge_points (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n name TEXT NOT NULL, -- 知识点名称\n mastery_level INTEGER NOT NULL, -- 0=待学习, 1=学习中, 2=已掌握\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id)\n )\n ''')\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n # 用户行为记录工具方法\n def log_user_behavior(self, user_id: int, action_type: str, document_id: Optional[int] = None,\n duration: Optional[int] = None, details: Optional[Dict[str, Any]] = None) -> None:\n \"\"\"\n 记录用户行为到 user_behaviors 表中\n duration 单位为秒;details 为可选的 JSON 信息\n \"\"\"\n try:\n with self.get_connection() as conn:\n conn.execute(\n '''\n INSERT INTO user_behaviors (user_id, document_id, action_type, duration, details)\n VALUES (?, ?, ?, ?, ?)\n ''',\n (\n user_id,\n document_id,\n action_type,\n duration,\n json.dumps(details, ensure_ascii=False) if details else None\n )\n )\n conn.commit()\n except Exception as e:\n # 打印错误但不影响主流程\n print(f\"记录用户行为失败: {e}\")\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 在保证精确度的前提下适当放宽召回\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配(稍微放宽阈值)\n if query_lower in content:\n # 计算出现次数,要求至少出现1次即可计分\n count = content.count(query_lower)\n if count >= 1:\n score += min(count * 1.2, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数(略微降低阈值,提升召回)\n min_score = 22.0 if is_filename_search else 10.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/record-online-time', methods=['POST'])\n @login_required\n def record_online_time():\n \"\"\"记录用户在线时长(前端定期上报)\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n duration = data.get('duration', 0) # 在线时长(秒)\n \n if duration <= 0:\n return jsonify({'success': False, 'message': '时长必须大于0'}), 400\n \n # 限制单次上报最大时长为10分钟(防止异常数据)\n if duration > 600:\n duration = 600\n \n try:\n db_manager.log_user_behavior(\n user_id=user_id,\n action_type='study',\n duration=duration,\n details={'source': 'page_online_time'}\n )\n \n return jsonify({\n 'success': True,\n 'message': f'已记录在线时长: {duration}秒',\n 'duration': duration\n })\n \n except Exception as e:\n print(f\"记录在线时长失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n values = []\n labels = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n # 生成标签:显示星期几和日期\n weekday = target_date.weekday() # 0=周一, 6=周日\n label = f\"{weekdays[weekday]}\\n{target_date.strftime('%m/%d')}\"\n labels.append(label)\n \n start_time = datetime.combine(target_date, datetime.min.time())\n # 使用下一天的开始时间作为结束时间,使用 < 比较,这样可以包含当天23:59:59的数据\n end_time = datetime.combine(target_date, datetime.max.time()) + timedelta(seconds=1)\n \n # 查询该天的学习时长(秒转小时)- 只统计在线时长(study类型)\n # 使用 date() 函数提取日期部分进行比较,更可靠\n date_str = target_date.strftime('%Y-%m-%d')\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND date(timestamp) = date(?)\n AND action_type = 'study'\n AND duration IS NOT NULL\n AND duration > 0\n ''', (user_id, date_str)).fetchone()\n \n total_duration = result['total_duration'] if result and result['total_duration'] else 0\n hours = round(total_duration / 3600.0, 1) if total_duration > 0 else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_date_str = week_start.strftime('%Y-%m-%d')\n end_date_str = week_end.strftime('%Y-%m-%d')\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND date(timestamp) >= date(?)\n AND date(timestamp) <= date(?)\n AND action_type = 'study'\n AND duration IS NOT NULL\n AND duration > 0\n ''', (user_id, start_date_str, end_date_str)).fetchone()\n \n total_duration = result['total_duration'] if result and result['total_duration'] else 0\n hours = round(total_duration / 3600.0, 1) if total_duration > 0 else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1).date()\n if month == 12:\n month_end = datetime(year + 1, 1, 1).date() - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1).date() - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n start_date_str = month_start.strftime('%Y-%m-%d')\n end_date_str = month_end.strftime('%Y-%m-%d')\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND date(timestamp) >= date(?)\n AND date(timestamp) <= date(?)\n AND action_type = 'study'\n AND duration IS NOT NULL\n AND duration > 0\n ''', (user_id, start_date_str, end_date_str)).fetchone()\n \n total_duration = result['total_duration'] if result and result['total_duration'] else 0\n hours = round(total_duration / 3600.0, 1) if total_duration > 0 else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n @login_required\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(基于用户手动输入的数据)\"\"\"\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户手动管理的知识点列表\n points = conn.execute('''\n SELECT id, name, mastery_level, created_at, updated_at\n FROM user_manual_knowledge_points\n WHERE user_id = ?\n ORDER BY mastery_level DESC, updated_at DESC\n ''', (user_id,)).fetchall()\n \n # 统计各等级的数量\n mastered = 0\n learning = 0\n pending = 0\n \n knowledge_points = {\n 'mastered': [],\n 'learning': [],\n 'pending': []\n }\n \n for point in points:\n level = point['mastery_level']\n point_data = {\n 'id': point['id'],\n 'name': point['name'],\n 'created_at': point['created_at'],\n 'updated_at': point['updated_at']\n }\n \n if level == 2: # 已掌握\n mastered += 1\n knowledge_points['mastered'].append(point_data)\n elif level == 1: # 学习中\n learning += 1\n knowledge_points['learning'].append(point_data)\n else: # 待学习\n pending += 1\n knowledge_points['pending'].append(point_data)\n \n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n total = mastered + learning + pending\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total,\n 'knowledge_points': knowledge_points\n }\n })\n \n @app.route('/api/analytics/knowledge-points', methods=['POST'])\n @login_required\n def save_knowledge_point():\n \"\"\"保存或更新知识点\"\"\"\n user_id = session.get('user_id', 1)\n data = request.get_json(force=True, silent=True) or {}\n \n point_id = data.get('id') # 如果有id则是更新,否则是新增\n name = data.get('name', '').strip()\n mastery_level = data.get('mastery_level', 0)\n \n if not name:\n return jsonify({'success': False, 'message': '知识点名称不能为空'}), 400\n \n if mastery_level not in [0, 1, 2]:\n return jsonify({'success': False, 'message': '掌握等级无效'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n from datetime import datetime\n now = datetime.now().isoformat()\n \n if point_id:\n # 更新\n conn.execute('''\n UPDATE user_manual_knowledge_points\n SET name = ?, mastery_level = ?, updated_at = ?\n WHERE id = ? AND user_id = ?\n ''', (name, mastery_level, now, point_id, user_id))\n conn.commit()\n return jsonify({'success': True, 'message': '知识点已更新'})\n else:\n # 新增\n cursor = conn.execute('''\n INSERT INTO user_manual_knowledge_points (user_id, name, mastery_level)\n VALUES (?, ?, ?)\n ''', (user_id, name, mastery_level))\n conn.commit()\n return jsonify({\n 'success': True,\n 'message': '知识点已添加',\n 'id': cursor.lastrowid\n })\n except Exception as e:\n print(f\"保存知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/knowledge-points/<int:point_id>', methods=['DELETE'])\n @login_required\n def delete_knowledge_point(point_id):\n \"\"\"删除知识点\"\"\"\n user_id = session.get('user_id', 1)\n \n try:\n with db_manager.get_connection() as conn:\n conn.execute('''\n DELETE FROM user_manual_knowledge_points\n WHERE id = ? AND user_id = ?\n ''', (point_id, user_id))\n conn.commit()\n return jsonify({'success': True, 'message': '知识点已删除'})\n except Exception as e:\n print(f\"删除知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/knowledge-points/batch', methods=['POST'])\n @login_required\n def batch_update_knowledge_points():\n \"\"\"批量更新知识点(用于快速设置数量)\"\"\"\n user_id = session.get('user_id', 1)\n data = request.get_json(force=True, silent=True) or {}\n \n mastered_count = max(0, int(data.get('mastered', 0)))\n learning_count = max(0, int(data.get('learning', 0)))\n pending_count = max(0, int(data.get('pending', 0)))\n \n try:\n with db_manager.get_connection() as conn:\n # 获取当前各等级的知识点\n mastered_points = [p['id'] for p in conn.execute('''\n SELECT id FROM user_manual_knowledge_points\n WHERE user_id = ? AND mastery_level = 2\n ''', (user_id,)).fetchall()]\n \n learning_points = [p['id'] for p in conn.execute('''\n SELECT id FROM user_manual_knowledge_points\n WHERE user_id = ? AND mastery_level = 1\n ''', (user_id,)).fetchall()]\n \n pending_points = [p['id'] for p in conn.execute('''\n SELECT id FROM user_manual_knowledge_points\n WHERE user_id = ? AND mastery_level = 0\n ''', (user_id,)).fetchall()]\n \n from datetime import datetime\n now = datetime.now().isoformat()\n \n # 调整已掌握的知识点数量\n if len(mastered_points) < mastered_count:\n # 需要添加\n for i in range(mastered_count - len(mastered_points)):\n conn.execute('''\n INSERT INTO user_manual_knowledge_points (user_id, name, mastery_level)\n VALUES (?, ?, ?)\n ''', (user_id, f'已掌握知识点{len(mastered_points) + i + 1}', 2))\n elif len(mastered_points) > mastered_count:\n # 需要删除多余的\n to_delete = mastered_points[mastered_count:]\n for point_id in to_delete:\n conn.execute('''\n DELETE FROM user_manual_knowledge_points WHERE id = ?\n ''', (point_id,))\n \n # 调整学习中的知识点数量\n if len(learning_points) < learning_count:\n for i in range(learning_count - len(learning_points)):\n conn.execute('''\n INSERT INTO user_manual_knowledge_points (user_id, name, mastery_level)\n VALUES (?, ?, ?)\n ''', (user_id, f'学习中知识点{len(learning_points) + i + 1}', 1))\n elif len(learning_points) > learning_count:\n to_delete = learning_points[learning_count:]\n for point_id in to_delete:\n conn.execute('''\n DELETE FROM user_manual_knowledge_points WHERE id = ?\n ''', (point_id,))\n \n # 调整待学习的知识点数量\n if len(pending_points) < pending_count:\n for i in range(pending_count - len(pending_points)):\n conn.execute('''\n INSERT INTO user_manual_knowledge_points (user_id, name, mastery_level)\n VALUES (?, ?, ?)\n ''', (user_id, f'待学习知识点{len(pending_points) + i + 1}', 0))\n elif len(pending_points) > pending_count:\n to_delete = pending_points[pending_count:]\n for point_id in to_delete:\n conn.execute('''\n DELETE FROM user_manual_knowledge_points WHERE id = ?\n ''', (point_id,))\n \n conn.commit()\n return jsonify({'success': True, 'message': '知识点数量已更新'})\n except Exception as e:\n print(f\"批量更新知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n # ========== 知识点学习行为记录API ==========\n \n @app.route('/api/knowledge-point/<int:node_id>/start-study', methods=['POST'])\n @login_required\n def start_study_knowledge_point(node_id):\n \"\"\"开始学习某个知识点\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 创建或更新学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n now = datetime.now().isoformat()\n \n if existing:\n # 更新现有记录\n conn.execute('''\n UPDATE knowledge_point_learning \n SET study_count = study_count + 1, \n last_study_time = ?,\n mastery_level = CASE WHEN mastery_level = 0 THEN 1 ELSE mastery_level END,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (now, now, user_id, node_id))\n else:\n # 创建新记录,状态为\"学习中\"\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, now, now))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'开始学习知识点: {node[\"name\"]}',\n 'node_id': node_id\n })\n \n except Exception as e:\n print(f\"开始学习知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-study', methods=['POST'])\n @login_required\n def record_study_time(node_id):\n \"\"\"记录知识点学习时长\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n duration = data.get('duration', 0) # 学习时长(秒)\n details = data.get('details') # 可选的详细信息\n \n if duration <= 0:\n return jsonify({'success': False, 'message': '学习时长必须大于0'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id, total_study_time, mastery_level FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n new_total_time = existing['total_study_time'] + duration\n # 如果累计学习时间超过30分钟且状态为待学习,则更新为学习中\n new_mastery = existing['mastery_level']\n if new_mastery == 0:\n new_mastery = 1\n \n conn.execute('''\n UPDATE knowledge_point_learning \n SET total_study_time = ?,\n last_study_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (new_total_time, now, new_mastery, now, user_id, node_id))\n else:\n # 创建新记录\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, total_study_time, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, duration, now, now))\n \n # 记录学习历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, duration, details)\n VALUES (?, ?, 'study', ?, ?)\n ''', (user_id, node_id, duration, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'已记录学习时长: {duration}秒',\n 'node_id': node_id,\n 'duration': duration\n })\n \n except Exception as e:\n print(f\"记录学习时长失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-test', methods=['POST'])\n @login_required\n def record_test_score(node_id):\n \"\"\"记录知识点测试分数并自动更新掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n score = data.get('score') # 测试分数(0-100)\n details = data.get('details') # 可选的详细信息\n \n if score is None or not (0 <= score <= 100):\n return jsonify({'success': False, 'message': '测试分数必须在0-100之间'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 根据分数决定掌握等级\n # 分数 >= 80: 已掌握(2)\n # 分数 >= 60: 学习中(1)\n # 分数 < 60: 学习中(1) - 需要继续学习\n if score >= 80:\n new_mastery = 2\n else:\n new_mastery = 1\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET test_score = ?,\n test_count = test_count + 1,\n last_test_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (score, now, new_mastery, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, test_score, test_count, last_test_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, ?, ?)\n ''', (user_id, node_id, score, now, new_mastery, now))\n \n # 记录测试历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, score, details)\n VALUES (?, ?, 'test', ?, ?)\n ''', (user_id, node_id, score, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n mastery_text = '已掌握' if new_mastery == 2 else '学习中'\n return jsonify({\n 'success': True,\n 'message': f'测试分数: {score}分,掌握状态: {mastery_text}',\n 'node_id': node_id,\n 'score': score,\n 'mastery_level': new_mastery,\n 'mastery_text': mastery_text\n })\n \n except Exception as e:\n print(f\"记录测试分数失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/update-mastery', methods=['POST'])\n @login_required\n def update_mastery_level(node_id):\n \"\"\"手动更新知识点掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n mastery_level = data.get('mastery_level') # 0=待学习, 1=学习中, 2=已掌握\n \n if mastery_level is None or mastery_level not in [0, 1, 2]:\n return jsonify({'success': False, 'message': '掌握等级必须为0、1或2'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (mastery_level, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, mastery_level, updated_at)\n VALUES (?, ?, ?, ?)\n ''', (user_id, node_id, mastery_level, now))\n \n conn.commit()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n return jsonify({\n 'success': True,\n 'message': f'已更新掌握状态: {mastery_texts[mastery_level]}',\n 'node_id': node_id,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts[mastery_level]\n })\n \n except Exception as e:\n print(f\"更新掌握状态失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/learning-detail')\n @login_required\n def get_knowledge_point_detail(node_id):\n \"\"\"获取知识点的学习详情,包含学习历史和掌握状态\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 获取知识点基本信息\n node = conn.execute('''\n SELECT kn.*, d.title as document_title\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kn.id = ?\n ''', (node_id,)).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 获取用户的学习记录\n learning = conn.execute('''\n SELECT * FROM knowledge_point_learning\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (user_id, node_id)).fetchone()\n \n # 获取学习历史\n history = conn.execute('''\n SELECT * FROM knowledge_point_study_history\n WHERE user_id = ? AND knowledge_node_id = ?\n ORDER BY created_at DESC\n LIMIT 20\n ''', (user_id, node_id)).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n # 构建返回数据\n node_data = {\n 'id': node['id'],\n 'name': node['name'],\n 'node_type': node['node_type'],\n 'description': node['description'],\n 'document_title': node['document_title']\n }\n \n learning_data = None\n if learning:\n learning_data = {\n 'total_study_time': learning['total_study_time'] or 0,\n 'study_count': learning['study_count'] or 0,\n 'last_study_time': learning['last_study_time'],\n 'test_score': learning['test_score'],\n 'test_count': learning['test_count'] or 0,\n 'last_test_time': learning['last_test_time'],\n 'mastery_level': learning['mastery_level'] or 0,\n 'mastery_text': mastery_texts.get(learning['mastery_level'], '待学习')\n }\n else:\n learning_data = {\n 'total_study_time': 0,\n 'study_count': 0,\n 'last_study_time': None,\n 'test_score': None,\n 'test_count': 0,\n 'last_test_time': None,\n 'mastery_level': 0,\n 'mastery_text': '待学习'\n }\n \n history_data = []\n for h in history:\n history_data.append({\n 'action_type': h['action_type'],\n 'duration': h['duration'],\n 'score': h['score'],\n 'details': json.loads(h['details']) if h['details'] else None,\n 'created_at': h['created_at']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'node': node_data,\n 'learning': learning_data,\n 'history': history_data\n }\n })\n \n except Exception as e:\n print(f\"获取知识点详情失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-points/learning-status')\n @login_required\n def get_all_knowledge_points_status():\n \"\"\"获取所有知识点的学习状态列表\"\"\"\n user_id = session.get('user_id')\n page = request.args.get('page', 1, type=int)\n per_page = request.args.get('per_page', 50, type=int)\n mastery_filter = request.args.get('mastery') # 0, 1, 2 或 'all'\n \n try:\n with db_manager.get_connection() as conn:\n # 构建查询\n base_query = '''\n SELECT kn.id, kn.name, kn.node_type, kn.description,\n kpl.total_study_time, kpl.study_count, kpl.test_score, \n kpl.test_count, kpl.mastery_level, kpl.last_study_time\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n LEFT JOIN knowledge_point_learning kpl ON kn.id = kpl.knowledge_node_id AND kpl.user_id = ?\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n '''\n \n params = [user_id]\n \n # 根据掌握等级过滤\n if mastery_filter and mastery_filter != 'all':\n try:\n mastery_level = int(mastery_filter)\n if mastery_level == 0:\n base_query += ' AND (kpl.mastery_level IS NULL OR kpl.mastery_level = 0)'\n else:\n base_query += ' AND kpl.mastery_level = ?'\n params.append(mastery_level)\n except ValueError:\n pass\n \n # 统计总数\n count_query = f'SELECT COUNT(*) FROM ({base_query})'\n total = conn.execute(count_query, params).fetchone()[0]\n \n # 分页查询\n offset = (page - 1) * per_page\n base_query += ' ORDER BY kpl.last_study_time DESC NULLS LAST, kn.name LIMIT ? OFFSET ?'\n params.extend([per_page, offset])\n \n nodes = conn.execute(base_query, params).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n result = []\n for n in nodes:\n mastery_level = n['mastery_level'] if n['mastery_level'] is not None else 0\n result.append({\n 'id': n['id'],\n 'name': n['name'],\n 'node_type': n['node_type'],\n 'description': n['description'],\n 'total_study_time': n['total_study_time'] or 0,\n 'study_count': n['study_count'] or 0,\n 'test_score': n['test_score'],\n 'test_count': n['test_count'] or 0,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts.get(mastery_level, '待学习'),\n 'last_study_time': n['last_study_time']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'items': result,\n 'total': total,\n 'page': page,\n 'per_page': per_page,\n 'total_pages': (total + per_page - 1) // per_page\n }\n })\n \n except Exception as e:\n print(f\"获取知识点状态列表失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/study-summary')\n @login_required\n def get_study_summary():\n \"\"\"获取学习概况统计(基于真实数据)\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 统计今日学习时长\n from datetime import datetime, timedelta\n today = datetime.now().date()\n today_start = datetime.combine(today, datetime.min.time())\n today_end = datetime.combine(today, datetime.max.time())\n \n today_study = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM knowledge_point_study_history\n WHERE user_id = ? AND action_type = 'study'\n AND created_at >= ? AND created_at < ?\n ''', (user_id, today_start.isoformat(), today_end.isoformat())).fetchone()\n \n today_hours = round((today_study['total_duration'] or 0) / 3600.0, 2)\n \n # 统计知识点掌握情况\n mastered_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 2\n ''', (user_id,)).fetchone()[0]\n \n learning_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n ''', (user_id,)).fetchone()[0]\n \n # 统计总知识点数\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n pending_count = total_nodes - mastered_count - learning_count\n \n # 统计待复习内容(超过7天未学习的\"学习中\"状态知识点)\n seven_days_ago = datetime.now() - timedelta(days=7)\n need_review = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n AND (last_study_time < ? OR last_study_time IS NULL)\n ''', (user_id, seven_days_ago.isoformat())).fetchone()[0]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'today_study_hours': today_hours,\n 'mastered_count': mastered_count,\n 'learning_count': learning_count,\n 'pending_count': pending_count,\n 'total_nodes': total_nodes,\n 'need_review_count': need_review\n }\n })\n \n except Exception as e:\n print(f\"获取学习概况失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n @login_required\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n user_id = session.get('user_id', 1)\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天,基于在线时长study类型)\n study_time_data = []\n today = datetime.now().date()\n for i in range(6, -1, -1):\n target_date = today - timedelta(days=i)\n date_str = target_date.strftime('%Y-%m-%d')\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND date(timestamp) = date(?)\n AND action_type = 'study'\n AND duration IS NOT NULL\n AND duration > 0\n ''', (user_id, date_str)).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': date_str,\n 'hours': hours\n })\n \n total_study_hours = round(sum(s['hours'] for s in study_time_data), 1)\n \n # 知识点学习进度(用户手动管理的知识点)\n knowledge_points_data = {\n 'mastered': [],\n 'learning': [],\n 'pending': []\n }\n \n points = conn.execute('''\n SELECT id, name, mastery_level\n FROM user_manual_knowledge_points\n WHERE user_id = ?\n ORDER BY mastery_level DESC, name ASC\n ''', (user_id,)).fetchall()\n \n for point in points:\n point_info = {\n 'id': point['id'],\n 'name': point['name']\n }\n level = point['mastery_level']\n if level == 2: # 已掌握\n knowledge_points_data['mastered'].append(point_info)\n elif level == 1: # 学习中\n knowledge_points_data['learning'].append(point_info)\n else: # 待学习\n knowledge_points_data['pending'].append(point_info)\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': total_study_hours,\n 'knowledge_points': {\n 'mastered': {\n 'count': len(knowledge_points_data['mastered']),\n 'items': knowledge_points_data['mastered']\n },\n 'learning': {\n 'count': len(knowledge_points_data['learning']),\n 'items': knowledge_points_data['learning']\n },\n 'pending': {\n 'count': len(knowledge_points_data['pending']),\n 'items': knowledge_points_data['pending']\n }\n }\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情,并记录一次查看行为供学习时长统计使用\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except Exception:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except Exception:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n\n # 记录一次查看行为(简单估计一次查看 5 分钟 = 300 秒)\n try:\n current_user_id = session.get('user_id')\n if current_user_id:\n db_manager.log_user_behavior(\n user_id=current_user_id,\n action_type='view',\n document_id=doc_id,\n duration=300,\n details={\n 'source': 'document_detail',\n 'title': doc_dict.get('title')\n }\n )\n except Exception as behavior_err:\n print(f\"记录查看行为失败: {behavior_err}\")\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "fcd99e64c282e9032bc579551cfb36efdf14c838": {"t": 1785758511.4036603, "n": 140764, "h": "8c2757e2c92ff361", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识节点表添加mastery_level字段(掌握等级:0=待学习, 1=学习中, 2=已掌握)\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN mastery_level INTEGER DEFAULT 0')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识点学习记录表 - 记录用户对每个知识点的学习行为\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_learning (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n total_study_time INTEGER DEFAULT 0, -- 累计学习时长(秒)\n study_count INTEGER DEFAULT 0, -- 学习次数\n last_study_time TIMESTAMP, -- 最后学习时间\n test_score REAL, -- 最近测试分数(0-100)\n test_count INTEGER DEFAULT 0, -- 测试次数\n last_test_time TIMESTAMP, -- 最后测试时间\n mastery_level INTEGER DEFAULT 0, -- 掌握等级:0=待学习, 1=学习中, 2=已掌握\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id),\n UNIQUE(user_id, knowledge_node_id)\n )\n ''')\n \n # 知识点学习历史表 - 记录每次学习的详细信息\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_study_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n action_type TEXT NOT NULL, -- study(学习)、test(测试)\n duration INTEGER, -- 本次学习时长(秒)\n score REAL, -- 本次测试分数(仅测试时有值)\n details TEXT, -- JSON格式存储详细信息\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n # 用户行为记录工具方法\n def log_user_behavior(self, user_id: int, action_type: str, document_id: Optional[int] = None,\n duration: Optional[int] = None, details: Optional[Dict[str, Any]] = None) -> None:\n \"\"\"\n 记录用户行为到 user_behaviors 表中\n duration 单位为秒;details 为可选的 JSON 信息\n \"\"\"\n try:\n with self.get_connection() as conn:\n conn.execute(\n '''\n INSERT INTO user_behaviors (user_id, document_id, action_type, duration, details)\n VALUES (?, ?, ?, ?, ?)\n ''',\n (\n user_id,\n document_id,\n action_type,\n duration,\n json.dumps(details, ensure_ascii=False) if details else None\n )\n )\n conn.commit()\n except Exception as e:\n # 打印错误但不影响主流程\n print(f\"记录用户行为失败: {e}\")\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 在保证精确度的前提下适当放宽召回\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配(稍微放宽阈值)\n if query_lower in content:\n # 计算出现次数,要求至少出现1次即可计分\n count = content.count(query_lower)\n if count >= 1:\n score += min(count * 1.2, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数(略微降低阈值,提升召回)\n min_score = 22.0 if is_filename_search else 10.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/record-online-time', methods=['POST'])\n @login_required\n def record_online_time():\n \"\"\"记录用户在线时长(前端定期上报)\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n duration = data.get('duration', 0) # 在线时长(秒)\n \n if duration <= 0:\n return jsonify({'success': False, 'message': '时长必须大于0'}), 400\n \n # 限制单次上报最大时长为10分钟(防止异常数据)\n if duration > 600:\n duration = 600\n \n try:\n db_manager.log_user_behavior(\n user_id=user_id,\n action_type='study',\n duration=duration,\n details={'source': 'page_online_time'}\n )\n \n return jsonify({\n 'success': True,\n 'message': f'已记录在线时长: {duration}秒',\n 'duration': duration\n })\n \n except Exception as e:\n print(f\"记录在线时长失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n @login_required\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(基于真实学习数据计算)\"\"\"\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n # 基于真实的学习数据计算掌握分布\n # 统计已掌握的知识点(mastery_level = 2)\n mastered = conn.execute('''\n SELECT COUNT(DISTINCT kpl.knowledge_node_id) \n FROM knowledge_point_learning kpl\n INNER JOIN knowledge_nodes kn ON kpl.knowledge_node_id = kn.id\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kpl.user_id = ? \n AND kpl.mastery_level = 2\n AND (kn.document_id IS NULL OR d.is_deleted = 0)\n ''', (user_id,)).fetchone()[0]\n \n # 统计学习中的知识点(mastery_level = 1)\n learning = conn.execute('''\n SELECT COUNT(DISTINCT kpl.knowledge_node_id) \n FROM knowledge_point_learning kpl\n INNER JOIN knowledge_nodes kn ON kpl.knowledge_node_id = kn.id\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kpl.user_id = ? \n AND kpl.mastery_level = 1\n AND (kn.document_id IS NULL OR d.is_deleted = 0)\n ''', (user_id,)).fetchone()[0]\n \n # 剩余的是待学习的知识点\n pending = total_nodes - mastered - learning\n \n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n # ========== 知识点学习行为记录API ==========\n \n @app.route('/api/knowledge-point/<int:node_id>/start-study', methods=['POST'])\n @login_required\n def start_study_knowledge_point(node_id):\n \"\"\"开始学习某个知识点\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 创建或更新学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n now = datetime.now().isoformat()\n \n if existing:\n # 更新现有记录\n conn.execute('''\n UPDATE knowledge_point_learning \n SET study_count = study_count + 1, \n last_study_time = ?,\n mastery_level = CASE WHEN mastery_level = 0 THEN 1 ELSE mastery_level END,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (now, now, user_id, node_id))\n else:\n # 创建新记录,状态为\"学习中\"\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, now, now))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'开始学习知识点: {node[\"name\"]}',\n 'node_id': node_id\n })\n \n except Exception as e:\n print(f\"开始学习知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-study', methods=['POST'])\n @login_required\n def record_study_time(node_id):\n \"\"\"记录知识点学习时长\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n duration = data.get('duration', 0) # 学习时长(秒)\n details = data.get('details') # 可选的详细信息\n \n if duration <= 0:\n return jsonify({'success': False, 'message': '学习时长必须大于0'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id, total_study_time, mastery_level FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n new_total_time = existing['total_study_time'] + duration\n # 如果累计学习时间超过30分钟且状态为待学习,则更新为学习中\n new_mastery = existing['mastery_level']\n if new_mastery == 0:\n new_mastery = 1\n \n conn.execute('''\n UPDATE knowledge_point_learning \n SET total_study_time = ?,\n last_study_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (new_total_time, now, new_mastery, now, user_id, node_id))\n else:\n # 创建新记录\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, total_study_time, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, duration, now, now))\n \n # 记录学习历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, duration, details)\n VALUES (?, ?, 'study', ?, ?)\n ''', (user_id, node_id, duration, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'已记录学习时长: {duration}秒',\n 'node_id': node_id,\n 'duration': duration\n })\n \n except Exception as e:\n print(f\"记录学习时长失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-test', methods=['POST'])\n @login_required\n def record_test_score(node_id):\n \"\"\"记录知识点测试分数并自动更新掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n score = data.get('score') # 测试分数(0-100)\n details = data.get('details') # 可选的详细信息\n \n if score is None or not (0 <= score <= 100):\n return jsonify({'success': False, 'message': '测试分数必须在0-100之间'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 根据分数决定掌握等级\n # 分数 >= 80: 已掌握(2)\n # 分数 >= 60: 学习中(1)\n # 分数 < 60: 学习中(1) - 需要继续学习\n if score >= 80:\n new_mastery = 2\n else:\n new_mastery = 1\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET test_score = ?,\n test_count = test_count + 1,\n last_test_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (score, now, new_mastery, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, test_score, test_count, last_test_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, ?, ?)\n ''', (user_id, node_id, score, now, new_mastery, now))\n \n # 记录测试历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, score, details)\n VALUES (?, ?, 'test', ?, ?)\n ''', (user_id, node_id, score, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n mastery_text = '已掌握' if new_mastery == 2 else '学习中'\n return jsonify({\n 'success': True,\n 'message': f'测试分数: {score}分,掌握状态: {mastery_text}',\n 'node_id': node_id,\n 'score': score,\n 'mastery_level': new_mastery,\n 'mastery_text': mastery_text\n })\n \n except Exception as e:\n print(f\"记录测试分数失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/update-mastery', methods=['POST'])\n @login_required\n def update_mastery_level(node_id):\n \"\"\"手动更新知识点掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n mastery_level = data.get('mastery_level') # 0=待学习, 1=学习中, 2=已掌握\n \n if mastery_level is None or mastery_level not in [0, 1, 2]:\n return jsonify({'success': False, 'message': '掌握等级必须为0、1或2'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (mastery_level, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, mastery_level, updated_at)\n VALUES (?, ?, ?, ?)\n ''', (user_id, node_id, mastery_level, now))\n \n conn.commit()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n return jsonify({\n 'success': True,\n 'message': f'已更新掌握状态: {mastery_texts[mastery_level]}',\n 'node_id': node_id,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts[mastery_level]\n })\n \n except Exception as e:\n print(f\"更新掌握状态失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/learning-detail')\n @login_required\n def get_knowledge_point_detail(node_id):\n \"\"\"获取知识点的学习详情,包含学习历史和掌握状态\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 获取知识点基本信息\n node = conn.execute('''\n SELECT kn.*, d.title as document_title\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kn.id = ?\n ''', (node_id,)).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 获取用户的学习记录\n learning = conn.execute('''\n SELECT * FROM knowledge_point_learning\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (user_id, node_id)).fetchone()\n \n # 获取学习历史\n history = conn.execute('''\n SELECT * FROM knowledge_point_study_history\n WHERE user_id = ? AND knowledge_node_id = ?\n ORDER BY created_at DESC\n LIMIT 20\n ''', (user_id, node_id)).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n # 构建返回数据\n node_data = {\n 'id': node['id'],\n 'name': node['name'],\n 'node_type': node['node_type'],\n 'description': node['description'],\n 'document_title': node['document_title']\n }\n \n learning_data = None\n if learning:\n learning_data = {\n 'total_study_time': learning['total_study_time'] or 0,\n 'study_count': learning['study_count'] or 0,\n 'last_study_time': learning['last_study_time'],\n 'test_score': learning['test_score'],\n 'test_count': learning['test_count'] or 0,\n 'last_test_time': learning['last_test_time'],\n 'mastery_level': learning['mastery_level'] or 0,\n 'mastery_text': mastery_texts.get(learning['mastery_level'], '待学习')\n }\n else:\n learning_data = {\n 'total_study_time': 0,\n 'study_count': 0,\n 'last_study_time': None,\n 'test_score': None,\n 'test_count': 0,\n 'last_test_time': None,\n 'mastery_level': 0,\n 'mastery_text': '待学习'\n }\n \n history_data = []\n for h in history:\n history_data.append({\n 'action_type': h['action_type'],\n 'duration': h['duration'],\n 'score': h['score'],\n 'details': json.loads(h['details']) if h['details'] else None,\n 'created_at': h['created_at']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'node': node_data,\n 'learning': learning_data,\n 'history': history_data\n }\n })\n \n except Exception as e:\n print(f\"获取知识点详情失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-points/learning-status')\n @login_required\n def get_all_knowledge_points_status():\n \"\"\"获取所有知识点的学习状态列表\"\"\"\n user_id = session.get('user_id')\n page = request.args.get('page', 1, type=int)\n per_page = request.args.get('per_page', 50, type=int)\n mastery_filter = request.args.get('mastery') # 0, 1, 2 或 'all'\n \n try:\n with db_manager.get_connection() as conn:\n # 构建查询\n base_query = '''\n SELECT kn.id, kn.name, kn.node_type, kn.description,\n kpl.total_study_time, kpl.study_count, kpl.test_score, \n kpl.test_count, kpl.mastery_level, kpl.last_study_time\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n LEFT JOIN knowledge_point_learning kpl ON kn.id = kpl.knowledge_node_id AND kpl.user_id = ?\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n '''\n \n params = [user_id]\n \n # 根据掌握等级过滤\n if mastery_filter and mastery_filter != 'all':\n try:\n mastery_level = int(mastery_filter)\n if mastery_level == 0:\n base_query += ' AND (kpl.mastery_level IS NULL OR kpl.mastery_level = 0)'\n else:\n base_query += ' AND kpl.mastery_level = ?'\n params.append(mastery_level)\n except ValueError:\n pass\n \n # 统计总数\n count_query = f'SELECT COUNT(*) FROM ({base_query})'\n total = conn.execute(count_query, params).fetchone()[0]\n \n # 分页查询\n offset = (page - 1) * per_page\n base_query += ' ORDER BY kpl.last_study_time DESC NULLS LAST, kn.name LIMIT ? OFFSET ?'\n params.extend([per_page, offset])\n \n nodes = conn.execute(base_query, params).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n result = []\n for n in nodes:\n mastery_level = n['mastery_level'] if n['mastery_level'] is not None else 0\n result.append({\n 'id': n['id'],\n 'name': n['name'],\n 'node_type': n['node_type'],\n 'description': n['description'],\n 'total_study_time': n['total_study_time'] or 0,\n 'study_count': n['study_count'] or 0,\n 'test_score': n['test_score'],\n 'test_count': n['test_count'] or 0,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts.get(mastery_level, '待学习'),\n 'last_study_time': n['last_study_time']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'items': result,\n 'total': total,\n 'page': page,\n 'per_page': per_page,\n 'total_pages': (total + per_page - 1) // per_page\n }\n })\n \n except Exception as e:\n print(f\"获取知识点状态列表失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/study-summary')\n @login_required\n def get_study_summary():\n \"\"\"获取学习概况统计(基于真实数据)\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 统计今日学习时长\n from datetime import datetime, timedelta\n today = datetime.now().date()\n today_start = datetime.combine(today, datetime.min.time())\n today_end = datetime.combine(today, datetime.max.time())\n \n today_study = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM knowledge_point_study_history\n WHERE user_id = ? AND action_type = 'study'\n AND created_at >= ? AND created_at < ?\n ''', (user_id, today_start.isoformat(), today_end.isoformat())).fetchone()\n \n today_hours = round((today_study['total_duration'] or 0) / 3600.0, 2)\n \n # 统计知识点掌握情况\n mastered_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 2\n ''', (user_id,)).fetchone()[0]\n \n learning_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n ''', (user_id,)).fetchone()[0]\n \n # 统计总知识点数\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n pending_count = total_nodes - mastered_count - learning_count\n \n # 统计待复习内容(超过7天未学习的\"学习中\"状态知识点)\n seven_days_ago = datetime.now() - timedelta(days=7)\n need_review = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n AND (last_study_time < ? OR last_study_time IS NULL)\n ''', (user_id, seven_days_ago.isoformat())).fetchone()[0]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'today_study_hours': today_hours,\n 'mastered_count': mastered_count,\n 'learning_count': learning_count,\n 'pending_count': pending_count,\n 'total_nodes': total_nodes,\n 'need_review_count': need_review\n }\n })\n \n except Exception as e:\n print(f\"获取学习概况失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情,并记录一次查看行为供学习时长统计使用\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except Exception:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except Exception:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n\n # 记录一次查看行为(简单估计一次查看 5 分钟 = 300 秒)\n try:\n current_user_id = session.get('user_id')\n if current_user_id:\n db_manager.log_user_behavior(\n user_id=current_user_id,\n action_type='view',\n document_id=doc_id,\n duration=300,\n details={\n 'source': 'document_detail',\n 'title': doc_dict.get('title')\n }\n )\n except Exception as behavior_err:\n print(f\"记录查看行为失败: {behavior_err}\")\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "d04402d8ec1415355e735508fef21f39b1569535": {"t": 1785758514.8011343, "n": 139676, "h": "bf5b074341dfb651", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识节点表添加mastery_level字段(掌握等级:0=待学习, 1=学习中, 2=已掌握)\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN mastery_level INTEGER DEFAULT 0')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识点学习记录表 - 记录用户对每个知识点的学习行为\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_learning (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n total_study_time INTEGER DEFAULT 0, -- 累计学习时长(秒)\n study_count INTEGER DEFAULT 0, -- 学习次数\n last_study_time TIMESTAMP, -- 最后学习时间\n test_score REAL, -- 最近测试分数(0-100)\n test_count INTEGER DEFAULT 0, -- 测试次数\n last_test_time TIMESTAMP, -- 最后测试时间\n mastery_level INTEGER DEFAULT 0, -- 掌握等级:0=待学习, 1=学习中, 2=已掌握\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id),\n UNIQUE(user_id, knowledge_node_id)\n )\n ''')\n \n # 知识点学习历史表 - 记录每次学习的详细信息\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_point_study_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n knowledge_node_id INTEGER NOT NULL,\n action_type TEXT NOT NULL, -- study(学习)、test(测试)\n duration INTEGER, -- 本次学习时长(秒)\n score REAL, -- 本次测试分数(仅测试时有值)\n details TEXT, -- JSON格式存储详细信息\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (knowledge_node_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n # 用户行为记录工具方法\n def log_user_behavior(self, user_id: int, action_type: str, document_id: Optional[int] = None,\n duration: Optional[int] = None, details: Optional[Dict[str, Any]] = None) -> None:\n \"\"\"\n 记录用户行为到 user_behaviors 表中\n duration 单位为秒;details 为可选的 JSON 信息\n \"\"\"\n try:\n with self.get_connection() as conn:\n conn.execute(\n '''\n INSERT INTO user_behaviors (user_id, document_id, action_type, duration, details)\n VALUES (?, ?, ?, ?, ?)\n ''',\n (\n user_id,\n document_id,\n action_type,\n duration,\n json.dumps(details, ensure_ascii=False) if details else None\n )\n )\n conn.commit()\n except Exception as e:\n # 打印错误但不影响主流程\n print(f\"记录用户行为失败: {e}\")\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 在保证精确度的前提下适当放宽召回\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配(稍微放宽阈值)\n if query_lower in content:\n # 计算出现次数,要求至少出现1次即可计分\n count = content.count(query_lower)\n if count >= 1:\n score += min(count * 1.2, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数(略微降低阈值,提升召回)\n min_score = 22.0 if is_filename_search else 10.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n @login_required\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(基于真实学习数据计算)\"\"\"\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n # 基于真实的学习数据计算掌握分布\n # 统计已掌握的知识点(mastery_level = 2)\n mastered = conn.execute('''\n SELECT COUNT(DISTINCT kpl.knowledge_node_id) \n FROM knowledge_point_learning kpl\n INNER JOIN knowledge_nodes kn ON kpl.knowledge_node_id = kn.id\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kpl.user_id = ? \n AND kpl.mastery_level = 2\n AND (kn.document_id IS NULL OR d.is_deleted = 0)\n ''', (user_id,)).fetchone()[0]\n \n # 统计学习中的知识点(mastery_level = 1)\n learning = conn.execute('''\n SELECT COUNT(DISTINCT kpl.knowledge_node_id) \n FROM knowledge_point_learning kpl\n INNER JOIN knowledge_nodes kn ON kpl.knowledge_node_id = kn.id\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kpl.user_id = ? \n AND kpl.mastery_level = 1\n AND (kn.document_id IS NULL OR d.is_deleted = 0)\n ''', (user_id,)).fetchone()[0]\n \n # 剩余的是待学习的知识点\n pending = total_nodes - mastered - learning\n \n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n # ========== 知识点学习行为记录API ==========\n \n @app.route('/api/knowledge-point/<int:node_id>/start-study', methods=['POST'])\n @login_required\n def start_study_knowledge_point(node_id):\n \"\"\"开始学习某个知识点\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 创建或更新学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n now = datetime.now().isoformat()\n \n if existing:\n # 更新现有记录\n conn.execute('''\n UPDATE knowledge_point_learning \n SET study_count = study_count + 1, \n last_study_time = ?,\n mastery_level = CASE WHEN mastery_level = 0 THEN 1 ELSE mastery_level END,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (now, now, user_id, node_id))\n else:\n # 创建新记录,状态为\"学习中\"\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, now, now))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'开始学习知识点: {node[\"name\"]}',\n 'node_id': node_id\n })\n \n except Exception as e:\n print(f\"开始学习知识点失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-study', methods=['POST'])\n @login_required\n def record_study_time(node_id):\n \"\"\"记录知识点学习时长\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n duration = data.get('duration', 0) # 学习时长(秒)\n details = data.get('details') # 可选的详细信息\n \n if duration <= 0:\n return jsonify({'success': False, 'message': '学习时长必须大于0'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id, total_study_time, mastery_level FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n new_total_time = existing['total_study_time'] + duration\n # 如果累计学习时间超过30分钟且状态为待学习,则更新为学习中\n new_mastery = existing['mastery_level']\n if new_mastery == 0:\n new_mastery = 1\n \n conn.execute('''\n UPDATE knowledge_point_learning \n SET total_study_time = ?,\n last_study_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (new_total_time, now, new_mastery, now, user_id, node_id))\n else:\n # 创建新记录\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, total_study_time, study_count, last_study_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, 1, ?)\n ''', (user_id, node_id, duration, now, now))\n \n # 记录学习历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, duration, details)\n VALUES (?, ?, 'study', ?, ?)\n ''', (user_id, node_id, duration, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n return jsonify({\n 'success': True,\n 'message': f'已记录学习时长: {duration}秒',\n 'node_id': node_id,\n 'duration': duration\n })\n \n except Exception as e:\n print(f\"记录学习时长失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/record-test', methods=['POST'])\n @login_required\n def record_test_score(node_id):\n \"\"\"记录知识点测试分数并自动更新掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n score = data.get('score') # 测试分数(0-100)\n details = data.get('details') # 可选的详细信息\n \n if score is None or not (0 <= score <= 100):\n return jsonify({'success': False, 'message': '测试分数必须在0-100之间'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 根据分数决定掌握等级\n # 分数 >= 80: 已掌握(2)\n # 分数 >= 60: 学习中(1)\n # 分数 < 60: 学习中(1) - 需要继续学习\n if score >= 80:\n new_mastery = 2\n else:\n new_mastery = 1\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET test_score = ?,\n test_count = test_count + 1,\n last_test_time = ?,\n mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (score, now, new_mastery, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, test_score, test_count, last_test_time, mastery_level, updated_at)\n VALUES (?, ?, ?, 1, ?, ?, ?)\n ''', (user_id, node_id, score, now, new_mastery, now))\n \n # 记录测试历史\n conn.execute('''\n INSERT INTO knowledge_point_study_history \n (user_id, knowledge_node_id, action_type, score, details)\n VALUES (?, ?, 'test', ?, ?)\n ''', (user_id, node_id, score, json.dumps(details, ensure_ascii=False) if details else None))\n \n conn.commit()\n \n mastery_text = '已掌握' if new_mastery == 2 else '学习中'\n return jsonify({\n 'success': True,\n 'message': f'测试分数: {score}分,掌握状态: {mastery_text}',\n 'node_id': node_id,\n 'score': score,\n 'mastery_level': new_mastery,\n 'mastery_text': mastery_text\n })\n \n except Exception as e:\n print(f\"记录测试分数失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/update-mastery', methods=['POST'])\n @login_required\n def update_mastery_level(node_id):\n \"\"\"手动更新知识点掌握状态\"\"\"\n user_id = session.get('user_id')\n data = request.get_json(force=True, silent=True) or {}\n mastery_level = data.get('mastery_level') # 0=待学习, 1=学习中, 2=已掌握\n \n if mastery_level is None or mastery_level not in [0, 1, 2]:\n return jsonify({'success': False, 'message': '掌握等级必须为0、1或2'}), 400\n \n try:\n with db_manager.get_connection() as conn:\n # 检查知识点是否存在\n node = conn.execute(\n 'SELECT id, name FROM knowledge_nodes WHERE id = ?',\n (node_id,)\n ).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n now = datetime.now().isoformat()\n \n # 更新或创建学习记录\n existing = conn.execute(\n 'SELECT id FROM knowledge_point_learning WHERE user_id = ? AND knowledge_node_id = ?',\n (user_id, node_id)\n ).fetchone()\n \n if existing:\n conn.execute('''\n UPDATE knowledge_point_learning \n SET mastery_level = ?,\n updated_at = ?\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (mastery_level, now, user_id, node_id))\n else:\n conn.execute('''\n INSERT INTO knowledge_point_learning \n (user_id, knowledge_node_id, mastery_level, updated_at)\n VALUES (?, ?, ?, ?)\n ''', (user_id, node_id, mastery_level, now))\n \n conn.commit()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n return jsonify({\n 'success': True,\n 'message': f'已更新掌握状态: {mastery_texts[mastery_level]}',\n 'node_id': node_id,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts[mastery_level]\n })\n \n except Exception as e:\n print(f\"更新掌握状态失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-point/<int:node_id>/learning-detail')\n @login_required\n def get_knowledge_point_detail(node_id):\n \"\"\"获取知识点的学习详情,包含学习历史和掌握状态\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 获取知识点基本信息\n node = conn.execute('''\n SELECT kn.*, d.title as document_title\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE kn.id = ?\n ''', (node_id,)).fetchone()\n \n if not node:\n return jsonify({'success': False, 'message': '知识点不存在'}), 404\n \n # 获取用户的学习记录\n learning = conn.execute('''\n SELECT * FROM knowledge_point_learning\n WHERE user_id = ? AND knowledge_node_id = ?\n ''', (user_id, node_id)).fetchone()\n \n # 获取学习历史\n history = conn.execute('''\n SELECT * FROM knowledge_point_study_history\n WHERE user_id = ? AND knowledge_node_id = ?\n ORDER BY created_at DESC\n LIMIT 20\n ''', (user_id, node_id)).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n # 构建返回数据\n node_data = {\n 'id': node['id'],\n 'name': node['name'],\n 'node_type': node['node_type'],\n 'description': node['description'],\n 'document_title': node['document_title']\n }\n \n learning_data = None\n if learning:\n learning_data = {\n 'total_study_time': learning['total_study_time'] or 0,\n 'study_count': learning['study_count'] or 0,\n 'last_study_time': learning['last_study_time'],\n 'test_score': learning['test_score'],\n 'test_count': learning['test_count'] or 0,\n 'last_test_time': learning['last_test_time'],\n 'mastery_level': learning['mastery_level'] or 0,\n 'mastery_text': mastery_texts.get(learning['mastery_level'], '待学习')\n }\n else:\n learning_data = {\n 'total_study_time': 0,\n 'study_count': 0,\n 'last_study_time': None,\n 'test_score': None,\n 'test_count': 0,\n 'last_test_time': None,\n 'mastery_level': 0,\n 'mastery_text': '待学习'\n }\n \n history_data = []\n for h in history:\n history_data.append({\n 'action_type': h['action_type'],\n 'duration': h['duration'],\n 'score': h['score'],\n 'details': json.loads(h['details']) if h['details'] else None,\n 'created_at': h['created_at']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'node': node_data,\n 'learning': learning_data,\n 'history': history_data\n }\n })\n \n except Exception as e:\n print(f\"获取知识点详情失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/knowledge-points/learning-status')\n @login_required\n def get_all_knowledge_points_status():\n \"\"\"获取所有知识点的学习状态列表\"\"\"\n user_id = session.get('user_id')\n page = request.args.get('page', 1, type=int)\n per_page = request.args.get('per_page', 50, type=int)\n mastery_filter = request.args.get('mastery') # 0, 1, 2 或 'all'\n \n try:\n with db_manager.get_connection() as conn:\n # 构建查询\n base_query = '''\n SELECT kn.id, kn.name, kn.node_type, kn.description,\n kpl.total_study_time, kpl.study_count, kpl.test_score, \n kpl.test_count, kpl.mastery_level, kpl.last_study_time\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n LEFT JOIN knowledge_point_learning kpl ON kn.id = kpl.knowledge_node_id AND kpl.user_id = ?\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n '''\n \n params = [user_id]\n \n # 根据掌握等级过滤\n if mastery_filter and mastery_filter != 'all':\n try:\n mastery_level = int(mastery_filter)\n if mastery_level == 0:\n base_query += ' AND (kpl.mastery_level IS NULL OR kpl.mastery_level = 0)'\n else:\n base_query += ' AND kpl.mastery_level = ?'\n params.append(mastery_level)\n except ValueError:\n pass\n \n # 统计总数\n count_query = f'SELECT COUNT(*) FROM ({base_query})'\n total = conn.execute(count_query, params).fetchone()[0]\n \n # 分页查询\n offset = (page - 1) * per_page\n base_query += ' ORDER BY kpl.last_study_time DESC NULLS LAST, kn.name LIMIT ? OFFSET ?'\n params.extend([per_page, offset])\n \n nodes = conn.execute(base_query, params).fetchall()\n \n mastery_texts = {0: '待学习', 1: '学习中', 2: '已掌握'}\n \n result = []\n for n in nodes:\n mastery_level = n['mastery_level'] if n['mastery_level'] is not None else 0\n result.append({\n 'id': n['id'],\n 'name': n['name'],\n 'node_type': n['node_type'],\n 'description': n['description'],\n 'total_study_time': n['total_study_time'] or 0,\n 'study_count': n['study_count'] or 0,\n 'test_score': n['test_score'],\n 'test_count': n['test_count'] or 0,\n 'mastery_level': mastery_level,\n 'mastery_text': mastery_texts.get(mastery_level, '待学习'),\n 'last_study_time': n['last_study_time']\n })\n \n return jsonify({\n 'success': True,\n 'data': {\n 'items': result,\n 'total': total,\n 'page': page,\n 'per_page': per_page,\n 'total_pages': (total + per_page - 1) // per_page\n }\n })\n \n except Exception as e:\n print(f\"获取知识点状态列表失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/study-summary')\n @login_required\n def get_study_summary():\n \"\"\"获取学习概况统计(基于真实数据)\"\"\"\n user_id = session.get('user_id')\n \n try:\n with db_manager.get_connection() as conn:\n # 统计今日学习时长\n from datetime import datetime, timedelta\n today = datetime.now().date()\n today_start = datetime.combine(today, datetime.min.time())\n today_end = datetime.combine(today, datetime.max.time())\n \n today_study = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM knowledge_point_study_history\n WHERE user_id = ? AND action_type = 'study'\n AND created_at >= ? AND created_at < ?\n ''', (user_id, today_start.isoformat(), today_end.isoformat())).fetchone()\n \n today_hours = round((today_study['total_duration'] or 0) / 3600.0, 2)\n \n # 统计知识点掌握情况\n mastered_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 2\n ''', (user_id,)).fetchone()[0]\n \n learning_count = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n ''', (user_id,)).fetchone()[0]\n \n # 统计总知识点数\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n pending_count = total_nodes - mastered_count - learning_count\n \n # 统计待复习内容(超过7天未学习的\"学习中\"状态知识点)\n seven_days_ago = datetime.now() - timedelta(days=7)\n need_review = conn.execute('''\n SELECT COUNT(*) FROM knowledge_point_learning\n WHERE user_id = ? AND mastery_level = 1\n AND (last_study_time < ? OR last_study_time IS NULL)\n ''', (user_id, seven_days_ago.isoformat())).fetchone()[0]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'today_study_hours': today_hours,\n 'mastered_count': mastered_count,\n 'learning_count': learning_count,\n 'pending_count': pending_count,\n 'total_nodes': total_nodes,\n 'need_review_count': need_review\n }\n })\n \n except Exception as e:\n print(f\"获取学习概况失败: {e}\")\n return jsonify({'success': False, 'message': str(e)}), 500\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情,并记录一次查看行为供学习时长统计使用\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except Exception:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except Exception:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n\n # 记录一次查看行为(简单估计一次查看 5 分钟 = 300 秒)\n try:\n current_user_id = session.get('user_id')\n if current_user_id:\n db_manager.log_user_behavior(\n user_id=current_user_id,\n action_type='view',\n document_id=doc_id,\n duration=300,\n details={\n 'source': 'document_detail',\n 'title': doc_dict.get('title')\n }\n )\n except Exception as behavior_err:\n print(f\"记录查看行为失败: {behavior_err}\")\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "17947259ab4084ff1d365ef4742a26d772978779": {"t": 1785758516.5863132, "n": 114204, "h": "c4c88bc4920f74f1", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n # 用户行为记录工具方法\n def log_user_behavior(self, user_id: int, action_type: str, document_id: Optional[int] = None,\n duration: Optional[int] = None, details: Optional[Dict[str, Any]] = None) -> None:\n \"\"\"\n 记录用户行为到 user_behaviors 表中\n duration 单位为秒;details 为可选的 JSON 信息\n \"\"\"\n try:\n with self.get_connection() as conn:\n conn.execute(\n '''\n INSERT INTO user_behaviors (user_id, document_id, action_type, duration, details)\n VALUES (?, ?, ?, ?, ?)\n ''',\n (\n user_id,\n document_id,\n action_type,\n duration,\n json.dumps(details, ensure_ascii=False) if details else None\n )\n )\n conn.commit()\n except Exception as e:\n # 打印错误但不影响主流程\n print(f\"记录用户行为失败: {e}\")\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 在保证精确度的前提下适当放宽召回\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配(稍微放宽阈值)\n if query_lower in content:\n # 计算出现次数,要求至少出现1次即可计分\n count = content.count(query_lower)\n if count >= 1:\n score += min(count * 1.2, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数(略微降低阈值,提升召回)\n min_score = 22.0 if is_filename_search else 10.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情,并记录一次查看行为供学习时长统计使用\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except Exception:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except Exception:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n\n # 记录一次查看行为(简单估计一次查看 5 分钟 = 300 秒)\n try:\n current_user_id = session.get('user_id')\n if current_user_id:\n db_manager.log_user_behavior(\n user_id=current_user_id,\n action_type='view',\n document_id=doc_id,\n duration=300,\n details={\n 'source': 'document_detail',\n 'title': doc_dict.get('title')\n }\n )\n except Exception as behavior_err:\n print(f\"记录查看行为失败: {behavior_err}\")\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "1bbeb833877fadce84de926de67ae68654bf8176": {"t": 1785758518.6465569, "n": 112401, "h": "9155b9df93b2a694", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 在保证精确度的前提下适当放宽召回\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配(稍微放宽阈值)\n if query_lower in content:\n # 计算出现次数,要求至少出现1次即可计分\n count = content.count(query_lower)\n if count >= 1:\n score += min(count * 1.2, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数(略微降低阈值,提升召回)\n min_score = 22.0 if is_filename_search else 10.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "c0c7668159be075a285b7f6b31c1d21b671a1a48": {"t": 1785758520.8398232, "n": 112378, "h": "077c644f926b0f4b", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations')\n @login_required\n def get_recommendations():\n \"\"\"获取个性化推荐(概览模块“为您推荐”)\"\"\"\n # 简化处理:基于当前用户最近的文档推荐相似的文档\n # 实际应用中可以接入更复杂的推荐算法或模型\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n # 获取用户最近浏览/学习的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,退化为推荐最新上传的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于标签推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词「{tag}」为你推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except Exception:\n continue\n \n # 如果仍然没有推荐结果,则给出兜底:取最新的几篇文档\n if not recommendations:\n fallback_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 3'\n ).fetchall()\n for doc in fallback_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(doc),\n 'reason': '为你推荐最新上传的文档',\n 'score': 0.5\n })\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs),\n 'user_id': user_id\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "8b8bbe98c8e7414987414597ee04964a17b49457": {"t": 1785758523.1312332, "n": 111766, "h": "787eca327d9db53b", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple, Generator\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for, Response\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n # 使用 type: ignore 避免静态检查在未安装依赖时报“无法解析导入”警告\n import textract # type: ignore\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API(非流式)\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n\n def _stream_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Generator[str, None, None]:\n \"\"\"\n 调用DeepSeek API(流式),逐步返回内容片段\n 注意:DeepSeek 接口与 OpenAI Chat Completions 兼容,支持 stream 模式\n \"\"\"\n if not self.api_key:\n return\n\n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n\n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000,\n \"stream\": True\n }\n\n with requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=60,\n stream=True,\n ) as response:\n if response.status_code != 200:\n print(f\"DeepSeek 流式 API 错误: {response.status_code} - {response.text}\")\n return\n\n for line in response.iter_lines(decode_unicode=True):\n if not line:\n continue\n # DeepSeek 兼容 OpenAI,数据行以 \"data: \" 开头\n if line.startswith(\"data:\"):\n payload = line[len(\"data:\"):].strip()\n else:\n payload = line.strip()\n\n if not payload or payload == \"[DONE]\":\n break\n\n try:\n data = json.loads(payload)\n choices = data.get(\"choices\") or []\n if not choices:\n continue\n delta = choices[0].get(\"delta\") or {}\n content = delta.get(\"content\")\n if content:\n # 返回每一小段内容\n yield content\n except Exception as e:\n print(f\"解析 DeepSeek 流式数据出错: {e}, 原始数据: {payload}\")\n continue\n except Exception as e:\n print(f\"调用 DeepSeek 流式 API 失败: {e}\")\n return\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n\n def stream_chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Generator[str, None, None]:\n \"\"\"与AI助手进行流式对话,逐步返回内容片段\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n\n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n\n if context:\n messages.extend(context)\n\n messages.append({\"role\": \"user\", \"content\": user_message})\n\n # 直接把底层流式 API 的内容片段 yield 出去\n for chunk in self._stream_api(messages):\n if chunk:\n yield chunk\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手(非流式)\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n\n @app.route('/api/ai-assistant/chat-stream', methods=['POST'])\n def ai_chat_stream():\n \"\"\"AI 对话接口(流式响应)\"\"\"\n try:\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n\n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n\n context = data.get('context', [])\n\n def generate():\n # 逐步把模型返回的内容片段写给前端\n first_chunk = True\n for chunk in ai_assistant.stream_chat(user_message, context):\n if not chunk:\n continue\n # 为了尽量减少前端解析负担,这里直接返回纯文本流\n # 浏览器端累积这些片段即可\n yield chunk\n first_chunk = False\n\n return Response(generate(), mimetype='text/plain; charset=utf-8')\n except Exception as e:\n print(f\"AI 流式对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理流式请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "704f8b84ac8507e63cec983134f73a4af33e91c8": {"t": 1785758524.8097038, "n": 107383, "h": "8bc462a7a340fc64", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n import textract\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"AI对话错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n # 检查AI助手是否可用\n if not ALIYUN_API_KEY:\n return jsonify({\n 'success': False,\n 'message': 'AI学习助手功能未配置,请在config.py中设置ALIYUN_API_KEY'\n }), 503\n \n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n print(f\"生成复习计划错误: {str(e)}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "a47b8c36343761f84fa9fc66c6d70ebfb51f8655": {"t": 1785758527.6802015, "n": 106693, "h": "205e2143db44ceca", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n import textract\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天'\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "e6c2a3adb7f162c43098cdf98d213271f4c9ef4a": {"t": 1785758529.4227068, "n": 107429, "h": "cc9fedce379b2e32", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory, session, redirect, url_for\nfrom flask_cors import CORS\nfrom functools import wraps\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\nimport hashlib as hash_lib\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 尝试导入textract库(支持多种格式,包括.doc)\ntry:\n import textract\n TEXTTRACT_AVAILABLE = True\nexcept ImportError:\n TEXTTRACT_AVAILABLE = False\n print(\"提示:未安装textract库,.doc格式文档处理功能将受限\")\n\n# 尝试导入win32com(Windows系统支持.doc格式)\ntry:\n import win32com.client\n WIN32COM_AVAILABLE = True\nexcept ImportError:\n WIN32COM_AVAILABLE = False\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档(.docx)提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_doc(file_path: str) -> str:\n \"\"\"从Word文档(.doc)提取内容(旧格式)\"\"\"\n # 方法1: 尝试使用textract\n if TEXTTRACT_AVAILABLE:\n try:\n text = textract.process(file_path).decode('utf-8')\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"textract处理失败: {e}\")\n \n # 方法2: 尝试使用win32com (Windows)\n if WIN32COM_AVAILABLE:\n try:\n word = win32com.client.Dispatch(\"Word.Application\")\n word.Visible = False\n # 转换为绝对路径\n abs_path = os.path.abspath(file_path)\n doc = word.Documents.Open(abs_path)\n text = doc.Content.Text\n doc.Close(False) # False表示不保存\n word.Quit()\n if text and text.strip():\n return text\n except Exception as e:\n print(f\"win32com处理失败: {e}\")\n try:\n # 确保Word应用被关闭\n word.Quit()\n except:\n pass\n \n # 如果都不可用,返回友好的提示信息\n return \"[.doc格式文档需要特殊处理库。\\n\\n解决方案:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式后重新上传\\n\\n注意:.doc是旧版Word格式,python-docx库无法处理。]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text_from_ppt(file_path: str) -> str:\n \"\"\"从PPT文件提取内容(暂不支持文本提取)\"\"\"\n return \"[PPT文件暂不支持文本提取,仅支持上传和下载]\"\n \n @staticmethod\n def extract_text(file_path: str, file_type: str = None) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n # 如果未提供file_type,从文件路径自动判断\n if not file_type:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n else:\n file_type = file_type.lower()\n \n # 确保文件存在\n if not os.path.exists(file_path):\n return f\"[文件不存在: {file_path}]\"\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type == 'docx':\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type == 'doc':\n return DocumentProcessor.extract_text_from_doc(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n elif file_type in ['ppt', 'pptx']:\n return DocumentProcessor.extract_text_from_ppt(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS users (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT UNIQUE NOT NULL,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_login TIMESTAMP\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id INTEGER NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id),\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False,生产环境应设为True\n app.config['SESSION_COOKIE_HTTPONLY'] = True\n app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 认证装饰器\n def login_required(f):\n @wraps(f)\n def decorated_function(*args, **kwargs):\n if 'user_id' not in session:\n # 如果是 API 请求,返回 JSON 错误\n if request.path.startswith('/api/'):\n return jsonify({\n 'success': False,\n 'message': '请先登录',\n 'requires_login': True\n }), 401\n # 否则重定向到登录页\n return redirect(url_for('login'))\n return f(*args, **kwargs)\n return decorated_function\n \n # 密码哈希函数\n def hash_password(password: str) -> str:\n \"\"\"生成密码哈希\"\"\"\n return hash_lib.sha256(password.encode('utf-8')).hexdigest()\n \n # 验证密码\n def verify_password(password: str, password_hash: str) -> bool:\n \"\"\"验证密码\"\"\"\n return hash_password(password) == password_hash\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n @login_required\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/login')\n def login():\n \"\"\"登录页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('login.html')\n \n @app.route('/register')\n def register():\n \"\"\"注册页面\"\"\"\n # 如果已登录,重定向到主页\n if 'user_id' in session:\n return redirect(url_for('index'))\n return render_template('register.html')\n \n @app.route('/api/auth/login', methods=['POST'])\n def api_login():\n \"\"\"登录API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n password = data.get('password', '').strip()\n \n if not username or not password:\n return jsonify({\n 'success': False,\n 'message': '用户名和密码不能为空'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 查找用户(支持用户名或邮箱登录)\n user = conn.execute('''\n SELECT id, username, email, password_hash \n FROM users \n WHERE username = ? OR email = ?\n ''', (username, username)).fetchone()\n \n if not user:\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 验证密码\n if not verify_password(password, user['password_hash']):\n return jsonify({\n 'success': False,\n 'message': '用户名或密码错误'\n }), 401\n \n # 更新最后登录时间\n conn.execute('''\n UPDATE users \n SET last_login = CURRENT_TIMESTAMP \n WHERE id = ?\n ''', (user['id'],))\n conn.commit()\n \n # 设置会话\n session['user_id'] = user['id']\n session['username'] = user['username']\n session['email'] = user['email']\n \n return jsonify({\n 'success': True,\n 'message': '登录成功',\n 'user': {\n 'id': user['id'],\n 'username': user['username'],\n 'email': user['email']\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'登录失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/register', methods=['POST'])\n def api_register():\n \"\"\"注册API\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n username = data.get('username', '').strip()\n email = data.get('email', '').strip()\n password = data.get('password', '').strip()\n \n # 验证输入\n if not username or not email or not password:\n return jsonify({\n 'success': False,\n 'message': '所有字段都不能为空'\n }), 400\n \n if len(password) < 6:\n return jsonify({\n 'success': False,\n 'message': '密码至少需要6个字符'\n }), 400\n \n # 验证邮箱格式\n import re\n email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'\n if not re.match(email_pattern, email):\n return jsonify({\n 'success': False,\n 'message': '邮箱格式不正确'\n }), 400\n \n with db_manager.get_connection() as conn:\n # 检查用户名是否已存在\n existing_user = conn.execute('''\n SELECT id FROM users WHERE username = ?\n ''', (username,)).fetchone()\n \n if existing_user:\n return jsonify({\n 'success': False,\n 'message': '用户名已存在'\n }), 400\n \n # 检查邮箱是否已存在\n existing_email = conn.execute('''\n SELECT id FROM users WHERE email = ?\n ''', (email,)).fetchone()\n \n if existing_email:\n return jsonify({\n 'success': False,\n 'message': '邮箱已被注册'\n }), 400\n \n # 创建新用户\n password_hash = hash_password(password)\n cursor = conn.execute('''\n INSERT INTO users (username, email, password_hash)\n VALUES (?, ?, ?)\n ''', (username, email, password_hash))\n conn.commit()\n \n user_id = cursor.lastrowid\n \n return jsonify({\n 'success': True,\n 'message': '注册成功',\n 'user': {\n 'id': user_id,\n 'username': username,\n 'email': email\n }\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'注册失败: {str(e)}'\n }), 500\n \n @app.route('/api/auth/logout', methods=['POST'])\n @login_required\n def api_logout():\n \"\"\"登出API\"\"\"\n session.clear()\n return jsonify({\n 'success': True,\n 'message': '已登出'\n })\n \n @app.route('/api/auth/check', methods=['GET'])\n def api_check_auth():\n \"\"\"检查认证状态\"\"\"\n if 'user_id' in session:\n return jsonify({\n 'success': True,\n 'authenticated': True,\n 'user': {\n 'id': session.get('user_id'),\n 'username': session.get('username'),\n 'email': session.get('email')\n }\n })\n else:\n return jsonify({\n 'success': True,\n 'authenticated': False\n })\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n @login_required\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc', 'ppt', 'pptx']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n @login_required\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n @login_required\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n @login_required\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = session.get('user_id', 1)\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ?\n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_edges < 30:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if total_docs > 0 and total_nodes > 0 and total_edges == 0:\n recommendations.append('为知识点建立关联关系,形成知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n full_content = None\n # 首先尝试从文件重新提取\n if doc['file_path'] and os.path.exists(doc['file_path']):\n try:\n # 从文件路径自动判断文件类型,确保正确\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n extracted_content = doc_manager.doc_processor.extract_text(doc['file_path'], actual_file_type)\n \n # 如果提取的内容不是错误信息(不以[开头),则使用它\n if extracted_content and not extracted_content.strip().startswith('['):\n full_content = extracted_content\n else:\n # 如果提取失败,尝试使用数据库中存储的内容\n print(f\"文件提取返回错误信息: {extracted_content[:100]}\")\n except Exception as e:\n print(f\"从文件提取内容失败: {e}\")\n import traceback\n traceback.print_exc()\n \n # 如果提取失败,使用数据库中存储的内容\n if not full_content:\n stored_content = doc['content'] or ''\n # 如果存储的内容也是错误信息,尝试重新处理\n if stored_content.startswith('[') and doc['file_path'] and os.path.exists(doc['file_path']):\n # 对于.doc文件,提供更友好的提示\n actual_file_type = os.path.splitext(doc['file_path'])[1][1:].lower()\n if actual_file_type == 'doc':\n full_content = \"[.doc格式文档需要特殊处理库。建议:\\n1. 安装textract库: pip install textract\\n2. 或在Windows系统上安装pywin32: pip install pywin32\\n3. 或将文档转换为.docx格式]\"\n else:\n full_content = stored_content\n else:\n full_content = stored_content or '无内容'\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n else:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n else:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n except Exception as e:\n print(f\"获取文档详情失败: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'获取文档详情失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>/download')\n @login_required\n def download_document(doc_id):\n \"\"\"下载文档\"\"\"\n try:\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT file_path, title, file_type FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n \n if not file_path or not os.path.exists(file_path):\n return jsonify({\n 'success': False,\n 'message': '文件不存在'\n }), 404\n \n # 获取原始文件名或使用标题\n original_filename = doc['title']\n if not original_filename.endswith('.' + doc['file_type']):\n original_filename = f\"{original_filename}.{doc['file_type']}\"\n \n return send_file(\n file_path,\n as_attachment=True,\n download_name=original_filename,\n mimetype='application/octet-stream'\n )\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'下载失败: {str(e)}'\n }), 500\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n cursor = conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = cursor.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n cursor = conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n deleted_edges = cursor.rowcount\n if deleted_edges > 0:\n print(f\"已删除 {deleted_edges} 条关联的知识边\")\n \n # 5. 删除数据库中的记录(硬删除:彻底从数据库中删除)\n cursor = conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n deleted_doc = cursor.rowcount\n \n if deleted_doc == 0:\n return jsonify({\n 'success': False,\n 'message': '文档删除失败,记录不存在'\n }), 404\n \n # 提交事务,确保所有删除操作生效\n conn.commit()\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已彻底删除'\n })\n except Exception as e:\n print(f\"删除文档时发生错误: {e}\")\n import traceback\n traceback.print_exc()\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "081f1581382d44c29708a532560d8943957565df": {"t": 1785758531.5528584, "n": 92616, "h": "560cd44c1ec55db7", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\nimport requests\n\n# 导入配置文件\ntry:\n from config import ALIYUN_API_KEY\nexcept ImportError:\n ALIYUN_API_KEY = None\n print(\"警告:未找到 config.py 文件,AI 学习助手功能将不可用\")\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# AI学习助手类\nclass AIAssistant:\n \"\"\"AI学习助手类 - 使用DeepSeek API提供智能问答和学习建议\"\"\"\n \n def __init__(self, api_key: str = None):\n self.api_key = api_key or ALIYUN_API_KEY\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.model = \"deepseek-chat\"\n \n def _call_api(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> Optional[str]:\n \"\"\"调用DeepSeek API\"\"\"\n if not self.api_key:\n return None\n \n try:\n headers = {\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\"\n }\n \n data = {\n \"model\": self.model,\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": 2000\n }\n \n response = requests.post(\n self.api_url,\n headers=headers,\n json=data,\n timeout=30\n )\n \n if response.status_code == 200:\n result = response.json()\n if 'choices' in result and len(result['choices']) > 0:\n return result['choices'][0]['message']['content']\n else:\n print(f\"DeepSeek API 错误: {response.status_code} - {response.text}\")\n return None\n \n except Exception as e:\n print(f\"调用DeepSeek API失败: {str(e)}\")\n return None\n \n def chat(self, user_message: str, context: List[Dict[str, str]] = None) -> Dict[str, Any]:\n \"\"\"与AI助手对话\"\"\"\n system_prompt = \"\"\"你是一个专业的学习助手,专门帮助学生进行知识管理和学习。\n你的职责包括:\n1. 回答学生关于学习内容的问题\n2. 提供学习建议和学习路径规划\n3. 解释复杂的概念和知识点\n4. 帮助学生理解知识之间的关联\n5. 提供复习建议和记忆技巧\n\n请用中文回答,回答要简洁明了、专业准确。\"\"\"\n \n messages = [{\"role\": \"system\", \"content\": system_prompt}]\n \n # 添加上下文对话历史\n if context:\n messages.extend(context)\n \n # 添加当前用户消息\n messages.append({\"role\": \"user\", \"content\": user_message})\n \n response = self._call_api(messages)\n \n if response:\n return {\n \"success\": True,\n \"message\": response,\n \"model\": self.model\n }\n else:\n return {\n \"success\": False,\n \"message\": \"抱歉,AI服务暂时不可用,请稍后再试。\",\n \"error\": \"API调用失败\"\n }\n \n def get_learning_suggestion(self, topic: str, user_documents: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"获取学习建议\"\"\"\n context_info = \"\"\n if user_documents:\n doc_titles = [doc.get('title', '') for doc in user_documents[:5]]\n context_info = f\"\\n用户已学习的文档包括:{', '.join(doc_titles)}\"\n \n prompt = f\"\"\"请为以下学习主题提供详细的学习建议和学习路径:\n主题:{topic}\n{context_info}\n\n请提供:\n1. 学习路径规划(从基础到高级)\n2. 重点知识点\n3. 推荐的学习资源类型\n4. 学习时间安排建议\n5. 实践建议\"\"\"\n \n return self.chat(prompt)\n \n def explain_concept(self, concept: str, related_docs: List[Dict] = None) -> Dict[str, Any]:\n \"\"\"解释概念\"\"\"\n context_info = \"\"\n if related_docs:\n doc_content = related_docs[0].get('content', '')[:500] if related_docs else \"\"\n context_info = f\"\\n相关文档内容片段:{doc_content}\"\n \n prompt = f\"\"\"请详细解释以下概念:\n概念:{concept}\n{context_info}\n\n请用通俗易懂的方式解释,包括:\n1. 基本定义\n2. 核心要点\n3. 实际应用\n4. 与其他概念的关系\"\"\"\n \n return self.chat(prompt)\n \n def generate_review_plan(self, knowledge_points: List[str]) -> Dict[str, Any]:\n \"\"\"生成复习计划\"\"\"\n points_str = \"\\n\".join([f\"- {point}\" for point in knowledge_points])\n \n prompt = f\"\"\"根据以下知识点,制定一个合理的复习计划:\n{points_str}\n\n请提供:\n1. 复习时间安排(基于艾宾浩斯遗忘曲线)\n2. 每个知识点的复习重点\n3. 复习方法建议\n4. 自测题目建议\"\"\"\n \n return self.chat(prompt)\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n ai_assistant = AIAssistant()\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = request.args.get('user_id', '1')\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_edges < 30:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if total_docs > 0 and total_nodes > 0 and total_edges == 0:\n recommendations.append('为知识点建立关联关系,形成知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n try:\n full_content = doc_manager.doc_processor.extract_text(doc['file_path'], doc['file_type'])\n except:\n full_content = doc['content']\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = conn.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n \n # 5. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已删除'\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n # AI学习助手API\n @app.route('/api/ai-assistant/chat', methods=['POST'])\n def ai_chat():\n \"\"\"AI对话接口\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n user_message = data.get('message', '').strip()\n \n if not user_message:\n return jsonify({\n 'success': False,\n 'message': '消息内容不能为空'\n }), 400\n \n # 获取对话历史(可选)\n context = data.get('context', [])\n \n # 调用AI助手\n result = ai_assistant.chat(user_message, context)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/suggestion', methods=['POST'])\n def ai_suggestion():\n \"\"\"获取学习建议\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n topic = data.get('topic', '').strip()\n \n if not topic:\n return jsonify({\n 'success': False,\n 'message': '学习主题不能为空'\n }), 400\n \n # 获取用户文档列表(可选)\n user_documents = []\n if data.get('include_documents', False):\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT id, title, content FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 10'\n ).fetchall()\n user_documents = [dict(doc) for doc in docs]\n \n # 调用AI助手获取建议\n result = ai_assistant.get_learning_suggestion(topic, user_documents)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/explain', methods=['POST'])\n def ai_explain():\n \"\"\"解释概念\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n concept = data.get('concept', '').strip()\n \n if not concept:\n return jsonify({\n 'success': False,\n 'message': '概念名称不能为空'\n }), 400\n \n # 查找相关文档\n related_docs = []\n if data.get('search_documents', True):\n results = doc_manager.search_documents(concept, limit=3)\n related_docs = results\n \n # 调用AI助手解释概念\n result = ai_assistant.explain_concept(concept, related_docs)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n @app.route('/api/ai-assistant/review-plan', methods=['POST'])\n def ai_review_plan():\n \"\"\"生成复习计划\"\"\"\n try:\n data = request.get_json(force=True, silent=True) or {}\n knowledge_points = data.get('knowledge_points', [])\n \n if not knowledge_points or not isinstance(knowledge_points, list):\n return jsonify({\n 'success': False,\n 'message': '知识点列表不能为空'\n }), 400\n \n # 调用AI助手生成复习计划\n result = ai_assistant.generate_review_plan(knowledge_points)\n \n return jsonify(result)\n \n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'处理请求失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "77a1bec31fbb4aeef70156894f291ca829338a10": {"t": 1785758533.598389, "n": 84746, "h": "e0ae28692a08208d", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 知识图谱方案表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_graph_schemes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_default BOOLEAN DEFAULT 0\n )\n ''')\n \n # 为知识节点表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 为知识边表添加scheme_id字段\n try:\n conn.execute('ALTER TABLE knowledge_edges ADD COLUMN scheme_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 创建默认方案(如果不存在)\n default_scheme = conn.execute('SELECT id FROM knowledge_graph_schemes WHERE is_default = 1').fetchone()\n if not default_scheme:\n conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description, is_default)\n VALUES ('默认方案', '系统默认的知识图谱方案', 1)\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self.current_scheme_id = None\n self._load_default_scheme()\n self._load_graph_from_db()\n \n def _load_default_scheme(self):\n \"\"\"加载默认方案ID\"\"\"\n try:\n with self.db.get_connection() as conn:\n default_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE is_default = 1 LIMIT 1'\n ).fetchone()\n if default_scheme:\n self.current_scheme_id = default_scheme['id']\n else:\n # 如果没有默认方案,获取第一个方案\n first_scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes ORDER BY id LIMIT 1'\n ).fetchone()\n if first_scheme:\n self.current_scheme_id = first_scheme['id']\n except Exception as e:\n print(f\"加载默认方案时出错: {e}\")\n \n def set_current_scheme(self, scheme_id: int):\n \"\"\"设置当前方案\"\"\"\n self.current_scheme_id = scheme_id\n self._load_graph_from_db()\n \n def _load_graph_from_db(self, scheme_id: int = None):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点和指定方案的节点\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return\n \n try:\n with self.db.get_connection() as conn:\n # 加载来自未删除文档的所有类型节点和当前方案的节点\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n self.graph.clear()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边,且属于当前方案\n all_edges = conn.execute('''\n SELECT * FROM knowledge_edges \n WHERE (scheme_id = ? OR scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None, scheme_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (name, node_type, description, document_id, scheme_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0, scheme_id: int = None) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return -1\n \n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight, scheme_id) VALUES (?, ?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight, scheme_id)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self, scheme_id: int = None) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 返回来自已上传且未删除文件的所有类型节点\n 确保每个节点名称只出现一次(去重)\n \"\"\"\n if scheme_id is None:\n scheme_id = self.current_scheme_id\n \n if scheme_id is None:\n return {'nodes': [], 'edges': []}\n \n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(所有类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个节点名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND (kn.scheme_id = ? OR kn.scheme_id IS NULL)\n GROUP BY kn.name, kn.node_type\n ''', (scheme_id,)).fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个节点名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n AND (ke.scheme_id = ? OR ke.scheme_id IS NULL)\n ''', (scheme_id,)).fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n scheme_id = request.args.get('scheme_id', type=int)\n graph_data = kg_manager.get_graph_data(scheme_id)\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges']),\n 'scheme_id': scheme_id or kg_manager.current_scheme_id\n })\n \n # 知识图谱方案管理\n @app.route('/api/kg/schemes', methods=['GET', 'POST'])\n def kg_schemes():\n \"\"\"获取方案列表或创建新方案\"\"\"\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('''\n SELECT * FROM knowledge_graph_schemes \n ORDER BY is_default DESC, created_at DESC\n ''').fetchall()\n schemes = [dict(r) for r in rows]\n return jsonify({\n 'success': True, \n 'data': schemes,\n 'current_scheme_id': kg_manager.current_scheme_id\n })\n else:\n # 创建新方案\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n description = data.get('description', '').strip()\n if not name:\n return jsonify({'success': False, 'message': '方案名称不能为空'}), 400\n \n with db_manager.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO knowledge_graph_schemes (name, description)\n VALUES (?, ?)\n ''', (name, description))\n scheme_id = cursor.lastrowid\n return jsonify({'success': True, 'id': scheme_id, 'scheme_id': scheme_id})\n \n @app.route('/api/kg/schemes/<int:scheme_id>', methods=['PUT', 'DELETE'])\n def kg_scheme_detail(scheme_id):\n \"\"\"更新或删除方案\"\"\"\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(scheme_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_graph_schemes SET {\", \".join(fields)} WHERE id = ?', params)\n return jsonify({'success': True})\n else:\n # 删除方案(同时删除关联的节点和边)\n with db_manager.get_connection() as conn:\n # 获取要删除的节点ID\n node_ids = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE scheme_id = ?',\n (scheme_id,)\n ).fetchall()\n node_id_list = [n['id'] for n in node_ids]\n \n # 删除关联的边\n if node_id_list:\n placeholders = ','.join(['?'] * len(node_id_list))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_id_list + node_id_list\n )\n \n # 删除节点\n conn.execute('DELETE FROM knowledge_nodes WHERE scheme_id = ?', (scheme_id,))\n \n # 删除方案\n conn.execute('DELETE FROM knowledge_graph_schemes WHERE id = ?', (scheme_id,))\n \n # 如果删除的是当前方案,切换到默认方案\n if kg_manager.current_scheme_id == scheme_id:\n kg_manager._load_default_scheme()\n kg_manager._load_graph_from_db()\n \n return jsonify({'success': True})\n \n @app.route('/api/kg/schemes/<int:scheme_id>/switch', methods=['POST'])\n def kg_switch_scheme(scheme_id):\n \"\"\"切换当前方案\"\"\"\n with db_manager.get_connection() as conn:\n scheme = conn.execute(\n 'SELECT id FROM knowledge_graph_schemes WHERE id = ?',\n (scheme_id,)\n ).fetchone()\n if not scheme:\n return jsonify({'success': False, 'message': '方案不存在'}), 404\n \n kg_manager.set_current_scheme(scheme_id)\n return jsonify({\n 'success': True,\n 'scheme_id': scheme_id,\n 'message': '方案切换成功'\n })\n \n @app.route('/api/kg/current-scheme', methods=['GET'])\n def kg_current_scheme():\n \"\"\"获取当前方案\"\"\"\n return jsonify({\n 'success': True,\n 'scheme_id': kg_manager.current_scheme_id\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_nodes WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n scheme_id = request.args.get('scheme_id', type=int) or kg_manager.current_scheme_id\n with db_manager.get_connection() as conn:\n if scheme_id:\n rows = conn.execute(\n 'SELECT * FROM knowledge_edges WHERE scheme_id = ? OR scheme_id IS NULL ORDER BY id DESC',\n (scheme_id,)\n ).fetchall()\n else:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n scheme_id = data.get('scheme_id') or kg_manager.current_scheme_id\n if not scheme_id:\n return jsonify({'success': False, 'message': '请先选择一个方案'}), 400\n edge_id = kg_manager.add_knowledge_edge(source_id, target_id, relation_type, weight, scheme_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\n 搜索节点时,显示与该节点建立了关系的节点以及它们之间的关系\n \"\"\"\n query = request.args.get('q', '')\n scheme_id = request.args.get('scheme_id', type=int)\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data(scheme_id)\n \n # 第一步:过滤包含搜索词的节点(不区分大小写)\n matched_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取匹配节点的ID集合(统一转换为字符串)\n matched_node_ids = {str(node['id']) for node in matched_nodes}\n \n # 如果没有匹配的节点,返回空结果\n if not matched_node_ids:\n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': [],\n 'edges': []\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': 0,\n 'related_count': 0\n })\n \n # 第二步:找到所有与匹配节点有关系的边\n related_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in matched_node_ids or str(edge['target']) in matched_node_ids\n ]\n \n # 第三步:收集所有相关节点的ID(包括匹配节点和与它们有关系的节点)\n related_node_ids = set(matched_node_ids)\n for edge in related_edges:\n related_node_ids.add(str(edge['source']))\n related_node_ids.add(str(edge['target']))\n \n # 第四步:获取所有相关节点的完整信息\n all_related_nodes = [\n node for node in graph_data['nodes']\n if str(node['id']) in related_node_ids\n ]\n \n # 第五步:获取所有相关节点之间的关系(包括相关节点之间的边)\n # 过滤边:只保留两个端点都在相关节点集合中的边\n final_edges = [\n edge for edge in graph_data['edges']\n if str(edge['source']) in related_node_ids and str(edge['target']) in related_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': all_related_nodes,\n 'edges': final_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'matched_count': len(matched_nodes),\n 'related_count': len(all_related_nodes) - len(matched_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n def get_study_time():\n \"\"\"获取学习时长统计(基于真实用户行为数据)\"\"\"\n period = request.args.get('period', 'week')\n user_id = request.args.get('user_id', '1')\n \n with db_manager.get_connection() as conn:\n if period == 'week':\n # 获取最近7天的学习时长\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(6, -1, -1): # 从6天前到今天\n target_date = today - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n # 查询该天的学习时长(秒转小时)\n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n elif period == 'month':\n # 获取最近4周的学习时长\n labels = [f'第{i}周' for i in range(1, 5)]\n values = []\n from datetime import datetime, timedelta\n today = datetime.now().date()\n \n for i in range(3, -1, -1): # 最近4周\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n start_time = datetime.combine(week_start, datetime.min.time())\n end_time = datetime.combine(week_end, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n else: # quarter\n # 获取最近3个月的学习时长\n labels = ['第1月', '第2月', '第3月']\n values = []\n from datetime import datetime, timedelta\n \n today = datetime.now()\n for i in range(2, -1, -1): # 最近3个月\n # 计算月份开始和结束\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n month_start = datetime(year, month, 1)\n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE user_id = ? \n AND timestamp >= ? AND timestamp <= ?\n AND action_type IN ('view', 'read', 'study')\n ''', (user_id, month_start.isoformat(), month_end.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n values.append(hours)\n \n # 计算总时长和平均时长\n total_hours = sum(values)\n avg_hours = round(total_hours / len(values), 1) if values else 0.0\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period,\n 'total_hours': total_hours,\n 'avg_hours': avg_hours\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(统计来自已上传文件的所有类型节点)\"\"\"\n with db_manager.get_connection() as conn:\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 统计来自未删除文档的所有类型节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n \n # 统计关系数量\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点,300个关系\n target_docs = 100\n target_nodes = 200\n target_edges = 300\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n edge_progress = min(100, int((total_edges / target_edges) * 100)) if target_edges > 0 else 0\n overall_progress = int((doc_progress + node_progress + edge_progress) / 3)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_edges < 30:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if total_docs > 0 and total_nodes > 0 and total_edges == 0:\n recommendations.append('为知识点建立关联关系,形成知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'doc_progress': doc_progress,\n 'node_progress': node_progress,\n 'edge_progress': edge_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/analytics/progress')\n def get_learning_progress():\n \"\"\"获取学习进度跟踪\"\"\"\n with db_manager.get_connection() as conn:\n # 获取文档统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n \n # 获取节点统计(按类型分组)\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 获取关系统计\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 获取最近的学习活动\n recent_activities = conn.execute('''\n SELECT action_type, COUNT(*) as count, MAX(timestamp) as last_time\n FROM user_behaviors\n WHERE action_type IN ('view', 'read', 'study', 'upload')\n GROUP BY action_type\n ORDER BY last_time DESC\n LIMIT 5\n ''').fetchall()\n \n # 计算学习天数\n first_doc = conn.execute('''\n SELECT MIN(created_at) as first_date\n FROM documents\n WHERE is_deleted = 0\n ''').fetchone()\n \n learning_days = 0\n if first_doc and first_doc['first_date']:\n from datetime import datetime\n try:\n if isinstance(first_doc['first_date'], str):\n first_date = datetime.fromisoformat(first_doc['first_date'].replace('Z', '+00:00'))\n else:\n first_date = first_doc['first_date']\n learning_days = (datetime.now() - first_date).days + 1\n except:\n learning_days = 1\n \n return jsonify({\n 'success': True,\n 'data': {\n 'total_docs': total_docs,\n 'total_nodes': sum(n['count'] for n in nodes_by_type),\n 'nodes_by_type': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'total_edges': total_edges,\n 'learning_days': learning_days,\n 'recent_activities': [{'type': a['action_type'], 'count': a['count'], 'last_time': a['last_time']} for a in recent_activities]\n }\n })\n \n @app.route('/api/analytics/growth')\n def get_knowledge_growth():\n \"\"\"获取知识网络成长轨迹\"\"\"\n period = request.args.get('period', 'month') # month, week, day\n with db_manager.get_connection() as conn:\n from datetime import datetime, timedelta\n \n if period == 'day':\n # 最近30天的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n for i in range(29, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n date_str = target_date.strftime('%m-%d')\n labels.append(date_str)\n \n # 统计到该日期为止的累计数量\n end_time = datetime.combine(target_date, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n elif period == 'week':\n # 最近12周的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now().date()\n for i in range(11, -1, -1):\n week_start = today - timedelta(days=today.weekday() + 7 * i)\n week_end = week_start + timedelta(days=6)\n labels.append(f'{week_start.strftime(\"%m-%d\")}')\n \n end_time = datetime.combine(week_end, datetime.max.time())\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (end_time.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n else: # month\n # 最近12个月的成长轨迹\n labels = []\n node_counts = []\n edge_counts = []\n doc_counts = []\n \n today = datetime.now()\n for i in range(11, -1, -1):\n month = today.month - i\n year = today.year\n if month <= 0:\n month += 12\n year -= 1\n \n labels.append(f'{year}-{month:02d}')\n \n if month == 12:\n month_end = datetime(year + 1, 1, 1) - timedelta(days=1)\n else:\n month_end = datetime(year, month + 1, 1) - timedelta(days=1)\n \n if i == 0:\n month_end = today\n \n node_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n edge_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM knowledge_edges\n WHERE created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n doc_count = conn.execute('''\n SELECT COUNT(*) as count\n FROM documents\n WHERE is_deleted = 0 AND created_at <= ?\n ''', (month_end.isoformat(),)).fetchone()[0]\n \n node_counts.append(node_count)\n edge_counts.append(edge_count)\n doc_counts.append(doc_count)\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'node_counts': node_counts,\n 'edge_counts': edge_counts,\n 'doc_counts': doc_counts,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/report')\n def generate_learning_report():\n \"\"\"生成学习报告\"\"\"\n report_type = request.args.get('type', 'full') # full, summary, detailed\n with db_manager.get_connection() as conn:\n from datetime import datetime\n \n # 基础统计\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n ''').fetchone()[0]\n total_edges = conn.execute('SELECT COUNT(*) FROM knowledge_edges').fetchone()[0]\n \n # 节点类型分布\n nodes_by_type = conn.execute('''\n SELECT node_type, COUNT(*) as count\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n GROUP BY node_type\n ''').fetchall()\n \n # 最近活动\n recent_docs = conn.execute('''\n SELECT title, created_at, file_type\n FROM documents\n WHERE is_deleted = 0\n ORDER BY created_at DESC\n LIMIT 5\n ''').fetchall()\n \n # 学习时长统计(最近7天)\n from datetime import timedelta\n study_time_data = []\n for i in range(6, -1, -1):\n target_date = datetime.now().date() - timedelta(days=i)\n start_time = datetime.combine(target_date, datetime.min.time())\n end_time = datetime.combine(target_date, datetime.max.time())\n \n result = conn.execute('''\n SELECT COALESCE(SUM(duration), 0) as total_duration\n FROM user_behaviors\n WHERE timestamp >= ? AND timestamp < ?\n AND action_type IN ('view', 'read', 'study')\n ''', (start_time.isoformat(), end_time.isoformat())).fetchone()\n \n hours = round(result['total_duration'] / 3600.0, 1) if result['total_duration'] else 0.0\n study_time_data.append({\n 'date': target_date.strftime('%Y-%m-%d'),\n 'hours': hours\n })\n \n # 生成报告\n report = {\n 'generated_at': datetime.now().isoformat(),\n 'summary': {\n 'total_documents': total_docs,\n 'total_knowledge_nodes': total_nodes,\n 'total_relationships': total_edges,\n 'knowledge_network_density': round(total_edges / total_nodes, 2) if total_nodes > 0 else 0\n },\n 'node_distribution': [{'type': n['node_type'], 'count': n['count']} for n in nodes_by_type],\n 'recent_documents': [{'title': d['title'], 'date': d['created_at'], 'type': d['file_type']} for d in recent_docs],\n 'study_time_week': study_time_data,\n 'total_study_hours': round(sum(s['hours'] for s in study_time_data), 1)\n }\n \n return jsonify({\n 'success': True,\n 'data': report\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n try:\n full_content = doc_manager.doc_processor.extract_text(doc['file_path'], doc['file_type'])\n except:\n full_content = doc['content']\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(所有类型)\n conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ?',\n (doc_id,)\n )\n deleted_nodes = conn.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n \n # 5. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已删除'\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "96f0bbeaa6f2124f434b4648f42dedfc81df9814": {"t": 1785758535.3025017, "n": 59081, "h": "61e60779a406d106", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self._load_graph_from_db()\n \n def _load_graph_from_db(self):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n # 只加载来自未删除文档的节点(concept类型)\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchall()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边\n all_edges = conn.execute('SELECT * FROM knowledge_edges').fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str = 'concept', description: str = None, document_id: int = None) -> int:\n \"\"\"添加知识节点(用户手动管理)\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id) VALUES (?, ?, ?, ?)',\n (name, node_type, description, document_id)\n )\n node_id = cursor.lastrowid\n\n # 内存图更新\n self.graph.add_node(\n node_id,\n name=name,\n node_type=node_type,\n description=description,\n document_id=document_id,\n frequency=1\n )\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 只返回来自已上传且未删除文件的节点\n 确保每个concept名称只出现一次(去重)\n \"\"\"\n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(对于concept类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个concept名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n GROUP BY kn.name, kn.node_type\n ''').fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个concept名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE kn1.node_type = 'concept' AND kn2.node_type = 'concept'\n AND (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n ''').fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n graph_data = kg_manager.get_graph_data()\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges'])\n })\n \n # 知识图谱:节点列表/创建\n @app.route('/api/kg/nodes', methods=['GET', 'POST'])\n def kg_nodes():\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('SELECT * FROM knowledge_nodes ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n name = data.get('name', '').strip()\n node_type = (data.get('type') or 'concept').strip() or 'concept'\n description = data.get('description')\n document_id = data.get('document_id')\n if not name:\n return jsonify({'success': False, 'message': '节点名称不能为空'}), 400\n node_id = kg_manager.add_knowledge_node(name, node_type, description, document_id)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': node_id})\n\n # 知识图谱:节点更新/删除\n @app.route('/api/kg/nodes/<int:node_id>', methods=['PUT', 'DELETE'])\n def kg_node_detail(node_id):\n if request.method == 'PUT':\n data = request.get_json(force=True, silent=True) or {}\n fields = []\n params = []\n if 'name' in data:\n fields.append('name = ?')\n params.append(data.get('name'))\n if 'type' in data:\n fields.append('node_type = ?')\n params.append(data.get('type'))\n if 'description' in data:\n fields.append('description = ?')\n params.append(data.get('description'))\n if 'document_id' in data:\n fields.append('document_id = ?')\n params.append(data.get('document_id'))\n if not fields:\n return jsonify({'success': False, 'message': '没有更新字段'}), 400\n params.append(node_id)\n with db_manager.get_connection() as conn:\n conn.execute(f'UPDATE knowledge_nodes SET {\", \".join(fields)} WHERE id = ?', params)\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n else:\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE source_id = ? OR target_id = ?', (node_id, node_id))\n conn.execute('DELETE FROM knowledge_nodes WHERE id = ?', (node_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n\n # 知识图谱:边列表/创建\n @app.route('/api/kg/edges', methods=['GET', 'POST'])\n def kg_edges():\n if request.method == 'GET':\n with db_manager.get_connection() as conn:\n rows = conn.execute('SELECT * FROM knowledge_edges ORDER BY id DESC').fetchall()\n return jsonify({'success': True, 'data': [dict(r) for r in rows]})\n else:\n data = request.get_json(force=True, silent=True) or {}\n try:\n source_id = int(data.get('source_id'))\n target_id = int(data.get('target_id'))\n except (TypeError, ValueError):\n return jsonify({'success': False, 'message': 'source_id 和 target_id 必须为整数'}), 400\n relation_type = (data.get('relation_type') or 'related').strip() or 'related'\n try:\n weight = float(data.get('weight', 1.0))\n except (TypeError, ValueError):\n weight = 1.0\n with db_manager.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight)\n )\n edge_id = cursor.lastrowid\n kg_manager._load_graph_from_db()\n return jsonify({'success': True, 'id': edge_id})\n\n # 知识图谱:边删除\n @app.route('/api/kg/edges/<int:edge_id>', methods=['DELETE'])\n def kg_edge_detail(edge_id):\n with db_manager.get_connection() as conn:\n conn.execute('DELETE FROM knowledge_edges WHERE id = ?', (edge_id,))\n kg_manager._load_graph_from_db()\n return jsonify({'success': True})\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data()\n \n # 过滤包含搜索词的节点(不区分大小写)\n filtered_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取过滤后节点的ID集合(统一转换为字符串)\n filtered_node_ids = {str(node['id']) for node in filtered_nodes}\n \n # 过滤相关的边(源节点或目标节点在过滤后的节点中)\n filtered_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in filtered_node_ids or str(edge['target']) in filtered_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': filtered_nodes,\n 'edges': filtered_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': len(filtered_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n def get_study_time():\n \"\"\"获取学习时长统计\"\"\"\n period = request.args.get('period', 'week')\n \n # 根据时间段生成模拟数据(实际应用中应从数据库查询)\n if period == 'week':\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n # 基于文档创建时间生成学习时长\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT created_at FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 7'\n ).fetchall()\n values = []\n for i in range(7):\n if i < len(docs):\n # 根据文档数量估算学习时长(小时)\n values.append(round(0.5 + (len(docs) - i) * 0.3, 1))\n else:\n values.append(round(0.5 + (7 - i) * 0.2, 1))\n elif period == 'month':\n labels = [f'第{i}周' for i in range(1, 5)]\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_week = total_docs / 4 if total_docs > 0 else 1\n values = [round(avg_per_week * 0.5 + i * 0.3, 1) for i in range(4)]\n else: # quarter\n labels = ['第1月', '第2月', '第3月']\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_month = total_docs / 3 if total_docs > 0 else 1\n values = [round(avg_per_month * 0.8 + i * 0.5, 1) for i in range(3)]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(只统计来自已上传文件的concept)\"\"\"\n with db_manager.get_connection() as conn:\n # 只统计来自未删除文档的concept节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 只统计来自未删除文档的concept节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点\n target_docs = 100\n target_nodes = 200\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n overall_progress = int((doc_progress + node_progress) / 2)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_docs > 0 and total_nodes > 0:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n try:\n full_content = doc_manager.doc_processor.extract_text(doc['file_path'], doc['file_type'])\n except:\n full_content = doc['content']\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ? AND node_type = ?',\n (doc_id, 'concept')\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(只删除concept类型的节点)\n conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ? AND node_type = ?',\n (doc_id, 'concept')\n )\n deleted_nodes = conn.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n \n # 5. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已删除'\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "77a6f683db3d5c5c2fb00aba936e710d7e83f4b5": {"t": 1785758537.1104763, "n": 57900, "h": "f9916e73fdd696be", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any, Tuple\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n document_id INTEGER, -- 关联的文档ID,确保节点来自已上传的文件\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n \n # 如果表已存在但没有document_id字段,添加该字段\n try:\n conn.execute('ALTER TABLE knowledge_nodes ADD COLUMN document_id INTEGER')\n except sqlite3.OperationalError:\n # 字段已存在,忽略错误\n pass\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self._load_graph_from_db()\n \n def _load_graph_from_db(self):\n \"\"\"从数据库加载知识图谱\n 只加载来自未删除文档的节点\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n # 只加载来自未删除文档的节点(concept类型)\n nodes = conn.execute('''\n SELECT kn.* FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchall()\n \n for node in nodes:\n # sqlite3.Row不支持.get()方法,使用try-except处理可能不存在的字段\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=description,\n document_id=document_id)\n\n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 只加载两个端点都在有效节点集合中的边\n all_edges = conn.execute('SELECT * FROM knowledge_edges').fetchall()\n for edge in all_edges:\n if edge['source_id'] in valid_node_ids and edge['target_id'] in valid_node_ids:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n import traceback\n traceback.print_exc()\n \n def add_knowledge_node(self, name: str, node_type: str, description: str = None, document_id: int = None) -> int:\n \"\"\"添加知识节点\n 参数:\n name: 节点名称\n node_type: 节点类型(如concept, person等)\n description: 节点描述\n document_id: 关联的文档ID,确保节点来自已上传的文件\n 注意: 对于concept类型的节点,确保全局唯一(相同名称只创建一个节点)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n # 对于concept类型,检查全局是否已存在相同名称的节点(不限制document_id)\n if node_type == 'concept':\n existing_node = conn.execute(\n 'SELECT id, frequency FROM knowledge_nodes WHERE name = ? AND node_type = ? LIMIT 1',\n (name, node_type)\n ).fetchone()\n \n if existing_node:\n # 如果节点已存在,增加频率并返回现有节点ID\n node_id = existing_node['id']\n new_frequency = existing_node['frequency'] + 1\n conn.execute(\n 'UPDATE knowledge_nodes SET frequency = ? WHERE id = ?',\n (new_frequency, node_id)\n )\n else:\n # 创建新节点\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id) VALUES (?, ?, ?, ?)',\n (name, node_type, description, document_id)\n )\n node_id = cursor.lastrowid\n else:\n # 对于非concept类型,检查是否已存在相同名称和类型的节点(来自同一文档)\n existing_node = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE name = ? AND node_type = ? AND document_id = ?',\n (name, node_type, document_id)\n ).fetchone()\n \n if existing_node:\n # 如果节点已存在,增加频率并返回现有节点ID\n conn.execute(\n 'UPDATE knowledge_nodes SET frequency = frequency + 1 WHERE id = ?',\n (existing_node['id'],)\n )\n node_id = existing_node['id']\n else:\n # 创建新节点\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description, document_id) VALUES (?, ?, ?, ?)',\n (name, node_type, description, document_id)\n )\n node_id = cursor.lastrowid\n \n # 更新内存中的图\n if node_id not in self.graph:\n self.graph.add_node(node_id, name=name, node_type=node_type, description=description, document_id=document_id)\n else:\n # 更新现有节点\n self.graph.nodes[node_id]['name'] = name\n self.graph.nodes[node_id]['node_type'] = node_type\n if description:\n self.graph.nodes[node_id]['description'] = description\n # 对于concept类型,不更新document_id(保持第一个文档的关联)\n \n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n import traceback\n traceback.print_exc()\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self) -> Dict:\n \"\"\"获取图谱数据用于可视化\n 只返回来自已上传且未删除文件的节点\n 确保每个concept名称只出现一次(去重)\n \"\"\"\n nodes = []\n edges = []\n \n # 从数据库获取节点信息,按名称去重(对于concept类型)\n with self.db.get_connection() as conn:\n # 使用GROUP BY确保每个concept名称只出现一次,合并频率\n db_nodes = conn.execute('''\n SELECT \n MIN(kn.id) as id,\n kn.name,\n kn.node_type,\n MAX(kn.description) as description,\n SUM(kn.frequency) as frequency,\n MIN(kn.document_id) as document_id\n FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n GROUP BY kn.name, kn.node_type\n ''').fetchall()\n \n # 构建节点映射\n name_to_node = {} # 名称到节点信息的映射\n node_id_map = {} # 旧ID到新ID的映射(用于边的重映射)\n \n for node in db_nodes:\n node_id = node['id']\n node_name = node['name']\n \n # 存储节点信息\n try:\n description = node['description']\n except (KeyError, IndexError):\n description = None\n \n try:\n document_id = node['document_id']\n except (KeyError, IndexError):\n document_id = None\n \n name_to_node[node_name] = {\n 'id': node_id,\n 'name': node_name,\n 'type': node['node_type'],\n 'description': description or '',\n 'frequency': node['frequency'],\n 'document_id': document_id\n }\n \n # 转换节点格式,确保每个concept名称只出现一次\n for node_name, node_info in name_to_node.items():\n nodes.append({\n 'id': node_info['id'],\n 'name': node_info['name'],\n 'type': node_info['type'],\n 'description': node_info['description'],\n 'frequency': node_info['frequency'],\n 'document_id': node_info['document_id']\n })\n \n # 获取所有节点名称到ID的映射(用于边的重映射)\n name_to_id_map = {node['name']: node['id'] for node in nodes}\n \n # 获取有效的节点ID集合\n valid_node_ids = {node['id'] for node in nodes}\n \n # 转换边格式,需要根据节点名称重映射边的端点\n with self.db.get_connection() as conn:\n # 获取所有有效的边,并获取源节点和目标节点的名称\n all_edges_query = conn.execute('''\n SELECT DISTINCT \n ke.source_id, \n ke.target_id, \n ke.relation_type, \n ke.weight,\n kn1.name as source_name,\n kn2.name as target_name\n FROM knowledge_edges ke\n INNER JOIN knowledge_nodes kn1 ON ke.source_id = kn1.id\n INNER JOIN knowledge_nodes kn2 ON ke.target_id = kn2.id\n LEFT JOIN documents d1 ON kn1.document_id = d1.id\n LEFT JOIN documents d2 ON kn2.document_id = d2.id\n WHERE kn1.node_type = 'concept' AND kn2.node_type = 'concept'\n AND (kn1.document_id IS NULL OR d1.is_deleted = 0)\n AND (kn2.document_id IS NULL OR d2.is_deleted = 0)\n ''').fetchall()\n \n # 构建边映射,用于去重\n edge_map = {}\n \n for edge in all_edges_query:\n try:\n source_name = edge['source_name']\n target_name = edge['target_name']\n except (KeyError, IndexError):\n continue\n \n # 如果两个节点都在有效节点集合中,添加边\n if source_name in name_to_id_map and target_name in name_to_id_map:\n new_source_id = name_to_id_map[source_name]\n new_target_id = name_to_id_map[target_name]\n \n # 避免自环和重复边\n if new_source_id != new_target_id:\n edge_key = (new_source_id, new_target_id)\n if edge_key not in edge_map:\n try:\n relation_type = edge['relation_type']\n except (KeyError, IndexError):\n relation_type = 'related'\n \n try:\n weight = edge['weight']\n except (KeyError, IndexError):\n weight = 1.0\n \n edges.append({\n 'source': new_source_id,\n 'target': new_target_id,\n 'type': relation_type,\n 'weight': weight\n })\n edge_map[edge_key] = True\n\n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def delete_document_by_hash(self, hash_value: str) -> bool:\n \"\"\"根据哈希值删除文档(包括文件和数据库记录)\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 查询要删除的文档\n doc = conn.execute(\n 'SELECT id, file_path FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (hash_value,)\n ).fetchone()\n \n if doc:\n doc_id = doc['id']\n file_path = doc['file_path']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n return True\n else:\n print(f\"未找到哈希值为 {hash_value} 的文档\")\n return False\n except Exception as e:\n print(f\"根据哈希值删除文档时出错: {e}\")\n return False\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any], force_replace: bool = False) -> Tuple[Optional[int], bool]:\n \"\"\"保存文档到数据库\n 参数:\n doc_data: 文档数据\n force_replace: 如果为True,遇到哈希值冲突时强制删除旧记录并插入新记录\n 返回: (文档ID, 是否已存在)\n \"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except sqlite3.IntegrityError as e:\n # 哈希值重复\n if force_replace:\n # 强制替换:删除旧记录并重新插入\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),强制删除旧记录并插入新记录\")\n with self.db.get_connection() as conn:\n # 删除旧记录\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (doc_data['hash_value'],)\n )\n conn.commit()\n \n # 重新插入\n try:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return (cursor.lastrowid, False) # 新文档\n except Exception as retry_error:\n print(f\"重新插入文档时出错: {retry_error}\")\n return (None, False)\n else:\n # 不强制替换:查询已存在的文档ID\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复),返回已存在的文档ID\")\n with self.db.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (doc_data['hash_value'],)\n ).fetchone()\n if existing_doc:\n return (existing_doc['id'], True) # 已存在的文档\n return (None, False)\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n import traceback\n traceback.print_exc()\n return (None, False)\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 计算文件哈希值\n file_hash = doc_manager.calculate_file_hash(file_path)\n \n # 检查数据库中是否已存在相同哈希值的文件\n is_replacement = False\n old_doc_id = None\n old_file_path = None\n old_title = None\n \n # 先查询是否存在相同哈希值的文件\n with db_manager.get_connection() as conn:\n existing_doc = conn.execute(\n 'SELECT id, file_path, title FROM documents WHERE hash_value = ? AND is_deleted = 0',\n (file_hash,)\n ).fetchone()\n \n if existing_doc:\n is_replacement = True\n old_doc_id = existing_doc['id']\n old_file_path = existing_doc['file_path']\n old_title = existing_doc['title']\n \n # 如果存在旧记录,先删除旧文件(在删除数据库记录之前)\n if is_replacement and old_file_path and old_file_path != file_path and os.path.exists(old_file_path):\n try:\n os.remove(old_file_path)\n print(f\"已删除旧文件: {old_file_path}\")\n except Exception as e:\n print(f\"删除旧文件失败 {old_file_path}: {e}\")\n \n # 删除数据库中的旧记录(如果存在)\n if is_replacement and old_doc_id:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (old_doc_id,)\n )\n conn.commit() # 显式提交,确保删除操作生效\n print(f\"已删除旧文档记录 ID: {old_doc_id},哈希值: {file_hash},旧标题: {old_title}\")\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if not doc_data:\n return jsonify({'success': False, 'message': '文档处理失败:无法提取文档内容'}), 500\n \n # 确保使用计算好的哈希值\n doc_data['hash_value'] = file_hash\n \n # 保存新文档(如果存在旧记录,强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=is_replacement)\n if not doc_id:\n # 如果保存失败,尝试强制删除并重新保存\n print(f\"保存失败,尝试强制删除旧记录并重新保存...\")\n with db_manager.get_connection() as conn:\n conn.execute(\n 'DELETE FROM documents WHERE hash_value = ?',\n (file_hash,)\n )\n conn.commit()\n # 再次尝试保存(强制替换)\n doc_id, is_existing = doc_manager.save_document(doc_data, force_replace=True)\n \n if not doc_id:\n return jsonify({\n 'success': False, \n 'message': f'文档保存失败:无法保存到数据库。请检查文件是否损坏或数据库是否正常。'\n }), 500\n \n # 从关键词创建知识节点,关联到当前文档\n if doc_data['keywords'] and not doc_data['content'].startswith('['):\n try:\n for keyword in doc_data['keywords'][:5]: # 限制前5个关键词\n kg_manager.add_knowledge_node(\n keyword, \n 'concept', \n f'从文档\"{doc_data[\"title\"]}\"提取的关键词',\n document_id=doc_id # 关联到当前文档\n )\n except Exception as e:\n print(f\"创建知识节点时出错: {e}\")\n \n # 根据是否替换旧文件返回不同的消息\n message = '文档上传成功(已替换旧文件)' if is_replacement else '文档上传成功'\n return jsonify({\n 'success': True,\n 'message': message,\n 'document_id': doc_id,\n 'is_existing': False,\n 'is_replacement': is_replacement,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n graph_data = kg_manager.get_graph_data()\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges'])\n })\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data()\n \n # 过滤包含搜索词的节点(不区分大小写)\n filtered_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取过滤后节点的ID集合(统一转换为字符串)\n filtered_node_ids = {str(node['id']) for node in filtered_nodes}\n \n # 过滤相关的边(源节点或目标节点在过滤后的节点中)\n filtered_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in filtered_node_ids or str(edge['target']) in filtered_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': filtered_nodes,\n 'edges': filtered_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': len(filtered_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n def get_study_time():\n \"\"\"获取学习时长统计\"\"\"\n period = request.args.get('period', 'week')\n \n # 根据时间段生成模拟数据(实际应用中应从数据库查询)\n if period == 'week':\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n # 基于文档创建时间生成学习时长\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT created_at FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 7'\n ).fetchall()\n values = []\n for i in range(7):\n if i < len(docs):\n # 根据文档数量估算学习时长(小时)\n values.append(round(0.5 + (len(docs) - i) * 0.3, 1))\n else:\n values.append(round(0.5 + (7 - i) * 0.2, 1))\n elif period == 'month':\n labels = [f'第{i}周' for i in range(1, 5)]\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_week = total_docs / 4 if total_docs > 0 else 1\n values = [round(avg_per_week * 0.5 + i * 0.3, 1) for i in range(4)]\n else: # quarter\n labels = ['第1月', '第2月', '第3月']\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_month = total_docs / 3 if total_docs > 0 else 1\n values = [round(avg_per_month * 0.8 + i * 0.5, 1) for i in range(3)]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布(只统计来自已上传文件的concept)\"\"\"\n with db_manager.get_connection() as conn:\n # 只统计来自未删除文档的concept节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n # 只统计来自未删除文档的concept节点\n total_nodes = conn.execute('''\n SELECT COUNT(*) FROM knowledge_nodes kn\n LEFT JOIN documents d ON kn.document_id = d.id\n WHERE (kn.document_id IS NULL OR d.is_deleted = 0)\n AND kn.node_type = 'concept'\n ''').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点\n target_docs = 100\n target_nodes = 200\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n overall_progress = int((doc_progress + node_progress) / 2)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_docs > 0 and total_nodes > 0:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n try:\n full_content = doc_manager.doc_processor.extract_text(doc['file_path'], doc['file_type'])\n except:\n full_content = doc['content']\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(硬删除:删除文件、数据库记录和哈希值)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n # 先获取文档信息(包括文件路径和哈希值)\n doc = conn.execute(\n 'SELECT file_path, hash_value FROM documents WHERE id = ?',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({\n 'success': False,\n 'message': '文档不存在'\n }), 404\n \n file_path = doc['file_path']\n hash_value = doc['hash_value']\n \n # 1. 删除uploads文件夹中的文件\n if file_path and os.path.exists(file_path):\n try:\n os.remove(file_path)\n print(f\"已删除文件: {file_path}\")\n except Exception as e:\n print(f\"删除文件失败 {file_path}: {e}\")\n # 即使文件删除失败,也继续删除数据库记录\n \n # 2. 获取要删除的知识节点ID(用于从内存图中移除)\n node_ids_to_delete = conn.execute(\n 'SELECT id FROM knowledge_nodes WHERE document_id = ? AND node_type = ?',\n (doc_id, 'concept')\n ).fetchall()\n node_ids = [node['id'] for node in node_ids_to_delete]\n \n # 3. 删除关联的知识节点(只删除concept类型的节点)\n conn.execute(\n 'DELETE FROM knowledge_nodes WHERE document_id = ? AND node_type = ?',\n (doc_id, 'concept')\n )\n deleted_nodes = conn.rowcount\n if deleted_nodes > 0:\n print(f\"已删除 {deleted_nodes} 个关联的知识节点\")\n \n # 4. 删除关联的知识边(如果源节点或目标节点被删除)\n if node_ids:\n placeholders = ','.join(['?'] * len(node_ids))\n conn.execute(\n f'DELETE FROM knowledge_edges WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders})',\n node_ids + node_ids\n )\n \n # 5. 删除数据库中的记录(包括哈希值)\n conn.execute(\n 'DELETE FROM documents WHERE id = ?',\n (doc_id,)\n )\n \n print(f\"已删除文档记录 ID: {doc_id}, 哈希值: {hash_value}\")\n \n # 6. 从内存中的知识图谱移除已删除的节点\n for node_id in node_ids:\n if node_id in kg_manager.graph:\n kg_manager.graph.remove_node(node_id)\n print(f\"从内存图谱中移除节点 ID: {node_id}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档已删除'\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "d7046e1a56d91c0774ad31182a3aaaebb26c4046": {"t": 1785758539.0038118, "n": 39722, "h": "1ed61e70faeb43bd", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nimport re\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file, send_from_directory\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )\n ''')\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self._load_graph_from_db()\n \n def _load_graph_from_db(self):\n \"\"\"从数据库加载知识图谱\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 加载节点\n nodes = conn.execute('SELECT * FROM knowledge_nodes').fetchall()\n for node in nodes:\n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=node['description'])\n \n # 加载边\n edges = conn.execute('SELECT * FROM knowledge_edges').fetchall()\n for edge in edges:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n \n def add_knowledge_node(self, name: str, node_type: str, description: str = None) -> int:\n \"\"\"添加知识节点\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description) VALUES (?, ?, ?)',\n (name, node_type, description)\n )\n node_id = cursor.lastrowid\n self.graph.add_node(node_id, name=name, node_type=node_type, description=description)\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self) -> Dict:\n \"\"\"获取图谱数据用于可视化\"\"\"\n nodes = []\n edges = []\n \n # 从数据库获取节点信息(包括频率)\n with self.db.get_connection() as conn:\n db_nodes = conn.execute('SELECT * FROM knowledge_nodes').fetchall()\n node_freq_map = {node['id']: node['frequency'] for node in db_nodes}\n \n # 转换节点格式\n for node_id, node_data in self.graph.nodes(data=True):\n nodes.append({\n 'id': node_id,\n 'name': node_data.get('name', f'Node {node_id}'),\n 'type': node_data.get('node_type', 'unknown'),\n 'description': node_data.get('description', ''),\n 'frequency': node_freq_map.get(node_id, 1)\n })\n \n # 转换边格式\n for source, target, edge_data in self.graph.edges(data=True):\n edges.append({\n 'source': source,\n 'target': target,\n 'type': edge_data.get('relation_type', 'related'),\n 'weight': edge_data.get('weight', 1.0)\n })\n \n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any]) -> Optional[int]:\n \"\"\"保存文档到数据库\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return cursor.lastrowid\n except sqlite3.IntegrityError:\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复)\")\n return None\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n return None\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档 - 精准搜索版本,只返回真正相关的文档\"\"\"\n with self.db.get_connection() as conn:\n query_lower = query.lower().strip()\n if not query_lower:\n return []\n \n # 判断搜索词类型:文件名搜索(短词、包含点号、或纯字母数字)还是内容搜索\n is_filename_search = (\n len(query_lower) <= 20 and # 短词更可能是文件名\n ('.' in query_lower or # 包含扩展名\n query_lower.replace('_', '').replace('-', '').replace('.', '').isalnum()) # 纯字母数字\n )\n \n # 获取所有未删除的文档\n all_docs = conn.execute('''\n SELECT * FROM documents \n WHERE is_deleted = 0\n ''').fetchall()\n \n # 计算相关性分数\n scored_docs = []\n for doc in all_docs:\n doc_dict = dict(doc)\n score = 0.0\n title = doc_dict.get('title', '').lower()\n content = doc_dict.get('content', '').lower()\n file_path = doc_dict.get('file_path', '')\n filename = os.path.basename(file_path).lower() if file_path else ''\n \n # 优先匹配文件名和标题\n # 1. 文件名完全匹配 - 最高优先级\n if filename == query_lower:\n score += 100.0\n # 2. 文件名开头匹配(去掉扩展名)\n elif filename.startswith(query_lower):\n score += 80.0\n # 3. 文件名包含(作为完整词)\n elif query_lower in filename:\n # 检查是否是完整词匹配(前后是分隔符)\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, filename):\n score += 60.0\n else:\n score += 40.0\n \n # 4. 标题完全匹配\n if title == query_lower:\n score += 90.0\n # 5. 标题开头匹配\n elif title.startswith(query_lower):\n score += 70.0\n # 6. 标题包含(作为完整词)\n elif query_lower in title:\n pattern = r'\\b' + re.escape(query_lower) + r'\\b'\n if re.search(pattern, title):\n score += 50.0\n else:\n score += 30.0\n \n # 如果是文件名搜索,只匹配文件名和标题,不匹配内容\n if is_filename_search:\n # 文件名搜索模式下,内容匹配权重极低\n if query_lower in content and score == 0:\n # 只有在完全没有文件名/标题匹配时,才考虑内容\n # 但即使这样,也只给很低的分数\n count = content.count(query_lower)\n if count >= 3: # 至少出现3次才考虑\n score += min(count * 0.5, 5.0) # 最多5分\n else:\n # 内容搜索模式:内容匹配\n if query_lower in content:\n # 计算出现次数,但要求至少出现2次\n count = content.count(query_lower)\n if count >= 2:\n score += min(count * 1.5, 25.0) # 最多25分\n \n # 关键词匹配(从tags中)\n tags_str = doc_dict.get('tags', '')\n if tags_str:\n try:\n tags = json.loads(tags_str)\n if isinstance(tags, list):\n for tag in tags:\n if query_lower in tag.lower():\n score += 15.0\n except:\n pass\n \n # 使用TF-IDF计算相似度(如果内容足够长且不是错误信息)\n if len(content) > 100 and not content.startswith('[') and not content.startswith('处理错误'):\n try:\n similarity = self.ai.calculate_similarity(query, content[:2000])\n # 只有相似度较高时才加分\n if similarity > 0.3:\n score += similarity * 10.0 # 最多10分\n except:\n pass\n \n # 只添加有意义的匹配结果\n # 文件名搜索:至少要有文件名或标题匹配\n # 内容搜索:至少要有一定分数\n min_score = 25.0 if is_filename_search else 15.0\n \n if score >= min_score:\n doc_dict['_search_score'] = score\n scored_docs.append(doc_dict)\n \n # 按分数降序排序,然后按创建时间降序\n def get_timestamp(doc):\n try:\n created_at = doc.get('created_at', '2000-01-01')\n if isinstance(created_at, str):\n try:\n if ' ' in created_at:\n dt = datetime.strptime(created_at.split('.')[0], '%Y-%m-%d %H:%M:%S')\n else:\n dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))\n return dt.timestamp()\n except:\n return 0.0\n return 0.0\n except:\n return 0.0\n \n scored_docs.sort(key=lambda x: (x.get('_search_score', 0), get_timestamp(x)), reverse=True)\n \n # 移除临时分数字段\n for doc in scored_docs:\n doc.pop('_search_score', None)\n \n return scored_docs[:limit]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if doc_data:\n doc_id = doc_manager.save_document(doc_data)\n if doc_id:\n # 如果处理成功,从关键词创建知识节点\n if doc_data['keywords'] and not doc_data['content'].startswith('['):\n try:\n for keyword in doc_data['keywords'][:5]: # 限制前5个关键词\n kg_manager.add_knowledge_node(keyword, 'concept', f'从文档\"{doc_data[\"title\"]}\"提取的关键词')\n except Exception as e:\n print(f\"创建知识节点时出错: {e}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档上传成功',\n 'document_id': doc_id,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n \n return jsonify({'success': False, 'message': '文档处理失败'}), 500\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n graph_data = kg_manager.get_graph_data()\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges'])\n })\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data()\n \n # 过滤包含搜索词的节点(不区分大小写)\n filtered_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取过滤后节点的ID集合(统一转换为字符串)\n filtered_node_ids = {str(node['id']) for node in filtered_nodes}\n \n # 过滤相关的边(源节点或目标节点在过滤后的节点中)\n filtered_edges = [\n edge for edge in graph_data['edges'] \n if str(edge['source']) in filtered_node_ids or str(edge['target']) in filtered_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': filtered_nodes,\n 'edges': filtered_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': len(filtered_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>', defaults={'user_id': 1})\n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n @app.route('/api/analytics/study-time')\n def get_study_time():\n \"\"\"获取学习时长统计\"\"\"\n period = request.args.get('period', 'week')\n \n # 根据时间段生成模拟数据(实际应用中应从数据库查询)\n if period == 'week':\n labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\n # 基于文档创建时间生成学习时长\n with db_manager.get_connection() as conn:\n docs = conn.execute(\n 'SELECT created_at FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 7'\n ).fetchall()\n values = []\n for i in range(7):\n if i < len(docs):\n # 根据文档数量估算学习时长(小时)\n values.append(round(0.5 + (len(docs) - i) * 0.3, 1))\n else:\n values.append(round(0.5 + (7 - i) * 0.2, 1))\n elif period == 'month':\n labels = [f'第{i}周' for i in range(1, 5)]\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_week = total_docs / 4 if total_docs > 0 else 1\n values = [round(avg_per_week * 0.5 + i * 0.3, 1) for i in range(4)]\n else: # quarter\n labels = ['第1月', '第2月', '第3月']\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n avg_per_month = total_docs / 3 if total_docs > 0 else 1\n values = [round(avg_per_month * 0.8 + i * 0.5, 1) for i in range(3)]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'labels': labels,\n 'values': values,\n 'period': period\n }\n })\n \n @app.route('/api/analytics/knowledge-level')\n def get_knowledge_level():\n \"\"\"获取知识点掌握分布\"\"\"\n with db_manager.get_connection() as conn:\n total_nodes = conn.execute('SELECT COUNT(*) FROM knowledge_nodes').fetchone()[0]\n \n # 简化的掌握度计算(实际应用中需要更复杂的算法)\n if total_nodes == 0:\n levels = [\n {'value': 0, 'name': '已掌握'},\n {'value': 0, 'name': '学习中'},\n {'value': 0, 'name': '待学习'}\n ]\n else:\n mastered = int(total_nodes * 0.35)\n learning = int(total_nodes * 0.45)\n pending = total_nodes - mastered - learning\n levels = [\n {'value': mastered, 'name': '已掌握'},\n {'value': learning, 'name': '学习中'},\n {'value': pending, 'name': '待学习'}\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'levels': levels,\n 'total': total_nodes\n }\n })\n \n @app.route('/api/analytics/prediction')\n def get_prediction():\n \"\"\"获取学习进展预测\"\"\"\n with db_manager.get_connection() as conn:\n total_docs = conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n total_nodes = conn.execute('SELECT COUNT(*) FROM knowledge_nodes').fetchone()[0]\n \n # 简化的预测算法\n # 假设目标:100个文档,200个知识点\n target_docs = 100\n target_nodes = 200\n \n doc_progress = min(100, int((total_docs / target_docs) * 100)) if target_docs > 0 else 0\n node_progress = min(100, int((total_nodes / target_nodes) * 100)) if target_nodes > 0 else 0\n overall_progress = int((doc_progress + node_progress) / 2)\n \n # 计算预测完成时间(基于当前进度和学习速度)\n if overall_progress > 0:\n remaining_progress = 100 - overall_progress\n # 假设每天进步2%\n estimated_days = remaining_progress / 2\n estimated_weeks = round(estimated_days / 7, 1)\n estimated_time = f'{estimated_weeks}周'\n else:\n estimated_time = '4周'\n \n # 生成建议\n recommendations = []\n if total_docs < 10:\n recommendations.append('增加文档上传频次,建立知识库基础')\n if total_nodes < 20:\n recommendations.append('系统会自动从文档中提取关键词,建议上传更多相关文档')\n if total_docs > 0 and total_nodes > 0:\n recommendations.append('建立更多知识点之间的关联,完善知识网络')\n if not recommendations:\n recommendations = [\n '保持当前学习节奏',\n '定期复习已学知识点',\n '探索新的知识领域'\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'progress': overall_progress,\n 'estimated_time': estimated_time,\n 'recommended_time': '2小时/天',\n 'recommendations': recommendations[:3] # 最多3条建议\n }\n })\n \n @app.route('/api/documents/<int:doc_id>')\n def get_document_detail(doc_id):\n \"\"\"获取文档详情\"\"\"\n with db_manager.get_connection() as conn:\n doc = conn.execute(\n 'SELECT * FROM documents WHERE id = ? AND is_deleted = 0',\n (doc_id,)\n ).fetchone()\n \n if not doc:\n return jsonify({'success': False, 'message': '文档不存在'}), 404\n \n # 读取完整内容\n try:\n full_content = doc_manager.doc_processor.extract_text(doc['file_path'], doc['file_type'])\n except:\n full_content = doc['content']\n \n doc_dict = dict(doc)\n doc_dict['full_content'] = full_content\n \n # 解析JSON字段\n if doc_dict.get('tags'):\n try:\n doc_dict['tags'] = json.loads(doc_dict['tags'])\n except:\n doc_dict['tags'] = []\n \n if doc_dict.get('metadata'):\n try:\n doc_dict['metadata'] = json.loads(doc_dict['metadata'])\n except:\n doc_dict['metadata'] = {}\n \n return jsonify({\n 'success': True,\n 'data': doc_dict\n })\n \n @app.route('/api/documents/<int:doc_id>', methods=['DELETE'])\n def delete_document(doc_id):\n \"\"\"删除文档(软删除)\"\"\"\n try:\n with db_manager.get_connection() as conn:\n conn.execute(\n 'UPDATE documents SET is_deleted = 1 WHERE id = ?',\n (doc_id,)\n )\n return jsonify({\n 'success': True,\n 'message': '文档已删除'\n })\n except Exception as e:\n return jsonify({\n 'success': False,\n 'message': f'删除失败: {str(e)}'\n }), 500\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}, "b70ae0434fc51156065963788a8a185d793ce7db": {"t": 1785758541.1818385, "n": 27306, "h": "850654fbf620ed78", "c": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n智能个人知识管理系统 - 修复版本\n支持PDF、Word、TXT、Markdown等多种文件格式\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport sqlite3\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Any\nimport hashlib\nimport mimetypes\n\n# 第三方库导入\nfrom flask import Flask, request, jsonify, render_template, send_file\nfrom flask_cors import CORS\nimport spacy\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.metrics.pairwise import cosine_similarity\nimport networkx as nx\n\n# 尝试导入PDF处理库\ntry:\n import PyPDF2\n PDF_AVAILABLE = True\nexcept ImportError:\n PDF_AVAILABLE = False\n print(\"警告:未安装PyPDF2,PDF处理功能将受限\")\n\n# 尝试导入Word处理库\ntry:\n from docx import Document\n DOCX_AVAILABLE = True\nexcept ImportError:\n DOCX_AVAILABLE = False\n print(\"警告:未安装python-docx,Word文档处理功能将受限\")\n\n# 配置类\nclass Config:\n \"\"\"应用配置类\"\"\"\n SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'\n DATABASE_PATH = os.path.join(os.path.dirname(__file__), 'data', 'knowledge.db')\n UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), 'uploads')\n MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB\n \n # AI模型配置\n NLP_MODEL = 'zh_core_web_sm' # 中文模型\n SIMILARITY_THRESHOLD = 0.3 # 相似度阈值\n \n @staticmethod\n def init_app(app):\n \"\"\"初始化应用配置\"\"\"\n # 创建必要的目录\n os.makedirs(os.path.dirname(Config.DATABASE_PATH), exist_ok=True)\n os.makedirs(Config.UPLOAD_FOLDER, exist_ok=True)\n\n# 文档处理工具类\nclass DocumentProcessor:\n \"\"\"文档处理工具类 - 支持多种文件格式\"\"\"\n \n @staticmethod\n def extract_text_from_txt(file_path: str) -> str:\n \"\"\"从文本文件提取内容\"\"\"\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n return f.read()\n except UnicodeDecodeError:\n # 尝试其他编码\n with open(file_path, 'r', encoding='gbk') as f:\n return f.read()\n \n @staticmethod\n def extract_text_from_pdf(file_path: str) -> str:\n \"\"\"从PDF文件提取内容\"\"\"\n if not PDF_AVAILABLE:\n return \"[PDF处理功能需要安装PyPDF2库]\"\n \n try:\n text = \"\"\n with open(file_path, 'rb') as f:\n reader = PyPDF2.PdfReader(f)\n for page in reader.pages:\n text += page.extract_text() + \"\\n\"\n return text\n except Exception as e:\n return f\"[PDF处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_docx(file_path: str) -> str:\n \"\"\"从Word文档提取内容\"\"\"\n if not DOCX_AVAILABLE:\n return \"[Word文档处理功能需要安装python-docx库]\"\n \n try:\n doc = Document(file_path)\n return \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n except Exception as e:\n return f\"[Word文档处理错误: {str(e)}]\"\n \n @staticmethod\n def extract_text_from_md(file_path: str) -> str:\n \"\"\"从Markdown文件提取内容\"\"\"\n return DocumentProcessor.extract_text_from_txt(file_path)\n \n @staticmethod\n def extract_text(file_path: str, file_type: str) -> str:\n \"\"\"根据文件类型提取文本内容\"\"\"\n file_type = file_type.lower()\n \n if file_type in ['txt', 'text']:\n return DocumentProcessor.extract_text_from_txt(file_path)\n elif file_type == 'pdf':\n return DocumentProcessor.extract_text_from_pdf(file_path)\n elif file_type in ['docx', 'doc']:\n return DocumentProcessor.extract_text_from_docx(file_path)\n elif file_type in ['md', 'markdown']:\n return DocumentProcessor.extract_text_from_md(file_path)\n else:\n return f\"[不支持的文件格式: {file_type}]\"\n\n# 数据库管理类\nclass DatabaseManager:\n \"\"\"数据库管理类\"\"\"\n \n def __init__(self, db_path: str):\n self.db_path = db_path\n self.init_database()\n \n def get_connection(self):\n \"\"\"获取数据库连接\"\"\"\n conn = sqlite3.connect(self.db_path)\n conn.row_factory = sqlite3.Row # 使查询结果可以像字典一样访问\n return conn\n \n def init_database(self):\n \"\"\"初始化数据库表\"\"\"\n with self.get_connection() as conn:\n # 文档表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT,\n file_path TEXT,\n file_type TEXT,\n file_size INTEGER,\n hash_value TEXT UNIQUE,\n tags TEXT, -- JSON格式存储标签\n metadata TEXT, -- JSON格式存储元数据\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n is_deleted BOOLEAN DEFAULT 0\n )\n ''')\n \n # 知识节点表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n node_type TEXT NOT NULL, -- concept, person, location, etc.\n description TEXT,\n properties TEXT, -- JSON格式存储属性\n frequency INTEGER DEFAULT 1,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )\n ''')\n \n # 知识边表(关系)\n conn.execute('''\n CREATE TABLE IF NOT EXISTS knowledge_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_id INTEGER NOT NULL,\n target_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n weight REAL DEFAULT 1.0,\n properties TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (source_id) REFERENCES knowledge_nodes(id),\n FOREIGN KEY (target_id) REFERENCES knowledge_nodes(id)\n )\n ''')\n \n # 用户行为表\n conn.execute('''\n CREATE TABLE IF NOT EXISTS user_behaviors (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n document_id INTEGER,\n action_type TEXT NOT NULL, -- view, edit, search, etc.\n duration INTEGER, -- 操作持续时间(秒)\n details TEXT, -- JSON格式存储详细信息\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (document_id) REFERENCES documents(id)\n )\n ''')\n\n# AI处理类\nclass AIProcessor:\n \"\"\"AI处理类 - 封装各种AI算法\"\"\"\n \n def __init__(self, model_name: str = None):\n self.model_name = model_name or Config.NLP_MODEL\n self.nlp = None\n self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')\n self._load_model()\n \n def _load_model(self):\n \"\"\"加载NLP模型\"\"\"\n try:\n self.nlp = spacy.load(self.model_name)\n except OSError:\n print(f\"警告:无法加载模型 {self.model_name},使用基础文本处理\")\n # 这里可以添加备选方案\n \n def extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:\n \"\"\"提取关键词\"\"\"\n if not text or text.startswith('['): # 跳过错误信息\n return []\n \n if self.nlp:\n doc = self.nlp(text)\n # 提取名词和形容词作为关键词\n keywords = [token.text.lower() for token in doc \n if token.pos_ in ['NOUN', 'ADJ'] \n and len(token.text) > 2 \n and not token.is_stop]\n \n # 计算TF-IDF得到最重要的关键词\n if keywords:\n keyword_freq = {}\n for kw in keywords:\n keyword_freq[kw] = keyword_freq.get(kw, 0) + 1\n \n # 返回频率最高的关键词\n sorted_keywords = sorted(keyword_freq.items(), \n key=lambda x: x[1], reverse=True)\n return [kw[0] for kw in sorted_keywords[:max_keywords]]\n \n # 备选方案:简单的词频统计\n words = text.lower().split()\n word_freq = {}\n for word in words:\n if len(word) > 2:\n word_freq[word] = word_freq.get(word, 0) + 1\n \n sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)\n return [word[0] for word in sorted_words[:max_keywords]]\n \n def calculate_similarity(self, text1: str, text2: str) -> float:\n \"\"\"计算文本相似度\"\"\"\n try:\n # 使用TF-IDF和余弦相似度\n tfidf_matrix = self.vectorizer.fit_transform([text1, text2])\n similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]\n return float(similarity)\n except:\n # 备选方案:简单的词重叠率\n words1 = set(text1.lower().split())\n words2 = set(text2.lower().split())\n intersection = words1.intersection(words2)\n union = words1.union(words2)\n return len(intersection) / len(union) if union else 0.0\n \n def extract_entities(self, text: str) -> Dict[str, List[str]]:\n \"\"\"提取命名实体\"\"\"\n entities = {\n 'persons': [],\n 'locations': [],\n 'organizations': [],\n 'concepts': []\n }\n \n if self.nlp and not text.startswith('['):\n doc = self.nlp(text)\n for ent in doc.ents:\n if ent.label_ == 'PERSON':\n entities['persons'].append(ent.text)\n elif ent.label_ in ['GPE', 'LOC']:\n entities['locations'].append(ent.text)\n elif ent.label_ == 'ORG':\n entities['organizations'].append(ent.text)\n else:\n entities['concepts'].append(ent.text)\n \n return entities\n\n# 知识图谱管理类\nclass KnowledgeGraphManager:\n \"\"\"知识图谱管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager):\n self.db = db_manager\n self.graph = nx.DiGraph()\n self._load_graph_from_db()\n \n def _load_graph_from_db(self):\n \"\"\"从数据库加载知识图谱\"\"\"\n try:\n with self.db.get_connection() as conn:\n # 加载节点\n nodes = conn.execute('SELECT * FROM knowledge_nodes').fetchall()\n for node in nodes:\n self.graph.add_node(node['id'], \n name=node['name'], \n node_type=node['node_type'],\n description=node['description'])\n \n # 加载边\n edges = conn.execute('SELECT * FROM knowledge_edges').fetchall()\n for edge in edges:\n self.graph.add_edge(edge['source_id'], edge['target_id'],\n relation_type=edge['relation_type'],\n weight=edge['weight'])\n except Exception as e:\n print(f\"加载知识图谱时出错: {e}\")\n \n def add_knowledge_node(self, name: str, node_type: str, description: str = None) -> int:\n \"\"\"添加知识节点\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_nodes (name, node_type, description) VALUES (?, ?, ?)',\n (name, node_type, description)\n )\n node_id = cursor.lastrowid\n self.graph.add_node(node_id, name=name, node_type=node_type, description=description)\n return node_id\n except Exception as e:\n print(f\"添加知识节点时出错: {e}\")\n return -1\n \n def add_knowledge_edge(self, source_id: int, target_id: int, \n relation_type: str, weight: float = 1.0) -> int:\n \"\"\"添加知识边(关系)\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute(\n 'INSERT INTO knowledge_edges (source_id, target_id, relation_type, weight) VALUES (?, ?, ?, ?)',\n (source_id, target_id, relation_type, weight)\n )\n edge_id = cursor.lastrowid\n self.graph.add_edge(source_id, target_id, relation_type=relation_type, weight=weight)\n return edge_id\n except Exception as e:\n print(f\"添加知识边时出错: {e}\")\n return -1\n \n def get_graph_data(self) -> Dict:\n \"\"\"获取图谱数据用于可视化\"\"\"\n nodes = []\n edges = []\n \n # 转换节点格式\n for node_id, node_data in self.graph.nodes(data=True):\n nodes.append({\n 'id': node_id,\n 'name': node_data.get('name', f'Node {node_id}'),\n 'type': node_data.get('node_type', 'unknown'),\n 'description': node_data.get('description', '')\n })\n \n # 转换边格式\n for source, target, edge_data in self.graph.edges(data=True):\n edges.append({\n 'source': source,\n 'target': target,\n 'type': edge_data.get('relation_type', 'related'),\n 'weight': edge_data.get('weight', 1.0)\n })\n \n return {'nodes': nodes, 'edges': edges}\n\n# 文档管理类\nclass DocumentManager:\n \"\"\"文档管理类\"\"\"\n \n def __init__(self, db_manager: DatabaseManager, ai_processor: AIProcessor):\n self.db = db_manager\n self.ai = ai_processor\n self.doc_processor = DocumentProcessor()\n \n def calculate_file_hash(self, file_path: str) -> str:\n \"\"\"计算文件哈希值\"\"\"\n hash_md5 = hashlib.md5()\n with open(file_path, \"rb\") as f:\n for chunk in iter(lambda: f.read(4096), b\"\"):\n hash_md5.update(chunk)\n return hash_md5.hexdigest()\n \n def process_document(self, file_path: str, title: str = None) -> Dict[str, Any]:\n \"\"\"处理文档,提取信息和知识\"\"\"\n try:\n file_type = os.path.splitext(file_path)[1][1:].lower()\n \n # 提取文档内容\n content = self.doc_processor.extract_text(file_path, file_type)\n \n # 提取关键词\n keywords = self.ai.extract_keywords(content)\n \n # 提取实体\n entities = self.ai.extract_entities(content)\n \n # 计算文档哈希\n file_hash = self.calculate_file_hash(file_path)\n \n # 准备文档数据\n doc_data = {\n 'title': title or os.path.basename(file_path),\n 'content': content[:2000] if not content.startswith('[') else content, # 存储前2000字符作为摘要\n 'file_path': file_path,\n 'file_type': file_type,\n 'file_size': os.path.getsize(file_path),\n 'hash_value': file_hash,\n 'keywords': keywords,\n 'entities': entities,\n 'metadata': {\n 'word_count': len(content.split()) if not content.startswith('[') else 0,\n 'char_count': len(content),\n 'processing_time': datetime.now().isoformat(),\n 'processing_status': 'success' if not content.startswith('[') else 'warning',\n 'processing_message': content if content.startswith('[') else '处理成功'\n }\n }\n \n return doc_data\n \n except Exception as e:\n print(f\"处理文档 {file_path} 时出错: {e}\")\n return {\n 'title': title or os.path.basename(file_path),\n 'content': f\"[处理错误: {str(e)}]\",\n 'file_path': file_path,\n 'file_type': os.path.splitext(file_path)[1][1:].lower(),\n 'file_size': os.path.getsize(file_path),\n 'hash_value': self.calculate_file_hash(file_path),\n 'keywords': [],\n 'entities': {'persons': [], 'locations': [], 'organizations': [], 'concepts': []},\n 'metadata': {\n 'processing_status': 'error',\n 'processing_message': str(e),\n 'processing_time': datetime.now().isoformat()\n }\n }\n \n def save_document(self, doc_data: Dict[str, Any]) -> Optional[int]:\n \"\"\"保存文档到数据库\"\"\"\n try:\n with self.db.get_connection() as conn:\n cursor = conn.execute('''\n INSERT INTO documents \n (title, content, file_path, file_type, file_size, hash_value, tags, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ''', (\n doc_data['title'],\n doc_data['content'],\n doc_data['file_path'],\n doc_data['file_type'],\n doc_data['file_size'],\n doc_data['hash_value'],\n json.dumps(doc_data['keywords'], ensure_ascii=False),\n json.dumps(doc_data['metadata'], ensure_ascii=False)\n ))\n return cursor.lastrowid\n except sqlite3.IntegrityError:\n print(f\"文档 {doc_data['title']} 已存在(哈希值重复)\")\n return None\n except Exception as e:\n print(f\"保存文档时出错: {e}\")\n return None\n \n def search_documents(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:\n \"\"\"搜索文档\"\"\"\n with self.db.get_connection() as conn:\n # 简单的全文搜索(实际应用中可以使用更高级的搜索技术)\n results = conn.execute('''\n SELECT * FROM documents \n WHERE (title LIKE ? OR content LIKE ?) AND is_deleted = 0\n ORDER BY created_at DESC\n LIMIT ?\n ''', (f'%{query}%', f'%{query}%', limit)).fetchall()\n \n return [dict(row) for row in results]\n\n# Flask应用创建函数\ndef create_app():\n \"\"\"创建Flask应用\"\"\"\n app = Flask(__name__)\n app.config.from_object(Config)\n app.config['JSON_AS_ASCII'] = False\n Config.init_app(app)\n \n # 启用CORS\n CORS(app)\n \n # 初始化各个管理器\n db_manager = DatabaseManager(Config.DATABASE_PATH)\n ai_processor = AIProcessor()\n kg_manager = KnowledgeGraphManager(db_manager)\n doc_manager = DocumentManager(db_manager, ai_processor)\n \n # 注册路由\n @app.route('/')\n def index():\n \"\"\"主页\"\"\"\n return render_template('index.html')\n \n @app.route('/static/<path:filename>')\n def serve_static(filename):\n return send_from_directory('static', filename)\n \n @app.route('/api/documents', methods=['GET', 'POST'])\n def handle_documents():\n \"\"\"文档管理API\"\"\"\n if request.method == 'GET':\n # 获取文档列表\n limit = request.args.get('limit', 20, type=int)\n offset = request.args.get('offset', 0, type=int)\n \n with db_manager.get_connection() as conn:\n documents = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?',\n (limit, offset)\n ).fetchall()\n \n return jsonify({\n 'success': True,\n 'data': [dict(doc) for doc in documents],\n 'total': conn.execute('SELECT COUNT(*) FROM documents WHERE is_deleted = 0').fetchone()[0]\n })\n \n elif request.method == 'POST':\n # 上传文档\n if 'file' not in request.files:\n return jsonify({'success': False, 'message': '没有上传文件'}), 400\n \n file = request.files['file']\n if file.filename == '':\n return jsonify({'success': False, 'message': '没有选择文件'}), 400\n \n if file:\n # 检查文件类型\n filename = file.filename\n file_ext = os.path.splitext(filename)[1][1:].lower()\n allowed_types = ['txt', 'md', 'pdf', 'docx', 'doc']\n \n if file_ext not in allowed_types:\n return jsonify({\n 'success': False, \n 'message': f'不支持的文件格式: {file_ext}. 支持的格式: {\", \".join(allowed_types)}'\n }), 400\n \n # 保存文件\n file_path = os.path.join(Config.UPLOAD_FOLDER, filename)\n file.save(file_path)\n \n # 处理文档\n doc_data = doc_manager.process_document(file_path, filename)\n if doc_data:\n doc_id = doc_manager.save_document(doc_data)\n if doc_id:\n # 如果处理成功,从关键词创建知识节点\n if doc_data['keywords'] and not doc_data['content'].startswith('['):\n try:\n for keyword in doc_data['keywords'][:5]: # 限制前5个关键词\n kg_manager.add_knowledge_node(keyword, 'concept', f'从文档\"{doc_data[\"title\"]}\"提取的关键词')\n except Exception as e:\n print(f\"创建知识节点时出错: {e}\")\n \n return jsonify({\n 'success': True,\n 'message': '文档上传成功',\n 'document_id': doc_id,\n 'processing_status': doc_data['metadata']['processing_status'],\n 'processing_message': doc_data['metadata']['processing_message']\n })\n \n return jsonify({'success': False, 'message': '文档处理失败'}), 500\n \n @app.route('/api/search')\n def search():\n \"\"\"搜索API\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n results = doc_manager.search_documents(query)\n return jsonify({\n 'success': True,\n 'data': results,\n 'query': query,\n 'count': len(results)\n })\n \n @app.route('/api/knowledge-graph')\n def get_knowledge_graph():\n \"\"\"获取知识图谱数据\"\"\"\n graph_data = kg_manager.get_graph_data()\n return jsonify({\n 'success': True,\n 'data': graph_data,\n 'node_count': len(graph_data['nodes']),\n 'edge_count': len(graph_data['edges'])\n })\n \n @app.route('/knowledge-graph')\n def knowledge_graph_page():\n \"\"\"知识图谱可视化页面\"\"\"\n return render_template('knowledge_graph.html')\n\n @app.route('/api/knowledge-graph/search')\n def search_knowledge_graph():\n \"\"\"搜索知识图谱\"\"\"\n query = request.args.get('q', '')\n if not query:\n return jsonify({'success': False, 'message': '搜索关键词不能为空'}), 400\n \n try:\n # 获取完整的图谱数据\n graph_data = kg_manager.get_graph_data()\n \n # 过滤包含搜索词的节点(不区分大小写)\n filtered_nodes = [\n node for node in graph_data['nodes'] \n if query.lower() in node['name'].lower() or \n (node.get('description') and query.lower() in node['description'].lower())\n ]\n \n # 获取过滤后节点的ID集合\n filtered_node_ids = {node['id'] for node in filtered_nodes}\n \n # 过滤相关的边(源节点或目标节点在过滤后的节点中)\n filtered_edges = [\n edge for edge in graph_data['edges'] \n if edge['source'] in filtered_node_ids or edge['target'] in filtered_node_ids\n ]\n \n return jsonify({\n 'success': True,\n 'data': {\n 'nodes': filtered_nodes,\n 'edges': filtered_edges\n },\n 'original_count': len(graph_data['nodes']),\n 'filtered_count': len(filtered_nodes)\n })\n \n except Exception as e:\n return jsonify({\n 'success': False, \n 'message': f'搜索出错: {str(e)}'\n }), 500\n \n @app.route('/api/recommendations/<int:user_id>')\n def get_recommendations(user_id):\n \"\"\"获取个性化推荐\"\"\"\n # 简化处理:基于用户最近的文档推荐相似的文档\n # 实际应用中需要更复杂的推荐算法\n \n with db_manager.get_connection() as conn:\n # 获取用户最近的文档\n recent_docs = conn.execute('''\n SELECT * FROM documents \n WHERE id IN (\n SELECT document_id FROM user_behaviors \n WHERE user_id = ? ORDER BY timestamp DESC LIMIT 5\n )\n ''', (str(user_id),)).fetchall()\n \n if not recent_docs:\n # 如果没有用户行为,推荐最新的文档\n recent_docs = conn.execute(\n 'SELECT * FROM documents WHERE is_deleted = 0 ORDER BY created_at DESC LIMIT 5'\n ).fetchall()\n \n # 基于关键词推荐相似文档\n recommendations = []\n for doc in recent_docs:\n if doc['tags']:\n try:\n tags = json.loads(doc['tags'])\n for tag in tags[:3]: # 使用前3个标签\n similar_docs = conn.execute('''\n SELECT * FROM documents \n WHERE tags LIKE ? AND id != ? AND is_deleted = 0\n ORDER BY created_at DESC LIMIT 3\n ''', (f'%{tag}%', doc['id'])).fetchall()\n \n for similar_doc in similar_docs:\n recommendations.append({\n 'source_document': dict(doc),\n 'recommended_document': dict(similar_doc),\n 'reason': f'基于关键词 \"{tag}\" 推荐',\n 'score': 0.8 # 简化的相似度评分\n })\n except:\n continue\n \n return jsonify({\n 'success': True,\n 'data': recommendations[:10], # 限制推荐数量\n 'based_on_documents': len(recent_docs)\n })\n \n @app.route('/api/system-info')\n def system_info():\n \"\"\"获取系统信息\"\"\"\n return jsonify({\n 'success': True,\n 'data': {\n 'name': '智能个人知识管理系统',\n 'version': '1.0.0',\n 'python_version': sys.version,\n 'spacy_model': Config.NLP_MODEL,\n 'pdf_support': PDF_AVAILABLE,\n 'docx_support': DOCX_AVAILABLE,\n 'upload_folder': Config.UPLOAD_FOLDER,\n 'database_path': Config.DATABASE_PATH\n }\n })\n \n return app\n\n# 主程序入口\nif __name__ == '__main__':\n app = create_app()\n \n # 开发模式下运行\n print(\"=\" * 60)\n print(\"智能个人知识管理系统 - 修复版本\")\n print(\"=\" * 60)\n print(f\"数据库路径: {Config.DATABASE_PATH}\")\n print(f\"上传目录: {Config.UPLOAD_FOLDER}\")\n print(f\"PDF支持: {'✓' if PDF_AVAILABLE else '✗'}\")\n print(f\"Word文档支持: {'✓' if DOCX_AVAILABLE else '✗'}\")\n print(\"=\" * 60)\n print(\"支持的文件格式: .txt, .md, .pdf, .docx, .doc\")\n print(\"访问地址: http://localhost:5000\")\n print(\"=\" * 60)\n \n app.run(debug=True, host='0.0.0.0', port=5000)"}}}}