feat: GEO data model and API endpoints

- SearchRanking: +5 fields (ai_search_engine, citation_snippet, citation_url, geo_score, content_type)
- New GeoReadinessScore model (6-dimension scoring: schema/faq/howto/citations/word_count/readability/headings)
- GET /api/seo/geo/overview, GET /api/seo/geo/readiness, POST /api/seo/geo/track

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Yuzhiran Dev
2026-06-16 08:25:18 +08:00
parent 63a6fabc00
commit 773080cf4f
2 changed files with 132 additions and 3 deletions
+50 -2
View File
@@ -860,7 +860,7 @@ class CollectorSource(Base):
class SearchRanking(Base):
"""搜索排名追踪"""
"""搜索排名追踪 + AI 搜索引用追踪"""
__tablename__ = "search_rankings"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
@@ -872,7 +872,12 @@ class SearchRanking(Base):
position = Column(Integer, nullable=True) # 搜索排名位置(null = 未上榜)
url_found = Column(String, nullable=True) # 被找到的具体 URL
ai_cited = Column(Boolean, default=False) # 是否被 AI 搜索引用
ai_source = Column(String, nullable=True) # AI 搜索来源名称
ai_source = Column(String, nullable=True) # AI 搜索来源chatgpt/perplexity/deepseek
ai_search_engine = Column(String, nullable=True) # AI 搜索具体引擎模型名
citation_snippet = Column(String, nullable=True) # 被引用的文本片段
citation_url = Column(String, nullable=True) # 引用来源链接
geo_score = Column(Integer, nullable=True) # GEO 就绪度评分 0-100
content_type = Column(String, nullable=True) # 内容类型: article/listicle/howto/faq/review
checked_at = Column(DateTime(timezone=True), server_default=func.now())
def to_dict(self):
@@ -887,5 +892,48 @@ class SearchRanking(Base):
"url_found": self.url_found,
"ai_cited": self.ai_cited,
"ai_source": self.ai_source,
"ai_search_engine": self.ai_search_engine,
"citation_snippet": self.citation_snippet,
"citation_url": self.citation_url,
"geo_score": self.geo_score,
"content_type": self.content_type,
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
}
class GeoReadinessScore(Base):
"""GEO 就绪度评分 — 按文章评估被 AI 搜索引用的概率"""
__tablename__ = "geo_readiness_scores"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
article_id = Column(String, ForeignKey("articles.id"), nullable=True, index=True)
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
platform = Column(String, nullable=True)
total_score = Column(Integer, default=0) # 总分 0-100
has_schema = Column(Boolean, default=False) # 是否有结构化数据
schema_types = Column(String, nullable=True) # 含有的 schema 类型列表
has_faq_format = Column(Boolean, default=False) # 是否含 FAQ 格式
has_howto_format = Column(Boolean, default=False) # 是否含 HowTo 格式
has_citations = Column(Boolean, default=False) # 是否引用数据源
word_count = Column(Integer, default=0)
readability_score = Column(Integer, default=0) # 可读性评分 0-100
heading_structure_score = Column(Integer, default=0) # 标题结构评分 0-100
checked_at = Column(DateTime(timezone=True), server_default=func.now())
def to_dict(self):
return {
"id": self.id,
"article_id": self.article_id,
"topic_id": self.topic_id,
"platform": self.platform,
"total_score": self.total_score,
"has_schema": self.has_schema,
"schema_types": self.schema_types,
"has_faq_format": self.has_faq_format,
"has_howto_format": self.has_howto_format,
"has_citations": self.has_citations,
"word_count": self.word_count,
"readability_score": self.readability_score,
"heading_structure_score": self.heading_structure_score,
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
}