├── data ├── tools │ ├── start.json │ ├── featured.json │ ├── tool_candidates.json │ ├── devops.json │ ├── design.json │ ├── mcp.json │ ├── review.json │ ├── doc.json │ ├── cli.json │ ├── ui.json │ ├── ai-test.json │ └── plugin.json ├── articles │ ├── ai_coding.json │ ├── ai_candidates.json │ └── ai_articles.json ├── weekly │ ├── .gitkeep │ ├── 2025weekly51.md │ ├── 2025weekly50.md │ ├── 2025weekly48.md │ └── 2025weekly49.md ├── config.json ├── README.md └── prompts │ └── prompts.json ├── app ├── data │ ├── articles │ │ ├── ai_candidates.json │ │ └── ai_articles.json │ └── tools │ │ └── tool_candidates.json ├── __init__.py ├── presentation │ ├── static │ │ └── wechat_mp_qr.jpg │ ├── __init__.py │ └── routes │ │ ├── __init__.py │ │ └── wechat.py ├── infrastructure │ ├── crawlers │ │ ├── __init__.py │ │ ├── rss.py │ │ ├── hackernews.py │ │ └── github_trending.py │ ├── __init__.py │ ├── notifiers │ │ ├── __init__.py │ │ └── wecom.py │ ├── db │ │ ├── __init__.py │ │ ├── database.py │ │ └── models.py │ ├── logging.py │ ├── file_lock.py │ └── scheduler.py ├── domain │ ├── digest │ │ ├── __init__.py │ │ ├── models.py │ │ └── render.py │ └── sources │ │ ├── __init__.py │ │ ├── tool_candidates.py │ │ ├── article_sources.py │ │ └── ai_candidates.py └── services │ ├── __init__.py │ ├── devmaster_news_service.py │ ├── crawler_service.py │ └── digest_service.py ├── tests ├── __init__.py ├── test_crawlers.py └── test_data_loader.py ├── .htaccess ├── .well-known └── pki-validation │ └── fileauth.txt ├── data.db ├── config ├── digest_schedule.json ├── wecom_template.json ├── crawler_keywords.json └── tool_keywords.json ├── pytest.ini ├── requirements.txt ├── .github └── workflows │ └── test.yml ├── .gitignore ├── pyproject.toml ├── LICENSE ├── docs ├── deploy │ ├── start.bat │ ├── start.sh │ ├── deploy_windows.md │ ├── deploy_python.md │ └── wechat_mp_guide.md ├── feature │ ├── v2.0_update_summary.md │ ├── tool_article_crawling_feature.md │ ├── 资讯分类映射关系.md │ ├── 资讯数据模型.md │ ├── v3.0_update_summary.md │ ├── tool_detail_feature.md │ ├── test_sources.md │ └── multi_sources_guide.md ├── technical │ └── ARCHITECTURE.md └── README.md ├── scripts ├── update_claude_code_subcategories.py ├── add_tool_identifiers.py ├── test_wechat_mp.py ├── test_sources.py ├── remove_expired_articles.py └── import_prompts_from_text.py ├── TODO.md ├── check_duplicate_jobs.py └── CHANGELOG.md /data/tools/start.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /data/articles/ai_coding.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /data/tools/featured.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /data/tools/tool_candidates.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /app/data/articles/ai_candidates.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /app/data/tools/tool_candidates.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /data/articles/ai_candidates.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """测试模块""" 2 | 3 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # 请将伪静态规则或自定义Apache配置填写到此处 2 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["create_app"] 2 | 3 | 4 | -------------------------------------------------------------------------------- /.well-known/pki-validation/fileauth.txt: -------------------------------------------------------------------------------- 1 | 50k90AFTYLbrrcHmmz1F6FoYMn10GQJZ -------------------------------------------------------------------------------- /data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunlongwen/AI-CodeNexus/HEAD/data.db -------------------------------------------------------------------------------- /data/weekly/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunlongwen/AI-CodeNexus/HEAD/data/weekly/.gitkeep -------------------------------------------------------------------------------- /config/digest_schedule.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_articles_per_keyword": 3, 3 | "hour": 14, 4 | "count": 5, 5 | "cron": "0 14 * * 1-5" 6 | } -------------------------------------------------------------------------------- /app/presentation/static/wechat_mp_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunlongwen/AI-CodeNexus/HEAD/app/presentation/static/wechat_mp_qr.jpg -------------------------------------------------------------------------------- /app/presentation/__init__.py: -------------------------------------------------------------------------------- 1 | """表示层:HTML模板和前端相关""" 2 | 3 | from .templates import get_index_html 4 | 5 | __all__ = ["get_index_html"] 6 | 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | asyncio_mode = auto 7 | 8 | -------------------------------------------------------------------------------- /app/presentation/routes/__init__.py: -------------------------------------------------------------------------------- 1 | """路由模块""" 2 | from . import wechat, digest, ai_assistant, api 3 | 4 | __all__ = ["wechat", "digest", "ai_assistant", "api"] 5 | -------------------------------------------------------------------------------- /app/infrastructure/crawlers/__init__.py: -------------------------------------------------------------------------------- 1 | """爬虫模块""" 2 | # 导出所有爬虫 3 | from . import devmaster, github_trending, hackernews, rss, sogou_wechat 4 | 5 | __all__ = ["devmaster", "github_trending", "hackernews", "rss", "sogou_wechat"] 6 | 7 | -------------------------------------------------------------------------------- /app/domain/digest/__init__.py: -------------------------------------------------------------------------------- 1 | """摘要领域模型""" 2 | from .models import ArticleItem, DailyDigest 3 | from .render import render_digest_for_mp, DAILY_TEMPLATE 4 | 5 | __all__ = ["ArticleItem", "DailyDigest", "render_digest_for_mp", "DAILY_TEMPLATE"] 6 | 7 | -------------------------------------------------------------------------------- /app/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | """基础设施层:日志、文件锁、调度器等底层组件""" 2 | 3 | from .logging import setup_logging 4 | from .file_lock import FileLock 5 | from .scheduler import SchedulerManager 6 | 7 | __all__ = ["setup_logging", "FileLock", "SchedulerManager"] 8 | 9 | -------------------------------------------------------------------------------- /app/infrastructure/notifiers/__init__.py: -------------------------------------------------------------------------------- 1 | """通知服务模块""" 2 | from .wecom import build_wecom_digest_markdown, send_markdown_to_wecom 3 | from .wechat_mp import WeChatMPClient 4 | 5 | __all__ = ["build_wecom_digest_markdown", "send_markdown_to_wecom", "WeChatMPClient"] 6 | -------------------------------------------------------------------------------- /app/data/articles/ai_articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "AI编码实践:从Vibe Coding到SDD", 4 | "url": "https://mp.weixin.qq.com/s/xVff9O2DPLssbfzp_GRwXQ", 5 | "source": "淘特用户技术团队", 6 | "summary": "本文系统回顾了淘特导购团队在AI编码实践中的演进历程,从初期的代码智能补全到Agent Coding再到引入Rules约束,最终探索SDD。" 7 | } 8 | ] -------------------------------------------------------------------------------- /config/wecom_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "**AI 编程优质文章推荐|{date}**", 3 | "theme": "> 今日主题:{theme}", 4 | "item": { 5 | "title": "{idx}. [{title}]({url})", 6 | "source": " - 来源:{source}", 7 | "summary": " - 摘要:{summary}" 8 | }, 9 | "footer": ">更多信息看云龙让AI开发的网站:AI-CodeNexus https://aicoding.100kwhy.fun" 10 | } -------------------------------------------------------------------------------- /data/articles/ai_articles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Cursor Rules 四大原则:让AI代码生成更稳定高效", 4 | "url": "https://mp.weixin.qq.com/s/vEUAZIJrb5kEjWptIhsC-g", 5 | "source": "梁波Eric", 6 | "summary": "Cursor Rules 在实际应用中可能遇到规则不生效、Token浪费、维护困难、输出不稳定和破坏项目风格等问题,本文提出MSEC四大原则:最小化原则、结构化原则、精准引用原则、一致性原则。通过这四个原则可有效提升AI代码生成的稳定性。" 7 | } 8 | ] -------------------------------------------------------------------------------- /config/crawler_keywords.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Cursor", 3 | "Antigravity", 4 | "Code Wiki", 5 | "Claude Skill", 6 | "Gemini 3", 7 | "Spec Kit", 8 | "Vibe Coding", 9 | "AI Coding", 10 | "AI代码审查", 11 | "Spec Coding", 12 | "Spec-Driven Development", 13 | "Spec-kit", 14 | "Agent", 15 | "ClaudeCode", 16 | "Android" 17 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.115.0 2 | uvicorn[standard]==0.30.0 3 | python-dotenv==1.0.1 4 | httpx==0.27.0 5 | apscheduler==3.10.4 6 | loguru==0.7.2 7 | beautifulsoup4==4.12.3 8 | lxml==6.0.2 9 | requests==2.32.3 10 | playwright==1.48.0 11 | feedparser==6.0.11 12 | sqlalchemy==2.0.35 13 | aiosqlite==0.20.0 14 | markdown==3.6 15 | html2text==2025.4.15 16 | 17 | -------------------------------------------------------------------------------- /app/domain/sources/__init__.py: -------------------------------------------------------------------------------- 1 | """数据源领域模型""" 2 | # 导出主要的数据源管理功能 3 | from . import ( 4 | ai_articles, 5 | ai_candidates, 6 | article_crawler, 7 | article_sources, 8 | tool_candidates, 9 | ) 10 | 11 | __all__ = [ 12 | "ai_articles", 13 | "ai_candidates", 14 | "article_crawler", 15 | "article_sources", 16 | "tool_candidates", 17 | ] 18 | -------------------------------------------------------------------------------- /app/infrastructure/db/__init__.py: -------------------------------------------------------------------------------- 1 | """数据库模块""" 2 | from .database import get_db, init_db, close_db, AsyncSessionLocal, engine 3 | from .models import Base, Article, Tool, Prompt, Rule, Resource 4 | 5 | __all__ = [ 6 | "get_db", 7 | "init_db", 8 | "close_db", 9 | "AsyncSessionLocal", 10 | "engine", 11 | "Base", 12 | "Article", 13 | "Tool", 14 | "Prompt", 15 | "Rule", 16 | "Resource", 17 | ] 18 | 19 | -------------------------------------------------------------------------------- /data/weekly/2025weekly51.md: -------------------------------------------------------------------------------- 1 | # 第51周资讯推荐 2 | 3 | 时间范围:2025年12月15日 - 2025年12月21日 4 | 5 | --- 6 | 7 | ## 🤖 AI资讯 8 | 9 | 本周暂无AI资讯。 10 | 11 | 12 | --- 13 | 14 | ## 💻 编程资讯 15 | 16 | 1. 下一场革命:Vibe Engineering|OpenAI 内部分享 17 | 最好、最新的内容,总来自赛博禅心 18 | 来源:金色传说大聪明 19 | 链接:https://mp.weixin.qq.com/s/dnyG27ReM4UJF6M11n7uoQ 20 | 21 | 2. AI研发新范式:基于技术方案全链路生成代码 22 | 实现生产力提升的人机协同新范式 23 | 来源:腾讯程序员 24 | 链接:https://mp.weixin.qq.com/s/WXXKpVth8OJqvjrnS1m7Cw 25 | 26 | 27 | --- 28 | 29 | 统计信息: 30 | 本周共推荐 2 篇优质资讯 31 | - AI资讯:0 篇 32 | - 编程资讯:2 篇 33 | 34 | --- 35 | 本报告由 [AI-CodeNexus](https://aicoding.100kwhy.fun) 自动生成 36 | -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- 1 | """服务层:业务逻辑服务""" 2 | 3 | from .data_loader import DataLoader 4 | from .database_data_service import DatabaseDataService 5 | from .database_write_service import DatabaseWriteService 6 | from .digest_service import DigestService 7 | from .backup_service import BackupService 8 | from .weekly_backup_service import WeeklyBackupService 9 | from .crawler_service import CrawlerService 10 | 11 | __all__ = [ 12 | "DataLoader", 13 | "DatabaseDataService", 14 | "DatabaseWriteService", 15 | "DigestService", 16 | "BackupService", 17 | "WeeklyBackupService", 18 | "CrawlerService", 19 | ] 20 | -------------------------------------------------------------------------------- /app/domain/digest/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import List, Optional 4 | 5 | 6 | @dataclass 7 | class ArticleItem: 8 | title: str 9 | url: str 10 | source: str # 公众号名 / 网站名 11 | category: str # ai_news / team_management / other 12 | pub_time: Optional[datetime] = None 13 | summary: Optional[str] = None # 1-2 句摘要 14 | comment: Optional[str] = None # 你的点评 15 | 16 | 17 | @dataclass 18 | class DailyDigest: 19 | date: datetime 20 | theme: str # 今日主题一句话 21 | items: List[ArticleItem] 22 | extra_note: Optional[str] = None # 结尾备注 / 招呼 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.11' 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install pytest pytest-asyncio 26 | 27 | - name: Run tests 28 | run: | 29 | pytest tests/ -v 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | env/ 26 | ENV/ 27 | .venv 28 | 29 | # IDE 30 | .vscode/ 31 | .idea/ 32 | *.swp 33 | *.swo 34 | *~ 35 | 36 | # Environment variables 37 | .env 38 | .env.local 39 | env.sh 40 | 41 | # Logs 42 | *.log 43 | .genlines 44 | debug/ 45 | logs/ 46 | !logs/.gitkeep 47 | 48 | # Weekly digest files are backed up daily with data/ directory 49 | # No need to ignore them 50 | 51 | # OS 52 | .DS_Store 53 | Thumbs.db 54 | 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "100kwhy-wechat-mp" 3 | version = "0.1.0" 4 | description = "WeChat official account backend and content pipeline for AI coding & engineering management." 5 | authors = ["Yunlong "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | fastapi = "^0.115.0" 10 | uvicorn = {extras = ["standard"], version = "^0.30.0"} 11 | python-dotenv = "^1.0.1" 12 | httpx = "^0.27.0" 13 | apscheduler = "^3.10.4" 14 | loguru = "^0.7.2" 15 | beautifulsoup4 = "^4.12.3" 16 | lxml = "^5.2.2" 17 | requests = "^2.32.3" 18 | playwright = "^1.48.0" 19 | 20 | 21 | [tool.poetry.dev-dependencies] 22 | pytest = "^8.0.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=1.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/tools/devops.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "keep", 4 | "url": "https://github.com/keephq/keep", 5 | "description": "开源的警报管理和 AIOps 平台,提供单一监控视图、去重、过滤等功能,支持双向集成、工作流和仪表板等。其主要功能包括可定制的用户界面、警报管理工具、深度集成与监控工具的同步、自动化监控工具的 GitHub Actions,以及基于 AI 的关联和总结(AIOps 2.0)。", 6 | "category": "devops", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 9, 10 | "created_at": "2024-12-03T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "keep" 14 | }, 15 | { 16 | "name": "HolmesGPT", 17 | "url": "https://github.com/robusta-dev/holmesgpt", 18 | "description": "HolmesGPT作为一款创新的开源DevOps助手,旨在简化Kubernetes故障排除、事件响应和工单管理。它的独特之处在于能够自主获取缺失数据,直到问题解决,潜在地将开发团队的问题解决速度提高一倍。其符合合规的特性和透明的结果对于关注数据隐私和法规遵从的组织至关重要", 19 | "category": "devops", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 4, 23 | "created_at": "2024-12-02T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "holmesgpt" 27 | } 28 | ] -------------------------------------------------------------------------------- /app/presentation/routes/wechat.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | 4 | from fastapi import APIRouter, Query 5 | 6 | WECHAT_TOKEN = os.getenv("WECHAT_TOKEN", "your_token") 7 | 8 | router = APIRouter() 9 | 10 | 11 | @router.get("/callback") 12 | async def wechat_verify( 13 | signature: str = Query(...), 14 | timestamp: str = Query(...), 15 | nonce: str = Query(...), 16 | echostr: str = Query(...), 17 | ) -> str: 18 | """ 19 | Minimal WeChat MP access verification endpoint. 20 | 21 | WeChat will send: signature, timestamp, nonce, echostr. 22 | You must: 23 | 1. Take your TOKEN (pre-shared with WeChat), timestamp, nonce. 24 | 2. Sort them lexicographically and join. 25 | 3. SHA1 hash the result. 26 | 4. If equals signature, return echostr. 27 | """ 28 | data = "".join(sorted([WECHAT_TOKEN, timestamp, nonce])) 29 | hashcode = hashlib.sha1(data.encode("utf-8")).hexdigest() 30 | if hashcode == signature: 31 | return echostr 32 | return "" 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/test_crawlers.py: -------------------------------------------------------------------------------- 1 | """爬虫测试""" 2 | import pytest 3 | from app.infrastructure.crawlers.rss import fetch_rss_articles 4 | from app.infrastructure.crawlers.github_trending import fetch_github_trending 5 | from app.infrastructure.crawlers.hackernews import fetch_hackernews_articles 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_fetch_rss_articles(): 10 | """测试 RSS 抓取""" 11 | # 使用一个公开的 RSS Feed 进行测试 12 | articles = await fetch_rss_articles("https://rss.cnn.com/rss/edition.rss", max_items=5) 13 | assert isinstance(articles, list) 14 | # 注意:实际测试可能需要 mock 网络请求 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_fetch_github_trending(): 19 | """测试 GitHub Trending 抓取""" 20 | articles = await fetch_github_trending("python", max_items=5) 21 | assert isinstance(articles, list) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_fetch_hackernews(): 26 | """测试 Hacker News 抓取""" 27 | articles = await fetch_hackernews_articles(min_points=50, max_items=5) 28 | assert isinstance(articles, list) 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yunlong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | -------------------------------------------------------------------------------- /data/tools/design.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "superdesign", 4 | "url": "https://www.superdesign.dev/", 5 | "description": "SuperDesign 是一个面向开发者的AI 设计助手,其核心理念是:让设计回归代码工作流,让开发者零门槛做设计。 它通过插件形式无缝嵌入到开发者日常使用的IDE 中,使你可以在写代码的同时完成界面设计、组件复用和快速迭代。", 6 | "category": "design", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 9, 10 | "created_at": "2025-08-11T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "superdesign" 14 | }, 15 | { 16 | "name": "motiff", 17 | "url": "https://motiff.com/", 18 | "description": "AI设计,一句话生成设计稿", 19 | "category": "design", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 6, 23 | "created_at": "2024-12-02T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "motiff" 27 | }, 28 | { 29 | "name": "lovart", 30 | "url": "https://www.lovart.ai/", 31 | "description": "Lovart 是一个跨模态的 AI 设计代理工具,它不仅能将自然语言文本转换为专业级视觉设计(如图像、视频、音频、3D 等),还能自主策划、执行并协同多种 AI 模型,像一个完整的设计团队一样帮你完成创意项目", 32 | "category": "design", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 4, 36 | "created_at": "2025-08-07T00:00:00Z", 37 | "is_featured": false, 38 | "id": 3, 39 | "identifier": "lovart" 40 | } 41 | ] -------------------------------------------------------------------------------- /app/domain/digest/render.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from .models import DailyDigest 4 | 5 | 6 | def render_digest_for_mp(digest: DailyDigest) -> str: 7 | """ 8 | Render daily digest to a simple Markdown-like text 9 | that you can直接复制到公众号后台,再做少量排版。 10 | """ 11 | lines: list[str] = [] 12 | lines.append(f"【AI 编程 & 团队管理日报】{digest.date:%Y-%m-%d}") 13 | lines.append("") 14 | lines.append(f"今日主题:{digest.theme}") 15 | lines.append("") 16 | 17 | for idx, item in enumerate(digest.items, start=1): 18 | lines.append(f"{idx}. {item.title}") 19 | lines.append(f" - 来源:{item.source}") 20 | if item.summary: 21 | lines.append(f" - 摘要:{item.summary}") 22 | if item.comment: 23 | lines.append(f" - 点评:{item.comment}") 24 | lines.append(f" - 原文链接:{item.url}") 25 | lines.append("") 26 | 27 | if digest.extra_note: 28 | lines.append("——") 29 | lines.append(digest.extra_note) 30 | 31 | return "\n".join(lines) 32 | 33 | 34 | DAILY_TEMPLATE = dedent( 35 | """ 36 | 【AI 编程最新资讯 · 管理员面板】{date} 37 | 38 | 今日主题:{theme} 39 | 40 | 1. 示例标题:xxxx 41 | - 来源:示例公众号 42 | - 摘要:一句话说明这篇文章讲了什么,对谁有用。 43 | - 点评:你站在工程实践 / 管理视角的一句短评。 44 | - 原文链接:粘贴原文链接 45 | 46 | (按上面格式列出 3–5 条即可) 47 | 48 | —— 49 | 你可以在菜单里查看往期日报,也可以在站点 100kwhy.fun 上看到更多实战文章。 50 | """ 51 | ).strip() 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/deploy/start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 >nul 3 | echo ======================================== 4 | echo 100kwhy_wechat_mp 一键启动脚本 (Windows) 5 | echo ======================================== 6 | echo. 7 | 8 | REM 获取脚本所在目录,然后向上两级到项目根目录 9 | cd /d "%~dp0\..\.." 10 | set PROJECT_ROOT=%CD% 11 | 12 | echo [1/4] 检查项目目录... 13 | if not exist "%PROJECT_ROOT%" ( 14 | echo 错误: 项目目录不存在: %PROJECT_ROOT% 15 | pause 16 | exit /b 1 17 | ) 18 | echo 项目目录: %PROJECT_ROOT% 19 | cd /d "%PROJECT_ROOT%" 20 | 21 | echo. 22 | echo [2/4] 检查虚拟环境... 23 | if not exist "venv\Scripts\activate.bat" ( 24 | echo 虚拟环境不存在,正在创建... 25 | python -m venv venv 26 | if errorlevel 1 ( 27 | echo 错误: 创建虚拟环境失败,请确保已安装 Python 3.10+ 28 | pause 29 | exit /b 1 30 | ) 31 | echo 虚拟环境创建成功 32 | ) 33 | 34 | echo. 35 | echo [3/4] 激活虚拟环境并检查依赖... 36 | call venv\Scripts\activate.bat 37 | if errorlevel 1 ( 38 | echo 错误: 激活虚拟环境失败 39 | pause 40 | exit /b 1 41 | ) 42 | 43 | REM 检查 uvicorn 是否已安装 44 | python -c "import uvicorn" 2>nul 45 | if errorlevel 1 ( 46 | echo 检测到依赖未安装,正在安装... 47 | pip install --upgrade pip 48 | pip install -r requirements.txt 49 | if errorlevel 1 ( 50 | echo 错误: 安装依赖失败 51 | pause 52 | exit /b 1 53 | ) 54 | echo 依赖安装完成 55 | ) else ( 56 | echo 依赖检查通过 57 | ) 58 | 59 | echo. 60 | echo [4/4] 启动应用... 61 | echo 服务地址: http://127.0.0.1:8000 62 | echo 管理面板: http://127.0.0.1:8000/digest/panel 63 | echo. 64 | echo 按 Ctrl+C 停止服务 65 | echo ======================================== 66 | echo. 67 | 68 | REM 启动应用(开发模式,支持热重载) 69 | uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload 70 | 71 | pause 72 | 73 | -------------------------------------------------------------------------------- /scripts/update_claude_code_subcategories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 更新现有Claude Code资源的subcategory字段 5 | """ 6 | 7 | import json 8 | import sys 9 | from pathlib import Path 10 | from typing import Dict 11 | 12 | # 添加项目根目录到路径 13 | project_root = Path(__file__).parent.parent 14 | sys.path.insert(0, str(project_root)) 15 | 16 | from scripts.crawl_devmaster_resources import classify_claude_code_resource 17 | 18 | 19 | def update_resources_subcategories(): 20 | """更新resources.json中Claude Code资源的subcategory字段""" 21 | resources_file = project_root / "data" / "resources.json" 22 | 23 | # 读取现有资源 24 | with resources_file.open('r', encoding='utf-8') as f: 25 | resources = json.load(f) 26 | 27 | updated_count = 0 28 | for resource in resources: 29 | if resource.get('category') == 'Claude Code 资源': 30 | # 如果已经有subcategory,跳过 31 | if 'subcategory' in resource and resource['subcategory']: 32 | continue 33 | 34 | # 自动分类 35 | title = resource.get('title', '') 36 | description = resource.get('description', '') 37 | url = resource.get('url', '') 38 | 39 | subcategory = classify_claude_code_resource(title, description, url) 40 | resource['subcategory'] = subcategory 41 | updated_count += 1 42 | print(f"更新: {title} -> {subcategory}") 43 | 44 | # 保存更新后的资源 45 | with resources_file.open('w', encoding='utf-8') as f: 46 | json.dump(resources, f, ensure_ascii=False, indent=2) 47 | 48 | print(f"\n✓ 完成!共更新 {updated_count} 个资源的subcategory字段") 49 | 50 | 51 | if __name__ == "__main__": 52 | update_resources_subcategories() 53 | 54 | -------------------------------------------------------------------------------- /data/weekly/2025weekly50.md: -------------------------------------------------------------------------------- 1 | # 第50周资讯推荐 2 | 3 | 时间范围:2025年12月08日 - 2025年12月14日 4 | 5 | --- 6 | 7 | ## 🤖 AI资讯 8 | 9 | 1. 424页!智能体设计模式手册 10 | 曾引发热议的谷歌工程师 Antonio Gulli发起编著的《Agentic Design Patterns》上架亚马逊。全书供424页,覆盖了常见的Agent开发的基础概念和实现模式,所有方案都经过... 11 | 来源:winkrun 12 | 链接:https://mp.weixin.qq.com/s/UmaDikC1q6bO4WTgVOGSSQ 13 | 14 | 2. GPT-5.2发布,真正的牛马打工人专属AI来了。 15 | OpenAI的十周年献礼 16 | 来源:数字生命卡兹克 17 | 链接:https://mp.weixin.qq.com/s/DXivP0vVCpijpAhRlP5kKw 18 | 19 | 3. 产品研发AI提效实践:PRD智能体在需求领域的提效探索 20 | 暂无摘要 21 | 来源:舒阳 22 | 链接:https://mp.weixin.qq.com/s/8cpIMJrgAVS0WeM_lmzUyg 23 | 24 | 25 | --- 26 | 27 | ## 💻 编程资讯 28 | 29 | 1. AI 修 Bug 越来越好了:Cursor 新调试模式,让 AI 真正拥有工程师级调试能力 30 | 🍹 Insight Daily 🪺 31 | 来源:Aitrainee 32 | 链接:https://mp.weixin.qq.com/s/JX6js_le1cDeJTUiU09NzQ 33 | 34 | 2. AI编程技巧和心得 35 | 暂无摘要 36 | 来源:微信公众平台 37 | 链接:https://mp.weixin.qq.com/s/E7fDUwMkKEh20UOsy7tpQw 38 | 39 | 3. 从代码基座模型到智能体与应用:代码智能的全面综述与实践指南 40 | 暂无摘要 41 | 来源:编辑部 42 | 链接:https://mp.weixin.qq.com/s/0KoL6ExaHJ2bfhyKTBTlNQ 43 | 44 | 4. 细聊热点话题:Spec-Driven开发(SDD) 45 | SDD 有望成为 AI 时代的主流开发范式之一,让开发者从重复的编码工作中解放,更专注于创意与价值本身 46 | 来源:朱少民 47 | 链接:https://mp.weixin.qq.com/s/OnibixInL5YyCej6WTAJ7Q 48 | 49 | 5. AI编程经验分享 50 | 从最初的copilot tab键补全,到现在的cursor、claude code、codex等各种编程 Agent,基本每个我都在实际的开发任务中使用过,积累了一些个人的AI编程经验,在此记录分享一... 51 | 来源:方圆yo 52 | 链接:https://mp.weixin.qq.com/s/0DQ6345nwlyIAhykg9o3gg 53 | 54 | 6. AI Coding 提效实战:Augment 在AI时代后端研发中的全方位应用 55 | AI Coding 提效实战:Augment 在AI时代后端研发中的全方位应用在 AI 编程时代,如何利用 56 | 来源:楚湘梁朝伟 Dylan 57 | 链接:https://mp.weixin.qq.com/s/BuumAKUnzvU400AnK_7gww 58 | 59 | 60 | --- 61 | 62 | 统计信息: 63 | 本周共推荐 9 篇优质资讯 64 | - AI资讯:3 篇 65 | - 编程资讯:6 篇 66 | 67 | --- 68 | 本报告由 [AI-CodeNexus](https://aicoding.100kwhy.fun) 自动生成 69 | -------------------------------------------------------------------------------- /docs/feature/v2.0_update_summary.md: -------------------------------------------------------------------------------- 1 | # Daily Digest v2.0 新功能说明 2 | 3 | ## ✅ 已完成功能 4 | 5 | ### 1. 多资讯源支持 6 | - **RSS/Atom Feed**:支持批量添加 RSS 源,自动抓取当天文章 7 | - **GitHub Trending**:支持按编程语言抓取热门项目 8 | - **Hacker News**:支持抓取高分文章(可配置最低分数阈值) 9 | - **统一资讯源管理器**:`app/sources/article_sources.py` 统一管理所有资讯源 10 | 11 | ### 2. 文章智能排序 12 | - **热度分计算**:综合考虑来源权重、时效性、标题长度、摘要等因素 13 | - **自动排序**:抓取的文章按热度分自动排序 14 | - **支持按热度/时效排序**:候选池支持多种排序方式 15 | 16 | ### 3. 微信公众号渠道 17 | - **Access Token 管理**:自动获取和刷新 access_token 18 | - **素材上传**:支持上传图片等素材 19 | - **草稿创建**:支持创建图文草稿 20 | - **一键发布**:支持将草稿发布到公众号 21 | 22 | ### 4. SQLite 持久化 23 | - **数据库模型**:Article、Candidate、Config、Statistic 表 24 | - **异步数据库操作**:使用 SQLAlchemy + aiosqlite 25 | - **数据迁移**:支持从 JSON 迁移到 SQLite(待实现迁移脚本) 26 | 27 | ### 5. 测试和 CI/CD 28 | - **单元测试框架**:pytest + pytest-asyncio 29 | - **GitHub Actions**:自动化测试和 Lint 检查 30 | - **测试覆盖**:数据获取模块基础测试 31 | 32 | ### 6. API 文档 33 | - **OpenAPI/Swagger**:FastAPI 自动生成 API 文档 34 | - **访问地址**:`/docs` (Swagger UI) 和 `/redoc` (ReDoc) 35 | 36 | ## 🚧 待完善功能 37 | 38 | ### 1. 多频道推送策略 39 | - [ ] 支持配置多个企业微信机器人 40 | - [ ] 每个渠道关联不同的关键词和推送时间 41 | - [ ] 渠道管理界面 42 | 43 | ### 2. 管理面板增强 44 | - [ ] 文章批注功能(数据库已支持,需添加 UI) 45 | - [ ] 批量操作(一键采纳/忽略某个关键词下的所有文章) 46 | - [ ] 数据统计(抓取量、采纳率等) 47 | 48 | ### 3. 数据迁移 49 | - [ ] JSON 到 SQLite 的迁移脚本 50 | - [ ] 向后兼容(支持 JSON 和 SQLite 双模式) 51 | 52 | ## 📝 使用说明 53 | 54 | ### 配置 RSS Feed 55 | 在配置文件中添加 RSS Feed URL 列表,系统会自动抓取。 56 | 57 | ### 配置 GitHub Trending 58 | 在配置中指定要抓取的编程语言列表(如:python, javascript, go)。 59 | 60 | ### 配置 Hacker News 61 | 设置最低分数阈值,只抓取高分文章。 62 | 63 | ### 微信公众号发布 64 | 配置 `WECHAT_MP_APPID` 和 `WECHAT_MP_SECRET` 环境变量,即可使用公众号发布功能。 65 | 66 | ## 🔧 技术栈更新 67 | 68 | 新增依赖: 69 | - `feedparser`:RSS/Atom Feed 解析 70 | - `sqlalchemy`:ORM 框架 71 | - `aiosqlite`:异步 SQLite 驱动 72 | - `pytest`:测试框架 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/infrastructure/db/database.py: -------------------------------------------------------------------------------- 1 | """数据库连接和会话管理""" 2 | 3 | import os 4 | from pathlib import Path 5 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker 6 | from sqlalchemy.orm import declarative_base 7 | from loguru import logger 8 | 9 | from .models import Base 10 | 11 | # 数据库文件路径(项目根目录下的 data.db) 12 | _project_root = Path(__file__).resolve().parent.parent.parent.parent 13 | DATABASE_URL = f"sqlite+aiosqlite:///{_project_root / 'data.db'}" 14 | 15 | # 创建异步引擎 16 | engine = create_async_engine( 17 | DATABASE_URL, 18 | echo=False, # 设置为True可以查看SQL语句 19 | future=True, 20 | connect_args={"check_same_thread": False} # SQLite需要这个参数 21 | ) 22 | 23 | # 创建异步会话工厂 24 | AsyncSessionLocal = async_sessionmaker( 25 | engine, 26 | class_=AsyncSession, 27 | expire_on_commit=False, 28 | autocommit=False, 29 | autoflush=False, 30 | ) 31 | 32 | 33 | async def init_db(): 34 | """初始化数据库(创建表)""" 35 | try: 36 | async with engine.begin() as conn: 37 | # 创建所有表 38 | await conn.run_sync(Base.metadata.create_all) 39 | logger.info("[数据库] 数据库表创建成功") 40 | except Exception as e: 41 | logger.error(f"[数据库] 数据库初始化失败: {e}") 42 | raise 43 | 44 | 45 | async def get_db(): 46 | """ 47 | 获取数据库会话(依赖注入,用于FastAPI) 48 | 49 | Usage in FastAPI: 50 | @app.get("/items") 51 | async def read_items(db: AsyncSession = Depends(get_db)): 52 | # 使用db进行数据库操作 53 | pass 54 | """ 55 | async with AsyncSessionLocal() as session: 56 | try: 57 | yield session 58 | await session.commit() 59 | except Exception: 60 | await session.rollback() 61 | raise 62 | finally: 63 | await session.close() 64 | 65 | 66 | async def close_db(): 67 | """关闭数据库连接""" 68 | await engine.dispose() 69 | logger.info("[数据库] 数据库连接已关闭") 70 | 71 | -------------------------------------------------------------------------------- /docs/technical/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Clean 架构重构说明 2 | 3 | ## 架构层次 4 | 5 | 本项目采用 Clean 架构(清洁架构)进行重构,将代码分为以下几个层次: 6 | 7 | ### 1. Domain Layer (领域层) 8 | **位置**: `app/domain/` 9 | 10 | 包含业务领域模型和核心业务逻辑: 11 | - `digest/`: 摘要领域模型和渲染 12 | - `models.py`: 摘要数据模型 13 | - `render.py`: 摘要渲染逻辑 14 | - `sources/`: 数据源管理 15 | - `ai_articles.py`: AI文章池管理 16 | - `ai_candidates.py`: AI文章候选池 17 | - `tool_candidates.py`: 工具候选池 18 | - `article_crawler.py`: 文章爬虫 19 | - `article_sources.py`: 文章源管理 20 | 21 | ### 2. Infrastructure Layer (基础设施层) 22 | **位置**: `app/infrastructure/` 23 | 24 | 负责底层技术实现和基础设施: 25 | - `logging.py`: 日志配置 26 | - `file_lock.py`: 跨进程文件锁 27 | - `scheduler.py`: 调度器管理 28 | - `crawlers/`: 外部数据爬虫 29 | - `sogou_wechat.py`: 搜狗微信搜索 30 | - `rss.py`: RSS源 31 | - `github_trending.py`: GitHub趋势 32 | - `hackernews.py`: HackerNews 33 | - `devmaster.py`: DevMaster API 34 | - `notifiers/`: 通知服务 35 | - `wecom.py`: 企业微信通知 36 | - `wechat_mp.py`: 微信公众号通知 37 | - `db/`: 数据库访问(如需要) 38 | 39 | ### 3. Service Layer (服务层) 40 | **位置**: `app/services/` 41 | 42 | 包含业务逻辑服务: 43 | - `digest_service.py`: 推送服务 44 | - `backup_service.py`: 数据备份服务 45 | - `crawler_service.py`: 文章抓取服务 46 | - `data_loader.py`: 数据加载服务 47 | - `weekly_digest.py`: 周报服务 48 | 49 | ### 4. Presentation Layer (表示层) 50 | **位置**: `app/presentation/` 51 | 52 | 负责用户界面和展示: 53 | - `templates.py`: HTML模板 54 | - `routes/`: API路由 55 | - `api.py`: 工具和资讯API 56 | - `digest.py`: 摘要管理路由 57 | - `wechat.py`: 微信路由 58 | - `ai_assistant.py`: AI助手路由 59 | - `static/`: 静态资源文件 60 | 61 | ### 5. Application Layer (应用层) 62 | **位置**: `app/main.py` 63 | 64 | 应用入口,负责: 65 | - 应用组装 66 | - 路由注册 67 | - 生命周期管理 68 | 69 | ## 依赖关系 70 | 71 | ``` 72 | main.py (应用层) 73 | ├── domain (领域层) 74 | ├── infrastructure (基础设施层) 75 | ├── services (服务层) 76 | │ ├── domain (服务层依赖领域层) 77 | │ └── infrastructure (服务层依赖基础设施层) 78 | └── presentation (表示层) 79 | ├── domain (表示层依赖领域层) 80 | ├── services (表示层依赖服务层) 81 | └── infrastructure (表示层依赖基础设施层) 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/deploy/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 设置脚本在遇到错误时退出 4 | set -e 5 | 6 | # 颜色定义 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | NC='\033[0m' # No Color 11 | 12 | echo "========================================" 13 | echo " 100kwhy_wechat_mp 一键启动脚本 (Linux)" 14 | echo "========================================" 15 | echo "" 16 | 17 | # 获取脚本所在目录,然后向上两级到项目根目录 18 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 19 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" 20 | 21 | echo "[1/4] 检查项目目录..." 22 | if [ ! -d "$PROJECT_ROOT" ]; then 23 | echo -e "${RED}错误: 项目目录不存在: $PROJECT_ROOT${NC}" 24 | exit 1 25 | fi 26 | echo "项目目录: $PROJECT_ROOT" 27 | cd "$PROJECT_ROOT" 28 | 29 | echo "" 30 | echo "[2/4] 检查虚拟环境..." 31 | if [ ! -f "venv/bin/activate" ]; then 32 | echo "虚拟环境不存在,正在创建..." 33 | python3 -m venv venv 34 | if [ $? -ne 0 ]; then 35 | echo -e "${RED}错误: 创建虚拟环境失败,请确保已安装 Python 3.10+${NC}" 36 | exit 1 37 | fi 38 | echo -e "${GREEN}虚拟环境创建成功${NC}" 39 | fi 40 | 41 | echo "" 42 | echo "[3/4] 激活虚拟环境并检查依赖..." 43 | source venv/bin/activate 44 | if [ $? -ne 0 ]; then 45 | echo -e "${RED}错误: 激活虚拟环境失败${NC}" 46 | exit 1 47 | fi 48 | 49 | # 检查 uvicorn 是否已安装 50 | python -c "import uvicorn" 2>/dev/null 51 | if [ $? -ne 0 ]; then 52 | echo "检测到依赖未安装,正在安装..." 53 | pip install --upgrade pip 54 | pip install -r requirements.txt 55 | if [ $? -ne 0 ]; then 56 | echo -e "${RED}错误: 安装依赖失败${NC}" 57 | exit 1 58 | fi 59 | echo -e "${GREEN}依赖安装完成${NC}" 60 | else 61 | echo -e "${GREEN}依赖检查通过${NC}" 62 | fi 63 | 64 | echo "" 65 | echo "[4/4] 启动应用..." 66 | echo -e "${GREEN}服务地址: http://127.0.0.1:8000${NC}" 67 | echo -e "${GREEN}管理面板: http://127.0.0.1:8000/digest/panel${NC}" 68 | echo "" 69 | echo "按 Ctrl+C 停止服务" 70 | echo "========================================" 71 | echo "" 72 | 73 | # 启动应用(开发模式,支持热重载) 74 | # 如果需要生产模式,可以去掉 --reload 参数,并修改 --host 为 0.0.0.0 75 | uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload 76 | 77 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 文档目录 2 | 3 | 本文档目录按照功能分类组织,方便查找和维护。 4 | 5 | ## 目录结构 6 | 7 | ``` 8 | docs/ 9 | ├── deploy/ # 部署相关文档 10 | │ ├── deploy_baota.md # 宝塔面板部署指南 11 | │ ├── deploy_python.md # Python环境部署指南 12 | │ ├── deploy_windows.md # Windows本地部署指南 13 | │ └── wechat_mp_guide.md # 微信公众号发布指南 14 | │ 15 | ├── feature/ # 功能描述文档 16 | │ ├── feature_plan.md # 功能开发计划 17 | │ ├── features_complete.md # 完整功能文档 ⭐ 18 | │ ├── v2.0_update_summary.md # v2.0 版本更新说明 19 | │ ├── tool_detail_feature.md # 工具详情功能说明 20 | │ ├── tool_article_crawling_feature.md # 工具相关资讯获取功能 21 | │ ├── v3.0_update_summary.md # v3.0 版本更新说明 22 | │ ├── multi_sources_guide.md # 多资讯源使用指南 23 | │ ├── test_sources.md # 测试指南 24 | │ ├── 资讯数据模型.md # 资讯数据模型定义和说明 25 | │ └── 资讯分类映射关系.md # 资讯分类映射关系说明 26 | │ 27 | └── ADR/ # 技术决策文档 (Architecture Decision Records) 28 | └── (待添加) 29 | ``` 30 | 31 | ## 文档说明 32 | 33 | ### 部署文档 (deploy/) 34 | 35 | 包含各种部署场景的详细指南: 36 | - **deploy_baota.md**: 使用宝塔面板部署的完整流程 37 | - **deploy_python.md**: Python生产环境部署指南 38 | - **deploy_windows.md**: Windows本地开发和部署指南 39 | - **wechat_mp_guide.md**: 微信公众号内容发布配置 40 | 41 | ### 功能文档 (feature/) 42 | 43 | 包含功能说明、使用指南和开发计划: 44 | - **features_complete.md**: 最全面的功能文档,包含所有模块的详细说明 45 | - **feature_plan.md**: 功能开发计划和路线图 46 | - **v2.0_update_summary.md**: v2.0 版本更新说明 47 | - **tool_detail_feature.md**: 工具详情页功能的实现说明 48 | - **tool_article_crawling_feature.md**: 工具相关资讯获取功能的详细说明 49 | - **v3.0_update_summary.md**: v3.0 版本更新说明 50 | - **multi_sources_guide.md**: 多资讯源配置和使用指南 51 | - **test_sources.md**: 测试方法和测试指南 52 | - **资讯数据模型.md**: 资讯文章的数据模型定义、必需字段和可选字段说明 53 | - **资讯分类映射关系.md**: UI模块、API端点、Category值和JSON文件的映射关系 54 | 55 | ### 技术决策文档 (ADR/) 56 | 57 | 记录重要的技术决策和架构选择(待补充)。 58 | 59 | ## 快速导航 60 | 61 | - 📖 [完整功能文档](feature/features_complete.md) - 推荐从这里开始 62 | - 🚀 [Python环境部署](deploy/deploy_python.md) - 生产环境部署 63 | - 🪟 [Windows部署](deploy/deploy_windows.md) - 本地开发环境 64 | - 📱 [微信公众号发布](deploy/wechat_mp_guide.md) - 内容发布配置 65 | 66 | -------------------------------------------------------------------------------- /app/infrastructure/logging.py: -------------------------------------------------------------------------------- 1 | """日志配置模块""" 2 | 3 | from pathlib import Path 4 | from loguru import logger 5 | 6 | 7 | def setup_logging(): 8 | """ 9 | 配置日志系统,将日志保存到文件 10 | """ 11 | # 创建 logs 目录 12 | project_root = Path(__file__).resolve().parent.parent.parent 13 | logs_dir = project_root / "logs" 14 | logs_dir.mkdir(exist_ok=True) 15 | 16 | # 配置主日志文件(所有日志) 17 | # 按日期轮转,保留30天,压缩旧日志 18 | logger.add( 19 | logs_dir / "app_{time:YYYY-MM-DD}.log", 20 | rotation="00:00", # 每天午夜轮转 21 | retention="30 days", # 保留30天 22 | compression="zip", # 压缩旧日志 23 | encoding="utf-8", 24 | level="INFO", 25 | format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}", 26 | enqueue=True, # 异步写入,避免阻塞 27 | ) 28 | 29 | # 配置错误日志文件(只记录 ERROR 及以上级别) 30 | logger.add( 31 | logs_dir / "error_{time:YYYY-MM-DD}.log", 32 | rotation="00:00", 33 | retention="90 days", # 错误日志保留更久 34 | compression="zip", 35 | encoding="utf-8", 36 | level="ERROR", 37 | format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} | {message}", 38 | enqueue=True, 39 | ) 40 | 41 | # 配置定时任务专用日志文件(包含关键前缀的日志) 42 | # 使用过滤器只记录定时任务相关的日志 43 | def scheduler_filter(record): 44 | """过滤定时任务相关的日志""" 45 | message = record["message"] 46 | return any( 47 | prefix in message 48 | for prefix in [ 49 | "[定时推送]", 50 | "[自动抓取]", 51 | "[数据备份]", 52 | "[调度器]", 53 | ] 54 | ) 55 | 56 | logger.add( 57 | logs_dir / "scheduler_{time:YYYY-MM-DD}.log", 58 | rotation="00:00", 59 | retention="90 days", # 定时任务日志保留更久 60 | compression="zip", 61 | encoding="utf-8", 62 | level="INFO", 63 | filter=scheduler_filter, # 只记录定时任务相关日志 64 | format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {message}", 65 | enqueue=True, 66 | ) 67 | 68 | logger.info("日志系统已配置,日志文件保存在 logs/ 目录") 69 | 70 | -------------------------------------------------------------------------------- /docs/deploy/deploy_windows.md: -------------------------------------------------------------------------------- 1 | # Windows 安装与部署指南 2 | 3 | 本指南适用于需要在 Windows 10/11 桌面或服务器上运行 `100kwhy_wechat_mp` 项目(特别是开发人员调试、验收或小型部署环境)。 4 | 推荐 **PowerShell 7+**,也可以使用 CMD / Windows Terminal。 5 | 6 | ## 先决条件 7 | 8 | 1. 安装 Python 3.11/3.13(官网 https://python.org/downloads/ ),并勾选“Add Python to PATH”。 9 | 2. 安装 Git 客户端,或确保你有方式把仓库克隆到本地。 10 | 3. 安装 Node.js(用于 Playwright 的浏览器下载),可选但推荐。 11 | 4. 预先准备好企业微信机器人 Webhook(可稍后在 `.env` 里配置)。 12 | 13 | ## 克隆项目 & 环境准备 14 | 15 | ```powershell 16 | cd xxx 17 | git clone https://github.com/yunlongwen/100kwhy_wechat_mp.git 18 | cd 100kwhy_wechat_mp 19 | python -m venv .venv 20 | .venv\Scripts\Activate.ps1 21 | pip install --upgrade pip setuptools wheel 22 | pip install -r requirements.txt 23 | playwright install 24 | ``` 25 | 26 | > 如果 PowerShell 报 `运行脚本已被禁用`,临时执行 `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`,然后再 `Activate.ps1`。 27 | 28 | ## 配置环境变量 29 | 30 | 在项目根目录创建 `.env`(或通过系统环境变量): 31 | 32 | ```powershell 33 | Set-Content .env @" 34 | WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" 35 | # 可选:自定义管理员授权码,默认无密码。 36 | # AICODING_ADMIN_CODE="your-admin-code" 37 | "@" 38 | ``` 39 | 40 | 你也可以在 PowerShell 中 `setx WECOM_WEBHOOK "..."` 设置系统范围变量。 41 | 42 | ## 玩转 Playwright 43 | 44 | Playwright 依赖 Chromium,安装后可以直接运行,但首次启动可能需要额外依赖。 45 | 如果提示缺少依赖,可尝试: 46 | 47 | ```powershell 48 | playwright install-deps 49 | ``` 50 | 51 | (在 Windows 上这个命令通常会提示已安装,可忽略) 52 | 53 | ## 运行服务 54 | 55 | 激活虚拟环境后: 56 | 57 | ```powershell 58 | cd D:\study\github\100kwhy_wechat_mp 59 | .venv\Scripts\Activate.ps1 60 | uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload 61 | ``` 62 | 63 | 需要在生产环境使用 `--proxy-headers`、`--log-config` 等可选参数,也可使用 `python -m uvicorn`。 64 | 65 | ## 管理面板 66 | 67 | - 访问 `http://127.0.0.1:8000/` 查看首页; 68 | - 管理面板在 `http://127.0.0.1:8000/digest/panel`; 69 | - 初次使用面板需要填写管理员授权码;若 `.env` 没配置,输入任意值即可; 70 | - 面板包含:文章添加/抓取、候选池、推送、配置管理(关键词/调度/模板)。 71 | 72 | ## 其他建议 73 | 74 | - 如果想后台运行,可用 `schtasks`、`nssm`、`winser` 等工具注册服务。 75 | - 定期检查 `data/articles/ai_articles.json` 与 `data/articles/ai_candidates.json`,确保推送数据准确。 76 | - 推送失败会在日志(console)打印,可以通过 `>> logs/app.log 2>&1` 重定向。 77 | 78 | 需要把该部署文档加入目录索引/README?反馈我再做。 79 | 80 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Daily Digest · 未来路线图 2 | 3 | 我们已经完成了资讯抓取、筛选、自动推送和内容池循环的核心链路,并实现了可视化的配置管理面板。接下来的目标是 **增强内容源**、**深化智能推荐** 并 **打通更多渠道**。 4 | 5 | ## ✅ 已完成功能 6 | 7 | - [x] **可视化配置管理面板** 8 | - [x] 关键词配置(支持在面板中编辑) 9 | - [x] 调度配置(Cron 表达式或小时+分钟,数量控制) 10 | - [x] 企业微信模板配置(JSON 格式,支持占位符) 11 | - [x] 系统配置(管理员验证码、企业微信推送地址) 12 | - [x] 所有配置支持在线编辑和保存 13 | 14 | - [x] **多资讯源支持** 15 | - [x] RSS/Atom Feed 抓取器 16 | - [x] GitHub Trending 抓取器 17 | - [x] Hacker News 抓取器 18 | - [x] 统一资讯源管理器 19 | 20 | - [x] **文章智能排序** 21 | - [x] 热度分计算算法 22 | - [x] 自动排序功能 23 | 24 | - [ ] **微信公众号渠道**(已暂时屏蔽,待需求明确后重新实现) 25 | - [x] Access Token 自动管理 26 | - [x] 素材上传接口 27 | - [x] 草稿创建和发布接口 28 | - [ ] 内容抓取和格式化(待重新设计) 29 | 30 | - [x] **SQLite 持久化** 31 | - [x] 数据库模型设计 32 | - [x] 异步数据库操作 33 | 34 | - [x] **测试和 CI/CD** 35 | - [x] 单元测试框架 36 | - [x] GitHub Actions 集成 37 | 38 | - [x] **API 文档** 39 | - [x] OpenAPI/Swagger 文档自动生成 40 | 41 | --- 42 | 43 | ### P0 · 核心功能增强 44 | 45 | - [x] **引入更多资讯源** 46 | - [x] **RSS / Atom Feed**:支持批量添加 RSS 源,作为候选池的主要来源之一。 47 | - [x] **GitHub Trending**:每日抓取 AI / Python / Engineering 等领域的 Trending 项目。 48 | - [x] **Hacker News**:抓取高分(例如 >100 points)的 AI 相关主题。 49 | 50 | - [ ] **打通微信公众号渠道**(已暂时屏蔽,待需求明确后重新实现) 51 | - [x] 实现 `access_token` 的自动获取与刷新。 52 | - [x] 实现"上传素材 → 创建草稿 → 发布图文"的核心 API 调用。 53 | - [ ] 内容抓取和格式化逻辑需要重新设计(从文章URL抓取完整HTML内容) 54 | - [ ] 在管理面板增加"一键同步到公众号"功能(API 已实现,UI 待完善)。 55 | 56 | - [x] **文章智能排序与推荐** 57 | - [x] 为文章增加"热度分"或"推荐指数",结合来源、时效性等因素。 58 | - [x] 支持按"热度"或"时效"对候选池进行排序。 59 | - [ ] (可选)引入 Embedding,实现基于相似度的内容去重和主题聚类。 60 | 61 | --- 62 | 63 | ### P1 · 管理与易用性 64 | 65 | - [ ] **多频道推送策略** 66 | - [ ] 支持配置多个企业微信机器人或公众号渠道。 67 | - [ ] 每个渠道可以关联不同的关键词、文章池和推送时间。 68 | 69 | - [ ] **管理面板增强** 70 | - [x] **配置管理**:支持在面板中配置关键词、调度、模板、系统环境变量(已完成) 71 | - [ ] **批注与协作**:支持在面板上为文章添加内部评论或推荐语。 72 | - [ ] **批量操作**:支持在候选池中一键采纳/忽略某个关键词下的所有文章。 73 | - [ ] **数据统计**:简单统计每个关键词下的文章抓取量、采纳率等。 74 | 75 | - [x] **持久化方案升级** 76 | - [x] 支持将 JSON 文件存储切换为 SQLite,提升数据操作的稳定性和性能。 77 | - [ ] JSON 到 SQLite 的数据迁移脚本(待实现)。 78 | 79 | --- 80 | 81 | ### P2 · 工程质量与文档 82 | 83 | - [x] **完善测试覆盖** 84 | - [x] 为爬虫、API、定时任务等核心模块补充单元测试和集成测试。 85 | - [x] 集成 GitHub Actions,实现自动化测试与 Lint 检查。 86 | 87 | - [x] **API 文档** 88 | - [x] 利用 FastAPI 的特性,为所有公开 API 生成并完善 OpenAPI (Swagger) 文档。 89 | - [x] 访问地址:`/docs` (Swagger UI) 和 `/redoc` (ReDoc) 90 | 91 | 欢迎随时在 Issue 中提出你的想法,或者直接认领任务发起 PR! 92 | -------------------------------------------------------------------------------- /app/services/devmaster_news_service.py: -------------------------------------------------------------------------------- 1 | """DevMaster 资讯抓取服务""" 2 | from typing import Dict, List 3 | from loguru import logger 4 | 5 | from ..infrastructure.crawlers.devmaster_news import fetch_today_devmaster_news 6 | from .database_write_service import DatabaseWriteService 7 | 8 | 9 | class DevMasterNewsService: 10 | """DevMaster 资讯抓取服务""" 11 | 12 | @staticmethod 13 | async def crawl_and_archive_today_news() -> int: 14 | """ 15 | 抓取今天的 DevMaster 资讯并归档到数据库 16 | 17 | Returns: 18 | 成功归档的资讯数量 19 | """ 20 | try: 21 | logger.info("[DevMaster资讯] 开始抓取今日资讯...") 22 | 23 | # 抓取资讯 24 | all_news = await fetch_today_devmaster_news() 25 | 26 | if not all_news or all(len(v) == 0 for v in all_news.values()): 27 | logger.warning("[DevMaster资讯] 未抓取到任何资讯") 28 | return 0 29 | 30 | # 归档到数据库 31 | success_count = 0 32 | failed_count = 0 33 | 34 | for category, news_list in all_news.items(): 35 | for news in news_list: 36 | try: 37 | # 提取标签 38 | tags = news.get("tags", []) 39 | 40 | # 归档到数据库 41 | success = await DatabaseWriteService.archive_article_to_category( 42 | article=news, 43 | category=category, 44 | tool_tags=tags 45 | ) 46 | 47 | if success: 48 | logger.info(f"[DevMaster资讯] 归档成功: {news['title'][:50]}...") 49 | success_count += 1 50 | else: 51 | logger.warning(f"[DevMaster资讯] 归档失败(可能已存在): {news['title'][:50]}...") 52 | failed_count += 1 53 | 54 | except Exception as e: 55 | logger.error(f"[DevMaster资讯] 归档失败: {e}", exc_info=True) 56 | failed_count += 1 57 | 58 | logger.info(f"[DevMaster资讯] 抓取完成!成功: {success_count}, 失败: {failed_count}") 59 | return success_count 60 | 61 | except Exception as e: 62 | logger.error(f"[DevMaster资讯] 抓取失败: {e}", exc_info=True) 63 | return 0 64 | 65 | -------------------------------------------------------------------------------- /app/domain/sources/tool_candidates.py: -------------------------------------------------------------------------------- 1 | """ 2 | 管理待审核的工具候选池(`data/tools/tool_candidates.json`) 3 | """ 4 | import json 5 | from dataclasses import dataclass, asdict 6 | from pathlib import Path 7 | from typing import List 8 | 9 | from loguru import logger 10 | 11 | 12 | @dataclass 13 | class CandidateTool: 14 | """待审核工具的数据结构""" 15 | name: str 16 | url: str 17 | description: str 18 | category: str 19 | tags: List[str] = None 20 | icon: str = "" 21 | submitted_by: str = "" # 提交者信息(可选) 22 | submitted_at: str = "" # 提交时间 23 | 24 | 25 | def _candidate_data_path() -> Path: 26 | """获取工具候选池数据文件的路径""" 27 | return Path(__file__).resolve().parents[2] / "data" / "tools" / "tool_candidates.json" 28 | 29 | 30 | def load_candidate_pool() -> List[CandidateTool]: 31 | """加载所有待审核的工具""" 32 | path = _candidate_data_path() 33 | if not path.exists(): 34 | return [] 35 | 36 | try: 37 | with path.open("r", encoding="utf-8") as f: 38 | raw_items = json.load(f) 39 | 40 | if not isinstance(raw_items, list): 41 | logger.warning(f"Tool candidate config is not a list, found {type(raw_items)}. Resetting.") 42 | return [] 43 | 44 | return [CandidateTool(**item) for item in raw_items] 45 | except (json.JSONDecodeError, TypeError) as e: 46 | logger.error(f"Failed to load or parse candidate tools: {e}") 47 | return [] 48 | 49 | 50 | def save_candidate_pool(candidates: List[CandidateTool]) -> bool: 51 | """将候选工具列表完整写入配置文件(覆盖)""" 52 | path = _candidate_data_path() 53 | logger.info(f"保存工具候选池到: {path}, 工具数量: {len(candidates)}") 54 | 55 | try: 56 | path.parent.mkdir(parents=True, exist_ok=True) 57 | 58 | # 转换为字典列表 59 | candidates_dict = [asdict(c) for c in candidates] 60 | logger.debug(f"转换后的候选工具数据: {candidates_dict[:2] if len(candidates_dict) > 0 else '[]'}") 61 | 62 | with path.open("w", encoding="utf-8") as f: 63 | json.dump(candidates_dict, f, ensure_ascii=False, indent=2) 64 | 65 | # 验证文件是否成功写入 66 | if path.exists(): 67 | file_size = path.stat().st_size 68 | logger.info(f"工具候选池文件已保存,大小: {file_size} 字节") 69 | return True 70 | else: 71 | logger.error(f"工具候选池文件保存后不存在: {path}") 72 | return False 73 | except Exception as e: 74 | logger.error(f"保存工具候选池失败: {e}", exc_info=True) 75 | return False 76 | 77 | 78 | def clear_candidate_pool() -> bool: 79 | """清空工具候选池""" 80 | return save_candidate_pool([]) 81 | 82 | -------------------------------------------------------------------------------- /data/tools/mcp.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "taskmaster", 4 | "url": "https://github.com/eyaltoledano/claude-task-master", 5 | "description": "它是一个强大的 AI 任务管理工具,专为 AI 驱动开发设计。它通过智能解析产品需求文档(PRD),生成结构化的任务列表,并支持任务拆分、依赖管理及复杂性分析。它不仅能与 Claude、Chatgpt、Gemini 等 AI 模型深度整合,还支持 VS Code、Cursor、Windsurf等编辑器,让开发者在熟悉的开发环境中轻松管理复杂项目。", 6 | "category": "mcp", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 6, 10 | "created_at": "2025-07-07T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "taskmaster" 14 | }, 15 | { 16 | "name": "mcpcn", 17 | "url": "https://mcpcn.com/", 18 | "description": "mcp 中文网站,有丰富的 mcp 资料", 19 | "category": "mcp", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 21, 23 | "created_at": "2025-10-15T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "mcpcn" 27 | }, 28 | { 29 | "name": "mcp.so", 30 | "url": "https://mcp.so/zh", 31 | "description": "mcp 市场,有丰富的 mcp 工具", 32 | "category": "mcp", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 4, 36 | "created_at": "2025-10-15T00:00:00Z", 37 | "is_featured": false, 38 | "id": 3, 39 | "identifier": "mcpso" 40 | }, 41 | { 42 | "name": "cursor-mcp", 43 | "url": "https://cursor.directory/mcp", 44 | "description": "另一个mcp 市场,有丰富的 mcp 工具", 45 | "category": "mcp", 46 | "tags": [], 47 | "icon": "🔧", 48 | "view_count": 0, 49 | "created_at": "2025-10-15T00:00:00Z", 50 | "is_featured": false, 51 | "id": 4, 52 | "identifier": "cursor-mcp" 53 | }, 54 | { 55 | "name": "context7", 56 | "url": "https://context7.com/", 57 | "description": "Context7  MCP 是一款基于 Model Context Protocol (MCP) 的服务工具,能够为 AI 编码助手在提示中自动注入最新库/框架的官方文档和代码示例,从而消除过时信息与“幻觉”代码,提升开发效率和准确性。", 58 | "category": "mcp", 59 | "tags": [], 60 | "icon": "🔧", 61 | "view_count": 2, 62 | "created_at": "2025-10-23T00:00:00Z", 63 | "is_featured": false, 64 | "id": 5, 65 | "identifier": "context7" 66 | }, 67 | { 68 | "name": "chrome-devtools", 69 | "url": "https://developer.chrome.com/blog/chrome-devtools-mcp?hl=zh-cn", 70 | "description": "Chrome DevTools MCP 是谷歌推出的 Model Context Protocol 服务器,作为 AI 编程助手(如 Claude、Cursor)与 Chrome 浏览器间的桥梁,将 DevTools 能力封装为可调用工具,支持 AI 完成页面操控、网络分析、性能追踪、控制台监控等操作,实现自动化测试、调试与优化的闭环,无需人工介入验证。", 71 | "category": "mcp", 72 | "tags": [], 73 | "icon": "🔧", 74 | "view_count": 1, 75 | "created_at": "2025-10-15T00:00:00Z", 76 | "is_featured": false, 77 | "id": 6, 78 | "identifier": "chrome-devtools" 79 | } 80 | ] -------------------------------------------------------------------------------- /config/tool_keywords.json: -------------------------------------------------------------------------------- 1 | [ 2 | "AI Code Reviewer", 3 | "Aider", 4 | "Antigravity", 5 | "Augment", 6 | "Auto-coder", 7 | "Bito", 8 | "BlinqIO", 9 | "ChatDev", 10 | "Claude-Code", 11 | "Cline", 12 | "CodeBuddy", 13 | "CodeDog", 14 | "CodeReviewBot", 15 | "CodeStory", 16 | "Codex", 17 | "Codium", 18 | "Comate", 19 | "Continue", 20 | "Cursor", 21 | "DevOpsGPT", 22 | "Devika", 23 | "Devin", 24 | "Draw-A-UI", 25 | "EarlyAI", 26 | "Factory", 27 | "Fine", 28 | "GitHub-Copilot", 29 | "HolmesGPT", 30 | "Jules", 31 | "Kiro", 32 | "Kombai", 33 | "KushoAI", 34 | "LingmaIDE", 35 | "Literallyanything", 36 | "Mage", 37 | "Marblism", 38 | "Melty", 39 | "MetaGPT", 40 | "MetaGPTX", 41 | "Meticulous.ai", 42 | "MutahunterAI", 43 | "Neovim", 44 | "OctoMind", 45 | "OpenHands", 46 | "PearAI", 47 | "Pico", 48 | "Qoder", 49 | "Replit", 50 | "Screenshot-to-Code", 51 | "Spec-kit", 52 | "Srcbook", 53 | "Tabnine", 54 | "Trae", 55 | "UI-Pilot", 56 | "Void", 57 | "Websim", 58 | "Windsurf", 59 | "Zed", 60 | "aigcode", 61 | "bolt.new-any-llm", 62 | "builder", 63 | "carbonate", 64 | "catpaw", 65 | "chrome-devtools", 66 | "codeflying", 67 | "codeguide", 68 | "coderabbit", 69 | "codewiki", 70 | "context7", 71 | "copilot-cli", 72 | "copycoder", 73 | "crush", 74 | "cursor-mcp", 75 | "deepwiki", 76 | "devchat", 77 | "flame", 78 | "fragments", 79 | "gambo", 80 | "gemini-cli", 81 | "iflow", 82 | "javaAI", 83 | "joycode", 84 | "junie", 85 | "keep", 86 | "lingma", 87 | "llamacoder", 88 | "locofy", 89 | "lovart", 90 | "mappie", 91 | "mcp.so", 92 | "mcpcn", 93 | "momen", 94 | "motiff", 95 | "potpie", 96 | "qodo", 97 | "qwen-code", 98 | "rocket", 99 | "roo-code", 100 | "same.new", 101 | "sudocode", 102 | "superdesign", 103 | "supermaven", 104 | "tabby", 105 | "taskmaster", 106 | "townie", 107 | "trae.cn", 108 | "trickle", 109 | "tusk", 110 | "warp", 111 | "webdraw", 112 | "youware", 113 | "zread", 114 | "verdent", 115 | "testsprite", 116 | "Momentic", 117 | "秒搭", 118 | "豆包编程", 119 | "yoyo", 120 | "toolify-tools", 121 | "superclaude", 122 | "nocode", 123 | "komment", 124 | "easycode", 125 | "devhunt", 126 | "codemagic", 127 | "code5", 128 | "buildship", 129 | "V0.dev", 130 | "SourceGraph", 131 | "Readdy", 132 | "Lovable", 133 | "Bolt.new", 134 | "Awesome AI Coding", 135 | "Awesome AI DevTools", 136 | "Awesome Code AI", 137 | "animaapp" 138 | ] -------------------------------------------------------------------------------- /data/tools/review.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "qodo", 4 | "url": "https://www.qodo.ai/", 5 | "description": "Qodo(前称Codium)是一个智能代码完整性平台,旨在审查、测试和编写代码。它将人工智能整合到开发工作流中,以加强每个阶段的代码质量。Qodo提供人工智能驱动的代码审查、测试和代码生成功能,旨在提升生产力和代码质量。它支持多种编程语言,并可与VSCode和JetBrains等集成开发环境,以及GitHub和GitLab等Git提供商集成。", 6 | "category": "review", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 8, 10 | "created_at": "2025-09-18T17:39:29.164000Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "qodo" 14 | }, 15 | { 16 | "name": "coderabbit", 17 | "url": "https://www.coderabbit.ai/blog", 18 | "description": "CodeRabbit 是一个由 AI 驱动的代码审查工具,可以在几分钟内为拉取请求提供上下文相关的反馈,大幅减少手动代码审查所需的时间和精力。", 19 | "category": "review", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 2, 23 | "created_at": "2024-12-01T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "coderabbit" 27 | }, 28 | { 29 | "name": "CodeReviewBot", 30 | "url": "https://codereviewbot.ai/", 31 | "description": "CodeReviewBot是一款自动化代码审查机器人,能够在代码提交时自动进行审查,提供改进建议。它支持多种编程语言,旨在提高代码审查的效率和一致性。", 32 | "category": "review", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 1, 36 | "created_at": "2024-12-01T00:00:00Z", 37 | "is_featured": false, 38 | "id": 3, 39 | "identifier": "codereviewbot" 40 | }, 41 | { 42 | "name": "CodeDog", 43 | "url": "https://www.codedog.ai/", 44 | "description": "CodeDog是一款AI驱动的代码审查工具,旨在通过自动化的方式提高代码质量和开发效率。它能够检测代码中的潜在问题、风格违规和安全漏洞,并提供修复建议,帮助开发者在早期阶段发现并解决问题。", 45 | "category": "review", 46 | "tags": [], 47 | "icon": "🔧", 48 | "view_count": 0, 49 | "created_at": "2024-12-01T00:00:00Z", 50 | "is_featured": false, 51 | "id": 4, 52 | "identifier": "codedog" 53 | }, 54 | { 55 | "name": "Bito", 56 | "url": "https://bito.ai/", 57 | "description": "Bito.ai\n 提供基于 AI 的代码审查服务,能结合整个代码库理解代码,在 GitLab、GitHub 等平台及 VS Code 等 IDE 中使用,支持自定义审查规则,可追踪 PR 数据,助力团队快速合并 PR、减少回归问题,且保障代码安全,帮团队提升开发效率。", 58 | "category": "review", 59 | "tags": [], 60 | "icon": "🔧", 61 | "view_count": 0, 62 | "created_at": "2024-12-01T00:00:00Z", 63 | "is_featured": false, 64 | "id": 5, 65 | "identifier": "bito" 66 | }, 67 | { 68 | "name": "AI Code Reviewer", 69 | "url": "https://github.com/buxuku/ai-code-reviewer", 70 | "description": "一个利用 openai api 对 gitlab 提交的 merge request 进行 code review 的小工具", 71 | "category": "review", 72 | "tags": [], 73 | "icon": "🔧", 74 | "view_count": 0, 75 | "created_at": "2024-12-01T00:00:00Z", 76 | "is_featured": false, 77 | "id": 6, 78 | "identifier": "aicodereviewer" 79 | } 80 | ] -------------------------------------------------------------------------------- /data/tools/doc.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "zread", 4 | "url": "https://zread.ai/", 5 | "description": "Zread能够把复杂的 GitHub 项目“翻译”成简单易懂的语言,包括快速生成结构化的 Wiki 文档,对代码进行梳理、总结,介绍项目的背景、口碑、团队等。有了这样的生产力工具,终于不用花好几天埋头研究代码,几分钟内就能掌握项目的核心内容。", 6 | "category": "doc", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 5, 10 | "created_at": "2025-08-16T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "zread" 14 | }, 15 | { 16 | "name": "mappie", 17 | "url": "https://www.mappie.ai/", 18 | "description": "产品需求文档AI生成", 19 | "category": "doc", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 5, 23 | "created_at": "2025-03-01T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "mappie" 27 | }, 28 | { 29 | "name": "deepwiki", 30 | "url": "https://deepwiki.com/", 31 | "description": "DeepWiki 是 Devin Wiki 和 Devin Search 的免费公共版本,可以将任何公共 GitHub 仓库快速转换成可对话的、类似维基式的文档界面", 32 | "category": "doc", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 0, 36 | "created_at": "2025-08-07T00:00:00Z", 37 | "is_featured": false, 38 | "id": 3, 39 | "identifier": "deepwiki" 40 | }, 41 | { 42 | "name": "codewiki", 43 | "url": "https://codewiki.google/", 44 | "description": "Code Wiki 是 Google 推出的 Gemini 驱动 AI 代码文档工具,可深度扫描 GitHub 代码仓库,自动生成含系统概览、模块说明与可视化图表的结构化 Wiki,支持代码提交实时同步更新,集成交互式聊天问答与代码直达导航,助力开发者快速理解代码库、团队协作与新成员上手,解放文档撰写精力。", 45 | "category": "doc", 46 | "tags": [], 47 | "icon": "🔧", 48 | "view_count": 2, 49 | "created_at": "2025-11-22T00:00:00Z", 50 | "is_featured": false, 51 | "id": 4, 52 | "identifier": "codewiki" 53 | }, 54 | { 55 | "name": "codeguide", 56 | "url": "https://www.codeguide.dev/", 57 | "description": "您的 AI 开发伴侣,帮助您编写文档、规划,以便更好的使用cursor,windsurf等工具进行开发", 58 | "category": "doc", 59 | "tags": [], 60 | "icon": "🔧", 61 | "view_count": 1, 62 | "created_at": "2024-11-27T00:00:00Z", 63 | "is_featured": false, 64 | "id": 5, 65 | "identifier": "codeguide" 66 | }, 67 | { 68 | "name": "Spec-kit", 69 | "url": "https://github.com/github/spec-kit", 70 | "description": "Spec-Kit 是 GitHub 官方推出的开源规范驱动开发工具包,以 “规范即代码” 为核心,通过深度集成 Copilot、Claude 等 AI 编码助手,将清晰完整的需求规格直接转化为可执行代码,颠覆传统开发中规格与实现脱节的问题。它依托 “项目宪法” 建立统一标准,覆盖从项目初始化到代码生成的全流程,支持多技术栈,适配新项目开发、遗留系统现代化等场景,能显著提升开发效率、保证代码质量与合规性,降低维护成本。", 71 | "category": "doc", 72 | "tags": [ 73 | "star", 74 | "hot" 75 | ], 76 | "icon": "🔧", 77 | "view_count": 0, 78 | "created_at": "2025-10-15T00:00:00Z", 79 | "is_featured": false, 80 | "id": 6, 81 | "identifier": "spec-kit" 82 | } 83 | ] -------------------------------------------------------------------------------- /app/infrastructure/crawlers/rss.py: -------------------------------------------------------------------------------- 1 | """RSS/Atom Feed 抓取器""" 2 | import asyncio 3 | from datetime import datetime 4 | from typing import List, Dict, Any 5 | from urllib.parse import urlparse 6 | 7 | import httpx 8 | from bs4 import BeautifulSoup 9 | from loguru import logger 10 | from feedparser import parse as feedparse 11 | 12 | 13 | async def fetch_rss_articles(feed_url: str, max_items: int = 10) -> List[Dict[str, Any]]: 14 | """ 15 | 从 RSS/Atom Feed 抓取文章 16 | 17 | Args: 18 | feed_url: RSS/Atom Feed URL 19 | max_items: 最多抓取的文章数量 20 | 21 | Returns: 22 | 文章列表,每个文章包含 title, url, source, summary, published_time 23 | """ 24 | try: 25 | async with httpx.AsyncClient(timeout=10.0) as client: 26 | resp = await client.get(feed_url) 27 | resp.raise_for_status() 28 | 29 | feed = feedparse(resp.text) 30 | 31 | if feed.bozo and feed.bozo_exception: 32 | logger.warning(f"Feed parse warning for {feed_url}: {feed.bozo_exception}") 33 | 34 | articles = [] 35 | for entry in feed.entries[:max_items]: 36 | # 提取发布时间 37 | published_time = None 38 | if hasattr(entry, 'published_parsed') and entry.published_parsed: 39 | published_time = datetime(*entry.published_parsed[:6]) 40 | elif hasattr(entry, 'updated_parsed') and entry.updated_parsed: 41 | published_time = datetime(*entry.updated_parsed[:6]) 42 | 43 | # 只抓取今天的文章 44 | if published_time and published_time.date() != datetime.now().date(): 45 | continue 46 | 47 | # 提取摘要 48 | summary = "" 49 | if hasattr(entry, 'summary'): 50 | summary = entry.summary 51 | elif hasattr(entry, 'description'): 52 | summary = entry.description 53 | 54 | # 清理 HTML 标签 55 | if summary: 56 | soup = BeautifulSoup(summary, 'html.parser') 57 | summary = soup.get_text().strip()[:200] # 限制长度 58 | 59 | articles.append({ 60 | "title": entry.title if hasattr(entry, 'title') else "无标题", 61 | "url": entry.link if hasattr(entry, 'link') else "", 62 | "source": "100kwhy", # 爬取的资讯统一使用"100kwhy"作为来源 63 | "summary": summary, 64 | "published_time": published_time.isoformat() if published_time else None, 65 | }) 66 | 67 | logger.info(f"从 RSS Feed {feed_url} 抓取到 {len(articles)} 篇文章") 68 | return articles 69 | 70 | except Exception as e: 71 | logger.error(f"抓取 RSS Feed {feed_url} 失败: {e}") 72 | return [] 73 | 74 | -------------------------------------------------------------------------------- /app/infrastructure/crawlers/hackernews.py: -------------------------------------------------------------------------------- 1 | """Hacker News 抓取器""" 2 | import asyncio 3 | from datetime import datetime 4 | from typing import List, Dict, Any 5 | 6 | import httpx 7 | from loguru import logger 8 | 9 | 10 | async def fetch_hackernews_articles(min_points: int = 100, max_items: int = 10) -> List[Dict[str, Any]]: 11 | """ 12 | 从 Hacker News 抓取高分文章 13 | 14 | Args: 15 | min_points: 最低分数阈值 16 | max_items: 最多抓取的文章数量 17 | 18 | Returns: 19 | 文章列表,每个文章包含 title, url, source, summary, points 20 | """ 21 | try: 22 | # Hacker News API 23 | top_stories_url = "https://hacker-news.firebaseio.com/v0/topstories.json" 24 | 25 | async with httpx.AsyncClient(timeout=10.0) as client: 26 | # 获取热门文章 ID 列表 27 | resp = await client.get(top_stories_url) 28 | resp.raise_for_status() 29 | story_ids = resp.json()[:max_items * 2] # 多获取一些以便筛选 30 | 31 | articles = [] 32 | async with httpx.AsyncClient(timeout=10.0) as client: 33 | # 并发获取文章详情 34 | tasks = [] 35 | for story_id in story_ids: 36 | tasks.append(_fetch_story_detail(client, story_id)) 37 | 38 | results = await asyncio.gather(*tasks, return_exceptions=True) 39 | 40 | for result in results: 41 | if isinstance(result, Exception): 42 | continue 43 | if result and result.get("points", 0) >= min_points: 44 | articles.append(result) 45 | if len(articles) >= max_items: 46 | break 47 | 48 | logger.info(f"从 Hacker News 抓取到 {len(articles)} 篇高分文章(≥{min_points} points)") 49 | return articles 50 | 51 | except Exception as e: 52 | logger.error(f"抓取 Hacker News 失败: {e}") 53 | return [] 54 | 55 | 56 | async def _fetch_story_detail(client: httpx.AsyncClient, story_id: int) -> Dict[str, Any]: 57 | """获取单篇文章详情""" 58 | try: 59 | url = f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json" 60 | resp = await client.get(url) 61 | resp.raise_for_status() 62 | data = resp.json() 63 | 64 | if data.get("type") != "story" or not data.get("url"): 65 | return None 66 | 67 | return { 68 | "title": data.get("title", "无标题"), 69 | "url": data.get("url", ""), 70 | "source": "100kwhy", # 爬取的资讯统一使用"100kwhy"作为来源 71 | "summary": f"分数: {data.get('score', 0)} points | 评论: {data.get('descendants', 0)}", 72 | "points": data.get("score", 0), 73 | } 74 | except Exception as e: 75 | logger.debug(f"获取 Hacker News 文章 {story_id} 详情失败: {e}") 76 | return None 77 | 78 | -------------------------------------------------------------------------------- /app/infrastructure/crawlers/github_trending.py: -------------------------------------------------------------------------------- 1 | """GitHub Trending 抓取器""" 2 | import asyncio 3 | from datetime import datetime 4 | from typing import List, Dict, Any 5 | 6 | import httpx 7 | from bs4 import BeautifulSoup 8 | from loguru import logger 9 | 10 | 11 | async def fetch_github_trending(language: str = "python", max_items: int = 10) -> List[Dict[str, Any]]: 12 | """ 13 | 从 GitHub Trending 抓取热门项目 14 | 15 | Args: 16 | language: 编程语言(python, javascript, go 等),空字符串表示所有语言 17 | max_items: 最多抓取的项目数量 18 | 19 | Returns: 20 | 项目列表,每个项目包含 title, url, source, summary 21 | """ 22 | try: 23 | url = "https://github.com/trending" 24 | if language: 25 | url += f"/{language}" 26 | 27 | async with httpx.AsyncClient(timeout=10.0, headers={ 28 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" 29 | }) as client: 30 | resp = await client.get(url) 31 | resp.raise_for_status() 32 | 33 | soup = BeautifulSoup(resp.text, 'html.parser') 34 | articles = [] 35 | 36 | # GitHub Trending 的 HTML 结构可能会变化,这里是一个基础实现 37 | repo_items = soup.select("article.Box-row")[:max_items] 38 | 39 | for item in repo_items: 40 | # 提取仓库名称和链接 41 | title_elem = item.select_one("h2 a") 42 | if not title_elem: 43 | continue 44 | 45 | repo_name = title_elem.get_text(strip=True) 46 | repo_url = "https://github.com" + title_elem.get("href", "") 47 | 48 | # 提取描述 49 | desc_elem = item.select_one("p.col-9") 50 | summary = desc_elem.get_text(strip=True) if desc_elem else "" 51 | 52 | # 提取语言和星标数 53 | lang_elem = item.select_one("span[itemprop='programmingLanguage']") 54 | lang = lang_elem.get_text(strip=True) if lang_elem else "" 55 | 56 | stars_elem = item.select_one("a[href*='/stargazers']") 57 | stars = stars_elem.get_text(strip=True) if stars_elem else "" 58 | 59 | if lang: 60 | summary = f"[{lang}] {summary}" if summary else f"编程语言: {lang}" 61 | if stars: 62 | summary = f"{summary} ⭐ {stars}" if summary else f"⭐ {stars}" 63 | 64 | articles.append({ 65 | "title": repo_name, 66 | "url": repo_url, 67 | "source": "100kwhy", # 爬取的资讯统一使用"100kwhy"作为来源 68 | "summary": summary or "GitHub 热门项目", 69 | }) 70 | 71 | logger.info(f"从 GitHub Trending ({language}) 抓取到 {len(articles)} 个项目") 72 | return articles 73 | 74 | except Exception as e: 75 | logger.error(f"抓取 GitHub Trending 失败: {e}") 76 | return [] 77 | 78 | -------------------------------------------------------------------------------- /scripts/add_tool_identifiers.py: -------------------------------------------------------------------------------- 1 | """ 2 | 为现有工具添加 identifier 字段 3 | 4 | 使用方法: 5 | python scripts/add_tool_identifiers.py 6 | """ 7 | import json 8 | import sys 9 | from pathlib import Path 10 | 11 | # 添加项目根目录到路径 12 | project_root = Path(__file__).resolve().parent.parent 13 | sys.path.insert(0, str(project_root)) 14 | 15 | from app.services.data_loader import DataLoader 16 | from loguru import logger 17 | 18 | # 配置日志 19 | logger.add( 20 | sys.stderr, 21 | format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function} - {message}", 22 | level="INFO" 23 | ) 24 | 25 | 26 | def generate_identifier(tool_name: str) -> str: 27 | """ 28 | 生成工具的 identifier 29 | 30 | Args: 31 | tool_name: 工具名称 32 | 33 | Returns: 34 | identifier 字符串 35 | """ 36 | if not tool_name: 37 | return "" 38 | 39 | # 转换为小写,保留字母、数字、连字符和下划线 40 | identifier = "".join(c.lower() if c.isalnum() or c in "-_" else "" for c in tool_name) 41 | return identifier 42 | 43 | 44 | def add_identifiers_to_tools(): 45 | """为所有工具添加 identifier 字段""" 46 | tools_dir = project_root / "data" / "tools" 47 | 48 | if not tools_dir.exists(): 49 | logger.error(f"工具目录不存在: {tools_dir}") 50 | return 51 | 52 | total_updated = 0 53 | 54 | # 遍历所有工具文件 55 | for tool_file in tools_dir.glob("*.json"): 56 | if tool_file.name == "featured.json": 57 | continue # 跳过 featured.json,因为它是从其他文件汇总的 58 | 59 | logger.info(f"处理文件: {tool_file.name}") 60 | 61 | # 加载工具 62 | tools = DataLoader._load_json_file(tool_file) 63 | 64 | if not tools: 65 | logger.warning(f"文件 {tool_file.name} 为空") 66 | continue 67 | 68 | updated_count = 0 69 | 70 | # 为每个工具添加 identifier 71 | for tool in tools: 72 | if "identifier" not in tool or not tool.get("identifier"): 73 | tool_name = tool.get("name", "").strip() 74 | if tool_name: 75 | tool["identifier"] = generate_identifier(tool_name) 76 | updated_count += 1 77 | logger.debug(f"为工具 '{tool_name}' 添加 identifier: {tool['identifier']}") 78 | 79 | # 保存文件 80 | if updated_count > 0: 81 | if DataLoader._save_json_file(tool_file, tools): 82 | total_updated += updated_count 83 | logger.success(f"✅ {tool_file.name}: 更新了 {updated_count} 个工具") 84 | else: 85 | logger.error(f"❌ {tool_file.name}: 保存失败") 86 | else: 87 | logger.info(f"ℹ️ {tool_file.name}: 所有工具已有 identifier") 88 | 89 | logger.info(f"🎉 完成!共更新 {total_updated} 个工具") 90 | 91 | 92 | if __name__ == "__main__": 93 | add_identifiers_to_tools() 94 | 95 | -------------------------------------------------------------------------------- /check_duplicate_jobs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 检查是否有多个应用进程在运行,可能导致重复推送 4 | """ 5 | import subprocess 6 | import sys 7 | import os 8 | 9 | def check_running_processes(): 10 | """检查是否有多个应用进程在运行""" 11 | if sys.platform == "win32": 12 | # Windows 13 | try: 14 | result = subprocess.run( 15 | ["tasklist", "/FI", "IMAGENAME eq python.exe", "/FO", "CSV"], 16 | capture_output=True, 17 | text=True, 18 | encoding='utf-8' 19 | ) 20 | lines = result.stdout.strip().split('\n') 21 | python_processes = [line for line in lines if 'python.exe' in line.lower()] 22 | print(f"发现 {len(python_processes)} 个 Python 进程:") 23 | for i, proc in enumerate(python_processes, 1): 24 | print(f" {i}. {proc}") 25 | 26 | # 检查是否有 uvicorn 进程 27 | result2 = subprocess.run( 28 | ["tasklist", "/FI", "IMAGENAME eq python.exe", "/V"], 29 | capture_output=True, 30 | text=True, 31 | encoding='utf-8' 32 | ) 33 | uvicorn_count = result2.stdout.lower().count('uvicorn') 34 | if uvicorn_count > 0: 35 | print(f"\n⚠️ 警告:发现 {uvicorn_count} 个可能包含 uvicorn 的进程") 36 | print(" 这可能导致多个调度器实例同时运行!") 37 | return True 38 | except Exception as e: 39 | print(f"检查进程时出错: {e}") 40 | else: 41 | # Linux/Mac 42 | try: 43 | result = subprocess.run( 44 | ["ps", "aux"], 45 | capture_output=True, 46 | text=True 47 | ) 48 | uvicorn_lines = [line for line in result.stdout.split('\n') if 'uvicorn' in line.lower() and 'app.main:app' in line] 49 | if len(uvicorn_lines) > 1: 50 | print(f"⚠️ 警告:发现 {len(uvicorn_lines)} 个 uvicorn 进程:") 51 | for i, line in enumerate(uvicorn_lines, 1): 52 | print(f" {i}. {line}") 53 | return True 54 | elif len(uvicorn_lines) == 1: 55 | print(f"✓ 只发现 1 个 uvicorn 进程(正常)") 56 | return False 57 | else: 58 | print("未发现运行中的 uvicorn 进程") 59 | return False 60 | except Exception as e: 61 | print(f"检查进程时出错: {e}") 62 | 63 | return False 64 | 65 | if __name__ == "__main__": 66 | print("=" * 60) 67 | print("检查重复进程") 68 | print("=" * 60) 69 | has_duplicate = check_running_processes() 70 | print("\n" + "=" * 60) 71 | if has_duplicate: 72 | print("建议:") 73 | print("1. 停止所有运行中的应用实例") 74 | print("2. 只启动一个应用实例") 75 | print("3. 检查是否有 systemd、supervisor 等服务管理工具启动了多个实例") 76 | else: 77 | print("未发现重复进程,问题可能在其他地方") 78 | print("=" * 60) 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "tools": { 4 | "id": "page-tools", 5 | "title": "热门工具", 6 | "description": "发现最优秀的开发工具和资源" 7 | }, 8 | "news": { 9 | "id": "page-news", 10 | "title": "编程资讯", 11 | "description": "最新技术文章和资讯" 12 | }, 13 | "ai-news": { 14 | "id": "page-ai-news", 15 | "title": "AI资讯", 16 | "description": "AI领域最新动态和技术文章" 17 | }, 18 | "recent": { 19 | "id": "page-recent", 20 | "title": "最新资讯", 21 | "description": "编程资讯和AI资讯的最新文章,按时间排序" 22 | }, 23 | "prompts": { 24 | "id": "page-prompts", 25 | "title": "提示词", 26 | "description": "精选AI编程提示词,提升开发效率" 27 | }, 28 | "rules": { 29 | "id": "page-rules", 30 | "title": "规则", 31 | "description": "Cursor Rules和其他AI编程规则" 32 | }, 33 | "resources": { 34 | "id": "page-resources", 35 | "title": "社区资源", 36 | "description": "AI编程教程、文章和社区资源" 37 | }, 38 | "wechat-mp": { 39 | "id": "page-wechat-mp", 40 | "title": "微信公众号", 41 | "description": "关注我们的微信公众号,获取最新技术资讯" 42 | } 43 | }, 44 | "categories": { 45 | "tools": { 46 | "ide": { 47 | "id": "cat-tools-ide", 48 | "name": "开发IDE", 49 | "description": "集成开发环境" 50 | }, 51 | "plugin": { 52 | "id": "cat-tools-plugin", 53 | "name": "IDE插件", 54 | "description": "编辑器插件和扩展" 55 | }, 56 | "cli": { 57 | "id": "cat-tools-cli", 58 | "name": "命令行工具", 59 | "description": "终端和命令行工具" 60 | }, 61 | "codeagent": { 62 | "id": "cat-tools-codeagent", 63 | "name": "CodeAgent", 64 | "description": "AI代码助手和代理" 65 | }, 66 | "ai-test": { 67 | "id": "cat-tools-ai-test", 68 | "name": "AI测试", 69 | "description": "AI驱动的测试工具" 70 | }, 71 | "review": { 72 | "id": "cat-tools-review", 73 | "name": "代码审查", 74 | "description": "代码审查和质量检查工具" 75 | }, 76 | "devops": { 77 | "id": "cat-tools-devops", 78 | "name": "DevOps 工具", 79 | "description": "开发和运维工具" 80 | }, 81 | "doc": { 82 | "id": "cat-tools-doc", 83 | "name": "文档相关", 84 | "description": "文档编写和管理工具" 85 | }, 86 | "design": { 87 | "id": "cat-tools-design", 88 | "name": "设计工具", 89 | "description": "UI/UX设计工具" 90 | }, 91 | "ui": { 92 | "id": "cat-tools-ui", 93 | "name": "UI生成", 94 | "description": "AI驱动的UI生成工具" 95 | }, 96 | "mcp": { 97 | "id": "cat-tools-mcp", 98 | "name": "MCP工具", 99 | "description": "Model Context Protocol工具" 100 | } 101 | }, 102 | "articles": { 103 | "programming": { 104 | "id": "cat-articles-programming", 105 | "name": "编程资讯", 106 | "description": "编程和技术相关文章" 107 | }, 108 | "ai_news": { 109 | "id": "cat-articles-ai-news", 110 | "name": "AI资讯", 111 | "description": "AI和机器学习相关文章" 112 | } 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # 数据目录说明 2 | 3 | 本目录用于存储工具和资讯的元数据(不存储实际内容,只存储链接和元信息)。 4 | 5 | ## 目录结构 6 | 7 | ``` 8 | data/ 9 | ├── tools/ # 工具数据 10 | │ ├── featured.json # 热门工具 11 | │ ├── cli.json # 命令行工具 12 | │ ├── ide.json # 开发IDE 13 | │ └── ... # 其他分类 14 | └── articles/ # 资讯数据 15 | ├── programming.json # 编程资讯 16 | ├── ai_news.json # AI资讯 17 | └── ... # 其他分类 18 | ``` 19 | 20 | ## 数据格式 21 | 22 | ### 工具数据格式 (tools/*.json) 23 | 24 | ```json 25 | { 26 | "id": 1, 27 | "name": "工具名称", 28 | "url": "https://tool-url.com", 29 | "description": "工具描述", 30 | "category": "cli", 31 | "tags": ["SaaS", "AI", "终端"], 32 | "icon": "", 33 | "score": 9.5, 34 | "view_count": 1250, 35 | "like_count": 89, 36 | "is_featured": true, 37 | "created_at": "2025-01-08T10:00:00Z" 38 | } 39 | ``` 40 | 41 | **字段说明:** 42 | - `id`: 唯一标识符 43 | - `name`: 工具名称 44 | - `url`: 工具官网链接 45 | - `description`: 工具描述 46 | - `category`: 工具分类(cli, ide, ai-test, devops, plugin, review, doc, design, ui, codeagent, mcp, other) 47 | - `tags`: 标签列表 48 | - `icon`: 图标(emoji或字符) 49 | - `score`: 热度分/推荐指数(0-10) 50 | - `view_count`: 访问次数 51 | - `like_count`: 点赞数 52 | - `is_featured`: 是否热门推荐 53 | - `created_at`: 创建时间(ISO 8601格式) 54 | 55 | ### 资讯数据格式 (articles/*.json) 56 | 57 | ```json 58 | { 59 | "id": 1, 60 | "title": "文章标题", 61 | "url": "https://article-url.com", 62 | "source": "来源名称", 63 | "summary": "文章摘要", 64 | "category": "programming", 65 | "archived_at": "2025-01-08T10:00:00Z", 66 | "created_at": "2025-01-08T10:00:00Z", 67 | "published_time": "2025-01-08T10:00:00Z", 68 | "tool_tags": ["工具名称1", "工具名称2"], 69 | "tags": ["标签1", "标签2"], 70 | "view_count": 0, 71 | "score": 8.5 72 | } 73 | ``` 74 | 75 | **字段说明:** 76 | - `id`: 唯一标识符(必需,归档时自动生成) 77 | - `title`: 文章标题(必需) 78 | - `url`: 文章链接(必需,唯一) 79 | - `source`: 来源(可选,公众号名/网站名,为空时显示"未知来源") 80 | - `summary`: 文章摘要(可选) 81 | - `category`: 文章分类(必需,programming, ai_news等) 82 | - `archived_at`: 采纳/归档时间(必需,ISO 8601格式,归档时自动设置) 83 | - `created_at`: 创建时间(必需,ISO 8601格式,归档时自动设置) 84 | - `published_time`: 原始发布时间(可选,ISO 8601格式) 85 | - `tool_tags`: 工具标签列表(可选,用于工具详情页关联) 86 | - `tags`: 普通标签列表(可选) 87 | - `view_count`: 点击次数(可选,默认0,用于热度计算) 88 | - `score`: 热度分(可选,0-10) 89 | 90 | **重要说明:** 91 | - `archived_at` 是必需字段,表示文章被采纳的时间,归档函数会自动设置 92 | - UI显示日期时优先使用 `archived_at`(采纳日期) 93 | - 如果现有数据缺少日期字段,系统会在加载时自动补充(使用文件修改时间) 94 | 95 | ## 注意事项 96 | 97 | 1. **不存储实际内容**:本平台只存储链接和元信息,不存储文章或工具的完整内容 98 | 2. **数据去重**:系统会自动根据 `id` 字段去重 99 | 3. **分页加载**:所有API都支持分页,默认每页20条,最大100条 100 | 4. **文件命名**:建议使用分类名称作为文件名(如 `cli.json`, `programming.json`) 101 | 5. **数据更新**:可以通过管理面板或直接编辑JSON文件来更新数据 102 | 103 | ## API使用 104 | 105 | ### 获取工具列表 106 | ``` 107 | GET /api/tools?category=cli&page=1&page_size=20 108 | ``` 109 | 110 | ### 获取热门工具 111 | ``` 112 | GET /api/tools/featured?page=1&page_size=20 113 | ``` 114 | 115 | ### 获取编程资讯 116 | ``` 117 | GET /api/news?category=programming&page=1&page_size=20 118 | ``` 119 | 120 | ### 获取AI资讯 121 | ``` 122 | GET /api/ai-news?page=1&page_size=20 123 | ``` 124 | 125 | ### 获取最近收录 126 | ``` 127 | GET /api/recent?type_filter=all&page=1&page_size=20 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /docs/feature/tool_article_crawling_feature.md: -------------------------------------------------------------------------------- 1 | # 工具相关资讯获取功能 2 | 3 | ## 功能概述 4 | 5 | 工具相关资讯获取功能允许管理员为每个工具手动触发相关资讯的获取,获取到的资讯会自动关联到对应工具,并在工具详情页展示。 6 | 7 | ## 核心特性 8 | 9 | ### 1. 工具关键字自动管理 10 | 11 | - **自动添加**:当工具被采纳到正式工具库时,系统会自动将工具名称添加到工具关键字配置中 12 | - **配置文件**:工具关键字存储在 `config/tool_keywords.json` 13 | - **去重处理**:自动避免重复添加相同的工具名称 14 | 15 | ### 2. 手动触发获取 16 | 17 | - **单个工具获取**:管理员可以选择特定工具关键字,每次获取1篇当天的相关资讯 18 | - **批量获取**:支持一次性获取所有工具的相关资讯 19 | - **当天内容**:只获取当天的资讯内容,确保内容时效性 20 | 21 | ### 3. 自动标签关联 22 | 23 | - **工具标签**:获取到的资讯会自动带有工具名称标签 24 | - **自动提取**:归档或采纳资讯时,系统会自动从获取来源中提取工具名称作为标签 25 | - **工具关联**:带有工具标签的资讯会在工具详情页自动展示 26 | 27 | ### 4. 资讯管理 28 | 29 | - **候选池**:获取到的资讯首先进入候选池,等待管理员审核 30 | - **采纳**:采纳后进入正式资讯列表,用于每日推送 31 | - **归档**:归档后进入网站资讯列表(编程资讯或AI资讯),可在前端页面展示 32 | - **忽略**:可以忽略不需要的资讯 33 | 34 | ## 技术实现 35 | 36 | ### 配置文件 37 | 38 | **工具关键字配置** (`config/tool_keywords.json`): 39 | ```json 40 | [ 41 | "Cursor", 42 | "GitHub-Copilot", 43 | "V0.dev", 44 | ... 45 | ] 46 | ``` 47 | 48 | ### API 接口 49 | 50 | #### 1. 获取工具关键字列表 51 | ``` 52 | GET /digest/tool-keywords 53 | ``` 54 | 返回所有工具关键字及其数量。 55 | 56 | #### 2. 获取工具相关资讯 57 | ``` 58 | POST /digest/crawl-tool-articles 59 | Body: { 60 | "keyword": "Cursor" // 可选,不提供则获取所有工具 61 | } 62 | ``` 63 | - 如果提供 `keyword`,只获取该工具的相关资讯 64 | - 如果不提供,获取所有工具的相关资讯 65 | - 每个工具关键字每次只获取1篇当天的文章 66 | 67 | ### 数据流程 68 | 69 | 1. **工具采纳** → 自动添加工具名称到关键字配置 70 | 2. **手动触发获取** → 使用工具关键字获取相关资讯 71 | 3. **资讯进入候选池** → 带有 `tool_keyword:工具名称` 标识 72 | 4. **归档/采纳** → 自动提取工具名称作为 `tool_tags` 73 | 5. **工具详情页** → 通过 `tool_tags` 匹配显示相关资讯 74 | 75 | ### 关键代码位置 76 | 77 | - **配置管理**: `app/config_loader.py` 78 | - `load_tool_keywords()` - 加载工具关键字 79 | - `save_tool_keywords()` - 保存工具关键字 80 | - `add_tool_keyword()` - 添加工具关键字 81 | 82 | - **API接口**: `app/routes/digest.py` 83 | - `POST /crawl-tool-articles` - 获取工具相关资讯 84 | - `GET /tool-keywords` - 获取工具关键字列表 85 | - `POST /accept-tool-candidate` - 采纳工具时自动添加关键字 86 | 87 | - **前端界面**: `app/routes/digest.py` (管理员面板HTML) 88 | - 工具关键字下拉选择器 89 | - 单个工具获取按钮 90 | - 批量获取按钮 91 | 92 | ## 使用指南 93 | 94 | ### 管理员操作流程 95 | 96 | 1. **查看工具关键字** 97 | - 进入管理员面板 98 | - 在"文章获取与候选池"部分查看"工具相关资讯获取"区域 99 | - 查看当前工具关键字数量 100 | 101 | 2. **获取单个工具资讯** 102 | - 从下拉列表选择工具关键字 103 | - 点击"获取该工具资讯"按钮 104 | - 系统会获取1篇当天的相关资讯 105 | 106 | 3. **批量获取所有工具资讯** 107 | - 点击"获取所有工具资讯"按钮 108 | - 系统会遍历所有工具关键字,每个关键字获取1篇资讯 109 | 110 | 4. **审核和管理资讯** 111 | - 在候选池中查看获取到的资讯 112 | - 归档时,工具标签会自动填充(如果是工具相关资讯) 113 | - 采纳或归档后,资讯会关联到对应工具 114 | 115 | ### 工具关键字管理 116 | 117 | - **自动添加**:工具被采纳时自动添加,无需手动操作 118 | - **手动更新**:如需批量更新,可以直接编辑 `config/tool_keywords.json` 119 | - **初始化**:系统启动时会从所有现有工具中提取名称并写入配置文件 120 | 121 | ## 注意事项 122 | 123 | 1. **获取频率**:建议不要过于频繁地获取,避免对数据源造成压力 124 | 2. **内容审核**:所有获取到的资讯都需要管理员审核后才能发布 125 | 3. **标签关联**:归档时请确认工具标签是否正确,可以手动修改 126 | 4. **当天内容**:系统只获取当天的资讯,确保内容时效性 127 | 128 | ## 数据关联 129 | 130 | ### 工具与资讯的关联机制 131 | 132 | - **工具标签** (`tool_tags`): 资讯归档或采纳时,会自动添加工具名称作为标签 133 | - **工具详情页**:通过匹配资讯的 `tool_tags` 和工具的 `name` 或 `identifier` 来显示相关资讯 134 | - **双向关联**:工具详情页可以查看相关资讯,资讯也可以关联多个工具 135 | 136 | ## 更新日志 137 | 138 | - **v3.0**: 新增工具相关资讯获取功能 139 | - 支持工具关键字自动管理 140 | - 支持手动触发单个或批量获取 141 | - 支持自动标签关联 142 | - 支持在工具详情页展示相关资讯 143 | 144 | -------------------------------------------------------------------------------- /scripts/test_wechat_mp.py: -------------------------------------------------------------------------------- 1 | """测试微信公众号功能""" 2 | import asyncio 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | # 添加项目根目录到路径 8 | project_root = Path(__file__).resolve().parents[1] 9 | sys.path.insert(0, str(project_root)) 10 | 11 | from app.notifier.wechat_mp import WeChatMPClient 12 | 13 | 14 | async def test_access_token(): 15 | """测试获取 Access Token""" 16 | print("\n" + "="*60) 17 | print("测试获取 Access Token") 18 | print("="*60) 19 | 20 | client = WeChatMPClient() 21 | 22 | if not client.appid or not client.secret: 23 | print("❌ 错误:未配置 WECHAT_MP_APPID 或 WECHAT_MP_SECRET") 24 | print("\n请在 .env 文件中配置:") 25 | print("WECHAT_MP_APPID=your_appid") 26 | print("WECHAT_MP_SECRET=your_secret") 27 | return False 28 | 29 | print(f"AppID: {client.appid[:10]}...") 30 | print("正在获取 Access Token...") 31 | 32 | token = await client.get_access_token() 33 | 34 | if token: 35 | print(f"✅ Access Token 获取成功: {token[:20]}...") 36 | return True 37 | else: 38 | print("❌ Access Token 获取失败") 39 | print("请检查:") 40 | print("1. AppID 和 Secret 是否正确") 41 | print("2. 网络连接是否正常") 42 | print("3. IP 是否在白名单中(如果设置了)") 43 | return False 44 | 45 | 46 | async def test_create_draft(): 47 | """测试创建草稿""" 48 | print("\n" + "="*60) 49 | print("测试创建草稿") 50 | print("="*60) 51 | 52 | client = WeChatMPClient() 53 | 54 | # 测试文章数据 55 | test_articles = [ 56 | { 57 | "title": "测试文章标题", 58 | "author": "测试作者", 59 | "digest": "这是一篇测试文章的摘要", 60 | "content": "

这是测试文章的内容。

阅读原文

", 61 | "content_source_url": "https://example.com", 62 | "thumb_media_id": "", # 可选 63 | "show_cover_pic": 1, 64 | } 65 | ] 66 | 67 | print("正在创建草稿...") 68 | media_id = await client.create_draft(test_articles) 69 | 70 | if media_id: 71 | print(f"✅ 草稿创建成功,media_id: {media_id}") 72 | print("\n注意:这只是测试,不会真正发布。") 73 | print("你可以在微信公众平台的草稿箱中查看。") 74 | return media_id 75 | else: 76 | print("❌ 草稿创建失败") 77 | print("请检查文章格式和配置") 78 | return None 79 | 80 | 81 | async def main(): 82 | """主函数""" 83 | print("开始测试微信公众号功能...") 84 | print("\n注意:需要先配置 WECHAT_MP_APPID 和 WECHAT_MP_SECRET") 85 | 86 | # 测试 Access Token 87 | token_ok = await test_access_token() 88 | 89 | if not token_ok: 90 | print("\n⚠️ Access Token 获取失败,无法继续测试") 91 | return 92 | 93 | # 测试创建草稿(可选) 94 | print("\n是否测试创建草稿?(y/n): ", end="") 95 | # 在脚本中默认跳过,避免误操作 96 | # choice = input().strip().lower() 97 | # if choice == 'y': 98 | # await test_create_draft() 99 | 100 | print("\n" + "="*60) 101 | print("测试完成!") 102 | print("="*60) 103 | print("\n下一步:") 104 | print("1. 使用 API 接口测试创建草稿和发布") 105 | print("2. 访问 http://localhost:8000/docs 查看 API 文档") 106 | print("3. 参考 docs/deploy/wechat_mp_guide.md 了解详细使用方法") 107 | 108 | 109 | if __name__ == "__main__": 110 | asyncio.run(main()) 111 | 112 | -------------------------------------------------------------------------------- /docs/feature/资讯分类映射关系.md: -------------------------------------------------------------------------------- 1 | # 资讯分类映射关系说明 2 | 3 | 本文档清晰说明了UI模块、API端点、Category值和数据库表之间的对应关系。 4 | 5 | **注意**:数据已迁移到SQLite数据库,不再使用JSON文件存储正式数据。JSON文件仅用于候选池等临时数据。 6 | 7 | ## 📋 映射关系总览 8 | 9 | | UI模块名称 | 路由路径 | API端点 | Category值 | 数据库表 | 数据位置 | 10 | |----------|---------|---------|-----------|---------|---------| 11 | | **编程资讯** | `/news` | `/api/news?category=programming` | `programming` | `articles` 表 | SQLite数据库(`data.db`) | 12 | | **AI资讯** | `/ai-news` | `/api/ai-news` (内部调用 `/api/news?category=ai_news`) | `ai_news` | `articles` 表 | SQLite数据库(`data.db`) | 13 | 14 | ## 🔍 详细说明 15 | 16 | ### 1. 编程资讯模块 17 | 18 | - **UI显示名称**: "编程资讯" 19 | - **前端路由**: `/news` 20 | - **API端点**: 21 | - 直接调用: `/api/news?category=programming` 22 | - 或调用: `/api/news` (不传category时获取所有文章,前端可筛选) 23 | - **Category值**: `programming` 24 | - **数据库表**: `articles` 表,category字段为 `programming` 25 | - **数据位置**: SQLite数据库(`data.db`) 26 | - **用途**: 存储编程相关的技术文章和资讯 27 | 28 | ### 2. AI资讯模块 29 | 30 | - **UI显示名称**: "AI资讯" 31 | - **前端路由**: `/ai-news` 32 | - **API端点**: 33 | - 专用端点: `/api/ai-news` (内部重定向到 `/api/news?category=ai_news`) 34 | - 或直接调用: `/api/news?category=ai_news` 35 | - **Category值**: `ai_news` 36 | - **数据库表**: `articles` 表,category字段为 `ai_news` 37 | - **数据位置**: SQLite数据库(`data.db`) 38 | - **用途**: 存储AI相关的文章和资讯 39 | 40 | ## ⚠️ 重要注意事项 41 | 42 | ### Category值与数据库的映射规则 43 | 44 | 在 `app/services/database_data_service.py` 中,Category值对应数据库表的category字段: 45 | 46 | ```python 47 | # category值对应数据库表的category字段 48 | query = select(Article).where(Article.category == category) 49 | ``` 50 | 51 | **规则说明**: 52 | - `category == "programming"` → 数据库查询:`articles` 表中 `category='programming'` 53 | - `category == "ai_news"` → 数据库查询:`articles` 表中 `category='ai_news'` 54 | - 其他category → 数据库查询:`articles` 表中 `category={category}` 55 | 56 | ### 工具关键字爬取的资讯 57 | 58 | - **来源标识**: `crawled_from` 字段以 `"tool_keyword:"` 开头 59 | - **采纳后的分类**: `programming` (编程资讯) 60 | - **保存位置**: SQLite数据库(`data.db`),`articles` 表,category字段为 `programming` 61 | - **UI显示**: 显示在"编程资讯"模块下 62 | - **来源**: 统一使用"100kwhy"作为来源 63 | 64 | ## 📝 代码中的使用示例 65 | 66 | ### 前端调用示例 67 | 68 | ```javascript 69 | // 加载编程资讯 70 | loadArticles('programming', 1); 71 | 72 | // 加载AI资讯 73 | loadArticles('ai_news', 1); 74 | // 或直接调用专用API 75 | fetch('/api/ai-news?page=1'); 76 | ``` 77 | 78 | ### 后端归档示例 79 | 80 | ```python 81 | # 归档到编程资讯 82 | DataLoader.archive_article_to_category( 83 | article, 84 | category="programming", # Category值 85 | tool_tags=tool_tags 86 | ) 87 | # 实际保存到: SQLite数据库(data.db),articles表,category='programming' 88 | 89 | # 归档到AI资讯 90 | DataLoader.archive_article_to_category( 91 | article, 92 | category="ai_news", # Category值 93 | tool_tags=tool_tags 94 | ) 95 | # 实际保存到: SQLite数据库(data.db),articles表,category='ai_news' 96 | ``` 97 | 98 | ## 🔄 数据流转 99 | 100 | 1. **工具关键字爬取** → 候选池 (`ai_candidates.json`) 101 | 2. **管理员采纳** → 根据来源判断: 102 | - `crawled_from.startswith("tool_keyword:")` → 归档到 `programming.json` (编程资讯) 103 | - 其他来源 → 添加到推送列表 (`ai_articles.json`) 104 | 3. **用户提交** → 候选池 → 管理员审核 → 归档到对应分类 105 | 106 | ## 📌 相关文件位置 107 | 108 | - **数据加载服务**: `app/services/data_loader.py` 109 | - **API路由**: `app/routes/api.py` 110 | - **管理路由**: `app/routes/digest.py` 111 | - **前端HTML**: `app/main.py` (内嵌JavaScript) 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs/feature/资讯数据模型.md: -------------------------------------------------------------------------------- 1 | # 资讯数据模型说明 2 | 3 | ## 📋 数据模型定义 4 | 5 | 资讯文章(Article)的完整数据模型如下: 6 | 7 | ```json 8 | { 9 | "id": 1, // 必需:唯一标识符(整数) 10 | "title": "文章标题", // 必需:文章标题 11 | "url": "https://article-url.com", // 必需:文章链接(唯一) 12 | "source": "来源名称", // 可选:来源(公众号名/网站名),为空时显示"未知来源" 13 | "summary": "文章摘要", // 可选:文章摘要 14 | "category": "programming", // 必需:文章分类(programming, ai_news等) 15 | "archived_at": "2025-01-08T10:00:00Z", // 必需:采纳/归档时间(ISO 8601格式) 16 | "created_at": "2025-01-08T10:00:00Z", // 必需:创建时间(ISO 8601格式) 17 | "published_time": "2025-01-08T10:00:00Z", // 可选:原始发布时间(ISO 8601格式) 18 | "tool_tags": ["工具名称1", "工具名称2"], // 可选:工具标签列表(用于工具详情页关联) 19 | "tags": ["标签1", "标签2"], // 可选:普通标签列表 20 | "view_count": 0, // 可选:点击次数(热度),默认为0 21 | "score": 8.5 // 可选:热度分(0-10) 22 | } 23 | ``` 24 | 25 | ## 🔑 必需字段(Required Fields) 26 | 27 | 以下字段在归档文章时**必须**存在: 28 | 29 | 1. **`id`** - 唯一标识符 30 | - 如果归档时没有,系统会自动生成(从现有文件中找到最大ID+1) 31 | 32 | 2. **`title`** - 文章标题 33 | - 必须提供 34 | 35 | 3. **`url`** - 文章链接 36 | - 必须提供,用于去重和唯一标识 37 | 38 | 4. **`category`** - 文章分类 39 | - 必须提供,如:`programming`、`ai_news` 40 | 41 | 5. **`archived_at`** - 采纳/归档时间 ⚠️ **必需** 42 | - 必须提供,表示文章被采纳的时间 43 | - 格式:ISO 8601(如:`2025-01-08T10:00:00Z`) 44 | - 归档函数会自动设置:`datetime.now().isoformat() + "Z"` 45 | 46 | 6. **`created_at`** - 创建时间 47 | - 如果归档时没有,系统会自动设置为当前时间 48 | - 格式:ISO 8601 49 | 50 | ## 📝 可选字段(Optional Fields) 51 | 52 | 以下字段是可选的,但建议提供: 53 | 54 | 1. **`source`** - 来源 55 | - 如果为空字符串或不存在,UI显示"未知来源" 56 | - 爬取的资讯统一使用"100kwhy"作为来源 57 | - 建议填写公众号名或网站名 58 | 59 | 2. **`summary`** - 文章摘要 60 | - 用于在列表中显示文章简介 61 | 62 | 3. **`published_time`** - 原始发布时间 63 | - 如果归档时没有,系统会使用 `created_at` 的值 64 | 65 | 4. **`tool_tags`** - 工具标签列表 66 | - 用于工具详情页关联相关文章 67 | - 格式:字符串数组,如:`["Cursor", "Aider"]` 68 | 69 | 5. **`tags`** - 普通标签列表 70 | - 用于分类和搜索 71 | - 格式:字符串数组 72 | 73 | 6. **`view_count`** - 点击次数 74 | - 用于计算热度,默认为0 75 | - 每次点击文章链接时自动增加 76 | 77 | 7. **`score`** - 热度分 78 | - 0-10的评分,用于排序 79 | 80 | ## 🔄 归档时的自动处理 81 | 82 | 当调用 `DataLoader.archive_article_to_category()` 归档文章时,系统会自动: 83 | 84 | 1. **生成ID**(如果没有): 85 | - 从所有文章文件中找到最大ID,然后+1 86 | 87 | 2. **设置时间戳**: 88 | - `created_at`:如果不存在,设置为当前时间 89 | - `published_time`:如果不存在,使用 `created_at` 的值 90 | - `archived_at`:**强制设置为当前时间**(采纳时间) 91 | 92 | 3. **设置分类**: 93 | - `category`:设置为指定的分类值 94 | 95 | 4. **初始化默认值**: 96 | - `view_count`:如果不存在,设置为0 97 | 98 | 5. **处理工具标签**: 99 | - 如果提供了 `tool_tags`,会同时添加到 `tags` 中(去重) 100 | 101 | ## 📂 文件存储位置 102 | 103 | 根据分类,文章会存储到对应的JSON文件: 104 | 105 | - `category="programming"` → `data/articles/programming.json` 106 | - `category="ai_news"` → `data/articles/ai_news.json` 107 | 108 | ## ⚠️ 重要注意事项 109 | 110 | 1. **`archived_at` 是必需字段**: 111 | - 归档函数会强制设置此字段 112 | - UI显示日期时优先使用此字段 113 | - 如果现有数据缺少此字段,系统会在加载时使用文件修改时间补充 114 | 115 | 2. **数据去重**: 116 | - 基于 `id` 或 `url` 去重 117 | - 如果URL已存在,归档会失败 118 | 119 | 3. **日期格式**: 120 | - 所有日期字段使用 ISO 8601 格式 121 | - 示例:`2025-01-08T10:00:00Z` 122 | 123 | 4. **向后兼容**: 124 | - 如果加载的文章缺少日期字段,系统会自动补充 125 | - 使用文件修改时间作为 `archived_at` 的默认值 126 | 127 | 5. **来源字段**: 128 | - 爬取的资讯统一使用"100kwhy"作为来源 129 | - 用户提交的资讯保持原有来源标识 130 | 131 | ## 🔧 数据迁移 132 | 133 | 如果现有数据缺少必需字段,可以: 134 | 135 | 1. **手动更新JSON文件**:添加缺失的字段 136 | 2. **使用归档函数重新归档**:系统会自动补充所有必需字段 137 | 3. **系统自动补充**:加载时会自动使用文件修改时间补充日期字段 138 | 139 | ## 📌 相关代码位置 140 | 141 | - **归档函数**:`app/services/data_loader.py::archive_article_to_category()` 142 | - **加载函数**:`app/services/data_loader.py::get_articles()` 143 | - **数据模型文档**:`data/README.md` 144 | 145 | 146 | -------------------------------------------------------------------------------- /scripts/test_sources.py: -------------------------------------------------------------------------------- 1 | """测试多资讯源功能""" 2 | import asyncio 3 | import sys 4 | from pathlib import Path 5 | 6 | # 添加项目根目录到路径 7 | project_root = Path(__file__).resolve().parents[1] 8 | sys.path.insert(0, str(project_root)) 9 | 10 | from app.crawlers.rss import fetch_rss_articles 11 | from app.crawlers.github_trending import fetch_github_trending 12 | from app.crawlers.hackernews import fetch_hackernews_articles 13 | from app.sources.article_sources import fetch_from_all_sources 14 | 15 | 16 | async def test_rss(): 17 | """测试 RSS Feed""" 18 | print("\n" + "="*60) 19 | print("测试 RSS Feed 抓取") 20 | print("="*60) 21 | 22 | # 使用一些公开的 RSS Feed 进行测试 23 | test_feeds = [ 24 | "https://rss.cnn.com/rss/edition.rss", # CNN 25 | "https://feeds.bbci.co.uk/news/rss.xml", # BBC News 26 | ] 27 | 28 | for feed_url in test_feeds: 29 | print(f"\n测试 Feed: {feed_url}") 30 | articles = await fetch_rss_articles(feed_url, max_items=3) 31 | print(f"抓取到 {len(articles)} 篇文章") 32 | for i, article in enumerate(articles, 1): 33 | print(f" {i}. {article.get('title', '无标题')[:60]}") 34 | print(f" 来源: {article.get('source', '未知')}") 35 | print(f" 链接: {article.get('url', '')[:80]}") 36 | 37 | 38 | async def test_github_trending(): 39 | """测试 GitHub Trending""" 40 | print("\n" + "="*60) 41 | print("测试 GitHub Trending 抓取") 42 | print("="*60) 43 | 44 | languages = ["python", "javascript"] 45 | 46 | for lang in languages: 47 | print(f"\n测试语言: {lang}") 48 | articles = await fetch_github_trending(lang, max_items=3) 49 | print(f"抓取到 {len(articles)} 个项目") 50 | for i, article in enumerate(articles, 1): 51 | print(f" {i}. {article.get('title', '无标题')}") 52 | print(f" 来源: {article.get('source', '未知')}") 53 | print(f" 摘要: {article.get('summary', '无摘要')[:60]}") 54 | 55 | 56 | async def test_hackernews(): 57 | """测试 Hacker News""" 58 | print("\n" + "="*60) 59 | print("测试 Hacker News 抓取") 60 | print("="*60) 61 | 62 | articles = await fetch_hackernews_articles(min_points=50, max_items=5) 63 | print(f"抓取到 {len(articles)} 篇高分文章") 64 | for i, article in enumerate(articles, 1): 65 | print(f" {i}. {article.get('title', '无标题')[:60]}") 66 | print(f" 分数: {article.get('points', 0)} points") 67 | print(f" 链接: {article.get('url', '')[:80]}") 68 | 69 | 70 | async def test_all_sources(): 71 | """测试统一资讯源管理器""" 72 | print("\n" + "="*60) 73 | print("测试统一资讯源管理器") 74 | print("="*60) 75 | 76 | articles = await fetch_from_all_sources( 77 | keywords=["AI"], # 搜狗微信搜索关键词 78 | rss_feeds=[ 79 | "https://rss.cnn.com/rss/edition.rss", 80 | ], 81 | github_languages=["python"], 82 | hackernews_min_points=50, 83 | max_per_source=3, 84 | ) 85 | 86 | print(f"\n总共抓取到 {len(articles)} 篇文章(已按热度分排序)") 87 | print("\n前 10 篇文章:") 88 | for i, article in enumerate(articles[:10], 1): 89 | score = article.get("score", 0) 90 | print(f" {i}. [{score:.1f}分] {article.get('title', '无标题')[:60]}") 91 | print(f" 来源: {article.get('source', '未知')}") 92 | 93 | 94 | async def main(): 95 | """主函数""" 96 | print("开始测试多资讯源功能...") 97 | 98 | # 测试各个资讯源 99 | await test_rss() 100 | await test_github_trending() 101 | await test_hackernews() 102 | 103 | # 测试统一管理器 104 | await test_all_sources() 105 | 106 | print("\n" + "="*60) 107 | print("测试完成!") 108 | print("="*60) 109 | 110 | 111 | if __name__ == "__main__": 112 | asyncio.run(main()) 113 | 114 | -------------------------------------------------------------------------------- /app/services/crawler_service.py: -------------------------------------------------------------------------------- 1 | """抓取服务模块""" 2 | 3 | import random 4 | from typing import List, Dict 5 | 6 | from loguru import logger 7 | 8 | from ..config_loader import load_crawler_keywords 9 | from ..infrastructure.crawlers.sogou_wechat import search_articles_by_keyword 10 | from ..domain.sources.ai_articles import get_all_articles, save_article_to_config 11 | 12 | 13 | class CrawlerService: 14 | """抓取服务""" 15 | 16 | async def crawl_and_pick_articles_by_keywords(self) -> int: 17 | """ 18 | 按关键字抓取文章,每个关键字随机选一篇,直接放到文章列表。 19 | 20 | Returns: 21 | 成功添加到文章列表的文章数量 22 | """ 23 | try: 24 | # 1. 读取关键词 25 | keywords = load_crawler_keywords() 26 | if not keywords: 27 | logger.warning("[自动抓取] 关键词列表为空,无法抓取文章") 28 | return 0 29 | 30 | logger.info(f"[自动抓取] 开始按关键字抓取文章,关键词数量: {len(keywords)}") 31 | 32 | # 2. 获取所有已存在的 URL 用于去重 33 | existing_urls = set() 34 | main_pool_articles = get_all_articles() 35 | for article in main_pool_articles: 36 | if article.get("url"): 37 | existing_urls.add(article["url"].strip()) 38 | 39 | logger.info(f"[自动抓取] 已存在 {len(existing_urls)} 篇文章,用于去重") 40 | 41 | # 3. 遍历关键词并抓取,每个关键词随机选一篇 42 | selected_articles = [] 43 | for keyword in keywords: 44 | try: 45 | logger.info(f"[自动抓取] 正在抓取关键词 '{keyword}' 的文章...") 46 | found_candidates = await search_articles_by_keyword(keyword, pages=1) 47 | 48 | if not found_candidates: 49 | logger.warning(f"[自动抓取] 关键词 '{keyword}' 未找到文章") 50 | continue 51 | 52 | # 过滤掉已存在的URL 53 | new_candidates = [ 54 | c for c in found_candidates 55 | if c.url.strip() not in existing_urls 56 | ] 57 | 58 | if not new_candidates: 59 | logger.info(f"[自动抓取] 关键词 '{keyword}' 的文章都已存在,跳过") 60 | continue 61 | 62 | # 随机选择一篇 63 | selected = random.choice(new_candidates) 64 | selected_articles.append({ 65 | "title": selected.title, 66 | "url": selected.url, 67 | "source": selected.source, 68 | "summary": selected.summary, 69 | }) 70 | 71 | # 添加到已存在URL集合,避免同一批次重复 72 | existing_urls.add(selected.url.strip()) 73 | 74 | logger.info(f"[自动抓取] 关键词 '{keyword}' 已选择文章: {selected.title[:50]}...") 75 | 76 | except Exception as e: 77 | logger.error(f"[自动抓取] 抓取关键词 '{keyword}' 失败: {e}") 78 | # 单个关键词失败不中断整个任务 79 | continue 80 | 81 | if not selected_articles: 82 | logger.warning("[自动抓取] 未找到新文章") 83 | return 0 84 | 85 | # 4. 直接保存到文章列表 86 | saved_count = 0 87 | for article in selected_articles: 88 | if save_article_to_config(article): 89 | saved_count += 1 90 | 91 | logger.info(f"[自动抓取] 成功抓取并保存 {saved_count} 篇文章到文章列表") 92 | return saved_count 93 | 94 | except Exception as e: 95 | logger.error(f"[自动抓取] 抓取文章失败: {e}", exc_info=True) 96 | return 0 97 | 98 | -------------------------------------------------------------------------------- /app/infrastructure/file_lock.py: -------------------------------------------------------------------------------- 1 | """文件锁模块,用于跨进程锁""" 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | if sys.platform == "win32": 9 | import msvcrt 10 | else: 11 | import fcntl 12 | 13 | from loguru import logger 14 | 15 | 16 | class FileLock: 17 | """跨进程文件锁""" 18 | 19 | def __init__(self, lock_name: str = "digest_job.lock"): 20 | """ 21 | 初始化文件锁 22 | 23 | Args: 24 | lock_name: 锁文件名 25 | """ 26 | self.lock_name = lock_name 27 | self._lock_file_path: Optional[Path] = None 28 | self._lock_fd: Optional[int] = None 29 | 30 | def _get_lock_file_path(self) -> Path: 31 | """获取文件锁路径""" 32 | if self._lock_file_path is None: 33 | project_root = Path(__file__).resolve().parent.parent.parent 34 | lock_dir = project_root / "data" / ".locks" 35 | lock_dir.mkdir(parents=True, exist_ok=True) 36 | self._lock_file_path = lock_dir / self.lock_name 37 | return self._lock_file_path 38 | 39 | def acquire(self, timeout: float = 0.1) -> bool: 40 | """ 41 | 尝试获取文件锁(跨进程锁) 42 | 43 | Args: 44 | timeout: 超时时间(未使用,保持接口一致性) 45 | 46 | Returns: 47 | True 如果成功获取锁,False 如果锁已被其他进程占用 48 | """ 49 | lock_file = self._get_lock_file_path() 50 | try: 51 | # 尝试以独占模式打开文件 52 | if sys.platform == "win32": 53 | # Windows 使用 msvcrt 54 | self._lock_fd = os.open(str(lock_file), os.O_CREAT | os.O_WRONLY | os.O_TRUNC) 55 | try: 56 | msvcrt.locking(self._lock_fd, msvcrt.LK_NBLCK, 1) # 非阻塞锁定 57 | return True 58 | except IOError: 59 | os.close(self._lock_fd) 60 | self._lock_fd = None 61 | return False 62 | else: 63 | # Linux/Mac 使用 fcntl 64 | self._lock_fd = os.open(str(lock_file), os.O_CREAT | os.O_WRONLY | os.O_TRUNC) 65 | try: 66 | fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 67 | return True 68 | except IOError: 69 | os.close(self._lock_fd) 70 | self._lock_fd = None 71 | return False 72 | except Exception as e: 73 | logger.warning(f"[定时推送] 获取文件锁失败: {e}") 74 | if self._lock_fd is not None: 75 | try: 76 | os.close(self._lock_fd) 77 | except Exception: 78 | pass 79 | self._lock_fd = None 80 | return False 81 | 82 | def release(self): 83 | """释放文件锁""" 84 | try: 85 | if self._lock_fd is not None: 86 | if sys.platform == "win32": 87 | msvcrt.locking(self._lock_fd, msvcrt.LK_UNLCK, 1) 88 | else: 89 | fcntl.flock(self._lock_fd, fcntl.LOCK_UN) 90 | os.close(self._lock_fd) 91 | self._lock_fd = None 92 | 93 | # 删除锁文件 94 | lock_file = self._get_lock_file_path() 95 | if lock_file.exists(): 96 | lock_file.unlink() 97 | except Exception as e: 98 | logger.warning(f"[定时推送] 释放文件锁失败: {e}") 99 | self._lock_fd = None 100 | 101 | def __enter__(self): 102 | """上下文管理器入口""" 103 | if not self.acquire(): 104 | raise RuntimeError("无法获取文件锁") 105 | return self 106 | 107 | def __exit__(self, exc_type, exc_val, exc_tb): 108 | """上下文管理器出口""" 109 | self.release() 110 | 111 | -------------------------------------------------------------------------------- /data/weekly/2025weekly48.md: -------------------------------------------------------------------------------- 1 | # 第48周资讯推荐 2 | 3 | 时间范围:2025年11月24日 - 2025年11月30日 4 | 5 | --- 6 | 7 | ## 🤖 AI资讯 8 | 9 | 1. 提效 50%!AI 时代 ToB 产品设计 4 大步骤:用意图原型解决需求错位难题 10 | 在企业级产品设计中,长期存在一个突出问题:高保真原型虽具视觉吸引力,但其底层的概念模型(如实体对象、关系和操作)以及用户流程往往薄弱。本文提出一种 AI 驱动的 ToB 产品意图原型设计方法论,旨在使... 11 | 来源:AI 博客精选 12 | 链接:https://mp.weixin.qq.com/s/DwM19g8e6e9WV4LzLIOC6g 13 | 14 | 2. 李飞飞:不要让AI把你变愚蠢,必须守住“人”的主导权 15 | 在AI时代,主导权必须牢牢掌握在人类手中 16 | 来源:微信公众平台 17 | 链接:https://mp.weixin.qq.com/s/1eH5_aVuevCK4jDBRSILXw 18 | 19 | 3. TRAE SOLO中国版终于来了,完全免费! 20 | 暂无摘要 21 | 来源:苍何 22 | 链接:https://mp.weixin.qq.com/s/AwT_NOL-B555dN8CIcnQbQ 23 | 24 | 4. 超全Nano Banana Pro 提示词案例库来啦,小白也能轻松上手 25 | Nano Banana Pro 提示词案例库快收藏!! 26 | 来源:苍何 27 | 链接:https://mp.weixin.qq.com/s/jsmsUOqNJh0u0ygRIKpOSA 28 | 29 | 5. 今年最好的国产 AI 浏览器,出现了 30 | 暂无摘要 31 | 来源:阿颖 32 | 链接:https://mp.weixin.qq.com/s/h3m_DPyfkk2c4J9CR4x0cA 33 | 34 | 6. 将设计转化为代码:Figma Make 与 Anima Playground 大比拼 35 | 暂无摘要 36 | 来源:Nick益 37 | 链接:https://mp.weixin.qq.com/s/4WYbeI1iRkzAv89K0XCeQA 38 | 39 | 7. 实测用 Claude Code 写小说,意外发现了组队开挂模式 40 | 暂无摘要 41 | 来源:丸美小沐 42 | 链接:https://mp.weixin.qq.com/s/lInXMvih3lAiqJDhbTUxxQ 43 | 44 | 8. 查资料、劝老板、写周报,给上班人准备的大模型评测 45 | 晚点测评 14 款大模型。 46 | 来源:晚点团队 47 | 链接:https://mp.weixin.qq.com/s/qvXJh-L-nytSH3qr1csUow 48 | 49 | 9. 灵光vs秒哒:3个案例实测,究竟谁更能打? 50 | 做应用这件事,真的越来越容易了。 51 | 来源:一只卷 52 | 链接:https://mp.weixin.qq.com/s/A1yokMv458LjwmacIFHCKQ 53 | 54 | 10. Gemini 3 Pro 案例聚合 55 | 暂无摘要 56 | 来源:xgemini3.millionweekend.com 57 | 链接:https://xgemini3.millionweekend.com/ 58 | 59 | 11. Stack Overflow CEO:因为 AI,我重新开始写代码了 60 | 最好、最新的内容,总来自赛博禅心 61 | 来源:金色传说大聪明 62 | 链接:https://mp.weixin.qq.com/s/odqUTzsGuf7_C3j6kACqzg 63 | 64 | 65 | --- 66 | 67 | ## 💻 编程资讯 68 | 69 | 1. 终极指南:100+ 最佳免费AI编程助手与平台(2025年11月) - DEV Community 70 | 暂无摘要 71 | 来源:100kWhy 72 | 链接:https://mp.weixin.qq.com/s/EL32KxVRIXFHy_tO9hIihg 73 | 74 | 2. AI写70%,剩下30%难得要命?Google工程师直言:代码审查已成“最大瓶颈” 75 | 写代码的人越来越轻松,审代码的人越来越痛苦。 76 | 来源:微信公众平台 77 | 链接:https://mp.weixin.qq.com/s/JRSjbfTduWAYl1ig2uGpUw 78 | 79 | 3. 10000个代码文件,我打几把游戏的功夫就搞成Wiki了! 80 | 实测新升级的Qoder 81 | 来源:关注前沿科技 82 | 链接:https://mp.weixin.qq.com/s/AV8Ej2HuViL-Wgk_fz7HLw 83 | 84 | 4. 在Antigravity里,让Claude Opus 4.5与Gemini 3 Pro左右互搏 85 | AI模型要用就用最强的。 86 | 来源:鲁工 87 | 链接:https://mp.weixin.qq.com/s/WRiobaah-MG4OVoYfJ9DPw 88 | 89 | 5. 从代码补全到真实软件的生产级工具:Qoder 如何改写 AI 编程规则 90 | ​AI coding 80% 的价值都藏在对已有工程的迭代中 91 | 来源:Cynthia 92 | 链接:https://mp.weixin.qq.com/s/0Emq362XrjjFPxb9S8rYeg 93 | 94 | 6. 驾驭AI:一位全流程工程师的软件工程智能化实践与思考 95 | 衡量一个开发者价值的,将不再是他/她写了多少行代码,而是他/她通过与AI协作,交付了多少高质量的业务价值。——AI时代的行业新共识 96 | 来源:梁山河 97 | 链接:https://mp.weixin.qq.com/s/zrfuWfKaF3bkBxUZw5WELg 98 | 99 | 7. Coding Agent 的真相:90%的时间都在给AI做Code Review 100 | 决定写什么的,还是你自己 101 | 来源:37Flow 102 | 链接:https://mp.weixin.qq.com/s/ufQDHs-rNkTg9bWBfGnRNg 103 | 104 | 8. 使用 Qoder 2 个月,我总结了一些经验 105 | Qoder 使用经验分享 106 | 来源:kiritomoe 107 | 链接:https://mp.weixin.qq.com/s/jFQWPEq-VUgttHHidhyJoQ 108 | 109 | 9. Gemini 3 Pro + V0 才是前端终局!别让 Claude 和 GPT 去干它们不擅长的“装修活” 110 | 本文基于我最近的高强度开发体验,拒绝参数罗列,只讲实战心法。为你深度解析一套“极致性价比”的模型搭配策略:手把手教你用 GPT-5.1 搞规划、Claude 4.5 也就是主力、Gemini 做“花瓶... 111 | 来源:春秋1 112 | 链接:https://mp.weixin.qq.com/s/P-Zl_xvINzy1fWP4R77VzQ 113 | 114 | 10. 在团队里如何使用AI编程?Spec coding实践 115 | Spec Coding 的本质不是工具,而是流程变革. 它需要配合Git 工作流的改造、Code Review 标准的升级才能真正落地.工具免费,但... 116 | 来源:100kwhy 117 | 链接:https://mp.weixin.qq.com/s?src=11×tamp=1764472915&ver=6389&signature=fkfISBSa8NvnKIsy*9eUCPcClnyrju0rWCSDBAs7wx8u4osseDCg*OUKgZ9VYIbshyMXos6pzzGIsLDDsizgyAK9jAjHdiDmHuAKVoyyiUG9m03Stqc56tri-FIktH*k&new=1 118 | 119 | 120 | --- 121 | 122 | 统计信息: 123 | 本周共推荐 21 篇优质资讯 124 | - AI资讯:11 篇 125 | - 编程资讯:10 篇 126 | 127 | --- 128 | 本报告由 [AI-CodeNexus](https://aicoding.100kwhy.fun) 自动生成 129 | -------------------------------------------------------------------------------- /docs/feature/v3.0_update_summary.md: -------------------------------------------------------------------------------- 1 | # v3.0 版本更新说明 2 | 3 | ## 更新概述 4 | 5 | v3.0 版本主要完成了工具相关资讯获取功能的开发,以及工具数据的录入和整理工作。 6 | 7 | ## 主要功能更新 8 | 9 | ### 1. 工具相关资讯获取功能 10 | 11 | #### 功能描述 12 | 为每个工具提供手动触发相关资讯获取的能力,获取到的资讯会自动关联到对应工具,并在工具详情页展示。 13 | 14 | #### 核心特性 15 | - **工具关键字自动管理** 16 | - 工具被采纳时,自动将工具名称添加到关键字配置 17 | - 配置文件:`config/tool_keywords.json` 18 | - 自动去重,避免重复添加 19 | 20 | - **手动触发获取** 21 | - 支持单个工具获取:选择特定工具关键字,每次获取1篇当天的相关资讯 22 | - 支持批量获取:一次性获取所有工具的相关资讯 23 | - 只获取当天的资讯内容,确保内容时效性 24 | 25 | - **自动标签关联** 26 | - 获取到的资讯自动带有工具名称标签 27 | - 归档或采纳资讯时,系统自动从获取来源中提取工具名称作为标签 28 | - 带有工具标签的资讯会在工具详情页自动展示 29 | 30 | - **资讯管理** 31 | - 获取到的资讯首先进入候选池,等待管理员审核 32 | - 支持采纳、归档、忽略操作 33 | - 归档时自动填充工具标签(如果是工具相关资讯) 34 | 35 | #### 技术实现 36 | 37 | **新增配置文件**: 38 | - `config/tool_keywords.json` - 工具关键字列表(自动管理) 39 | 40 | **新增API接口**: 41 | - `GET /digest/tool-keywords` - 获取工具关键字列表 42 | - `POST /digest/crawl-tool-articles` - 获取工具相关资讯 43 | - Body: `{"keyword": "Cursor"}` - 可选,不提供则获取所有工具 44 | 45 | **代码更新**: 46 | - `app/config_loader.py` - 新增工具关键字管理函数 47 | - `load_tool_keywords()` - 加载工具关键字 48 | - `save_tool_keywords()` - 保存工具关键字 49 | - `add_tool_keyword()` - 添加工具关键字 50 | 51 | - `app/routes/digest.py` - 新增API接口和管理界面 52 | - 工具采纳时自动添加关键字 53 | - 工具相关资讯获取接口 54 | - 管理员面板新增工具相关资讯获取区域 55 | 56 | - `app/sources/ai_articles.py` - 支持保存工具标签 57 | - `save_article_to_config()` - 支持保存 `tool_tags` 字段 58 | 59 | - `app/routes/digest.py` - 归档和采纳逻辑优化 60 | - 自动从 `crawled_from` 字段提取工具名称 61 | - 归档对话框自动填充工具标签 62 | 63 | ### 2. 工具数据录入和整理 64 | 65 | #### 完成工作 66 | - 完善工具分类体系 67 | - 录入112个工具数据 68 | - 清空 `other.json` 分类,为后续页面展示做准备 69 | - 初始化工具关键字配置,从所有现有工具中提取名称 70 | 71 | #### 数据文件 72 | - `data/tools/` - 各分类工具数据文件 73 | - `config/tool_keywords.json` - 工具关键字配置(112个工具名称) 74 | 75 | ## 文档更新 76 | 77 | ### 新增文档 78 | - `docs/feature/tool_article_crawling_feature.md` - 工具相关资讯获取功能详细说明 79 | 80 | ### 更新文档 81 | - `docs/feature/features_complete.md` - 添加工具相关资讯获取功能说明 82 | - `docs/README.md` - 更新文档目录,添加新文档链接 83 | 84 | ## 使用说明 85 | 86 | ### 管理员操作 87 | 88 | 1. **查看工具关键字** 89 | - 进入管理员面板 `/digest/panel` 90 | - 在"文章获取与候选池"部分查看"工具相关资讯获取"区域 91 | - 查看当前工具关键字数量 92 | 93 | 2. **获取单个工具资讯** 94 | - 从下拉列表选择工具关键字 95 | - 点击"获取该工具资讯"按钮 96 | - 系统会获取1篇当天的相关资讯并进入候选池 97 | 98 | 3. **批量获取所有工具资讯** 99 | - 点击"获取所有工具资讯"按钮 100 | - 系统会遍历所有工具关键字,每个关键字获取1篇资讯 101 | 102 | 4. **审核和管理资讯** 103 | - 在候选池中查看获取到的资讯 104 | - 归档时,工具标签会自动填充(如果是工具相关资讯) 105 | - 采纳或归档后,资讯会关联到对应工具 106 | 107 | ### 工具关键字管理 108 | 109 | - **自动添加**:工具被采纳时自动添加,无需手动操作 110 | - **手动更新**:如需批量更新,可以直接编辑 `config/tool_keywords.json` 111 | - **初始化**:系统启动时会从所有现有工具中提取名称并写入配置文件 112 | 113 | ## 注意事项 114 | 115 | 1. **获取频率**:建议不要过于频繁地获取,避免对数据源造成压力 116 | 2. **内容审核**:所有获取到的资讯都需要管理员审核后才能发布 117 | 3. **标签关联**:归档时请确认工具标签是否正确,可以手动修改 118 | 4. **当天内容**:系统只获取当天的资讯,确保内容时效性 119 | 120 | ## 技术细节 121 | 122 | ### 数据流程 123 | 124 | 1. **工具采纳** → 自动添加工具名称到关键字配置 125 | 2. **手动触发获取** → 使用工具关键字获取相关资讯 126 | 3. **资讯进入候选池** → 带有 `tool_keyword:工具名称` 标识 127 | 4. **归档/采纳** → 自动提取工具名称作为 `tool_tags` 128 | 5. **工具详情页** → 通过 `tool_tags` 匹配显示相关资讯 129 | 130 | ### 工具与资讯的关联机制 131 | 132 | - **工具标签** (`tool_tags`): 资讯归档或采纳时,会自动添加工具名称作为标签 133 | - **工具详情页**:通过匹配资讯的 `tool_tags` 和工具的 `name` 或 `identifier` 来显示相关资讯 134 | - **双向关联**:工具详情页可以查看相关资讯,资讯也可以关联多个工具 135 | 136 | ## 相关文件 137 | 138 | ### 配置文件 139 | - `config/tool_keywords.json` - 工具关键字列表 140 | 141 | ### 代码文件 142 | - `app/config_loader.py` - 工具关键字管理 143 | - `app/routes/digest.py` - API接口和管理界面 144 | - `app/sources/ai_articles.py` - 文章保存逻辑 145 | - `app/services/data_loader.py` - 数据加载和关联查询 146 | 147 | ### 文档文件 148 | - `docs/feature/tool_article_crawling_feature.md` - 功能详细说明 149 | - `docs/feature/features_complete.md` - 完整功能文档 150 | - `docs/README.md` - 文档目录 151 | 152 | ## 后续计划 153 | 154 | - [ ] 在页面上添加 "others" 分类展示 155 | - [ ] 优化工具关键字管理界面 156 | - [ ] 支持工具关键字的批量编辑和删除 157 | 158 | --- 159 | 160 | **更新日期**:2024年11月 161 | **版本**:v3.0 162 | 163 | -------------------------------------------------------------------------------- /app/domain/sources/article_sources.py: -------------------------------------------------------------------------------- 1 | """统一资讯源管理器""" 2 | import asyncio 3 | from datetime import datetime 4 | from typing import List, Dict, Any, Optional 5 | 6 | from loguru import logger 7 | 8 | from ...infrastructure.crawlers.rss import fetch_rss_articles 9 | from ...infrastructure.crawlers.github_trending import fetch_github_trending 10 | from ...infrastructure.crawlers.hackernews import fetch_hackernews_articles 11 | from ...infrastructure.crawlers.sogou_wechat import search_articles_by_keyword 12 | 13 | 14 | async def fetch_from_all_sources( 15 | keywords: List[str], 16 | rss_feeds: Optional[List[str]] = None, 17 | github_languages: Optional[List[str]] = None, 18 | hackernews_min_points: int = 100, 19 | max_per_source: int = 5, 20 | ) -> List[Dict[str, Any]]: 21 | """ 22 | 从所有配置的资讯源抓取文章 23 | 24 | Args: 25 | keywords: 关键词列表(用于搜狗微信搜索) 26 | rss_feeds: RSS Feed URL 列表 27 | github_languages: GitHub Trending 语言列表 28 | hackernews_min_points: Hacker News 最低分数 29 | max_per_source: 每个源最多抓取的文章数 30 | 31 | Returns: 32 | 所有抓取到的文章列表 33 | """ 34 | all_articles = [] 35 | 36 | # 1. 搜狗微信搜索(关键词) 37 | if keywords: 38 | try: 39 | for keyword in keywords: 40 | articles = await search_articles_by_keyword(keyword, max_pages=1) 41 | all_articles.extend(articles[:max_per_source]) 42 | await asyncio.sleep(1) # 避免请求过快 43 | except Exception as e: 44 | logger.error(f"搜狗微信搜索失败: {e}") 45 | 46 | # 2. RSS Feeds 47 | if rss_feeds: 48 | tasks = [fetch_rss_articles(feed, max_per_source) for feed in rss_feeds] 49 | results = await asyncio.gather(*tasks, return_exceptions=True) 50 | for result in results: 51 | if isinstance(result, list): 52 | all_articles.extend(result) 53 | 54 | # 3. GitHub Trending 55 | if github_languages: 56 | tasks = [fetch_github_trending(lang, max_per_source) for lang in github_languages] 57 | results = await asyncio.gather(*tasks, return_exceptions=True) 58 | for result in results: 59 | if isinstance(result, list): 60 | all_articles.extend(result) 61 | 62 | # 4. Hacker News 63 | try: 64 | hn_articles = await fetch_hackernews_articles(hackernews_min_points, max_per_source * 2) 65 | all_articles.extend(hn_articles) 66 | except Exception as e: 67 | logger.error(f"Hacker News 抓取失败: {e}") 68 | 69 | # 计算热度分 70 | for article in all_articles: 71 | article["score"] = _calculate_article_score(article) 72 | 73 | # 按热度分排序 74 | all_articles.sort(key=lambda x: x.get("score", 0), reverse=True) 75 | 76 | logger.info(f"从所有资讯源共抓取到 {len(all_articles)} 篇文章") 77 | return all_articles 78 | 79 | 80 | def _calculate_article_score(article: Dict[str, Any]) -> float: 81 | """ 82 | 计算文章热度分 83 | 84 | 考虑因素: 85 | - 来源权重 86 | - 时效性(今天发布的文章得分更高) 87 | - Hacker News points(如果有) 88 | - 标题长度(适中长度得分更高) 89 | """ 90 | score = 0.0 91 | 92 | # 来源权重 93 | source = article.get("source", "").lower() 94 | if "hacker news" in source: 95 | score += article.get("points", 0) * 0.1 # Hacker News 分数加权 96 | elif "github" in source: 97 | score += 50 # GitHub Trending 基础分 98 | elif "rss" in source or "feed" in source: 99 | score += 30 # RSS Feed 基础分 100 | else: 101 | score += 20 # 其他来源基础分 102 | 103 | # 时效性:今天发布的文章额外加分 104 | if article.get("published_time"): 105 | try: 106 | pub_time = datetime.fromisoformat(article["published_time"]) 107 | if pub_time.date() == datetime.now().date(): 108 | score += 30 109 | except: 110 | pass 111 | 112 | # 标题长度:适中长度(20-60字符)得分更高 113 | title_len = len(article.get("title", "")) 114 | if 20 <= title_len <= 60: 115 | score += 10 116 | elif title_len > 0: 117 | score += 5 118 | 119 | # 有摘要的文章得分更高 120 | if article.get("summary"): 121 | score += 5 122 | 123 | return score 124 | 125 | -------------------------------------------------------------------------------- /app/infrastructure/notifiers/wecom.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Iterable, List 3 | 4 | import httpx 5 | from loguru import logger 6 | 7 | from ...config_loader import load_wecom_template 8 | 9 | WECOM_WEBHOOK = os.getenv("WECOM_WEBHOOK", "") 10 | 11 | 12 | async def send_markdown_to_wecom(content: str) -> bool: 13 | """ 14 | Send a markdown message to Enterprise WeChat group via robot webhook. 15 | 16 | Docs (CN): https://developer.work.weixin.qq.com/document/path/91770 17 | 18 | Returns: 19 | bool: 是否发送成功 20 | """ 21 | if not WECOM_WEBHOOK: 22 | logger.warning("WECOM_WEBHOOK not set, skip sending message.") 23 | return False 24 | 25 | payload = {"msgtype": "markdown", "markdown": {"content": content}} 26 | 27 | try: 28 | async with httpx.AsyncClient(timeout=10) as client: 29 | resp = await client.post(WECOM_WEBHOOK, json=payload) 30 | try: 31 | data = resp.json() 32 | except Exception as exc: # noqa: BLE001 33 | logger.error(f"Failed to parse WeCom response: {exc}, response text: {resp.text[:200]}") 34 | return False 35 | 36 | if data.get("errcode") != 0: 37 | logger.error(f"WeCom robot send failed: {data}") 38 | return False 39 | else: 40 | logger.info("WeCom robot message sent successfully.") 41 | return True 42 | except Exception as exc: # noqa: BLE001 43 | logger.error(f"Failed to send message to WeCom: {exc}") 44 | return False 45 | 46 | 47 | def build_wecom_digest_markdown( 48 | date_str: str, 49 | theme: str, 50 | items: Iterable[dict], 51 | ) -> str: 52 | """ 53 | Build a markdown message tailored for WeCom group. 54 | 55 | `items` is an iterable of dicts with keys: 56 | - title 57 | - url 58 | - source 59 | - summary (optional) 60 | """ 61 | template = load_wecom_template() 62 | 63 | def _format(fmt: str, **kwargs: str) -> str: 64 | try: 65 | return fmt.format(**kwargs) 66 | except Exception as exc: # noqa: BLE001 67 | logger.warning(f"Invalid WeCom template expression {fmt!r}: {exc}") 68 | return fmt 69 | 70 | lines: List[str] = [] 71 | title_fmt = template.get("title", "**AI 编程优质文章推荐|{date}**") 72 | if title_fmt: 73 | lines.append(_format(title_fmt, date=date_str, theme=theme)) 74 | lines.append("") 75 | 76 | theme_fmt = template.get("theme", "> 今日主题:{theme}") 77 | if theme_fmt: 78 | lines.append(_format(theme_fmt, date=date_str, theme=theme)) 79 | lines.append("") 80 | 81 | item_template = template.get("item", {}) if isinstance(template.get("item"), dict) else {} 82 | title_line = item_template.get("title", "{idx}. [{title}]({url})") 83 | source_line = item_template.get("source", " - 来源:{source}") 84 | summary_line = item_template.get("summary", " - 摘要:{summary}") 85 | extra_lines = item_template.get("extra", []) 86 | if not isinstance(extra_lines, list): 87 | extra_lines = [] 88 | 89 | for idx, item in enumerate(items, start=1): 90 | context = { 91 | "idx": idx, 92 | "title": item["title"], 93 | "url": item["url"], 94 | "source": item.get("source", ""), 95 | "summary": item.get("summary") or "", 96 | "theme": theme, 97 | "date": date_str, 98 | } 99 | 100 | if title_line: 101 | lines.append(_format(title_line, **context)) 102 | if context["source"] and source_line: 103 | lines.append(_format(source_line, **context)) 104 | if context["summary"] and summary_line: 105 | lines.append(_format(summary_line, **context)) 106 | for extra in extra_lines: 107 | lines.append(_format(extra, **context)) 108 | lines.append("") 109 | 110 | footer_fmt = template.get("footer", "> 更多关于 AI 编程的实践与思考,见:100kwhy.fun") 111 | if footer_fmt: 112 | lines.append(_format(footer_fmt, date=date_str, theme=theme)) 113 | 114 | return "\n".join(lines) 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/deploy/deploy_python.md: -------------------------------------------------------------------------------- 1 | # Python 环境一键部署指南 2 | 3 | 本指南将引导你如何在一个标准的 Linux 服务器上,通过 Python 环境(而非 Docker 或其他 PaaS 平台)部署 AICoding Daily 项目。 4 | 5 | ### 前提条件 6 | 7 | - 一台已安装 Python 3.10+ 和 `git` 的服务器。 8 | - 对 `systemd`(或你偏好的进程管理工具)有基本了解。 9 | - (可选但强烈推荐)一个域名,用于配置 Nginx 反向代理。 10 | 11 | --- 12 | 13 | ### 步骤 1:克隆仓库与安装依赖 14 | 15 | 首先,将项目代码克隆到你的服务器上,例如 `/var/www/aicoding` 目录: 16 | 17 | ```bash 18 | sudo mkdir -p /var/www 19 | sudo chown $USER:$USER /var/www 20 | 21 | git clone https://github.com/your-name/100kwhy_wechat_mp.git /var/www/aicoding 22 | cd /var/www/aicoding 23 | ``` 24 | 25 | 创建 Python 虚拟环境并安装所有依赖: 26 | 27 | ```bash 28 | python3 -m venv venv 29 | source venv/bin/activate 30 | 31 | pip install --upgrade pip 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | 最后,安装 Playwright 所需的浏览器内核(通常是 Chromium): 36 | 37 | ```bash 38 | playwright install 39 | ``` 40 | 41 | --- 42 | 43 | ### 步骤 2:配置环境变量 44 | 45 | 在项目根目录 `/var/www/aicoding` 下创建一个 `.env` 文件,用于存放敏感信息: 46 | 47 | ```bash 48 | cat > .env <<'EOF' 49 | # 必填:你的企业微信群机器人 Webhook URL 50 | WECOM_WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" 51 | 52 | # 必填:用于保护管理面板的授权码 53 | AICODING_ADMIN_CODE="a-very-strong-and-secret-password" 54 | EOF 55 | ``` 56 | 57 | > **重要**:请务必将 `AICODING_ADMIN_CODE` 设置为一个强密码,以防未授权访问。 58 | 59 | --- 60 | 61 | ### 步骤 3:配置应用 62 | 63 | 根据你的需求,调整 `config/` 目录下的配置文件: 64 | 65 | 1. **推送策略 (`config/digest_schedule.json`)** 66 | 67 | ```json 68 | { 69 | "cron": "0 14 * * *", 70 | "count": 5, 71 | "max_articles_per_keyword": 3 72 | } 73 | ``` 74 | - `cron`:每日推送时间,格式为标准的 5 段 cron 表达式。 75 | - `count`:每次推送的文章数量。 76 | - `max_articles_per_keyword`:每个关键词最多抓取并保留的文章数。 77 | 78 | 2. **抓取关键词 (`config/crawler_keywords.json`)** 79 | 80 | ```json 81 | [ 82 | "AI 编程", 83 | "Cursor.sh", 84 | "Devin", 85 | "代码大模型" 86 | ] 87 | ``` 88 | - 在此列表中添加或修改你关心的公众号文章关键词。 89 | 90 | --- 91 | 92 | ### 步骤 4:使用 systemd 运行应用 93 | 94 | 为了让应用能在后台稳定运行并实现开机自启,我们使用 `systemd` 来管理它。 95 | 96 | 创建 systemd 服务文件: 97 | 98 | ```bash 99 | sudo nano /etc/systemd/system/aicoding.service 100 | ``` 101 | 102 | 将以下内容粘贴进去,并根据你的实际情况修改 `User` 和 `Group`(通常是 `www-data` 或你的用户名): 103 | 104 | ```ini 105 | [Unit] 106 | Description=AICoding Daily Backend Service 107 | After=network.target 108 | 109 | [Service] 110 | # 运行服务的用户和组 111 | User=www-data 112 | Group=www-data 113 | 114 | # 项目根目录 115 | WorkingDirectory=/var/www/aicoding 116 | 117 | # 环境变量文件路径 118 | EnvironmentFile=/var/www/aicoding/.env 119 | 120 | # 启动命令:使用虚拟环境中的 uvicorn 121 | ExecStart=/var/www/aicoding/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 122 | 123 | # 失败后自动重启 124 | Restart=always 125 | RestartSec=3 126 | 127 | [Install] 128 | WantedBy=multi-user.target 129 | ``` 130 | 131 | 保存并退出后,启动并设置开机自启: 132 | 133 | ```bash 134 | sudo systemctl daemon-reload 135 | sudo systemctl start aicoding.service 136 | sudo systemctl enable aicoding.service 137 | ``` 138 | 139 | 检查服务状态,确保它正在运行: 140 | 141 | ```bash 142 | sudo systemctl status aicoding.service 143 | ``` 144 | 145 | --- 146 | 147 | ### 步骤 5(可选):配置 Nginx 反向代理 148 | 149 | 直接暴露 8000 端口不安全且不方便。推荐使用 Nginx 作为反向代理,并通过域名访问。 150 | 151 | 安装 Nginx(如果尚未安装): 152 | 153 | ```bash 154 | sudo apt update 155 | sudo apt install nginx 156 | ``` 157 | 158 | 创建一个新的 Nginx 站点配置: 159 | 160 | ```bash 161 | sudo nano /etc/nginx/sites-available/aicoding 162 | ``` 163 | 164 | 粘贴以下配置,并将 `your-domain.com` 替换为你的域名: 165 | 166 | ```nginx 167 | server { 168 | listen 80; 169 | server_name your-domain.com; 170 | 171 | location / { 172 | proxy_pass http://127.0.0.1:8000; 173 | proxy_set_header Host $host; 174 | proxy_set_header X-Real-IP $remote_addr; 175 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 176 | proxy_set_header X-Forwarded-Proto $scheme; 177 | } 178 | } 179 | ``` 180 | 181 | 启用该站点并测试配置: 182 | 183 | ```bash 184 | sudo ln -s /etc/nginx/sites-available/aicoding /etc/nginx/sites-enabled/ 185 | sudo nginx -t 186 | ``` 187 | 188 | 如果测试通过,重启 Nginx 使配置生效: 189 | 190 | ```bash 191 | sudo systemctl restart nginx 192 | ``` 193 | 194 | 现在,你可以通过 `http://your-domain.com/digest/panel` 访问你的管理面板了。 195 | 196 | > **提示**:为了安全,建议后续为你的域名配置 SSL 证书(HTTPS),可以使用 Let's Encrypt 免费实现。 197 | 198 | -------------------------------------------------------------------------------- /data/weekly/2025weekly49.md: -------------------------------------------------------------------------------- 1 | # 第49周资讯推荐 2 | 3 | 时间范围:2025年12月01日 - 2025年12月07日 4 | 5 | --- 6 | 7 | ## 🤖 AI资讯 8 | 9 | 1. 斯坦福CS变天!最火新课竟教「不写一行代码」,学生挤爆了 10 | 暂无摘要 11 | 来源:新智元 12 | 链接:https://mp.weixin.qq.com/s/HJjhjvsVTTdbNwHyuVRzIA 13 | 14 | 2. 年轻人,开始用AI 「造工具」过日子了 15 | 真灵光啊! 16 | 来源:钟立磊 17 | 链接:https://mp.weixin.qq.com/s/NgGB2b9z3irakagx9H068g 18 | 19 | 3. 专访OpenAI研究员:AI 时代的终极生存技能,用AI学习AI! 20 | 暂无摘要 21 | 来源:Datawhale 22 | 链接:https://mp.weixin.qq.com/s/y2fgmLo4Uug3iJsJrn6SPw 23 | 24 | 25 | --- 26 | 27 | ## 💻 编程资讯 28 | 29 | 1. 2025 年 8 大最佳 AI 编码工具 30 | Bolt.new适合:基于浏览器的原型制作与试验.Bolt.new 完全在浏览器中运行,无需本地环境即可快速打样.关键特性:浏览器原生的... 31 | 来源:100kwhy 32 | 链接:https://mp.weixin.qq.com/s?src=11×tamp=1764551299&ver=6391&signature=bHsBvq-EO5jJ-KQ*oQii8maW5Q6EdNyQgWPAdhAOHwSpmgZyIP3*Jxh*ajTz1UN3YGvjL7YFJI4-rxqAui5Xn0Shdf5abAg*-TP07c*3J2MWFsFrx1Lc0ziI9FlPnSim&new=1 33 | 34 | 2. 建议收藏!15个提示词,复刻「马斯克」的顶级思维模型 35 | 暂无摘要 36 | 来源:微信公众平台 37 | 链接:https://mp.weixin.qq.com/s/NeMeqbnBjmrjznUjv9cP7g 38 | 39 | 3. AICoding实践:从Prd到代码生成 40 | 暂无摘要 41 | 来源:素舒 42 | 链接:https://mp.weixin.qq.com/s/msk1rLwcVsISfNJiTbZZag 43 | 44 | 4. 一开口就是“麦肯锡”范儿?教你用AI打造自己的专属方法论框架!(附Prompt) 45 | 这个Prompt,值得收藏! 46 | 来源:甲木Zuiyn 47 | 链接:https://mp.weixin.qq.com/s/2D9_aeqKT97Y0D6mGfXpbQ 48 | 49 | 5. 用 AI Native 的方式开发 AI Native 的产品 50 | Vibe Coding 的核心是重构整个软件开发生命周期的全流程。 51 | 来源:曹犟 52 | 链接:https://mp.weixin.qq.com/s/B2KywStpKHh5Vv5heGPrfw 53 | 54 | 6. Martin Fowler 深度访谈:AI 是软件工程 40 年来的最大变局 55 | 对于 AI,Martin Fowler 给出了一个极高的评价:这是他职业生涯中见过的最大变革!为何要警惕 Vibe Coding?重构为何更重要?本文深度解读从“确定性”到“非确定性”的思维重塑,带你... 56 | 来源:大圣不是圣 57 | 链接:https://mp.weixin.qq.com/s/pBhto12Y8qtNgqzvxhoRJA 58 | 59 | 7. AI for Coding:从 Vibe Coding 到规范驱动开发 60 | 软件开发的未来不再是编写更多的代码,而是指导 AI 编写更好的代码。 61 | 来源:自我书写 62 | 链接:https://mp.weixin.qq.com/s/HaazAEMGqH1GBTmn9qI73g 63 | 64 | 8. Claude用不好浪费钱?10个高级技巧让效率翻3倍 65 | 深度讲解Claude的Artifacts、Projects、Extended Thinking等10个高级功能。提供20+实战Prompt模板,帮你效率提升3倍。适合程序员、内容创作者、产品经理,从系... 66 | 来源:技术更好说 67 | 链接:https://mp.weixin.qq.com/s/JGmTFwHC990OUx7s66_egQ 68 | 69 | 9. SpecKit 在成熟 Java 项目中的 AI 编码实践 70 | 暂无摘要 71 | 来源:悟壳 72 | 链接:https://mp.weixin.qq.com/s/AZJknKuyRfvjQxWAMaFFlg 73 | 74 | 10. 产业之声 | 从编写代码到对话未来:生成式AI重塑软件的新范式 75 | 生成式AI正推动软件开发经历“范式级跃迁”:传统以人工编写、调试、迭代为核心的开发流程,正加速向“自然语言驱动、AI自主生成、人机协同优化”的新模式演进。 76 | 来源:袁媛 77 | 链接:https://mp.weixin.qq.com/s/G_3PrbzMZVZ7isNHSnJICg 78 | 79 | 11. SpecKit 在成熟 Java 项目中的 AI 编码实践 80 | 背景初次尝试 AI Code + SpecKit,过程中被其能力所震撼.本文将总结一些实践经验,与大家共同学习探讨.当前 vibe coding 正如火... 81 | 来源:100kwhy 82 | 链接:https://mp.weixin.qq.com/s?src=11×tamp=1764738834&ver=6395&signature=WTKeYiRhek5IQVc9wfPCZv-VPXZjF9EkZICB4Kd8wePBjZjtM2*c017YZ1FUzhNk0LPVQCrzD8ZhgB06jNmEgix0XOrtbjwgoRX043O8rLNbQMTKCmuNZd1h-H9D6EoE&new=1 83 | 84 | 12. Google 重磅新工具 Code Wiki:让AI写代码更规范,搭配 Vibe Coding 玩转 AI 编程新范式 85 | Google CodeWiki 是什么CodeWiki 是 Google 推出的一个基于 AI 的文档平台,用于自动生成、更新和维护代码仓库的文档 .它会扫描... 86 | 来源:100kwhy 87 | 链接:https://mp.weixin.qq.com/s?src=11×tamp=1764738704&ver=6395&signature=RdujWWUpHA-CeXPCvHBwPV15CrsISSC*XkhyC5prGT9AmjbjqznAfdoXP4VuC8-qTbaA0*GIydRpLmLqkCtisWWjU2MjTm1kL7qXkpqaSTWPmbVXmo7JZM1jWriz1KcH&new=1 88 | 89 | 13. Cursor Rules 四大原则:让AI代码生成更稳定高效 90 | Cursor Rules 在实际应用中可能遇到规则不生效、Token浪费、维护困难、输出不稳定和破坏项目风格等问题,本文提出MSEC四大原则:最小化原则、结构化原则、精准引用原则、一致性原则。通过这四... 91 | 来源:梁波Eric 92 | 链接:https://mp.weixin.qq.com/s/vEUAZIJrb5kEjWptIhsC-g 93 | 94 | 14. Vibe Coding 产品最大的错觉,是以为自己真的有护城河 95 | 8000 万「卖身」后,Base44 想得更清楚了。 96 | 来源:Founder Park 97 | 链接:https://mp.weixin.qq.com/s/CCVYQx9ad52RzXl2027-NA 98 | 99 | 15. 三端齐发,TalkCody 正式开源 100 | 今天,由我开发的 AI Coding Agent —— TalkCody 正式开源了,欢迎大家试用、反馈 101 | 来源:康凯森 102 | 链接:https://mp.weixin.qq.com/s/Tf69os9nXL_xJ5t8DVib1g 103 | 104 | 16. 如何调教一名合格的“编程搭子” 105 | 大家好,我是来自阿里云智能集团的王月成,今天我分享的主题是:如何调教一名合格的编程搭子。 106 | 来源:微信公众平台 107 | 链接:https://mp.weixin.qq.com/s/2UBz1kWFvFTT3vado4eFfg 108 | 109 | 17. 大模型到底能否真正提升写代码效率?Anthropic 内部 20 万条数据首次公开大模型在真实代码工作流中的表现 110 | Anthropic 发布了一篇很有意思的研究博客:132 名工程师与研究员、53 场深度访谈、20 万条 Claude Code 内部使用记录,试图回答在一家 AI 公司内部,当工程师人人有一个“AI... 111 | 来源:DataLearner 112 | 链接:https://mp.weixin.qq.com/s/p39y9XzYaJmoge1x3qQFKA 113 | 114 | 18. AI Coding与单元测试的协同进化:从验证到驱动 115 | 一线技术实践,三大策略破解AI编程痛点! 116 | 来源:业务研发平台 117 | 链接:https://mp.weixin.qq.com/s/vqqw60fwEfI3pjBgUc4QUg 118 | 119 | 120 | --- 121 | 122 | 统计信息: 123 | 本周共推荐 21 篇优质资讯 124 | - AI资讯:3 篇 125 | - 编程资讯:18 篇 126 | 127 | --- 128 | 本报告由 [AI-CodeNexus](https://aicoding.100kwhy.fun) 自动生成 129 | -------------------------------------------------------------------------------- /docs/feature/tool_detail_feature.md: -------------------------------------------------------------------------------- 1 | # 工具详情页和关联资讯功能 2 | 3 | ## 功能概述 4 | 5 | 实现了工具详情页面功能,每个工具都有独立的详情页,展示工具信息和关联的资讯文章。 6 | 7 | ## 核心特性 8 | 9 | ### 1. 工具标识符(Identifier) 10 | 11 | 每个工具都有一个隐藏的 `identifier` 字段,用于和资讯的 `tool_tags` 关联: 12 | 13 | - **字段名**:`identifier` 14 | - **生成规则**:基于工具名称,转换为小写,保留字母、数字、连字符和下划线 15 | - **用途**:在归档文章时,可以将工具的 `identifier` 添加到文章的 `tool_tags` 中,实现精确关联 16 | 17 | **示例**: 18 | ```json 19 | { 20 | "name": "keep", 21 | "identifier": "keep", 22 | ... 23 | } 24 | ``` 25 | 26 | ### 2. 工具详情页面 27 | 28 | **路由**:`/tool/{tool_id}` 29 | 30 | **功能**: 31 | - 显示工具完整信息(名称、图标、描述、分类、标签、访问量) 32 | - 显示"访问工具"按钮,点击后跳转到工具官网 33 | - 显示"相关资讯"部分,展示与该工具关联的文章 34 | - 支持刷新相关资讯 35 | - 支持返回上一页 36 | 37 | ### 3. 关联资讯匹配机制 38 | 39 | 系统通过以下方式匹配相关文章: 40 | 41 | 1. **优先使用 identifier**:如果文章的 `tool_tags` 中包含工具的 `identifier`,则匹配 42 | 2. **备用使用工具名称**:如果没有 identifier 或 identifier 未匹配,则使用工具名称进行模糊匹配 43 | 44 | **匹配逻辑**: 45 | ```python 46 | # 优先使用 identifier(精确匹配) 47 | if tool_identifier in article.tool_tags: 48 | matched = True 49 | 50 | # 如果没有 identifier 或未匹配,使用工具名称(模糊匹配) 51 | if not matched and tool_name: 52 | if tool_name in article.tool_tags or article.tags: 53 | matched = True 54 | ``` 55 | 56 | ### 4. API 接口 57 | 58 | #### 获取工具详情 59 | ``` 60 | GET /api/tools/{tool_id} 61 | ``` 62 | 63 | **响应**: 64 | ```json 65 | { 66 | "id": 1, 67 | "name": "keep", 68 | "url": "https://github.com/keephq/keep", 69 | "description": "...", 70 | "category": "devops", 71 | "tags": [], 72 | "icon": "🔧", 73 | "view_count": 10, 74 | "created_at": "2024-12-03T00:00:00Z", 75 | "is_featured": false, 76 | "identifier": "keep", 77 | "related_articles": [...], 78 | "related_articles_count": 5 79 | } 80 | ``` 81 | 82 | ## 使用方法 83 | 84 | ### 1. 为工具添加 identifier 85 | 86 | **手动添加**:为现有工具添加 identifier: 87 | ```bash 88 | python scripts/add_tool_identifiers.py 89 | ``` 90 | 91 | **注意**:identifier 会在添加工具数据时自动生成,无需手动设置。 92 | 93 | ### 2. 关联文章和工具 94 | 95 | 在管理员面板归档文章时,可以在"工具标签"字段中输入工具的 `identifier`,多个标签用逗号分隔。 96 | 97 | **示例**: 98 | - 工具:`keep` (identifier: `keep`) 99 | - 归档文章时,工具标签填写:`keep, devops` 100 | 101 | ### 3. 访问工具详情页 102 | 103 | - **从工具列表**:点击工具卡片,跳转到 `/tool/{tool_id}` 104 | - **直接访问**:在浏览器中输入 `/tool/{tool_id}` 105 | 106 | ## 前端实现 107 | 108 | ### 工具详情页布局 109 | 110 | ``` 111 | ┌─────────────────────────────────────┐ 112 | │ ← 返回分类 │ 113 | │ │ 114 | │ [图标] 工具名称 │ 115 | │ 分类 • 🔥 X 次访问 │ 116 | │ [访问工具] │ 117 | │ │ 118 | │ 工具描述 │ 119 | │ ... │ 120 | │ │ 121 | │ 标签 │ 122 | │ [tag1] [tag2] │ 123 | │ │ 124 | │ ─────────────────────────────── │ 125 | │ │ 126 | │ 📚 相关资讯 [刷新] [查看更多]│ 127 | │ │ 128 | │ [文章卡片1] │ 129 | │ [文章卡片2] │ 130 | │ ... │ 131 | └─────────────────────────────────────┘ 132 | ``` 133 | 134 | ### 相关资讯显示 135 | 136 | - 显示文章标题、来源、发布时间、摘要 137 | - 显示文章分类标签(编程资讯/AI资讯) 138 | - 显示工具标签(如果有) 139 | - 支持点击文章跳转到原文 140 | - 支持刷新获取最新关联文章 141 | 142 | ## 数据模型 143 | 144 | ### 工具数据模型 145 | ```json 146 | { 147 | "id": 1, 148 | "name": "工具名称", 149 | "url": "https://example.com/tool", 150 | "description": "工具描述", 151 | "category": "devops", 152 | "tags": ["标签1", "标签2"], 153 | "icon": "🔧", 154 | "view_count": 0, 155 | "created_at": "2024-12-03T00:00:00Z", 156 | "is_featured": false, 157 | "identifier": "tool-name" // 新增字段 158 | } 159 | ``` 160 | 161 | ### 文章数据模型(关联部分) 162 | ```json 163 | { 164 | "id": "article_123", 165 | "title": "文章标题", 166 | "url": "https://example.com/article", 167 | "tool_tags": ["keep", "devops"], // 工具标识符列表 168 | "tags": ["标签1", "标签2"], 169 | ... 170 | } 171 | ``` 172 | 173 | ## 注意事项 174 | 175 | 1. **identifier 唯一性**:每个工具的 identifier 应该是唯一的,建议使用工具名称的小写版本 176 | 2. **中文工具名称**:对于中文工具名称,identifier 可能包含中文字符,建议在归档文章时使用英文 identifier 177 | 3. **匹配优先级**:identifier 匹配优先于工具名称匹配 178 | 4. **工具标签格式**:归档文章时,工具标签应该使用工具的 identifier,而不是工具名称 179 | 180 | ## 相关文件 181 | 182 | - `app/services/data_loader.py` - 数据加载和关联查询逻辑 183 | - `app/routes/api.py` - 工具详情 API 接口 184 | - `app/main.py` - 前端工具详情页实现 185 | - `scripts/add_tool_identifiers.py` - 为工具添加 identifier 的脚本 186 | 187 | -------------------------------------------------------------------------------- /docs/feature/test_sources.md: -------------------------------------------------------------------------------- 1 | # 多资讯源功能测试指南 2 | 3 | ## 方法一:使用测试脚本(推荐) 4 | 5 | ### 1. 安装依赖 6 | 7 | 确保已安装所有依赖: 8 | 9 | ```bash 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ### 2. 运行测试脚本 14 | 15 | ```bash 16 | python scripts/test_sources.py 17 | ``` 18 | 19 | 测试脚本会依次测试: 20 | - RSS Feed 抓取 21 | - GitHub Trending 抓取 22 | - Hacker News 抓取 23 | - 统一资讯源管理器 24 | 25 | ### 3. 查看结果 26 | 27 | 脚本会输出每个资讯源的测试结果,包括: 28 | - 抓取到的文章数量 29 | - 文章标题、来源、链接等信息 30 | - 热度分排序结果 31 | 32 | ## 方法二:使用 API 接口测试 33 | 34 | ### 1. 启动服务 35 | 36 | ```bash 37 | uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload 38 | ``` 39 | 40 | ### 2. 访问 API 文档 41 | 42 | 打开浏览器访问:`http://localhost:8000/docs` 43 | 44 | ### 3. 测试各个资讯源 45 | 46 | #### 测试 RSS Feed 47 | 48 | ```bash 49 | curl -X POST "http://localhost:8000/digest/test/rss" \ 50 | -H "X-Admin-Code: your-admin-code" \ 51 | -H "Content-Type: application/json" \ 52 | -d '{"feed_url": "https://rss.cnn.com/rss/edition.rss"}' 53 | ``` 54 | 55 | #### 测试 GitHub Trending 56 | 57 | ```bash 58 | curl -X POST "http://localhost:8000/digest/test/github-trending" \ 59 | -H "X-Admin-Code: your-admin-code" \ 60 | -H "Content-Type: application/json" \ 61 | -d '{"language": "python"}' 62 | ``` 63 | 64 | #### 测试 Hacker News 65 | 66 | ```bash 67 | curl -X POST "http://localhost:8000/digest/test/hackernews" \ 68 | -H "X-Admin-Code: your-admin-code" \ 69 | -H "Content-Type: application/json" \ 70 | -d '{"min_points": 50}' 71 | ``` 72 | 73 | #### 测试所有资讯源 74 | 75 | ```bash 76 | curl -X POST "http://localhost:8000/digest/test/all-sources" \ 77 | -H "X-Admin-Code: your-admin-code" \ 78 | -H "Content-Type: application/json" \ 79 | -d '{ 80 | "keywords": ["AI"], 81 | "rss_feeds": ["https://rss.cnn.com/rss/edition.rss"], 82 | "github_languages": ["python"], 83 | "hackernews_min_points": 50, 84 | "max_per_source": 3 85 | }' 86 | ``` 87 | 88 | ## 方法三:使用 Python 代码测试 89 | 90 | ### 测试单个资讯源 91 | 92 | ```python 93 | import asyncio 94 | from app.crawlers.rss import fetch_rss_articles 95 | from app.crawlers.github_trending import fetch_github_trending 96 | from app.crawlers.hackernews import fetch_hackernews_articles 97 | 98 | async def test(): 99 | # 测试 RSS 100 | articles = await fetch_rss_articles("https://rss.cnn.com/rss/edition.rss", max_items=5) 101 | print(f"RSS: {len(articles)} 篇文章") 102 | 103 | # 测试 GitHub Trending 104 | articles = await fetch_github_trending("python", max_items=5) 105 | print(f"GitHub: {len(articles)} 个项目") 106 | 107 | # 测试 Hacker News 108 | articles = await fetch_hackernews_articles(min_points=50, max_items=5) 109 | print(f"Hacker News: {len(articles)} 篇文章") 110 | 111 | asyncio.run(test()) 112 | ``` 113 | 114 | ### 测试统一资讯源管理器 115 | 116 | ```python 117 | import asyncio 118 | from app.sources.article_sources import fetch_from_all_sources 119 | 120 | async def test(): 121 | articles = await fetch_from_all_sources( 122 | keywords=["AI"], 123 | rss_feeds=["https://rss.cnn.com/rss/edition.rss"], 124 | github_languages=["python"], 125 | hackernews_min_points=50, 126 | max_per_source=3, 127 | ) 128 | 129 | print(f"总共抓取到 {len(articles)} 篇文章") 130 | for article in articles[:10]: 131 | print(f"[{article.get('score', 0):.1f}分] {article.get('title')}") 132 | 133 | asyncio.run(test()) 134 | ``` 135 | 136 | ## 测试数据示例 137 | 138 | ### RSS Feed URL 示例 139 | 140 | - CNN: `https://rss.cnn.com/rss/edition.rss` 141 | - BBC News: `https://feeds.bbci.co.uk/news/rss.xml` 142 | - TechCrunch: `https://techcrunch.com/feed/` 143 | - Hacker News RSS: `https://hnrss.org/frontpage` 144 | 145 | ### GitHub Trending 语言示例 146 | 147 | - `python` 148 | - `javascript` 149 | - `go` 150 | - `rust` 151 | - `java` 152 | 153 | ### Hacker News 分数阈值 154 | 155 | - 低分:`50` points 156 | - 中分:`100` points 157 | - 高分:`200` points 158 | 159 | ## 常见问题 160 | 161 | ### 1. RSS Feed 抓取失败 162 | 163 | - 检查 Feed URL 是否可访问 164 | - 确认网络连接正常 165 | - 某些 Feed 可能需要特定的 User-Agent 166 | 167 | ### 2. GitHub Trending 抓取失败 168 | 169 | - GitHub 可能会限制频繁请求 170 | - 尝试降低请求频率 171 | - 检查网络连接 172 | 173 | ### 3. Hacker News 抓取失败 174 | 175 | - Hacker News API 是公开的,通常很稳定 176 | - 如果失败,检查网络连接 177 | - 可以降低 `min_points` 阈值获取更多文章 178 | 179 | ### 4. 热度分计算 180 | 181 | 热度分综合考虑: 182 | - 来源权重(Hacker News > GitHub > RSS > 其他) 183 | - 时效性(今天发布的文章加分) 184 | - 标题长度(适中长度加分) 185 | - 是否有摘要 186 | 187 | ## 集成到现有系统 188 | 189 | 要将多资讯源集成到现有的抓取流程中,可以修改 `app/main.py` 中的抓取逻辑,使用 `fetch_from_all_sources` 函数替代原来的单一来源抓取。 190 | 191 | -------------------------------------------------------------------------------- /data/tools/cli.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "warp", 4 | "url": "https://www.warp.dev/", 5 | "description": "Warp Dev 工具是一款面向开发者的现代终端,核心特点包括:采用 Rust 开发,性能高效;支持块状命令输出,便于阅读和交互;内置 AI 助手,可智能补全、生成命令和脚本;提供命令搜索、历史管理、快捷片段等功能;支持团队协作与共享,提高开发效率;界面现代化,兼顾命令行的灵活性与图形化的易用性。", 6 | "category": "cli", 7 | "tags": [ 8 | "hot" 9 | ], 10 | "icon": "🔧", 11 | "view_count": 3, 12 | "created_at": "2025-09-06T08:54:28.860000Z", 13 | "is_featured": false, 14 | "id": 1, 15 | "identifier": "warp" 16 | }, 17 | { 18 | "name": "qwen-code", 19 | "url": "https://github.com/QwenLM/qwen-code", 20 | "description": "Qwen Code 是阿里巴巴通义实验室基于 Qwen3-Coder 模型优化的 AI 编程工具,专为开发者设计的命令行(CLI)工作流工具,旨在通过 AI 能力提升代码开发全流程效率。", 21 | "category": "cli", 22 | "tags": [], 23 | "icon": "🔧", 24 | "view_count": 3, 25 | "created_at": "2025-07-25T00:00:00Z", 26 | "is_featured": false, 27 | "id": 2, 28 | "identifier": "qwen-code" 29 | }, 30 | { 31 | "name": "iflow", 32 | "url": "https://github.com/iflow-ai/iflow-cli", 33 | "description": "iFlow CLI 是阿里心流团队开发的终端级 AI 智能体,支持自然语言交互,可执行代码分析、文档生成、调试排错、文件管理等任务。它内置 Qwen3-Coder、Kimi K2 等免费模型,能理解项目上下文并调用本地或云端工具,实现从简单脚本到复杂 DevOps 流程的自动化", 34 | "category": "cli", 35 | "tags": [], 36 | "icon": "🔧", 37 | "view_count": 0, 38 | "created_at": "2025-08-31T00:00:00Z", 39 | "is_featured": false, 40 | "id": 3, 41 | "identifier": "iflow" 42 | }, 43 | { 44 | "name": "gemini-cli", 45 | "url": "https://github.com/google-gemini/gemini-cli", 46 | "description": "Gemini CLI 是谷歌推出的开源AI编程工具,基于Gemini 2.5 Pro多模态模型,支持自然语言交互,可直接在终端中实现代码生成、调试、文件操作和命令执行,免费且跨平台,极大提升开发者效率。", 47 | "category": "cli", 48 | "tags": [ 49 | "hot" 50 | ], 51 | "icon": "🔧", 52 | "view_count": 30, 53 | "created_at": "2025-06-30T00:00:00Z", 54 | "is_featured": false, 55 | "id": 4, 56 | "identifier": "gemini-cli" 57 | }, 58 | { 59 | "name": "crush", 60 | "url": "https://github.com/charmbracelet/crush", 61 | "description": "开源的终端开发工具", 62 | "category": "cli", 63 | "tags": [], 64 | "icon": "🔧", 65 | "view_count": 0, 66 | "created_at": "2025-06-30T00:00:00Z", 67 | "is_featured": false, 68 | "id": 5, 69 | "identifier": "crush" 70 | }, 71 | { 72 | "name": "copilot-cli", 73 | "url": "https://github.com/github/copilot-cli", 74 | "description": "github copilot 对应的终端开发工具,类似 claude-code", 75 | "category": "cli", 76 | "tags": [], 77 | "icon": "🔧", 78 | "view_count": 0, 79 | "created_at": "2025-10-15T00:00:00Z", 80 | "is_featured": false, 81 | "id": 6, 82 | "identifier": "copilot-cli" 83 | }, 84 | { 85 | "name": "Codex", 86 | "url": "https://openai.com/codex/", 87 | "description": "openAI 出品,Codex 可以并行处理多项任务,例如编写功能、回答代码库问题、运行测试以及提交 PR 以供审查。每项任务都在其自己的安全云沙箱中运行,并预加载了您的代码库", 88 | "category": "cli", 89 | "tags": [ 90 | "hot", 91 | "star" 92 | ], 93 | "icon": "🔧", 94 | "view_count": 0, 95 | "created_at": "2025-06-21T00:00:00Z", 96 | "is_featured": false, 97 | "id": 7, 98 | "identifier": "codex" 99 | }, 100 | { 101 | "name": "Claude-Code", 102 | "url": "https://www.anthropic.com/claude-code", 103 | "description": "Claude Code 是 Anthropic 推出的终端原生 AI 编程助手,能通过自然语言交互生成代码、调试修复,以代理式搜索深度感知项目结构,支持跨文件批量编辑与重构,无缝集成 Git、测试套件等开发工具,改动需用户确认,兼具企业级安全与可脚本化特性,适配多系统与 IDE。", 104 | "category": "cli", 105 | "tags": [ 106 | "hot", 107 | "star" 108 | ], 109 | "icon": "🔧", 110 | "view_count": 2, 111 | "created_at": "2025-06-21T00:00:00Z", 112 | "is_featured": false, 113 | "id": 8, 114 | "identifier": "claude-code" 115 | }, 116 | { 117 | "name": "Auto-coder", 118 | "url": "https://auto-coder.chat/", 119 | "description": "命令行 AI 编程工具,AI-powered interactive coding assistant", 120 | "category": "cli", 121 | "tags": [], 122 | "icon": "🔧", 123 | "view_count": 1, 124 | "created_at": "2024-12-01T00:00:00Z", 125 | "is_featured": false, 126 | "id": 9, 127 | "identifier": "auto-coder" 128 | }, 129 | { 130 | "name": "Aider", 131 | "url": "https://aider.chat/", 132 | "description": "终端内的 AI 编程助手,Aider is AI pair programming in your terminal", 133 | "category": "cli", 134 | "tags": [], 135 | "icon": "🔧", 136 | "view_count": 0, 137 | "created_at": "2024-12-01T00:00:00Z", 138 | "is_featured": false, 139 | "id": 10, 140 | "identifier": "aider" 141 | } 142 | ] -------------------------------------------------------------------------------- /app/services/digest_service.py: -------------------------------------------------------------------------------- 1 | """推送服务模块""" 2 | 3 | import asyncio 4 | from datetime import datetime 5 | from typing import List, Dict 6 | 7 | from loguru import logger 8 | 9 | from ..infrastructure.file_lock import FileLock 10 | from ..infrastructure.notifiers.wecom import build_wecom_digest_markdown, send_markdown_to_wecom 11 | from ..domain.sources.ai_articles import ( 12 | pick_daily_ai_articles, 13 | todays_theme, 14 | clear_articles, 15 | get_all_articles, 16 | save_article_to_config, 17 | ) 18 | from ..domain.sources.ai_candidates import promote_candidates_to_articles, clear_candidate_pool 19 | from .crawler_service import CrawlerService 20 | 21 | 22 | class DigestService: 23 | """推送服务""" 24 | 25 | def __init__(self): 26 | """初始化推送服务""" 27 | self._lock = asyncio.Lock() 28 | self._file_lock = FileLock() 29 | self._crawler_service = CrawlerService() 30 | 31 | async def send_daily_digest(self, digest_count: int) -> None: 32 | """ 33 | 发送每日推送 34 | 35 | Args: 36 | digest_count: 推送文章数量 37 | """ 38 | # 首先尝试获取文件锁(跨进程锁),防止多个进程同时执行 39 | if not self._file_lock.acquire(): 40 | logger.warning("[定时推送] 检测到其他进程正在执行推送任务,跳过本次执行以避免重复推送") 41 | return 42 | 43 | try: 44 | # 使用进程内锁防止同一进程内的并发执行 45 | if self._lock.locked(): 46 | logger.warning("[定时推送] 检测到任务正在执行中,跳过本次执行以避免重复推送") 47 | self._file_lock.release() 48 | return 49 | 50 | async with self._lock: 51 | now = datetime.now() 52 | logger.info( 53 | f"[定时推送] 开始执行定时推送任务,时间: {now.strftime('%Y-%m-%d %H:%M:%S')}, " 54 | f"目标篇数: {digest_count}" 55 | ) 56 | 57 | articles = pick_daily_ai_articles(k=digest_count) 58 | if not articles: 59 | logger.info("[定时推送] 文章池为空,尝试从候选池提升文章...") 60 | promoted = promote_candidates_to_articles(per_keyword=2) 61 | if promoted: 62 | logger.info(f"[定时推送] 从候选池提升了 {promoted} 篇文章") 63 | articles = pick_daily_ai_articles(k=digest_count) 64 | 65 | # 如果文章池和候选池都为空,按关键字抓取文章 66 | if not articles: 67 | logger.info("[定时推送] 文章池和候选池都为空,开始按关键字自动抓取文章...") 68 | crawled_count = await self._crawler_service.crawl_and_pick_articles_by_keywords() 69 | if crawled_count > 0: 70 | logger.info(f"[定时推送] 自动抓取成功,获得 {crawled_count} 篇文章") 71 | articles = pick_daily_ai_articles(k=digest_count) 72 | else: 73 | logger.warning("[定时推送] 自动抓取失败或未找到新文章,跳过推送") 74 | return 75 | 76 | if not articles: 77 | logger.warning("[定时推送] 文章池为空且无法获取文章,跳过推送") 78 | return 79 | 80 | logger.info(f"[定时推送] 准备推送 {len(articles)} 篇文章") 81 | theme = todays_theme(now) 82 | date_str = now.strftime("%Y-%m-%d") 83 | items = [ 84 | { 85 | "title": a.title, 86 | "url": a.url, 87 | "source": a.source, 88 | "summary": a.summary, 89 | } 90 | for a in articles 91 | ] 92 | 93 | content = build_wecom_digest_markdown(date_str=date_str, theme=theme, items=items) 94 | logger.info("[定时推送] 正在发送到企业微信群...") 95 | success = await send_markdown_to_wecom(content) 96 | if not success: 97 | logger.error("[定时推送] 推送失败,但继续清理文章池和候选池") 98 | else: 99 | logger.info("[定时推送] 推送成功") 100 | 101 | logger.info("[定时推送] 正在清理文章池和候选池...") 102 | clear_articles() 103 | clear_candidate_pool() 104 | if success: 105 | logger.info("[定时推送] 定时推送任务执行成功") 106 | else: 107 | logger.warning("[定时推送] 定时推送任务完成,但推送失败") 108 | except Exception as e: 109 | logger.error(f"[定时推送] 定时推送任务执行失败: {e}", exc_info=True) 110 | finally: 111 | # 确保释放文件锁 112 | self._file_lock.release() 113 | 114 | -------------------------------------------------------------------------------- /app/infrastructure/db/models.py: -------------------------------------------------------------------------------- 1 | """数据库模型定义""" 2 | 3 | from datetime import datetime 4 | from typing import Optional 5 | from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON, Index 6 | from sqlalchemy.ext.declarative import declarative_base 7 | 8 | Base = declarative_base() 9 | 10 | 11 | class Article(Base): 12 | """文章模型""" 13 | __tablename__ = "articles" 14 | 15 | id = Column(Integer, primary_key=True, index=True) 16 | title = Column(String(500), nullable=False, index=True) 17 | url = Column(String(1000), nullable=False, unique=True, index=True) 18 | summary = Column(Text, nullable=True) 19 | source = Column(String(200), nullable=True) 20 | category = Column(String(100), nullable=True, index=True) 21 | published_time = Column(String(100), nullable=True) 22 | created_at = Column(String(100), nullable=True) 23 | archived_at = Column(String(100), nullable=True, index=True) 24 | view_count = Column(Integer, default=0, index=True) 25 | score = Column(Integer, default=0) 26 | tags = Column(JSON, nullable=True) 27 | tool_tags = Column(JSON, nullable=True) 28 | extra_data = Column(JSON, nullable=True) # 存储其他额外字段 29 | created_at_db = Column(DateTime, default=datetime.now) 30 | updated_at_db = Column(DateTime, default=datetime.now, onupdate=datetime.now) 31 | 32 | __table_args__ = ( 33 | Index('idx_article_category_archived', 'category', 'archived_at'), 34 | Index('idx_article_view_count', 'view_count'), 35 | ) 36 | 37 | 38 | class Tool(Base): 39 | """工具模型""" 40 | __tablename__ = "tools" 41 | 42 | id = Column(Integer, primary_key=True, index=True) 43 | identifier = Column(String(200), nullable=True, unique=True, index=True) 44 | name = Column(String(500), nullable=False, index=True) 45 | url = Column(String(1000), nullable=False, index=True) 46 | description = Column(Text, nullable=True) 47 | category = Column(String(100), nullable=True, index=True) 48 | is_featured = Column(Boolean, default=False, index=True) 49 | view_count = Column(Integer, default=0, index=True) 50 | score = Column(Integer, default=0) 51 | created_at = Column(String(100), nullable=True) 52 | extra_data = Column(JSON, nullable=True) # 存储其他额外字段 53 | created_at_db = Column(DateTime, default=datetime.now) 54 | updated_at_db = Column(DateTime, default=datetime.now, onupdate=datetime.now) 55 | 56 | __table_args__ = ( 57 | Index('idx_tool_category_featured', 'category', 'is_featured'), 58 | Index('idx_tool_view_count', 'view_count'), 59 | ) 60 | 61 | 62 | class Prompt(Base): 63 | """提示词模型""" 64 | __tablename__ = "prompts" 65 | 66 | id = Column(Integer, primary_key=True, index=True) 67 | identifier = Column(String(200), nullable=True, unique=True, index=True) 68 | name = Column(String(500), nullable=False, index=True) 69 | description = Column(Text, nullable=True) 70 | content = Column(Text, nullable=False) 71 | category = Column(String(100), nullable=True, index=True) 72 | extra_data = Column(JSON, nullable=True) # 存储其他额外字段 73 | created_at_db = Column(DateTime, default=datetime.now) 74 | updated_at_db = Column(DateTime, default=datetime.now, onupdate=datetime.now) 75 | 76 | 77 | class Rule(Base): 78 | """规则模型""" 79 | __tablename__ = "rules" 80 | 81 | id = Column(Integer, primary_key=True, index=True) 82 | name = Column(String(500), nullable=False, index=True) 83 | description = Column(Text, nullable=True) 84 | content = Column(Text, nullable=False) 85 | category = Column(String(100), nullable=True, index=True) 86 | extra_data = Column(JSON, nullable=True) # 存储其他额外字段 87 | created_at_db = Column(DateTime, default=datetime.now) 88 | updated_at_db = Column(DateTime, default=datetime.now, onupdate=datetime.now) 89 | 90 | 91 | class Resource(Base): 92 | """社区资源模型""" 93 | __tablename__ = "resources" 94 | 95 | id = Column(Integer, primary_key=True, index=True) 96 | title = Column(String(500), nullable=False, index=True) 97 | url = Column(String(1000), nullable=True) 98 | description = Column(Text, nullable=True) 99 | type = Column(String(100), nullable=True, index=True) 100 | category = Column(String(100), nullable=True, index=True) 101 | subcategory = Column(String(100), nullable=True, index=True) 102 | created_at = Column(String(100), nullable=True) 103 | extra_data = Column(JSON, nullable=True) # 存储其他额外字段 104 | created_at_db = Column(DateTime, default=datetime.now) 105 | updated_at_db = Column(DateTime, default=datetime.now, onupdate=datetime.now) 106 | 107 | __table_args__ = ( 108 | Index('idx_resource_type_category', 'type', 'category'), 109 | ) 110 | 111 | -------------------------------------------------------------------------------- /app/infrastructure/scheduler.py: -------------------------------------------------------------------------------- 1 | """调度器管理模块""" 2 | 3 | import asyncio 4 | from typing import Optional, Callable, Any, Dict 5 | from datetime import datetime 6 | 7 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 8 | from apscheduler.triggers.cron import CronTrigger 9 | from loguru import logger 10 | 11 | 12 | class SchedulerManager: 13 | """调度器管理器""" 14 | 15 | def __init__(self, timezone: str = "Asia/Shanghai"): 16 | """ 17 | 初始化调度器管理器 18 | 19 | Args: 20 | timezone: 时区 21 | """ 22 | self.scheduler: Optional[AsyncIOScheduler] = None 23 | self.timezone = timezone 24 | self._lock = asyncio.Lock() 25 | 26 | def create_scheduler(self) -> AsyncIOScheduler: 27 | """创建调度器实例""" 28 | if self.scheduler is not None and self.scheduler.running: 29 | logger.warning("[调度器] 检测到已有调度器在运行,正在关闭...") 30 | try: 31 | self.scheduler.shutdown(wait=False) 32 | except Exception as e: 33 | logger.warning(f"[调度器] 关闭旧调度器时出错: {e}") 34 | 35 | self.scheduler = AsyncIOScheduler(timezone=self.timezone) 36 | logger.info("[调度器] 调度器实例已创建") 37 | return self.scheduler 38 | 39 | def add_job( 40 | self, 41 | func: Callable, 42 | trigger: str | CronTrigger, 43 | job_id: str, 44 | **kwargs: Any 45 | ) -> None: 46 | """ 47 | 添加定时任务 48 | 49 | Args: 50 | func: 要执行的函数 51 | trigger: 触发器(cron表达式字符串或CronTrigger对象) 52 | job_id: 任务ID 53 | **kwargs: 其他参数(如kwargs传递给函数) 54 | """ 55 | if self.scheduler is None: 56 | raise RuntimeError("调度器未初始化,请先调用 create_scheduler()") 57 | 58 | # 如果trigger是字符串,转换为CronTrigger 59 | if isinstance(trigger, str): 60 | trigger = CronTrigger.from_crontab(trigger, timezone=self.timezone) 61 | 62 | self.scheduler.add_job( 63 | func, 64 | trigger=trigger, 65 | id=job_id, 66 | replace_existing=True, 67 | **kwargs 68 | ) 69 | logger.info(f"[调度器] 已添加任务: {job_id}") 70 | 71 | def add_cron_job( 72 | self, 73 | func: Callable, 74 | hour: int, 75 | minute: int, 76 | job_id: str, 77 | **kwargs: Any 78 | ) -> None: 79 | """ 80 | 添加cron定时任务 81 | 82 | Args: 83 | func: 要执行的函数 84 | hour: 小时 85 | minute: 分钟 86 | job_id: 任务ID 87 | **kwargs: 其他参数 88 | """ 89 | if self.scheduler is None: 90 | raise RuntimeError("调度器未初始化,请先调用 create_scheduler()") 91 | 92 | self.scheduler.add_job( 93 | func, 94 | "cron", 95 | hour=hour, 96 | minute=minute, 97 | id=job_id, 98 | replace_existing=True, 99 | **kwargs 100 | ) 101 | logger.info( 102 | f"[调度器] 已添加任务: {job_id}, " 103 | f"执行时间: {hour:02d}:{minute:02d} ({self.timezone})" 104 | ) 105 | 106 | def start(self) -> None: 107 | """启动调度器""" 108 | if self.scheduler is None: 109 | raise RuntimeError("调度器未初始化,请先调用 create_scheduler()") 110 | 111 | self.scheduler.start() 112 | logger.info("[调度器] 调度器已启动,等待触发定时任务...") 113 | 114 | # 列出所有已添加的任务 115 | all_jobs = self.scheduler.get_jobs() 116 | logger.info(f"[调度器] 当前共有 {len(all_jobs)} 个定时任务:") 117 | for job in all_jobs: 118 | next_run = getattr(job, 'next_run_time', None) or getattr(job, 'next_run', None) 119 | if next_run: 120 | logger.info(f"[调度器] - {job.id}: 下次执行时间 = {next_run}") 121 | else: 122 | logger.info(f"[调度器] - {job.id}: 已添加(执行时间待计算)") 123 | 124 | def shutdown(self, wait: bool = True) -> None: 125 | """关闭调度器""" 126 | if self.scheduler is not None: 127 | try: 128 | if self.scheduler.running: 129 | self.scheduler.shutdown(wait=wait) 130 | logger.info("[调度器] 调度器已关闭") 131 | else: 132 | logger.info("[调度器] 调度器未运行,无需关闭") 133 | except Exception as e: 134 | logger.error(f"[调度器] 关闭调度器时出错: {e}") 135 | finally: 136 | self.scheduler = None 137 | 138 | def get_job(self, job_id: str) -> Optional[Any]: 139 | """获取任务""" 140 | if self.scheduler is None: 141 | return None 142 | return self.scheduler.get_job(job_id) 143 | 144 | @property 145 | def running(self) -> bool: 146 | """检查调度器是否运行中""" 147 | return self.scheduler is not None and self.scheduler.running 148 | 149 | -------------------------------------------------------------------------------- /data/tools/ui.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "webdraw", 4 | "url": "https://webdraw.ai/", 5 | "description": "webdraw.ai:提供了一个直观的环境,将草图直接转换为完整的应用程序", 6 | "category": "ui", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 13, 10 | "created_at": "2024-12-01T00:00:00Z", 11 | "is_featured": false, 12 | "id": 8, 13 | "identifier": "webdraw" 14 | }, 15 | { 16 | "name": "same.new", 17 | "url": "https://same.new/", 18 | "description": "Same.dev是一款专注于UI克隆的AI工具,其核心价值主张简单明了:\"Copy any UI\"(复制任何UI)。与传统的UI设计工具不同,Same.dev采用了一种全新的方法:用户只需提供目标网站的URL,系统就能生成该界面的像素级完美复制版本。", 19 | "category": "ui", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 2, 23 | "created_at": "2025-03-18T00:00:00Z", 24 | "is_featured": false, 25 | "id": 9, 26 | "identifier": "samenew" 27 | }, 28 | { 29 | "name": "locofy", 30 | "url": "https://www.locofy.ai/", 31 | "description": "Locofy.ai:将您的设计转换为适合开发人员的前端代码,支持移动应用和网页。它使构建者能够在现有设计工具、技术栈和工作流程的基础上将产品交付速度提高 10 倍", 32 | "category": "ui", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 2, 36 | "created_at": "2024-12-01T00:00:00Z", 37 | "is_featured": false, 38 | "id": 10, 39 | "identifier": "locofy" 40 | }, 41 | { 42 | "name": "flame", 43 | "url": "https://github.com/Flame-Code-VLM/Flame-Code-VLM", 44 | "description": "Flame 是一款开源的多模态人工智能系统,专为将 UI 设计稿转化为高质量的 React 代码而构建。该系统通过融合视觉语言建模、自动化数据合成和结构化训练流程,致力于消除设计与前端开发之间的鸿沟,实现设计稿到可运行代码的端到端转换。", 45 | "category": "ui", 46 | "tags": [], 47 | "icon": "🔧", 48 | "view_count": 3, 49 | "created_at": "2025-03-06T00:00:00Z", 50 | "is_featured": false, 51 | "id": 11, 52 | "identifier": "flame" 53 | }, 54 | { 55 | "name": "copycoder", 56 | "url": "https://copycoder.ai/", 57 | "description": "为下一代 AI 编程人员打造。上传完整应用程序、UI 原型或自定义设计的图片,并使用我们生成的提示更快地构建您的应用程序。", 58 | "category": "ui", 59 | "tags": [], 60 | "icon": "🔧", 61 | "view_count": 2, 62 | "created_at": "2024-12-01T00:00:00Z", 63 | "is_featured": false, 64 | "id": 12, 65 | "identifier": "copycoder" 66 | }, 67 | { 68 | "name": "builder", 69 | "url": "https://builder.io/", 70 | "description": "Figma 插件:将设计转换为干净、响应式的代码,支持多个框架和 CSS 库。", 71 | "category": "ui", 72 | "tags": [], 73 | "icon": "🔧", 74 | "view_count": 0, 75 | "created_at": "2024-12-01T00:00:00Z", 76 | "is_featured": false, 77 | "id": 13, 78 | "identifier": "builder" 79 | }, 80 | { 81 | "name": "UI-Pilot", 82 | "url": "https://ui-pilot.com/", 83 | "description": "UI Pilot 是一个由 AI 驱动的代码生成器。您可以要求它为您创建一个表单式 UI,它会提供代码和一个运行示例。您可以输入类似以下内容:“创建一个包含名字和姓氏字段的表单”。", 84 | "category": "ui", 85 | "tags": [], 86 | "icon": "🔧", 87 | "view_count": 1, 88 | "created_at": "2024-12-01T00:00:00Z", 89 | "is_featured": false, 90 | "id": 14, 91 | "identifier": "ui-pilot" 92 | }, 93 | { 94 | "name": "Screenshot-to-Code", 95 | "url": "https://screenshottocode.com/", 96 | "description": "根据截屏生成代码,Drop in a screenshot and convert it to clean code (HTML/Tailwind/React/Vue)", 97 | "category": "ui", 98 | "tags": [], 99 | "icon": "🔧", 100 | "view_count": 0, 101 | "created_at": "2024-12-01T00:00:00Z", 102 | "is_featured": false, 103 | "id": 15, 104 | "identifier": "screenshot-to-code" 105 | }, 106 | { 107 | "name": "Literallyanything", 108 | "url": "https://www.literallyanything.io/", 109 | "description": "HTML 和 JavaScript Web 应用生成器:生成完整的前端应用,支持 HTML 和 JavaScript", 110 | "category": "ui", 111 | "tags": [], 112 | "icon": "🔧", 113 | "view_count": 2, 114 | "created_at": "2024-12-01T00:00:00Z", 115 | "is_featured": false, 116 | "id": 16, 117 | "identifier": "literallyanything" 118 | }, 119 | { 120 | "name": "Kombai", 121 | "url": "https://kombai.com/", 122 | "description": "从 Figma 生成前端代码的AI工具。", 123 | "category": "ui", 124 | "tags": [], 125 | "icon": "🔧", 126 | "view_count": 1, 127 | "created_at": "2024-12-01T00:00:00Z", 128 | "is_featured": false, 129 | "id": 17, 130 | "identifier": "kombai" 131 | }, 132 | { 133 | "name": "Draw-A-UI", 134 | "url": "https://draw-a-ui.com/", 135 | "description": "根据草图生成代码,Use AI to turn your UI sketches into HTML code instantly, right from your browser.", 136 | "category": "ui", 137 | "tags": [], 138 | "icon": "🔧", 139 | "view_count": 2, 140 | "created_at": "2024-12-01T00:00:00Z", 141 | "is_featured": false, 142 | "id": 18, 143 | "identifier": "draw-a-ui" 144 | }, 145 | { 146 | "id": 523792, 147 | "name": "animaapp", 148 | "url": "https://dev.animaapp.com/", 149 | "description": "Anima(访问 \ndev.animaapp.com\n)是一款将设计稿(如 Figma 设计)或网页 URL “一键”自动转换为可运行、可编辑、响应式网页/应用代码(React / Vue / HTML + CSS 或 Tailwind 等)的 AI 驱动 “design-to-code” 平台,帮助设计师和开发者从视觉稿快速生成生产就绪代码,极大缩短从“设计”到“上线”的开发周期。", 150 | "category": "ui", 151 | "tags": [], 152 | "icon": "🔧", 153 | "score": 0, 154 | "view_count": 2, 155 | "like_count": 0, 156 | "is_featured": false, 157 | "created_at": "2025-11-29T07:45:15.027133Z" 158 | } 159 | ] -------------------------------------------------------------------------------- /data/tools/ai-test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "tusk", 4 | "url": "https://www.usetusk.ai/", 5 | "description": "通过高影响力的测试增加代码覆盖率。生成基于代码库上下文的单元测试和集成测试,捕捉开发者遗漏的边界情况。", 6 | "category": "ai-test", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 9, 10 | "created_at": "2024-12-01T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "tusk" 14 | }, 15 | { 16 | "name": "devchat", 17 | "url": "https://www.devchat.ai/", 18 | "description": "从文档到测试脚本,只需一步\n\n根据 API 文档、调用日志、业务场景描述等,自动生成测试脚本。 将你的测试开发效率提升 10 倍,轻松提升接口测试覆盖。", 19 | "category": "ai-test", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 2, 23 | "created_at": "2025-10-13T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "devchat" 27 | }, 28 | { 29 | "name": "carbonate", 30 | "url": "https://carbonate.dev/", 31 | "description": "使用自然语言进行端到端测试。与现有的测试套件(目前支持 Jest、PHPUnit 和 Python 的 unittest)集成。", 32 | "category": "ai-test", 33 | "tags": [], 34 | "icon": "🔧", 35 | "view_count": 0, 36 | "created_at": "2024-12-01T00:00:00Z", 37 | "is_featured": false, 38 | "id": 3, 39 | "identifier": "carbonate" 40 | }, 41 | { 42 | "name": "OctoMind", 43 | "url": "https://www.octomind.dev/", 44 | "description": "自动维护并生成基于浏览器的端到端测试,集成到 Github Actions、Azure DevOps 等工具中。", 45 | "category": "ai-test", 46 | "tags": [], 47 | "icon": "🔧", 48 | "view_count": 2, 49 | "created_at": "2024-12-01T00:00:00Z", 50 | "is_featured": false, 51 | "id": 4, 52 | "identifier": "octomind" 53 | }, 54 | { 55 | "name": "MutahunterAI", 56 | "url": "https://github.com/codeintegrity-ai/mutahunter", 57 | "description": "通过发现代码中的漏洞并为其生成测试,加速开发者的生产力和代码安全。开源并提供 CLI 或 CI/CD 管道支持。", 58 | "category": "ai-test", 59 | "tags": [], 60 | "icon": "🔧", 61 | "view_count": 1, 62 | "created_at": "2024-12-01T00:00:00Z", 63 | "is_featured": false, 64 | "id": 5, 65 | "identifier": "mutahunterai" 66 | }, 67 | { 68 | "name": "Meticulous.ai", 69 | "url": "https://www.meticulous.ai/", 70 | "description": "自动生成、自动维护的端到端测试:随着应用程序的发展,测试套件也会不断更新。", 71 | "category": "ai-test", 72 | "tags": [], 73 | "icon": "🔧", 74 | "view_count": 4, 75 | "created_at": "2024-12-01T00:00:00Z", 76 | "is_featured": false, 77 | "id": 6, 78 | "identifier": "meticulousai" 79 | }, 80 | { 81 | "name": "KushoAI", 82 | "url": "https://kusho.ai/", 83 | "description": "API 测试的 AI 代理,可以将您的 Postman 集合、OpenAPI 规范、curl 命令等转换为完整的测试套件,并集成到您的 CI/CD 管道中。", 84 | "category": "ai-test", 85 | "tags": [], 86 | "icon": "🔧", 87 | "view_count": 1, 88 | "created_at": "2024-12-01T00:00:00Z", 89 | "is_featured": false, 90 | "id": 7, 91 | "identifier": "kushoai" 92 | }, 93 | { 94 | "name": "EarlyAI", 95 | "url": "https://www.startearly.ai/", 96 | "description": "EarlyAI 是一款智能开发工具,通过自动生成高覆盖率的单元测试来提升代码质量,帮助开发者专注于创新应用。它提供自动测试生成、易于导航的界面、文档建议和 Pull Request 测试等功能,用户已生成超过 50,000 个测试,显著提升了代码覆盖率和开发效率。", 97 | "category": "ai-test", 98 | "tags": [], 99 | "icon": "🔧", 100 | "view_count": 1, 101 | "created_at": "2024-12-01T00:00:00Z", 102 | "is_featured": false, 103 | "id": 8, 104 | "identifier": "earlyai" 105 | }, 106 | { 107 | "name": "BlinqIO", 108 | "url": "https://blinq.io/", 109 | "description": "BlinqIO AI Test Engineer 实现了应用程序的完整端到端测试自动化,帮助您更快发布高质量软件。借助 AI Test Engineer,您可以轻松将手动测试转换为测试自动化代码(Playwright,开源),并将其集成到您的 CI/CD 管道中。", 110 | "category": "ai-test", 111 | "tags": [], 112 | "icon": "🔧", 113 | "view_count": 1, 114 | "created_at": "2024-12-01T00:00:00Z", 115 | "is_featured": false, 116 | "id": 9, 117 | "identifier": "blinqio" 118 | }, 119 | { 120 | "id": 285305, 121 | "name": "testsprite", 122 | "url": "https://www.testsprite.com/", 123 | "description": "这是一款由 AI 驱动的“零代码”自动化测试平台,能够自动生成并执行 GUI、前端交互、后端 API 等测试用例,在几十分钟内完成完整测试流程,从而帮你快速发现与修复 bug,提高代码发布质量与效率。", 124 | "category": "ai-test", 125 | "tags": [], 126 | "icon": "🔧", 127 | "score": 0, 128 | "view_count": 2, 129 | "like_count": 0, 130 | "is_featured": false, 131 | "created_at": "2025-11-28T08:53:05.665076Z" 132 | }, 133 | { 134 | "id": 300322, 135 | "name": "Momentic", 136 | "url": "https://momentic.ai/", 137 | "description": "一款由 AI 驱动的端到端自动化测试平台——你只需用自然语言描述用户流程或断言,Momentic 就能自动生成稳定、不易\"断裂\"的测试脚本,并随着应用 UI 演化自适应更新,极大节省开发/QA 团队编写和维护测试的时间,提高发布效率与质量", 138 | "category": "ai-test", 139 | "tags": [], 140 | "icon": "🔧", 141 | "score": 0, 142 | "view_count": 0, 143 | "like_count": 0, 144 | "is_featured": false, 145 | "created_at": "2025-11-28T08:53:05.665169Z" 146 | }, 147 | { 148 | "name": "QA Wolf", 149 | "url": "https://www.qawolf.com/", 150 | "description": "QA Wolf 通过高效的自动化测试服务,帮助软件团队提升质量管理,自动生成和维护端到端测试,大幅减少手动测试工作。", 151 | "category": "ai-test", 152 | "tags": [], 153 | "icon": "🔧", 154 | "view_count": 0, 155 | "created_at": "2025-12-01T00:00:00Z", 156 | "is_featured": false, 157 | "id": 10, 158 | "identifier": "qa-wolf" 159 | } 160 | ] -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | 本文档记录了 AI-CodeNexus 项目的所有重要更新和变更。 4 | 5 | --- 6 | 7 | ## v4.1(当前版本) 8 | 9 | ### 新增功能 10 | - ✅ **Claude Code 资源模块** 11 | - 新增 Claude Code 资源分类,包含17个精选资源 12 | - 支持四个子分类浏览: 13 | - 🔌 插件市场:Claude Code 插件和扩展市场 14 | - 🌐 模型服务:API代理、路由服务等 15 | - 🎯 Skill:Agent Skills 相关资源 16 | - 📦 其他:其他 Claude Code 相关资源 17 | - 智能分类算法,根据资源标题、描述和URL自动分类 18 | - 桌面端支持hover子菜单,移动端支持展开子菜单 19 | - 从 devmaster.cn 自动抓取资源数据 20 | 21 | ### 修复问题 22 | - ✅ 修复每周资讯页面重复显示标题的问题 23 | - API返回的HTML已包含标题,前端不再重复渲染 24 | - ✅ 修复资源显示代码的语法错误 25 | - 修复else分支中代码缩进问题,导致JavaScript语法错误 26 | - 修复后菜单点击功能恢复正常 27 | 28 | ### 技术实现 29 | - 新增抓取脚本:`scripts/crawl_devmaster_resources.py` 30 | - 支持抓取 Claude Code 资源 31 | - 使用 Playwright 抓取动态内容 32 | - 自动分类到四个子类 33 | - 新增更新脚本:`scripts/update_claude_code_subcategories.py` 34 | - 为现有资源添加subcategory字段 35 | - 扩展API:支持 `subcategory` 参数过滤 36 | - 扩展数据模型:为Claude Code资源添加 `subcategory` 字段 37 | - 更新UI:添加子分类菜单和显示逻辑 38 | 39 | ### 数据文件 40 | - `data/resources.json` - 新增17个Claude Code资源,包含subcategory字段 41 | 42 | --- 43 | 44 | ## v4.0 45 | 46 | ### 重大更新 47 | - ✅ 全新导航菜单结构 48 | - 移除:热门资讯、提交资讯 49 | - 优化:将编程资讯和AI资讯合并为"最新资讯"下拉菜单 50 | - 新增:提示词、规则、社区资源 51 | - 聚焦AI编程中最重要的四个核心内容 52 | 53 | ### 新增功能 54 | - ✅ 提示词模块(Prompts) 55 | - 精选AI编程相关提示词 56 | - 支持分类浏览和搜索 57 | - 参考 waytoagi.com 的编码提示词 58 | - 支持一键复制提示词内容 59 | - 优化排版,完整显示提示词内容 60 | 61 | - ✅ 规则模块(Rules) 62 | - Cursor Rules 和其他AI编程规则 63 | - 支持分类浏览和搜索 64 | - 参考 cursor.directory 的规则格式 65 | - 从 dotcursorrules.com 扩展规则库,包含31个规则 66 | 67 | - ✅ 社区资源模块(Resources) 68 | - AI编程教程、文章和社区资源 69 | - 支持类型和分类筛选 70 | - 参考 cursor101.com 的资源内容 71 | - 按分类模块化显示(飞书知识库、技术社区) 72 | 73 | ### 修复问题 74 | - ✅ 修复导航菜单激活状态问题 75 | - 修复菜单项点击后激活状态不更新的问题 76 | - 优化路由匹配逻辑,支持所有路由正确激活 77 | - 在内容加载完成后自动更新导航状态 78 | 79 | - ✅ 修复社区资源加载失败问题 80 | - 修复 page_size=1000 超过API限制导致的422错误 81 | - 调整为使用 page_size=100 获取资源 82 | 83 | - ✅ 修复提示词复制功能语法错误 84 | - 修复JavaScript正则表达式语法错误 85 | - 使用base64编码安全传递提示词内容 86 | - 优化复制功能的用户体验 87 | 88 | ### 技术实现 89 | - 新增API路由:`/api/prompts`、`/api/rules`、`/api/resources` 90 | - 扩展 DataLoader 服务,支持新数据类型 91 | - 新增前端路由和页面渲染逻辑 92 | - 更新配置文件,添加新页面配置 93 | - 优化导航菜单激活状态管理 94 | - 创建社区资源抓取脚本:`scripts/crawl_devmaster_resources.py` 95 | 96 | ### 数据文件 97 | - `data/prompts.json` - 提示词数据(包含飞书文档中的提示词) 98 | - `data/rules.json` - 规则数据(31个规则) 99 | - `data/resources.json` - 社区资源数据(按分类组织) 100 | 101 | --- 102 | 103 | ## v3.6 104 | 105 | ### 新增功能 106 | - ✅ 管理员文章删除功能 107 | - 管理员登录后,前端页面的每个文章右侧显示删除按钮 108 | - 支持从所有数据源删除文章: 109 | - 文章池(`ai_articles.json`) 110 | - 归档分类文件(`ai_news.json`、`programming.json`、`ai_coding.json`) 111 | - 周报文件(`data/weekly/` 目录下的Markdown文件) 112 | - 删除后自动更新周报 113 | - 删除按钮仅在管理员登录后显示(通过暗码登录) 114 | - 支持在所有文章列表页面删除(分类列表、工具详情页相关文章、热门资讯、最新资讯等) 115 | 116 | ### 技术实现 117 | - 前端:添加 `isAdmin()` 和 `deleteArticle()` 函数 118 | - 后端:扩展删除API,支持从所有数据源删除 119 | - 数据服务:添加 `delete_article_from_all_categories()` 方法 120 | - 周报服务:添加 `delete_article_from_weekly()` 方法 121 | 122 | --- 123 | 124 | ## v3.5 125 | 126 | ### 移动端优化 127 | - ✅ 全面优化移动端浏览器体验 128 | - 左侧菜单栏在移动端默认隐藏,可通过汉堡菜单按钮打开/关闭 129 | - 添加移动端侧边栏抽屉效果,支持滑动显示和遮罩层关闭 130 | - 顶部导航栏在移动端隐藏,添加下拉菜单按钮(⋮)访问所有导航链接 131 | - 主内容区域在移动端自动占满屏幕宽度,不再被侧边栏占据空间 132 | - 优化移动端布局和交互体验,支持触摸操作和键盘快捷键(ESC关闭菜单) 133 | - 响应式设计,完美适配手机浏览器 134 | 135 | --- 136 | ## v3.4 137 | 138 | ### 新增功能 139 | - ✅ 添加每周资讯推荐功能 140 | - 自动整理本周新增的资讯为Markdown格式 141 | - 当有资讯被采纳或归档时自动更新周报 142 | - 文件命名格式:`{年份}weekly{周数}.md` 143 | - 适合复制到微信公众号的格式 144 | - 随数据备份一起提交到GitHub 145 | 146 | --- 147 | 148 | ## v3.3 149 | 150 | ### 新增功能 151 | - ✅ 添加数据自动备份功能 152 | - 每天 23:00 自动将 `data/` 和 `config/` 目录提交到 GitHub 153 | - 自动处理冲突,拉取最新代码后重新推送 154 | - 智能检测变更,没有变更时跳过提交 155 | - 详细的日志记录,便于排查问题 156 | 157 | - ✅ 添加日志存档功能 158 | - 自动将日志保存到文件(`logs/` 目录) 159 | - 按日期自动轮转和压缩 160 | - 定时任务关键日志单独存档(保留90天) 161 | - 错误日志单独存档(保留90天) 162 | - 应用日志存档(保留30天) 163 | 164 | --- 165 | 166 | ## v3.2 167 | 168 | ### 修复问题 169 | - ✅ 修复归档状态检查问题 170 | - 改进微信文章URL规范化逻辑,避免动态参数导致的误判 171 | - 对于只有动态参数的URL,使用精确匹配而非规范化匹配 172 | - 修复关键词摘取和工具关键词摘取的资讯误判为已归档的问题 173 | 174 | - ✅ 修复热门工具显示问题 175 | - 修复热门工具只显示29个工具的问题(实际有113个工具) 176 | - 热门工具模式不去重,显示所有工具并按访问量排序 177 | - 其他模式使用URL去重(比ID更可靠) 178 | 179 | ### 改进功能 180 | - ✅ 改进微信公众号页面 181 | - 添加GitHub仓库宣传卡片 182 | - 修复文字对齐问题,GitHub链接和描述文字居中显示 183 | - 优化页面布局和视觉效果 184 | 185 | - ✅ 实现全局浮动按钮功能 186 | - 反馈/联系按钮:点击跳转到提交资讯页面 187 | - 回到顶部按钮:滚动超过300px时显示,点击平滑滚动到顶部 188 | 189 | --- 190 | 191 | ## v3.1 192 | 193 | ### 功能增强 194 | - ✅ 自动推送功能增强 195 | - 文章池和候选池都为空时,自动按关键字抓取文章 196 | - 每个关键字随机选择1篇文章,直接放入文章列表 197 | - 推送完成后自动清理文章池和候选池 198 | - 完善的错误处理和日志记录 199 | 200 | - ✅ 推送函数改进 201 | - 推送函数返回成功/失败状态 202 | - 详细的日志记录(`[定时推送]`、`[自动抓取]`、`[调度器]`) 203 | - 统一手动推送和定时推送的逻辑 204 | 205 | --- 206 | 207 | ## v3.0 208 | 209 | ### 新增功能 210 | - ✅ 工具相关资讯获取功能 211 | - 工具关键字自动管理 212 | - 手动触发单个或批量获取 213 | - 自动标签关联 214 | - 工具详情页展示相关资讯 215 | 216 | - ✅ 工具数据录入和整理 217 | - 完善工具分类体系 218 | - 工具标识符(identifier)支持 219 | - 工具详情页功能 220 | 221 | --- 222 | 223 | ## v2.0 224 | 225 | ### 核心功能 226 | - ✅ 完整的资讯和工具聚合平台 227 | - ✅ 用户提交功能 228 | - ✅ 管理员审核功能 229 | - ✅ 点击统计和热度排序 230 | - ✅ 文章-工具关联 231 | - ✅ 现代化UI设计 232 | - ✅ 标准URL路由(无 `#`) 233 | 234 | --- 235 | 236 | ## v1.0 237 | 238 | ### 初始版本 239 | - ✅ 基础的文章抓取和管理系统 240 | - ✅ 多资讯源支持 241 | - ✅ 微信公众号发布 242 | 243 | --- 244 | 245 | **文档维护**:本文档会随着项目更新持续维护,记录所有重要的功能变更和修复。 246 | 247 | -------------------------------------------------------------------------------- /docs/deploy/wechat_mp_guide.md: -------------------------------------------------------------------------------- 1 | # 微信公众号发布功能使用指南 2 | 3 | ## 功能说明 4 | 5 | 微信公众号发布功能允许你将抓取的文章自动发布到微信公众号,支持: 6 | 7 | 1. **创建草稿**:将文章创建为微信公众号草稿 8 | 2. **发布草稿**:将草稿发布到公众号 9 | 3. **一键发布日报**:直接将当前日报发布到公众号 10 | 11 | ## 当前状态 12 | 13 | ✅ **代码已实现**:微信公众号客户端代码已完成 14 | ⚠️ **需要配置**:需要配置 AppID 和 Secret 15 | ⚠️ **需要测试**:功能已实现但需要实际测试验证 16 | 17 | ## 配置步骤 18 | 19 | ### 1. 获取微信公众号 AppID 和 Secret 20 | 21 | 1. 登录[微信公众平台](https://mp.weixin.qq.com/) 22 | 2. 进入"开发" -> "基本配置" 23 | 3. 获取 `AppID` 和 `AppSecret` 24 | 25 | ### 2. 配置环境变量 26 | 27 | 在 `.env` 文件中添加: 28 | 29 | ```bash 30 | WECHAT_MP_APPID="your_appid" 31 | WECHAT_MP_SECRET="your_secret" 32 | ``` 33 | 34 | 或者在系统配置面板中配置(需要重启服务生效)。 35 | 36 | ### 3. 设置 IP 白名单(可选) 37 | 38 | 如果使用 IP 白名单,需要在微信公众平台添加服务器 IP。 39 | 40 | ## 使用方法 41 | 42 | ### 方法一:通过 API 接口 43 | 44 | #### 1. 创建草稿 45 | 46 | ```bash 47 | curl -X POST "http://localhost:8000/digest/wechat-mp/create-draft" \ 48 | -H "X-Admin-Code: your-admin-code" \ 49 | -H "Content-Type: application/json" \ 50 | -d '{ 51 | "articles": [ 52 | { 53 | "title": "文章标题", 54 | "author": "作者", 55 | "digest": "摘要", 56 | "content": "

文章内容

", 57 | "content_source_url": "https://example.com/article", 58 | "thumb_media_id": "", 59 | "show_cover_pic": 1 60 | } 61 | ] 62 | }' 63 | ``` 64 | 65 | #### 2. 发布草稿 66 | 67 | ```bash 68 | curl -X POST "http://localhost:8000/digest/wechat-mp/publish" \ 69 | -H "X-Admin-Code: your-admin-code" \ 70 | -H "Content-Type: application/json" \ 71 | -d '{ 72 | "media_id": "your_media_id" 73 | }' 74 | ``` 75 | 76 | #### 3. 一键发布日报 77 | 78 | ```bash 79 | curl -X POST "http://localhost:8000/digest/wechat-mp/publish-digest" \ 80 | -H "X-Admin-Code: your-admin-code" 81 | ``` 82 | 83 | ### 方法二:通过 Swagger UI 84 | 85 | 1. 启动服务:`uvicorn app.main:app --host 0.0.0.0 --port 8000` 86 | 2. 访问:`http://localhost:8000/docs` 87 | 3. 找到微信公众号相关接口: 88 | - `POST /digest/wechat-mp/create-draft` - 创建草稿 89 | - `POST /digest/wechat-mp/publish` - 发布草稿 90 | - `POST /digest/wechat-mp/publish-digest` - 一键发布日报 91 | 4. 点击 "Try it out",填写参数,执行测试 92 | 93 | ### 方法三:在代码中使用 94 | 95 | ```python 96 | from app.notifier.wechat_mp import WeChatMPClient 97 | 98 | # 创建客户端 99 | client = WeChatMPClient() 100 | 101 | # 准备文章数据 102 | articles = [ 103 | { 104 | "title": "文章标题", 105 | "author": "作者", 106 | "digest": "摘要(120字以内)", 107 | "content": "

文章内容 HTML

", 108 | "content_source_url": "https://example.com/article", 109 | "thumb_media_id": "", # 可选:封面图 media_id 110 | "show_cover_pic": 1, # 是否显示封面 111 | } 112 | ] 113 | 114 | # 创建草稿 115 | media_id = await client.create_draft(articles) 116 | if media_id: 117 | print(f"草稿创建成功,media_id: {media_id}") 118 | 119 | # 发布草稿 120 | success = await client.publish(media_id) 121 | if success: 122 | print("发布成功!") 123 | ``` 124 | 125 | ## 文章格式说明 126 | 127 | 微信公众号文章需要以下字段: 128 | 129 | - **title** (必填): 文章标题 130 | - **author** (必填): 作者 131 | - **digest** (可选): 摘要,最多120字 132 | - **content** (必填): 文章内容,HTML 格式 133 | - **content_source_url** (必填): 原文链接 134 | - **thumb_media_id** (可选): 封面图 media_id(需要先上传) 135 | - **show_cover_pic** (可选): 是否显示封面,0/1 136 | 137 | ## 注意事项 138 | 139 | ### 1. Access Token 管理 140 | 141 | - Access Token 有效期为 7200 秒(2小时) 142 | - 系统会自动刷新 Token(提前5分钟) 143 | - 如果 Token 过期,会自动重新获取 144 | 145 | ### 2. 文章数量限制 146 | 147 | - 单次最多创建 8 篇文章 148 | - 如果文章过多,建议分批创建 149 | 150 | ### 3. 封面图 151 | 152 | - 如果需要封面图,需要先使用 `upload_media` 上传图片 153 | - 上传后获得 `media_id`,在创建文章时使用 154 | 155 | ### 4. 发布限制 156 | 157 | - 每天有发布次数限制(根据公众号类型不同) 158 | - 建议先创建草稿,确认后再发布 159 | 160 | ### 5. 内容格式 161 | 162 | - 内容必须是 HTML 格式 163 | - 支持基本的 HTML 标签(p, a, img, strong 等) 164 | - 不支持 JavaScript 和外部 CSS 165 | 166 | ## 测试步骤 167 | 168 | ### 1. 测试 Access Token 获取 169 | 170 | ```python 171 | from app.notifier.wechat_mp import WeChatMPClient 172 | 173 | client = WeChatMPClient() 174 | token = await client.get_access_token() 175 | print(f"Access Token: {token}") 176 | ``` 177 | 178 | ### 2. 测试创建草稿 179 | 180 | 使用 Swagger UI 或 curl 测试创建草稿接口,检查返回的 `media_id`。 181 | 182 | ### 3. 测试发布 183 | 184 | 在微信公众平台查看草稿,确认无误后再发布。 185 | 186 | ## 常见问题 187 | 188 | ### Q: 获取 Access Token 失败? 189 | 190 | A: 检查: 191 | 1. AppID 和 Secret 是否正确 192 | 2. 网络连接是否正常 193 | 3. IP 是否在白名单中(如果设置了) 194 | 195 | ### Q: 创建草稿失败? 196 | 197 | A: 检查: 198 | 1. 文章格式是否正确 199 | 2. 必填字段是否都有 200 | 3. 内容是否符合微信公众号规范 201 | 202 | ### Q: 发布失败? 203 | 204 | A: 检查: 205 | 1. media_id 是否正确 206 | 2. 草稿是否已存在 207 | 3. 是否达到每日发布限制 208 | 209 | ## 集成到现有流程 210 | 211 | 可以将微信公众号发布集成到定时推送任务中: 212 | 213 | ```python 214 | from app.notifier.wechat_mp import WeChatMPClient 215 | from app.sources.ai_articles import get_all_articles 216 | 217 | async def publish_to_wechat_mp(): 218 | """将日报发布到微信公众号""" 219 | # 获取文章 220 | articles_data = get_all_articles() 221 | 222 | # 转换为微信公众号格式 223 | wechat_articles = convert_to_wechat_format(articles_data["articles"]) 224 | 225 | # 创建并发布 226 | client = WeChatMPClient() 227 | media_id = await client.create_draft(wechat_articles) 228 | if media_id: 229 | await client.publish(media_id) 230 | ``` 231 | 232 | ## 相关文档 233 | 234 | - [微信公众平台开发文档](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html) 235 | - [素材管理 API](https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html) 236 | - [草稿箱 API](https://developers.weixin.qq.com/doc/offiaccount/Draft_Box/Add_draft.html) 237 | 238 | -------------------------------------------------------------------------------- /scripts/remove_expired_articles.py: -------------------------------------------------------------------------------- 1 | """ 2 | 删除过期微信链接的文章脚本 3 | 4 | 该脚本会: 5 | 1. 扫描所有文章数据文件 6 | 2. 识别包含临时参数的微信链接(已过期) 7 | 3. 提供选项:删除过期文章或标记为过期 8 | """ 9 | import json 10 | from pathlib import Path 11 | from typing import Dict, List 12 | from loguru import logger 13 | 14 | 15 | def find_expired_weixin_links(data: List[Dict]) -> List[tuple]: 16 | """ 17 | 查找数据中所有包含临时参数的微信链接 18 | 19 | Returns: 20 | List[tuple]: [(index, article_dict), ...] 包含过期链接的文章 21 | """ 22 | expired_items = [] 23 | for idx, item in enumerate(data): 24 | url = item.get("url", "") 25 | if url and "mp.weixin.qq.com" in url: 26 | # 检查是否包含临时参数 27 | if any(param in url for param in ["timestamp=", "signature=", "src=11"]): 28 | expired_items.append((idx, item)) 29 | return expired_items 30 | 31 | 32 | def remove_expired_articles(file_path: Path, dry_run: bool = True) -> Dict[str, int]: 33 | """ 34 | 删除文件中的过期文章 35 | 36 | Args: 37 | file_path: 文件路径 38 | dry_run: 如果为True,只显示将要删除的文章,不实际删除 39 | 40 | Returns: 41 | Dict: 统计信息 42 | """ 43 | logger.info(f"正在处理文件: {file_path}") 44 | 45 | # 读取文件 46 | try: 47 | with file_path.open("r", encoding="utf-8") as f: 48 | data = json.load(f) 49 | except Exception as e: 50 | logger.error(f"无法读取文件 {file_path}: {e}") 51 | return {"removed": 0, "total": 0} 52 | 53 | if not isinstance(data, list): 54 | logger.warning(f"文件 {file_path} 不是列表格式,跳过") 55 | return {"removed": 0, "total": 0} 56 | 57 | # 查找过期链接 58 | expired_items = find_expired_weixin_links(data) 59 | if not expired_items: 60 | logger.info(f"文件 {file_path} 中没有找到过期链接") 61 | return {"removed": 0, "total": 0} 62 | 63 | logger.info(f"找到 {len(expired_items)} 个过期链接的文章") 64 | 65 | # 显示将要删除的文章 66 | for idx, item in expired_items: 67 | title = item.get("title", "未知标题") 68 | url = item.get("url", "")[:60] 69 | logger.warning(f" [{idx}] {title[:50]}... | {url}...") 70 | 71 | if dry_run: 72 | logger.info(f"【预览模式】将会删除 {len(expired_items)} 篇文章") 73 | return {"removed": 0, "total": len(expired_items)} 74 | 75 | # 实际删除过期文章(从后往前删除,避免索引变化) 76 | expired_indices = sorted([idx for idx, _ in expired_items], reverse=True) 77 | removed_count = 0 78 | 79 | for idx in expired_indices: 80 | removed_item = data.pop(idx) 81 | removed_count += 1 82 | logger.success(f"✓ 已删除: {removed_item.get('title', '')[:50]}...") 83 | 84 | # 保存文件 85 | try: 86 | # 创建备份(在删除前,保存原始数据) 87 | backup_path = file_path.with_suffix(f".json.backup") 88 | if not backup_path.exists(): 89 | # 只有在备份不存在时才创建,避免覆盖之前的备份 90 | with file_path.open("r", encoding="utf-8") as src, backup_path.open("w", encoding="utf-8") as dst: 91 | dst.write(src.read()) 92 | logger.info(f"已创建备份: {backup_path}") 93 | else: 94 | logger.info(f"备份已存在,跳过创建: {backup_path}") 95 | 96 | # 保存更新后的文件 97 | with file_path.open("w", encoding="utf-8") as f: 98 | json.dump(data, f, ensure_ascii=False, indent=2) 99 | logger.success(f"✓ 文件已更新: {file_path}") 100 | except Exception as e: 101 | logger.error(f"保存文件失败 {file_path}: {e}") 102 | return {"removed": 0, "total": len(expired_items)} 103 | 104 | return {"removed": removed_count, "total": len(expired_items)} 105 | 106 | 107 | def main(): 108 | """主函数""" 109 | import sys 110 | 111 | # 项目根目录 112 | project_root = Path(__file__).resolve().parents[1] 113 | data_dir = project_root / "data" / "articles" 114 | 115 | # 要处理的文件列表 116 | files_to_fix = [ 117 | data_dir / "ai_news.json", 118 | data_dir / "programming.json", 119 | data_dir / "ai_coding.json", 120 | data_dir / "ai_articles.json", 121 | ] 122 | 123 | # 检查命令行参数 124 | dry_run = "--dry-run" in sys.argv or "-n" in sys.argv 125 | force = "--force" in sys.argv or "-f" in sys.argv 126 | if dry_run: 127 | logger.info("=" * 60) 128 | logger.info("【预览模式】将显示要删除的文章,但不会实际删除") 129 | logger.info("=" * 60) 130 | elif force: 131 | logger.warning("=" * 60) 132 | logger.warning("【强制执行模式】将删除所有过期链接的文章") 133 | logger.warning("=" * 60) 134 | else: 135 | logger.warning("=" * 60) 136 | logger.warning("【实际执行模式】将删除所有过期链接的文章") 137 | logger.warning("输入 'yes' 确认继续,或使用 --dry-run 先预览,或使用 --force 跳过确认") 138 | logger.warning("=" * 60) 139 | confirmation = input("确认删除?(yes/no): ") 140 | if confirmation.lower() != "yes": 141 | logger.info("已取消操作") 142 | return 143 | 144 | total_stats = {"removed": 0, "total": 0} 145 | 146 | for file_path in files_to_fix: 147 | if not file_path.exists(): 148 | logger.warning(f"文件不存在,跳过: {file_path}") 149 | continue 150 | 151 | stats = remove_expired_articles(file_path, dry_run=dry_run) 152 | total_stats["removed"] += stats["removed"] 153 | total_stats["total"] += stats["total"] 154 | logger.info("") 155 | 156 | # 打印总结 157 | logger.info("=" * 60) 158 | if dry_run: 159 | logger.info("预览完成!") 160 | logger.info(f"总计找到: {total_stats['total']} 个过期链接的文章") 161 | logger.info(f"运行脚本时不加 --dry-run 参数将删除这些文章") 162 | else: 163 | logger.info("删除完成!") 164 | logger.info(f"总计删除: {total_stats['removed']} 篇文章") 165 | logger.info("=" * 60) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | 171 | -------------------------------------------------------------------------------- /data/tools/plugin.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "tabby", 4 | "url": "https://www.tabbyml.com/", 5 | "description": "免费开源的自托管AI编程助手", 6 | "category": "plugin", 7 | "tags": [], 8 | "icon": "🔧", 9 | "view_count": 6, 10 | "created_at": "2025-01-07T00:00:00Z", 11 | "is_featured": false, 12 | "id": 1, 13 | "identifier": "tabby" 14 | }, 15 | { 16 | "name": "supermaven", 17 | "url": "https://supermaven.com/", 18 | "description": "Supermaven通过100万个token的超大上下文窗口和极低的延迟,提升代码生成的精准性和速度,并结合上下文感知技术增强开发体验。", 19 | "category": "plugin", 20 | "tags": [], 21 | "icon": "🔧", 22 | "view_count": 3, 23 | "created_at": "2024-12-01T00:00:00Z", 24 | "is_featured": false, 25 | "id": 2, 26 | "identifier": "supermaven" 27 | }, 28 | { 29 | "name": "roo-code", 30 | "url": "https://github.com/RooVetGit/Roo-Code", 31 | "description": "Roo Code (prev. Roo Cline) gives you a whole dev team of AI agents in your code editor.", 32 | "category": "plugin", 33 | "tags": [ 34 | "open-source" 35 | ], 36 | "icon": "🔧", 37 | "view_count": 2, 38 | "created_at": "2025-05-12T00:00:00Z", 39 | "is_featured": false, 40 | "id": 3, 41 | "identifier": "roo-code" 42 | }, 43 | { 44 | "name": "lingma", 45 | "url": "https://tongyi.aliyun.com/lingma", 46 | "description": "阿里巴巴的 AI 编程工具,通义灵码", 47 | "category": "plugin", 48 | "tags": [], 49 | "icon": "🔧", 50 | "view_count": 2, 51 | "created_at": "2024-12-01T00:00:00Z", 52 | "is_featured": false, 53 | "id": 4, 54 | "identifier": "lingma" 55 | }, 56 | { 57 | "name": "junie", 58 | "url": "https://www.jetbrains.com/zh-cn/junie/", 59 | "description": "jetbrains 出品,您的智能编码智能体,不仅提高工作效率 – 一种新的编码方式", 60 | "category": "plugin", 61 | "tags": [], 62 | "icon": "🔧", 63 | "view_count": 2, 64 | "created_at": "2025-06-21T00:00:00Z", 65 | "is_featured": false, 66 | "id": 5, 67 | "identifier": "junie" 68 | }, 69 | { 70 | "name": "javaAI", 71 | "url": "https://www.feisuanyz.com/home", 72 | "description": "不仅仅是一个代码生成工具,引导式开发,辅助需求细化和功能设计精准生成完整工程源码", 73 | "category": "plugin", 74 | "tags": [], 75 | "icon": "🔧", 76 | "view_count": 5, 77 | "created_at": "2025-03-01T00:00:00Z", 78 | "is_featured": false, 79 | "id": 6, 80 | "identifier": "javaai" 81 | }, 82 | { 83 | "name": "Tabnine", 84 | "url": "https://www.tabnine.com/", 85 | "description": "Tabnine 的 AI 代码助手帮助您更快地交付更高质量的软件", 86 | "category": "plugin", 87 | "tags": [], 88 | "icon": "🔧", 89 | "view_count": 0, 90 | "created_at": "2024-12-04T00:00:00Z", 91 | "is_featured": false, 92 | "id": 7, 93 | "identifier": "tabnine" 94 | }, 95 | { 96 | "name": "GitHub-Copilot", 97 | "url": "https://github.com/features/copilot", 98 | "description": "AI 编程辅助插件", 99 | "category": "plugin", 100 | "tags": [], 101 | "icon": "🔧", 102 | "view_count": 1, 103 | "created_at": "2024-12-01T00:00:00Z", 104 | "is_featured": false, 105 | "id": 8, 106 | "identifier": "github-copilot" 107 | }, 108 | { 109 | "name": "Continue", 110 | "url": "https://www.continue.dev/", 111 | "description": "Continue 是一款开源免费的 AI 编程助手插件,支持 VS Code 和 JetBrains IDE,核心特点包括灵活配置多种 AI 模型(如 GPT-4、Claude 等)、本地化部署保障数据隐私,以及智能代码补全、自然语言交互和上下文感知编辑功能。其开源特性与高度可定制化(如自定义模型、上下文工具)显著区别于闭源工具如 GitHub Copilot 和 Cursor,尤其适合对安全性和扩展性要求高的开发者", 112 | "category": "plugin", 113 | "tags": [ 114 | "hot" 115 | ], 116 | "icon": "🔧", 117 | "view_count": 2, 118 | "created_at": "2024-12-01T00:00:00Z", 119 | "is_featured": false, 120 | "id": 9, 121 | "identifier": "continue" 122 | }, 123 | { 124 | "name": "Comate", 125 | "url": "https://comate.baidu.com/zh", 126 | "description": "百度的 AI 编程插件", 127 | "category": "plugin", 128 | "tags": [], 129 | "icon": "🔧", 130 | "view_count": 0, 131 | "created_at": "2024-12-01T00:00:00Z", 132 | "is_featured": false, 133 | "id": 10, 134 | "identifier": "comate" 135 | }, 136 | { 137 | "name": "Codium", 138 | "url": "https://www.codium.ai/", 139 | "description": "AI 辅助代码生成和优化", 140 | "category": "plugin", 141 | "tags": [], 142 | "icon": "🔧", 143 | "view_count": 1, 144 | "created_at": "2024-12-01T00:00:00Z", 145 | "is_featured": false, 146 | "id": 11, 147 | "identifier": "codium" 148 | }, 149 | { 150 | "name": "CodeBuddy", 151 | "url": "https://copilot.tencent.com/", 152 | "description": "腾讯云代码助手,兼容 Visual Studio Code、Visual Studio、JetBrains IDEs 等主流编程工具, 为你提供高效、流畅的智能编码体验", 153 | "category": "plugin", 154 | "tags": [], 155 | "icon": "🔧", 156 | "view_count": 2, 157 | "created_at": "2025-06-21T00:00:00Z", 158 | "is_featured": false, 159 | "id": 12, 160 | "identifier": "codebuddy" 161 | }, 162 | { 163 | "name": "Cline", 164 | "url": "https://github.com/cline/cline", 165 | "description": "Cline是一款开源的VS Code AI编程助手,通过多模型支持(如DeepSeek、Claude等)实现智能代码生成、错误修复及终端命令执行,其核心特点是灵活可控的AI协作:所有操作需用户确认确保安全,支持本地模型降低成本,并能通过无头浏览器等扩展功能处理复杂任务,区别于收费工具(如Cursor)的封闭性", 166 | "category": "plugin", 167 | "tags": [ 168 | "hot" 169 | ], 170 | "icon": "🔧", 171 | "view_count": 0, 172 | "created_at": "2024-12-01T00:00:00Z", 173 | "is_featured": false, 174 | "id": 13, 175 | "identifier": "cline" 176 | }, 177 | { 178 | "name": "Augment", 179 | "url": "https://www.augmentcode.com/", 180 | "description": "Augment 是一个 AI 驱动的开发辅助工具,能在 IDE 中为代码提供实时解释、上下文搜索、调试建议和架构可视化,帮助开发者更快理解和维护大型代码库。", 181 | "category": "plugin", 182 | "tags": [ 183 | "hot" 184 | ], 185 | "icon": "🔧", 186 | "view_count": 0, 187 | "created_at": "2024-12-07T00:00:00Z", 188 | "is_featured": false, 189 | "id": 14, 190 | "identifier": "augment" 191 | } 192 | ] -------------------------------------------------------------------------------- /scripts/import_prompts_from_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 从文本文件中提取提示词并添加到 prompts.json 5 | 支持多种格式的文本输入 6 | """ 7 | 8 | import json 9 | import re 10 | from pathlib import Path 11 | from datetime import datetime 12 | from typing import List, Dict, Optional 13 | 14 | 15 | def extract_prompts_from_text(text: str) -> List[Dict]: 16 | """ 17 | 从文本中提取提示词 18 | 支持多种格式: 19 | 1. 以标题开头,内容在代码块中的格式 20 | 2. 以 # Role、# Goal 等标记的格式 21 | 3. 简单的标题+内容格式 22 | """ 23 | prompts = [] 24 | 25 | # 按章节分割(根据常见的章节标记) 26 | sections = re.split(r'(?:^|\n)(?:#{1,3}\s+|Part\d+[::]\s*|第[一二三四五六七八九十]+[部分章]|##\s+)', text, flags=re.MULTILINE) 27 | 28 | current_title = None 29 | current_content = [] 30 | 31 | for section in sections: 32 | section = section.strip() 33 | if not section: 34 | continue 35 | 36 | # 检查是否是标题行 37 | lines = section.split('\n') 38 | first_line = lines[0].strip() 39 | 40 | # 如果是明显的标题(短行,不含代码块标记) 41 | if len(first_line) < 100 and not first_line.startswith('```'): 42 | # 保存上一个提示词 43 | if current_title and current_content: 44 | prompts.append({ 45 | 'title': current_title, 46 | 'content': '\n'.join(current_content).strip() 47 | }) 48 | 49 | current_title = first_line 50 | current_content = lines[1:] if len(lines) > 1 else [] 51 | else: 52 | # 继续添加到当前内容 53 | current_content.extend(lines) 54 | 55 | # 添加最后一个提示词 56 | if current_title and current_content: 57 | prompts.append({ 58 | 'title': current_title, 59 | 'content': '\n'.join(current_content).strip() 60 | }) 61 | 62 | return prompts 63 | 64 | 65 | def create_prompt_entry(title: str, content: str, base_id: int, 66 | category: str = "代码", 67 | tags: Optional[List[str]] = None, 68 | url: str = "https://superhuang.feishu.cn/wiki/W1LCwYA8eiTl77kpi81c31yNnJd") -> Dict: 69 | """创建标准化的提示词条目""" 70 | 71 | # 生成标识符 72 | identifier = re.sub(r'[^\w\s-]', '', title.lower()) 73 | identifier = re.sub(r'[-\s]+', '-', identifier)[:50] 74 | 75 | # 生成描述(取内容的前200字符) 76 | description = content[:200].replace('\n', ' ').strip() 77 | if len(content) > 200: 78 | description += '...' 79 | 80 | return { 81 | "name": title, 82 | "description": description, 83 | "category": category, 84 | "tags": tags or [], 85 | "author": "", 86 | "url": url, 87 | "content": content, 88 | "view_count": 0, 89 | "created_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), 90 | "is_featured": False, 91 | "id": base_id, 92 | "identifier": identifier 93 | } 94 | 95 | 96 | def import_prompts_to_json(text_file: Path, prompts_json: Path, 97 | start_id: Optional[int] = None): 98 | """ 99 | 从文本文件导入提示词到JSON文件 100 | """ 101 | # 读取现有提示词 102 | if prompts_json.exists(): 103 | with prompts_json.open('r', encoding='utf-8') as f: 104 | existing_prompts = json.load(f) 105 | max_id = max((p.get('id', 0) for p in existing_prompts), default=0) 106 | next_id = max_id + 1 107 | else: 108 | existing_prompts = [] 109 | next_id = start_id or 1 110 | 111 | # 读取文本文件 112 | with text_file.open('r', encoding='utf-8') as f: 113 | text_content = f.read() 114 | 115 | # 提取提示词 116 | extracted = extract_prompts_from_text(text_content) 117 | 118 | print(f"从文本中提取了 {len(extracted)} 个提示词片段") 119 | 120 | # 转换为标准格式 121 | new_prompts = [] 122 | for idx, item in enumerate(extracted): 123 | prompt_entry = create_prompt_entry( 124 | title=item['title'], 125 | content=item['content'], 126 | base_id=next_id + idx 127 | ) 128 | new_prompts.append(prompt_entry) 129 | print(f" - {prompt_entry['name']} (ID: {prompt_entry['id']})") 130 | 131 | # 合并到现有提示词 132 | all_prompts = existing_prompts + new_prompts 133 | 134 | # 保存 135 | with prompts_json.open('w', encoding='utf-8') as f: 136 | json.dump(all_prompts, f, ensure_ascii=False, indent=2) 137 | 138 | print(f"\n成功添加 {len(new_prompts)} 个提示词到 {prompts_json}") 139 | print(f"总提示词数: {len(all_prompts)}") 140 | 141 | 142 | def manual_add_prompts(): 143 | """手动添加提示词的辅助函数""" 144 | prompts_data = [ 145 | { 146 | "title": "06 必备的Windsurf技巧", 147 | "description": "Windsurf IDE 使用技巧和最佳实践", 148 | "category": "代码", 149 | "tags": ["Windsurf", "IDE", "技巧"], 150 | }, 151 | { 152 | "title": "使用AI IDE进行创作", 153 | "description": "如何利用AI IDE进行内容创作", 154 | "category": "代码", 155 | "tags": ["AI IDE", "创作"], 156 | }, 157 | { 158 | "title": "改写提示词", 159 | "description": "提示词改写技巧和方法", 160 | "category": "代码", 161 | "tags": ["提示词", "改写"], 162 | }, 163 | { 164 | "title": "使用提示词生成精美图片", 165 | "description": "使用AI提示词生成图片的方法", 166 | "category": "代码", 167 | "tags": ["提示词", "图片生成"], 168 | }, 169 | { 170 | "title": "图片字幕生成器", 171 | "description": "开发图片字幕生成器的提示词和规则", 172 | "category": "代码", 173 | "tags": ["图片", "字幕", "生成器"], 174 | }, 175 | ] 176 | 177 | return prompts_data 178 | 179 | 180 | if __name__ == "__main__": 181 | import sys 182 | 183 | if len(sys.argv) < 2: 184 | print("用法: python import_prompts_from_text.py <文本文件路径>") 185 | print("\n或者直接运行脚本查看手动添加提示词的示例") 186 | sys.exit(1) 187 | 188 | text_file = Path(sys.argv[1]) 189 | prompts_json = Path(__file__).parent.parent / "data" / "prompts" / "prompts.json" 190 | 191 | if not text_file.exists(): 192 | print(f"错误: 文件 {text_file} 不存在") 193 | sys.exit(1) 194 | 195 | import_prompts_to_json(text_file, prompts_json) 196 | 197 | -------------------------------------------------------------------------------- /tests/test_data_loader.py: -------------------------------------------------------------------------------- 1 | """数据加载器测试""" 2 | import json 3 | import tempfile 4 | from pathlib import Path 5 | from unittest.mock import patch, MagicMock 6 | 7 | import pytest 8 | 9 | from app.services.data_loader import DataLoader 10 | 11 | 12 | class TestDataLoader: 13 | """数据加载器测试类""" 14 | 15 | def test_is_article_archived_excludes_article_pool(self, tmp_path): 16 | """测试归档状态检查排除文章池文件""" 17 | # 创建临时目录结构 18 | articles_dir = tmp_path / "articles" 19 | articles_dir.mkdir() 20 | 21 | # 创建文章池文件(不应该被检查) 22 | article_pool_file = articles_dir / "ai_articles.json" 23 | article_pool_data = [ 24 | { 25 | "title": "测试文章1", 26 | "url": "https://example.com/article1", 27 | "source": "测试来源", 28 | "summary": "测试摘要" 29 | } 30 | ] 31 | with open(article_pool_file, 'w', encoding='utf-8') as f: 32 | json.dump(article_pool_data, f, ensure_ascii=False) 33 | 34 | # 创建归档文件(应该被检查) 35 | archived_file = articles_dir / "programming.json" 36 | archived_data = [ 37 | { 38 | "id": 1, 39 | "title": "已归档文章", 40 | "url": "https://example.com/archived", 41 | "source": "来源", 42 | "summary": "摘要", 43 | "archived_at": "2025-01-01T00:00:00Z" 44 | } 45 | ] 46 | with open(archived_file, 'w', encoding='utf-8') as f: 47 | json.dump(archived_data, f, ensure_ascii=False) 48 | 49 | # 创建候选池文件(不应该被检查) 50 | candidate_file = articles_dir / "ai_candidates.json" 51 | candidate_data = [ 52 | { 53 | "title": "候选文章", 54 | "url": "https://example.com/candidate", 55 | "source": "来源", 56 | "summary": "摘要" 57 | } 58 | ] 59 | with open(candidate_file, 'w', encoding='utf-8') as f: 60 | json.dump(candidate_data, f, ensure_ascii=False) 61 | 62 | # 使用patch替换ARTICLES_DIR 63 | with patch('app.services.data_loader.ARTICLES_DIR', articles_dir): 64 | # 测试:文章池中的文章不应该被识别为已归档 65 | assert DataLoader.is_article_archived("https://example.com/article1") == False 66 | 67 | # 测试:归档文件中的文章应该被识别为已归档 68 | assert DataLoader.is_article_archived("https://example.com/archived") == True 69 | 70 | # 测试:候选池中的文章不应该被识别为已归档 71 | assert DataLoader.is_article_archived("https://example.com/candidate") == False 72 | 73 | def test_is_article_archived_exact_match(self, tmp_path): 74 | """测试归档状态检查的精确匹配""" 75 | articles_dir = tmp_path / "articles" 76 | articles_dir.mkdir() 77 | 78 | archived_file = articles_dir / "programming.json" 79 | archived_data = [ 80 | { 81 | "id": 1, 82 | "title": "测试文章", 83 | "url": "https://example.com/test", 84 | "source": "来源", 85 | "summary": "摘要", 86 | "archived_at": "2025-01-01T00:00:00Z" 87 | } 88 | ] 89 | with open(archived_file, 'w', encoding='utf-8') as f: 90 | json.dump(archived_data, f, ensure_ascii=False) 91 | 92 | with patch('app.services.data_loader.ARTICLES_DIR', articles_dir): 93 | # 精确匹配 94 | assert DataLoader.is_article_archived("https://example.com/test") == True 95 | # 不匹配 96 | assert DataLoader.is_article_archived("https://example.com/other") == False 97 | 98 | def test_is_article_archived_empty_url(self): 99 | """测试空URL的处理""" 100 | assert DataLoader.is_article_archived("") == False 101 | assert DataLoader.is_article_archived(None) == False 102 | 103 | def test_archive_article_to_category(self, tmp_path): 104 | """测试归档文章到分类""" 105 | articles_dir = tmp_path / "articles" 106 | articles_dir.mkdir() 107 | 108 | category_file = articles_dir / "programming.json" 109 | # 初始为空列表 110 | with open(category_file, 'w', encoding='utf-8') as f: 111 | json.dump([], f, ensure_ascii=False) 112 | 113 | article = { 114 | "title": "测试文章", 115 | "url": "https://example.com/test", 116 | "source": "来源", 117 | "summary": "摘要" 118 | } 119 | 120 | with patch('app.services.data_loader.ARTICLES_DIR', articles_dir): 121 | result = DataLoader.archive_article_to_category(article, "programming", []) 122 | assert result == True 123 | 124 | # 验证文件已更新 125 | with open(category_file, 'r', encoding='utf-8') as f: 126 | data = json.load(f) 127 | assert len(data) == 1 128 | assert data[0]["url"] == "https://example.com/test" 129 | assert data[0]["category"] == "programming" 130 | assert "archived_at" in data[0] 131 | 132 | def test_archive_article_duplicate(self, tmp_path): 133 | """测试归档重复文章""" 134 | articles_dir = tmp_path / "articles" 135 | articles_dir.mkdir() 136 | 137 | category_file = articles_dir / "programming.json" 138 | existing_data = [ 139 | { 140 | "id": 1, 141 | "title": "已存在", 142 | "url": "https://example.com/test", 143 | "source": "来源", 144 | "summary": "摘要", 145 | "archived_at": "2025-01-01T00:00:00Z" 146 | } 147 | ] 148 | with open(category_file, 'w', encoding='utf-8') as f: 149 | json.dump(existing_data, f, ensure_ascii=False) 150 | 151 | article = { 152 | "title": "新文章", 153 | "url": "https://example.com/test", # 相同的URL 154 | "source": "来源", 155 | "summary": "摘要" 156 | } 157 | 158 | with patch('app.services.data_loader.ARTICLES_DIR', articles_dir): 159 | result = DataLoader.archive_article_to_category(article, "programming", []) 160 | assert result == False # 应该返回False,因为已存在 161 | 162 | -------------------------------------------------------------------------------- /data/prompts/prompts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "AI编程工具中文提示词合集", 4 | "description": "AI编程工具中文提示词合集,包含Cursor、Devin、VSCode Agent等多种AI编程工具的提示词,为中文开发者提供AI辅助编程参考资源。持续更新中文编程Rules和最新AI编程提示词。", 5 | "category": "编程", 6 | "tags": ["AI编程", "提示词", "中文", "Cursor", "Devin", "VSCode"], 7 | "author": "", 8 | "url": "https://github.com/IsHexx/system-prompts-and-models-of-ai-tools-chinese", 9 | "content": "# AI编程工具中文提示词合集\n\n这是一个专门为中文开发者打造的AI编程工具提示词资源库,包含了多种主流AI编程工具的系统提示词和模型设计文档。\n\n## 项目特色\n\n- **全面覆盖**: 包含Cursor、Devin、VSCode Agent、Windsurf等多种AI编程工具\n- **中文优化**: 所有提示词都经过中文环境优化,更适合中文开发者的使用习惯\n- **持续更新**: 定期更新最新的AI编程提示词和中文编程Rules\n- **实践导向**: 提供实际可用的提示词模板和最佳实践\n\n## 包含工具\n\n- Cursor Prompts - Cursor编辑器的提示词\n- Devin AI - Devin AI的系统提示词\n- VSCode Agent - VSCode Agent的相关文档\n- Windsurf - Windsurf相关文档\n- Claude Code - Claude Code提示词\n- Trae - Trae AI工具提示词\n- 以及更多AI编程工具\n\n## 使用方法\n\n1. 访问GitHub仓库: https://github.com/IsHexx/system-prompts-and-models-of-ai-tools-chinese\n2. 根据您的开发环境选择对应的工具文件夹\n3. 复制相应的提示词到您的AI编程工具中\n4. 根据实际需求调整和优化提示词\n\n## 贡献指南\n\n欢迎提交更多AI工具的中文提示词,共同完善这个资源库。\n\n## 许可证\n\n采用MIT许可证开源。", 10 | "copy_count": 0, 11 | "created_at": "2025-12-03T23:30:00.000000Z", 12 | "is_featured": true, 13 | "id": 1, 14 | "identifier": "system-prompts-and-models-of-ai-tools-chinese" 15 | }, 16 | { 17 | "name": "AI-IDE-Agent", 18 | "description": "AI-IDE-Agent提供了Claude、Cursor、Trae等AI IDE智能体的提示词合集,涵盖61个专业领域,旨在帮助开发者更高效地利用AI工具进行编程开发。", 19 | "category": "编程", 20 | "tags": ["AI编程", "提示词", "IDE", "Cursor", "Claude", "Trae"], 21 | "author": "", 22 | "url": "https://github.com/bjlida/AI-IDE-Agent", 23 | "content": "# AI-IDE-Agent\n\nAI-IDE-Agent是一个专注于AI集成开发环境的提示词资源库,为开发者提供Claude、Cursor、Trae等主流AI IDE智能体的提示词合集。\n\n## 项目特色\n\n- **全面覆盖**: 涵盖61个专业领域的提示词\n- **多工具支持**: 支持Claude、Cursor、Trae等多种AI IDE工具\n- **专业领域**: 覆盖多个专业领域的开发场景\n- **实用导向**: 提供可直接使用的提示词模板\n\n## 包含内容\n\n- Claude IDE提示词\n- Cursor编辑器提示词\n- Trae AI工具提示词\n- 61个专业领域的提示词集合\n\n## 使用方法\n\n1. 访问GitHub仓库: https://github.com/bjlida/AI-IDE-Agent\n2. 根据您使用的AI IDE工具选择对应的提示词\n3. 根据您的专业领域选择相应的提示词模板\n4. 复制并应用到您的开发环境中\n\n## 适用场景\n\n- 代码生成和优化\n- 代码审查和重构\n- 问题诊断和调试\n- 文档生成\n- 测试编写\n\n## 许可证\n\n请查看GitHub仓库了解具体许可证信息。", 24 | "copy_count": 0, 25 | "created_at": "2025-12-04T00:00:00.000000Z", 26 | "is_featured": false, 27 | "id": 2, 28 | "identifier": "ai-ide-agent" 29 | }, 30 | { 31 | "name": "Prompt Optimizer", 32 | "description": "Prompt Optimizer是一款开源的提示词优化器,支持一键优化提示词,提供多轮迭代改进测试和版本回溯功能。支持主流AI模型,完全开源,纯客户端处理,保障数据安全。", 33 | "category": "工具", 34 | "tags": ["提示词", "优化", "工具", "开源", "AI编程"], 35 | "author": "", 36 | "url": "https://github.com/linshenkx/prompt-optimizer", 37 | "content": "# Prompt Optimizer\n\nPrompt Optimizer是一款专业的提示词优化工具,旨在帮助用户编写高质量的提示词,从而提升AI输出的质量。\n\n## 核心功能\n\n- **一键优化**: 快速优化提示词,提升AI输出质量\n- **多轮迭代**: 支持多轮迭代改进测试,持续优化提示词\n- **版本回溯**: 支持版本回溯,方便对比不同版本的优化效果\n- **多模型支持**: 支持OpenAI、Gemini、DeepSeek等主流AI模型\n- **数据安全**: 完全开源,纯客户端处理,保障数据安全\n\n## 使用方式\n\n- **Web应用**: 在线使用,无需安装\n- **桌面应用**: 提供桌面版本,支持离线使用\n- **Chrome插件**: 浏览器插件,方便快速调用\n- **Docker部署**: 支持Docker部署,适合企业使用\n\n## 技术特点\n\n- **无跨域限制**: 支持Model Context Protocol (MCP) 协议\n- **自动更新**: 支持自动更新功能\n- **独立运行**: 可独立运行,不依赖外部服务\n\n## 使用方法\n\n1. 访问GitHub仓库: https://github.com/linshenkx/prompt-optimizer\n2. 选择适合您的使用方式(Web/桌面/插件/Docker)\n3. 输入您的提示词\n4. 使用优化功能提升提示词质量\n5. 对比不同版本的优化效果\n\n## 适用场景\n\n- 提示词优化和测试\n- AI对话效果提升\n- 提示词版本管理\n- 团队协作开发\n\n## 许可证\n\n请查看GitHub仓库了解具体许可证信息。", 38 | "copy_count": 0, 39 | "created_at": "2025-12-04T00:00:00.000000Z", 40 | "is_featured": false, 41 | "id": 3, 42 | "identifier": "prompt-optimizer" 43 | }, 44 | { 45 | "name": "OpenPromptStudio", 46 | "description": "OpenPromptStudio是一个AIGC提示词可视化编辑器,提供直观的界面,帮助用户创建和管理提示词,提升AI生成内容的质量和多样性。", 47 | "category": "工具", 48 | "tags": ["提示词", "可视化", "编辑器", "AIGC", "工具"], 49 | "author": "", 50 | "url": "https://github.com/Moonvy/OpenPromptStudio", 51 | "content": "# OpenPromptStudio\n\nOpenPromptStudio是一个专业的AIGC提示词可视化编辑器,旨在帮助用户更高效地创建、管理和优化提示词。\n\n## 项目特色\n\n- **可视化编辑**: 提供直观的可视化界面,方便编辑和管理提示词\n- **AIGC支持**: 专注于AIGC(AI生成内容)场景的提示词编辑\n- **内容质量**: 帮助提升AI生成内容的质量和多样性\n- **用户友好**: 简洁易用的界面设计\n\n## 核心功能\n\n- 提示词可视化编辑\n- 提示词模板管理\n- 提示词效果预览\n- 提示词优化建议\n- 多场景支持\n\n## 使用方法\n\n1. 访问GitHub仓库: https://github.com/Moonvy/OpenPromptStudio\n2. 下载并安装工具\n3. 使用可视化界面创建和编辑提示词\n4. 预览和测试提示词效果\n5. 保存和管理您的提示词库\n\n## 适用场景\n\n- AI内容生成\n- 提示词设计和优化\n- 提示词模板管理\n- 团队协作开发\n\n## 许可证\n\n请查看GitHub仓库了解具体许可证信息。", 52 | "copy_count": 0, 53 | "created_at": "2025-12-04T00:00:00.000000Z", 54 | "is_featured": false, 55 | "id": 4, 56 | "identifier": "openpromptstudio" 57 | }, 58 | { 59 | "name": "飞书AI提示词表格", 60 | "description": "飞书AI表格工具,提供AI提示词相关的资源和模板,帮助用户在表格中更高效地使用AI功能。", 61 | "category": "资源", 62 | "tags": ["提示词", "飞书", "表格", "AI", "资源"], 63 | "author": "", 64 | "url": "https://ai.feishu.cn/sheets/shtcnMklYu0WsXEDUXXanrSEB2m", 65 | "content": "# 飞书AI提示词表格\n\n飞书AI表格是一个集成了AI功能的表格工具,提供丰富的AI提示词资源和模板,帮助用户在表格场景中更高效地使用AI。\n\n## 功能特点\n\n- **AI集成**: 在表格中直接使用AI功能\n- **提示词资源**: 提供丰富的提示词模板和资源\n- **高效协作**: 支持团队协作和共享\n- **场景应用**: 适用于多种业务场景\n\n## 使用方法\n\n1. 访问飞书AI表格: https://ai.feishu.cn/sheets/shtcnMklYu0WsXEDUXXanrSEB2m\n2. 浏览和搜索提示词资源\n3. 复制适合的提示词模板\n4. 在您的表格中使用AI功能\n5. 根据实际需求调整提示词\n\n## 适用场景\n\n- 数据分析和处理\n- 内容生成和编辑\n- 表格数据智能处理\n- 团队协作和知识管理\n\n## 注意事项\n\n- 需要飞书账号才能访问\n- 部分功能可能需要相应权限\n- 建议根据实际业务场景选择合适的提示词", 66 | "copy_count": 0, 67 | "created_at": "2025-12-04T00:00:00.000000Z", 68 | "is_featured": false, 69 | "id": 5, 70 | "identifier": "feishu-ai-prompt-sheets" 71 | }, 72 | { 73 | "name": "HelloAgents", 74 | "description": "HelloAgents提供了面向AI编程智能体的\"轻量路由 + 多阶段 + 知识库驱动\"规则集(系统全局提示词),旨在将混乱的AI智能体输出转化为结构化、可追踪且适用于生产环境的代码。", 75 | "category": "编程", 76 | "tags": ["AI编程", "智能体", "规则集", "提示词", "Agent"], 77 | "author": "", 78 | "url": "https://github.com/hellowind777/helloagents", 79 | "content": "# HelloAgents\n\nHelloAgents是一个专注于AI编程智能体的规则集项目,提供系统全局提示词,帮助开发者构建更可靠的AI智能体应用。\n\n## 项目特色\n\n- **轻量路由**: 提供轻量级的智能体路由机制\n- **多阶段处理**: 支持多阶段的任务处理流程\n- **知识库驱动**: 基于知识库的智能体决策\n- **结构化输出**: 将混乱的AI输出转化为结构化代码\n- **生产就绪**: 适用于生产环境的代码生成\n\n## 核心功能\n\n- 系统全局提示词规则集\n- 智能体路由和调度\n- 多阶段任务处理\n- 知识库集成\n- 代码结构化和追踪\n\n## 架构特点\n\n- **轻量级**: 简洁的架构设计,易于集成\n- **可追踪**: 支持任务执行过程的追踪和调试\n- **可扩展**: 灵活的扩展机制,适应不同场景\n- **生产级**: 适用于生产环境的稳定性和可靠性\n\n## 使用方法\n\n1. 访问GitHub仓库: https://github.com/hellowind777/helloagents\n2. 查看项目文档和示例\n3. 集成规则集到您的AI智能体项目\n4. 根据实际需求调整和优化规则\n5. 测试和验证智能体行为\n\n## 适用场景\n\n- AI智能体开发\n- 代码生成和优化\n- 任务自动化\n- 知识库驱动的智能应用\n- 生产环境部署\n\n## 许可证\n\n请查看GitHub仓库了解具体许可证信息。", 80 | "copy_count": 0, 81 | "created_at": "2025-12-04T00:00:00.000000Z", 82 | "is_featured": false, 83 | "id": 6, 84 | "identifier": "helloagents" 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /docs/feature/multi_sources_guide.md: -------------------------------------------------------------------------------- 1 | # 多资讯源支持使用指南 2 | 3 | ## 什么是多资讯源支持? 4 | 5 | 多资讯源支持允许系统从**多个不同的资讯来源**自动抓取文章,而不仅仅局限于搜狗微信搜索。这样可以: 6 | 7 | 1. **扩大内容覆盖面**:从更多渠道获取资讯 8 | 2. **提高内容质量**:不同来源的文章质量不同,可以筛选优质内容 9 | 3. **减少单一依赖**:不依赖单一平台,提高系统稳定性 10 | 4. **智能排序**:根据热度分自动排序,优先展示高质量内容 11 | 12 | ## 支持的资讯源 13 | 14 | ### 1. 搜狗微信搜索(原有功能) 15 | - **用途**:搜索微信公众号文章 16 | - **配置**:在"关键词配置"中设置关键词 17 | - **特点**:中文内容为主,适合国内资讯 18 | 19 | ### 2. RSS/Atom Feed 20 | - **用途**:抓取 RSS 订阅源的文章 21 | - **配置**:添加 RSS Feed URL 22 | - **特点**:支持各种新闻网站、博客、技术网站 23 | - **示例**: 24 | - CNN: `https://rss.cnn.com/rss/edition.rss` 25 | - BBC News: `https://feeds.bbci.co.uk/news/rss.xml` 26 | - TechCrunch: `https://techcrunch.com/feed/` 27 | 28 | ### 3. GitHub Trending 29 | - **用途**:抓取 GitHub 热门项目 30 | - **配置**:指定编程语言(如 python, javascript, go) 31 | - **特点**:技术项目为主,适合开发者 32 | - **示例语言**:python, javascript, go, rust, java 33 | 34 | ### 4. Hacker News 35 | - **用途**:抓取 Hacker News 高分文章 36 | - **配置**:设置最低分数阈值 37 | - **特点**:高质量技术文章和新闻 38 | - **推荐阈值**:50-200 points 39 | 40 | ## 如何使用 41 | 42 | ### 方法一:通过代码直接使用(推荐用于集成) 43 | 44 | 在代码中调用统一资讯源管理器: 45 | 46 | ```python 47 | from app.sources.article_sources import fetch_from_all_sources 48 | 49 | # 从所有配置的资讯源抓取文章 50 | articles = await fetch_from_all_sources( 51 | keywords=["AI", "Python"], # 搜狗微信搜索关键词 52 | rss_feeds=[ 53 | "https://rss.cnn.com/rss/edition.rss", 54 | "https://techcrunch.com/feed/", 55 | ], 56 | github_languages=["python", "javascript"], 57 | hackernews_min_points=100, 58 | max_per_source=5, # 每个源最多抓取5篇 59 | ) 60 | 61 | # 文章已按热度分自动排序 62 | for article in articles: 63 | print(f"[{article['score']:.1f}分] {article['title']}") 64 | print(f" 来源: {article['source']}") 65 | print(f" 链接: {article['url']}") 66 | ``` 67 | 68 | ### 方法二:通过 API 接口使用 69 | 70 | #### 1. 测试单个资讯源 71 | 72 | ```bash 73 | # 测试 RSS Feed 74 | curl -X POST "http://localhost:8000/digest/test/rss" \ 75 | -H "X-Admin-Code: your-admin-code" \ 76 | -H "Content-Type: application/json" \ 77 | -d '{"feed_url": "https://rss.cnn.com/rss/edition.rss"}' 78 | 79 | # 测试 GitHub Trending 80 | curl -X POST "http://localhost:8000/digest/test/github-trending" \ 81 | -H "X-Admin-Code: your-admin-code" \ 82 | -H "Content-Type: application/json" \ 83 | -d '{"language": "python"}' 84 | 85 | # 测试 Hacker News 86 | curl -X POST "http://localhost:8000/digest/test/hackernews" \ 87 | -H "X-Admin-Code: your-admin-code" \ 88 | -H "Content-Type: application/json" \ 89 | -d '{"min_points": 100}' 90 | ``` 91 | 92 | #### 2. 测试所有资讯源 93 | 94 | ```bash 95 | curl -X POST "http://localhost:8000/digest/test/all-sources" \ 96 | -H "X-Admin-Code: your-admin-code" \ 97 | -H "Content-Type: application/json" \ 98 | -d '{ 99 | "keywords": ["AI", "Python"], 100 | "rss_feeds": [ 101 | "https://rss.cnn.com/rss/edition.rss", 102 | "https://techcrunch.com/feed/" 103 | ], 104 | "github_languages": ["python", "javascript"], 105 | "hackernews_min_points": 100, 106 | "max_per_source": 5 107 | }' 108 | ``` 109 | 110 | ### 方法三:集成到现有抓取流程 111 | 112 | 修改 `app/main.py` 或抓取任务,使用多资讯源: 113 | 114 | ```python 115 | from app.sources.article_sources import fetch_from_all_sources 116 | from app.config_loader import load_crawler_keywords 117 | 118 | async def crawl_articles(): 119 | # 获取配置的关键词 120 | keywords = load_crawler_keywords() 121 | 122 | # 从所有资讯源抓取 123 | articles = await fetch_from_all_sources( 124 | keywords=keywords, 125 | rss_feeds=[ 126 | "https://rss.cnn.com/rss/edition.rss", 127 | # 添加更多 RSS Feed 128 | ], 129 | github_languages=["python"], 130 | hackernews_min_points=100, 131 | max_per_source=5, 132 | ) 133 | 134 | # 将抓取的文章添加到候选池 135 | for article in articles: 136 | # 添加到候选池的逻辑 137 | add_to_candidate_pool(article) 138 | ``` 139 | 140 | ## 热度分计算说明 141 | 142 | 系统会自动为每篇文章计算热度分,考虑因素: 143 | 144 | 1. **来源权重**: 145 | - Hacker News: 分数 × 0.1(高分文章得分更高) 146 | - GitHub Trending: +50 基础分 147 | - RSS Feed: +30 基础分 148 | - 其他来源: +20 基础分 149 | 150 | 2. **时效性**: 151 | - 今天发布的文章: +30 分 152 | 153 | 3. **内容质量**: 154 | - 标题长度适中(20-60字符): +10 分 155 | - 有摘要: +5 分 156 | 157 | 文章会按热度分从高到低自动排序。 158 | 159 | ## 实际应用场景 160 | 161 | ### 场景一:技术资讯聚合 162 | 163 | ```python 164 | articles = await fetch_from_all_sources( 165 | keywords=["AI", "机器学习"], 166 | rss_feeds=[ 167 | "https://techcrunch.com/feed/", 168 | "https://www.theverge.com/rss/index.xml", 169 | ], 170 | github_languages=["python", "javascript", "go"], 171 | hackernews_min_points=100, 172 | max_per_source=5, 173 | ) 174 | ``` 175 | 176 | **效果**:从技术网站、GitHub 热门项目、Hacker News 高分文章等多个来源抓取,获得全面的技术资讯。 177 | 178 | ### 场景二:新闻资讯聚合 179 | 180 | ```python 181 | articles = await fetch_from_all_sources( 182 | keywords=["科技", "AI"], 183 | rss_feeds=[ 184 | "https://rss.cnn.com/rss/edition.rss", 185 | "https://feeds.bbci.co.uk/news/rss.xml", 186 | "https://www.reuters.com/rssFeed/technologyNews", 187 | ], 188 | max_per_source=10, 189 | ) 190 | ``` 191 | 192 | **效果**:从多个新闻源抓取,获得更全面的新闻覆盖。 193 | 194 | ### 场景三:开发者日报 195 | 196 | ```python 197 | articles = await fetch_from_all_sources( 198 | github_languages=["python", "javascript", "rust", "go"], 199 | hackernews_min_points=50, 200 | max_per_source=10, 201 | ) 202 | ``` 203 | 204 | **效果**:专注于技术内容,从 GitHub 和 Hacker News 获取高质量技术文章。 205 | 206 | ## 配置建议 207 | 208 | ### RSS Feed 推荐 209 | 210 | **技术类**: 211 | - TechCrunch: `https://techcrunch.com/feed/` 212 | - The Verge: `https://www.theverge.com/rss/index.xml` 213 | - Hacker News RSS: `https://hnrss.org/frontpage` 214 | 215 | **新闻类**: 216 | - CNN: `https://rss.cnn.com/rss/edition.rss` 217 | - BBC News: `https://feeds.bbci.co.uk/news/rss.xml` 218 | - Reuters: `https://www.reuters.com/rssFeed/technologyNews` 219 | 220 | **中文类**: 221 | - 36氪: `https://36kr.com/feed` 222 | - 虎嗅: `https://www.huxiu.com/rss/0.xml` 223 | 224 | ### GitHub Trending 语言推荐 225 | 226 | - `python` - Python 项目 227 | - `javascript` - JavaScript 项目 228 | - `go` - Go 语言项目 229 | - `rust` - Rust 项目 230 | - `java` - Java 项目 231 | 232 | ### Hacker News 分数阈值 233 | 234 | - **低阈值(50)**:获取更多文章,但质量可能参差不齐 235 | - **中阈值(100)**:平衡质量和数量(推荐) 236 | - **高阈值(200)**:只获取高质量文章,但数量较少 237 | 238 | ## 注意事项 239 | 240 | 1. **请求频率**:避免过于频繁的请求,建议在抓取之间添加延迟 241 | 2. **网络稳定性**:某些 Feed 可能需要稳定的网络连接 242 | 3. **内容过滤**:抓取的文章可能需要进一步过滤,确保符合你的需求 243 | 4. **存储空间**:多资讯源可能产生大量文章,注意存储空间 244 | 245 | ## 下一步 246 | 247 | 1. **配置 RSS Feed**:在代码或配置文件中添加你感兴趣的 RSS Feed 248 | 2. **测试抓取**:使用测试脚本或 API 测试各个资讯源 249 | 3. **集成到系统**:将多资讯源集成到现有的抓取和推送流程中 250 | 4. **调整参数**:根据实际效果调整各源的抓取数量和分数阈值 251 | 252 | ## 相关文档 253 | 254 | - [测试指南](test_sources.md) - 如何测试多资讯源功能 255 | - [API 文档](../README.md) - 完整的 API 使用说明 256 | 257 | -------------------------------------------------------------------------------- /app/domain/sources/ai_candidates.py: -------------------------------------------------------------------------------- 1 | """ 2 | 管理待审核的文章候选池(`data/articles/ai_candidates.json`) 3 | """ 4 | import json 5 | import random 6 | from dataclasses import dataclass, asdict 7 | from pathlib import Path 8 | from typing import Dict, List 9 | 10 | from loguru import logger 11 | 12 | # 导入URL规范化函数 13 | from .article_crawler import normalize_weixin_url 14 | 15 | 16 | @dataclass 17 | class CandidateArticle: 18 | """待审核文章的数据结构""" 19 | title: str 20 | url: str 21 | source: str 22 | summary: str 23 | # 可以增加爬取时间、关键词等元数据 24 | crawled_from: str = "" 25 | 26 | 27 | def _candidate_data_path() -> Path: 28 | """获取候选池数据文件的路径""" 29 | return Path(__file__).resolve().parents[2] / "data" / "articles" / "ai_candidates.json" 30 | 31 | 32 | def load_candidate_pool() -> List[CandidateArticle]: 33 | """加载所有待审核的文章""" 34 | path = _candidate_data_path() 35 | if not path.exists(): 36 | return [] 37 | 38 | try: 39 | with path.open("r", encoding="utf-8") as f: 40 | raw_items = json.load(f) 41 | 42 | if not isinstance(raw_items, list): 43 | logger.warning(f"Candidate config is not a list, found {type(raw_items)}. Resetting.") 44 | return [] 45 | 46 | return [CandidateArticle(**item) for item in raw_items] 47 | except (json.JSONDecodeError, TypeError) as e: 48 | logger.error(f"Failed to load or parse candidate articles: {e}") 49 | return [] 50 | 51 | 52 | def save_candidate_pool(candidates: List[CandidateArticle]) -> bool: 53 | """将候选文章列表完整写入配置文件(覆盖)""" 54 | path = _candidate_data_path() 55 | logger.info(f"保存候选池到: {path}, 文章数量: {len(candidates)}") 56 | 57 | try: 58 | path.parent.mkdir(parents=True, exist_ok=True) 59 | 60 | # 规范化所有候选文章的URL(双重保险) 61 | normalized_candidates = [] 62 | for candidate in candidates: 63 | normalized_url = candidate.url 64 | if candidate.url and "mp.weixin.qq.com" in candidate.url: 65 | normalized_url = normalize_weixin_url(candidate.url) 66 | if normalized_url != candidate.url: 67 | logger.debug(f"候选池保存前规范化URL: {candidate.url[:60]}... -> {normalized_url[:60]}...") 68 | # 创建新的候选文章对象,使用规范化后的URL 69 | candidate = CandidateArticle( 70 | title=candidate.title, 71 | url=normalized_url, 72 | source=candidate.source, 73 | summary=candidate.summary, 74 | crawled_from=candidate.crawled_from, 75 | ) 76 | normalized_candidates.append(candidate) 77 | 78 | # 转换为字典列表 79 | candidates_dict = [asdict(c) for c in normalized_candidates] 80 | logger.debug(f"转换后的候选文章数据: {candidates_dict[:2] if len(candidates_dict) > 0 else '[]'}") # 只记录前2条 81 | 82 | with path.open("w", encoding="utf-8") as f: 83 | json.dump(candidates_dict, f, ensure_ascii=False, indent=2) 84 | 85 | # 验证文件是否成功写入 86 | if path.exists(): 87 | file_size = path.stat().st_size 88 | logger.info(f"候选池文件已保存,大小: {file_size} 字节") 89 | return True 90 | else: 91 | logger.error(f"候选池文件保存后不存在: {path}") 92 | return False 93 | except Exception as e: 94 | logger.error(f"保存候选池失败: {e}", exc_info=True) 95 | return False 96 | 97 | 98 | def add_candidates_to_pool(new_candidates: List[CandidateArticle], existing_urls: set) -> int: 99 | """ 100 | 将一批新抓取的文章添加到候选池,同时进行去重。 101 | 102 | Args: 103 | new_candidates: 新抓取的候选文章列表。 104 | existing_urls: 已存在于正式文章池和候选池中的所有 URL,用于去重。 105 | 106 | Returns: 107 | 成功添加的新文章数量。 108 | """ 109 | if not new_candidates: 110 | return 0 111 | 112 | current_candidates = load_candidate_pool() 113 | 114 | added_count = 0 115 | for candidate in new_candidates: 116 | # 规范化URL用于去重比较 117 | normalized_url = candidate.url 118 | if candidate.url and "mp.weixin.qq.com" in candidate.url: 119 | normalized_url = normalize_weixin_url(candidate.url) 120 | # 如果URL被规范化了,更新候选文章的URL 121 | if normalized_url != candidate.url: 122 | candidate = CandidateArticle( 123 | title=candidate.title, 124 | url=normalized_url, 125 | source=candidate.source, 126 | summary=candidate.summary, 127 | crawled_from=candidate.crawled_from, 128 | ) 129 | 130 | # 使用规范化后的URL进行去重检查 131 | if normalized_url not in existing_urls: 132 | current_candidates.append(candidate) 133 | existing_urls.add(normalized_url) # 使用规范化后的URL避免重复添加 134 | added_count += 1 135 | 136 | if added_count > 0: 137 | save_candidate_pool(current_candidates) 138 | logger.info(f"Added {added_count} new candidates to the pool.") 139 | else: 140 | logger.info("No new unique candidates to add.") 141 | 142 | return added_count 143 | 144 | 145 | def clear_candidate_pool() -> bool: 146 | """清空候选池""" 147 | return save_candidate_pool([]) 148 | 149 | 150 | def promote_candidates_to_articles(per_keyword: int = 2) -> int: 151 | """ 152 | 将候选池中的文章按关键词随机挑选若干篇,写入正式文章池。 153 | 每个关键词随机选 per_keyword 篇(不足则全取),剩余文章继续留在候选池。 154 | 返回写入正式文章池的文章数量。 155 | """ 156 | from .ai_articles import overwrite_articles 157 | 158 | if per_keyword <= 0: 159 | logger.warning("per_keyword <= 0, skip promoting candidates.") 160 | return 0 161 | 162 | candidates = load_candidate_pool() 163 | if not candidates: 164 | logger.info("Candidate pool is empty, nothing to promote.") 165 | return 0 166 | 167 | grouped: Dict[str, List[CandidateArticle]] = {} 168 | for candidate in candidates: 169 | parts = candidate.crawled_from.split(":", 1) 170 | keyword = parts[1] if len(parts) > 1 else "未知关键词" 171 | grouped.setdefault(keyword, []).append(candidate) 172 | 173 | selected: List[CandidateArticle] = [] 174 | remaining: List[CandidateArticle] = [] 175 | 176 | for keyword, items in grouped.items(): 177 | random.shuffle(items) 178 | take = items[:per_keyword] 179 | selected.extend(take) 180 | remaining.extend(items[per_keyword:]) 181 | 182 | if not selected: 183 | logger.info("No candidates selected for promotion.") 184 | return 0 185 | 186 | overwrite_articles([asdict(item) for item in selected]) 187 | save_candidate_pool(remaining) 188 | logger.info( 189 | f"Promoted {len(selected)} articles from candidates " 190 | f"(across {len(grouped)} keywords) into the main pool." 191 | ) 192 | return len(selected) 193 | 194 | --------------------------------------------------------------------------------