Files
yu-zhi-ran/platform/backend/app/initial_data.py
T
Yuzhiran Dev 499c511140 chore: opencode冗余清理 + LLM任务级模型选择 + systemd服务化
- 删除 opencode_search.py / mcp_search_server.py 及所有 MCP 引用
- 移除搜索缓存定时任务(scheduled_refresh_search_cache)
- 清理前后端所有 opencode/MCP 代码和注释
- LLM 提供商量换:opencode-go→nvidia(默认)+sensenova(合规审查)
- llm_configs 新增 is_default 字段,API 层互斥逻辑
- 所有定时任务支持独立 LLM 模型选择(LLM_TASK_PROVIDER env)
- compliance_optimizer.py 修复:import os / 解硬编码 / 关键词过滤
- Scheduler 日志修复:始终 INSERT,避免僵尸 running 行
- Systemd 服务化:Restart=always / 单 worker / Type=exec
- 搜索提供商:替换 opencode→360/搜狗/微信(免 Key)
- 更新 AGENTS.md / PROGRESS.md
2026-06-02 15:38:16 +08:00

316 lines
17 KiB
Python

import json
import os
from datetime import datetime
from pathlib import Path
from .database import SessionLocal, init_db
from .models import (
Topic, TopicField, TopicConfigField, TopicStatusConfig,
User, Case, LLMConfig, SystemConfig, PlatformConfig,
CollectorCategory, CollectorSource, Role, Menu, SearchProvider
)
import bcrypt
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
TOPICS_FILE = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
CASES_FILE = PROJECT_ROOT / "automation" / "data" / "initial_cases.json"
DEFAULT_ADMIN_USERNAME = os.getenv('DEFAULT_ADMIN_USERNAME', 'admin')
DEFAULT_ADMIN_PASSWORD = os.getenv('DEFAULT_ADMIN_PASSWORD', 'admin123')
def import_initial_data():
db = SessionLocal()
try:
if db.query(User).filter(User.username == DEFAULT_ADMIN_USERNAME).first() is None:
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
admin = User(
username=DEFAULT_ADMIN_USERNAME,
password_hash=hashed.decode('utf-8'),
role="admin",
org_id="default"
)
db.add(admin)
db.commit()
print(f"✅ 创建默认管理员: {DEFAULT_ADMIN_USERNAME}")
# 补充或更新 LLM 供应商配置(nvidia 为主,opencode-go 为备)
expected = {
"opencode-go": dict(provider="opencode-go", model="deepseek-v4-flash",
base_url="https://opencode.ai/zen/go/v1", temperature=0.7, max_tokens=131072, is_active=True,
user_prompt_template="将以下内容扩展为完整文章:\n{topic_title}\n{core_concept}"),
"nvidia": dict(provider="nvidia", model="stepfun-ai/step-3.5-flash",
base_url="https://integrate.api.nvidia.com/v1", temperature=0.5, max_tokens=131072, is_active=False,
user_prompt_template="将以下内容扩展为完整章节:\n{section_content}"),
}
existing = {c.name: c for c in db.query(LLMConfig).all()}
for name, cfg in expected.items():
if name in existing:
c = existing[name]
# 仅补缺失字段,不覆写用户已修改的值
for k, v in cfg.items():
if getattr(c, k, None) is None:
setattr(c, k, v)
else:
db.add(LLMConfig(name=name, **cfg))
db.commit()
print(f"✅ LLM 配置已同步: {', '.join(expected.keys())}")
default_system_configs = [
{"key": "collector_enabled", "value": "false", "description": "是否启用采集器"},
{"key": "scheduler_interval", "value": "daily", "description": "调度间隔:daily/hourly/weekly"},
]
for cfg in default_system_configs:
if db.query(SystemConfig).filter(SystemConfig.key == cfg["key"]).first() is None:
db.add(SystemConfig(**cfg))
if db.query(SystemConfig).filter(SystemConfig.key == "review_llm_id").first() is None:
first_llm = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
if first_llm:
db.add(SystemConfig(key="review_llm_id", value=str(first_llm.id), description="审查使用的 LLM 配置 ID(留空则用环境变量默认值)"))
db.commit()
print("✅ 插入默认系统配置")
# 初始化默认搜索 API 提供商
if db.query(SearchProvider).count() == 0:
providers = [
SearchProvider(name="百度千帆", provider_type="baidu", api_key="", api_url="https://qianfan.baidubce.com/v2/ai_search/web_search", console_url="https://console.bce.baidu.com/qianfan/ais/console/onlineService", priority=1, enabled=True, daily_limit=200),
SearchProvider(name="360搜索", provider_type="360", api_key="", api_url="", console_url="https://www.so.com", priority=0, enabled=True, daily_limit=99999),
SearchProvider(name="搜狗搜索", provider_type="sogou", api_key="", api_url="", console_url="https://sogou.com", priority=1, enabled=True, daily_limit=99999),
SearchProvider(name="微信搜一搜", provider_type="wechat", api_key="", api_url="", console_url="https://wx.sogou.com/weixin", priority=2, enabled=True, daily_limit=99999),
]
for p in providers:
db.add(p)
db.commit()
print("✅ 插入默认搜索 API 提供商")
if db.query(PlatformConfig).count() == 0:
platforms = [
{
"platform": "zhihu",
"name": "知乎",
"icon": "🔍",
"default_format": "长文深度分析,1500-3000字,有数据支撑",
"compliance_rules": {
"max_title_len": 100,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"forbidden_patterns": ["加微信", "私聊", "付费咨询", "点击领取"]
},
"is_active": True,
"requires_image": False,
"image_count_min": 0,
"image_count_max": 0,
"image_width": 0,
"image_height": 0,
"min_words": 1500,
"max_words": 3000
},
{
"platform": "wechat",
"name": "微信公众号",
"icon": "💚",
"default_format": "公众号图文,800-1500字,亲切口语化",
"compliance_rules": {
"max_title_len": 32,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"forbidden_patterns": ["诱导分享", "朋友圈", "转发群"]
},
"is_active": True,
"requires_image": True,
"image_count_min": 1,
"image_count_max": 3,
"image_width": 1080,
"image_height": 1080,
"min_words": 800,
"max_words": 1500
},
{
"platform": "xiaohongshu",
"name": "小红书",
"icon": "📕",
"default_format": "图文笔记,300-800字,emoji+标签",
"compliance_rules": {
"max_title_len": 50,
"allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"],
"forbidden_patterns": ["私信", "加群", "导流"]
},
"is_active": True,
"requires_image": True,
"image_count_min": 3,
"image_count_max": 6,
"image_width": 1080,
"image_height": 1440,
"min_words": 300,
"max_words": 800
}
]
for p in platforms:
db.add(PlatformConfig(**p))
db.commit()
print("✅ 插入平台配置")
if db.query(TopicField).count() == 0:
fields = [
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程工作、零工经济、职业转型", "sort_order": 1},
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具、数字助手、效率方法", "sort_order": 2},
{"name": "可持续生活", "icon": "🌿", "color": "#67c23a", "description": "环保、低碳、自然生活方式", "sort_order": 3},
{"name": "数字游民", "icon": "🌍", "color": "#409eff", "description": "旅行、地理自由、海外生活", "sort_order": 4},
{"name": "个人成长", "icon": "📚", "color": "#e6a23c", "description": "学习、技能、认知升级", "sort_order": 5},
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "科技伦理、数字生活反思", "sort_order": 6},
]
for f in fields:
db.add(TopicField(**f))
db.commit()
print("✅ 插入默认领域配置")
if db.query(CollectorCategory).count() == 0:
default_cats = [
{"name": "循环消费", "search_query": "以旧换新 二手交易 闲置 循环 2026", "description": "以旧换新/二手交易/租赁经济 | 2025年二手交易额1.69万亿", "sort_order": 1, "is_active": True},
{"name": "低碳出行", "search_query": "新能源车 骑行 绿色通勤 低碳出行 2026", "description": "新能源车/骑行/绿色出行 | 年产销破1000万辆", "sort_order": 2, "is_active": True},
{"name": "干净饮食", "search_query": "干净饮食 有机食品 植物基 本地食材 2026", "description": "有机食品/植物基/本地食材 | 有机食品1247亿", "sort_order": 3, "is_active": True},
{"name": "零浪费生活", "search_query": "零浪费 自带杯 极简生活 可持续时尚 2026", "description": "自带杯/极简/可持续时尚 | 自带杯笔记277万篇", "sort_order": 4, "is_active": True},
{"name": "绿色家电与节能", "search_query": "绿色家电 一级能效 以旧换新 节能 2026", "description": "一级能效/国补政策 | 一级能效占比90%+", "sort_order": 5, "is_active": True},
{"name": "碳普惠", "search_query": "碳账户 碳普惠 个人碳减排 蚂蚁森林 2026", "description": "碳账户/碳普惠/个人减排 | 武汉200万碳账户", "sort_order": 6, "is_active": True},
{"name": "环保科技产品", "search_query": "环保科技 绿色产品 可持续材料 2026", "description": "可持续材料/绿色产品 | 购买占比超32%", "sort_order": 7, "is_active": True},
{"name": "AI与效率", "search_query": "AI工具 人工智能 效率提升 2026", "description": "AI工具/效率方法/数字助手 | 2026年AI深度融入消费与生活", "sort_order": 8, "is_active": True},
]
for cd in default_cats:
existing = db.query(CollectorCategory).filter(CollectorCategory.name == cd["name"]).first()
if not existing:
db.add(CollectorCategory(**cd))
db.commit()
print("✅ 插入默认采集类别和信息源")
db.commit()
# 补充缺失的类别和信息源(对已有数据库的迁移)
for sd in [
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 人工智能 效率提升 2026", "credibility": "medium", "focus": "AI与效率", "sort_order": 99, "is_active": True},
]:
if not db.query(CollectorSource).filter(CollectorSource.name == sd["name"]).first():
db.add(CollectorSource(**sd))
db.commit()
if db.query(TopicStatusConfig).count() == 0:
statuses = [
{"status": "pending", "label": "待处理", "color": "#E6A23C", "icon": "", "sort_order": 1, "is_default": True},
{"status": "review", "label": "待审查", "color": "#F56C6C", "icon": "🔍", "sort_order": 2},
{"status": "draft", "label": "草稿", "color": "#909399", "icon": "📝", "sort_order": 3},
{"status": "ready", "label": "待发布", "color": "#67C23A", "icon": "", "sort_order": 4},
{"status": "published", "label": "已发布", "color": "#409EFF", "icon": "🚀", "sort_order": 5},
]
for s in statuses:
db.add(TopicStatusConfig(**s))
db.commit()
print("✅ 插入状态配置")
field_map = {}
for f in db.query(TopicField).all():
field_map[f.name] = f.id
if db.query(Topic).count() == 0:
if os.path.exists(TOPICS_FILE):
topics = json.loads(open(TOPICS_FILE, encoding='utf-8').read())
seen = {}
for t in topics:
seen[t['id']] = t
unique_topics = list(seen.values())
for t in unique_topics:
field_id = field_map.get(t.get('field'))
field_name = t.get('field')
topic = Topic(
id=t['id'],
field_id=field_id,
field_name=field_name,
title=t['title'],
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', []),
tags=t.get('tags', []),
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'),
platform_urls=t.get('platform_urls', {})
)
db.add(topic)
db.commit()
print(f"✅ 导入 {len(unique_topics)} 个选题")
else:
print(f"⚠️ 选题文件不存在: {TOPICS_FILE}")
if db.query(Case).count() == 0:
if os.path.exists(CASES_FILE):
cases_data = json.loads(open(CASES_FILE, encoding='utf-8').read())
for c in cases_data:
case = Case(
id=c['id'],
title=c['title'],
field=c['field'],
summary=c['summary'],
key_metrics=c.get('key_metrics'),
date=c.get('date'),
source=c['source'],
source_url=c.get('source_url'),
credibility_rating=c.get('credibility_rating'),
china_applicability=c.get('china_applicability')
)
db.add(case)
db.commit()
print(f"✅ 导入 {len(cases_data)} 条案例")
# 初始化默认角色
if db.query(Role).count() == 0:
db.add(Role(name="admin", description="系统管理员", is_system=True))
db.add(Role(name="editor", description="编辑人员", is_system=True))
db.commit()
print("✅ 插入默认角色")
# 初始化默认菜单(与 uni-nav.js 对齐)
if db.query(Menu).count() == 0:
default_menus = [
{"name": "仪表盘", "path": "/", "icon": "IconHome", "sort_order": 0, "roles": ["admin", "editor"]},
{"name": "选题", "path": "topics.html", "icon": "IconTopic", "sort_order": 1, "roles": ["admin", "editor"]},
{"name": "数据", "path": "metrics.html", "icon": "IconData", "sort_order": 2, "roles": ["admin", "editor"]},
{"name": "日历", "path": "calendar.html", "icon": "IconCalendar", "sort_order": 3, "roles": ["admin", "editor"]},
{"name": "素材", "path": "assets.html", "icon": "IconFolder", "sort_order": 4, "roles": ["admin", "editor"]},
{"name": "任务", "path": "tasks.html", "icon": "IconMenu", "sort_order": 5, "roles": ["admin", "editor"]},
{"name": "系统", "path": "admin.html", "icon": "IconSetting", "sort_order": 6, "roles": ["admin"]},
]
for m in default_menus:
db.add(Menu(**m))
db.commit()
print("✅ 插入默认菜单")
# 同步 PostgreSQL 自增序列
if os.getenv('USE_POSTGRES', 'true').lower() == 'true':
try:
from sqlalchemy import text
tables = ["cases", "users", "content_calendar", "media_assets", "content_metrics", "content_tasks", "audit_logs", "task_logs", "topic_config_fields"]
for table in tables:
db.execute(text(f"SELECT setval(pg_get_serial_sequence('{table}', 'id'), COALESCE((SELECT MAX(id) FROM {table}), 0) + 1, false)"))
db.commit()
print("✅ PostgreSQL 自增序列已同步")
except Exception as e:
print(f"⚠️ 序列同步警告: {e}")
print("✅ 初始化完成")
except Exception as e:
import traceback
traceback.print_exc()
print(f"初始化失败: {e}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
init_db()
import_initial_data()