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:
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
"""发送企业微信通知"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user