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)) # 白标默认值(私有化部署/卖给他人时可改) default_branding = [ {"key": "brand_name", "value": "宇之然内容创作平台", "description": "白标:平台名称"}, {"key": "brand_logo", "value": "", "description": "白标:Logo URL(留空用文字)"}, {"key": "brand_primary_color", "value": "#2563eb", "description": "白标:主色(导航/按钮)"}, {"key": "brand_support_email", "value": "", "description": "白标:支持邮箱"}, ] for b in default_branding: if db.query(SystemConfig).filter(SystemConfig.key == b["key"]).first() is None: db.add(SystemConfig(**b)) 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("✅ 插入默认角色") # 归一化首页菜单路径,避免 index.html 与 /index.html 产生重复入口 idx_plain = db.query(Menu).filter(Menu.path == "index.html").first() idx_slash = db.query(Menu).filter(Menu.path == "/index.html").first() if idx_plain and not idx_slash: idx_plain.path = "/index.html" db.commit() elif idx_plain and idx_slash: db.delete(idx_plain) db.commit() # 初始化默认菜单:幂等按 path 对齐(已有时更新排序/角色,缺失时插入)。 # 运行时会用 DB 菜单覆盖 uni-nav.js 的 fallback,因此这里必须保证文章管理/内容工厂/白标设置等入口存在。 default_menus = [ {"name": "工作台", "path": "/index.html", "icon": "IconHome", "sort_order": 0, "roles": ["admin", "editor"]}, {"name": "创作工作台", "path": "studio.html", "icon": "IconEdit", "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": "campaigns.html", "icon": "IconStar", "sort_order": 6, "roles": ["admin", "editor"]}, {"name": "AI味检测", "path": "ai-slop.html", "icon": "IconSearch", "sort_order": 7, "roles": ["admin", "editor"]}, {"name": "白标设置", "path": "branding.html", "icon": "IconSetting", "sort_order": 8, "roles": ["admin"]}, {"name": "系统", "path": "admin.html", "icon": "IconSetting", "sort_order": 9, "roles": ["admin"]}, ] for m in default_menus: found = db.query(Menu).filter(Menu.path == m["path"]).first() if found: found.sort_order = m["sort_order"] found.roles = m["roles"] found.icon = m["icon"] found.is_active = True # 保留原名称,避免意外重命名 else: db.add(Menu(**m)) # 原独立的选题/内容工厂/文章管理已合并进「创作工作台」,从主导航隐藏(页面仍可被 studio 内嵌与直接访问) for legacy in ["topics.html", "factory.html", "articles.html"]: legacy_menu = db.query(Menu).filter(Menu.path == legacy).first() if legacy_menu: legacy_menu.is_active = False 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()