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:
Yuzhiran Dev
2026-05-17 23:08:43 +08:00
parent 5674114599
commit 41b0f694ee
9 changed files with 174 additions and 51 deletions
+4 -1
View File
@@ -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 多租户隔离 (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 | admin.html 新增组织管理标签页 (列表/创建/编辑/删除); users.html 增加组织列+H5卡片显示 |
| Phase 4 预置数据修复 | 2026-05-16 | initial_data.py 管理员用户添加 org_id; opencode-go LLM config 补全 user_prompt_template | | 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 |
### ⏳ 待办 ### ⏳ 待办
+7
View File
@@ -53,6 +53,13 @@ def init_db():
for table, col, typ in [ for table, col, typ in [
("users", "org_id", "VARCHAR DEFAULT 'default'"), ("users", "org_id", "VARCHAR DEFAULT 'default'"),
("topics", "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: try:
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}")) conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}"))
+33 -11
View File
@@ -81,11 +81,18 @@ def import_initial_data():
"icon": "🔍", "icon": "🔍",
"default_format": "长文深度分析,1500-3000字,有数据支撑", "default_format": "长文深度分析,1500-3000字,有数据支撑",
"compliance_rules": { "compliance_rules": {
"max_length": 50000, "max_title_len": 100,
"requires_authentication": False, "allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"sensitive_words": ["敏感词示例1", "敏感词示例2"] "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", "platform": "wechat",
@@ -93,10 +100,18 @@ def import_initial_data():
"icon": "💚", "icon": "💚",
"default_format": "公众号图文,800-1500字,亲切口语化", "default_format": "公众号图文,800-1500字,亲切口语化",
"compliance_rules": { "compliance_rules": {
"max_length": 20000, "max_title_len": 32,
"requires_authentication": True "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", "platform": "xiaohongshu",
@@ -104,11 +119,18 @@ def import_initial_data():
"icon": "📕", "icon": "📕",
"default_format": "图文笔记,300-800字,emoji+标签", "default_format": "图文笔记,300-800字,emoji+标签",
"compliance_rules": { "compliance_rules": {
"max_length": 1000, "max_title_len": 50,
"requires_tags": True, "allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"],
"max_tags": 10 "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: for p in platforms:
+14
View File
@@ -361,6 +361,13 @@ class PlatformConfig(Base):
compliance_rules = Column(JSON, default=dict) compliance_rules = Column(JSON, default=dict)
default_format = Column(Text, nullable=True) default_format = Column(Text, nullable=True)
is_active = Column(Boolean, default=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()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@@ -375,6 +382,13 @@ class PlatformConfig(Base):
"compliance_rules": self.compliance_rules or {}, "compliance_rules": self.compliance_rules or {},
"default_format": self.default_format, "default_format": self.default_format,
"is_active": self.is_active, "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, "created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None,
} }
+14
View File
@@ -263,6 +263,13 @@ class PlatformConfigBase(BaseModel):
compliance_rules: Dict[str, Any] = {} compliance_rules: Dict[str, Any] = {}
default_format: Optional[str] = None default_format: Optional[str] = None
is_active: bool = True 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): class PlatformConfigCreate(PlatformConfigBase):
@@ -278,6 +285,13 @@ class PlatformConfigUpdate(BaseModel):
compliance_rules: Optional[Dict[str, Any]] = None compliance_rules: Optional[Dict[str, Any]] = None
default_format: Optional[str] = None default_format: Optional[str] = None
is_active: Optional[bool] = 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): class PlatformConfigResponse(PlatformConfigBase):
+50 -24
View File
@@ -78,52 +78,74 @@
<span class="platform-meta-label">平台标识</span> <span class="platform-meta-label">平台标识</span>
<span class="platform-meta-value">{{ p.platform }}</span> <span class="platform-meta-value">{{ p.platform }}</span>
</div> </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-label">平台名称</span>
<span class="platform-meta-value">{{ p.platform_name }}</span> <span class="platform-meta-value">{{ p.name }}</span>
</div> </div>
<div v-if="p.min_words || p.max_words" class="platform-meta-item"> <div v-if="p.min_words || p.max_words" class="platform-meta-item">
<span class="platform-meta-label">字数限制</span> <span class="platform-meta-label">字数限制</span>
<span class="platform-meta-value">{{ p.min_words || 0 }} - {{ p.max_words || '不限' }}</span> <span class="platform-meta-value">{{ p.min_words || 0 }} - {{ p.max_words || '不限' }}</span>
</div> </div>
<div class="platform-meta-item">
<span class="platform-meta-label">需要配图</span>
<span class="platform-meta-value">{{ p.requires_image ? '是' : '否' }}</span>
</div>
</div> </div>
<div v-if="p.format_rules && Object.keys(p.format_rules).length > 0" class="rules-section"> <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 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 v-for="(rule, key) in p.format_template" :key="key" class="rule-item">
<strong>{{ key }}:</strong> {{ typeof rule === 'object' ? JSON.stringify(rule) : rule }} <strong>{{ key }}:</strong> {{ typeof rule === 'object' ? JSON.stringify(rule) : rule }}
</div> </div>
</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 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>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
<el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="600px"> <el-dialog v-model="showEditDialog" :title="isEditing ? '编辑平台' : '新增平台'" width="650px">
<el-form :model="platformForm" label-width="100px"> <el-form :model="platformForm" label-width="120px">
<el-form-item label="平台标识" :required="!isEditing"> <el-form-item label="平台标识" :required="!isEditing">
<el-input v-model="platformForm.platform" :disabled="isEditing" placeholder="如: zhihu, wechat, xiaohongshu"></el-input> <el-input v-model="platformForm.platform" :disabled="isEditing" placeholder="如: zhihu, wechat, xiaohongshu"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="平台名称"> <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>
<el-form-item label="启用状态"> <el-form-item label="启用状态">
<el-switch v-model="platformForm.is_active"></el-switch> <el-switch v-model="platformForm.is_active"></el-switch>
</el-form-item> </el-form-item>
<el-form-item label="标题模板"> <el-form-item label="默认格式">
<el-input v-model="platformForm.title_template" placeholder="如: 【{{tag}}】{{title}}"></el-input> <el-input v-model="platformForm.default_format" type="textarea" :rows="2" placeholder="如: 公众号图文,800-1500字,亲切口语化"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="正文模板"> <el-divider content-position="left">字数与配图</el-divider>
<el-input v-model="platformForm.body_template" type="textarea" :rows="4" placeholder="如: {{content}}"></el-input> <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>
<el-form-item label="字数限制"> <el-form-item label="最多字数">
<el-input-number v-model="platformForm.min_words" :min="0" placeholder="最少"></el-input-number> <el-input-number v-model="platformForm.max_words" :min="0" :max="100000"></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> </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> </el-form>
<template #footer> <template #footer>
<el-button @click="showEditDialog = false">取消</el-button> <el-button @click="showEditDialog = false">取消</el-button>
@@ -147,7 +169,7 @@ const PlatformsApp = {
showEditDialog: false, showEditDialog: false,
isEditing: false, isEditing: false,
saving: 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: { methods: {
@@ -188,12 +210,16 @@ const PlatformsApp = {
this.isEditing = true; this.isEditing = true;
this.platformForm = { this.platformForm = {
platform: p.platform, platform: p.platform,
platform_name: p.platform_name || '', name: p.name || '',
is_active: p.is_active, is_active: p.is_active,
title_template: p.title_template || '', default_format: p.default_format || '',
body_template: p.body_template || '', requires_image: p.requires_image || false,
min_words: p.min_words, image_count_min: p.image_count_min || 0,
max_words: p.max_words 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; this.showEditDialog = true;
}, },
+25 -14
View File
@@ -40,8 +40,19 @@ PLATFORM_RULES = {
class ComplianceChecker: class ComplianceChecker:
"""合规审查器""" """合规审查器"""
def __init__(self): def __init__(self, platform_config: Dict = None):
self.issues = [] 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: def check_text(self, text: str, platform: str, topic_data: Dict = None) -> Dict:
"""执行全面合规检查""" """执行全面合规检查"""
@@ -94,19 +105,21 @@ class ComplianceChecker:
rules = PLATFORM_RULES.get(platform, {}) rules = PLATFORM_RULES.get(platform, {})
# 标题长度(从HTML中提取) # 标题长度(从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) 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)) title_len = len(title_match.group(1))
if title_len > rules["max_title_len"]: if title_len > max_title_len:
self.issues.append({ self.issues.append({
"type": "平台规则", "type": "平台规则",
"category": "标题长度", "category": "标题长度",
"detail": f"标题{title_len}字,超过{platform}限制{rules['max_title_len']}", "detail": f"标题{title_len}字,超过{platform}限制{max_title_len}",
"suggestion": "缩短标题" "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): if re.search(pattern, text):
self.issues.append({ self.issues.append({
"type": "平台规则", "type": "平台规则",
@@ -123,17 +136,16 @@ class ComplianceChecker:
# 过滤掉纯十六进制颜色码(如 #1a1a1a, #fff # 过滤掉纯十六进制颜色码(如 #1a1a1a, #fff
tags = [t for t in tags if not re.fullmatch(r'[0-9a-fA-F]{3,6}', t)] tags = [t for t in tags if not re.fullmatch(r'[0-9a-fA-F]{3,6}', t)]
else: else:
# 没有标签容器时,不检查标签
tags = [] tags = []
allowed = rules.get("allowed_tags", []) allowed_tags = self._get_platform_rule('allowed_tags', rules.get("allowed_tags", []))
if allowed: if allowed_tags:
for tag in tags: for tag in tags:
if tag not in allowed: if tag not in allowed_tags:
self.issues.append({ self.issues.append({
"type": "平台规则", "type": "平台规则",
"category": "标签合规", "category": "标签合规",
"tag": tag, "tag": tag,
"suggestion": f"使用平台允许的标签,如{', '.join(allowed[:3])}" "suggestion": f"使用平台允许的标签,如{', '.join(allowed_tags[:3])}"
}) })
def _check_legal_compliance(self, text: str): def _check_legal_compliance(self, text: str):
@@ -230,10 +242,9 @@ class ComplianceChecker:
def _check_min_length(self, text: str, platform: str): def _check_min_length(self, text: str, platform: str):
"""检查文章最小字数(去除HTML标签)""" """检查文章最小字数(去除HTML标签)"""
# 简单去除HTML标签
plain = re.sub(r'<[^>]+>', '', text) plain = re.sub(r'<[^>]+>', '', text)
word_count = len(plain.strip()) 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: if word_count < min_words:
self.issues.append({ self.issues.append({
"type": "内容完整度", "type": "内容完整度",
@@ -281,9 +292,9 @@ class ComplianceChecker:
"detail": f"使用过时年份: {', '.join(sorted(outdated))},需更新为2025年及以后的数据", "detail": f"使用过时年份: {', '.join(sorted(outdated))},需更新为2025年及以后的数据",
"suggestion": "替换为最新数据,或使用'近期'等模糊表述" "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) return checker.check_text(html_content, platform, topic_data)
if __name__ == "__main__": if __name__ == "__main__":
+16 -1
View File
@@ -185,6 +185,17 @@ def optimize_article(html: str, platform: str, topic_data: Dict, remaining_issue
logs.append(pol_log) logs.append(pol_log)
return html, logs 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): def main(topic_ids: List[str] = None):
logger.info("=== 合规审查与优化开始 ===") logger.info("=== 合规审查与优化开始 ===")
llm_cfg = get_llm_config() llm_cfg = get_llm_config()
@@ -193,6 +204,9 @@ def main(topic_ids: List[str] = None):
else: else:
logger.info("LLM 配置: 使用环境变量默认值") logger.info("LLM 配置: 使用环境变量默认值")
platform_configs = _load_platform_configs()
logger.info(f"已加载 {len(platform_configs)} 个平台配置")
articles = get_articles_from_db(topic_ids) articles = get_articles_from_db(topic_ids)
if not articles: if not articles:
logger.warning("未找到任何文章(可能尚未创作或同步到 DB)") logger.warning("未找到任何文章(可能尚未创作或同步到 DB)")
@@ -216,7 +230,8 @@ def main(topic_ids: List[str] = None):
logger.warning(f"未找到选题: {topic_id}") logger.warning(f"未找到选题: {topic_id}")
continue 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'] issues = check_result['issues']
score = check_result['score'] score = check_result['score']
label = f"{platform_dir}/{topic_id}" label = f"{platform_dir}/{topic_id}"
+11
View File
@@ -351,6 +351,17 @@ class Writer:
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME) html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME)
html_content = _md_parser(adapted) 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) html = html.replace("<!-- CONTENT -->", html_content)
tags_html = self._get_platform_tags(platform) tags_html = self._get_platform_tags(platform)