From c32ed1ac9f64078a4f1aa615d74f4d784196ae7e Mon Sep 17 00:00:00 2001 From: Wcowin <1135801806@qq.com> Date: Wed, 4 Jun 2025 01:33:44 +0800 Subject: [PATCH] 25/6/4 --- .../plugin/git-committers/page-authors.json | 2 +- docs/about/geren.md | 2 +- .../__pycache__/ai_summary.cpython-311.pyc | Bin 27203 -> 41109 bytes docs/overrides/hooks/ai_summary.py | 973 ++++++++++++------ docs/overrides/hooks/test.py | 2 +- site/.ai_cache/service_config.json | 9 + 6 files changed, 677 insertions(+), 311 deletions(-) create mode 100644 site/.ai_cache/service_config.json diff --git a/.cache/plugin/git-committers/page-authors.json b/.cache/plugin/git-committers/page-authors.json index 554c7a3..d6004e2 100644 --- a/.cache/plugin/git-committers/page-authors.json +++ b/.cache/plugin/git-committers/page-authors.json @@ -1 +1 @@ -{"cache_date": "2025-06-03", "page_authors": {}} \ No newline at end of file +{"cache_date": "2025-06-04", "page_authors": {}} \ No newline at end of file diff --git a/docs/about/geren.md b/docs/about/geren.md index 5e53154..beefa5d 100644 --- a/docs/about/geren.md +++ b/docs/about/geren.md @@ -241,7 +241,7 @@ status: new !!! pied-piper1 "About me" - [x] Hey, I'm [Wcowin](https://wcowin.work/VitePress/){target=“_blank”}~ - [x] 咖啡重度爱好者 - - [x] 热爱(xiā)折腾技术/Math,目前的研究领域是[密码学](../blog/Cryptography/index.md) + - [x] 热爱(xiā)折腾技术/Math,目前的研究领域是[密码学](https://wcowin.work/blog/Cryptography/) - [x] 读书明志可识春秋;诗词爱好者;喜欢村上春树;擅长羽毛球 - [x] 清醒,知趣,明得失,知进退 diff --git a/docs/overrides/hooks/__pycache__/ai_summary.cpython-311.pyc b/docs/overrides/hooks/__pycache__/ai_summary.cpython-311.pyc index fcbce627799349b98490f7f1c9c32dddfcaac8b4..700047c122a54db50ad1e6583aa88f5af0aba1ec 100644 GIT binary patch literal 41109 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 SVwSsIo1xu;i1aS&i~kQ%zG!>^ literal 27203 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]+>') - self.image_pattern = re.compile(r'!\[.*?\]\([^)]+\)') - self.link_pattern = re.compile(r'\[([^\]]+)\]\([^)]+\)') + # 🌍 语言配置/Language Configuration + self.summary_language = 'en' # 默认中文,可选 'zh'、'en'、'both' - # 支持的编程语言 - self.programming_languages = frozenset({ - 'python', 'py', 'javascript', 'js', 'typescript', 'ts', 'java', 'cpp', 'c', - 'go', 'rust', 'php', 'ruby', 'swift', 'kotlin', 'csharp', 'cs', - 'bash', 'sh', 'powershell', 'ps1', 'zsh', 'fish', 'bat', 'cmd', - 'html', 'css', 'scss', 'sass', 'less', 'yaml', 'yml', 'json', 'xml', - 'toml', 'ini', 'conf', 'dockerfile', 'makefile', - 'sql', 'mysql', 'postgresql', 'sqlite', 'mongodb', - 'r', 'matlab', 'scala', 'perl', 'lua', 'dart', 'tex', 'latex', - 'csv', 'properties', '' - }) + # 在初始化时就进行环境检查 + self._check_environment() + + # 检查服务变更并处理缓存 + self._check_service_change() - def configure_folders(self, folders=None, exclude_patterns=None, exclude_files=None): + def _check_environment(self): + """初始化时检查环境""" + is_ci = self.is_ci_environment() + + if is_ci: + ci_name = self._get_ci_name() + if self.ci_config['enabled_in_ci']: + print(f"🚀 检测到 CI 环境 ({ci_name}),AI 摘要功能已启用") + self._should_run = True + else: + print(f"🚫 检测到 CI 环境 ({ci_name}),AI 摘要功能已禁用") + self._should_run = False + else: + # 本地环境检查 + if self.ci_config['enabled_in_local']: + print("💻 本地环境检测到,AI 摘要功能已启用") + self._should_run = True + else: + print("🚫 本地环境检测到,AI 摘要功能已禁用(仅在 CI 环境中启用)") + self._should_run = False + + def _check_service_change(self): + """检查AI服务是否发生变更,如有变更则自动清理缓存""" + current_config = { + 'default_service': self.default_service, + 'available_services': list(self.ai_services.keys()), + 'summary_language': self.summary_language, + 'check_time': datetime.now().isoformat() + } + + if self.service_config_file.exists(): + try: + with open(self.service_config_file, 'r', encoding='utf-8') as f: + previous_config = json.load(f) + + # 检查默认服务或语言是否变更 + if (previous_config.get('default_service') != current_config['default_service'] or + previous_config.get('summary_language') != current_config['summary_language']): + old_service = previous_config.get('default_service', 'unknown') + new_service = current_config['default_service'] + old_lang = previous_config.get('summary_language', 'zh') + new_lang = current_config['summary_language'] + + if old_service != new_service: + print(f"🔄 检测到AI服务变更: {old_service} → {new_service}") + if old_lang != new_lang: + print(f"🌍 检测到语言变更: {old_lang} → {new_lang}") + + print("🧹 自动清理AI摘要缓存...") + + try: + # 删除整个缓存目录 + if self.cache_dir.exists(): + shutil.rmtree(self.cache_dir) + print(f"✅ 已删除缓存文件夹: {self.cache_dir}") + + # 重新创建缓存目录 + self.cache_dir.mkdir(exist_ok=True) + print("📁 已重新创建缓存目录") + + except Exception as e: + print(f"❌ 清理缓存失败: {e}") + # 如果删除失败,尝试清理单个文件 + try: + self._clear_cache_files() + except: + print("⚠️ 缓存清理失败,新摘要可能会混用旧配置的缓存") + + except Exception as e: + print(f"读取服务配置失败: {e}") + + # 保存当前配置 + try: + with open(self.service_config_file, 'w', encoding='utf-8') as f: + json.dump(current_config, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"保存服务配置失败: {e}") + + def _clear_cache_files(self): + """清理缓存文件(备用方法)""" + cleared_count = 0 + try: + for cache_file in self.cache_dir.glob("*.json"): + if cache_file.name != "service_config.json": + cache_file.unlink() + cleared_count += 1 + print(f"✅ 已清理 {cleared_count} 个缓存文件") + except Exception as e: + print(f"❌ 单文件清理失败: {e}") + + def configure_ai_service(self, service_name, config=None): """ - 配置启用AI摘要的文件夹 + 配置AI服务 Args: - folders: 启用AI摘要的文件夹列表 - exclude_patterns: 排除的模式列表 - exclude_files: 排除的特定文件列表 + service_name: 服务名称 ('deepseek', 'openai', 'azure_openai', 'claude', 'gemini') + config: 服务配置字典 """ + old_service = self.default_service + + if config: + self.ai_services[service_name] = config + self.default_service = service_name + + # 如果服务发生变更,自动清理缓存 + if old_service != service_name: + print(f"🔄 AI服务已切换: {old_service} → {service_name}") + print("🧹 自动清理所有AI摘要缓存...") + + try: + if self.cache_dir.exists(): + shutil.rmtree(self.cache_dir) + print(f"✅ 已删除缓存文件夹: {self.cache_dir}") + + # 重新创建缓存目录 + self.cache_dir.mkdir(exist_ok=True) + print("📁 已重新创建缓存目录") + + except Exception as e: + print(f"❌ 清理缓存失败: {e}") + # 如果删除失败,尝试清理单个文件 + try: + self._clear_cache_files() + except: + print("⚠️ 缓存清理失败,新摘要可能会混用旧服务的缓存") + + # 更新服务配置记录 + self._check_service_change() + + def configure_language(self, language='zh'): + """ + 配置摘要语言 + + Args: + language: 语言设置 ('zh': 中文, 'en': 英文, 'both': 双语) + """ + old_language = self.summary_language + self.summary_language = language + + if old_language != language: + print(f"🌍 摘要语言已切换: {old_language} → {language}") + print("🧹 自动清理摘要缓存以应用新语言设置...") + + try: + if self.cache_dir.exists(): + shutil.rmtree(self.cache_dir) + print(f"✅ 已删除缓存文件夹: {self.cache_dir}") + + # 重新创建缓存目录 + self.cache_dir.mkdir(exist_ok=True) + print("📁 已重新创建缓存目录") + + except Exception as e: + print(f"❌ 清理缓存失败: {e}") + + # 更新服务配置记录 + self._check_service_change() + + def configure_folders(self, folders=None, exclude_patterns=None, exclude_files=None): + """配置启用AI摘要的文件夹""" if folders is not None: self.enabled_folders = folders if exclude_patterns is not None: @@ -81,8 +257,9 @@ class AISummaryGenerator: self.exclude_files = exclude_files def get_content_hash(self, content): - """生成内容hash用于缓存""" - return hashlib.md5(content.encode('utf-8')).hexdigest() + """生成内容hash用于缓存(包含语言设置)""" + content_with_lang = f"{content}_{self.summary_language}" + return hashlib.md5(content_with_lang.encode('utf-8')).hexdigest() def get_cached_summary(self, content_hash): """获取缓存的摘要""" @@ -104,6 +281,7 @@ class AISummaryGenerator: cache_file = self.cache_dir / f"{content_hash}.json" try: summary_data['timestamp'] = datetime.now().isoformat() + summary_data['language'] = self.summary_language with open(cache_file, 'w', encoding='utf-8') as f: json.dump(summary_data, f, ensure_ascii=False, indent=2) except Exception as e: @@ -152,10 +330,81 @@ class AISummaryGenerator: return content - def generate_ai_summary(self, content, page_title=""): - """使用DeepSeek生成摘要""" - # 优化的提示词 - prompt = f"""请为以下技术文章生成一个高质量的摘要,要求: + def build_headers(self, service_config): + """构建请求头""" + headers = { + 'Content-Type': 'application/json' + } + + # 根据服务类型添加认证头 + if 'azure_openai' in service_config.get('url', ''): + headers['api-key'] = service_config['api_key'] + elif 'anthropic.com' in service_config.get('url', ''): + headers['x-api-key'] = service_config['api_key'] + headers['anthropic-version'] = '2023-06-01' + elif 'googleapis.com' in service_config.get('url', ''): + # Google API使用URL参数 + pass + else: + # OpenAI和DeepSeek使用Bearer token + headers['Authorization'] = f"Bearer {service_config['api_key']}" + + # 添加额外的头部 + if 'headers_extra' in service_config: + headers.update(service_config['headers_extra']) + + return headers + + def build_payload(self, service_name, service_config, content, page_title): + """构建请求载荷""" + # 根据语言设置生成不同的prompt + if self.summary_language == 'en': + prompt = f"""Please generate a high-quality summary for the following technical article with these requirements: + +1. **Length Control**: Strictly limit to 80-120 words +2. **Content Requirements**: + - Accurately summarize the core themes and key points of the article + - Highlight technical features, application scenarios, or problems solved + - Use professional but understandable language + - Avoid repeating the article title content +3. **Format Requirements**: + - Return summary content directly without any prefix or suffix + - Use concise declarative sentences + - Technical terms are appropriate + +Article Title: {page_title} + +Article Content: +{content[:2500]} + +Please generate summary:""" + + elif self.summary_language == 'both': + prompt = f"""Please generate a bilingual summary (Chinese and English) for the following technical article with these requirements: + +1. **Length Control**: + - Chinese: 80-120 characters + - English: 80-120 words +2. **Content Requirements**: + - Accurately summarize the core themes and key points + - Highlight technical features, application scenarios, or problems solved + - Use professional but understandable language + - Avoid repeating the article title content +3. **Format Requirements**: + - First provide Chinese summary + - Then provide English summary + - Separate with a blank line + - No prefixes or additional formatting + +Article Title: {page_title} + +Article Content: +{content[:2500]} + +Please generate bilingual summary:""" + + else: # 默认中文 + prompt = f"""请为以下技术文章生成一个高质量的摘要,要求: 1. **长度控制**:严格控制在80-120字以内 2. **内容要求**: @@ -175,54 +424,161 @@ class AISummaryGenerator: 请生成摘要:""" - try: - payload = { - "model": self.api_config['model'], + if service_name == 'claude': + # Claude API格式 + return { + "model": service_config['model'], + "max_tokens": service_config['max_tokens'], + "temperature": service_config['temperature'], + "messages": [ + { + "role": "user", + "content": prompt + } + ] + } + elif service_name == 'gemini': + # Gemini API格式 + return { + "contents": [ + { + "parts": [ + { + "text": prompt + } + ] + } + ], + "generationConfig": { + "temperature": service_config['temperature'], + "maxOutputTokens": service_config['max_tokens'] + } + } + else: + # OpenAI格式 (OpenAI, DeepSeek, Azure OpenAI) + system_content = { + 'zh': "你是一个专业的技术文档摘要专家,擅长提取文章核心要点并生成简洁准确的中文摘要。", + 'en': "You are a professional technical documentation summary expert, skilled at extracting core points from articles and generating concise and accurate English summaries.", + 'both': "You are a professional technical documentation summary expert, skilled at extracting core points from articles and generating concise and accurate bilingual summaries in both Chinese and English." + } + + return { + "model": service_config['model'], "messages": [ { "role": "system", - "content": "你是一个专业的技术文档摘要专家,擅长提取文章核心要点并生成简洁准确的摘要。" + "content": system_content.get(self.summary_language, system_content['zh']) }, { - "role": "user", + "role": "user", "content": prompt } ], - "max_tokens": 150, - "temperature": 0.3, # 降低随机性,提高准确性 + "max_tokens": service_config['max_tokens'] * (2 if self.summary_language == 'both' else 1), + "temperature": service_config['temperature'], "top_p": 0.9 } + + def extract_response_content(self, service_name, response_data): + """从响应中提取内容""" + try: + if service_name == 'claude': + return response_data['content'][0]['text'] + elif service_name == 'gemini': + return response_data['candidates'][0]['content']['parts'][0]['text'] + else: + # OpenAI格式 + return response_data['choices'][0]['message']['content'] + except (KeyError, IndexError) as e: + print(f"解析{service_name}响应失败: {e}") + return None + + def generate_ai_summary_with_service(self, content, page_title, service_name): + """使用指定服务生成摘要""" + if service_name not in self.ai_services: + print(f"不支持的AI服务: {service_name}") + return None + + service_config = self.ai_services[service_name] + + # 检查API密钥 + if not service_config['api_key'] or service_config['api_key'].startswith('your-'): + print(f"{service_name} API密钥未配置") + return None + + try: + headers = self.build_headers(service_config) + payload = self.build_payload(service_name, service_config, content, page_title) + + # 对于Google API,添加API密钥到URL + url = service_config['url'] + if service_name == 'gemini': + url = f"{url}?key={service_config['api_key']}" response = requests.post( - self.api_config['url'], - headers=self.api_config['headers'], + url, + headers=headers, json=payload, timeout=30 ) if response.status_code == 200: result = response.json() - summary = result['choices'][0]['message']['content'].strip() + summary = self.extract_response_content(service_name, result) - # 清理可能的格式问题 - summary = re.sub(r'^["""''`]+|["""''`]+$', '', summary) - summary = re.sub(r'^\s*摘要[::]\s*', '', summary) - summary = re.sub(r'^\s*总结[::]\s*', '', summary) + if summary: + # 清理可能的格式问题 + summary = re.sub(r'^["""''`]+|["""''`]+$', '', summary.strip()) + summary = re.sub(r'^\s*摘要[::]\s*', '', summary) + summary = re.sub(r'^\s*总结[::]\s*', '', summary) + summary = re.sub(r'^\s*Summary[::]\s*', '', summary) + summary = re.sub(r'^\s*Abstract[::]\s*', '', summary) + return summary - return summary else: - print(f"DeepSeek API请求失败: {response.status_code} - {response.text}") + print(f"{service_name} API请求失败: {response.status_code} - {response.text}") return None except requests.exceptions.RequestException as e: - print(f"DeepSeek API请求异常: {e}") + print(f"{service_name} API请求异常: {e}") return None except Exception as e: - print(f"AI摘要生成异常: {e}") + print(f"{service_name} 摘要生成异常: {e}") return None + def generate_ai_summary(self, content, page_title=""): + """生成AI摘要(支持CI环境策略)""" + is_ci = self.is_ci_environment() + + # 如果在 CI 环境中且配置为只使用缓存 + if is_ci and self.ci_config['ci_only_cache']: + print(f"📦 CI 环境仅使用缓存模式") + return None, 'ci_cache_only' + + # 按优先级尝试不同服务 + services_to_try = [self.default_service] + [s for s in self.service_fallback_order if s != self.default_service] + + for service_name in services_to_try: + if service_name in self.ai_services: + lang_desc = {'zh': '中文', 'en': '英文', 'both': '双语'} + env_desc = '(CI)' if is_ci else '(本地)' + print(f"🔄 尝试使用 {service_name} 生成{lang_desc.get(self.summary_language, '中文')}摘要 {env_desc}...") + summary = self.generate_ai_summary_with_service(content, page_title, service_name) + if summary: + return summary, service_name + + print("⚠️ 所有AI服务均不可用") + return None, None + def generate_fallback_summary(self, content, page_title=""): - """生成备用摘要(基于规则的智能摘要)""" + """生成备用摘要(考虑CI环境配置)""" + is_ci = self.is_ci_environment() + + # 如果在 CI 环境中且禁用了备用摘要 + if is_ci and not self.ci_config['ci_fallback_enabled']: + print(f"🚫 CI 环境禁用备用摘要") + return None + # 移除格式符号 clean_text = re.sub(r'^#+\s*', '', content, flags=re.MULTILINE) clean_text = re.sub(r'\*\*([^*]+)\*\*', r'\1', clean_text) @@ -272,87 +628,144 @@ class AISummaryGenerator: # 简化冗长的摘要 if len(summary) > 120: summary = selected_sentences[0] + '.' - return summary - else: - # 根据标题生成通用摘要 - if any(keyword in page_title for keyword in ['教程', '指南', 'Tutorial']): - return '本文提供了详细的教程指南,通过实例演示帮助读者掌握相关技术要点。' - elif any(keyword in page_title for keyword in ['配置', '设置', '安装', 'Config']): - return '本文介绍了系统配置的方法和步骤,提供实用的设置建议和最佳实践。' - elif any(keyword in page_title for keyword in ['开发', '编程', 'Development']): - return '本文分享了开发经验和技术实践,提供了实用的代码示例和解决方案。' + + # 根据语言设置生成不同的备用摘要 + if self.summary_language == 'en': + return self._generate_english_fallback(page_title) + elif self.summary_language == 'both': + zh_summary = summary + en_summary = self._generate_english_fallback(page_title) + return f"{zh_summary}\n\n{en_summary}" else: - return '本文深入探讨了相关技术内容,提供了实用的方法和解决方案。' + return summary + else: + # 根据标题和语言生成通用摘要 + if self.summary_language == 'en': + return self._generate_english_fallback(page_title) + elif self.summary_language == 'both': + zh_summary = self._generate_chinese_fallback(page_title) + en_summary = self._generate_english_fallback(page_title) + return f"{zh_summary}\n\n{en_summary}" + else: + return self._generate_chinese_fallback(page_title) + + def is_ci_environment(self): + """检测是否在 CI 环境中运行""" + # 常见的 CI 环境变量 + ci_indicators = [ + 'CI', 'CONTINUOUS_INTEGRATION', # 通用 CI 标识 + 'GITHUB_ACTIONS', # GitHub Actions + 'GITLAB_CI', # GitLab CI + 'JENKINS_URL', # Jenkins + 'TRAVIS', # Travis CI + 'CIRCLECI', # CircleCI + 'AZURE_HTTP_USER_AGENT', # Azure DevOps + 'TEAMCITY_VERSION', # TeamCity + 'BUILDKITE', # Buildkite + 'CODEBUILD_BUILD_ID', # AWS CodeBuild + 'NETLIFY', # Netlify + 'VERCEL', # Vercel + 'CF_PAGES', # Cloudflare Pages + ] + + for indicator in ci_indicators: + if os.getenv(indicator): + return True + + return False + + def should_run_in_current_environment(self): + """判断是否应该在当前环境中运行 AI 摘要""" + return self._should_run + + def _get_ci_name(self): + """获取 CI 环境名称""" + if os.getenv('GITHUB_ACTIONS'): + return 'GitHub Actions' + elif os.getenv('GITLAB_CI'): + return 'GitLab CI' + elif os.getenv('JENKINS_URL'): + return 'Jenkins' + elif os.getenv('TRAVIS'): + return 'Travis CI' + elif os.getenv('CIRCLECI'): + return 'CircleCI' + elif os.getenv('AZURE_HTTP_USER_AGENT'): + return 'Azure DevOps' + elif os.getenv('NETLIFY'): + return 'Netlify' + elif os.getenv('VERCEL'): + return 'Vercel' + elif os.getenv('CF_PAGES'): + return 'Cloudflare Pages' + elif os.getenv('CODEBUILD_BUILD_ID'): + return 'AWS CodeBuild' + else: + return 'Unknown CI' def process_page(self, markdown, page, config): - """处理页面,生成AI摘要和阅读统计""" - # 检查是否需要显示阅读信息 - show_reading_info = self.should_show_reading_info(page, markdown) - - # 检查是否需要生成AI摘要 - should_generate_ai_summary = self.should_generate_summary(page, markdown) - - # 如果两者都不需要,直接返回原内容 - if not show_reading_info and not should_generate_ai_summary: + """处理页面,生成AI摘要(支持CI环境检测)""" + # 检查是否应该在当前环境运行 + if not self.should_run_in_current_environment(): return markdown - # 计算阅读统计 - reading_time, chinese_chars, code_lines = self.calculate_reading_stats(markdown) + if not self.should_generate_summary(page, markdown): + return markdown - result_blocks = [] + clean_content = self.clean_content_for_ai(markdown) - # 处理AI摘要 - if should_generate_ai_summary: - clean_content = self.clean_content_for_ai(markdown) + # 内容长度检查 + if len(clean_content) < 100: + print(f"📄 内容太短,跳过摘要生成: {page.file.src_path}") + return markdown + + content_hash = self.get_content_hash(clean_content) + page_title = getattr(page, 'title', '') + is_ci = self.is_ci_environment() + + # 检查缓存 + cached_summary = self.get_cached_summary(content_hash) + if cached_summary: + summary = cached_summary.get('summary', '') + ai_service = cached_summary.get('service', 'cached') + env_desc = '(CI)' if is_ci else '(本地)' + print(f"✅ 使用缓存摘要 {env_desc}: {page.file.src_path}") + else: + # 生成新摘要 + lang_desc = {'zh': '中文', 'en': '英文', 'both': '双语'} + env_desc = '(CI)' if is_ci else '(本地)' + print(f"🤖 正在生成{lang_desc.get(self.summary_language, '中文')}AI摘要 {env_desc}: {page.file.src_path}") + summary, ai_service = self.generate_ai_summary(clean_content, page_title) - # 内容长度检查 - if len(clean_content) >= 200: - content_hash = self.get_content_hash(clean_content) - page_title = getattr(page, 'title', '') - - # 检查缓存 - cached_summary = self.get_cached_summary(content_hash) - if cached_summary: - summary = cached_summary.get('summary', '') - ai_service = 'cached' - print(f"✅ 使用缓存摘要: {page.file.src_path}") + if not summary: + # 尝试生成备用摘要 + summary = self.generate_fallback_summary(clean_content, page_title) + if summary: + ai_service = 'fallback' + print(f"📝 使用备用摘要 {env_desc}: {page.file.src_path}") else: - # 生成新摘要 - print(f"🤖 正在生成AI摘要: {page.file.src_path}") - summary = self.generate_ai_summary(clean_content, page_title) - - if not summary: - summary = self.generate_fallback_summary(clean_content, page_title) - ai_service = 'fallback' - print(f"📝 使用备用摘要: {page.file.src_path}") - else: - ai_service = 'deepseek' - print(f"✅ AI摘要生成成功: {page.file.src_path}") - - # 保存到缓存 - self.save_summary_cache(content_hash, { - 'summary': summary, - 'service': ai_service, - 'page_title': page_title - }) - - # 添加AI摘要块 - ai_summary_block = self.format_ai_summary(summary, ai_service) - result_blocks.append(ai_summary_block) + print(f"❌ 无法生成摘要 {env_desc}: {page.file.src_path}") + return markdown + else: + print(f"✅ AI摘要生成成功 ({ai_service}) {env_desc}: {page.file.src_path}") + + # 保存到缓存 + if summary: + self.save_summary_cache(content_hash, { + 'summary': summary, + 'service': ai_service, + 'page_title': page_title + }) - # 添加阅读信息块 - if show_reading_info: - reading_info_block = self.format_reading_info(reading_time, chinese_chars, code_lines) - result_blocks.append(reading_info_block) - - # 合并所有块并返回 - if result_blocks: - return '\n'.join(result_blocks) + '\n\n' + markdown + # 添加摘要到页面最上面 + if summary: + summary_html = self.format_summary(summary, ai_service) + return summary_html + '\n\n' + markdown else: return markdown def should_generate_summary(self, page, markdown): - """判断是否应该生成摘要 - 可自定义文件夹""" + """判断是否应该生成摘要""" # 检查页面元数据 if hasattr(page, 'meta'): # 明确禁用 @@ -378,203 +791,147 @@ class AISummaryGenerator: for folder in self.enabled_folders: if src_path.startswith(folder) or f'/{folder}' in src_path: folder_name = folder.rstrip('/') - print(f"🎯 {folder_name}文件夹文章检测到,启用AI摘要: {src_path}") + lang_desc = {'zh': '中文', 'en': '英文', 'both': '双语'} + print(f"🎯 {folder_name}文件夹文章检测到,启用{lang_desc.get(self.summary_language, '中文')}AI摘要: {src_path}") return True # 默认不生成摘要 return False - def calculate_reading_stats(self, markdown): - """计算中文字符数和代码行数""" - # 清理内容用于中文字符统计 - content = markdown - content = self.yaml_front_pattern.sub('', content) - content = self.html_tag_pattern.sub('', content) - content = self.image_pattern.sub('', content) - content = self.link_pattern.sub(r'\1', content) - content = self.code_block_pattern.sub('', content) - content = self.inline_code_pattern.sub('', content) - - chinese_chars = len(self.chinese_chars_pattern.findall(content)) - - # 统计代码行数 - code_lines = self.count_code_lines(markdown) - - # 计算阅读时间(中文:400字/分钟) - reading_time = max(1, round(chinese_chars / 400)) - - return reading_time, chinese_chars, code_lines - - def count_code_lines(self, markdown): - """统计代码行数""" - code_blocks = self.code_block_pattern.findall(markdown) - total_code_lines = 0 - - for block in code_blocks: - # 提取语言标识 - lang_match = re.match(r'^```(\w*)', block) - language = lang_match.group(1).lower() if lang_match else '' - - # 移除开头的语言标识和结尾的``` - code_content = re.sub(r'^```\w*\n?', '', block) - code_content = re.sub(r'\n?```$', '', code_content) - - # 过滤空代码块 - if not code_content.strip(): - continue - - # 计算有效行数 - lines = [line for line in code_content.split('\n') if line.strip()] - line_count = len(lines) - - # 如果有明确的编程语言标识,直接统计 - if language and language in self.programming_languages: - total_code_lines += line_count - continue - - # 检测是否为代码内容 - is_code = self.is_code_content(code_content) - - if is_code: - total_code_lines += line_count - - return total_code_lines - - def is_code_content(self, content): - """判断内容是否为代码""" - # 命令行检测 - command_indicators = [ - 'sudo ', 'npm ', 'pip ', 'git ', 'cd ', 'ls ', 'mkdir ', 'rm ', 'cp ', 'mv ', - 'chmod ', 'chown ', 'grep ', 'find ', 'ps ', 'kill ', 'top ', 'cat ', 'echo ', - 'wget ', 'curl ', 'tar ', 'zip ', 'unzip ', 'ssh ', 'scp ', 'rsync ', - '$ ', '# ', '% ', '> ', 'C:\\>', 'PS>', '#!/', - '/Applications/', '/usr/', '/etc/', '/var/', '/home/', '~/', - ] - - if any(indicator in content for indicator in command_indicators): - return True - - # 编程语法检测 - programming_indicators = [ - 'def ', 'class ', 'import ', 'from ', 'return ', 'function', 'var ', 'let ', 'const ', - 'public ', 'private ', 'protected ', 'static ', 'void ', 'int ', 'string ', - '==', '!=', '<=', '>=', '&&', '||', '++', '--', '+=', '-=', - 'while ', 'for ', 'if ', 'else:', 'switch ', 'case ', - ' 1 and any(line.startswith(' ') or line.startswith('\t') for line in lines): - return True - - if '<' in content and '>' in content: - return True - - if any(char in content for char in ['{', '}', '(', ')', '[', ']']) and ('=' in content or ':' in content): - return True - - return False - - def should_show_reading_info(self, page, markdown): - """判断是否应该显示阅读信息""" - # 检查页面元数据 - if page.meta.get('hide_reading_time', False): - return False - - # 获取文件路径 - src_path = page.file.src_path.replace('\\', '/') - - # 使用现有的排除模式检查 - exclude_patterns = [ - r'^index\.md$', r'^about/', r'^trip/index\.md$', r'^relax/index\.md$', - r'^blog/indexblog\.md$', r'^blog/posts\.md$', r'^develop/index\.md$', - r'waline\.md$', r'link\.md$', r'404\.md$' - ] - - for pattern in exclude_patterns: - if re.match(pattern, src_path): - return False - - # 检查页面类型 - page_type = page.meta.get('type', '') - if page_type in {'landing', 'special', 'widget'}: - return False - - # 内容长度检查 - if len(markdown) < 300: - return False - - # 计算中文字符数 - _, chinese_chars, _ = self.calculate_reading_stats(markdown) - if chinese_chars < 50: - return False - - return True - - def format_ai_summary(self, summary, ai_service): - """格式化AI摘要显示""" - service_config = { - 'deepseek': { - 'icon': '🤖', - 'name': 'AI智能摘要', - 'color': 'info' + def format_summary(self, summary, ai_service): + """格式化摘要显示(包含CI环境标识)""" + # 根据语言设置显示不同的标题 + service_names = { + 'zh': { + 'deepseek': 'AI智能摘要 (DeepSeek)', + 'openai': 'AI智能摘要 (ChatGPT)', + 'azure_openai': 'AI智能摘要 (Azure OpenAI)', + 'claude': 'AI智能摘要 (Claude)', + 'gemini': 'AI智能摘要 (Gemini)', + 'fallback': '自动摘要', + 'cached': 'AI智能摘要', + 'ci_cache_only': 'AI智能摘要 (缓存)' }, - 'fallback': { - 'icon': '📝', - 'name': '自动摘要', - 'color': 'tip' + 'en': { + 'deepseek': 'AI Summary (DeepSeek)', + 'openai': 'AI Summary (ChatGPT)', + 'azure_openai': 'AI Summary (Azure OpenAI)', + 'claude': 'AI Summary (Claude)', + 'gemini': 'AI Summary (Gemini)', + 'fallback': 'Auto Summary', + 'cached': 'AI Summary', + 'ci_cache_only': 'AI Summary (Cached)' }, - 'cached': { - 'icon': '🤖', - 'name': 'AI智能摘要', - 'color': 'info' + 'both': { + 'deepseek': 'AI智能摘要 / AI Summary (DeepSeek)', + 'openai': 'AI智能摘要 / AI Summary (ChatGPT)', + 'azure_openai': 'AI智能摘要 / AI Summary (Azure OpenAI)', + 'claude': 'AI智能摘要 / AI Summary (Claude)', + 'gemini': 'AI智能摘要 / AI Summary (Gemini)', + 'fallback': '自动摘要 / Auto Summary', + 'cached': 'AI智能摘要 / AI Summary', + 'ci_cache_only': 'AI智能摘要 / AI Summary (缓存)' } } - config = service_config.get(ai_service, service_config['deepseek']) + name_config = service_names.get(self.summary_language, service_names['zh']) + service_name = name_config.get(ai_service, name_config['fallback']) - return f'''??? {config['color']} "{config['icon']} {config['name']}" - {summary}''' - - def format_reading_info(self, reading_time, chinese_chars, code_lines): - """格式化阅读信息显示""" - if code_lines > 0: - return f'''!!! info "📖 阅读信息" - 阅读时间:**{reading_time}** 分钟 | 中文字符:**{chinese_chars}** | 有效代码行数:**{code_lines}**''' - else: - return f'''!!! info "📖 阅读信息" - 阅读时间:**{reading_time}** 分钟 | 中文字符:**{chinese_chars}**''' - - def format_summary(self, summary, ai_service): - """保持向后兼容的格式化方法""" - return self.format_ai_summary(summary, ai_service) + # 图标和颜色配置 + icon = '💾' if ai_service not in ['fallback', 'ci_cache_only'] else '📝' + color = 'info' if ai_service not in ['fallback', 'ci_cache_only'] else 'tip' + + return f'''!!! {color} "{icon} {service_name}" + {summary} + +''' # 创建全局实例 ai_summary_generator = AISummaryGenerator() -# 🔧 自定义配置函数 -def configure_ai_summary(enabled_folders=None, exclude_patterns=None, exclude_files=None): +# 🔧 配置函数 +def configure_ai_summary(enabled_folders=None, exclude_patterns=None, exclude_files=None, + ai_service=None, service_config=None, language='zh', + ci_enabled=None, local_enabled=None, ci_only_cache=None, ci_fallback=None): """ - 配置AI摘要功能 + 配置AI摘要功能(支持CI和本地环境分别配置) Args: - enabled_folders: 启用AI摘要的文件夹列表,例如 ['blog/', 'docs/', 'posts/'] - exclude_patterns: 排除的模式列表,例如 ['404.md', '/admin/'] - exclude_files: 排除的特定文件列表,例如 ['blog/index.md'] + enabled_folders: 启用AI摘要的文件夹列表 + exclude_patterns: 排除的模式列表 + exclude_files: 排除的特定文件列表 + ai_service: 使用的AI服务 ('deepseek', 'openai', 'claude', 'gemini') + service_config: AI服务配置 + language: 摘要语言 ('zh': 中文, 'en': 英文, 'both': 双语) + ci_enabled: 是否在 CI 环境中启用 + local_enabled: 是否在本地环境中启用 + ci_only_cache: CI 环境是否仅使用缓存 + ci_fallback: CI 环境是否启用备用摘要 Example: - # 只在blog和docs文件夹启用 - configure_ai_summary(['blog/', 'docs/']) - - # 在所有文件夹启用,但排除特定文件 - configure_ai_summary([''], exclude_files=['index.md', 'about.md']) + # 推荐配置:只在 CI 中启用,本地禁用 + configure_ai_summary( + enabled_folders=['blog/', 'docs/'], + language='zh', + ci_enabled=True, # CI 中启用 + local_enabled=False, # 本地禁用 + ci_only_cache=False, # CI 中允许生成新摘要 + ci_fallback=True # CI 中启用备用摘要 + ) """ ai_summary_generator.configure_folders(enabled_folders, exclude_patterns, exclude_files) + ai_summary_generator.configure_language(language) + + # 配置环境行为 + if any(x is not None for x in [ci_enabled, local_enabled, ci_only_cache, ci_fallback]): + configure_ci_behavior(ci_enabled, local_enabled, ci_only_cache, ci_fallback) + + if ai_service: + if service_config: + # 合并配置 + current_config = ai_summary_generator.ai_services.get(ai_service, {}) + current_config.update(service_config) + ai_summary_generator.configure_ai_service(ai_service, current_config) + else: + ai_summary_generator.configure_ai_service(ai_service) + +# 🔧 新增 CI 配置函数 +def configure_ci_behavior(enabled_in_ci=None, enabled_in_local=None, ci_only_cache=None, ci_fallback_enabled=None): + """ + 配置 CI 和本地环境行为 + + Args: + enabled_in_ci: 是否在 CI 环境中启用 AI 摘要 + enabled_in_local: 是否在本地环境中启用 AI 摘要 + ci_only_cache: CI 环境是否仅使用缓存 + ci_fallback_enabled: CI 环境是否启用备用摘要 + + Example: + # 只在 CI 中启用,本地禁用(推荐配置) + configure_ci_behavior(enabled_in_ci=True, enabled_in_local=False) + + # 本地和 CI 都启用 + configure_ci_behavior(enabled_in_ci=True, enabled_in_local=True) + + # 只在本地启用,CI 中禁用 + configure_ci_behavior(enabled_in_ci=False, enabled_in_local=True) + """ + if enabled_in_ci is not None: + ai_summary_generator.ci_config['enabled_in_ci'] = enabled_in_ci + print(f"✅ CI 环境 AI 摘要: {'启用' if enabled_in_ci else '禁用'}") + + if enabled_in_local is not None: + ai_summary_generator.ci_config['enabled_in_local'] = enabled_in_local + print(f"✅ 本地环境 AI 摘要: {'启用' if enabled_in_local else '禁用'}") + + if ci_only_cache is not None: + ai_summary_generator.ci_config['ci_only_cache'] = ci_only_cache + print(f"✅ CI 环境仅缓存模式: {'启用' if ci_only_cache else '禁用'}") + + if ci_fallback_enabled is not None: + ai_summary_generator.ci_config['ci_fallback_enabled'] = ci_fallback_enabled + print(f"✅ CI 环境备用摘要: {'启用' if ci_fallback_enabled else '禁用'}") def on_page_markdown(markdown, page, config, files): - """MkDocs hook入口点 - 统一处理AI摘要和阅读统计""" + """MkDocs hook入口点""" return ai_summary_generator.process_page(markdown, page, config) \ No newline at end of file diff --git a/docs/overrides/hooks/test.py b/docs/overrides/hooks/test.py index 777e6cc..e2f8638 100644 --- a/docs/overrides/hooks/test.py +++ b/docs/overrides/hooks/test.py @@ -18,7 +18,7 @@ class AISummaryGenerator: 'model': 'deepseek-chat', 'headers': { 'Content-Type': 'application/json', - 'Authorization': 'Bearer sk-7dbcd6e21fb3417299b50aecff76c7bf' + 'Authorization': 'Bearer sk-' } } diff --git a/site/.ai_cache/service_config.json b/site/.ai_cache/service_config.json new file mode 100644 index 0000000..35a4e68 --- /dev/null +++ b/site/.ai_cache/service_config.json @@ -0,0 +1,9 @@ +{ + "default_service": "deepseek", + "available_services": [ + "deepseek", + "openai" + ], + "summary_language": "en", + "check_time": "2025-06-04T01:31:56.082195" +} \ No newline at end of file