3fab87ee11
- search_utils.py: 新增 _call_360/_call_sogou/_call_wechat HTML爬取函数 - initial_data.py: 种子数据新增三个搜索提供商(priority 3/4/5) - models.py: provider_type 注释补充新类型 - admin.html: 搜索提供商类型下拉框新增三个选项 - AGENTS.md/PROGRESS.md/README.md: PostgreSQL 15→16 - README.md: 移除硬编码数据库密码
849 lines
34 KiB
Python
849 lines
34 KiB
Python
from sqlalchemy import Column, String, Integer, Float, Date, DateTime, Text, Boolean, JSON, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.sql import func
|
|
from sqlalchemy.orm import relationship
|
|
from .database import Base
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
class SearchProvider(Base):
|
|
__tablename__ = "search_providers"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, nullable=False, comment="显示名称")
|
|
provider_type = Column(String, nullable=False, comment="baidu / qiniu / tinyfish / bing / mcp / 360 / sogou / wechat")
|
|
api_key = Column(String, nullable=True, comment="API密钥")
|
|
api_url = Column(String, nullable=True, comment="API地址")
|
|
priority = Column(Integer, default=1, comment="优先级,越小越优先")
|
|
enabled = Column(Boolean, default=True)
|
|
daily_limit = Column(Integer, default=1500, comment="每日调用上限")
|
|
usage_today = Column(Integer, default=0, comment="当日已用次数")
|
|
console_url = Column(String, nullable=True, comment="官网控制台地址")
|
|
extra_config = Column(JSON, default=dict, comment="额外配置")
|
|
last_used_at = Column(DateTime(timezone=True), nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"provider_type": self.provider_type,
|
|
"api_key": self.api_key,
|
|
"api_url": self.api_url,
|
|
"console_url": self.console_url,
|
|
"priority": self.priority,
|
|
"enabled": self.enabled,
|
|
"daily_limit": self.daily_limit,
|
|
"usage_today": self.usage_today,
|
|
"extra_config": self.extra_config or {},
|
|
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class AuditLog(Base):
|
|
__tablename__ = "audit_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
user_id = Column(Integer, nullable=True, index=True)
|
|
username = Column(String, nullable=False)
|
|
action = Column(String, nullable=False, index=True)
|
|
resource_type = Column(String, nullable=True, index=True)
|
|
resource_id = Column(String, nullable=True)
|
|
details = Column(JSON, default=dict, nullable=True)
|
|
ip_address = Column(String, nullable=True)
|
|
user_agent = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"username": self.username,
|
|
"action": self.action,
|
|
"resource_type": self.resource_type,
|
|
"resource_id": self.resource_id,
|
|
"details": self.details or {},
|
|
"ip_address": self.ip_address,
|
|
"user_agent": self.user_agent,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
username = Column(String, unique=True, nullable=False, index=True)
|
|
password_hash = Column(String, nullable=False)
|
|
role = Column(String, default="user", nullable=False)
|
|
org_id = Column(String, default="default", nullable=True)
|
|
last_login = Column(DateTime(timezone=True), nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"username": self.username,
|
|
"role": self.role,
|
|
"org_id": self.org_id,
|
|
"last_login": self.last_login.isoformat() if self.last_login else None,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class Role(Base):
|
|
__tablename__ = "roles"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, unique=True, nullable=False)
|
|
description = Column(String, default="")
|
|
is_system = Column(Boolean, default=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"is_system": self.is_system,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class Menu(Base):
|
|
__tablename__ = "menus"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
parent_id = Column(Integer, ForeignKey("menus.id"), nullable=True)
|
|
name = Column(String, nullable=False)
|
|
path = Column(String, nullable=False)
|
|
icon = Column(String, default="")
|
|
sort_order = Column(Integer, default=0)
|
|
roles = Column(JSON, default=list)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
parent = relationship("Menu", remote_side=[id], backref="children")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"parent_id": self.parent_id,
|
|
"name": self.name,
|
|
"path": self.path,
|
|
"icon": self.icon,
|
|
"sort_order": self.sort_order,
|
|
"roles": self.roles or [],
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class TopicField(Base):
|
|
__tablename__ = "topic_fields"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, nullable=False)
|
|
icon = Column(String, nullable=True)
|
|
color = Column(String, nullable=True)
|
|
description = Column(Text, nullable=True)
|
|
parent_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
parent = relationship("TopicField", remote_side=[id], backref="children")
|
|
scoring_fields = relationship("TopicConfigField", back_populates="field", cascade="all, delete-orphan")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"icon": self.icon,
|
|
"color": self.color,
|
|
"description": self.description,
|
|
"parent_id": self.parent_id,
|
|
"sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TopicConfigField(Base):
|
|
__tablename__ = "topic_config_fields"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
key = Column(String, nullable=False)
|
|
field_type = Column(String, default="number") # number/select/multi_select/text
|
|
weight = Column(Float, default=1.0)
|
|
options = Column(JSON, default=list) # for select/multi_select
|
|
min_value = Column(Float, nullable=True)
|
|
max_value = Column(Float, nullable=True)
|
|
is_required = Column(Boolean, default=False)
|
|
sort_order = Column(Integer, default=0)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
field = relationship("TopicField", back_populates="scoring_fields")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"field_id": self.field_id,
|
|
"name": self.name,
|
|
"key": self.key,
|
|
"field_type": self.field_type,
|
|
"weight": self.weight,
|
|
"options": self.options or [],
|
|
"min_value": self.min_value,
|
|
"max_value": self.max_value,
|
|
"is_required": self.is_required,
|
|
"sort_order": self.sort_order,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class TopicStatusConfig(Base):
|
|
__tablename__ = "topic_status_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
status = Column(String, unique=True, nullable=False)
|
|
label = Column(String, nullable=False)
|
|
color = Column(String, nullable=True)
|
|
icon = Column(String, nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
is_default = Column(Boolean, default=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"status": self.status,
|
|
"label": self.label,
|
|
"color": self.color,
|
|
"icon": self.icon,
|
|
"sort_order": self.sort_order,
|
|
"is_default": self.is_default,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class Topic(Base):
|
|
__tablename__ = "topics"
|
|
|
|
id = Column(String, primary_key=True, index=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
field_name = Column(String, nullable=True)
|
|
org_id = Column(String, default="default", nullable=True)
|
|
title = Column(String, nullable=False)
|
|
format = Column(String)
|
|
core_concept = Column(Text)
|
|
audience_pain = Column(Text)
|
|
unique_angle = Column(Text)
|
|
priority = Column(String)
|
|
priority_score = Column(Integer, default=0)
|
|
total_score = Column(Float)
|
|
status = Column(String, default="pending")
|
|
cases = Column(JSON, default=list)
|
|
source_file = Column(String)
|
|
tags = Column(JSON, default=list)
|
|
custom_data = Column(JSON, default=dict)
|
|
scoring_data = Column(JSON, default=dict)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
generated_at = Column(DateTime(timezone=True), nullable=True)
|
|
reviewed_at = Column(DateTime(timezone=True), nullable=True)
|
|
ready_at = Column(Date)
|
|
published_at = Column(Date)
|
|
compliance_score = Column(Integer)
|
|
platform_urls = Column(JSON, default=dict)
|
|
lock_by = Column(String, nullable=True)
|
|
lock_at = Column(DateTime, nullable=True)
|
|
|
|
field = relationship("TopicField", backref="topics")
|
|
|
|
@property
|
|
def field_info(self):
|
|
if self.field:
|
|
return self.field.to_dict()
|
|
return None
|
|
|
|
|
|
class Article(Base):
|
|
__tablename__ = "articles"
|
|
|
|
id = Column(String, primary_key=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
|
platform = Column(String, nullable=False)
|
|
file_path = Column(String, nullable=False)
|
|
status = Column(String, default="draft")
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
compliance_score = Column(Integer)
|
|
html_content = Column(Text)
|
|
word_count = Column(Integer, nullable=True)
|
|
outline = Column(Text, nullable=True)
|
|
images = Column(JSON, default=dict) # {"cover": "/path/to/cover.png", "chart": "/path/to/chart.png"}
|
|
|
|
|
|
class PublishRecord(Base):
|
|
__tablename__ = "publish_records"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
|
platform = Column(String, nullable=False)
|
|
action = Column(String, nullable=False)
|
|
status = Column(String, nullable=False)
|
|
operator = Column(String, nullable=True)
|
|
description = Column(Text, nullable=True)
|
|
suggestion = Column(Text, nullable=True)
|
|
url = Column(String, nullable=True)
|
|
error_msg = Column(Text, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
|
|
class ContentCalendar(Base):
|
|
__tablename__ = "content_calendar"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
title = Column(String, nullable=False)
|
|
planned_date = Column(Date, nullable=False, index=True)
|
|
published_date = Column(Date, nullable=True)
|
|
platform = Column(String, nullable=True)
|
|
status = Column(String, default="planned") # planned/published/delayed/cancelled
|
|
reminder_time = Column(DateTime, nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"field_id": self.field_id,
|
|
"title": self.title,
|
|
"planned_date": self.planned_date.isoformat() if self.planned_date else None,
|
|
"published_date": self.published_date.isoformat() if self.published_date else None,
|
|
"platform": self.platform,
|
|
"status": self.status,
|
|
"reminder_time": self.reminder_time.isoformat() if self.reminder_time else None,
|
|
"notes": self.notes,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class ContentMetrics(Base):
|
|
__tablename__ = "content_metrics"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False, index=True)
|
|
platform = Column(String, nullable=False)
|
|
publish_url = Column(String, nullable=True)
|
|
views = Column(Integer, default=0)
|
|
likes = Column(Integer, default=0)
|
|
favorites = Column(Integer, default=0)
|
|
comments = Column(Integer, default=0)
|
|
shares = Column(Integer, default=0)
|
|
last_fetched = Column(DateTime(timezone=True), nullable=True)
|
|
data_snapshot = Column(JSON, default=dict)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
@property
|
|
def engagement_rate(self):
|
|
total = self.views or 0
|
|
if total == 0:
|
|
return 0
|
|
return round((self.likes or 0) / total * 100, 2)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"platform": self.platform,
|
|
"publish_url": self.publish_url,
|
|
"views": self.views,
|
|
"likes": self.likes,
|
|
"favorites": self.favorites,
|
|
"comments": self.comments,
|
|
"shares": self.shares,
|
|
"engagement_rate": self.engagement_rate,
|
|
"last_fetched": self.last_fetched.isoformat() if self.last_fetched else None,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class MediaAsset(Base):
|
|
__tablename__ = "media_assets"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
filename = Column(String, nullable=False)
|
|
file_path = Column(String, nullable=False)
|
|
file_url = Column(String, nullable=True)
|
|
file_type = Column(String, nullable=False) # image/video/document
|
|
mime_type = Column(String, nullable=True)
|
|
size = Column(Integer, nullable=True)
|
|
width = Column(Integer, nullable=True)
|
|
height = Column(Integer, nullable=True)
|
|
thumbnail_path = Column(String, nullable=True)
|
|
alt_text = Column(String, nullable=True)
|
|
tags = Column(JSON, default=list)
|
|
topic_ids = Column(JSON, default=list)
|
|
usage_count = Column(Integer, default=0)
|
|
uploaded_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"filename": self.filename,
|
|
"file_path": self.file_path,
|
|
"file_url": self.file_url,
|
|
"file_type": self.file_type,
|
|
"mime_type": self.mime_type,
|
|
"size": self.size,
|
|
"width": self.width,
|
|
"height": self.height,
|
|
"thumbnail_path": self.thumbnail_path,
|
|
"alt_text": self.alt_text,
|
|
"tags": self.tags or [],
|
|
"topic_ids": self.topic_ids or [],
|
|
"usage_count": self.usage_count,
|
|
"uploaded_by": self.uploaded_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class PlatformConfig(Base):
|
|
__tablename__ = "platform_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
platform = Column(String, unique=True, nullable=False) # zhihu/wechat/xiaohongshu
|
|
name = Column(String, nullable=False)
|
|
icon = Column(String, nullable=True)
|
|
website_url = Column(String, nullable=True)
|
|
api_endpoint = Column(String, nullable=True)
|
|
auth_config = Column(JSON, default=dict)
|
|
format_template = Column(JSON, default=dict)
|
|
compliance_rules = Column(JSON, default=dict)
|
|
default_format = Column(Text, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
requires_image = Column(Boolean, default=False)
|
|
image_count_min = Column(Integer, default=0)
|
|
image_count_max = Column(Integer, default=0)
|
|
image_width = Column(Integer, default=0)
|
|
image_height = Column(Integer, default=0)
|
|
min_words = Column(Integer, default=0)
|
|
max_words = Column(Integer, default=0)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"platform": self.platform,
|
|
"name": self.name,
|
|
"icon": self.icon,
|
|
"website_url": self.website_url,
|
|
"api_endpoint": self.api_endpoint,
|
|
"format_template": self.format_template or {},
|
|
"compliance_rules": self.compliance_rules or {},
|
|
"default_format": self.default_format,
|
|
"is_active": self.is_active,
|
|
"requires_image": self.requires_image,
|
|
"image_count_min": self.image_count_min,
|
|
"image_count_max": self.image_count_max,
|
|
"image_width": self.image_width,
|
|
"image_height": self.image_height,
|
|
"min_words": self.min_words,
|
|
"max_words": self.max_words,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class ContentTask(Base):
|
|
__tablename__ = "content_tasks"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=True, index=True)
|
|
task_id = Column(String, unique=True, nullable=False)
|
|
stage = Column(String, nullable=False) # research/outline/writer/optimizer/format/publish
|
|
status = Column(String, default="pending") # pending/running/completed/failed/cancelled
|
|
progress = Column(Integer, default=0)
|
|
message = Column(Text, nullable=True)
|
|
result_data = Column(JSON, default=dict)
|
|
error_msg = Column(Text, nullable=True)
|
|
started_at = Column(DateTime(timezone=True), nullable=True)
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"task_id": self.task_id,
|
|
"stage": self.stage,
|
|
"status": self.status,
|
|
"progress": self.progress,
|
|
"message": self.message,
|
|
"error_msg": self.error_msg,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class Case(Base):
|
|
__tablename__ = "cases"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
title = Column(String, nullable=False)
|
|
field = Column(String, nullable=False)
|
|
summary = Column(Text)
|
|
key_metrics = Column(Text, nullable=True)
|
|
date = Column(String, nullable=True)
|
|
source = Column(String, nullable=True)
|
|
credibility_rating = Column(String, nullable=True)
|
|
china_applicability = Column(String, nullable=True)
|
|
source_url = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"field": self.field,
|
|
"summary": self.summary,
|
|
"key_metrics": self.key_metrics,
|
|
"date": self.date,
|
|
"source": self.source,
|
|
"credibility_rating": self.credibility_rating,
|
|
"china_applicability": self.china_applicability,
|
|
"source_url": self.source_url,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TaskLog(Base):
|
|
__tablename__ = "task_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
module_id = Column(String, nullable=False, index=True) # scheduled_collect / scheduled_generate 等
|
|
task_name = Column(String, nullable=False)
|
|
topic_id = Column(String, nullable=True, index=True)
|
|
status = Column(String, nullable=False) # pending / running / success / failed / cancelled
|
|
message = Column(Text, nullable=True)
|
|
error_trace = Column(Text, nullable=True)
|
|
triggered_by = Column(String, default="scheduler") # scheduler / manual / api
|
|
result_data = Column(JSON, default=dict) # 产出摘要:{topics_found, articles_created, issues_fixed, ...}
|
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True) # seconds
|
|
next_run_time = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"module_id": self.module_id,
|
|
"task_name": self.task_name,
|
|
"topic_id": self.topic_id,
|
|
"status": self.status,
|
|
"message": self.message,
|
|
"error_trace": self.error_trace,
|
|
"triggered_by": self.triggered_by,
|
|
"result_data": self.result_data or {},
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
"next_run_time": self.next_run_time.isoformat() if self.next_run_time else None,
|
|
}
|
|
|
|
|
|
class TaskConfig(Base):
|
|
__tablename__ = "task_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
module_id = Column(String, unique=True, nullable=False)
|
|
enabled = Column(Boolean, default=True)
|
|
params = Column(JSON, default=dict) # 各任务自定义参数,JSON 格式
|
|
schedule = Column(String, nullable=True) # cron 表达式,覆盖默认
|
|
last_modified_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"module_id": self.module_id,
|
|
"enabled": self.enabled,
|
|
"params": self.params or {},
|
|
"schedule": self.schedule,
|
|
"last_modified_by": self.last_modified_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class LLMConfig(Base):
|
|
__tablename__ = "llm_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, unique=True, nullable=False)
|
|
system_prompt = Column(Text, nullable=True)
|
|
user_prompt_template = Column(Text, nullable=False)
|
|
temperature = Column(Float, default=0.7)
|
|
max_tokens = Column(Integer, default=2000)
|
|
model = Column(String, nullable=True)
|
|
provider = Column(String, default="opencode-go") # opencode-go / nvidia
|
|
base_url = Column(String, nullable=True)
|
|
api_key = Column(String, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"system_prompt": self.system_prompt,
|
|
"user_prompt_template": self.user_prompt_template,
|
|
"temperature": self.temperature,
|
|
"max_tokens": self.max_tokens,
|
|
"model": self.model,
|
|
"provider": self.provider,
|
|
"base_url": self.base_url,
|
|
"api_key": f"{self.api_key[:8]}..." if self.api_key else None,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class PromptConfig(Base):
|
|
__tablename__ = "prompt_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
key = Column(String, unique=True, nullable=False, index=True)
|
|
module_id = Column(String, nullable=True, index=True)
|
|
category = Column(String, default="prompt") # prompt / rule / template
|
|
version = Column(String, default="v1")
|
|
content = Column(Text, nullable=False)
|
|
variables = Column(JSON, default=[]) # [{name, description, default_value}]
|
|
description = Column(String, nullable=True)
|
|
enabled = Column(Boolean, default=True)
|
|
temperature = Column(Float, nullable=True)
|
|
max_tokens = Column(Integer, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"key": self.key,
|
|
"module_id": self.module_id,
|
|
"category": self.category,
|
|
"version": self.version,
|
|
"content": self.content,
|
|
"variables": self.variables or [],
|
|
"description": self.description,
|
|
"enabled": self.enabled,
|
|
"temperature": self.temperature,
|
|
"max_tokens": self.max_tokens,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class KeywordDomainMap(Base):
|
|
__tablename__ = "keyword_domain_map"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
pattern = Column(String, nullable=False)
|
|
domain = Column(String, nullable=False)
|
|
sort_order = Column(Integer, default=0)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "pattern": self.pattern, "domain": self.domain,
|
|
"sort_order": self.sort_order, "is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class SensitiveWord(Base):
|
|
__tablename__ = "sensitive_words"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
word = Column(String, nullable=False)
|
|
category = Column(String, default="general")
|
|
is_active = Column(Boolean, default=True)
|
|
added_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "word": self.word, "category": self.category,
|
|
"is_active": self.is_active, "added_by": self.added_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class ContentCleanRule(Base):
|
|
__tablename__ = "content_clean_rules"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
rule_type = Column(String, nullable=False) # thinking / preface / verbosity / html_thinking
|
|
pattern = Column(Text, nullable=False)
|
|
description = Column(String, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
sort_order = Column(Integer, default=0)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "rule_type": self.rule_type, "pattern": self.pattern,
|
|
"description": self.description, "is_active": self.is_active,
|
|
"sort_order": self.sort_order,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class SystemConfig(Base):
|
|
__tablename__ = "system_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
key = Column(String, unique=True, nullable=False)
|
|
value = Column(Text, nullable=True)
|
|
description = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
import json
|
|
return {
|
|
"id": self.id,
|
|
"key": self.key,
|
|
"value": json.loads(self.value) if self.value else None,
|
|
"description": self.description,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class CollectorCategory(Base):
|
|
"""采集类别(可在运营管理中动态编辑)"""
|
|
__tablename__ = "collector_categories"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, unique=True, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
search_query = Column(String, nullable=True)
|
|
pain_template = Column(Text, nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
sources = relationship("CollectorSource", back_populates="category", cascade="all, delete-orphan")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"search_query": self.search_query,
|
|
"pain_template": self.pain_template,
|
|
"sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TrendFieldMapping(Base):
|
|
__tablename__ = "trend_field_mappings"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
trend_keyword = Column(String, nullable=False)
|
|
field_name = Column(String, nullable=False)
|
|
sort_order = Column(Integer, default=0)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id, "trend_keyword": self.trend_keyword, "field_name": self.field_name,
|
|
"sort_order": self.sort_order, "is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class CollectorSource(Base):
|
|
"""采集信息源(可在运营管理中动态编辑)"""
|
|
__tablename__ = "collector_sources"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
category_id = Column(Integer, ForeignKey("collector_categories.id"), nullable=True)
|
|
name = Column(String, nullable=False)
|
|
source_type = Column(String, nullable=False) # rss / web_search / local
|
|
url = Column(Text, nullable=True)
|
|
query = Column(String, nullable=True)
|
|
credibility = Column(String, default="medium")
|
|
focus = Column(String, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
sort_order = Column(Integer, default=0)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
category = relationship("CollectorCategory", back_populates="sources")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"category_id": self.category_id,
|
|
"name": self.name,
|
|
"source_type": self.source_type,
|
|
"url": self.url,
|
|
"query": self.query,
|
|
"credibility": self.credibility,
|
|
"focus": self.focus,
|
|
"is_active": self.is_active,
|
|
"sort_order": self.sort_order,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
} |