feat: 数据源统一与前端预览修复

=== 后端核心 ===
- db_helper: 统一数据库访问抽象层
- system.py API:
  * 参数绑定修复: 使用 Body(embed=True) 接收 JSON
  * 添加请求日志记录
- sync.py: 仅导出 DB→JSON(备份)

=== 合规与流水线 ===
- compliance_checker: 标签检测优化(仅检查容器,避免正文误判)
- 所有脚本(creator/collector/writer/outline/research等)统一使用数据库

=== 前端改版 ===
- topics.html:
  * 创作/优化 API 路径修正
  * 预览弹窗重设计:多平台并行加载、富文本显示、单复制按钮
  * 状态中文映射(getStatusLabel)
  * 认证检查
- 所有 HTML 静态资源路径修复(移除 /static 前缀)

=== 数据一致性 ===
- 数据库状态统一为英文(pending/review/ready/published)
- 前端显示中文化映射

已测试 A03 流水线完整通过。
This commit is contained in:
lt
2026-05-07 11:25:42 +08:00
parent 8dd19a2179
commit 31d6306e3b
24 changed files with 1018 additions and 530 deletions
+72 -9
View File
@@ -1,10 +1,73 @@
#!/usr/bin/env python3
import json
data = json.load(open('automation/data/sustainability_topics.json', 'r', encoding='utf-8'))
for t in data:
if t['id'] == 'D01':
t['priority_score'] = 11
elif t['id'] == 'B05':
t['priority_score'] = 10
json.dump(data, open('automation/data/sustainability_topics.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
print('优先级调整完成:D01=11, B05=10')
"""
调整选题优先级(数据库 + JSON 备份)
"""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
try:
from db_helper import get_topic_by_id, update_topic_status
from app.database import SessionLocal
from app.models import Topic as DBTopic
HAVE_DB = True
except ImportError:
HAVE_DB = False
print("Warning: db_helper not available, will only update JSON")
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
def adjust_json_priority(adjustments):
try:
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
except:
topics = []
updated = []
for t in topics:
if t['id'] in adjustments:
old = t.get('priority_score', 0)
t['priority_score'] = adjustments[t['id']]
updated.append(f"{t['id']}: {old} -> {adjustments[t['id']]}")
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
return updated
def adjust_db_priority(adjustments):
if not HAVE_DB:
return []
updated = []
db = SessionLocal()
try:
for tid, new_score in adjustments.items():
topic = db.query(DBTopic).filter(DBTopic.id == tid).first()
if topic:
topic.priority_score = new_score
topic.updated_at = datetime.now()
updated.append(f"{tid}: {topic.priority_score} -> {new_score}")
db.commit()
finally:
db.close()
return updated
def main():
# 定义需要调整的优先级:ID -> 新分数
adjustments = {
'D01': 11,
'B05': 10
}
print("调整优先级...")
db_updated = adjust_db_priority(adjustments) if HAVE_DB else []
if db_updated:
print("[DB] updated:", ', '.join(db_updated))
json_updated = adjust_json_priority(adjustments)
print("[JSON] updated:", ', '.join(json_updated))
print("完成")
if __name__ == "__main__":
import json, datetime
main()
+48 -60
View File
@@ -1,40 +1,62 @@
#!/usr/bin/env python3
"""
批量合规审查脚本
批量合规审查脚本 - 数据库版
遍历指定日期所有发布版本,执行合规检查,生成汇总报告
"""
import json
import re
import json, re, datetime
from pathlib import Path
from datetime import datetime
import sys
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.compliance_checker import check_article
# 尝试导入数据库
try:
from db_helper import export_topics_to_json
HAVE_DB = True
except ImportError:
HAVE_DB = False
# 配置
RELEASE_DIR = PROJECT_ROOT / "automation" / "data" / "releases"
TOPICS_FILE = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
TODAY = "2026-04-16" # 可参数化
TODAY = datetime.date.today().isoformat() # 默认今天,可修改
def load_topics():
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def load_topics_from_db():
if not HAVE_DB:
raise RuntimeError("Database not available")
topics = export_topics_to_json()
return {t['id']: t for t in topics}
def extract_topic_id(filename: str) -> str:
"""从文件名提取 topic ID,如 zhihu_A01_zhihu.html -> A01"""
parts = filename.stem.split('_')
def load_topics_from_json():
json_path = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
with open(json_path, 'r', encoding='utf-8') as f:
topics = json.load(f)
return {t['id']: t for t in topics}
def extract_topic_id(filename: Path) -> str:
stem = filename.stem
parts = stem.split('_')
if len(parts) >= 2:
return parts[1]
return None
def main():
topics = load_topics()
topics_by_id = {t['id']: t for t in topics}
def main(target_date: str = None):
if target_date is None:
target_date = TODAY
print(f"批量合规审查: {target_date}")
release_path = RELEASE_DIR / TODAY
# 加载选题数据(优先DB,失败则备援JSON)
try:
topics_by_id = load_topics_from_db()
print("[数据源] 数据库")
except Exception as e:
print(f"[数据源] 数据库失败: {e}, 改用 JSON")
topics_by_id = load_topics_from_json()
release_path = RELEASE_DIR / target_date
if not release_path.exists():
print(f"错误:发布日期目录不存在 {release_path}")
return
@@ -48,11 +70,9 @@ def main():
topic_id = extract_topic_id(html_file)
topic_data = topics_by_id.get(topic_id) if topic_id else None
# 读取HTML
with open(html_file, 'r', encoding='utf-8') as f:
html_content = f.read()
# 执行合规检查
result = check_article(html_content, platform, topic_data)
result['file'] = str(html_file.relative_to(PROJECT_ROOT))
result['platform'] = platform
@@ -60,49 +80,17 @@ def main():
result['topic_title'] = topic_data.get('title') if topic_data else "未知"
results.append(result)
status = "✅ PASS" if result['passed'] else "❌ FAIL"
print(f"{status} {topic_id} {platform:12} {result['topic_title'][:30]:30} 问题数: {len(result['issues'])} 得分: {result['score']}")
# 汇总报告
# 输出摘要
passed = sum(1 for r in results if r['passed'])
failed = len(results) - passed
avg_score = sum(r['score'] for r in results) / len(results) if results else 0
print(f"\n========== 合规审查汇总 ==========")
print(f"总计: {len(results)}")
print(f"通过: {passed}")
print(f"失败: {failed}")
print(f"平均分: {avg_score:.1f}")
# 保存详细报告
report = {
"date": TODAY,
"summary": {
"total": len(results),
"passed": passed,
"failed": failed,
"average_score": avg_score
},
"details": results
}
report_file = PROJECT_ROOT / "automation" / "data" / "drafts" / TODAY / "compliance_summary.json"
report_file.parent.mkdir(parents=True, exist_ok=True)
with open(report_file, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"\n📁 详细报告已保存: {report_file}")
# 列出失败项
if failed > 0:
print("\n⚠️ 需要修复的文章:")
for r in results:
if not r['passed']:
print(f" {r['file']}")
for issue in r['issues'][:3]: # 只显示前3个问题
print(f" - {issue['type']}/{issue.get('category','')}: {issue.get('suggestion','')}")
if len(r['issues']) > 3:
print(f" ... 等共{len(r['issues'])}个问题")
else:
print("\n🎉 所有文章均通过合规审查!")
print(f"\n✅ 通过: {passed}, ⚠️ 需人工: {failed}")
for r in results:
status = "" if r['passed'] else "⚠️"
print(f" {status} {r['topic_id']} {r['topic_title'][:40]}...")
if __name__ == "__main__":
main()
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--date', help='审查的日期目录,默认今天')
args = parser.parse_args()
main(args.date)
+45 -7
View File
@@ -1,8 +1,46 @@
#!/usr/bin/env python3
import json
data = json.load(open('automation/data/sustainability_topics.json', 'r', encoding='utf-8'))
for t in data:
if t['id'] == 'B05':
t['priority_score'] = 15
print(f"B05 priority_score set to {t['priority_score']}")
json.dump(data, open('automation/data/sustainability_topics.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
try:
from db_helper import update_topic_status
from app.database import SessionLocal
from app.models import Topic as DBTopic
HAVE_DB = True
except ImportError:
HAVE_DB = False
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
def adjust(adjustments):
db_ok = False
if HAVE_DB:
db = SessionLocal()
try:
for tid, new_score in adjustments.items():
topic = db.query(DBTopic).filter(DBTopic.id == tid).first()
if topic:
topic.priority_score = new_score
topic.updated_at = datetime.datetime.now()
db.commit()
db_ok = True
finally:
db.close()
try:
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t['id'] in adjustments:
t['priority_score'] = adjustments[t['id']]
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
except:
pass
return db_ok
def main():
adjustments = {'B05': 15}
adjust(adjustments)
print("B05 priority_score set to 15")
if __name__ == "__main__":
import json, datetime
main()
+45 -7
View File
@@ -1,8 +1,46 @@
#!/usr/bin/env python3
import json
data = json.load(open('automation/data/sustainability_topics.json', 'r', encoding='utf-8'))
for t in data:
if t and t.get('id') == 'D01':
t['priority_score'] = 12
print(f"D01 priority_score set to {t['priority_score']}")
json.dump(data, open('automation/data/sustainability_topics.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
try:
from db_helper import update_topic_status
from app.database import SessionLocal
from app.models import Topic as DBTopic
HAVE_DB = True
except ImportError:
HAVE_DB = False
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
def adjust(adjustments):
db_ok = False
if HAVE_DB:
db = SessionLocal()
try:
for tid, new_score in adjustments.items():
topic = db.query(DBTopic).filter(DBTopic.id == tid).first()
if topic:
topic.priority_score = new_score
topic.updated_at = datetime.datetime.now()
db.commit()
db_ok = True
finally:
db.close()
try:
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t['id'] in adjustments:
t['priority_score'] = adjustments[t['id']]
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
except:
pass
return db_ok
def main():
adjustments = {'D01': 12}
adjust(adjustments)
print("D01 priority_score set to 12")
if __name__ == "__main__":
import json, datetime
main()
+42 -21
View File
@@ -41,6 +41,7 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
@dataclass
class SustainabilitySource:
"""可持续性信息源"""
@@ -511,33 +512,53 @@ class SustainabilityCollector:
logger.info(f"保存了 {len(self.new_cases)} 个案例和 {len(self.new_topics)} 个选题")
def update_main_database(self):
"""更新主数据库(简化版)"""
# 实际应更新Notion/数据库,这里仅保存到文件
"""更新主数据库和JSON备份"""
# 1. 更新案例库 (sustainability_cases.json)
main_cases_file = DATA_DIR / "sustainability_cases.json"
main_topics_file = DATA_DIR / "sustainability_topics.json"
# 读取现有数据
existing_cases = []
existing_topics = []
if main_cases_file.exists():
with open(main_cases_file, 'r', encoding='utf-8') as f:
existing_cases = json.load(f)
if main_topics_file.exists():
with open(main_topics_file, 'r', encoding='utf-8') as f:
existing_topics = json.load(f)
# 合并新数据
try:
with open(main_cases_file, 'r', encoding='utf-8') as f:
existing_cases = json.load(f)
except:
existing_cases = []
all_cases = existing_cases + [asdict(case) for case in self.new_cases]
all_topics = existing_topics + [asdict(topic) for topic in self.new_topics]
# 保存(限制总数)
# 去重
seen = set()
unique_cases = []
for c in all_cases:
title = c.get('title', '').strip()
if title and title not in seen:
seen.add(title)
unique_cases.append(c)
unique_cases.sort(key=lambda x: x.get('collection_date', ''), reverse=True)
with open(main_cases_file, 'w', encoding='utf-8') as f:
json.dump(all_cases[:100], f, ensure_ascii=False, indent=2)
json.dump(unique_cases[:200], f, ensure_ascii=False, indent=2)
logger.info(f"案例库更新: 总计 {len(unique_cases)} 个案例 (新增 {len(self.new_cases)})")
with open(main_topics_file, 'w', encoding='utf-8') as f:
json.dump(all_topics[:50], f, ensure_ascii=False, indent=2)
# 2. 更新选题数据库 (主数据源)
try:
from db_helper import save_topics_to_db
save_topics_to_db([asdict(topic) for topic in self.new_topics])
logger.info(f"选题数据库更新: 处理了 {len(self.new_topics)} 个选题")
except Exception as e:
logger.error(f"选题数据库保存失败: {e}")
# 3. 可选: 更新 JSON 备份 (仅新增,避免覆盖锁信息)
main_topics_file = DATA_DIR / "sustainability_topics.json"
try:
if main_topics_file.exists():
with open(main_topics_file, 'r', encoding='utf-8') as f:
existing_topics = json.load(f)
else:
existing_topics = []
existing_ids = {t['id'] for t in existing_topics}
new_additions = [asdict(topic) for topic in self.new_topics if topic.id not in existing_ids]
existing_topics.extend(new_additions)
with open(main_topics_file, 'w', encoding='utf-8') as f:
json.dump(existing_topics, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"JSON备份失败: {e}")
def send_wecom_notification(self):
"""发送企业微信通知"""
+10 -5
View File
@@ -115,11 +115,16 @@ class ComplianceChecker:
"suggestion": "移除违规内容或联系方式"
})
# 标签检查(只匹配 #话题 格式,排除颜色码如 #1a1a1a
# 标签模式:#开头,后跟字母数字,长度2-10,不全是十六进制字符
tags = re.findall(r'#([A-Za-z0-9\u4e00-\u9fa5]{2,10})', text)
# 过滤掉纯十六进制颜色码(如 #1a1a1a, #fff
tags = [t for t in tags if not re.fullmatch(r'[0-9a-fA-F]{3,6}', t)]
# 标签检查:仅检查专门的标签容器(避免误伤正文中的话题引用
tags_container_match = re.search(r'<div class="tags">([^<]+)</div>', text) or re.search(r'<div class="hashtags">([^<]+)</div>', text)
if tags_container_match:
tags_text = tags_container_match.group(1)
tags = re.findall(r'#([A-Za-z0-9一-龥]{2,10})', tags_text)
# 过滤掉纯十六进制颜色码(如 #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:
for tag in tags:
+33 -59
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""
合规审查与优化任务
合规审查与优化任务(数据库版)
每天 05:45 运行,处理当天所有 draft 文章:
1. 执行合规检查(compliance_checker
2. 自动修复已知问题(标题、标签)
3. 重写合规版本
4. 更新选题状态为「审查通过待发布」
4. 更新选题状态为「待发布」
5. 生成优化报告通知
"""
@@ -26,10 +26,12 @@ try:
except ImportError:
HAVE_LLM = False
# 导入数据库辅助模块
from db_helper import get_topic_by_id, update_topic_status
DATA_DIR = PROJECT_ROOT / "automation" / "data"
RELEASES_DIR = DATA_DIR / "releases"
DRAFTS_DIR = DATA_DIR / "drafts"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -55,42 +57,15 @@ class OptimizationResult:
status: str
def load_topic_map():
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
"""从数据库加载所有选题数据"""
from db_helper import export_topics_to_json
topics = export_topics_to_json()
return {t['id']: t for t in topics}
def update_topic_status(topic_id: str, status: str):
"""更新选题状态(JSON + 数据库)"""
# 更新 JSON
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
updated = False
for t in topics:
if t.get('id') == topic_id:
t['status'] = status
updated = True
break
if updated:
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
# 更新数据库
try:
import sys
from pathlib import Path
backend_path = Path(__file__).resolve().parents[2] / 'platform' / 'backend'
if str(backend_path) not in sys.path:
sys.path.insert(0, str(backend_path))
from app.database import SessionLocal
from app.models import Topic
db = SessionLocal()
topic_db = db.query(Topic).filter(Topic.id == topic_id).first()
if topic_db:
topic_db.status = status
db.commit()
db.close()
except Exception as e:
logger.error(f"更新数据库失败: {e}")
def update_topic_status_db_only(topic_id: str, status: str):
"""仅更新数据库状态(不更新JSON"""
from db_helper import update_topic_status
update_topic_status(topic_id, status)
def fix_wechat_title(html: str, title: str) -> str:
"""微信标题优化:<title>和<h1>都控制长度(考虑后缀)"""
@@ -101,7 +76,6 @@ def fix_wechat_title(html: str, title: str) -> str:
title_tag = re.search(r'<title>([^<]+)</title>', html)
if title_tag:
full_title = title_tag.group(1)
# 提取 base(去掉后缀)
if full_title.endswith(suffix):
base = full_title[:-len(suffix)]
else:
@@ -111,11 +85,10 @@ def fix_wechat_title(html: str, title: str) -> str:
new_full = base + suffix
html = html.replace(full_title, new_full)
# 处理 <h1>...</h1>(不含后缀,但要截断)
# 处理 <h1>...</h1>
h1_match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
if h1_match:
current_h1 = h1_match.group(1)
# 如果 h1 包含后缀(不应该),去掉
base_h1 = current_h1.split(" - ")[0] if " - " in current_h1 else current_h1
if len(base_h1) > 32:
base_h1 = base_h1[:29] + "..."
@@ -127,7 +100,6 @@ def fix_tags(html: str, platform: str) -> str:
"""强制替换标签为平台白名单"""
if platform == "zhihu":
tags_str = " ".join(f"#{t}" for t in PLATFORM_TAGS["zhihu"])
# 替换 <div class="tags">...</div>
if '<div class="tags">' in html:
old = html.split('<div class="tags">')[1].split('</div>')[0]
html = html.replace(f'<div class="tags">{old}</div>', f'<div class="tags">{tags_str}</div>')
@@ -140,18 +112,14 @@ def fix_tags(html: str, platform: str) -> str:
def optimize_article(html: str, platform: str, topic_data: Dict) -> (str, List[str]):
logs = []
# 1. 标题优化(微信)
if platform == "wechat":
html = fix_wechat_title(html, topic_data.get("title", ""))
logs.append("标题截断(含后缀)")
# 2. 标签优化
if platform in ["zhihu", "xiaohongshu"]:
before = html
html = fix_tags(html, platform)
if html != before:
logs.append(f"标签标准化为{PLATFORM_TAGS[platform]}")
# 3. 图片内联检查
# 提取所有 img 标签
img_tags = re.findall(r'<img[^>]*>', html, re.IGNORECASE)
for tag in img_tags:
m = re.search(r'src=["\']([^"\']+)["\']', tag, re.IGNORECASE)
@@ -160,7 +128,6 @@ def optimize_article(html: str, platform: str, topic_data: Dict) -> (str, List[s
if not src.startswith('data:image/'):
logs.append(f"图片未内联: {src[:50]}... 需手动修复")
# 4. LLM 内容优化(使用 NVIDIA step-3.5-flash
if HAVE_LLM:
try:
polish_prompt = f"""你是一个专业的内容润色助手。请优化以下文章内容,提升表达的专业性和可读性,保持原文事实、数据、章节结构不变,输出相同的HTML格式(保留<h2>, <h3>, <p>标签)。
@@ -198,7 +165,6 @@ def main(topic_ids: List[str] = None):
if len(parts) < 2:
continue
topic_id = parts[1]
# 如果指定了 topic_ids,则只处理匹配的
if topic_ids is not None and topic_id not in topic_ids:
continue
topic_data = topic_map.get(topic_id)
@@ -227,7 +193,7 @@ def main(topic_ids: List[str] = None):
final_score=recheck['score'],
status="passed"
))
update_topic_status(topic_id, '待发布')
update_topic_status_db_only(topic_id, 'pending')
else:
logger.warning(f"⚠️ {html_file.name} 优化后仍有问题,需人工审核")
results.append(OptimizationResult(
@@ -255,20 +221,28 @@ def main(topic_ids: List[str] = None):
if not check_result['passed']:
all_passed = False
# 更新选题状态
# 更新选题状态 (JSON + 数据库)
for res in results:
if res.status == "passed":
tid = res.topic_id
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t.get('id') == tid:
t['status'] = '待发布'
t['ready_at'] = TODAY
t['compliance_score'] = res.final_score
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
# 更新数据库状态为 'pending'(待发布)
update_topic_status(tid, 'ready')
# 可选:同时更新 JSON 以保持兼容
# (已废弃,但保留更新,避免其他组件出错)
try:
json_path = DATA_DIR / "sustainability_topics.json"
with open(json_path, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t.get('id') == tid:
t['status'] = 'ready'
t['ready_at'] = TODAY
t['compliance_score'] = res.final_score
break
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.warning(f"更新 JSON 失败: {e}")
# 生成报告
report_file = DRAFTS_DIR / TODAY / "optimization_report.json"
+23 -66
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v2
宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v3 - DB version
"""
import json, datetime, logging, sys, subprocess
@@ -10,8 +10,11 @@ from typing import Dict
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
sys.path.insert(0, str(PROJECT_ROOT))
# 导入数据库辅助模块
from db_helper import get_topic_by_id, get_next_topic, update_topic_status
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json" # 保留用于备份
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -26,71 +29,35 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def select_next_topic(topic_id: str = None) -> Dict:
"""选择并锁定要创作的选题"""
def save_topics(topics_list):
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics_list, f, ensure_ascii=False, indent=2)
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
"""选择并锁定要创作的选题(从数据库)"""
if topic_id:
# 指定ID尝试直接锁定
topic = next((t for t in topics if t['id'] == topic_id), None)
# 指定ID查询数据库
topic = get_topic_by_id(topic_id)
if not topic:
raise ValueError(f"Topic {topic_id} not found")
# 检查状态:禁止已发布状态重新创作
current_status = topic.get('status')
if current_status in ['已发布', 'published']:
raise ValueError(f"Topic {topic_id} is already published, cannot recreate")
# 允许:待处理、待审查、待发布 等非已发布状态
# 加锁
topic['lock_by'] = 'creator'
topic['lock_at'] = datetime.datetime.now().isoformat()
save_topics(topics)
# 更新状态为「审查中」表示已经开始处理
update_topic_status(topic_id, 'review')
return topic
# 自动选择:优先选pending且无锁的
def is_available(t):
status = t.get('status')
# 只处理 pending 或 待处理
if status not in ['pending', '待处理']:
return False
# 检查锁
lock_by = t.get('lock_by')
if lock_by:
# 如果有人锁了,检查是否超时(>2小时)
lock_at_str = t.get('lock_at')
if lock_at_str:
try:
lock_at = datetime.datetime.fromisoformat(lock_at_str)
if (datetime.datetime.now() - lock_at).total_seconds() < 7200:
return False
except:
pass # 解析失败,认为是有效锁
else:
return False
return True
available = [t for t in topics if is_available(t)]
if not available:
# 自动选择:下一个待处理的选题
topic = get_next_topic(priority='') or get_next_topic()
if not topic:
raise ValueError("No available topics to create (all locked or wrong status)")
available.sort(key=lambda t: t.get('priority_score', 0), reverse=True)
chosen = available[0]
# 锁定
chosen['lock_by'] = 'creator'
chosen['lock_at'] = datetime.datetime.now().isoformat()
save_topics(topics)
return chosen
# 更新状态为「审查中」表示已锁定
update_topic_status(topic['id'], 'review')
return topic
def run_step(script_name: str, topic_id: str) -> bool:
"""运行一个流水线步骤(research/outline/writer"""
script_path = PROJECT_ROOT / "scripts" / script_name
cmd = ["python3", str(script_path), "--topic-id", topic_id]
logger.info(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT), capture_output=True, text=True, timeout=1800) # 30分钟超时,适应AI撰写
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT), capture_output=True, text=True, timeout=1800)
if result.returncode != 0:
logger.error(f"{script_name} 失败: {result.stderr}")
return False
@@ -119,41 +86,31 @@ def run_pipeline(topic_id: str = None) -> Dict:
# 1. 研究
if not run_step("research.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "research step failed"}
# 2. 大纲
if not run_step("outline.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "outline step failed"}
# 3. 撰写
if not run_step("writer.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "writer step failed"}
# 4. 合规优化(自动审核并标记为「待发布」)
if not run_optimizer_step(tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "optimizer step failed"}
logger.info(f"创作流水线完成: topic_id={tid}")
return {"ok": True, "topic_id": tid, "stdout": f"SUCCESS: Topic {tid} processed through full pipeline"}
except Exception as e:
logger.exception("流水线执行失败")
return {"ok": False, "error": str(e)}
finally:
# 清理锁(无论成功失败)
if tid:
try:
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
for t in topics:
if t.get('id') == tid:
# 如果成功或需要人工,保留状态,但清除锁
t['lock_by'] = None
t['lock_at'] = None
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
logger.debug(f"已清理选题锁: {tid}")
except Exception as ex:
logger.error(f"清理锁失败: {ex}")
update_topic_status(tid, 'pending')
return {"ok": False, "error": str(e)}
def main():
import argparse
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
数据库辅助模块:为自动化脚本提供统一的数据库访问
"""
import sys
from pathlib import Path
from datetime import datetime, date
from typing import Optional, Dict, List
# 添加项目根和 backend 路径
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
from app.database import SessionLocal
from app.models import Topic
from sqlalchemy.orm import Session
def get_topic_by_id(topic_id: str, db: Optional[Session] = None) -> Optional[Dict]:
close_db = False
if db is None:
db = SessionLocal()
close_db = True
try:
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
return None
return topic_to_dict(topic)
finally:
if close_db:
db.close()
def get_topics_by_status(status: str, db: Optional[Session] = None) -> List[Dict]:
close_db = False
if db is None:
db = SessionLocal()
close_db = True
try:
topics = db.query(Topic).filter(Topic.status == status).order_by(Topic.created_at).all()
return [topic_to_dict(t) for t in topics]
finally:
if close_db:
db.close()
def get_next_topic(priority: Optional[str] = None, db: Optional[Session] = None) -> Optional[Dict]:
"""获取下一个待处理的选题(状态为 pending/待处理)"""
close_db = False
if db is None:
db = SessionLocal()
close_db = True
try:
# 兼容两种状态表示
status_filter = ['pending', '待处理']
query = db.query(Topic).filter(Topic.status.in_(status_filter))
if priority:
query = query.filter(Topic.priority == priority)
topic = query.order_by(Topic.priority_score.desc().nullslast(), Topic.created_at.asc()).first()
return topic_to_dict(topic) if topic else None
finally:
if close_db:
db.close()
def update_topic_status(topic_id: str, status: str, db: Optional[Session] = None) -> bool:
close_db = False
if db is None:
db = SessionLocal()
close_db = True
try:
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
return False
topic.status = status
topic.updated_at = datetime.now()
if status in ['ready', 'published'] and topic.generated_at is None:
topic.generated_at = datetime.now()
db.commit()
return True
finally:
if close_db:
db.close()
def topic_to_dict(topic: Topic) -> Dict:
return {
'id': topic.id,
'title': topic.title,
'field': topic.field,
'format': topic.format,
'core_concept': topic.core_concept,
'audience_pain': topic.audience_pain,
'unique_angle': topic.unique_angle,
'priority': topic.priority,
'priority_score': topic.priority_score or 0,
'total_score': topic.total_score,
'status': topic.status,
'cases': topic.cases or [],
'source_file': topic.source_file,
'created_at': topic.created_at.isoformat() if topic.created_at else None,
'updated_at': topic.updated_at.isoformat() if topic.updated_at else None,
'ready_at': topic.ready_at.isoformat() if topic.ready_at else None,
'published_at': topic.published_at.isoformat() if topic.published_at else None,
'compliance_score': topic.compliance_score,
'platform_urls': topic.platform_urls or {},
'lock_by': None,
'lock_at': None,
}
def export_topics_to_json(db: Optional[Session] = None) -> List[Dict]:
close_db = False
if db is None:
db = SessionLocal()
close_db = True
try:
topics = db.query(Topic).order_by(Topic.created_at).all()
return [topic_to_dict(t) for t in topics]
finally:
if close_db:
db.close()
if __name__ == "__main__":
topics = export_topics_to_json()
print(f"Total topics: {len(topics)}")
for t in topics[:5]:
print(f"- {t['id']}: {t['title'][:50]} ({t['status']})")
def save_topics_to_db(topics_data: List[Dict]):
"""保存/更新选题列表到数据库"""
db = SessionLocal()
try:
for t in topics_data:
existing = db.query(Topic).filter(Topic.id == t['id']).first()
if existing:
# 更新字段
for field in ['title', 'field', 'format', 'core_concept', 'audience_pain', 'unique_angle', 'priority', 'priority_score', 'total_score', 'status', 'cases', 'source_file', 'compliance_score', 'platform_urls']:
setattr(existing, field, t.get(field, getattr(existing, field)))
if t.get('ready_at'):
try:
existing.ready_at = datetime.strptime(t['ready_at'], '%Y-%m-%d').date()
except:
pass
if t.get('published_at'):
try:
existing.published_at = datetime.strptime(t['published_at'], '%Y-%m-%d').date()
except:
pass
existing.updated_at = datetime.now()
else:
new_topic = Topic(
id=t['id'],
title=t['title'],
field=t.get('field', '可持续生活系统'),
format=t.get('format'),
core_concept=t.get('core_concept'),
audience_pain=t.get('audience_pain'),
unique_angle=t.get('unique_angle'),
priority=t.get('priority', ''),
priority_score=t.get('priority_score', 0),
total_score=t.get('total_score'),
status=t.get('status', 'pending'),
cases=t.get('cases', []),
source_file=t.get('source_file'),
ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None,
published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None,
compliance_score=t.get('compliance_score', 100),
platform_urls=t.get('platform_urls', {}),
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(new_topic)
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
+106 -64
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
"""
将 content/ideas/ 目录下的 Markdown 选题文件转换为 JSON 格式
供 content creator 脚本使用
将 content/ideas/ 目录下的 Markdown 选题文件转换为并导入数据库
"""
import os
@@ -12,13 +11,22 @@ from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 数据库导入
try:
from app.database import SessionLocal
from app.models import Topic as DBTopic
HAVE_DB = True
except ImportError as e:
HAVE_DB = False
print(f"[Warning] Database import failed: {e}")
IDEAS_DIR = PROJECT_ROOT / "content" / "ideas"
DATA_DIR = PROJECT_ROOT / "automation" / "data"
OUTPUT_FILE = DATA_DIR / "sustainability_topics.json"
OUTPUT_FILE = DATA_DIR / "sustainability_topics.json" # 仅备份,不再作为主数据源
def extract_field(content, field_name):
"""从 Markdown 中提取字段值"""
# 支持 **字段名**:值 或 字段名:值 格式
patterns = [
rf"\*\*{re.escape(field_name)}\*\*\s*[:]\s*(.+?)(?:\n|$)",
rf"{re.escape(field_name)}\s*[:]\s*(.+?)(?:\n|$)",
@@ -29,28 +37,9 @@ def extract_field(content, field_name):
return match.group(1).strip()
return None
def extract_list(content, start_keyword):
"""提取列表数据(如数据/案例)"""
lines = content.split('\n')
result = []
capturing = False
for line in lines:
if start_keyword in line:
capturing = True
continue
if capturing:
if line.strip().startswith(('**', '#', '-', '*', '1.', '2.')):
if re.match(r'^(#|\*\*|-|\*|\d+\.)\s', line):
result.append(line.strip())
elif line.strip() == '' or line.startswith('##'):
break
return result
def parse_evaluation_matrix(content):
"""解析选题评估矩阵表格"""
scores = {}
lines = content.split('\n')
in_table = False
for line in lines:
if '|' in line and '---' not in line and '维度' not in line:
parts = [p.strip() for p in line.split('|')]
@@ -69,78 +58,122 @@ def parse_evaluation_matrix(content):
return scores
def md_to_topic(md_path):
"""将单个 Markdown 文件转换为 topic 字典"""
with open(md_path, 'r', encoding='utf-8') as f:
content = f.read()
# 提取标题 (第一行 # 开头)
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
title = title_match.group(1).strip() if title_match else md_path.stem
# 提取基础字段
field = extract_field(content, '领域')
format_type = extract_field(content, '形式')
word_count = extract_field(content, '预估字数')
core_concept = extract_field(content, '核心观点')
audience_pain = extract_field(content, '受众痛点')
unique_angle = extract_field(content, '独特角度')
data_cases = extract_list(content, '数据/案例')
field = extract_field(content, '领域') or '可持续生活系统'
format_type = extract_field(content, '形式') or '趋势洞察 + 实操指南'
core_concept = extract_field(content, '核心观点') or ''
audience_pain = extract_field(content, '受众痛点') or ''
unique_angle = extract_field(content, '独特角度') or ''
estimated_days = extract_field(content, '预估完成时间')
priority_str = extract_field(content, '优先级')
priority_str = extract_field(content, '优先级') or ''
publish_date = extract_field(content, '预计发布时间')
status = extract_field(content, '状态') or '待处理'
# 解析优先级为分数
priority_map = {'': 10, '': 7, '': 4}
priority_score = priority_map.get(priority_str, 5)
# 解析评估矩阵
evaluation = parse_evaluation_matrix(content)
total_score = evaluation.get('总分', 0)
# 生成 topic ID
topic_id = md_path.stem.split('-')[0] # 如 "001-上海阳台种菜一年.md" -> "001"
# 生成 ID:从文件名提取前缀数字,如果没有则使用标题哈希
stem = md_path.stem # e.g., "001-上海阳台种菜一年"
m = re.match(r'^(\d{3})', stem)
if m:
num = m.group(1)
topic_id = f'M{num}' # M 系列表示手动导入
else:
import hashlib
short = hashlib.md5(title.encode()).hexdigest()[:6].upper()
topic_id = f'M{short}'
# 构建 topic 对象
topic = {
return {
"id": topic_id,
"title": title,
"field": field or "未知",
"format": format_type or "未指定",
"word_count": word_count,
"field": field,
"format": format_type,
"core_concept": core_concept,
"audience_pain": audience_pain,
"unique_angle": unique_angle,
"data_cases": data_cases,
"estimated_days": estimated_days,
"priority": priority_str,
"priority_score": priority_score if priority_score > 0 else (total_score if total_score > 0 else 5),
"publish_date": publish_date,
"status": status,
"evaluation": evaluation,
"priority_score": priority_score,
"total_score": total_score,
"cases": [], # 关联的案例ID列表,待填充
"status": status,
"cases": [],
"source_file": md_path.name,
"created_at": datetime.now().isoformat()
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"ready_at": publish_date,
"published_at": None,
"compliance_score": 100,
"platform_urls": {}
}
return topic
def save_to_db(topic_dict):
if not HAVE_DB:
print("数据库不可用,跳过入库")
return False
db = SessionLocal()
try:
existing = db.query(DBTopic).filter(DBTopic.id == topic_dict['id']).first()
if existing:
# 更新字段
for field in ['title', 'field', 'format', 'core_concept', 'audience_pain', 'unique_angle', 'priority', 'priority_score', 'total_score', 'status', 'cases', 'source_file', 'compliance_score', 'platform_urls']:
setattr(existing, field, topic_dict.get(field, getattr(existing, field)))
if topic_dict.get('ready_at'):
try:
existing.ready_at = datetime.strptime(topic_dict['ready_at'], '%Y-%m-%d').date()
except:
pass
existing.updated_at = datetime.now()
else:
# 新增
new_topic = DBTopic(
id=topic_dict['id'],
title=topic_dict['title'],
field=topic_dict['field'],
format=topic_dict['format'],
core_concept=topic_dict['core_concept'],
audience_pain=topic_dict['audience_pain'],
unique_angle=topic_dict['unique_angle'],
priority=topic_dict['priority'],
priority_score=topic_dict['priority_score'],
total_score=topic_dict['total_score'],
status=topic_dict['status'],
cases=topic_dict['cases'],
source_file=topic_dict['source_file'],
ready_at=datetime.strptime(topic_dict['ready_at'], '%Y-%m-%d').date() if topic_dict.get('ready_at') else None,
published_at=None,
compliance_score=topic_dict['compliance_score'],
platform_urls=topic_dict['platform_urls'],
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(new_topic)
db.commit()
return True
except Exception as e:
db.rollback()
print(f"数据库保存失败: {e}")
return False
finally:
db.close()
def main():
"""主函数:导入所有 Markdown 选题文件"""
if not IDEAS_DIR.exists():
print(f"错误:选题目录不存在 {IDEAS_DIR}")
return
# 只导入主选题文件(格式:NNN-标题.md),排除 research/compliance 等辅助文件
md_files = []
for f in IDEAS_DIR.glob("*.md"):
if f.name == "README.md":
continue
# 排除 research 和 compliance 文件
if f.name.endswith('-research.md') or f.name.endswith('-compliance.md'):
continue
# 匹配 001-xxx.md 格式
if re.match(r'^\d{3}-.+\.md$', f.name):
md_files.append(f)
@@ -156,23 +189,32 @@ def main():
topic = md_to_topic(md_file)
topics.append(topic)
print(f" 标题: {topic['title']}")
print(f" ID: {topic['id']}")
print(f" 总分: {topic['total_score']}")
print(f" 状态: {topic['status']}")
# 确保输出目录存在
# 保存 JSON 备份
DATA_DIR.mkdir(parents=True, exist_ok=True)
# 写入 JSON
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
print(f"\n✅ 已备份选题到 {OUTPUT_FILE}")
print(f"\n✅ 已导入 {len(topics)} 个选题到 {OUTPUT_FILE}")
# 导入数据库
if HAVE_DB:
success_count = 0
for t in topics:
if save_to_db(t):
success_count += 1
print(f"✅ 已导入 {success_count}/{len(topics)} 个选题到数据库")
else:
print("⚠️ 数据库不可用,仅生成了 JSON 备份")
# 统计
ready_topics = [t for t in topics if t['status'] != '已发布']
print(f"📊 可用选题数: {len(ready_topics)}")
avg_score = sum(t['total_score'] for t in ready_topics) / len(ready_topics) if ready_topics else 0
print(f"🎯 平均评分: {avg_score:.1f}")
if ready_topics:
avg_score = sum(t['total_score'] for t in ready_topics) / len(ready_topics)
print(f"📊 可用选题数: {len(ready_topics)}")
print(f"🎯 平均评分: {avg_score:.1f}")
if __name__ == "__main__":
main()
+8 -7
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
大纲阶段:基于研究笔记生成文章大纲
大纲阶段:基于研究笔记生成文章大纲(数据库版)
"""
import json, datetime, logging, sys
@@ -10,8 +10,10 @@ from typing import Dict
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 导入数据库辅助模块
from db_helper import get_topic_by_id
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
RESEARCH_DIR = DATA_DIR / "research"
OUTPUT_DIR = DATA_DIR / "outlines"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
@@ -33,11 +35,10 @@ class Outliner:
self.output_dir.mkdir(parents=True, exist_ok=True)
def _load_topic(self) -> Dict:
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
for t in topics:
if t['id'] == self.topic_id:
return t
raise ValueError(f"Topic {self.topic_id} not found")
topic = get_topic_by_id(self.topic_id)
if not topic:
raise ValueError(f"Topic {self.topic_id} not found")
return topic
def generate_outline(self) -> str:
"""生成文章大纲 Markdown(基于模板)"""
+8 -7
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
研究阶段:为选题收集资料并生成研究笔记
研究阶段:为选题收集资料并生成研究笔记(数据库版)
"""
import json, datetime, logging, sys, re
@@ -10,9 +10,11 @@ from typing import Dict, List
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 导入数据库辅助模块
from db_helper import get_topic_by_id
DATA_DIR = PROJECT_ROOT / "automation" / "data"
CASES_FILE = DATA_DIR / "sustainability_cases.json"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
OUTPUT_DIR = DATA_DIR / "research" # 研究笔记输出目录
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -30,11 +32,10 @@ class Researcher:
self.output_dir.mkdir(parents=True, exist_ok=True)
def _load_topic(self) -> Dict:
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
for t in topics:
if t['id'] == self.topic_id:
return t
raise ValueError(f"Topic {self.topic_id} not found")
topic = get_topic_by_id(self.topic_id)
if not topic:
raise ValueError(f"Topic {self.topic_id} not found")
return topic
def _load_cases(self) -> List[Dict]:
if CASES_FILE.exists():
+67 -9
View File
@@ -1,10 +1,68 @@
#!/usr/bin/env python3
import json
data = json.load(open('automation/data/sustainability_topics.json', 'r', encoding='utf-8'))
for t in data:
if t['id'] in ['D01', 'B05']:
t['status'] = '待处理'
if 'ready_at' in t:
del t['ready_at']
json.dump(data, open('automation/data/sustainability_topics.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
print('已重置选题状态:', [t['id'] for t in data if t['id'] in ['D01','B05']])
"""
重置指定选题状态为「待处理」(数据库 + JSON 备份)
"""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
try:
from db_helper import update_topic_status, get_topic_by_id
HAVE_DB = True
except ImportError:
HAVE_DB = False
print("Warning: db_helper not available, will only update JSON")
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
def reset_json_status(ids):
try:
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
except:
topics = []
updated_ids = []
for t in topics:
if t['id'] in ids:
t['status'] = 'pending'
if 'ready_at' in t:
del t['ready_at']
updated_ids.append(t['id'])
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
return updated_ids
def reset_db_status(ids):
if not HAVE_DB:
return []
updated = []
for tid in ids:
if update_topic_status(tid, 'pending'):
updated.append(tid)
return updated
def main():
# 指定要重置的ID列表
target_ids = ['D01', 'B05'] # 可修改
print(f"正在重置选题状态: {target_ids}")
# 更新数据库
db_updated = reset_db_status(target_ids) if HAVE_DB else []
if db_updated:
print(f"[DB] 已重置: {db_updated}")
else:
print("[DB] 未更新或数据库不可用")
# 更新 JSON 备份
json_updated = reset_json_status(target_ids)
print(f"[JSON] 已重置: {json_updated}")
print("完成")
if __name__ == "__main__":
import json
main()
+18 -60
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
撰写阶段:基于大纲和选题生成完整文章(三平台版本)
撰写阶段:基于大纲和选题生成完整文章(三平台版本)- 数据库版
"""
import json, datetime, logging, sys, re, subprocess
@@ -22,11 +22,13 @@ except ImportError as e:
logging.warning(f"LLM client unavailable: {e}")
HAVE_LLM = False
# 导入数据库辅助模块
from db_helper import get_topic_by_id, update_topic_status
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
OUTLINE_DIR = DATA_DIR / "outlines"
RELEASE_DIR = DATA_DIR / "releases"
TEMPLATES_DIR = PROJECT_ROOT / "automation" / "templates"
@@ -59,11 +61,10 @@ class Writer:
self.research_notes = research_file.read_text(encoding='utf-8') if research_file.exists() else ""
def _load_topic(self) -> Dict:
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
for t in topics:
if t['id'] == self.topic_id:
return t
raise ValueError(f"Topic {self.topic_id} not found")
topic = get_topic_by_id(self.topic_id)
if not topic:
raise ValueError(f"Topic {self.topic_id} not found")
return topic
def _clean_title(self, title: str) -> str:
"""去除标题中的指导性文字(如字数说明、MVP标记等)"""
@@ -117,7 +118,7 @@ class Writer:
if expanded and len(expanded.strip()) > len(content):
return expanded.strip()
else:
logger.warning("LLM 扩写结果为空或过短,使用占位")
logger.warning("LLM 扩写失败,返回占位")
raise ValueError("Empty expansion")
except Exception as e:
logger.warning(f"LLM 扩写失败: {e},使用占位内容")
@@ -127,13 +128,11 @@ class Writer:
return content
def generate_full_markdown(self) -> str:
"""根据大纲生成完整 Markdown 正文(不用原标题,全部由 LLM 扩写生成)"""
"""根据大纲生成完整 Markdown 正文"""
sections = self._parse_outline_sections()
parts = []
# 只保留 LLM 扩写的内容,不添加任何原始标题标记
for sec in sections:
# 如果内容极短,LLM 扩写后返回的完整段落中可能包含标题,我们不过滤
if sec.get('content'):
expanded = self._expand_section(sec)
parts.append(expanded + "\n\n")
@@ -153,11 +152,9 @@ class Writer:
template = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>{{TITLE}}</title></head><body><h1>{{TITLE}}</h1><!-- CONTENT --></body></html>"
# 替换变量
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME).replace("{{GEN_TIME}}", GEN_TIME)
html = template.replace("{{TITLE}}", title).replace("{{DATE}}", TODAY).replace("{{GEN_TIME}}", GEN_TIME)
# 注入内容 (简单处理:markdown 转 HTML 可以用 marked.js 或 simple转换,这里暂时用 <pre> 包裹或简单段落化)
# 为了快速展示,我们将 markdown 的段落转换为 <p> 标签
# 实际中建议使用 markdown 库(如 python-markdown)转换
# 注入内容
html_content = self._markdown_to_html(markdown)
html = html.replace("<!-- CONTENT -->", html_content)
@@ -169,7 +166,6 @@ class Writer:
hashtags = '<div class="hashtags">#AI #可持续 #生活方式</div>'
html = html.replace("<!-- HASHTAGS -->", hashtags)
elif platform == "wechat":
# 微信公众号可能还需要摘要等,模板已处理
pass
return html
@@ -193,7 +189,7 @@ class Writer:
elif line.strip():
html_parts.append(f"<p>{line}</p>")
else:
html_parts.append("") # 空行
html_parts.append("")
return "\n".join(html_parts)
def save_html(self, html: str, platform: str) -> Path:
@@ -206,48 +202,10 @@ class Writer:
return out_path
def mark_draft(self):
"""标记选题为「待发布」,同时更新数据库"""
# 更新 JSON 文件
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
updated = False
for t in topics:
if t.get('id') == self.topic_id:
t['status'] = '待审查'
updated = True
break
if updated:
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
# 更新数据库
db = SessionLocal()
try:
topic_db = db.query(Topic).filter(Topic.id == self.topic_id).first()
if topic_db:
topic_db.status = '待审查'
db.commit()
logger.info(f"选题 {self.topic_id} 状态已更新为待审查(数据库)")
else:
logger.warning(f"数据库中未找到选题 {self.topic_id}")
except Exception as e:
logger.error(f"更新数据库失败: {e}")
db.rollback()
finally:
db.close()
logger.info(f"选题 {self.topic_id} 状态更新为「待发布」(JSON")
"""标记选题为「待发布」"""
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t.get('id') == self.topic_id:
t['status'] = '待审查'
# ready_at 留空,待合规审核通过后设置
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
logger.info(f"选题 {self.topic_id} 状态更新为「待发布」")
"""标记选题为「待审查」"""
# 更新数据库状态
update_topic_status(self.topic_id, 'review')
logger.info(f"选题 {self.topic_id} 状态已更新为待审查(数据库)")
def run(self):
logger.info("开始撰写阶段")
@@ -257,7 +215,7 @@ class Writer:
html = self.generate_platform_html(markdown, platform)
results[platform] = str(self.save_html(html, platform))
self.mark_draft()
logger.info(f"撰写完成,状态改为 draft,待合规审核")
logger.info(f"撰写完成,状态已更新为待审查")
return {"ok": True, "files": results}
def main():