{"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/')\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/', 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//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/', 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/', 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/', 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//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//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//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//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//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/')\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//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/', 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/')\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/', 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//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/', 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/', 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//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//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//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//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//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/')\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//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/', 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/')\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/', 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//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/', 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/', 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//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//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//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//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//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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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//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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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/', 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/')\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/', 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//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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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/', 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/')\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/', 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/', 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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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/', 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/')\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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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/', 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/')\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/', defaults={'user_id': 1})\n @app.route('/api/recommendations/')\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/')\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/', 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/')\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/')\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)"}}}}