feat: 平台配置表扩展配图/字数字段,微信公众号正文插入配图,合规检查从DB读规则
- PlatformConfig模型新增requires_image、image_count_min/max、image_width/height、min_words/max_words - schemas.py同步PlatformConfigBase/Create/Update/Response新字段 - initial_data.py为三大平台填充初始值(微信需配图、字数800-1500等) - database.py添加新列ALTER TABLE迁移 - platforms.html重写编辑弹窗(正确字段名+配图/字数设置) - writer.py微信公众号文章正文h1后插入<img>占位 - compliance_checker.py接受platform_config参数,从DB读取规则替代硬编码 - compliance_optimizer.py启动时加载DB平台配置传入checker
This commit is contained in:
+4
-1
@@ -3,7 +3,7 @@
|
||||
> 本文件为项目进度唯一真理源,所有进度信息以此为准。
|
||||
> 其他文档中的进度描述一律以本文为准。
|
||||
|
||||
**最后更新**:2026-05-16 (v14)
|
||||
**最后更新**:2026-05-17 (v15)
|
||||
|
||||
---
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
| Phase 4 多租户隔离 (JWT+API 层) | 2026-05-16 | org_id 注入 JWT payload; 12 个 API 模块添加 org 过滤; 创建选题自动继承用户 org; 管理端组织 CRUD |
|
||||
| Phase 4 前端多租户 | 2026-05-16 | admin.html 新增组织管理标签页 (列表/创建/编辑/删除); users.html 增加组织列+H5卡片显示 |
|
||||
| Phase 4 预置数据修复 | 2026-05-16 | initial_data.py 管理员用户添加 org_id; opencode-go LLM config 补全 user_prompt_template |
|
||||
| PlatformConfig 模型字段扩展 | 2026-05-17 | 增加 requires_image/image_count_min/image_count_max/image_width/image_height/min_words/max_words; schemas/initial_data/前端表单同步; database.py 迁移 |
|
||||
| 微信公众号正文配图 | 2026-05-17 | writer.py wechat 分支在 h1 后插入 `<img>` 占位 |
|
||||
| 合规检查从 DB 读平台规则 | 2026-05-17 | compliance_checker.py 接受 platform_config 参数; compliance_optimizer.py 从 DB 加载平台配置传入 checker |
|
||||
|
||||
### ⏳ 待办
|
||||
|
||||
|
||||
@@ -53,6 +53,13 @@ def init_db():
|
||||
for table, col, typ in [
|
||||
("users", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||
("topics", "org_id", "VARCHAR DEFAULT 'default'"),
|
||||
("platform_configs", "requires_image", "BOOLEAN DEFAULT FALSE"),
|
||||
("platform_configs", "image_count_min", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "image_count_max", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "image_width", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "image_height", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "min_words", "INTEGER DEFAULT 0"),
|
||||
("platform_configs", "max_words", "INTEGER DEFAULT 0"),
|
||||
]:
|
||||
try:
|
||||
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}"))
|
||||
|
||||
@@ -81,11 +81,18 @@ def import_initial_data():
|
||||
"icon": "🔍",
|
||||
"default_format": "长文深度分析,1500-3000字,有数据支撑",
|
||||
"compliance_rules": {
|
||||
"max_length": 50000,
|
||||
"requires_authentication": False,
|
||||
"sensitive_words": ["敏感词示例1", "敏感词示例2"]
|
||||
"max_title_len": 100,
|
||||
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
|
||||
"forbidden_patterns": ["加微信", "私聊", "付费咨询", "点击领取"]
|
||||
},
|
||||
"is_active": True
|
||||
"is_active": True,
|
||||
"requires_image": False,
|
||||
"image_count_min": 0,
|
||||
"image_count_max": 0,
|
||||
"image_width": 0,
|
||||
"image_height": 0,
|
||||
"min_words": 1500,
|
||||
"max_words": 3000
|
||||
},
|
||||
{
|
||||
"platform": "wechat",
|
||||
@@ -93,10 +100,18 @@ def import_initial_data():
|
||||
"icon": "💚",
|
||||
"default_format": "公众号图文,800-1500字,亲切口语化",
|
||||
"compliance_rules": {
|
||||
"max_length": 20000,
|
||||
"requires_authentication": True
|
||||
"max_title_len": 32,
|
||||
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
|
||||
"forbidden_patterns": ["诱导分享", "朋友圈", "转发群"]
|
||||
},
|
||||
"is_active": True
|
||||
"is_active": True,
|
||||
"requires_image": True,
|
||||
"image_count_min": 1,
|
||||
"image_count_max": 3,
|
||||
"image_width": 1080,
|
||||
"image_height": 1080,
|
||||
"min_words": 800,
|
||||
"max_words": 1500
|
||||
},
|
||||
{
|
||||
"platform": "xiaohongshu",
|
||||
@@ -104,11 +119,18 @@ def import_initial_data():
|
||||
"icon": "📕",
|
||||
"default_format": "图文笔记,300-800字,emoji+标签",
|
||||
"compliance_rules": {
|
||||
"max_length": 1000,
|
||||
"requires_tags": True,
|
||||
"max_tags": 10
|
||||
"max_title_len": 50,
|
||||
"allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"],
|
||||
"forbidden_patterns": ["私信", "加群", "导流"]
|
||||
},
|
||||
"is_active": True
|
||||
"is_active": True,
|
||||
"requires_image": True,
|
||||
"image_count_min": 3,
|
||||
"image_count_max": 6,
|
||||
"image_width": 1080,
|
||||
"image_height": 1440,
|
||||
"min_words": 300,
|
||||
"max_words": 800
|
||||
}
|
||||
]
|
||||
for p in platforms:
|
||||
|
||||
@@ -361,6 +361,13 @@ class PlatformConfig(Base):
|
||||
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())
|
||||
|
||||
@@ -375,6 +382,13 @@ class PlatformConfig(Base):
|
||||
"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,
|
||||
}
|
||||
|
||||
@@ -263,6 +263,13 @@ class PlatformConfigBase(BaseModel):
|
||||
compliance_rules: Dict[str, Any] = {}
|
||||
default_format: Optional[str] = None
|
||||
is_active: bool = True
|
||||
requires_image: bool = False
|
||||
image_count_min: int = 0
|
||||
image_count_max: int = 0
|
||||
image_width: int = 0
|
||||
image_height: int = 0
|
||||
min_words: int = 0
|
||||
max_words: int = 0
|
||||
|
||||
|
||||
class PlatformConfigCreate(PlatformConfigBase):
|
||||
@@ -278,6 +285,13 @@ class PlatformConfigUpdate(BaseModel):
|
||||
compliance_rules: Optional[Dict[str, Any]] = None
|
||||
default_format: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
requires_image: Optional[bool] = None
|
||||
image_count_min: Optional[int] = None
|
||||
image_count_max: Optional[int] = None
|
||||
image_width: Optional[int] = None
|
||||
image_height: Optional[int] = None
|
||||
min_words: Optional[int] = None
|
||||
max_words: Optional[int] = None
|
||||
|
||||
|
||||
class PlatformConfigResponse(PlatformConfigBase):
|
||||
|
||||
@@ -78,52 +78,74 @@
|
||||
<span class="platform-meta-label">平台标识</span>
|
||||
<span class="platform-meta-value">{{ p.platform }}</span>
|
||||
</div>
|
||||
<div v-if="p.platform_name" class="platform-meta-item">
|
||||
<div class="platform-meta-item">
|
||||
<span class="platform-meta-label">平台名称</span>
|
||||
<span class="platform-meta-value">{{ p.platform_name }}</span>
|
||||
<span class="platform-meta-value">{{ p.name }}</span>
|
||||
</div>
|
||||
<div v-if="p.min_words || p.max_words" class="platform-meta-item">
|
||||
<span class="platform-meta-label">字数限制</span>
|
||||
<span class="platform-meta-value">{{ p.min_words || 0 }} - {{ p.max_words || '不限' }}</span>
|
||||
</div>
|
||||
<div class="platform-meta-item">
|
||||
<span class="platform-meta-label">需要配图</span>
|
||||
<span class="platform-meta-value">{{ p.requires_image ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div v-if="p.format_rules && Object.keys(p.format_rules).length > 0" class="rules-section">
|
||||
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconTopic /></el-icon> 格式规则</div>
|
||||
<div v-for="(rule, key) in p.format_rules" :key="key" class="rule-item">
|
||||
</div>
|
||||
<div v-if="p.format_template && Object.keys(p.format_template).length > 0" class="rules-section">
|
||||
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconTopic /></el-icon> 格式模板</div>
|
||||
<div v-for="(rule, key) in p.format_template" :key="key" class="rule-item">
|
||||
<strong>{{ key }}:</strong> {{ typeof rule === 'object' ? JSON.stringify(rule) : rule }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="p.compliance_rules && p.compliance_rules.length > 0" class="rules-section">
|
||||
<div v-if="p.compliance_rules && Object.keys(p.compliance_rules).length > 0" class="rules-section">
|
||||
<div class="rules-title"><el-icon style="vertical-align:-2px;"><IconWarning /></el-icon> 合规规则</div>
|
||||
<div v-for="(rule, idx) in p.compliance_rules" :key="idx" class="rule-item">{{ rule }}</div>
|
||||
<div v-for="(val, key) in p.compliance_rules" :key="key" class="rule-item">
|
||||
<strong>{{ key }}:</strong> {{ typeof val === 'object' ? JSON.stringify(val) : val }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="600px">
|
||||
<el-form :model="platformForm" label-width="100px">
|
||||
<el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="650px">
|
||||
<el-form :model="platformForm" label-width="120px">
|
||||
<el-form-item label="平台标识" :required="!isEditing">
|
||||
<el-input v-model="platformForm.platform" :disabled="isEditing" placeholder="如: zhihu, wechat, xiaohongshu"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台名称">
|
||||
<el-input v-model="platformForm.platform_name" placeholder="如: 知乎"></el-input>
|
||||
<el-input v-model="platformForm.name" placeholder="如: 知乎"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="platformForm.is_active"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题模板">
|
||||
<el-input v-model="platformForm.title_template" placeholder="如: 【{{tag}}】{{title}}"></el-input>
|
||||
<el-form-item label="默认格式">
|
||||
<el-input v-model="platformForm.default_format" type="textarea" :rows="2" placeholder="如: 公众号图文,800-1500字,亲切口语化"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="正文模板">
|
||||
<el-input v-model="platformForm.body_template" type="textarea" :rows="4" placeholder="如: {{content}}"></el-input>
|
||||
<el-divider content-position="left">字数与配图</el-divider>
|
||||
<el-form-item label="最少字数">
|
||||
<el-input-number v-model="platformForm.min_words" :min="0" :max="100000"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="字数限制">
|
||||
<el-input-number v-model="platformForm.min_words" :min="0" placeholder="最少"></el-input-number>
|
||||
<span style="margin: 0 8px;">-</span>
|
||||
<el-input-number v-model="platformForm.max_words" :min="0" placeholder="最多"></el-input-number>
|
||||
<el-form-item label="最多字数">
|
||||
<el-input-number v-model="platformForm.max_words" :min="0" :max="100000"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="需要配图">
|
||||
<el-switch v-model="platformForm.requires_image"></el-switch>
|
||||
</el-form-item>
|
||||
<template v-if="platformForm.requires_image">
|
||||
<el-form-item label="最少图片数">
|
||||
<el-input-number v-model="platformForm.image_count_min" :min="0" :max="20"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="最多图片数">
|
||||
<el-input-number v-model="platformForm.image_count_max" :min="0" :max="20"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片宽度(px)">
|
||||
<el-input-number v-model="platformForm.image_width" :min="0" :max="4096" :step="1"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片高度(px)">
|
||||
<el-input-number v-model="platformForm.image_height" :min="0" :max="4096" :step="1"></el-input-number>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
@@ -147,7 +169,7 @@ const PlatformsApp = {
|
||||
showEditDialog: false,
|
||||
isEditing: false,
|
||||
saving: false,
|
||||
platformForm: { platform: '', platform_name: '', is_active: true, title_template: '', body_template: '', min_words: null, max_words: null }
|
||||
platformForm: { platform: '', name: '', is_active: true, default_format: '', requires_image: false, image_count_min: 0, image_count_max: 0, image_width: 0, image_height: 0, min_words: 0, max_words: 0 }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -188,12 +210,16 @@ const PlatformsApp = {
|
||||
this.isEditing = true;
|
||||
this.platformForm = {
|
||||
platform: p.platform,
|
||||
platform_name: p.platform_name || '',
|
||||
name: p.name || '',
|
||||
is_active: p.is_active,
|
||||
title_template: p.title_template || '',
|
||||
body_template: p.body_template || '',
|
||||
min_words: p.min_words,
|
||||
max_words: p.max_words
|
||||
default_format: p.default_format || '',
|
||||
requires_image: p.requires_image || false,
|
||||
image_count_min: p.image_count_min || 0,
|
||||
image_count_max: p.image_count_max || 0,
|
||||
image_width: p.image_width || 0,
|
||||
image_height: p.image_height || 0,
|
||||
min_words: p.min_words || 0,
|
||||
max_words: p.max_words || 0
|
||||
};
|
||||
this.showEditDialog = true;
|
||||
},
|
||||
|
||||
@@ -40,8 +40,19 @@ PLATFORM_RULES = {
|
||||
class ComplianceChecker:
|
||||
"""合规审查器"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, platform_config: Dict = None):
|
||||
self.issues = []
|
||||
self.platform_config = platform_config or {}
|
||||
|
||||
def _get_platform_rule(self, key: str, default=None):
|
||||
"""从 platform_config 读取规则,fallback 到硬编码 PLATFORM_RULES"""
|
||||
if self.platform_config:
|
||||
compliance_rules = self.platform_config.get('compliance_rules', {})
|
||||
if key in compliance_rules:
|
||||
return compliance_rules[key]
|
||||
if key == 'min_word_count' and self.platform_config.get('min_words'):
|
||||
return self.platform_config['min_words']
|
||||
return default
|
||||
|
||||
def check_text(self, text: str, platform: str, topic_data: Dict = None) -> Dict:
|
||||
"""执行全面合规检查"""
|
||||
@@ -94,19 +105,21 @@ class ComplianceChecker:
|
||||
rules = PLATFORM_RULES.get(platform, {})
|
||||
|
||||
# 标题长度(从HTML中提取)
|
||||
max_title_len = self._get_platform_rule('max_title_len', rules.get("max_title_len"))
|
||||
title_match = re.search(r'<title>([^<]+)</title>', text) or re.search(r'<h1[^>]*>([^<]+)</h1>', text)
|
||||
if title_match and rules.get("max_title_len"):
|
||||
if title_match and max_title_len:
|
||||
title_len = len(title_match.group(1))
|
||||
if title_len > rules["max_title_len"]:
|
||||
if title_len > max_title_len:
|
||||
self.issues.append({
|
||||
"type": "平台规则",
|
||||
"category": "标题长度",
|
||||
"detail": f"标题{title_len}字,超过{platform}限制{rules['max_title_len']}字",
|
||||
"detail": f"标题{title_len}字,超过{platform}限制{max_title_len}字",
|
||||
"suggestion": "缩短标题"
|
||||
})
|
||||
|
||||
# 禁止的模式匹配
|
||||
for pattern in rules.get("forbidden_patterns", []):
|
||||
forbidden_patterns = self._get_platform_rule('forbidden_patterns', rules.get("forbidden_patterns", []))
|
||||
for pattern in forbidden_patterns:
|
||||
if re.search(pattern, text):
|
||||
self.issues.append({
|
||||
"type": "平台规则",
|
||||
@@ -123,17 +136,16 @@ class ComplianceChecker:
|
||||
# 过滤掉纯十六进制颜色码(如 #1a1a1a, #fff)
|
||||
tags = [t for t in tags if not re.fullmatch(r'[0-9a-fA-F]{3,6}', t)]
|
||||
else:
|
||||
# 没有标签容器时,不检查标签
|
||||
tags = []
|
||||
allowed = rules.get("allowed_tags", [])
|
||||
if allowed:
|
||||
allowed_tags = self._get_platform_rule('allowed_tags', rules.get("allowed_tags", []))
|
||||
if allowed_tags:
|
||||
for tag in tags:
|
||||
if tag not in allowed:
|
||||
if tag not in allowed_tags:
|
||||
self.issues.append({
|
||||
"type": "平台规则",
|
||||
"category": "标签合规",
|
||||
"tag": tag,
|
||||
"suggestion": f"使用平台允许的标签,如{', '.join(allowed[:3])}"
|
||||
"suggestion": f"使用平台允许的标签,如{', '.join(allowed_tags[:3])}"
|
||||
})
|
||||
|
||||
def _check_legal_compliance(self, text: str):
|
||||
@@ -230,10 +242,9 @@ class ComplianceChecker:
|
||||
|
||||
def _check_min_length(self, text: str, platform: str):
|
||||
"""检查文章最小字数(去除HTML标签)"""
|
||||
# 简单去除HTML标签
|
||||
plain = re.sub(r'<[^>]+>', '', text)
|
||||
word_count = len(plain.strip())
|
||||
min_words = PLATFORM_RULES.get(platform, {}).get("min_word_count", 1000)
|
||||
min_words = self._get_platform_rule('min_word_count', PLATFORM_RULES.get(platform, {}).get("min_word_count", 1000))
|
||||
if word_count < min_words:
|
||||
self.issues.append({
|
||||
"type": "内容完整度",
|
||||
@@ -281,9 +292,9 @@ class ComplianceChecker:
|
||||
"detail": f"使用过时年份: {', '.join(sorted(outdated))},需更新为2025年及以后的数据",
|
||||
"suggestion": "替换为最新数据,或使用'近期'等模糊表述"
|
||||
})
|
||||
def check_article(html_content: str, platform: str, topic_data: Dict = None) -> Dict:
|
||||
def check_article(html_content: str, platform: str, topic_data: Dict = None, platform_config: Dict = None) -> Dict:
|
||||
"""便捷函数:执行完整合规检查"""
|
||||
checker = ComplianceChecker()
|
||||
checker = ComplianceChecker(platform_config=platform_config)
|
||||
return checker.check_text(html_content, platform, topic_data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -185,6 +185,17 @@ def optimize_article(html: str, platform: str, topic_data: Dict, remaining_issue
|
||||
logs.append(pol_log)
|
||||
return html, logs
|
||||
|
||||
def _load_platform_configs() -> Dict[str, Dict]:
|
||||
"""从 DB 加载所有平台配置"""
|
||||
from app.database import SessionLocal
|
||||
from app.models import PlatformConfig
|
||||
db = SessionLocal()
|
||||
try:
|
||||
configs = db.query(PlatformConfig).all()
|
||||
return {c.platform: c.to_dict() for c in configs}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def main(topic_ids: List[str] = None):
|
||||
logger.info("=== 合规审查与优化开始 ===")
|
||||
llm_cfg = get_llm_config()
|
||||
@@ -193,6 +204,9 @@ def main(topic_ids: List[str] = None):
|
||||
else:
|
||||
logger.info("LLM 配置: 使用环境变量默认值")
|
||||
|
||||
platform_configs = _load_platform_configs()
|
||||
logger.info(f"已加载 {len(platform_configs)} 个平台配置")
|
||||
|
||||
articles = get_articles_from_db(topic_ids)
|
||||
if not articles:
|
||||
logger.warning("未找到任何文章(可能尚未创作或同步到 DB)")
|
||||
@@ -216,7 +230,8 @@ def main(topic_ids: List[str] = None):
|
||||
logger.warning(f"未找到选题: {topic_id}")
|
||||
continue
|
||||
|
||||
check_result = check_article(html, platform_dir, topic_data)
|
||||
pc = platform_configs.get(platform_dir, {})
|
||||
check_result = check_article(html, platform_dir, topic_data, platform_config=pc)
|
||||
issues = check_result['issues']
|
||||
score = check_result['score']
|
||||
label = f"{platform_dir}/{topic_id}"
|
||||
|
||||
@@ -351,6 +351,17 @@ class Writer:
|
||||
|
||||
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME)
|
||||
html_content = _md_parser(adapted)
|
||||
|
||||
# WeChat: insert image placeholder at start of body
|
||||
if platform == "wechat":
|
||||
img_tag = '<p><img src="placeholder.jpg" alt="配图" style="width:100%;max-width:1080px;border-radius:8px;"></p>\n'
|
||||
# Insert after <h1> if present, else prepend
|
||||
h1_end = html_content.find('</h1>')
|
||||
if h1_end != -1:
|
||||
html_content = html_content[:h1_end + 5] + '\n' + img_tag + html_content[h1_end + 5:]
|
||||
else:
|
||||
html_content = img_tag + html_content
|
||||
|
||||
html = html.replace("<!-- CONTENT -->", html_content)
|
||||
|
||||
tags_html = self._get_platform_tags(platform)
|
||||
|
||||
Reference in New Issue
Block a user