9fba863ce7
initial_data: field descriptions humanized, collector search queries include long-tail keywords, platform formats updated collector.py: DEFAULT_CHINA_PAINS expanded with specific pain questions (e.g. '35岁学AI来得及吗') topic_selector.py: domain mapping 11->18 entries (Prompt/AI编程/AI写作/数据隐私/AI教育/一人企业) Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
338 lines
19 KiB
Python
338 lines
19 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 为主,sensenova 为备,含多模型)
|
|
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="qwen/qwen3.5-397b-a17b",
|
|
base_url="https://integrate.api.nvidia.com/v1", temperature=0.5, max_tokens=131072, is_active=False,
|
|
user_prompt_template="将以下内容扩展为完整章节:\n{section_content}"),
|
|
"sensenova-6.7-flash-lite": dict(provider="sensenova", model="sensenova-6.7-flash-lite",
|
|
base_url="https://token.sensenova.cn/v1", temperature=0.3, max_tokens=16384, is_active=True,
|
|
rate_limit=1500, rate_limit_window_minutes=300,
|
|
user_prompt_template="将以下内容扩展为完整章节:\n{section_content}"),
|
|
"sensenova-u1-fast": dict(provider="sensenova", model="sensenova-u1-fast",
|
|
base_url="https://token.sensenova.cn/v1", temperature=0.3, max_tokens=16384, is_active=True,
|
|
rate_limit=1500, rate_limit_window_minutes=300,
|
|
user_prompt_template="根据以下内容生成信息图:\n{section_content}"),
|
|
"sensenova-deepseek": dict(provider="sensenova", model="deepseek-v4-flash",
|
|
base_url="https://token.sensenova.cn/v1", temperature=0.3, max_tokens=16384, is_active=True,
|
|
rate_limit=500, rate_limit_window_minutes=300,
|
|
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": "深度分析 3000-8000字,数据驱动",
|
|
"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": 3000,
|
|
"max_words": 8000
|
|
},
|
|
{
|
|
"platform": "wechat",
|
|
"name": "微信公众号",
|
|
"icon": "💚",
|
|
"default_format": "个人叙事 2000-4000字,对话感",
|
|
"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": 2000,
|
|
"max_words": 4000
|
|
},
|
|
{
|
|
"platform": "xiaohongshu",
|
|
"name": "小红书",
|
|
"icon": "📕",
|
|
"default_format": "精炼干货 400-1000字,实用优先",
|
|
"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": 400,
|
|
"max_words": 1000
|
|
}
|
|
]
|
|
for p in platforms:
|
|
db.add(PlatformConfig(**p))
|
|
db.commit()
|
|
print("✅ 插入平台配置")
|
|
else:
|
|
# 更新已有平台配置的字数要求(迁移:2026-06 内容质量升级)
|
|
platform_updates = {
|
|
"zhihu": {"min_words": 3000, "max_words": 8000, "default_format": "深度分析 3000-8000字,数据驱动+观点交锋,用一手数据和独特视角切入"},
|
|
"wechat": {"min_words": 2000, "max_words": 4000, "default_format": "科技人文叙事 2000-4000字,个人反思+情感共鸣,记录人与技术之间的故事"},
|
|
"xiaohongshu": {"min_words": 400, "max_words": 1000, "default_format": "精炼干货 400-1000字,亲测数据+实操结果,每个结论配真实对比"},
|
|
}
|
|
for p in db.query(PlatformConfig).all():
|
|
if p.platform in platform_updates:
|
|
up = platform_updates[p.platform]
|
|
p.min_words = up["min_words"]
|
|
p.max_words = up["max_words"]
|
|
p.default_format = up["default_format"]
|
|
db.commit()
|
|
print("✅ 平台配置已更新(字数/格式)")
|
|
|
|
if db.query(TopicField).count() == 0:
|
|
fields = [
|
|
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具实测对比、工作流效率方法、前沿资讯解读——关注技术如何改变人的工作方式", "sort_order": 1},
|
|
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "AI伦理困境、数字生活反思、人机关系探索——科技的温度与边界", "sort_order": 2},
|
|
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程协作实践、AI时代职业转型、一人企业模式——未来不是等来的", "sort_order": 3},
|
|
]
|
|
for f in fields:
|
|
db.add(TopicField(**f))
|
|
db.commit()
|
|
print("✅ 插入默认领域配置(聚焦AI×人文×未来工作)")
|
|
|
|
if db.query(CollectorCategory).count() == 0:
|
|
default_cats = [
|
|
{"name": "AI前沿资讯", "search_query": "AI 人工智能 大模型 2026 前沿 突破 论文解读", "description": "AI行业动态、大模型发布、技术突破 | 2026年AI全面嵌入产业", "sort_order": 1, "is_active": True},
|
|
{"name": "AI工具实测", "search_query": "AI工具 效率提升 工作流 prompt教程 Cursor Copilot 2026", "description": "AI工具评测、效率工作流、实操指南 | 2026年AI工具爆发", "sort_order": 2, "is_active": True},
|
|
{"name": "AI与职场", "search_query": "AI 裁员 职业转型 AI技能 远程工作 一人企业 2026", "description": "AI对就业影响、职业转型、技能升级 | 2026年AI重塑就业结构", "sort_order": 3, "is_active": True},
|
|
{"name": "AI生活化", "search_query": "AI陪伴 AI心理咨询 生活助手 AI写作 智能体 2026", "description": "AI心理咨询/生活搭子/AI人格化 | 商业笔记互动增长263%", "sort_order": 4, "is_active": True},
|
|
{"name": "科技人文", "search_query": "AI伦理 数字生活 科技反思 人机关系 数据隐私 2026", "description": "AI伦理/数字生活反思/科技温度 | AI从工具到共生", "sort_order": 5, "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("✅ 插入默认采集类别(聚焦AI×科技人文)")
|
|
|
|
db.commit()
|
|
|
|
# 补充缺失的采集源(对已有数据库的迁移)
|
|
for sd in [
|
|
{"name": "AI前沿搜索", "source_type": "web_search", "query": "AI 人工智能 大模型 前沿 2026", "credibility": "medium", "focus": "AI前沿资讯", "sort_order": 1, "is_active": True},
|
|
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 效率提升 AI工作流 2026", "credibility": "medium", "focus": "AI工具实测", "sort_order": 2, "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() |