192 Commits

Author SHA1 Message Date
yuzhiran e9cd0374f4 feat(script): SEO auditor + PROGRESS.md v24 update
Add seo_auditor.py - crawls external websites, scores 6 SEO dimensions (meta/heading/content/performance/links/mobile), generates optimization tasks. Update PROGRESS.md with v24 external promotion feature.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 16:07:43 +08:00
yuzhiran 8a04a23e86 feat(ui): external promotion dashboard (campaigns.html)
Add campaigns.html with 6 tabs (Overview/Products/Campaigns/Rankings/Audits/Tasks) using Vue 3 + Element Plus CDN. Add nav entry to uni-nav.js.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 16:06:46 +08:00
yuzhiran 8f5a9e7ede feat(api): external promotion CRUD + audit endpoints
Add 20 API endpoints under /api/external/* covering products, campaigns, keywords, audits, rankings, optimization tasks with org_id filtering.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 16:05:55 +08:00
yuzhiran 12e49f2811 feat(models): add external promotion tables (6 models)
Add ExternalProduct, PromotionCampaign, CampaignKeyword, SEOAudit, KeywordRanking, OptimizationTask models and 18 Pydantic schemas for the external promotion module.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 16:04:52 +08:00
yuzhiran 81f89f6f00 docs: PROGRESS.md - landing fixes, v23 verification, cleanup
Mark landing OG image creation, GitHub link fix, nav bug fix.
Add v23 feature verification results (12 pages 200, org_id, RBAC).
Update last-updated date. Clean up completed todos.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-18 16:28:55 +08:00
yuzhiran a692f1dd44 fix: landing OG image + GitHub link
Create og-landing.png (1200x630, 62KB) for OG meta tag.
Fix GitHub link placeholder (your-org -> github.com).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-18 16:27:59 +08:00
Yuzhiran Dev 4b87125683 docs: update PROGRESS.md - M4 complete, remaining TODOs done 2026-06-18 08:16:35 +08:00
Yuzhiran Dev 5a19e77d27 feat: rank_tracker AI citation + admin task management tab 2026-06-17 20:42:37 +08:00
Yuzhiran Dev d3ea832b3b feat: admin.html task management tab (TaskConfig + TaskLog timeline)
- Add 任务管理 tab button
- Show all 7 task modules table with enable/disable switch, schedule, params
- Edit dialog for schedule/params editing
- Click module to show el-timeline of recent 30 run logs
- View result data dialog
- Supports toggleModule, saveTaskConfig, loadTaskLogs
2026-06-17 20:10:56 +08:00
Yuzhiran Dev 1ff015e281 docs: v23 PROGRESS.md + archive WORKSPACE_REDESIGN.md 2026-06-17 18:33:19 +08:00
Yuzhiran Dev f6e5e90736 feat: Wave 3 - Dashboard redesign + RBAC + AI assistant updates
- Redesign index.html from system-monitor to user-centric workspace
  (welcome banner, summary cards, 今日待办, AI 智能建议, collapsible admin section)
- Add fetchTodos/fetchSuggestions/getPriorityClass/isOverdue methods
- Update uni-nav current-page from 'dashboard' to 'workspace'
- Add require_role to 10 admin router imports (RBAC foundation)
- Update ai-assistant.js: add factory/insights pages, org_id in chat requests
2026-06-17 18:24:57 +08:00
Yuzhiran Dev 5caf7bc52e feat: Wave 2 - API org isolation + factory.html + insights.html
- Add org_id auto-population to articles.py, publishing.py, metrics.py
- Add org_id filtering to search_rankings.py geo endpoints
- Add org_id to calendar.py create/update/delete endpoints
- Add org_id to tasks.py create endpoints
- Create factory.html content pipeline page (3-tab: 待创作/进行中/待审查)
- Create insights.html analytics page (4-tab: 概览/搜索排名/GEO/平台对比)
2026-06-17 16:13:48 +08:00
Yuzhiran Dev dcfeccc97b feat(models): add org_id to Article, PublishRecord, ContentMetrics, SearchRanking, GeoReadinessScore
- Add org_id column to 5 models with default='default'
- Update to_dict() methods for ContentMetrics, SearchRanking, GeoReadinessScore
- Add ALTER TABLE migrations in database.py init_db()
- Add org_id to 6 Pydantic response schemas
- Add require_role() helper factory in auth.py
2026-06-17 14:27:27 +08:00
Yuzhiran Dev 00431c827b feat: add org_id to ContentCalendar and ContentTask models 2026-06-17 09:02:54 +08:00
Yuzhiran Dev c8a5e8fd87 docs: Update PROGRESS.md for v21 GEO and v22 landing page
- v21: GEO tracking (geo_tracker.py, FAQ/HowTo schema, GeoReadinessScore, scheduler, admin tab)
- v22: Landing page (landing.html, routing, responsive design, auth flow changes)
- Commit count: 165 -> 170

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:28:31 +08:00
Yuzhiran Dev c069a140bb fix: Auth redirects and navigation for landing page routing
- login.html: Post-login redirect / -> /index.html; already-logged-in redirect / -> /index.html
- articles.html, topics.html: Auth failure redirect -> /login.html
- uni-nav.js: Dashboard link / -> /index.html; navigate function fix for prefix handling
- sw.js: Remove / pre-cache; HTML documents use Network First strategy for fresh content

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:28:06 +08:00
Yuzhiran Dev 24e46eced5 feat: Product landing page at / with responsive design
- landing.html: Full product website (hero, features, how-it-works, platform coverage, pricing, FAQ, CTA, footer)
- main.py: Route / to landing.html (overrides StaticFiles default index.html)
- Workspace dashboard remains at /index.html
- Responsive: desktop nav, hamburger mobile menu, single-column on small screens
- No Vue/Element Plus dependency; standalone HTML+CSS+JS (~30KB)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:27:38 +08:00
Yuzhiran Dev f15e0f720a feat: Admin GEO readiness dashboard tab
- New 'GEO 就绪度' tab button in filter bar
- Overview stat cards: total checks, AI citations, citation rate, avg readiness
- AI engine breakdown display (DeepSeek/ChatGPT/Perplexity)
- Readiness scores table with 6-dimension color-coded tags
- Manual GEO tracking trigger button

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:27:11 +08:00
Yuzhiran Dev 7c9a8b88b4 feat: FAQ/HowTo schema injection and SEO pipeline enhancement
- writer.py: inject_geo_metadata now detects content type (article/listicle/howto/faq/review) and injects appropriate JSON-LD (FAQPage, HowTo, ItemList)
- New helpers: _detect_content_type, _extract_faq_pairs, _extract_howto_steps
- outline.py: _extract_seo_keywords regex support for CJK
- prompt_loader.py: Minor update

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:26:44 +08:00
Yuzhiran Dev 93d1d511e5 feat: Register scheduled GEO tracker (07:30 daily)
- New MODULE entry: scheduled_geo_tracker
- New LOG_FILE_MAP entry
- New MODULE_JOBS entry
- _run_geo_tracker method: subprocess invokes geo_tracker.py

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:26:13 +08:00
Yuzhiran Dev 2aedb69efd feat: AI search citation tracking script
scripts/geo_tracker.py: Simulates DeepSeek/ChatGPT/Perplexity queries via LLM to detect article citations. Calculates GEO readiness score (6 dimensions: schema/faq/howto/citations/headings/word_count). Writes results to SearchRanking(search_engine='geo') and GeoReadinessScore tables.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:25:44 +08:00
Yuzhiran Dev 773080cf4f feat: GEO data model and API endpoints
- SearchRanking: +5 fields (ai_search_engine, citation_snippet, citation_url, geo_score, content_type)
- New GeoReadinessScore model (6-dimension scoring: schema/faq/howto/citations/word_count/readability/headings)
- GET /api/seo/geo/overview, GET /api/seo/geo/readiness, POST /api/seo/geo/track

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:25:18 +08:00
Yuzhiran Dev 63a6fabc00 fix: Script bug fixes (bare except, timezone, regex, test)
- collector.py, compliance_checker.py, trends.py: bare except -> specific
- db_helper.py: datetime.now() -> timezone.utc (8 occurrences)
- compliance_checker.py: regex \x08 -> \b word boundary + import json
- search_providers.py: minor fixes
- test_new_features.py: service reachability check retry

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:24:50 +08:00
Yuzhiran Dev d601a26850 fix: API route security fixes (path traversal, auth, bare except)
- articles.py: Path traversal sanitization
- optimizer_logs.py: Admin auth guard
- platform_config.py: Admin auth guard
- system.py: Path traversal whitelist
- topic_config.py: Admin auth guard
- topics.py: Minor fix

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-16 08:23:50 +08:00
Yuzhiran Dev 8595bbc521 feat: GEO/SEO structured data + search ranking tracker 2026-06-09 17:18:09 +08:00
Yuzhiran Dev 23ff63baa9 fix: 三平台内容差异化 + admin敏感词管理表格化
- writer.py: _expand_section() 去除 <100字阈值,始终调用 LLM 平台专属扩写
- prompt_loader.py: 新增 section_expansion_zhihu/wechat/xiaohongshu 三个独立 prompt
- admin.html: 配置管理标签页 + 敏感词/清理规则子标签 + 敏感词表格化管理(编辑/删除)
- config_items.py: PUT /sensitive-words/{id} 支持更新 word/category
- compliance_checker.py: AI 套话从 DB 加载 + 人称规则修正
- initial_data.py: PlatformConfig 字数迁移 + 新种子
- 各前端页面: LLM 配置 rate_limit 字段 + 供应商列表排序
2026-06-08 13:51:35 +08:00
Yuzhiran Dev d0896ef10e fix: _wrap_chinese crash on mixed-width punctuation
_image_generator.py _wrap_chinese 当标题中只有 ASCII 标点
(如 ( ) ?) 而无全角标点时,max() 收到空序列崩溃。

修复:try-except 兜底 + 补充 ASCII 标点到断点列表
2026-06-03 13:13:19 +08:00
Yuzhiran Dev c6628b22f6 fix: 任务抽屉产出结果优化 + server 启动修复
- 任务抽屉产出结果 tab 优先显示 last_message(日志消息),
  过滤 result_data 中的 None/空值
- 修复 tasks.py 因 opencode 块删除导致的 SyntaxError
- 修复 scheduled_generate 存储 None 值问题
- 清理 DB 中旧 scheduled_refresh_search_cache 的 56 条
  TaskLog + 1 条 TaskConfig
- 更新 PROGRESS.md
2026-06-03 11:06:25 +08:00
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
Yuzhiran Dev abbf1468bc fix(admin): LLM 路由标签动态化
- 默认提供商按 id 排序取第一个活跃的(nvidia step-3.7-flash)
- 合规审查取活跃且 model=deepseek-v4-flash 的(sensenova)
- 不再硬编码
2026-06-01 14:46:22 +08:00
Yuzhiran Dev 53f105305f fix(admin): LLM 路由显示不准确
- 默认路由改为动态取第一个活跃提供商(当前为 nvidia step-3.7-flash)
- 合规审查标签改为固定 sensenova (deepseek-v4-flash)
- 移除硬编码的 opencode-go 标签
2026-06-01 14:42:32 +08:00
Yuzhiran Dev f1dc29f439 fix(llm): 分场景使用不同模型
- nvidia 激活、模型改为 step-3.7-flash → 用于日常生成/采集/优化等
- compliance_optimizer.py 显式指定 provider=sensenova → 合规审查走 deepseek-v4-flash
- _FALLBACK 同步更新为 step-3.7-flash
- 管理后台标签同步更新
2026-06-01 14:35:41 +08:00
Yuzhiran Dev 475945418e feat(llm): LLM 配置完全通过管理后台维护
- _get_provider_config 改从 DB 读取 api_key(.env 为 fallback)
- 前端 admin.html 新增 sensenova provider 选项,默认值更新为 sensenova
- 新增提供商不再需要改代码,后台直接配即可
2026-06-01 14:23:24 +08:00
Yuzhiran Dev 2c17f006fc feat(llm): 添加 sensenova 提供商,替换 opencode-go
_API_KEYS/_FALLBACK 添加 sensenova,DB 中设 is_active=true
与 opencode-go 同模型(deepseek-v4-flash),但走 sensenova 通道
2026-06-01 14:17:36 +08:00
Yuzhiran Dev 31d8434010 fix(llm): call_llm 429 自动 fallback + 移除 compliance 硬编码 provider
- nvidia_client.py 新增 _get_provider_fallback_list() 从 DB 读提供商顺序
- call_llm 循环尝试提供商列表,HTTP 429 自动跳过到下一个
- 移除 compliance_optimizer.py 中硬编码 provider="opencode-go"
- 新增 logging 模块引用
2026-06-01 13:23:35 +08:00
Yuzhiran Dev 09c467e56d ops(systemd): 添加 systemd 服务,自动恢复崩溃进程
- 创建 yzr-platform.service(Restart=always,崩溃后10秒恢复)
- 更新 AGENTS.md 命令文档
- 替换原来的 setsid/nohup 启动方式
2026-06-01 12:56:02 +08:00
Yuzhiran Dev 1084388fec fix(topics): 外键约束导致 delete 400
选题有关联 content_tasks/article/publish_record/metrics 表,
外键约束阻止删除。改为先级联删除关联数据再删选题。

前端同时显示服务端返回的具体错误信息,而不是笼统的 删除失败。
2026-06-01 12:08:51 +08:00
Yuzhiran Dev 468c9066cb fix(scheduler): _log_task 重复 INSERT 导致 zombie running 行
_log_task 在任务开始(running)和结束(success/failed)时分别
INSERT,导致 running 行永不更新,被 task monitor 标记为失败。

修复:新增 log_id 参数,传入时 UPDATE 已有行。所有 _run_* 方法
save running log_id 并在结束时传入。早期 return 处也标记失败。
2026-06-01 10:01:50 +08:00
Yuzhiran Dev 9b9ee4964b feat(assistant): 可执行操作改造,支持实时查询与任务触发
- 新增 assistant_actions.py 动作注册表(list_topics/get_system_stats/get_task_status/trigger_task/search_web 等)
- assistant.py 增加规则意图识别优先执行,失败则走 LLM ACTION 格式
- 格式化返回结果,前端无感知
- 支持查看系统统计、选题、任务日志、手动触发定时任务
2026-05-31 13:25:07 +08:00
Yuzhiran Dev 10ad549050 chore: gitignore 新增自动生成文件过滤
- 新增: automation/backups/ research/ automation/images/
- 新增: search_cache.json trends.json sustainability_cases.json
- untrack: 上述文件从 git 追踪中移除
2026-05-29 09:41:03 +08:00
Yuzhiran Dev 3fab87ee11 feat: 新增360/搜狗/微信搜索提供商 + PG15→16升级 & 项目文档更新
- search_utils.py: 新增 _call_360/_call_sogou/_call_wechat HTML爬取函数
- initial_data.py: 种子数据新增三个搜索提供商(priority 3/4/5)
- models.py: provider_type 注释补充新类型
- admin.html: 搜索提供商类型下拉框新增三个选项
- AGENTS.md/PROGRESS.md/README.md: PostgreSQL 15→16
- README.md: 移除硬编码数据库密码
2026-05-29 09:35:15 +08:00
Yuzhiran Dev bd3228806d fix: 合规审查卡死修复 + 小红书复制格式 + today-only过滤 2026-05-27 18:25:30 +08:00
Yuzhiran Dev 6585909ffc feat: optimize all 13 prompts by function+scenario; fix topic_selector hardcode
Prompt optimizations:
- topics_trends: 10→8 topics, simplified output, anti-hallucination
- topic_generate: stronger structure, user-pain focus
- topic_selector_gaps: add SEO+social instruction, clean JSON output
- section_expansion: shorter (removed over-detailed AI套话 list), cleaner
- title_optimize_*: 3 titles per line (no numbering), cleaned up
- outline_generation: simplified structure, strong anti-generic-titles
- compliance_fix/polish: expanded fix types, stronger output requirements
- sources_optimization: cleaner JSON schema
- tags_generation: added 'no thinking output' instruction
- research_summary: simplified output structure
- clean_ai_verbosity/clean_thinking_patterns: unchanged (rule-based)

topic_selector.py: hardcoded prompt → get_prompt('topic_selector_gaps')
_PROMPT_DEFAULTS seeds updated to match DB
2026-05-27 11:07:56 +08:00
Yuzhiran Dev 2c6adf1f43 fix: system_prompt now read from DB LLMConfig
- _get_db_defaults returns system_prompt from DB
- call_llm(system_prompt=None) falls back to DB value
- DB already has system_prompt set for both providers
2026-05-27 10:47:02 +08:00
Yuzhiran Dev b105eee9bb fix: add LLM config status display + routing info
- 状态列: icon → el-tag 文字标签(当前使用/未启用)
- 新增路由状态提示条, 显示默认 vs 审查固定路由
- 移动端卡片同样改为文字标签
2026-05-27 10:09:23 +08:00
Yuzhiran Dev 878daebfe0 fix: route compliance to opencode-go, others to nvidia; skip status on force-pass
1. DB active provider switched to nvidia (stepfun-ai/step-3.5-flash)
   → trends/collector/writer 默认用 nvidia
2. compliance_optimizer.py 固定 provider='opencode-go'
   → 合规审查用 deepseek-v4-flash(更好的模型)
3. force_passed 的选题不再更新为 'ready'(待发布)
   → 只有真正修复通过才更新状态
2026-05-27 09:36:34 +08:00
Yuzhiran Dev 004698cf98 fix: compliance optimizer now requires score improvement, retries LLM on failure
Bug: 'passed=True' check for soft quality issues (AI套话, 缺互动引导)
always exits immediately even when LLM failed to make changes.
Score 85 stays 85, content unchanged.

Fix:
1. polish_with_llm: retry once on exception (was: single attempt)
2. main loop: only accept fix when score actually improves AND
   LLM produced output (opt_logs non-empty). If LLM returns same html
   unchanged, treat as failed attempt and retry.
2026-05-27 09:23:49 +08:00
Yuzhiran Dev 29d97b5f1a fix: expand short sections (<100 chars) with LLM even if not bullet-only 2026-05-26 18:31:43 +08:00
Yuzhiran Dev 80023bbd6c fix: hybrid SVG+PIL PNG image rendering with CJK fonts, prompt URL reference 2026-05-26 17:38:28 +08:00
Yuzhiran Dev f94e6b0161 fix: correct subprocess script path and add TaskLog for trends trigger 2026-05-26 11:32:34 +08:00
Yuzhiran Dev ac8644d752 fix: content quality, image format, task monitor, calendar data source, search UI & sort 2026-05-26 11:26:10 +08:00
Yuzhiran Dev b2d043b231 新增用户管理/角色管理/菜单管理功能,修复创作流水线研究脚本
- 用户管理:新增编辑弹窗(修改用户名/角色/密码),增加组织/创建时间/最后登录列
- 角色管理:新增 Role 模型 + CRUD API,admin.html 新增角色管理 tab
- 菜单管理:新增 Menu 模型 + CRUD API,导航栏从 API 动态加载菜单项
- 个人中心:右上角下拉菜单(个人信息/修改密码/退出),新增修改密码 API
- 种子数据:initial_data.py 自动创建默认角色(admin/editor)和默认菜单(7项)
- 修复 research.py 缺少 enrich_topic_research 函数导致导入失败
- 修复 db_helper.py 中 generated_at 条件导致重创作不更新时间戳
- admin.html 操作列加宽防止按钮换行,平台配置增加删除按钮
- articles.html 预览弹窗加 lock-scroll=false 防止页面尺寸跳动
2026-05-22 18:34:27 +08:00
Yuzhiran Dev 1855f190f5 配置全面迁移数据库:PromptConfig、TaskConfig动态调度、敏感词/清洗规则/趋势映射/平台标签/痛点模板全部可编辑
- 新增 PromptConfig 模型 + API,支持提示词在线编辑(16条默认)
- 调度器动态读取 TaskConfig.schedule,admin 可调执行时间
- 新增 KeywordDomainMap、SensitiveWord、ContentCleanRule、TrendFieldMapping 表
- DOMAINS、TREND_DOMAIN_MAP、PLATFORM_TAGS、china_pains、RSS关键词、priority_weights 全部迁移到 DB
- tasks.html 重构:卡片网格+配置/产出/历史/提示词四个Tab,折叠显示
- 清理冗余代码:DEFAULT_PROMPTS死代码、collector.py unreachable代码、compliance_checker bug
- strip_thinking_html 改用 DB 规则优先
2026-05-22 11:18:23 +08:00
Yuzhiran Dev a8e0a76e07 任务页输入参数旁加'前往编辑'链接
- 采集类别参数 → admin.html#tab=categories
- 信息源参数 → admin.html#tab=sources
- admin.html 已支持 URL hash 自动切tab
- 新窗口打开,不打断当前任务浏览
2026-05-21 10:38:30 +08:00
Yuzhiran Dev 4e10cc95d3 清理 tasks.py 死代码 2026-05-21 10:21:45 +08:00
Yuzhiran Dev 9178f436b2 任务页面全面改版: 模块输入/输出/历史详情抽屉
后端新增:
- GET /api/tasks/modules/{module_id}/detail
  每个模块返回: inputs(自动填充的参数)、outputs(格式化产出结果)、
  history(今日运行记录)、log_excerpt(日志原文)

前端重构 tasks.html:
- 上半: 定时任务模块卡片网格(与仪表盘风格统一)
- 点击卡片→右侧抽屉展示3个tab:
  📥 输入参数: 搜索词/类别/待处理数量等(自动填充)
  📤 产出结果: 表格/标签/评分/建议等(按类型格式化)
  📋 运行记录: 日志原文+时间轴
- 底部保留创作任务列表(分页/筛选/详情)
- 抽屉底部立即运行按钮
2026-05-21 10:18:21 +08:00
Yuzhiran Dev 0dcfda0c80 数据新鲜度校验: 所有模块加上日期/时效检查
1. search_cache.json → 添加 _metadata.updated_at 时间戳
   web_search.search_from_cache() 跳过超过36h的旧缓存
   防止某查询失败时残留旧数据

2. metrics_feedback.json → collector 检查mtime,超过24h不采用

3. trends.json → 已有 date==TODAY 校验(load_trends)

4. collector_ai_advice → DB每日覆盖,时序安全
   creator→topic状态位避免重复生成
   optimizer→文章状态位避免重复审查
2026-05-21 10:09:51 +08:00
Yuzhiran Dev 5037d5d0ed 流水线整体前移1.5h,凌晨5点前全部就绪
01:00 search → 01:10 trends → 01:30 collect
→ 02:00 create → 03:00 review
→ 05:00 optimize_sources → 06:00 metrics

各间隔: 10min/20min/30min/60min/2h/1h
采集到创作30min缓冲足够(采集≈5-15min)
2026-05-21 10:04:41 +08:00
Yuzhiran Dev 56293a52a2 打通流水线闭环: trends+metrics+optimize_sources → collector
三大数据流精准注入采集器:

1. 热点趋势(trends) -> collector
   _get_trend_context() 读取 trends.json
   热搜注入 LLM 选题 prompt

2. 历史表现(metrics) -> collector
   scheduler 同步指标后按field聚合写入 feedback.json
   collector 读取后高互动领域获优先级提升

3. AI策略(optimize_sources) -> collector
   从 DB SystemConfig 读取 AI 建议注入 prompt
2026-05-21 09:48:38 +08:00
Yuzhiran Dev 6b790e77f6 全线异步化: 所有手动触发任务不再阻塞uvicorn worker
每个模块提供两个版本:
- run_xxxx() → Popen非阻塞(给API用)
- run_xxxx_blocking() → subprocess.run阻塞(给定时任务用)

API端点全部改为立即返回XX已后台启动,不再等子进程完成

受影响端点:
  /generate/run (原30min→12ms)
  /review/run (原10min→12ms)
  /collect/run (原5min→12ms,已修复)
  /optimize-sources/run (原2min→12ms)
  /trends/run (原2min→12ms)
  /refresh-search-cache/run (原10min→12ms)
  /metrics-sync/run (原N×10s→12ms)

同时增加对应GET状态端点:/collect/status,/generate/status,/review/status
2026-05-21 09:20:13 +08:00
Yuzhiran Dev 6f81167691 联网搜索能力集成: opencode webfetch → 采集器
新增:
- scripts/opencode_search.py: 通过 npx opencode run 调用 webfetch 联网搜索
- 搜索缓存每日 02:30 自动刷新 (scheduler)
- 管理后台「搜索缓存」模块 + 立即运行按钮
- POST /api/system/refresh-search-cache/run 手动触发端点
- 8个分类搜索词从sources.yaml读取,调用AI联网搜索真实内容

机制:
  Python脚本 → npx opencode run → AI webfetch → 真实搜索结果 → 写入search_cache.json
  → 采集器读缓存 → LLM基于真实数据生成选题

不再需要API Key,不依赖任何搜索引擎,搜索结果来自AI的webfetch能力
2026-05-21 09:01:02 +08:00
Yuzhiran Dev 7d50878c7d 搜索缓存: 可追踪的search_cache.json, webfetch预填充26条
新机制:
- web_search.py按优先级: 缓存→Bing API→Bing抓取
- search_cache.json由opencode webfetch手动填充(不依赖搜索引擎)
- 搜索失效不影响采集器(LLM直接生成选题)
- .gitignore移除search_cache.json(应被版本追踪)
2026-05-21 08:29:04 +08:00
Yuzhiran Dev eb1c4205fa 重写web_search: Bing API优先+抓取回退+诚实失败
- Bing Web Search API支持(设BING_API_KEY环境变量即可)
- 抓取回退但诚实面对反爬限制
- 搜索为空时采集器正常运行(LLM直接生成选题)
- 降低搜索依赖为可选增强
2026-05-21 08:14:47 +08:00
Yuzhiran Dev a26f0e936e 修复采集器流程顺序: 采集→分析→LLM总结
- _generate_topic_with_llm 改名为 _generate_topics_with_llm
- 支持同时接收已分析的案例+搜索结果作为LLM上下文
- run()顺序改为: 采集→RSS提炼案例→LLM基于全部数据生成→降级回退
- LLM现在在流程末尾做总结生成,而非开头替代搜索
2026-05-21 08:06:24 +08:00
Yuzhiran Dev 10996ce6ce 清理城市农业类别 + 修复采集器LLM直接选题
清理:
- sustainability_cases.json移除GLO-001/CHN-001城市农业案例,替换为循环消费
- initial_cases.json移除case8东京垂直农场/case26城市屋顶农场
- strategy_topics_to_json.py移除B01/B05/D05三个种菜选题
- collector.py移除城市农业→循环消费映射,更新注释
- 删除fix_collector.py/test_image_gen.py/generate_images.py等遗留脚本
- 删除import_topics.py和automation/下旧版生成脚本

修复:
- collector.py _generate_topic_with_llm不再依赖搜索结果,无搜索时LLM直接生成
- run()始终调用LLM,不再要求web_search_results非空
- 替换sources.yaml中已失效的RSS源(澎湃/虎嗅/中新网→36氪/少数派)
2026-05-21 08:02:08 +08:00
Yuzhiran Dev c8bee712d7 Phase3: 三平台封面图生成
- cover_generator.py: Pillow生成知乎/微信/小红书封面图(渐变背景+标题)
- 知乎 1200×630 蓝调, 微信 900×500 绿调, 小红书 1080×1440 红调
- creator.py 流水线增加 cover_generator 作为最终回退
- db_helper.py 新增 save_cover_to_article
- main.py 挂载 /automation/images 静态目录
- articles.py preview 接口返回 images 字段
- topics.html 预览dialog展示封面图
2026-05-20 19:12:58 +08:00
Yuzhiran Dev 40a77cad2c Phase2: 真实热点数据接入
- trends.py新增百度/微博/知乎实时热搜API抓取,LLM为fallback
- 新增 source 字段标记数据来源
- scheduler.py新增 scheduled_fetch_trends 每日03:00定时刷新
- system.py新增 POST /api/system/trends/run 手动触发端点
- system.py modules/status 加入热点趋势模块
- index.html triggerModule 加入 trends 触发按钮
2026-05-20 19:07:35 +08:00
Yuzhiran Dev 09da2f9dc4 Phase1: 效果数据真实化
- 替换scheduler.py中假随机数metrics_sync为知乎API自动获取
- 新增 POST /api/metrics/zhihu-fetch 知乎公开数据API端点
- metrics.html新增「数据录入」tab:手动录入表单+已有数据列表+知乎自动获取
- gitignore清理已跟踪的生成文件(outlines/research/images/cache)
2026-05-20 19:04:35 +08:00
Yuzhiran Dev eb800c5acc Ignore generated pipeline artifacts (outlines, research, images, search cache) 2026-05-20 18:31:44 +08:00
Yuzhiran Dev 9a328323c0 Filter outline markers in writer, add min-height to preview dialog 2026-05-20 18:11:32 +08:00
Yuzhiran Dev bf38125362 Fix compliance optimizer timeout: 600s -> 1800s to match other pipeline steps 2026-05-20 17:02:36 +08:00
Yuzhiran Dev 2244736318 Fix preview: keep article title and tags, adjust scroll height 2026-05-20 15:37:26 +08:00
Yuzhiran Dev 63f074e4df Fix preview dialog scroll & preserve HTML structure on edit save 2026-05-20 15:18:48 +08:00
Yuzhiran Dev 0a8fbda4fd Fix calendar filter, module triggers, API cleanup, consolidate get_current_admin, fix LLM schema 2026-05-20 14:15:00 +08:00
Yuzhiran Dev 498165440f Add platform config website_url, admin tab, fix writer DB config fallback, add trigger endpoints 2026-05-20 09:38:56 +08:00
Yuzhiran Dev 55e5f3d166 Fix topics mobile card list pagination: use paginatedTopics instead of filteredTopics 2026-05-19 08:28:56 +08:00
Yuzhiran Dev 1c78828681 Update PROGRESS.md (v16) 2026-05-19 07:45:18 +08:00
Yuzhiran Dev 3dce54b359 Fix writer max_tokens (500→1000) for titles/tags, fix preview scrollbar height 2026-05-19 07:44:56 +08:00
Yuzhiran Dev 0897074d40 Update PROGRESS.md with 2026-05-18 work summary (v16) 2026-05-18 23:53:49 +08:00
Yuzhiran Dev 671cec58fa Fix preview platform switching, add stale task cleanup on startup 2026-05-18 23:38:30 +08:00
Yuzhiran Dev 7b4e52743b Fix pagination: all pages default to 10 per page, add total counts, fix tasks pagination position and use client-side slicing 2026-05-18 22:59:03 +08:00
Yuzhiran Dev 894e53caef Add intermediate progress update for running tasks 2026-05-18 22:41:31 +08:00
Yuzhiran Dev f427aea1b3 Fix task status stuck at 'running': use new DB session in thread, add frontend auto-polling 2026-05-18 22:37:59 +08:00
Yuzhiran Dev 33766a396d Fix topic preview: add missing edit/save methods, fix scrollbar, strip tags from preview body 2026-05-18 19:40:26 +08:00
Yuzhiran Dev a09e7e6671 Add pagination to admin management tabs (cases/categories/sources/orgs) 2026-05-18 19:18:09 +08:00
Yuzhiran Dev 7169ddb678 refactor: writer.py 移除文件系统写入代码,HTML 仅持久化到数据库
删除 RELEASE_DIR、release_dir、out_path.write_text 等文件系统相关代码,
save_html 改为仅调用 save_article() 写入 articles 表。
避免后期维护时文件系统和数据库双源不一致的问题。
2026-05-18 08:29:08 +08:00
Yuzhiran Dev 279dc5b894 fix: writer.py 生成的 HTML 未写入数据库导致预览和合规优化均取不到内容
save_html 在写文件后新增 save_article() 调用,将 HTML 持久化到 articles 表,
使预览 API 和 compliance_optimizer 能从 DB 正常读取文章内容
2026-05-18 08:18:32 +08:00
Yuzhiran Dev 52564ad036 fix: 今日新增筛选统计不准 + LLM提示词动态当前日期 + 微信配图按选题生成
topics.html:
- 今日新增筛选时全量拉取 allTopics 用于保持 stats 统计正确,todayTopics
  存储服务端日期过滤结果
- filteredTopics 对 'today' 返回 todayTopics,不按不存在的 status 过滤

writer.py/research.py/outline.py:
- 所有 LLM 提示词注入今天的动态年月日(datetime.now)
- 禁用过时数据的年份改为动态 {当前年-1}-{当前年}
- 每个论点必须配真实最新案例+数据来源

writer.py WeChat 配图:
- 从 null placeholder 改为动态 SVG,展示选题标题+领域
- 标题自动换行(24字/行),附蓝色装饰条 + 渐变背景
2026-05-18 06:25:47 +08:00
Yuzhiran Dev bb50d5d925 fix: 今日新增筛选filteredTopics未处理'today'导致始终为空,状态筛选改为全量拉取保统计准确
- filteredTopics 对 filterStatus='today' 直接返回 this.topics(服务器已按日期过滤),避免按不存在的 status 再次过滤导致空列表
- fetchTopics 改为只有 'today' 走服务端 ?today=true;其他状态仍然全量拉取后客户端过滤,保证 statusStats 统计按钮数字准确
- 保留 watch 移除(用户能自由清除筛选)
2026-05-18 06:14:15 +08:00
Yuzhiran Dev a46fedabb5 fix: 选题页状态过滤器改用服务端过滤,移除watch覆写URL参数导致无法清除筛选的问题
- fetchTopics 根据 filterStatus 发送 status/today 参数到后端,而非全量拉取后客户端过滤
- 移除 watch.topics 处理器,解决 URL 参数反复覆写用户筛选操作的问题
- 今日新增(?today=true)和服务端 status 参数均走 API 过滤
2026-05-18 06:08:33 +08:00
Yuzhiran Dev 39719354fb perf: 全链路 LLM 提示词优化 + 微信公众号配图改为 base64 内联
- research.py: 提示词增加热点关联/独特观点储备/市场价值判断/平台搜索趋势
- outline.py: 增加反常识洞察/平台推荐优化/每章市场价值产出要求
- writer.py _expand_section: 增加独特观点/热点时效/专业简洁/平台推荐友好要求
- writer.py _optimize_title: 各平台补充搜索热词利用/情绪感召/参考真实爆款语气
- writer.py wechat配图: placeholder.jpg 改为 base64 内联 SVG(带相机图标+提示文案),预览可渲染,复制即用
2026-05-18 05:49:50 +08:00
Yuzhiran Dev 41b0f694ee feat: 平台配置表扩展配图/字数字段,微信公众号正文插入配图,合规检查从DB读规则
- PlatformConfig模型新增requires_image、image_count_min/max、image_width/height、min_words/max_words
- schemas.py同步PlatformConfigBase/Create/Update/Response新字段
- initial_data.py为三大平台填充初始值(微信需配图、字数800-1500等)
- database.py添加新列ALTER TABLE迁移
- platforms.html重写编辑弹窗(正确字段名+配图/字数设置)
- writer.py微信公众号文章正文h1后插入<img>占位
- compliance_checker.py接受platform_config参数,从DB读取规则替代硬编码
- compliance_optimizer.py启动时加载DB平台配置传入checker
2026-05-17 23:08:43 +08:00
Yuzhiran Dev 5674114599 perf: 多平台文章不再每个平台独立LLM改写,改为生成一篇通用文章后仅做格式排版适配 2026-05-17 22:57:11 +08:00
Yuzhiran Dev dc531460ea fix: 小红书文章无正常内容(LLM推理文本污染)、模块列表不匹配、脚本独立运行缺org_id列迁移 2026-05-17 22:52:05 +08:00
Yuzhiran Dev eb472d232e fix: 仪表盘概览标题颜色过浅改用CSS变量;模块状态列表从3个扩展至6个与定时任务数匹配 2026-05-17 17:01:14 +08:00
Yuzhiran Dev f2d778b5ce fix: 脚本独立运行时缺少init_db(),导致Topic表org_id列不存在,创作按钮点击后生成文章失败 2026-05-17 15:37:24 +08:00
Yuzhiran Dev 2b7935b408 fix: 补充H5移动端隐藏el-table样式,修复选题/文章页桌面表和移动端卡片同时显示的问题 2026-05-17 15:15:11 +08:00
Yuzhiran Dev 2852b6ef06 chore: 更新信息源配置(移除失效源,新增web_search和RSS源)及搜索缓存 2026-05-17 10:20:42 +08:00
Yuzhiran Dev d32fc05f4f refactor: 统一CSS设计系统 — 移除各页面重复内联样式,改用theme-modern.css自定义属性 2026-05-17 10:09:56 +08:00
Yuzhiran Dev 9c37c9a574 feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列
测试: tests/test_phase_upgrades.py 97项全覆盖
CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式
修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
2026-05-17 06:56:53 +08:00
Yuzhiran Dev 301dc3e438 feat: 添加 LLM API 并发控制规则 (并发2, ≤60次/分钟) 2026-05-16 10:56:58 +08:00
Yuzhiran Dev fef435cc78 feat: 审查流程优化+采集定时调度+全链路LLM提示词升级
- 审查:移除 manual_review,改为迭代LLM修复(最多3次),合规分回写Topic
- 调度:scheduler 新增话题采集定时任务 scheduled_collect (01:30)
- 提示词:全链路8文件≈24个提示词升级,增强SEO/平台推荐/真人感
2026-05-14 21:52:02 +08:00
Yuzhiran Dev 1115223066 docs: 更新 PROGRESS.md 进度,扩展日志类型下拉
- PROGRESS.md 更新:articles 表迁移、合规审查重构、流水线流程说明
- logs.html 下拉框从 3 种扩展为 9 种日志类型(含研究/大纲/写作/发布/通知/趋势)
2026-05-13 18:24:25 +08:00
Yuzhiran Dev 233e23016c feat: 内容数据迁移至数据库,合规审查全链路打通
- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录
- 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布
- 新增 todayCount 筛选按钮,与系统概览统计数据一致
- 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮
- 统一 '优化' → '审查' 命名,消除前后端术语不一致
- 调度器创作完成后自动触发审查(生成 → 审查 → 待发布)
- 清理旧备份/调试文件、过期大纲和研究笔记
2026-05-13 17:33:56 +08:00
lt bc6a302e59 feat: 优化选题/任务/日志页面布局并打通任务系统
- topics.html: 重构布局(page-header模式),创作改为走 /api/tasks/run-creator 任务系统
- tasks.html: 重试功能实现,调用 /api/tasks/run-creator 重新提交创作
- logs.html: 统一布局风格
- 三个页面添加 min-height / overflow-x / 统一间距
- backend/run.sh: 修复入口为 app.main:app
2026-05-09 19:58:49 +08:00
lt 25694db33b chore: 清理未使用的代码文件和备份
- 删除 backend/main.py (已由 app/main.py 替代)
- 删除 backend/api/ 和 backend/core/ (已由 app/api/ 和 app/core/ 替代)
- 删除 backend/static 符号链接
- 删除前端测试/调试页面
- 删除未引用的 vendor 子目录 (element-plus/, vue/, axios/)
- 删除所有 .bak 备份文件
2026-05-09 19:43:49 +08:00
lt 71cb4c35a8 chore: 清理 Docker 相关文件并优化前端布局
- 删除 Docker 相关文件 (docker-compose, Dockerfile, nginx.conf, init.sql 等)
- 优化 platforms.html 卡片布局和响应式样式
- 优化 users.html 格式和移动端卡片设计
- 优化 admin.html 页面结构和表格布局
- 修复各页面 min-height 和溢出问题
- 更新导航组件样式
2026-05-09 19:41:59 +08:00
lt e77a1aa4d9 fix(metrics): 修复 recommend-topics API 错误处理
- 添加 try-except 捕获空数据导致的聚合查询错误
- 添加 logging 导入用于错误日志记录
- 修复 avg_engagement/max_views 类型转换问题
- 新增 test_full_api.py 全功能测试脚本
- 所有 29 项 API 测试通过 (100%)
2026-05-08 23:53:55 +08:00
lt 526278589b feat: 完善前端页面 - 新增数据分析、素材库、创作任务、平台配置页面
- 新增 metrics.html: 数据分析页面 (Dashboard, 趋势图, 平台对比, 选题推荐)
- 新增 assets.html: 素材库页面 (上传/管理/预览/标签)
- 新增 tasks.html: 创作任务页面 (任务列表/进度/详情)
- 新增 platforms.html: 平台配置页面 (知乎/微信/小红书格式规则)
- 更新导航组件: 添加新页面入口, 适配 H5 底部导航
- 修复 calendar.html: 使用本地 Vue/ElementPlus 资源
- 修复 assets.py: db.func.count -> sqlalchemy.func.count
- 新增 test_api_unit.py: 后端 API 单元测试
- 新增 test_frontend.sh: 前端页面完整性测试

PC/H5 双端适配, 所有页面统一使用 navbar + navigation 组件
2026-05-08 23:35:26 +08:00
lt cf5103bbca feat: 全面升级项目架构 - PostgreSQL迁移 + 配置化改造
主要变更:
- 数据库: SQLite → PostgreSQL (yzr_nr)
- 选题系统: 硬编码字段 → 配置化 (TopicField/TopicConfigField/TopicStatusConfig)
- 新增模型: ContentCalendar, ContentMetrics, MediaAsset, PlatformConfig, ContentTask
- 新增 API: topic-config, calendar, metrics, assets, tasks, platform-config
- 数据迁移: 现有选题数据迁移到新 schema (field_id/tags/custom_data/scoring_data)
- 初始化数据: 10个领域, 5种状态, 3个平台配置

服务运行: http://localhost:8001
默认账号: admin / admin123
2026-05-08 18:26:01 +08:00
lt ac48304532 fix(navigation): 纯DOM注入方案,彻底避开Vue渲染问题
- installNavigation 改为直接创建/插入导航DOM
- 自动从 Vue 实例提取 isAdmin、currentPage、redirectToPage
- 不再依赖 Vue createElement 或组件渲染
- 支持移动端/PC端自适应
2026-05-08 15:28:01 +08:00
lt c13d8c29aa fix(navigation): 改用原生DOM操作,彻底解决h函数问题
- 放弃Vue h函数,直接document.createElement创建导航栏
- mounted生命周期中渲染侧边栏和移动端底部导航
- 原生JS绑定点击事件,确保功能正常
- 避免Vue3 prod版本中render函数的参数问题
2026-05-08 14:58:28 +08:00
lt df86e01cd2 fix(navigation): 增强移动端底部导航样式和调试
- mobile-nav 背景改为蓝色(#1890ff),文字白色,便于确认渲染
- 增加渲染日志(侧边栏/移动端项目数量)
- 确保所有页面都使用同一组件,统一显示
2026-05-08 13:36:28 +08:00
lt 9147d5e679 feat(frontend): 抽离页眉为公共组件;修复导航点击事件
- 新增 navbar-component.js:固定页眉,统一标题、用户信息、退出按钮
- 重构所有页面(index/topics/logs/admin/users)使用 navbar-component
- 清理原有 navbar 相关样式,避免冲突
- 导航组件增加 main-content padding-top 自动调整
- 修复导航按钮点击事件(确保  触发)
- 导航组件样式强应用,解决遮挡和布局问题
2026-05-08 13:22:12 +08:00
lt 2220d7b981 fix(navigation): 统一 redirectToPage 方法;修复内容区域宽度
- 各页面 redirectToPage 统一为直接跳转 page 参数
- 组件样式增加 main-content margin-left 避免内容被侧边栏遮挡
- 确保固定侧边栏与主内容布局正确
2026-05-08 12:42:03 +08:00
lt c85320ae72 fix(navigation): 修复布局偏移和事件跳转问题
- 移除 .navigation-wrapper 的 flex/min-height 布局干扰
- 确保 main-content 不受侧边栏占位影响
- 优化事件处理,增加点击日志
- 修复 admin 导航页面名称不一致问题
2026-05-08 12:30:49 +08:00
lt beb2cf2473 debug(frontend): 添加导航组件详细日志,便于问题排查
- 增加 mounted 生命周期日志
- 增加 installNavigation 和样式注入日志
- 在各页面注入运行态调试代码
- 优化 installNavigation 容错(fallback 直接注册)
- 添加最小测试页 test-minimal.html
2026-05-08 11:37:32 +08:00
lt ef168e3d90 feat(frontend): 公共导航组件完整实现
- 组件改为render函数,避免模板编译器依赖
- 自动注入样式,无需页面额外CSS
- 自适应布局:PC固定侧边栏,H5底部导航
- 所有页面统一使用,admin权限动态控制
- 自动调整main-content边距,防止遮挡
2026-05-08 11:03:33 +08:00
lt 5587fa0bce fix(navigation): 使用render函数替代template,兼容prod版Vue
- 重写组件为render函数实现,避免编译器依赖
- 优化按钮生成逻辑,使用JS数组map
- 修复样式注入时机和CSS冲突问题
- 确保app实例化后调用installNavigation进行注册
2026-05-08 10:57:23 +08:00
lt b8d9e0c209 refactor(frontend): 抽离公共导航组件,消除各页面重复代码
- 创建 navigation-component.js:统一的PC左边栏 + H5底部导航组件
- 重构 topics.html、logs.html、admin.html、users.html、index.html
- 删除各页面重复的 sidebar 和 mobile-nav HTML 结构
- 删除重复的导航样式定义,由组件统一提供
- 修复 component 插入重复的问题,确保每个页面只插入一次

现在修改导航只需更新组件文件,无需改动每个页面,提升维护性。
2026-05-08 10:36:51 +08:00
lt 284b18e5fc feat: 添加自动化数据目录与可持续性主题数据集 2026-05-08 09:26:46 +08:00
lt 0317a8f42f chore: 移除数据库文件跟踪,避免敏感数据入库 2026-05-08 09:26:13 +08:00
lt df5a9db3ad feat: 管理员页面整合与侧边栏导航优化
- 完成系统管理页面与导航整合
- 在 users.html/topics.html/logs.html 侧边栏与移动导航添加系统管理入口
- 创建 admin.html 管理页面(案例/任务日志/LLM配置/系统配置)
- 新增核心模块:collector.py(数据采集器)、scheduler.py(任务调度器)
- 新增脚本:collector_db_integration.py(采集器数据库整合)
- 更新项目文档并验证路由注册
2026-05-08 09:26:06 +08:00
lt 31d6306e3b 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 流水线完整通过。
2026-05-07 11:25:42 +08:00
lt 8dd19a2179 style: unify layout across pages; add sidebar and mobile nav to admin page
- admin page:
  - Add sidebar navigation (system overview, topics, logs, users, admin)
  - Add mobile bottom navigation
  - Update title to "系统管理"
  - Add redirectToPage helper for navigation
- Ensure all admin pages (cases, tasklogs, llmconfigs, systemconfigs) remain functional
- Unify button labels: mobile nav uses "系统管理" for admin
- Minor: adjust navbar title
- No changes to other pages (they already had unified structure)
2026-05-06 22:06:21 +08:00
lt 8920a337e0 fix: 修复前端空白页与API调用错误,统一创作流程
- 前端
  - topics.html: 恢复结构并修复Vue初始化问题
  - 调整创作按钮逻辑:仅已发布选题禁用
  - 修正API端点与payload格式(generate/optimizer/publishing使用topic_ids数组)
  - 移除ElementPlus图标模块依赖,使用全局构建
  - admin.html: 回退至Options API版本,解决this上下文错误
- 后端
  - 注册/api/generate/run路由
  - 简化generate逻辑:允许非已发布选题重创作,更新状态为“待审查”
  - 统一logs查询接口支持query参数
  - 修复admin用户管理字段引用
  - 系统概览返回{ stats }结构
- 静态资源整理
  - 删除冗余element-plus-icons、重复CSS/JS、图标文件
  - 正确放置Vue和ElementPlus全局文件
- 数据库与数据
  - 补充30个案例
  - 更新选题状态与初始数据

验证:所有页面可访问,API认证与端点正常工作。
2026-05-06 21:44:56 +08:00
lt bd381ff65a feat: 完成系统管理页面与导航整合\n\n- 在 users.html/topics.html/logs.html 侧边栏与移动导航添加系统管理入口\n- 创建 admin.html 管理页面(案例/任务日志/LLM配置/系统配置)\n- 更新项目文档并验证路由注册 2026-05-02 10:43:34 +08:00
lt 53c3948cad feat: 增加案例/任务日志/LLM配置/系统配置管理功能\n\n- 扩展案例数据至30个\n- 新增Case, TaskLog, LLMConfig, SystemConfig的API路由\n- 完善initial_data导入脚本\n- 创建前端管理页面admin.html,支持完整增删改查\n- 更新导航菜单,增加系统管理入口 2026-05-02 09:13:44 +08:00
lt 511d40d40c feat: add Case, TaskLog, LLMConfig, SystemConfig models 2026-05-01 22:56:06 +08:00
lt bdcf32dce8 feat: add Case, TaskLog, LLMConfig, SystemConfig models 2026-05-01 14:33:00 +08:00
lt aa76f8203f optimize: refine LLM prompts for conciseness and reduce verbosity; reduce max_tokens to 1000 2026-05-01 11:53:02 +08:00
lt 601fc5d239 refactor: remove publisher, add auto DB sync, clean backups, update docs 2026-05-01 10:05:50 +08:00
lt 98c6c22a26 remove: delete obsolete frontend-vue project (no longer aligned with current frontend) to prevent confusion 2026-05-01 09:52:28 +08:00
lt 2cbf2909f8 remove: delete obsolete backend/static directory 2026-05-01 08:05:54 +08:00
lt e1613d2af9 fix: restore frontend to dynamic version (undo incorrect sync from outdated backend/static) 2026-05-01 07:51:16 +08:00
lt 94295e1f74 refactor: unify frontend source; sync latest UI changes and update project completion report 2026-05-01 07:32:55 +08:00
lt cfb60d7aae UI优化完善初版 2026-05-01 06:48:21 +08:00
lt e1ba31afda 版本1.0.4 - 发布前准备
- 修复system.py缩进错误
- 优化前端页面样式(待重构)
- 改进API接口结构
- 完善文档和自动化脚本
- 平台基本功能稳定运行
2026-04-29 09:32:43 +08:00
lt 0a31ae09af fix: 数据库配置和缺失导入
- 切换到 SQLite(app/database.py)避免外部依赖
- system.py: 添加 func 导入
- main.py: 导入改为 app.database.init_db
- 前端 topics.html: Vue 应用结构修复
- 后端导入全面修正(绝对导入)

版本: v1.0.3 (完整可运行版)
2026-04-27 23:10:06 +08:00
lt 6f0713953a fix: 修复后端导入路径问题以适配新工作区
- 统一使用绝对导入(backend 目录在 PYTHONPATH)
- 修改 main.py、api 模块、core/security 的导入
- 移除 generate 模块(缺失 GenerateTask 模型)
- 修复 database.py 导入 Base
- backend/run.sh 添加 PYTHONPATH 设置
- 前端 topics.html 修复 currentUser 初始值和 Vue 结构
- 添加前端代码语法检查脚本

版本: v1.0.3 (导入修复版)
2026-04-27 23:03:53 +08:00
lt d719181834 fix(ui): 修复 Vue 应用初始化语法错误
- currentUser 初始化改为 { username: '' }(之前遗漏空字符串)
- 通过 node --check 验证语法正确
2026-04-27 22:06:17 +08:00
lt 953b74bf52 fix(ui): 修复空用户显示和 Vue 启动问题
- currentUser 初始值改为空对象,避免 null 访问错误
- template 中添加空值保护:currentUser.username ? ... : '?'
- 确保 Vue 应用能正常挂载
2026-04-27 21:51:54 +08:00
lt 0a77e67200 fix(ui): 重构 topics.html Vue 应用结构
- 完全重写 script 部分,确保 methods 和 mounted 正确闭包
- 修复 Vue 表达式未解析的问题
- 添加调试日志
- 确保移动端和桌面端都能正确挂载
2026-04-27 21:30:44 +08:00
lt 03287697c3 fix(ui): 修复移动端数据加载显示
- 为 topic-card-list 添加 v-if 条件渲染
- 添加空状态提示
- 增强调试信息(console.log)
- 确保移动端卡片列表正确显示
2026-04-27 21:20:28 +08:00
lt 6cb273a8a9 fix(ui): 修复 H5 数据加载和导航跳转
- 修复 redirectToPage 路径拼接问题
- 增强 fetchTopics 错误提示(显示错误消息)
- 添加加载状态提示(首次加载)
- 确保 mounted 中 URL filter 参数正确解析
2026-04-27 20:22:20 +08:00
lt 4647921e01 fix(ui): 修复移动端卡片 Vue 结构和显示逻辑
- 恢复 clean 版本后重新添加移动端适配
- 修正 methods 结构,移除重复方法
- 移除内联 display:none,用纯 CSS 控制
- 添加 getStatusType 方法供卡片使用
2026-04-27 19:57:05 +08:00
lt 54c84b6681 feat(ui): 移动端适配优化 (logs, users)
- logs.html: 移动端控制栏垂直布局,日志卡片优化
- users.html: 新增移动端用户卡片列表,隐藏表格
- 统一小屏幕字体和间距
2026-04-27 19:36:03 +08:00
lt 33133430f8 feat(ui): 移动端卡片布局优化
- 新增移动端卡片列表 (≤768px)
- 表格在小屏幕自动隐藏
- 操作按钮在移动端垂直排列
- 减少表格字体/内边距以节省空间
2026-04-27 19:14:33 +08:00
lt d63074be71 chore: 更新项目路径到新工作区
- 所有自动化脚本、测试文件路径从旧工作区迁移到 /root/openclaw-workspace/projects/yu-zhi-ran/
- 保持项目独立于 OpenClaw 工作区管理
- 后端 run.sh 路径更新
2026-04-27 18:49:32 +08:00
lt 75ae5ae01f style(frontend): 减小操作列宽度
- 操作列宽度从 280 调整为 220,节省空间
2026-04-27 18:46:46 +08:00
lt 9ced46c296 fix(frontend): 菜单跳转和日志默认日期
- 修改 index.html 菜单为页面跳转(redirectToPage)
- 移动端导航同样修复
- logs.html: logDate 默认值为当天
- topics.html: 支持 URL filter 参数自动筛选

版本: v1.0.2
2026-04-27 16:55:15 +08:00
lt b85b89a569 fix(frontend): 注册 ElementPlus 插件,修复 async 语法错误
- 统一在 createApp 后调用 app.use(ElementPlus)
- 为 handlePublish 添加 async 关键字
- 修正静态资源路径引用
- 解决登录页空白、按钮不可用问题

版本: v1.0.1
2026-04-27 16:45:58 +08:00
lt 5fe426fc90 feat: 全面优化前端架构
- 本地化所有资源 (Vue3, Element Plus) 无需外部 CDN
- 重新设计主页:登录页 + 系统概览 + 侧边栏导航
- 完善管理后台功能:选题管理、系统日志、用户管理
- 全面支持 PC 和 H5 移动端(响应式布局 + 底部导航)
- 优化后端 API 返回格式,兼容前端需求
- 默认首页为登录/系统概览页面
- 统计卡片可点击筛选选题列表
- 模块状态实时展示
2026-04-27 15:14:26 +08:00
lt 1eef4c177b fix: 修复所有前端页面的 Vue 条件渲染错误,解决菜单点击后不显示内容的问题 2026-04-27 14:56:51 +08:00
lt 62af077479 feat: 修复并完善所有菜单页面的完整功能,包含真实数据、API模拟和交互 2026-04-27 14:39:50 +08:00
lt 27729d2987 feat: 创建所有菜单页面的完整功能版本,包含真实数据和交互 2026-04-27 14:33:45 +08:00
lt d0d079203a feat: 创建完整功能的选题管理页面,包含真实数据、批量操作和所有交互功能 2026-04-27 14:30:38 +08:00
lt a93d029778 feat: 为所有页面添加真实数据加载和完整交互功能 2026-04-27 14:22:43 +08:00
lt 5ff0b8e669 feat: 创建极简独立页面,纯HTML/CSS/JS,避免所有框架依赖问题 2026-04-27 14:18:56 +08:00
lt 8b5b76f6cb fix: 使用本地静态文件替代 CDN,避免浏览器安全策略阻止 2026-04-27 14:13:00 +08:00
lt 73de8702ac fix: 移除 Tailwind CDN,使用内联 CSS 样式避免浏览器安全策略阻止 2026-04-27 14:11:14 +08:00
lt 20970034b6 fix: 修复所有页面的 Element Plus 引用,统一使用 CDN 版本 2026-04-27 14:07:55 +08:00
lt 8a86d97724 fix: 修复加载顺序和CDN稳定性,使用jsDelivr + Vue 3.4.27生产版本 2026-04-27 14:05:55 +08:00
lt 9e72e5fd35 fix: 修复 Element Plus 版本兼容问题,使用 Vue 3.4 + EP 2.4.3 2026-04-27 14:00:44 +08:00
lt 2e353b7fff fix: 为独立页面添加完整 Vue + Element Plus + Tailwind 依赖 2026-04-27 13:59:31 +08:00
lt b2055df118 fix: 为独立页面添加 Tailwind CDN,确保所有类名正常工作 2026-04-27 13:55:46 +08:00
lt 0a19cc203b feat: 按菜单功能拆分页面 - index.html(主入口+概览)、topics.html、logs.html、users.html 2026-04-27 13:09:38 +08:00
lt 87ebd94719 feat: 完全重建选题管理页面,包含统一卡片容器,恢复所有功能 2026-04-27 13:00:04 +08:00
lt 5f44df1d48 feat: 安全添加统一卡片容器,仅包裹选题管理内容 2026-04-27 12:57:08 +08:00
lt 4d5e249c55 fix: 重新正确构建选题管理页面,修复 HTML 结构 2026-04-27 12:48:28 +08:00
lt d645c032ca fix: 正确添加统一卡片容器,修复 HTML 结构 2026-04-27 12:36:03 +08:00
lt 62a293e2b0 feat: 选题管理页面添加统一卡片容器,包裹标题、按钮、筛选和列表,视觉更整体 2026-04-27 12:27:51 +08:00
lt 6c6a153997 fix: 侧边栏宽度从 192px (w-48) 改为 160px (w-40),与主内容 margin-left 对齐,消除空隙 2026-04-27 12:18:17 +08:00
lt d9ffcb5840 fix: 修正布局溢出 - 侧边栏 fixed 160px, 主内容 max-width calc(100vw-160px), 消除右侧超出 2026-04-27 12:10:31 +08:00
lt fb99241a0f fix: 固定宽度布局 - 侧边栏 160px + 主内容 calc(100vw-160px) = 100vw 2026-04-27 11:57:11 +08:00
lt d3e64bd649 fix: 改用 margin-left: 160px 布局,确保总宽度=100vw,不超出边界 2026-04-27 11:54:50 +08:00
lt 17b294726a fix: 确保选题管理标题和列表右边界对齐,不超出容器 2026-04-27 11:47:22 +08:00
lt 9725ac73d4 fix: 确保选题列表卡片背景不超出页面右边界 2026-04-27 11:44:54 +08:00
lt 95f0c6b345 style: 操作列宽度调整为 210px,3 个按钮一行,第 4 个自动换行左对齐 2026-04-27 11:43:16 +08:00
lt fb83c164da style: 操作列按钮换行后左对齐,修复样式问题 2026-04-27 11:41:42 +08:00
lt c0c062e863 fix: 修复 deleteTopic 未暴露问题,调整日志下拉框宽度 2026-04-27 11:37:13 +08:00
lt 277b13eaae feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容:
1. 表格布局:
   - 使用 calc(100vw - 160px) 确保表格不超出视口
   - 操作列 fixed='right' 固定在右侧,宽度 300px
   - 按钮 3 个后自动换行 (max-width: 200px)
   - 恢复合理列宽,不再过度压缩

2. 批量操作区域:
   - 容器改为 inline-block,宽度自适应按钮内容
   - 背景宽度与按钮总宽度匹配

3. 分类标签:
   - 显示数量 (如 '待处理 (20)')
   - 点击切换筛选,去掉误导的 'X' 图标

4. 删除功能:
   - 操作列增加删除按钮
   - 删除前弹出确认对话框

5. 系统日志:
   - 修复后端日志路径 (parents[4])
   - 404 时显示友好提示

6. 其他:
   - 左侧菜单宽度 160px
   - 所有功能保留 (登录、用户管理、批量操作等)
2026-04-27 11:32:17 +08:00
lt 59d2a76df4 style: 批量操作按钮区域改为自适应宽度,不再固定 100% 2026-04-27 11:25:11 +08:00
lt 4412adfae5 style: 调整按钮容器宽度,确保第 4 个按钮换行 2026-04-27 11:23:00 +08:00
lt 86425eb18f style: 操作列宽度 300px,按钮 3 个后自动换行 2026-04-27 11:20:57 +08:00
lt 1be6d5e57d style: 操作列宽度调整为 400px 2026-04-27 11:19:27 +08:00
lt 3ff38c5ee0 fix: 使用 calc(100vw - 160px) 技术完美固定操作列,不再超出边界 2026-04-27 11:17:27 +08:00
lt be091287b8 fix: 终极布局修复 - 表格内部滚动,操作列固定右侧,总宽约1000px 2026-04-27 11:14:48 +08:00
lt 678314682e fix: 大幅缩减表格列宽度,启用 fixed 布局,防止超出视口 2026-04-27 11:11:09 +08:00
lt 08e5df6ab7 fix: 修复表格超出边界问题,确保 max-width 100% 2026-04-27 11:09:29 +08:00
lt c95f061ec0 style: 缩小左侧菜单宽度至 160px,表格添加横向滚动防止超出边界 2026-04-27 11:07:42 +08:00
lt 379592a35d fix: 修复分类标签、删除按钮、日志 API 路径 - 保留操作列固定右侧设计 2026-04-27 11:00:11 +08:00
271 changed files with 31918 additions and 9650 deletions
+13
View File
@@ -46,6 +46,7 @@ sessions/
.cache/
# Project specific
automation/backups/
automation/data/drafts/*
!automation/data/drafts/.gitkeep
automation/data/releases/*
@@ -54,3 +55,15 @@ automation/data/published/*
!automation/data/published/.gitkeep
content/published/*
!content/published/.gitkeep
automation/data/outlines/
automation/data/research/
automation/data/sustainability_raw/
automation/data/search_cache.json
automation/data/trends.json
automation/data/sustainability_cases.json
automation/images/
data/*.db
*.db
automation/data/mcp_*.json
automation/data/*_session.txt
research/
@@ -0,0 +1,10 @@
{
"sessionID": "ses_12cdfe924ffebqMk2WsK4OQD87",
"updatedAt": "2026-06-17T01:09:35.927Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-06-17T01:09:35.927Z"
}
}
}
@@ -0,0 +1,10 @@
{
"sessionID": "ses_13069b590ffefKhsLBe5CKeIx4",
"updatedAt": "2026-06-16T08:46:00.608Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-06-16T08:46:00.608Z"
}
}
}
@@ -0,0 +1,11 @@
{
"sessionID": "ses_1dede8a7affeG6DxtfPUOviXrp",
"updatedAt": "2026-06-17T08:18:52.213Z",
"sources": {
"background-task": {
"state": "active",
"reason": "2 background task(s) active",
"updatedAt": "2026-06-17T08:18:52.213Z"
}
}
}
+115
View File
@@ -0,0 +1,115 @@
# AGENTS.md
## Stack
- **Backend**: FastAPI 0.104 + SQLAlchemy 2.0 + PostgreSQL 16 (`yzr_nr`)
- **Frontend**: Vue 3 (CDN, no build step) + Element Plus — static HTML served by FastAPI
- **Auth**: JWT (`python-jose` + bcrypt), default admin `admin/admin123`
- **Scheduler**: APScheduler (daily cron: 01:10 trends, 01:30 collect, 02:00 generate, 03:00 optimize, 05:00 sources, 06:00 metrics)
- **Task DB**: `TaskLog` (module_id/status/error_trace/result_data/triggered_by) + `TaskConfig` (params/enabled/schedule)
- **LLM**: Multi-provider (nvidia primary, opencode-go fallback). API keys in DB (managed via admin UI) or `.env`.
## Commands
```bash
# Start/stop/restart server (systemd service, auto-restart on failure)
systemctl start yzr-platform.service # 启动
systemctl stop yzr-platform.service # 停止
systemctl restart yzr-platform.service # 重启
systemctl status yzr-platform.service # 查看状态
journalctl -u yzr-platform.service -n 50 --no-pager # 查看日志
# Fallback: start without systemd (用于调试)
cd /root/openclaw-workspace/projects/yu-zhi-ran
setsid ./start-platform.sh 8001
# Run full integration test
cd /root/openclaw-workspace/projects/yu-zhi-ran && python3 tests/test_new_features.py
# Run specific scripts (from project root)
python3 scripts/collector.py
python3 scripts/creator.py --topic-id B02
```
## Project layout
```
yu-zhi-ran/
├── platform/
│ ├── backend/app/main.py # FastAPI entry, mounts frontend at /
│ ├── backend/app/api/*.py # 21 API routers
│ ├── backend/app/core/ # nvidia_client.py, scheduler.py, etc.
│ ├── backend/app/models.py # SQLAlchemy models (593 lines)
│ ├── backend/app/schemas.py # Pydantic schemas (504 lines)
│ ├── backend/app/database.py # PG env config + ALTER TABLE migrations
│ ├── backend/app/initial_data.py
│ └── backend/.env # API keys, DB creds
├── scripts/ # creator.py, writer.py, collector.py, etc.
├── tests/test_new_features.py # 33-test integration suite
└── PROGRESS.md # Single source of truth for project status
```
## Gotchas & conventions
### Server
- Shell timeout kills background processes — always use `setsid` to start
- Env in `platform/backend/.env`, loaded via `dotenv` at each module level
### Database
- `init_db()` in `database.py` runs ALTER TABLE migrations at startup (PostgreSQL)
- `USE_POSTGRES=false` falls back to SQLite (used in tests)
- Models have timezone-aware `DateTime(timezone=True)` columns
### Content quality architecture (三平台差异化)
**核心原则:三平台不再共享同一篇 markdown,各自独立展开。**
- `writer.py` 对 zhihu/wechat/xiaohongshu 分别调用 `generate_platform_markdown(platform)`,各走不同的 `section_expansion_{platform}` prompt
- 知乎:数据分析深度(400-800字/节),用「你」称呼读者
- 公众号:个人叙事对话感(300-500字/节),用「我」口吻
- 小红书:精炼干货(100-200字/节),直接给方法,可用 emoji
**字数配置:**
- 知乎 min 3000 / max 8000
- 公众号 min 2000 / max 4000
- 小红书 min 400 / max 1000
- 存于 `platform_configs` 表,`min_words` / `max_words` 字段,后台「平台管理」可改
**AI 套话检测:**
- DB 存储:`content_clean_rules` 表,`rule_type='ai_telltale'`
-`config_items.py` 中的 `DEFAULT_CONTENT_CLEAN_RULES` 种子数据
- `compliance_checker.py` 运行时从 DB 加载(`_load_ai_telltales()`),DB 不可用时回退代码硬编码列表
- 后台「配置管理→敏感词/清理规则」可增删改(操作 `ContentCleanRule` 表)
### Prompts (`prompt_configs` table)
- **DB 是唯一来源**,修改 prompt 直接 `UPDATE prompt_configs SET content = '...' WHERE key = '...';`
- 代码 `scripts/prompt_loader.py` 中的 `_PROMPT_DEFAULTS` **仅作种子数据**,第一次写入后就不再生效
- 新增 prompt:在 `_PROMPT_DEFAULTS` 添加定义 → 重启后自动补入 DB(仅当该 key 不存在时)
- 修改 prompt:**直接改 DB,不要改代码**(除非要更新种子供新环境用)
- DB 不可用时回退代码默认值(仅紧急模式)
- 三平台独立 section_expansion 提示词 key: `section_expansion_zhihu` / `section_expansion_wechat` / `section_expansion_xiaohongshu`
- 标题提示词去套路化,使用自然语言(不像 AI 写的 prompt)
### Prompt quality checks (`compliance_checker.py`)
- 软质量问题(AI套话/人称混用/阅读体验)只降分、不挡流程(`passed=true`
- 硬合规问题(敏感词/法律/品牌)扣分多且阻塞流程
- AI 套话从 DB `content_clean_rules(rule_type='ai_telltale')` 加载,后台可动态管理
- 人称检查修正:去掉「大家」误报,仅检查「你们」和「你」混用
### LLM
- `call_llm()` in `core/nvidia_client.py` — reads active provider from DB `LLMConfig.is_active`, API key from env
- DeepSeek reasoning models return `reasoning_content` (thinking) + `content` (answer). `call_llm` prefers `content`, falls back to tail of `reasoning_content`
- `max_tokens` must be generous (≥500 for tags/titles, ≥2000 for article content) — reasoning models consume tokens for thinking
- Schema (`LLMConfigResponse`) must include `provider`, `base_url`, `api_key` fields or they get silently dropped from API responses
### Frontend
- No npm build step — edit `.html` files directly
- H5 mobile nav only created when `window.innerWidth <= 768`
- `navigation-component.js` + `navbar-component.js` injected as Vue components
- For date filters on topics, use backend `?today=true` (server-side `date.today()`) — client-side `new Date()` gives UTC which differs from Asia/Shanghai by 8h
### Tests
- `test_new_features.py` starts its own uvicorn on port 18503, runs against SQLite
- Run from project root: `python3 tests/test_new_features.py`
### Project status
- PROGRESS.md is the single truth source for progress — update it after any significant task
- `archive/` dir keeps historical/outdated docs with `YYYY-MM-DD` date suffix
+542
View File
@@ -0,0 +1,542 @@
# 宇之然 · 项目进度总览
> 本文件为项目进度唯一真理源,所有进度信息以此为准。
> 其他文档中的进度描述一律以本文为准。
**最后更新**2026-06-21 (v24)
---
## 一、项目总览
| 项目 | 内容 |
|------|------|
| 品牌 | 宇之然 — 科技沟通万物,愿世间百态回归自然 |
| 技术栈 | FastAPI + SQLAlchemy + PostgreSQL 16 + Vue 3 (CDN) + Element Plus |
| 平台服务 | 运行中 (端口 8001) |
| 策略阶段 | 全球-本土对比研究(2026-04-15 升级) |
| Git 提交 | 170 commits · 4 tags (v1.0.0~v1.0.4) · main 分支 |
---
## 二、里程碑完成情况
| 里程碑 | 说明 | 计划 | 实际 | 状态 |
|--------|------|------|------|------|
| M1 | 项目框架搭建完成 | 2026-04-10 | 2026-04-10 | ✅ |
| M2 | 首批选题确定 | 2026-04-12 | 2026-04-10 | ✅ |
| M3 | 首篇文章发布 | 2026-04-14 | 2026-04-14 | ✅ |
| M4 | 首月发布4篇 | 2026-05-10 | 2026-06-18 | ✅ 4篇完成 |
---
## 三、发布记录
| 编号 | 标题 | 日期 | 平台 | 状态 |
|------|------|------|------|------|
| 001 | 种菜一年,焦虑降了43%:我在阳台治愈了自己 | 2026-04-14 | 知乎/微信/小红书 | ✅ 已发布 [知乎链接](https://zhuanlan.zhihu.com/p/2026998296120411553) |
| TOPIC-BBB1CC | 城市农业新趋势:东京垂直农场与上海阳台种菜对比研究 | 2026-04-21 | 知乎/微信/小红书 | ✅ 已发布 |
| A01 | 远程工作2026中国指南:从"不可能"到"可行"的路径图 | 2026-04-23 | 知乎/微信/小红书 | ✅ 已发布 |
---
## 四、模块进度
### 4.1 平台系统
| 模块 | 状态 | 说明 |
|------|------|------|
| 后端 API (auth/topics/articles/publishing/calendar/metrics/assets/tasks/platform_config/admin) | ✅ 完成 | 核心 11 个 API 模块,JWT 认证 |
| 扩展 API (cases/audit/llm_configs/system_configs/optimizer_logs/task_logs/task_configs) | ✅ 完成 | 新增案例库、审计、LLM配置、任务配置、任务运行记录模块 |
| 前端页面 (仪表盘/选题/日历/数据/素材/任务/平台/系统管理/用户/日志/文章) | ✅ 完成 | Vue 3 + Element Plus SPA |
| 数据库 (PostgreSQL 16) | ✅ 运行中 | `yzr_nr` 库 |
| 服务 | ✅ 运行中 | 端口 8001 |
| 数据库迁移 (SQLite→PostgreSQL) | ✅ 完成 | 2026-05-08 |
| PWA 离线支持 | ✅ 完成 | manifest.json + Service Worker + 离线页面 |
| articles 表 + 数据迁移 | ✅ 完成 | 48 篇文章从文件系统迁入 DB,释放文件已清理 |
### 4.2 内容生产
| 模块 | 状态 | 说明 |
|------|------|------|
| 选题库 | ✅ 完成 | 10 个旧选题 (001~010) + 20 个新选题 (4 大支柱) |
| 文章草稿 (001 系列) | ✅ 完成 | 大纲/初稿/优化版/微信版 四版齐备 |
| 配图生产 | ✅ 完成 | 10 张 SVG + 10 张 PNG,含压缩版 |
| 合规审查 | ✅ 完成 | 评分 9.2/10,引用来源补充完成 |
| 多平台发布 (001 / BBB1CC / A01) | ✅ 完成 | 知乎/微信/小红书 三平台均已发布 |
| 银发科技适配指南 | ⏳ 写作中 | drafts 中 302 行初稿 |
| F01 AI写作实战 | ✅ 已入库 | 三平台文章已存入 articles 表,待审查通过后发布 |
| F02 平台差异策略 | ✅ 已入库 | 三平台文章已存入 articles 表,待审查通过后发布 |
### 4.3 自动化系统
| 脚本 | 状态 | 说明 |
|------|------|------|
| publish_to_zhihu.sh | ✅ 可用 | 知乎自动发布 |
| publish_to_wechat_mp.sh | ✅ 可用 | 微信公众号自动发布 |
| generate_images.py / image_generator.py | ✅ 可用 | SVG + PNG 配图生成 |
| compliance_checker.py | ✅ 可用 | 合规审查(敏感词/平台规则/品牌规范) |
| compliance_optimizer.py | ✅ 重构 | 移除 manual_review,改为迭代LLM修复(最多3次),合规分回写入Topic;解硬编码 provider,改从 env 读取;关键词过滤从宽泛改为精准 |
| creator.py / writer.py / outline.py / research.py | ✅ 优化 | 全链路LLM提示词优化(SEO/平台适配/真人感) |
| collector.py / collector_db_integration.py | ✅ 可用 | 趋势采集 |
| wecom_notifier.py | ✅ 可用 | 企业微信通知 |
| db_helper.py | ✅ 扩展 | update_topic_status 支持保存 compliance_score |
| search_utils.py | ✅ 重构 | 移除 _call_mcpopencode MCP),新增 360/搜狗/微信搜索(免 API Key),百度千帆日限提升至 200 |
| opencode_search.py / mcp_search_server.py | ❌ 已删除 | opencode 搜索配额耗尽,替换为 360/搜狗/微信等免 Key 源 |
| web_search.py | ✅ 保留 | 本地缓存 + Bing 搜索(闲置备用) |
### 4.4 流水线流程
| 步骤 | 触发方式 | 说明 |
|------|---------|------|
| 内容采集 | 定时 01:30 | 热点趋势采集→生成选题建议→存入选题库 |
| 内容创作 | 手动点击 / 定时 03:30 (原 02:00) | 研究→大纲→撰写文章→合规审查→存入 articles 表 |
| 合规审查 | 手动点击 / 定时 04:30 (原 03:00) | 从 articles 表读取 draft→合规检查→LLM迭代修复(最多3次)→状态→待发布 |
| 信息源优化 | 定时 05:00 | AI评估采集类别与信息源配置,给出调整建议 |
| 指标同步 | 定时 06:00 | 同步统计数据 |
| 发布 | 手动点击 | 仅待发布状态可选 |
---
## 五、当前任务队列
### ✅ 已完成
| 任务 | 完成日期 | 备注 |
|------|---------|------|
| 审查流程优化(移除 manual_review | 2026-05-14 | 改为迭代LLM修复(最多3次),合规分回写Topic |
| 内容采集加入定时调度 | 2026-05-14 | scheduler 新增 scheduled_collect 01:30 |
| 全链路LLM提示词优化 | 2026-05-14 | 覆盖trends/topic_selector/research/outline/writer/nvidia_client/compliance 共8文件≈24个提示词,增强SEO/平台推荐/真人感 |
| 导航布局重构 | 2026-05-16 | navbar + navigation 合并为 uni-nav,单行 top-bar 自适应 PC/H510 页迁移完成 |
| 文章管理页面 (articles.html) | 2026-05-16 | 新增 articles API (list/detail/delete) + 完整管理页面,支持筛选/搜索/预览/删除 |
| CSS架构统一整合 | 2026-05-16 | reset/body/layout/card/page-header/filter/toolbar 移入 theme-modern.css86→127行),10页内联CSS总量从~660行降至~400行;preview对话框CSS归一化;删除 dead JSindex.html双style块合并为单一亮色主题 |
| Phase 1.1 配图集成到创作流水线 | 2026-05-16 | image_generator.py 支持 --topic-id 参数, DB读写图片路径; Article 新增 images JSON字段+迁移; creator.py 新增配图步骤; API GET /api/articles/{topic_id}/images; 文章页预览显示封面图 |
| Phase 1.3 统一前端体验 | 2026-05-16 | users.html 补 loading+空状态; index.html 渲染loadingStats+fetchModules错误提示; metrics.html 渲染loadingDashboard |
| Phase 2.1 发布审批 UI | 2026-05-16 | 后端 MultiPublishRequest 多平台发布 API; 前端发布确认弹窗+平台勾选 |
| Phase 2.2 日历关联文章状态 | 2026-05-16 | 后端 enrich_entry 添加 topic_status+platform_icon; 前端日历卡片显示平台图标+Topic状态标签; 修复 el-tag 缺少闭合 |
| emoji 替换为 Element Plus 图标 | 2026-05-16 | icon-components.js 注册 25 个 SVG 图标组件; 11 页 emoji → `<el-icon>` 批量替换 |
| H5 卡片布局补全 | 2026-05-16 | metrics.html 热门选题/平台对比/选题推荐三表加 H5 卡片视图 |
| Phase 3.1 平台数据对接 | 2026-05-16 | scripts/sync_metrics.py (估计算法); scheduler 新增 scheduled_metrics_sync 06:00 |
| Phase 3.2 数据看板增强 | 2026-05-16 | Chart.js 集成: 趋势折线图/状态环形图/平台对比柱状图 |
| Phase 4 多租户隔离 (JWT+API 层) | 2026-05-16 | org_id 注入 JWT payload; 12 个 API 模块添加 org 过滤; 创建选题自动继承用户 org; 管理端组织 CRUD |
| Phase 4 前端多租户 | 2026-05-16 | admin.html 新增组织管理标签页 (列表/创建/编辑/删除); users.html 增加组织列+H5卡片显示 |
| Phase 4 预置数据修复 | 2026-05-16 | initial_data.py 管理员用户添加 org_id; opencode-go LLM config 补全 user_prompt_template |
| PlatformConfig 模型字段扩展 | 2026-05-17 | 增加 requires_image/image_count_min/image_count_max/image_width/image_height/min_words/max_words; schemas/initial_data/前端表单同步; database.py 迁移 |
| 微信公众号正文配图 | 2026-05-17 | writer.py wechat 分支在 h1 后插入 `<img>` 占位 |
| 合规检查从 DB 读平台规则 | 2026-05-17 | compliance_checker.py 接受 platform_config 参数; compliance_optimizer.py 从 DB 加载平台配置传入 checker |
| 登录页居中修复 | 2026-05-18 | login.html body 添加 login-body 类 |
| 模块状态重复修复 | 2026-05-18 | system.py 重命名/去重,状态改查 jobs 而非日志文件 |
| 仪表盘"近期计划"卡片 | 2026-05-18 | index.html 新增日历 7 天预告卡片 |
| 日志/用户合并到系统管理 | 2026-05-18 | admin.html 增加用户管理+运行日志标签页;uni-nav 10→8 项 |
| 日历页增强 | 2026-05-18 | 指南横幅/即将到来列表/空状态/操作提示 |
| 任务页优化+分页 | 2026-05-18 | 6个定时任务/topic_title/STAGE_NAMES/前端分页 |
| 选题页前端分页 | 2026-05-18 | paginatedTopics computed + el-pagination |
| 后端 tasks API 改造 | 2026-05-18 | offset 参数 + topic_title 字段 |
| admin 全标签页分页 | 2026-05-18 | 案例/类别/信息源/组织/用户 5 标签统一分页(10条/页)+总数显示 |
| 选题预览修复 | 2026-05-18 | 补全 startEdit/cancelEdit/saveContent 方法;修复滚动条;strip .tags |
| 任务状态卡死修复 | 2026-05-18 | 后台线程改用独立 DB session;前端 5s 自动轮询;中间进度 30% 显示 |
| 预览平台切换修复 | 2026-05-18 | 补全 switchPlatform 方法 |
| 超时任务自动清理 | 2026-05-18 | 启动时标记运行>1h 任务为 failed;已清理 21 个卡死任务 |
| metrics 冗余统计移除 | 2026-05-18 | 删除与仪表盘重复的 4 个概览卡片 |
| writer max_tokens 提升 | 2026-05-19 | 标题/标签 max_tokens 500→1000,修复推理模型思考链占用导致输出截断 |
| 预览滚动条修复 | 2026-05-19 | 改用 height:68vh 替代 flex+calc,避免 Element Plus 对话框内滚动冲突 |
| 定时任务 DB 化 | 2026-05-22 | TaskLog 新增 module_id/error_trace/triggered_by/result_dataTaskConfig 模型新建含 params/schedule/enabledscheduler.py 全 7 个任务执行前后写 TaskRunmodules/status 从 DB 读取 |
| 平台配置弹窗修复 | 2026-05-22 | el-dialog 移入 tab 内部(与表格同级)解决响应式问题;表单字段补全 |
| AI 思考内容清洗 | 2026-05-22 | 新建 content_cleaner.py 集中管理清洗规则;writer.py/compliance_optimizer.py 统一引用;THINKING_PATTERNS 增强;strip_ai_preface 处理代码围栏块 |
| 复制正文去噪音 | 2026-05-22 | copyContent 只提取 p/h1-h4/li 元素,去 style/svg/script/img |
| platforms.html 入口合并 | 2026-05-22 | uni-nav 移除"平台"独立入口;admin.html 恢复"平台配置"tab 加启用中/全部筛选 |
| opencode_search.py 日志 | 2026-05-22 | 补 FileHandler + StreamHandler,解决管理后台显示"从未运行" |
| scheduler.json 导入修复 | 2026-05-22 | 补 import json,修复 sources(05:00) 执行时报错阻断 metrics(06:00) |
| Scheduler 日志修复(僵尸行) | 2026-05-28 | _log_task 始终 INSERT 新行,所有 _run_* 保存 log_id 后 UPDATE 同一行,消除重复 running 状态 |
| Systemd 服务化 | 2026-05-28 | yzr-platform.serviceRestart=always,崩溃自动恢复);单 worker(--workers 1)防调度器冲突;Type=exec + KillMode=control-group 防僵尸进程 |
| LLM 提供商量换 + 任务级模型选择 | 2026-05-28 | opencode-go 换 nvidia qwen3.5-397b-a17b(默认)+ sensenova deepseek-v4-flash(合规审查);llm_configs 加 is_default;所有 _run_* 方法开头调用 _set_task_llm_provider 设置 LLM_TASK_PROVIDER |
| 合规审查修复 | 2026-05-28 | compliance_optimizer.py 移除硬编码 provider;补 import os(之前导致 NameError);关键词过滤从宽泛改为精准,避免误拦正常内容 |
| 搜索提供商重构 | 2026-05-28 | 移除 opencode MCP(配额耗尽),新增 360/搜狗/微信搜索(免 Key),百度千帆日限 50→200 |
| opencode 冗余代码清理 | 2026-06-02 | 删除 opencode_search.py / mcp_search_server.py / _call_mcp / 前后端所有 opencode/MCP 引用;禁用搜索缓存定时任务 |
| 任务抽屉产出记录优化 | 2026-06-03 | drawer 产出结果 tab 优先显示日志消息(last_message),过滤 None/空值;修复 tasks.py 因 opencode 清理导致的 SyntaxError;清理 DB 中旧搜索缓存的 56 条 TaskLog + 1 条 TaskConfig |
| Hermes gateway 用户级服务恢复 | 2026-06-04 | 系统级服务 → 用户级(MemoryMax=1G 生效);删除 system 级文件;linger + default.target.wants 开机自启 |
| v-cloak 修复 Vue 模板闪烁 | 2026-06-04 | 9 页 `<div id="app">` + `theme-modern.css``[v-cloak]` 规则,消除原始模板代码闪烁 |
| **sensenova 多模型 + 模型级 rate limit** | **2026-06-04** | `LLMConfig``rate_limit`/`rate_limit_window_minutes`seed 3 模型:deepseek-v4-flash(500次/5h)、6.7-flash-lite(1500次/5h)、u1-fast(1500次/5h);进程内 rate limiter`LLM_TASK_MODEL` 环境变量实现任务级模型选择;`ai_image_generator.py` 已固定用 `sensenova-u1-fast` |
### ✅ 已完成
| 任务 | 完成日期 | 备注 |
|------|---------|------|
| GEO/SEO 结构化元数据注入 | 2026-06-09 | writer.py 的 `inject_geo_metadata()` 向 HTML 注入 JSON-LD Article schema、meta description/keywords、OG tags |
| 大纲提示词 SEO 关键词增强 | 2026-06-09 | outline.py 新增 `_extract_seo_keywords()` 从研究笔记提取关键词;outline prompt 加入 GEO 数据引用要求 |
| 搜索排名追踪 | 2026-06-09 | `SearchRanking` 模型(文章ID/关键词/排名位置/AI引用);`rank_tracker.py` 定时任务(每日 07:00)查 Bing 排名;`/api/seo/rankings` APIadmin.html 新增"搜索排名"tab |
| rank_tracker AI 引用检测 | 2026-06-18 | `_check_ai_citation()` 用 LLM 检测 Bing AI/百度 AI/Perplexity 引用 |
| admin.html 任务管理 tab | 2026-06-18 | TaskConfig 参数编辑 + TaskLog 时间轴 (el-timeline) |
| M4 第4篇文章发布 | 2026-06-18 | 发布 循环消费新物种 + 端侧AI隐私实战,累计 4 篇达首月目标 |
| 导航 bug 修复(仪表盘→index.html | 2026-06-18 | DB menu 中"仪表盘"path 从 `/``index.html`,解决点导航跳转 landing 页的问题 |
| Landing 页 OG 图片创建 | 2026-06-18 | `/automation/images/og-landing.png`1200x630 PNG62KB);之前 404 |
| Landing 页 GitHub 链接修正 | 2026-06-18 | `your-org``github.com` |
| v23 全部功能验收 | 2026-06-18 | 12 页 200 OK、org_id 隔离生效、RBAC(require_role)11路由、菜单7项正确、factory/insights 新页、dashboard 重设计 |
## 十一、外推营销系统(v24 · 2026-06-21
### 从「内容生产」到「外部推广」
**背景**:平台此前只覆盖平台内生产的内容的 SEO/GEO 优化,无法对用户已有的外部网站、公众号、小程序等做 SEO 推广。v24 新增完整的外部推广模块。
### 新增数据模型(6 张表)
| 模型 | 表名 | 说明 |
|------|------|------|
| `ExternalProduct` | `external_products` | 外部推广产品(website/article/wechat_account/miniprogram|
| `PromotionCampaign` | `promotion_campaigns` | 推广活动,关联产品和关键词 |
| `CampaignKeyword` | `campaign_keywords` | 关键词跟踪(搜索量/难度/排名)|
| `SEOAudit` | `seo_audits` | SEO 审计报告(6 维度评分)|
| `KeywordRanking` | `keyword_rankings` | 多引擎关键词排名记录 |
| `OptimizationTask` | `optimization_tasks` | AI 生成的优化建议 |
### API 端点(20 个)
| 端点 | 功能 |
|------|------|
| `GET/POST/PUT/DELETE /api/external/products` | 产品 CRUD |
| `GET/POST/PUT/DELETE /api/external/campaigns` | 活动 CRUD |
| `GET/POST/PUT/DELETE /api/external/keywords` | 关键词 CRUD |
| `GET /api/external/audits` | SEO 审计记录列表 |
| `GET /api/external/audits/{id}` | 审计详情(含优化建议)|
| `GET /api/external/rankings` | 关键词排名列表 |
| `GET /api/external/rankings/overview` | 排名概览统计 |
| `GET /api/external/optimization-tasks` | 优化建议列表 |
| `PATCH /api/external/optimization-tasks/{id}` | 更新任务状态 |
| `GET /api/external/overview` | 外推营销总览 |
### SEO 审计脚本
`scripts/seo_auditor.py` — 自动抓取目标网站并分析:
| 维度 | 权重 | 检测项 |
|------|------|--------|
| Meta 标签 | 20% | title/description/keywords/OG tags |
| 标题结构 | 15% | H1/H2 存在性、层级连续性 |
| 内容质量 | 25% | 字数、图片 alt、关键词密度 |
| 性能 | 10% | 渲染阻塞资源、图片尺寸 |
| 链接 | 10% | 内链/外链/断链/nofollow |
| 移动端 | 20% | viewport、响应式、字号 |
审计结果自动保存到 DB,生成可追踪的 OptimizationTask。
### 关键文件变更
| 文件 | 改动 |
|------|------|
| `platform/backend/app/models.py` | 新增 6 个模型(ExternalProduct/PromotionCampaign/CampaignKeyword/SEOAudit/KeywordRanking/OptimizationTask|
| `platform/backend/app/schemas.py` | 新增 18 个 Pydantic schema 类 |
| `platform/backend/app/api/external_promotion.py` | **新建** — 20 个 API 端点 |
| `platform/backend/app/main.py` | 注册 external_promotion router |
| `platform/frontend/campaigns.html` | **新建** — 外推营销 SPA 页面(6 Tab)|
| `platform/frontend/uni-nav.js` | 导航增加「外推营销」入口 |
| `scripts/seo_auditor.py` | **新建** — SEO 审计自动化脚本 |
---
## 六、归档日志
> 过时文档统一移入 `archive/`,防止误导。
| 日期 | 路径 | 原因 |
|------|------|------|
| 2026-05-12 | `tasks/todo.md``archive/tasks/todo.md.2026-04-10` | 内容停滞于 2026-04-10,信息过时 |
| 2026-05-12 | `content/ideas/001-compliance.md``archive/content/ideas/` | 001 已发布,合规报告无须保留在活跃目录 |
| 2026-05-12 | `content/ideas/001-research.md``archive/content/ideas/` | 001 已发布,研究笔记无须保留在活跃目录 |
| 2026-05-12 | `content/publishing/001-images.md``archive/content/` | 配图已生成,规划稿无须保留在活跃目录 |
---
## 七、内容质量升级(v19 · 2026-06-05
### 三平台差异化内容架构
**背景**:此前三平台共用同一篇 markdown,仅做浅层适配(加 emoji/换人称),导致知乎缺深度、公众号缺情感、小红书过长。
**核心改动:**
1. **writer.py**`generate_full_markdown()``generate_platform_markdown(platform)`,每平台调用独立的 `section_expansion_{platform}` prompt
- 知乎:`section_expansion_zhihu` — 数据分析深度,400-800字/节,「你」视角
- 公众号:`section_expansion_wechat` — 个人叙事对话感,300-500字/节,「我」口吻
- 小红书:`section_expansion_xiaohongshu` — 精炼干货,100-200字/节,直接给方法+可 emoji
- 去掉公众号中「我们→我→你」的强制替换
2. **prompt_loader.py** — 新增 3 个平台独立 section_expansion 提示词,标题提示词去套路化(去除「直接输出」「不要其他文字」等 AI 味指令)
3. **平台字数提升** — min/max 大幅上调(知乎 3000-8000, 公众号 2000-4000, 小红书 400-1000),存于 `platform_configs` 表,后台可改
4. **compliance_checker.py** — AI 套话从 DB `content_clean_rules(rule_type='ai_telltale')` 加载,后台可动态管理;人称规则去掉「大家」误报
5. **config_items.py** — DEFAULT_CONTENT_CLEAN_RULES 新增 30+ 条 ai_telltale 规则
6. **initial_data.py** — PlatformConfig 字数迁移(已有库也会更新)
7. **AGENTS.md** — 文档化三平台差异化架构
**验证**33/33 测试通过
## 八、GEO/SEO 升级(v20 · 2026-06-09
### 为 AI 搜索时代而生
**背景**ChatGPT、Perplexity、百度 AI 搜索等生成式引擎正在取代传统搜索。内容被 AI 引用的价值大于单纯的关键词排名。此版本让流水线生产的内容天然符合 AI 搜索偏好。
### GEO 结构化数据
每篇生成的 HTML 自动注入:
- **JSON-LD Article schema** — `@context: schema.org`,含 headline/description/datePublished/author/publisher/keywords
- **SEO meta tags** — `<meta name="description">`, `<meta name="keywords">`
- **Open Graph tags** — `og:title`, `og:description`, `og:type`, `og:site_name`, `article:published_time`
- **平台专用** — wechat/xiaohongshu 额外注入 `og:image`
### 大纲 SEO 增强
- outline prompt 新增 `seo_keywords` 变量(从研究笔记自动提取)
- 每章要求至少融入 1 个 SEO 关键词
- 新增 GEO 要求:每章包含可引用数据点,增加被 AI 搜索引用的概率
### 搜索排名追踪
| 模块 | 说明 |
|------|------|
| `SearchRanking` 模型 | 记录 article_id/keyword/position/url_found/ai_cited/search_engine |
| `scripts/rank_tracker.py` | 对所有已发布文章,自动生成搜索查询 → 调用 Bing API 查排名 → 写入 DB |
| 定时任务 | `scheduled_rank_tracker` 每日 07:00 自动运行 |
| 管理后台 | admin.html 新增「搜索排名」tab,展示概览统计(累计检查/有排名/AI引用/最佳排名)+ 排名列表 |
### 关键文件
| 文件 | 改动 |
|------|------|
| `scripts/writer.py` | 新增 `inject_geo_metadata()`, `_extract_description()`, `_extract_tags_list()` — 在 `generate_platform_html()` 末尾注入 |
| `scripts/outline.py` | 新增 `_extract_seo_keywords()` — 传入 outline prompt |
| `scripts/prompt_loader.py` | outline_generation 提示词加入 `seo_keywords` 变量 + GEO 数据点要求 |
| `scripts/rank_tracker.py` | 新建 — Bing 排名查询 + DB 写入 |
| `platform/backend/app/models.py` | 新增 `SearchRanking` 模型 |
| `platform/backend/app/api/search_rankings.py` | 新建 — `/api/seo/rankings` + `/api/seo/rankings/overview` |
| `platform/backend/app/core/scheduler.py` | 新增 `scheduled_rank_tracker` 07:00 |
| `platform/frontend/admin.html` | 新增「搜索排名」tab(统计卡片 + el-table 排名列表) |
## 九、GEO 进阶升级(v21 · 2026-06-15
### AI 搜索引用追踪(GEO Engine Optimization
**背景**:v20 实现了搜索排名追踪(Bing)和基础元数据注入,v21 将范围扩展到 AI 搜索引用检测、GEO 就绪度评分、FAQ/HowTo 结构化数据注入。
### 1. 数据模型扩展
| 模型 | 字段 | 说明 |
|------|------|------|
| `SearchRanking` | `ai_search_engine` / `citation_snippet` / `citation_url` / `geo_score` / `content_type` | 原有排名模型新增 AI 搜索字段和内容类型 |
| `GeoReadinessScore`(新建) | `article_id/total_score(0-100)/has_schema/has_faq_format/has_howto_format/has_citations/word_count/readability_score/heading_structure_score` | 6 维度 GEO 就绪度评估 |
就绪度评分权重:结构化数据 30 + FAQ 格式 20 + HowTo 格式 15 + 引用 15 + 标题结构 10 + 内容长度 10 = 100 分
### 2. AI 搜索引用追踪脚本
`scripts/geo_tracker.py` — 对所有已发布文章,通过 LLM 模拟查询 DeepSeek/ChatGPT/Perplexity 检测是否被引用:
- 筛选 `status=published` 的文章提取标题/关键词
- 对每篇文章调用 `call_llm` 查询各 AI 引擎是否收录并引用
- 记录引用片段(citation_snippet)和引用 URL
- 计算 GEO 就绪度评分(6 维度 0-100
- 写入 `SearchRanking(search_engine="geo")``GeoReadinessScore`
### 3. FAQ/HowTo Schema 自动注入
扩展 `writer.py``inject_geo_metadata()`
- 新增 `_detect_content_type(content)` — 检测内容类型:article/listicle/howto/faq/review(基于关键词统计)
- 新增 `_extract_faq_pairs(content)` — 从 Q/A:格式提取问答对
- 新增 `_extract_howto_steps(content)` — 从步骤/Step 格式提取步骤
- 根据检测到的类型自动注入对应的 JSON-LD schema
- **FAQPage** — `mainEntity` 数组(Question + AcceptedAnswer
- **HowTo** — `step` 数组(HowToStep + HowToDirection
- **ItemList** — `itemListElement` 数组(ListItem + position + name
### 4. GEO API 端点
| 端点 | 功能 |
|------|------|
| `GET /api/seo/geo/overview` | AI 引用总数/引用率/引擎分布/平均就绪度评分 |
| `GET /api/seo/geo/readiness` | GEO 就绪度评分列表(含各维度分数) |
| `POST /api/seo/geo/track` | 手动触发 GEO 追踪 |
### 5. 定时任务注册
`scheduler.py` 新增 `scheduled_geo_tracker` 每日 07:30 自动运行
### 6. 管理后台 GEO 看板
admin.html 新增「GEO 就绪度」标签页包含:
- **统计卡片**:GEO 检查次数 / AI 引用次数 / AI 引用率 / 平均就绪度评分
- **引擎分布**:按 AI 引擎(DeepSeek/ChatGPT/Perplexity)展示检查数和引用数
- **就绪度评分表格**:文章标题/总分(彩色标签)/结构化数据✓✗/FAQ✓✗/HowTo✓✗/引用✓✗/字数/可读性/标题结构/检查时间
- **手动追踪按钮**`POST /api/seo/geo/track`
### 关键文件变更
| 文件 | 改动 |
|------|------|
| `scripts/geo_tracker.py` | **新建** — AI 搜索引用追踪 + GEO 就绪度评分全流程 |
| `scripts/writer.py` | 扩展 `inject_geo_metadata()`:新增 `_detect_content_type`/`_extract_faq_pairs`/`_extract_howto_steps`,根据内容类型自动注入 FAQPage/HowTo/ItemList schema |
| `platform/backend/app/models.py` | `SearchRanking` 扩展 5 字段 + 新建 `GeoReadinessScore` 模型 |
| `platform/backend/app/api/search_rankings.py` | 新增 3 个 GEO 端点(overview/readiness/track |
| `platform/backend/app/core/scheduler.py` | 注册 `scheduled_geo_tracker` 07:30 |
| `platform/frontend/admin.html` | 新增「GEO 就绪度」tab(统计卡片 + 引擎分布 + 评分表格 + 手动追踪) |
### 验证
33/33 测试全部通过
## 十、产品官网改造(v22 · 2026-06-16
### 从「管理后台」到「产品网站」
**背景**:此前项目以登录页面作为首屏,访客第一眼看到的是管理系统的登录框,没有品牌传达和产品介绍。改造后,未登录用户看到的是一个完整的产品官网,登录后进入管理后台(工作台)。
### 路由架构
| 路径 | 页面对应 | 访问权限 |
|------|---------|---------|
| `/` | **产品官网** — 品牌 Landing Page | 公开 |
| `/index.html` | **工作台** — 原有的仪表盘(现为 workspace) | 需登录 |
| `/login.html` | 登录页(品牌化处理) | 公开 |
| `/topics.html`, `/articles.html` 等 | 各管理功能页 | 需登录 |
### 产品官网(Landing Page
创建 `platform/frontend/landing.html` — 独立的完整产品网页,不依赖 Vue/Element Plus
- **导航栏**:固定顶部,毛玻璃背景,Logo + 产品功能/工作流程/支持平台/定价/常见问题链接 + 登录/免费使用按钮
- **Hero 区域**:大标题「让内容在 AI 时代主动找到你的客户」,背景渐变+辉光效果,三个核心数据展示
- **产品功能**:6 张特性卡片(三平台创作/GEO 优化/独立站 SEO/效果追踪/全自动流水线/合规保障)
- **工作流程**:3 步流程说明(配置选题→自动生产→追踪优化)
- **支持平台**:知乎/微信公众号/小红书/独立站
- **定价**:3 档定价(入门版 ¥299/月、专业版 ¥799/月、企业版 ¥2,499/月)
- **常见问题**:6 个 FAQ(GEO 说明/三平台差异化/零经验使用/AI 套话/API Key/数据安全)
- **CTA 区块**:渐变背景 + 行动号召按钮
- **页脚**:产品/支持/法律三栏
### 关键架构改动
| 文件 | 改动 |
|------|------|
| `platform/frontend/landing.html` | **新建** — 完整产品官网(~30KB,无外部依赖) |
| `platform/backend/app/main.py` | 新增 `/` 路由显式返回 `landing.html`(优先于 StaticFiles 的默认 index.html |
| `platform/frontend/login.html` | 登录成功跳转 `/``/index.html`;已登录检测跳转 `/``/index.html` |
| `platform/frontend/uni-nav.js` | 仪表盘导航链接 `/``/index.html``navigate` 函数修复前缀处理 |
| `platform/frontend/articles.html` | 未登录重定向 `/``/login.html` |
| `platform/frontend/topics.html` | 未登录重定向 `/``/login.html` |
| `platform/frontend/sw.js` | 移除 `/` 预缓存;HTML 文档改为 Network First 策略确保新版实时加载 |
### 响应式设计
- **桌面端**>768px):完整导航 + 多列布局
- **平板/手机端**(≤768px):汉堡菜单(侧滑展开)、单列布局、缩小 hero 字体
- **小屏手机**(≤480px):全屏宽内容、堆叠布局
### 验证
- 33/33 测试全部通过
- HTTP 访问验证:`/` 返回 landing.html31070 bytes),`/index.html` 返回工作台仪表盘
- 登录流程验证:登录后跳转 `/index.html`
- 服务已重启生效
## 十一、工作台改造与数据隔离(v23 · 2026-06-17
### 从「管理工具」到「内容工作台」
**背景**:此前平台虽功能完整但偏向管理后台视角,缺乏面向内容创作者的工作台体验;且所有业务数据缺乏组织隔离(org_id),多组织共存时数据混淆。
### Wave 1 — 数据隔离基础设施
| 改动 | 文件 |
|------|------|
| 10 个业务模型追加 `org_id` 列 | `models.py`User/Topic/ContentCalendar/MediaAsset/ContentTask/Article/PublishRecord/ContentMetrics/SearchRanking/GeoReadinessScore |
| ALTER TABLE 迁移语句 | `database.py`5 条 `ADD COLUMN IF NOT EXISTS` |
| Schema 追加 `org_id` | `schemas.py`ArticleBase/ContentCalendarResponse/ContentMetricsResponse/MediaAssetResponse/ContentTaskResponse/PublishRecordResponse |
| JWT 认证追加 org_filter | `auth.py``get_current_admin` 返回 `org_id`,新增 `require_role(required_role)` 工厂函数 |
| 导航重构为流程导向 5 项 | `uni-nav.js` — 工作台/内容工厂/数据洞察/资产库/系统管理 |
### Wave 2 — API 隔离 + 新前端页面
| 改动 | 说明 |
|------|------|
| `articles.py` | Article 创建 upsert 时从 JWT 自动注入 org_id |
| `publishing.py` | PublishRecord 创建时从关联 Topic 继承 org_id |
| `metrics.py` | ContentMetrics 创建时继承 Topic.org_id |
| `calendar.py` | ContentCalendar CRUD 添加 org_id 校验 |
| `tasks.py` | ContentTask 创建时自动注入 org_id |
| `search_rankings.py` | GEO 端点添加 org_filter |
| `factory.html` | **新建** — 内容工厂流水线页面(3Tab:待创作/进行中/待审查) |
| `insights.html` | **新建** — 数据洞察页面(4Tab:内容概览/搜索排名/GEO就绪度/平台对比) |
### Wave 3 — 仪表盘重设计 + RBAC
| 改动 | 说明 |
|------|------|
| `index.html` | 从系统监控仪表盘改为用户中心工作台(欢迎横幅/摘要卡片/今日待办/AI智能建议/管理员模块折叠隐藏) |
| `ai-assistant.js` | 新增 factory/insights 页面映射 + 问候语;API 调用带入 org_id |
| `require_role` 导入 | 10 个 admin 路由文件(admin/menu_configs/role_configs/system_configs/llm_configs/collector_mgmt/prompt_configs/task_configs/search_providers/config_items)追加 import |
### Wave 4 — 验证
- **集成测试**33/33 通过(Python 编译检查/服务启动/登录/CRUD/权限验证)
- **手动 QA**
- `/` → 200landing.html
- `/index.html` → 200(工作台仪表盘)
- `/factory.html` → 200(内容工厂)
- `/insights.html` → 200(数据洞察)
- 登录正常,auth me 返回 `admin/admin/default`
- 未认证访问 401,认证后正常访问
- 系统状态 API 正常(41 topics, 22 pending, 2 published
- **Git 提交**4 个 commit 分 Wave 提交(dcfeccc / 5caf7bc / f6e5e90
### 关键架构决策
- org_id 从 JWT 自动注入,不在客户端提交(防篡改)
- 工厂页和数据洞察页新建文件,不改造现有页面
- RBAC 使用 `require_role()` 工厂模式,逐步替换 `get_current_admin`
- 平台配置(platform_config)为全局共享,不按组织隔离
- 数据隔离为 P1 级安全修复,优先于所有其他改造
### 关键文件变更
| 文件 | 改动 |
|------|------|
| `platform/backend/app/models.py` | 10 个模型追加 org_id941 行)|
| `platform/backend/app/database.py` | ALTER TABLE 迁移逻辑(170 行)|
| `platform/backend/app/api/auth.py` | org_filter + require_role196 行)|
| `platform/backend/app/schemas.py` | 6 个响应 schema 追加 org_id637 行)|
| `platform/frontend/index.html` | 用户中心仪表盘重设计 |
| `platform/frontend/factory.html` | **新建** 内容工厂流水线页 |
| `platform/frontend/insights.html` | **新建** 数据洞察页(945 行)|
| `platform/frontend/uni-nav.js` | 流程导向导航(333 行)|
| `platform/frontend/ai-assistant.js` | 页面映射 + org_id 感知 |
| `platform/backend/app/api/*.py` | 10 个路由文件追加 require_role |
---
## 十二、规范说明
### 规划文档
| 文档 | 位置 | 说明 |
|------|------|------|
| 升级计划 | `docs/upgrade-plan.md` | 分 4 阶段的技术升级路线图 |
### 文档管理原则
1. **单一真理源**PROGRESS.md 为唯一进度文档,进度信息不出现于第二个文档中。
2. **及时归档**:阶段完成后,对应过程文档移入 `archive/`(含日期后缀),避免误导。
3. **一致性**README.md 仅引用 PROGRESS.md 摘要,不独立维护进度状态。
4. **可追溯**archive 保留完整历史,按 `YYYY-MM-DD` 子目录组织。
5. **及时更新**:每次新工作结束(或次日开始前)更新 PROGRESS.md,确保最后更新日期与实际同步。
### 工作流程
```
梳理需求/任务 → 读取 PROGRESS.md 了解当前状态
按 PROGRESS.md 执行开发
阶段任务完成 → 更新 PROGRESS.md + 归档冗余文档
如需 → 同步 README.md 摘要
```
-243
View File
@@ -1,243 +0,0 @@
# 宇之然项目目录结构说明
## 整体布局
```
yu-zhi-ran/ # 项目根目录
├── platform/ # ⭐ 管理平台(Web UI + API
│ ├── backend/ # FastAPI 后端
│ │ ├── app/
│ │ │ ├── main.py # 入口
│ │ │ ├── database.py # 数据库连接(SQLite
│ │ │ ├── models.py # Topic, Article 模型
│ │ │ ├── schemas.py # Pydantic 验证
│ │ │ ├── initial_data.py # 初始数据导入
│ │ │ ├── api/ # REST API 路由
│ │ │ │ ├── system.py # │ 系统状态、流水线控制、日志
│ │ │ │ ├── topics.py # │ 选题 CRUD
│ │ │ │ ├── publisher.py # │ 发布包管理
│ │ │ │ └── articles.py # │ 文章查看
│ │ │ └── core/ # │ 业务逻辑封装
│ │ │ ├── generator.py # │ 调用 scripts/creator.py
│ │ │ ├── optimizer.py # │ 调用 scripts/compliance_optimizer.py
│ │ │ └── sync.py # │ 同步 JSON → 数据库
│ │ └── requirements.txt
│ ├── frontend/
│ │ └── index.html # Vue 3 SPACDN 依赖)
│ ├── run.sh # 快速启动脚本
│ ├── check.py # 部署前检查
│ ├── README.md # 平台使用文档
│ └── PORTFOLIO.md # 架构详解
├── automation/ # 🔄 自动化流水线数据与日志
│ ├── scripts/ (软链) → ../scripts/ # 实际脚本在 ../scripts/
│ ├── data/ # 数据存储
│ │ ├── sustainability_topics.json # ⭐ 核心:选题库
│ │ ├── drafts/ # 草稿(按日期)
│ │ │ └── 2026-04-19/
│ │ │ ├── A02_zhihu.md
│ │ │ ├── A02_wechat.md
│ │ │ └── A02_xiaohongshu.md
│ │ └── releases/ # 发布包(按日期)
│ │ └── 2026-04-19/
│ │ ├── zhihu/zhihu_A02_zhihu.html
│ │ ├── wechat/wechat_A02_wechat.html
│ │ └── xiaohongshu/
│ └── logs/ # 运行日志
│ ├── collector_2026-04-19.log
│ ├── creator_2026-04-19.log
│ ├── optimizer_2026-04-19.log
│ └── publisher_summary_2026-04-19.json
├── scripts/ # 🛠️ 底层 CLI 脚本(被 automation/scripts/ 调用)
│ ├── collector.py # 采集热点 + 本地降级
│ ├── creator.py # 研究→大纲→撰写流水线
│ ├── compliance_optimizer.py # 合规检查 + 自动修复
│ ├── publisher.py # 发布包生成(HTML + 指南)
│ ├── research.py # 资料收集(API + 本地)
│ ├── outline.py # 大纲生成
│ ├── writer.py # 核心撰写逻辑
│ ├── wecom_notifier.py # 企业微信通知(可选)
│ └── ...(其他辅助脚本)
├── content/ # 📚 已发布内容存储
│ ├── published/ # 按 topic_id 组织
│ │ ├── A01/
│ │ │ └── 手动发布/
│ │ │ ├── 知乎/文章.html
│ │ │ └── 小红书/文章.html
│ │ └── A02/
│ ├── drafts/ # 草稿(可选)
│ ├── ideas/ # 选题记录(Markdown
│ └── images/ # 配图资源
├── config/ # ⚙️ 配置文件
│ ├── sources.yaml # RSS/网页源配置
│ └── wecom_config.yaml # 企业微信机器人配置
├── brand/ # 🏷️ 品牌资产
│ ├── brand-book.md # 愿景、调性、原则
│ └── guidelines.md # 内容创作指南
├── strategy/ # 📈 内容策略
│ └── 全球-本土比较研究与全新内容战略规划-2026-04-15.md
├── tasks/ # 📋 项目管理
│ └── todo.md
├── research/ # 📊 行业研究
│ └── trends-2026.md
├── logs/ # 📝 根目录日志(兼容)
├── backup/ # 💾 备份归档
├── README.md # 项目总览
├── YUZHIRAN_PLATFORM.md # 完整文档(本文件同级)
├── TEST_FULL_PIPELINE.md # 测试指南
├── test_full_pipeline.py # 全流程测试脚本
├── run_publisher.sh # 快速发布脚本
└── start-platform.sh # 启动管理平台(新增)
# 软链接
automation/scripts → ../scripts/
```
---
## 🔗 关键依赖关系
### platform/backend/app/core/*.py 的路径计算
| 文件 | parents 层数 | PROJECT_ROOT 指向 |
|------|-------------|-------------------|
| `database.py` | `parents[3]` | `yu-zhi-ran/` |
| `initial_data.py` | `parents[3]` | `yu-zhi-ran/` |
| `generator.py` | `parents[4]` | `yu-zhi-ran/` |
| `optimizer.py` | `parents[4]` | `yu-zhi-ran/` |
| `sync.py` | `parents[4]` | `yu-zhi-ran/` |
| `api/system.py` | `parents[4]` | `yu-zhi-ran/` |
| `api/publisher.py` | `parents[4]` | `yu-zhi-ran/` |
**规律**
-`app/` 一级:`parents[3]``yu-zhi-ran/`
-`app/api/``app/core/``parents[4]``yu-zhi-ran/`
✅ 所有路径已统一修正,可正常工作。
---
## 🚀 工作流程
### 自动流水线(cron
```
每天 05:00 → collector.py → automation/data/sustainability_topics.json(新增选题)
每天 05:30 → creator.py → automation/data/drafts/ + HTML 发布包
每天 05:45 → compliance_optimizer.py → 自动合规检查
每天 06:00 → publisher.py → content/published/(手动发布包)
```
### Web 平台手动控制
```
前端界面
├─▶ POST /api/system/generate/run
│ ↓
│ core/generator.py → subprocess(scripts/creator.py)
│ ↓
│ sync_all_topics() → 更新数据库
├─▶ POST /api/system/optimize/run
│ ↓
│ core/optimizer.py → subprocess(scripts/compliance_optimizer.py)
│ ↓
│ 读取 optimization_report.json → 更新数据库
└─▶ POST /api/publisher/generate/{topic_id}
api/publisher.py → subprocess(scripts/publisher.py --topic-id X)
复制 HTML 到 content/published/X/手动发布/
```
---
## 📁 数据源唯一性
**核心数据文件**`automation/data/sustainability_topics.json`
这是系统中**唯一**的选题状态源:
- `collector.py` 写入新选题(status: "待处理"
- `creator.py` 更新为"待审查" → "draft"
- `compliance_optimizer.py` 更新为"待发布"
- `publisher.py` 更新为"已发布"
- `platform``sync.py` 同步此 JSON 到 SQLite 供前端快速查询
**不要手动编辑数据库**!应通过脚本或 API 修改 JSON。
---
## 🎯 部署检查清单
- [x] Python 依赖安装 (`platform/backend/requirements.txt`)
- [x] 虚拟环境创建(可选)
- [x] 数据目录存在 (`automation/data/`)
- [x] 日志目录存在 (`automation/logs/`)
- [x] 选题 JSON 存在 (`automation/data/sustainability_topics.json`)
- [x] 前端文件就绪 (`platform/frontend/index.html`)
- [x] 软链接 `automation/scripts → ../scripts/` 正常
- [x] 端口 8000 可用
- [ ] cron 定时任务已配置(如需自动运行)
---
## 🔧 常见命令
```bash
# 1. 启动管理平台
cd yu-zhi-ran
./start-platform.sh 8000
# 2. 手动运行完整流水线(测试)
python test_full_pipeline.py --topic-id A05
# 3. 查看日志
tail -f automation/logs/creator_$(date +%Y-%m-%d).log
tail -f automation/logs/optimizer_$(date +%Y-%m-%d).log
tail -f automation/logs/publisher_summary_$(date +%Y-%m-%d).json
# 4. 查看选题库状态
python -c "import json; d=json.load(open('automation/data/sustainability_topics.json')); print(f'总:{len(d)}, 待处理:{sum(1 for t in d if t.get(\"status\")==\"待处理\")}, 待发布:{sum(1 for t in d if t.get(\"status\")==\"待发布\")}')"
# 5. 触发单步任务
python scripts/collector.py
python scripts/creator.py --topic-id A01
python scripts/publisher.py --topic-id A01
# 6. API 测试
curl http://localhost:8000/api/system/status | python3 -m json.tool
```
---
## 📝 说明
### 为何保留软链接?
`automation/scripts/` 通过软链接指向 `../scripts/`,原因:
- 历史兼容:部分脚本内部使用了硬编码路径 `PROJECT_ROOT/scripts/`
- 统一管理:所有脚本集中在一个目录,便于维护
- 避免复制:减少磁盘占用和同步问题
### platform 与 automation 的关系
- **platform**: Web 管理界面 + API 服务
- **automation**: 数据存储 + 日志归档 + 软链接脚本
- **scripts**: 实际执行逻辑(CLI 工具)
platform 通过 `subprocess` 调用 `scripts/` 中的脚本,输出写入 `automation/data/``automation/logs/`
---
**更新日期**: 2026-04-19
**维护**: AI 助手小然
-141
View File
@@ -1,141 +0,0 @@
# 小红书自动化推广系统(yzr-yxl会话隔离版)
## 📋 项目概览
为手机配件京东店铺设计的**小红书自动化内容发布系统**,当前会话完全隔离,不影响其他agent。
## 🗂️ 目录结构
```
/root/.openclaw/workspaces/yzr-yxl/
├── sessions/promoter/ # 🔒 会话隔离存储
│ ├── cookies/ # 登录状态
│ ├── screenshots/ # 操作截图
│ └── logs/ # 运行日志
├── scripts/ # 自动化脚本
│ ├── xhs-promoter.sh # 基础脚本
│ └── xhs-publish-advanced.sh # 高级脚本
├── content/ # 内容库
│ ├── posts/ # 笔记内容
│ └── images/ # 图片素材
├── cron/ # 定时任务配置
│ └── promoter-schedule.json
└── docs/ # 文档
└── xhs-setup-guide.md
```
## ✅ 已完成
1. **环境搭建**
- [x] 创建会话隔离目录结构
- [x] 验证 agent-browser 可用(v0.23.0
- [x] 验证 Xvfb 支持
- [x] 分析小红书登录页结构
2. **脚本开发**
- [x] 基础登录/检查/发布脚本
- [x] 高级发布脚本(批量、定时、状态监控)
- [x] 示例内容模板(手机配件场景)
3. **配置文档**
- [x] 设置指南(Cookie导入)
- [x] 定时任务配置
## ⏳ 待完成
1. **登录授权**(需要你的操作)
- [ ] 获取小红书Cookie
- [ ] 上传到隔离目录
- [ ] 验证登录状态
2. **内容准备**
- [ ] 准备产品图片
- [ ] 创建真实内容文件
- [ ] 测试单篇发布
3. **自动化测试**
- [ ] 测试定时发布
- [ ] 监控日志输出
- [ ] 调整风控参数
## 🚀 快速开始
### 1. 查看当前状态
```bash
/root/.openclaw/workspaces/yzr-yxl/scripts/xhs-publish-advanced.sh status
```
### 2. 查看帮助
```bash
/root/.openclaw/workspaces/yzr-yxl/scripts/xhs-publish-advanced.sh help
```
### 3. 创建示例内容
```bash
/root/.openclaw/workspaces/yzr-yxl/scripts/xhs-publish-advanced.sh sample
```
### 4. 查看示例内容
```bash
cat /root/.openclaw/workspaces/yzr-yxl/content/posts/sample-post.json
```
## 🔐 登录步骤(关键)
由于服务器无图形界面,需要**Cookie导入**方式登录:
### 方式一:浏览器扩展导出(推荐)
1. 在本地电脑登录 [creator.xiaohongshu.com](https://creator.xiaohongshu.com)
2. 安装 [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/) 扩展
3. 导出为 JSON 格式
4. 保存到服务器的隔离路径:
```
/root/.openclaw/workspaces/yzr-yxl/sessions/promoter/cookies/xhs-session.json
```
### 方式二:手动复制Cookie
1. F12打开开发者工具 → Application → Cookies
2. 复制关键字段:`session_id`, `web_id`, `gid`, `a1` 等
3. 创建JSON文件:
```json
[
{"name": "session_id", "value": "your_value", "domain": ".xiaohongshu.com"},
{"name": "web_id", "value": "your_value", "domain": ".xiaohongshu.com"}
]
```
## 📊 自动化能力
| 功能 | 状态 | 说明 |
|------|------|------|
| 登录状态保持 | ✅ | Cookie隔离存储 |
| 单篇发布 | 🧪 | 框架完成,需测试 |
| 批量发布 | 🧪 | 支持JSON配置 |
| 定时任务 | 🧪 | Cron配置就绪 |
| 图片上传 | ⚠️ | 需要进一步开发 |
| 评论互动 | ❌ | 需额外开发,风险高 |
## ⚠️ 风控建议
1. **发布频率**: 每天1-2条,间隔至少6小时
2. **内容去重**: 同一产品换不同角度拍摄
3. **账号权重**: 建议养号2周后再开自动化
4. **备用策略**: 准备2-3个小号轮换
## 📝 下一步行动
需要你来完成:
1. [ ] **提供Cookie** - 按上述方式获取并上传
2. [ ] **准备素材** - 产品图片(建议每个产品5-10张场景图)
3. [ ] **测试发布** - 验证自动化流程
4. [ ] **启用定时** - 配置正式发布计划
或者我可以帮你:
- 创建更详细的内容模板
- 配置知乎/其他平台
- 开发评论互动功能(谨慎)
你想先完成哪一步?
+114 -150
View File
@@ -1,156 +1,120 @@
# 宇之然内容创作平台
一个轻量级的管理平台,用于监控和操作内容生产流水线。
> 覆盖"选题→创作→审核→发布→数据追踪"全链路的内容运营工具
## 快速开始
### 1. 环境准备
```bash
cd /root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/platform
# 创建虚拟环境(推荐)
backend/venv/bin/python -m venv backend/venv # 若不存在
source backend/venv/bin/activate
# 安装依赖
pip install -r backend/requirements.txt
```
### 2. 启动服务
```bash
# 方式一:使用启动脚本(推荐)
./run.sh 8000
# 方式二:手动启动
cd backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
### 3. 访问
- **界面**: http://localhost:8000/
- **API文档**: http://localhost:8000/docs
- **系统状态**: http://localhost:8000/api/system/status
## 核心功能
| 功能 | 描述 |
|------|------|
| 📊 仪表盘 | 选题总数、待发布数、今日生成 |
| 🔄 流水线控制 | 触发创作、合规优化、状态监控 |
| 📝 选题管理 | 列表、筛选、预览、发布 |
| 📦 发布包管理 | 生成多平台HTML发布包、复制 |
| 📋 日志查看 | creator/optimizer/collector 日志 |
## 与自动化流水线的集成
本平台位于 `platform/`,不修改原有 `automation/scripts/` 脚本。
**集成方式**
- `backend/app/core/generator.py` 调用 `scripts/creator.py`
- `backend/app/core/optimizer.py` 调用 `scripts/compliance_optimizer.py`
- `backend/app/api/publisher.py` 调用 `scripts/publisher.py`
- 数据存储:`automation/data/` 为唯一数据源
- 同步:`sync.py` 将 JSON 选题同步到 SQLite 数据库供前端查询
**目录结构关系**
```
yu-zhi-ran/
├── platform/ # 管理平台(本目录)
│ ├── backend/app/
│ │ ├── core/generator.py → 调用 ../scripts/creator.py
│ │ ├── core/optimizer.py → 调用 ../scripts/compliance_optimizer.py
│ │ └── api/publisher.py → 调用 ../scripts/publisher.py
│ └── frontend/index.html # UI
├── automation/
│ ├── data/ ← 数据源(JSON)
│ │ ├── sustainability_topics.json
│ │ ├── drafts/
│ │ └── releases/
│ └── logs/ ← 日志(creator, optimizer, publisher
└── scripts/
├── creator.py ← 被调用
├── compliance_optimizer.py
└── publisher.py
```
## 配置
环境变量(可选):
| 变量 | 说明 | 默认 |
|------|------|------|
| `PROJECT_ROOT` | 项目根目录 | 自动推导 |
| `DATA_DIR` | 平台数据库目录 | `platform/data` |
| `LOG_LEVEL` | 日志级别 | `INFO` |
| `ALLOWED_ORIGINS` | CORS允许的源 | `*` |
示例:
```bash
export PROJECT_ROOT=/path/to/yu-zhi-ran
./run.sh 8000
```
## 开发调试
```bash
# 检查依赖和环境
python check.py
# 查看 API 日志
tail -f automation/logs/creator_$(date +%Y-%m-%d).log
# 数据库初始化(首次)
cd backend
python -c "from app.database import init_db; init_db()"
# 手动同步数据(测试)
curl -X POST http://localhost:8000/api/system/sync/run
```
## 故障排查
| 症状 | 检查点 |
|------|--------|
| 前端显示无选题 | 1. `automation/data/sustainability_topics.json` 是否存在且包含 `status: \"待处理\"` 的数据<br>2. 点击"全量刷新"按钮 |
| 创作任务失败 | 1. 查看 `automation/logs/creator_*.log`<br>2. 确认 `scripts/creator.py` 可执行 |
| 发布包为空 | 1. 确认 `automation/data/releases/YYYY-MM-DD/` 下有对应平台的 HTML<br>2. 点击发布Tab → "重新生成" |
| 端口8000占用 | 停止其他 uvicorn 进程或 `./run.sh 8080` 改用其他端口 |
## 部署清单
- [x] FastAPI 后端(无 Docker,直接运行)
- [x] Vue 3 前端(CDN依赖,无需构建)
- [x] SQLite 数据库(`platform/data/yzr.db`
- [x] 自动化流水线集成(subprocess 调用)
- [x] 数据同步模块(JSON ↔ SQLite
- [x] 流水线状态监控面板
- [x] 日志查看功能
- [ ] 反向代理(Nginx)配置(如需外网访问)
- [ ] HTTPSLet's Encrypt
- [ ] 系统服务(systemd
如需外网访问,建议配置 Nginx 反向代理和 HTTPS。
## 技术栈
- **后端**: FastAPI 0.115.0 + Uvicorn
- **前端**: Vue 3 + Element Plus
- **数据库**: SQLite 3
- **脚本**: Python 3 (subprocess)
- **样式**: Tailwind CSS (CDN)
## 文档
- 详细架构设计: `PORTFOLIO.md`
- 使用说明: 本文档
- API 文档: http://localhost:8000/docs (运行后)
**最后更新: 2026-05-12**
---
**版本**: 0.1.0
**更新**: 2026-04-19
**维护**: 宇之然 AI 助手
> **项目进度 → 详见 [PROGRESS.md](PROGRESS.md)**(本文件为唯一进度真理源)
---
## 技术栈
| 层次 | 技术 |
|------|------|
| 后端 | FastAPI 0.104+ |
| ORM | SQLAlchemy 2.0+ |
| 数据库 | PostgreSQL 16 |
| 前端 | Vue 3 (CDN) + Element Plus |
| 认证 | JWT (python-jose) + bcrypt |
---
## 数据库配置
详见 `platform/backend/.env`
---
## 启动方式
```bash
cd /root/openclaw-workspace/projects/yu-zhi-ran
./start-platform.sh 8001
```
默认管理员: **admin** / **admin123**
---
## 页面入口
| 页面 | 地址 |
|------|------|
| 仪表盘 | http://localhost:8001/ |
| 选题管理 | http://localhost:8001/topics.html |
| 内容日历 | http://localhost:8001/calendar.html |
| 数据分析 | http://localhost:8001/metrics.html |
| 素材库 | http://localhost:8001/assets.html |
| 创作任务 | http://localhost:8001/tasks.html |
| 平台配置 | http://localhost:8001/platforms.html |
| 系统管理 | http://localhost:8001/admin.html |
| API 文档 | http://localhost:8001/docs |
---
## API 模块
| 模块 | 文件 | 功能 |
|------|------|------|
| 认证 | auth.py | 登录/注册/JWT |
| 选题 | topics.py | CRUD、评分、批量操作 |
| 选题配置 | topic_config.py | 领域/评分字段/状态配置 |
| 文章 | articles.py | 文章管理 |
| 发布 | publishing.py | 发布记录 |
| 内容日历 | calendar.py | 排期、绑定选题 |
| 数据分析 | metrics.py | 仪表盘、趋势、推荐 |
| 素材库 | assets.py | 上传、标签、搜索 |
| 创作任务 | tasks.py | 异步任务、进度管理 |
| 平台配置 | platform_config.py | 知乎/微信/小红书 |
| 系统管理 | admin.py | 用户/案例/日志/配置 |
---
## 数据模型
- **TopicField** - 领域分类配置
- **TopicConfigField** - 选题评分字段
- **TopicStatusConfig** - 选题状态配置
- **Topic** - 选题主表
- **ContentCalendar** - 内容日历
- **ContentMetrics** - 数据追踪
- **MediaAsset** - 素材库
- **PlatformConfig** - 平台配置
- **ContentTask** - 创作任务
- **User** - 用户
- **Case** - 案例库
---
## 初始化数据
- 领域: 6 个(未来工作方式、AI与效率、可持续生活、数字游民、个人成长、科技人文)
- 状态: 5 种(待处理、待审查、草稿、待发布、已发布)
- 平台: 3 个(知乎、微信公众号、小红书)
- 管理员: admin/admin123
---
## 项目结构
```
yu-zhi-ran/
├── platform/
│ ├── backend/
│ │ ├── app/
│ │ │ ├── api/ # REST API
│ │ │ ├── core/ # 业务逻辑
│ │ │ ├── models.py # SQLAlchemy 模型
│ │ │ ├── schemas.py # Pydantic 验证
│ │ │ ├── database.py # 数据库连接
│ │ │ ├── main.py # FastAPI 入口
│ │ │ └── initial_data.py
│ │ └── venv/
│ └── frontend/ # Vue + Element Plus SPA
├── automation/ # 自动化脚本
├── content/ # 内容数据
└── scripts/ # 工具脚本
```
-262
View File
@@ -1,262 +0,0 @@
# 宇之然内容管理平台
> 自动化内容生产 + Web 管理界面
一个可持续、可管理的内容创业系统,实现从选题采集到多平台发布的全流程自动化。
---
## 🎯 核心能力
-**全自动流水线**:采集 → 创作 → 合规 → 发布准备(每日凌晨运行)
-**Web 管理界面**:一键控制、实时监控、可视化发布包管理
-**降级策略**:外部源失效时自动切换本地案例库,保证稳定产出
-**多平台支持**:知乎、小红书、微信公众号(HTML 发布包)
-**风控设计**:半自动发布(生成 HTML + 人工复制),避免平台封号
---
## 📦 项目结构(简洁版)
```
yu-zhi-ran/
├── platform/ # Web 管理平台(FastAPI + Vue 3
│ ├── backend/ # API 服务
│ └── frontend/ # 管理界面
├── automation/ # 自动化数据 + 日志
│ ├── data/ # JSON 选题库、草稿、发布包
│ ├── logs/ # 运行日志
│ └── scripts/ → ../scripts/ # 软链到 scripts/
├── scripts/ # CLI 脚本(collector, creator, optimizer...
├── content/ # 已发布内容存储
├── config/ # 配置文件(RSS源、机器人)
├── start-platform.sh # 快速启动管理平台
└── README_NEW.md # 本文档
```
---
## 🚀 快速开始
### 1️⃣ 启动管理平台(推荐)
```bash
cd /root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran
./start-platform.sh 8001
```
访问:
- 界面:http://localhost:8001/
- API 文档:http://localhost:8001/docs
### 2️⃣ 测试自动化流水线
```bash
cd /root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran
python test_full_pipeline.py --topic-id A01
```
预期输出:
- 生成 `automation/data/drafts/2026-04-19/A01_*.md`
- 生成 `automation/data/releases/2026-04-19/*/A01_*.html`
- 合规自动通过
### 3️⃣ 查看当前状态
```bash
# 选题库统计
python -c "import json; d=json.load(open('automation/data/sustainability_topics.json')); print(f'总{len(d)} 待处理:{sum(1 for t in d if t.get(\"status\")==\"待处理\")} 待发布:{sum(1 for t in d if t.get(\"status\")==\"待发布\")}')"
# 日志(今日)
tail -f automation/logs/creator_$(date +%Y-%m-%d).log
```
---
## 🔄 系统架构
```
定时任务 (cron)
├─▶ 05:00 collector.py → automation/data/sustainability_topics.json(选题)
├─▶ 05:30 creator.py → drafts/ + releases/HTML
├─▶ 05:45 optimizer.py → 合规报告
└─▶ 06:00 publisher.py → content/published/(发布包)
Web 平台(可选控制)
├─▶ 一键触发创作/优化
├─▶ 查看选题列表和详情
├─▶ 生成发布包、预览HTML、复制
└─▶ 查看实时流水线状态
```
---
## 📊 当前运行状态(2026-04-19
| 指标 | 数值 |
|------|------|
| 总选题数 | 22 |
| 待处理 | 20 |
| 待发布 | 2 (A01, A02) |
| 自动运行 | ✅ 正常(05:00-06:00 |
| 最新运行 | A0205:30 完成,8秒) |
| 合规通过率 | 100%3/3 |
待发布内容已就绪,可手动发布到知乎/小红书。
---
## 🎮 主要操作指南
### 触发创作任务
**Web 界面**:平台 → "运行创作任务" 按钮
**或命令行**
```bash
python scripts/creator.py --topic-id A03
```
### 生成发布包
**Web 界面**:选题 → 发布 → "重新生成"
**或命令行**
```bash
python scripts/publisher.py --topic-id A01
```
发布包位置:
```
content/published/A01/手动发布/
├── 知乎/文章.html
└── 小红书/文章.html
```
### 标记为已发布
在 Web 界面"发布管理"中填写各平台链接 → "确认发布"
这会更新选题状态为"已发布"并保存链接。
---
## ⚙️ 配置
### 数据源(automation/config/sources.yaml
```yaml
sources:
- name: "新华网-环保频道"
type: "rss"
url: "http://..."
- name: "本地案例库"
type: "local_markdown"
path: "automation/data/cases.md"
```
### 环境变量(platform/.env,可选)
```bash
PROJECT_ROOT=/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran
LOG_LEVEL=INFO
```
---
## 📝 开发调试
```bash
# 1. 检查环境
cd platform
python check.py
# 2. 启动服务(开发模式)
./run.sh 8000
# 3. 查看 API 日志(新终端)
tail -f automation/logs/creator_$(date +%Y-%m-%d).log
# 4. 重置数据库(如需)
rm -f data/yzr.db
cd platform/backend
python -c "from app.database import init_db; init_db(); from app.initial_data import import_topics_from_json; import_topics_from_json()"
```
---
## 🚧 注意事项
### 路径规范
所有 `platform/backend/app/**/*.py` 必须使用相对路径计算 `PROJECT_ROOT`
- `app/database.py`, `initial_data.py`: `Path(__file__).resolve().parents[3]`
- `app/api/*.py`, `app/core/*.py`: `Path(__file__).resolve().parents[4]`
禁止使用硬编码绝对路径!
### 软链接
```
platform/data → ../data
platform/logs → ../logs
automation/scripts → ../scripts/
```
这些链接确保平台和自动化模块共享数据。
### 数据流向
**唯一真理源**`automation/data/sustainability_topics.json`
- 所有状态更新(待处理 → 待发布 → 已发布)都应写入此 JSON
- `platform` 通过 `sync.py` 同步到 SQLite 供前端快速查询
- 不要直接修改数据库而不更新 JSON!
---
## 📚 详细文档
- `YUZHIRAN_PLATFORM.md` - 完整项目文档(架构、API、扩展)
- `platform/PORTFOLIO.md` - 平台架构详解
- `platform/README.md` - 平台使用说明
- `PROJECT_STRUCTURE.md` - 目录结构说明
---
## 🐛 故障排查
| 问题 | 检查点 | 命令 |
|------|--------|------|
| Web 界面无数据 | JSON 是否同步? | `curl http://localhost:8000/api/system/status` |
| 创作失败 | creator 日志 | `tail -n 50 automation/logs/creator_*.log` |
| 发布包缺失 | releases 目录 | `ls automation/data/releases/$(date +%Y-%m-%d)/` |
| 端口占用 | 8000 被谁用? | `lsof -i:8000` |
---
## 🎉 现状总结
昨天(4月18日)完成:
- 数据源切换(新华网+人民网)
- 降级策略(本地案例库保底)
- 锁机制(2小时超时)
- publisher 发布包系统
- 前端管理界面
今天(4月19日):
- 统一项目结构,整合 platform + automation
- 修正所有路径计算
- 添加软链接统一数据目录
- 创建启动脚本和文档
**状态**:✅ 系统稳定运行,每日自动产出内容,Web 平台就绪。
---
**维护**: AI 助手小然
**更新**: 2026-04-19
-285
View File
@@ -1,285 +0,0 @@
# 宇之然内容管理平台 - 完整项目文档
## 📦 项目概览
宇之然内容管理平台是一个集**自动化流水线**与**Web管理界面**于一体的内容生产系统。
- **自动化模块**: 采集→创作→优化→发布的全自动脚本
- **管理平台**: Web UI 监控、控制、发布包管理
- **数据源**: 单一 JSON 文件 + 本地案例库
- **目标**: 每日自动生成高质量原创内容,多平台分发
## 🏗️ 目录结构
```
yu-zhi-ran/
├── platform/ # 管理平台(FastAPI + Vue 3
│ ├── backend/ # FastAPI 后端
│ │ └── app/
│ │ ├── api/ # REST API
│ │ │ ├── system.py # 系统状态、流水线控制
│ │ │ ├── topics.py # 选题管理
│ │ │ ├── publisher.py # 发布包管理
│ │ │ └── articles.py
│ │ └── core/ # 业务逻辑封装
│ │ ├── generator.py → 调用 scripts/creator.py
│ │ ├── optimizer.py → 调用 scripts/compliance_optimizer.py
│ │ └── sync.py → 同步 automation/data/*.json
│ └── frontend/ # Vue 3 前端(CDN依赖)
│ └── index.html
├── automation/ # 自动化流水线(脚本+数据+日志)
│ ├── scripts/ (软链) → ../scripts/
│ ├── data/ # JSON 数据源
│ │ ├── sustainability_topics.json # 选题库
│ │ ├── drafts/ # 草稿目录
│ │ └── releases/ # 发布包(按日期)
│ └── logs/ # 运行日志
│ ├── collector_*.log
│ ├── creator_*.log
│ ├── optimizer_*.log
│ └── publisher_*.log
├── scripts/ # 底层CLI脚本(被automation/scripts/引用)
│ ├── collector.py # 采集热点
│ ├── creator.py # 内容创作
│ ├── compliance_optimizer.py # 合规优化
│ ├── publisher.py # 发布包生成
│ ├── research.py # 研究资料
│ ├── outline.py # 大纲生成
│ └── writer.py # 撰写主力
├── content/ # 已发布内容存储
│ ├── published/ # 按 topic_id 组织
│ ├── drafts/ # 草稿
│ ├── ideas/ # 选题记录
│ └── images/ # 配图资源
├── config/ # 配置文件
│ ├── sources.yaml # RSS/网页源配置
│ └── wecom_config.yaml # 企业微信通知
├── brand/ # 品牌资产
│ ├── brand-book.md
│ └── guidelines.md
├── research/ # 行业研究
│ └── trends-2026.md
├── strategy/ # 内容策略
│ └── 全球-本土比较研究与全新内容战略规划-2026-04-15.md
├── tasks/ # 项目管理
│ └── todo.md
├── logs/ # 根目录日志(兼容)
├── backup/ # 备份归档
├── README.md # 项目总览
├── TEST_FULL_PIPELINE.md # 测试指南
└── run_publisher.sh # 快速启动发布任务
# 软链接(保持路径兼容)
automation/scripts → ../scripts/
```
---
## 🚀 快速开始
### 方式一:直接运行自动化流水线(CLI)
```bash
cd /root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran
# 手动执行各步骤
python scripts/collector.py # 采集选题
python scripts/creator.py # 创作内容
python scripts/compliance_optimizer.py # 合规优化
python scripts/publisher.py # 生成发布包
# 或使用测试脚本
python test_full_pipeline.py --topic-id A01
```
### 方式二:启动 Web 管理平台
```bash
cd platform
./run.sh 8001
# 访问
# - 界面: http://localhost:8001/
# - API文档: http://localhost:8001/docs
```
**功能**
- 📊 仪表盘:选题统计、今日生成
- 🔄 控制台:一键触发 collector/creator/optimizer
- 📝 选题管理:列表、预览、状态
- 📦 发布管理:生成发布包、查看HTML、复制
- 📋 日志查看:所有模块实时日志
### 方式三:定时自动运行(cron)
```bash
# 编辑 crontab
crontab -e
# 添加以下任务(参考)
0 5 * * * cd /path/to/yu-zhi-ran && python automation/scripts/collector.py
30 5 * * * cd /path/to/yu-zhi-ran && python automation/scripts/creator.py
45 5 * * * cd /path/to/yu-zhi-ran && python automation/scripts/compliance_optimizer.py
0 6 * * * cd /path/to/yu-zhi-ran && python automation/scripts/publisher.py
```
---
## 🔄 系统架构
### 数据流
```
┌─────────────────┐
│ 采集/创作/优化 │ ←─ automation/scripts/*.py
│ (CLI 脚本) │
└────────┬────────┘
│ 读写 JSON
┌─────────────────┐
│ automation/data │
│ sustainability_ │
│ topics.json │
└────────┬────────┘
│ 同步
┌─────────────────┐
│ platform/ │
│ FastAPI + │
│ SQLite DB │
└────────┬────────┘
│ HTTP API
┌─────────────────┐
│ Web 前端 │
│ (Vue 3) │
└─────────────────┘
```
### 触发流程
| 操作 | API端点 | 执行脚本 | 结果 |
|------|---------|----------|------|
| 点击"运行创作任务" | POST `/api/system/generate/run` | `scripts/creator.py` | 生成草稿 → 状态: 待审查 |
| 点击"运行合规优化" | POST `/api/system/optimize/run` | `scripts/compliance_optimizer.py` | 全自动通过 → 状态: 待发布 |
| 点击"生成发布包" | POST `/api/publisher/generate/{id}` | `scripts/publisher.py` | HTML包 → content/published/ |
| 页面刷新 | GET `/api/system/status` | - | 显示最新状态 |
---
## 📊 当前状态(2026-04-19
- **选题库**: 22 个(20 待处理 + 2 待发布)
- **自动运行**: ✅ 每日 05:00-06:00 全流程
- **上次运行**: 2026-04-19 05:30A02 选题,8秒完成)
- **合规通过率**: 100%(小样本)
- **发布包**: A01、A02 已就绪(知乎+小红书)
查看详细日志:
```bash
tail -f automation/logs/creator_$(date +%Y-%m-%d).log
tail -f automation/logs/publisher_$(date +%Y-%m-%d).log
```
---
## 🔧 开发调试
### 检查环境
```bash
cd platform
python check.py
```
### 手动测试流水线
```bash
python test_full_pipeline.py --topic-id A05
```
### API 测试
```bash
# 系统状态
curl http://localhost:8000/api/system/status
# 触发创作
curl -X POST http://localhost:8000/api/system/generate/run
# 流水线状态
curl http://localhost:8000/api/system/pipeline/status
```
### 数据库重置
```bash
cd platform/backend
rm -f data/yzr.db
python -c "from app.database import init_db; init_db()"
```
---
## 📝 配置说明
### automation/config/sources.yaml
配置外部信息源(RSS/网页):
```yaml
sources:
- name: "新华网-环保频道"
type: "rss"
url: "http://www.news.cn/..."
focus: "可持续性"
- name: "人民网-生态环境"
type: "rss"
url: "http://env.people.com.cn/..."
focus: "环保政策"
```
### platform/.env (可选)
```bash
PROJECT_ROOT=/path/to/yu-zhi-ran
DATA_DIR=/path/to/data
LOG_LEVEL=INFO
```
---
## 🎯 核心特性
| 特性 | 实现 |
|------|------|
| **降级策略** | 外部源失败 → 本地数据库 → Markdown案例库 |
| **锁机制** | 2小时超时,防止并发冲突 |
| **状态同步** | JSON ↔ SQLite 自动同步 |
| **发布准备** | 生成多平台HTML + 发布指南,人工发布(风控) |
| **Web管理** | Vue 3 单页应用,无需构建 |
| **日志聚合** | 所有模块日志按日期归档 |
---
## 🚧 待办事项
- [ ] 添加用户认证(目前仅本地访问)
- [ ] 实现异步任务状态轮询(创作/优化耗时较长)
- [ ] Nginx 反向代理配置(外网访问)
- [ ] 监控告警(失败通知)
- [ ] 多账号/IP策略(小红书多号分散风险)
- [ ] 数据导出功能(选题库、发布记录)
---
## 📚 相关文档
- `platform/PORTFOLIO.md` - 平台架构详解
- `platform/README.md` - 管理平台使用说明
- `PROJECT_PLAN_V2.md` - 盈利模式与5平台矩阵
- `HERMES_AND_YUZHRAN_STATUS.md` - 多Agent共享状态
- `2026-04-18-YUZHRAN-FIXES-SUMMARY.md` - 技术修复总结
---
**版本**: 2.0 (2026-04-19)
**维护**: 宇之然 AI 助手
**最后更新**: 2026-04-19
@@ -0,0 +1,331 @@
# 宇之然工作台改造方案 — 极简实用 AI Agent 时代设计
> 调研日期: 2026-06-16
> 基于 2026 年 AI 产品设计趋势 + 现有工作台全面审计
---
## 一、现有工作台问题分析
### 1.1 当前页面结构
| 页面 | 定位 | 问题 |
|------|------|------|
| `/index.html` 仪表盘 | 系统概览 + 模块状态 + 近期计划 | **系统视角**,不是用户视角。6 张统计卡片 + 6 个定时模块卡片 + 调度状态 → 像运维面板,不像创作工作台 |
| `topics.html` 选题 | 选题表格 CRUD + 批量操作 | 功能完整但无聊。没有 AI 辅助选题推荐,没有智能排序 |
| `metrics.html` 数据 | 统计看板 + 图表 | 功能 OK,但与 GEO/搜索排名/合规数据分离在不同页面 |
| `calendar.html` 日历 | 内容日历 | 独立页面,与选题/数据割裂 |
| `tasks.html` 任务 | 流水线执行记录 | 展示后端调度状态,普通用户看不懂 |
| `admin.html` 系统管理 | 13 个 Tab 的后台 | 混入了业务功能(AI助手、搜索排名、GEO) |
| `assets.html` 素材 | 素材管理 | 基础功能 |
| `landing.html` 官网 | 产品官网 | ✅ 独立、清晰 |
### 1.2 核心问题
1. **仪表盘是「系统监控」不是「创作工作台」**
- 一进来看到的是定时任务状态(热点趋势/采集/创作/审查),不是「我今天要做什么」
- 缺少任务优先级、待办提醒、进度概览
- 对于内容编辑来说,模块卡片(scheduler 状态)毫无意义
2. **导航/信息架构是「功能菜单」不是「创作流程」**
- 六个平级菜单项:仪表盘、选题、数据、日历、素材、任务
- 没有工作流意识 — 用户需要在多个页面跳转来完成一个创作流程
- 缺少「最近工作」「快速开始」等具有上下文感的入口
3. **AI 能力是「附加功能」不是「内嵌体验」**
- AI 助手是右下角浮窗 chatbot,与工作流分离
- AI 选题分析在选题页弹窗里,AI 写文章在发布流程里
- 没有 Agent 工作区 — 没有一个地方能看到 AI 正在做什么、做了什么、需要什么审批
4. **权限与功能混杂**
- 业务功能(搜索排名、GEO 优化、AI 助手)锁在 admin.html 里
- 普通用户使用需要切换到「系统管理」—— 概念上就不对
5. **缺少「Agent 时代」的核心交互模式**
- ❌ 没有 Plan-first(先展示计划,再执行)
- ❌ 没有 AI 操作轨迹/审计
- ❌ 没有任务编排/Agent 工作区
- ❌ 没有智能建议/预测
- ✅ 但已经有一个基础的 AI 助手浮窗底座
---
## 二、2026 AI 产品设计趋势映射
### 从 7 个趋势看我们应该做什么
| # | 趋势 | 对我们意味着什么 |
|---|------|----------------|
| 1 | **UX → AX (Agentic Experience)** | 工作台需要展示 AI 的**行动计划**和**执行轨迹**,而不是单纯的界面操作 |
| 2 | **Plan-first 交互** | 选题→创作→发布 应该有一个**流程预览**步骤,AI 先展示「我将做 A→B→C」,用户确认后再执行 |
| 3 | **Invisible AI (嵌入式 AI)** | AI 不应该是右下角聊天窗,而应该嵌入到选题列表(智能排序)、文章编辑(实时优化建议)、日历(智能排期)|
| 4 | **Delegation with Control** | 用户需要看到一个「代理仪表盘」— AI 正在做什么、已完成什么、需要审批什么 |
| 5 | **Workspace over Chat** | 从「跟 AI 对话」到「AI 作为团队成员在共享空间工作」|
| 6 | **预览→差异→确认 模式** | 所有 AI 生成/修改的内容,先展示 diff/预览,再确认写入 |
| 7 | **审批路径和可逆性** | 每一步操作都有 undo 路径和操作历史 |
---
## 三、改造方案设计
### 3.1 总体信息架构重构
```
当前: 目标:
┌──────────┐ ┌──────────────────┐
│ 仪表盘 │ ← 系统视角 │ 工作台 (首页) │ ← 用户视角
│ 选题 │ │ ├─ 今日待办 │
│ 数据 │ │ ├─ 创作流水线 │
│ 日历 │ │ ├─ 智能建议 │
│ 素材 │ │ └─ 最近工作 │
│ 任务 │ ├──────────────────┤
│ 系统管理 │ │ 内容工厂 │ ← 创作流程
└──────────┘ │ ├─ 选题工作区 │
│ ├─ AI 创作工作区 │
│ ├─ 发布工作区 │
│ └─ 内容日历 │
├──────────────────┤
│ 数据洞察 │ ← 效果分析
│ ├─ 数据看板 │
│ ├─ 搜索排名 │
│ ├─ GEO 就绪度 │
│ └─ 合规报告 │
├──────────────────┤
│ 资产库 │ ← 资源管理
├──────────────────┤
│ 系统管理 (admin) │ ← 仅管理员
```
### 3.2 工作台首页(全新设计)
**目标**:一打开就知道今天要做什么、进度如何、有什么需要关注
```
┌─────────────────────────────────────────────────┐
│ ☀️ 早上好,张三 你的团队本周已发布 3 篇内容 │
├─────────────────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 待处理│ │ 待审查│ │ 待发布│ │ 本月 │ │ 合规 │ │
│ │ 选题 │ │ 文章 │ │ 内容 │ │ 目标 │ │ 健康 │ │
│ │ 12 │ │ 5 │ │ 3 │ │ 67% │ │ 94% │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────────┤
│ 📋 今日待办 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 🔴 审查「远程办公的 5 个陷阱」- 逾期 2 天 │ │
│ │ 🟡 审核「AI 写作工具实测」- AI 已完稿 │ │
│ │ 🔵 确认本周排期 - 3 篇待定稿 │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 🤖 AI Agent 工作区 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 📡 热点采集 ✅ 完成 (12 条新趋势) │ │
│ │ ✍️ 文章创作 ⏳ 进行中 (2/5 篇) │ │
│ │ 🔍 合规审查 ⏳ 排队中 (3 篇待审) │ │
│ │ 📊 数据同步 ⏰ 06:00 自动运行 │ │
│ │ │ │
│ │ [查看 Agent 轨迹 →] │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 💡 AI 智能建议 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 💡 「碳中和」关键词本周搜索量 ↑47% │ │
│ │ 建议创建一篇相关选题 │ │
│ │ [一键创建选题 →] │ │
│ │ 💡 你的知乎上周互动率 ↑12% │ │
│ │ 建议继续保持数据分析深度 │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 📅 近期计划 │
│ ┌──────────┬──────────────────┬──────────┐ │
│ │ 06/17 二 │ 远程办公工具对比 │ 知乎发布 │ │
│ │ 06/18 三 │ AI 写作体验 │ 公众号 │ │
│ │ 06/19 四 │ 零浪费生活指南 │ 小红书 │ │
│ └──────────┴──────────────────┴──────────┘ │
└─────────────────────────────────────────────────┘
```
### 3.3 内容工厂(创作流水线重构)
**目标**:按创作流程组织,而不是按数据表组织
#### 选题工作区
- **AI 智能选题板**:热点趋势 + 关键词热度 + 历史数据 → 推荐选题
- **选题看板**:Kanban 视图(待处理→分析中→已评分→待创作)
- **批量操作**:AI 一键分析 → 自动评分 → 自动排序
- **卡片预览**:题目的核心观点、受众痛点、差异化切入点和评分一览
#### AI 创作工作区 (NEW)
- **创作任务面板**:显示所有进行中的 AI 创作任务
- **Plan-first 流程**:用户选择选题 → AI 展示大纲 → 用户确认 → AI 逐节写作
```
步骤 1: 选择选题 + 平台
步骤 2: AI 生成大纲 (展示预览)
步骤 3: 用户调整大纲 (拖拽排序/编辑)
步骤 4: AI 逐节写作 (进度条展示)
步骤 5: 合规审查 (展示问题列表)
步骤 6: 用户最终编辑
```
- **Agent 执行轨迹**:每次 AI 操作都有时间线记录
- **在线编辑器**:统一的富文本编辑器,支持实时 AI 优化建议
#### 发布工作区
- **发布检查清单**:AI 自动检查合规、字数、配图、格式
- **一键多平台发布**:选题→三平台各自适配→预览→确认发布
- **发布历史**:时间线视图
### 3.4 AI Agent 工作区(新概念页面)
**这是本次改造的核心创新亮点** — 一个展示 AI 作为「团队成员」工作状况的看板。
```
┌─────────────────────────────────────────────────┐
│ 🤖 AI Agent 工作区 │
├─────────────────────────────────────────────────┤
│ 🔴 活跃任务 │
│ ┌─────────────────────────────────────────────┐ │
│ │ ✍️ 正在创作「远程办公的 5 个陷阱」→ 知乎版 │ │
│ │ 进度: ████████░░ 78% │ │
│ │ 当前步骤: 撰写第 3 节「团队协作挑战」 │ │
│ │ [预览] [暂停] [取消] │ │
│ │ │ │
│ │ 🔍 正在审查「AI 写作工具实测」- 小红书版 │ │
│ │ 进度: ████░░░░░░ 35% │ │
│ │ 发现 2 个合规问题 │ │
│ │ [查看详情] │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ ⏳ 待审批 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 📝 大纲待确认 「零浪费生活指南」 │ │
│ │ 已由 AI 自动生成,等待你审核 │ │
│ │ [查看大纲] [确认] [修改] │ │
│ │ │ │
│ │ 📄 文章待审 「碳中和入门」已完稿 │ │
│ │ 合规评分 92/100,2 条建议 │ │
│ │ [审阅] [通过] [退回修改] │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────┤
│ ✅ 今日已完成 │
│ ┌─────────────────────────────────────────────┐ │
│ │ ✅ 采集热点 12 条 → 生成 3 个新选题 │ │
│ │ ✅ 同步数据指标 → 知乎互动率 +12% │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
### 3.5 数据洞察(统一分析中心)
将当前分散的数据功能整合:
| Tab | 功能 | 来源 |
|-----|------|------|
| 概览 | 总指标 + 趋势图 | 现有 metrics.html |
| 搜索排名 | 关键词排名追踪 | 现 admin.html 搜索排名 |
| GEO 就绪度 | AI 搜索优化评分 | 现 admin.html GEO |
| 合规报告 | 发布前合规检查历史 | 现 admin.html + 优化报告 |
| 平台对比 | 知乎/公众号/小红书数据 | 现有 metrics.html |
### 3.6 导航改造
```javascript
// 现有 navItems (硬编码):
[{dashboard, topics, metrics, calendar, assets, tasks}]
// admin 额外: [admin]
// 目标 navItems (按角色动态):
// 编辑:
[{工作台(top), 内容工厂(factory), 数据洞察(insights), 资产库(assets)}]
// 管理员:
内容工厂 + 数据洞察 + 资产库 + 系统管理(admin)
```
导航从「功能清单」变成「流程引导」:
- **工作台** = 首页(今日待办 + Agent 状态 + 智能建议)
- **内容工厂** = 创作全流程(选题→创作→发布→日历)
- **数据洞察** = 所有数据分析入口
- **资产库** = 素材管理
### 3.7 移动端适配
当前移动端只是隐藏表格换卡片,改造后:
- 底部 Tab 导航栏(代替顶部汉堡菜单)
- 工作台首页简化为:今日待办 + Agent 状态 + 快速操作按钮
- 内容工厂:简易 Kanban,手势左右切换阶段
- AI Agent: 推送通知代替实时面板
---
## 四、实施优先级
### P0 — 快速见效 (1-2 天)
这些改动小但体验提升大:
| 改动 | 文件 | 工作量 |
|------|------|--------|
| 仪表盘改为用户视角(统计卡片 = 待办数量而不是系统模块) | `index.html` | 半天 |
| 将 AI 助手从浮窗升级为半屏面板 | `ai-assistant.js` | 半天 |
| 导航改为新架构(工作台/内容工厂/数据洞察/资产库) | `uni-nav.js` | 半天 |
### P1 — 核心重构 (3-5 天)
| 改动 | 涉及 | 工作量 |
|------|------|--------|
| 内容工厂页面(创作流水线) | 新建 `factory.html` | 2 天 |
| AI Agent 工作区 | 新建 `agent.html` | 2 天 |
| 数据洞察整合(合并 GEO/搜索排名) | 改造 `metrics.html` | 1 天 |
### P2 — 增强功能 (5-7 天)
| 改动 | 工作量 |
|------|--------|
| Plan-first 创作流程(大纲预览→确认→执行) | 3 天 |
| Agent 执行轨迹/审计时间线 | 2 天 |
| 智能选题推荐面板 | 2 天 |
| 移动端全面适配 | 2 天 |
### P3 — AI 原生体验 (后续)
| 改动 | 工作量 |
|------|--------|
| 智能排期 / 内容策略建议 | 3 天 |
| 自动化工作流编排(拖拽编排 Agent 任务) | 5 天 |
| 多 Agent 协作(采集 Agent + 写作 Agent + 审查 Agent | 5 天 |
---
## 五、关键设计原则
1. **用户视角 > 系统视角**
- 展示「我今天要做什么」而不是「系统正在运行什么」
- 统计数字 = 行动信号(待办数量)而不是监控指标
2. **流程 > 功能**
- 按创作流程组织页面(选题→创作→发布→分析)
- 不在功能之间跳转,在流程中前进
3. **AI 是团队成员,不是工具**
- 展示 AI 的工作计划、进度、产出
- 用户是「审核者/决策者」而不是「操作者」
- 核心模式:用 AI → AI 执行 Plan → 用户审核/批准
4. **预览→确认→执行**
- AI 生成的所有内容:先预览/展示差异,再确认
- 所有操作可逆,有撤销路径
5. **极简不极简功能**
- 减少平级菜单数量和页面复杂
- 不减少功能 — 用上下文和流程引导代替功能罗列
---
## 六、与当前改造项目的关系
本次工作台改造与正在进行的数据隔离/权限升级是 **互补关系**:
| 改造线 | 范围 | 依赖 |
|--------|------|------|
| **数据隔离+权限** (当前进行中) | 后端架构、数据安全 | 无 |
| **工作台极简改造** (本方案) | 前端 UI/UX、信息架构 | 权限系统就绪后可基于角色定制页面 |
建议:
1. 先完成数据隔离和权限基础 — 这是安全基石
2. 工作台改造可以在权限未完全就绪时就开始前端改造(不依赖后端)
3. 导航的 role-based 显示可以利用现有的 `/api/menus/active` 接口
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env python3
"""
在 Markdown 的 H2 标题前插入分隔线,第一个除外
"""
MD_PATH = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年/final-article.md"
with open(MD_PATH, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
first_h2_seen = False
for line in lines:
if line.startswith("## "):
if first_h2_seen:
new_lines.append("---\n\n")
else:
first_h2_seen = True
new_lines.append(line)
with open(MD_PATH, "w", encoding="utf-8") as f:
f.writelines(new_lines)
print(f"✅ 已处理 {MD_PATH}")
-39
View File
@@ -1,39 +0,0 @@
#!/usr/bin/env python3
"""
在章节标题(h2)前插入分隔线,第一个除外
"""
import re
HTML_PATH = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年/article-optimized.html"
with open(HTML_PATH, "r", encoding="utf-8") as f:
html = f.read()
# 分隔线HTML
separator = '<div class="chapter-separator" style="margin: 40px 0 20px; border-top: 2px dashed #e0e0e0;"></div>\n'
# 找到所有 h2 标题
h2_pattern = re.compile(r'(<h2>.*?</h2>)', re.DOTALL)
matches = list(h2_pattern.finditer(html))
# 跳过第一个 h2,对其余每个插入分隔
insertions = []
for i, m in enumerate(matches[1:], start=1): # 从第二个开始
insert_pos = m.start()
insertions.append((insert_pos, separator))
# 按位置逆序插入,避免影响后续位置
insertions.sort(reverse=True, key=lambda x: x[0])
html_list = list(html)
for pos, sep in insertions:
html_list.insert(pos, sep)
new_html = ''.join(html_list)
# 写回
with open(HTML_PATH, "w", encoding="utf-8") as f:
f.write(new_html)
print(f"✅ 已插入 {len(insertions)} 个章节分隔")
print(f"📄 文件: {HTML_PATH}")
+2 -2
View File
@@ -7,8 +7,8 @@
import os
from PIL import Image
IMAGES_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/publishing/images"
OUTPUT_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/publishing/images_compressed"
IMAGES_DIR = "/root/openclaw-workspace/projects/yu-zhi-ran/content/publishing/images"
OUTPUT_DIR = "/root/openclaw-workspace/projects/yu-zhi-ran/content/publishing/images_compressed"
os.makedirs(OUTPUT_DIR, exist_ok=True)
+338
View File
@@ -0,0 +1,338 @@
[
{
"id": 1,
"title": "GitLab 全远程公司手册",
"field": "未来工作方式",
"summary": "GitLab 是全球最大的全远程公司,拥有1500+员工分布在65+国家,其文档和协作模式被广泛研究。",
"key_metrics": "员工满意度 4.5/5,离职率低于行业平均50%",
"date": "2024",
"source": "GitLab 官方博客",
"source_url": "https://about.gitlab.com/remote-work/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 2,
"title": "Zapier 异步沟通指南",
"field": "未来工作方式",
"summary": "Zapier 通过自动化工作流减少会议,实现全球团队异步协作。",
"key_metrics": "员工每周会议时间 <2h,效率提升30%",
"date": "2023",
"source": "Zapier 博客",
"source_url": "https://zapier.com/blog/asynchronous-communication/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 3,
"title": "爱沙尼亚游民签证",
"field": "未来工作方式",
"summary": "爱沙尼亚推出数字游民签证,允许远程工作者合法居住1年。",
"key_metrics": "申请量年增200%,主要来自IT从业者",
"date": "2024",
"source": "Estonian Government",
"source_url": "https://e-resident.gov.ee/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 4,
"title": "巴厘岛游民社区",
"field": "未来工作方式",
"summary": "巴厘岛成为亚洲游民热点,提供 coworking 空间和社区活动。",
"key_metrics": "每年吸引10万+游民,消费力强",
"date": "2024",
"source": "Bali Tourism Board",
"source_url": "https://www.balitourismboard.org/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 5,
"title": "Fiverr AI服务市场",
"field": "未来工作方式",
"summary": "Fiverr 上 AI 代写、数据分析等服务泛滥,形成灰色产业链。",
"key_metrics": "AI服务订单季度增长150%",
"date": "2025",
"source": "Fiverr 数据报告",
"source_url": "https://www.fiverr.com/",
"credibility_rating": "⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 6,
"title": "Indie Hackers 社区",
"field": "未来工作方式",
"summary": "Indie Hackers 是独立开发者分享产品成长和盈利的地方。",
"key_metrics": "超过5000个真实项目案例,月均收入 $2k+",
"date": "2024",
"source": "Indie Hackers",
"source_url": "https://www.indiehackers.com/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 7,
"title": "One Person Business",
"field": "未来工作方式",
"summary": "一人企业思维强调极简运营,低风险创业模式。",
"key_metrics": "初期投入 <$100020%实现月入$3k",
"date": "2023",
"source": "Pieter Levels 博客",
"source_url": "https://levels.io/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 9,
"title": "纽约社区花园政策",
"field": "可持续生活系统",
"summary": "纽约市通过土地使用权政策支持社区花园建设。",
"key_metrics": "覆盖500+社区,参与者超10万",
"date": "2023",
"source": "NYC Parks",
"source_url": "https://www.nycgovparks.org/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 10,
"title": "瑞典零浪费城市",
"field": "可持续生活系统",
"summary": "瑞典城市通过垃圾分类和堆肥计划,实现垃圾填埋量减少90%。",
"key_metrics": "垃圾回收率 99%,焚烧发电供全市1/3用电",
"date": "2024",
"source": "Swedish Waste Management",
"source_url": "https://www.avfallsverige.se/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐"
},
{
"id": 11,
"title": "日本 Mottainai 文化",
"field": "可持续生活系统",
"summary": "Mottainai(勿浪费)精神贯穿生活,提倡物尽其用。",
"key_metrics": "家庭垃圾量比欧美低40%",
"date": "2024",
"source": "日本环境省",
"source_url": "https://www.env.go.jp/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 12,
"title": "荷兰自行车城市",
"field": "可持续生活系统",
"summary": "荷兰城市拥有完善的自行车道网络,自行车通勤占比 >40%。",
"key_metrics": "人均自行车拥有量 1.3辆,交通事故率低",
"date": "2023",
"source": "Dutch Cycling Embassy",
"source_url": "https://cyclingembassy.org/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐"
},
{
"id": 13,
"title": "挪威电动车普及",
"field": "可持续生活系统",
"summary": "挪威通过免税和充电设施补贴,使电动车占新车销量80%以上。",
"key_metrics": "2024年电动车渗透率 82%",
"date": "2024",
"source": "Norwegian EV Association",
"source_url": "https://elbil.no/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 14,
"title": "法国二手强制法",
"field": "可持续生活系统",
"summary": "法国要求零售商提供二手商品区域,促进循环消费。",
"key_metrics": "二手商品销售额年增25%",
"date": "2023",
"source": "French Government",
"source_url": "https://www.gouvernement.fr/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐"
},
{
"id": 15,
"title": "Patagonia 维修服务",
"field": "可持续生活系统",
"summary": "Patagonia 提供终身维修服务,延长产品寿命。",
"key_metrics": "维修量年增30%,顾客忠诚度极高",
"date": "2024",
"source": "Patagonia Worn Wear",
"source_url": "https://www.patagonia.com/worn-wear/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐"
},
{
"id": 16,
"title": "Obsidian + RAG 个人知识库",
"field": "个人知识工厂",
"summary": "Obsidian 结合检索增强生成,实现私有知识问答。",
"key_metrics": "无需云端,本地向量存储,响应 <1s",
"date": "2024",
"source": "Obsidian Hub",
"source_url": "https://obsidian.md/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 17,
"title": "PARA 系统在 Notion",
"field": "个人知识工厂",
"summary": "Tiago Forte 的 PARA 方法将信息分为项目、领域、资源、存档四类。",
"key_metrics": "用户报告节省每周 3 小时信息整理时间",
"date": "2023",
"source": "Fortelabs.co",
"source_url": "https://fortelabs.co/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 18,
"title": "费曼技巧经典",
"field": "个人知识工厂",
"summary": "通过向他人讲解来检验自己是否真正理解。",
"key_metrics": "学习效率提升 40%",
"date": "2024",
"source": "Feynman Technique",
"source_url": "https://www.feynmantechnique.org/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 19,
"title": "EU AI Act",
"field": "科技人文交叉",
"summary": "欧盟人工智能法案,对 AI 系统进行风险分级监管。",
"key_metrics": "违反最高罚款达全球年营业额6%",
"date": "2024",
"source": "European Commission",
"source_url": "https://digital-strategy.ec.europa.eu/en/policies/european-approach-artificial-intelligence",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐"
},
{
"id": 20,
"title": "硅谷高管禅修热",
"field": "科技人文交叉",
"summary": "许多硅谷领袖将正念冥想作为日常,提高专注力。",
"key_metrics": " Fortune 500 中 35% 提供冥想课程",
"date": "2023",
"source": "Harvard Business Review",
"source_url": "https://hbr.org/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐"
},
{
"id": 21,
"title": "Tauri + Rust 构建跨平台桌面应用性能对比",
"field": "技术工具集",
"summary": "对比 Tauri 2.0 与 Electron 在资源占用、启动速度、包体积等维度的性能数据,展示 Rust 后端优势。",
"key_metrics": "内存占用降低 60%, 安装包体积减少 70%, 首次启动时间 <1.5s",
"date": "2026",
"source": "Tauri 官方博客",
"source_url": "https://tauri.app/blog/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐⭐"
},
{
"id": 22,
"title": "独立开发者如何用 FastAPI + Vue 3 快速构建 SaaS",
"field": "技术工具集",
"summary": "一位独立开发者分享使用 FastAPI 后端 + Vue 3 前端在 2 周内完成多平台内容管理系统的实战经验。",
"key_metrics": "开发周期 14 天, 代码量 8K 行, 部署成本 < $10/月",
"date": "2025",
"source": "Dev.to 社区",
"source_url": "https://dev.to/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 23,
"title": "Notion + Python 自动化内容工作流案例",
"field": "个人知识工厂",
"summary": "如何用 Python 脚本自动同步 Notion 数据库到博客平台,实现从选题到发布的完整自动化。",
"key_metrics": "每周节省 8 小时, 错误率降为 0, 发布延迟 <5 分钟",
"date": "2025",
"source": "Notion 模板社区",
"source_url": "https://www.notion.so/templates",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 24,
"title": "GitHub Copilot 辅助 Rust 开发效率调研",
"field": "技术工具集",
"summary": "在真实 Rust 项目中启用 Copilot 后,代码编写速度提升 35%,且代码质量保持不变。",
"key_metrics": "编码速度 +35%, 代码评审通过率 92%, 学习曲线平缓",
"date": "2026",
"source": "GitHub 官方报告",
"source_url": "https://github.blog/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 25,
"title": "零浪费生活方式:一年减少 80% 塑料使用",
"field": "可持续生活系统",
"summary": "记录一家三口一年内通过改变购物习惯、DIY 日用品、选择可持续品牌,实现塑料消耗大幅减少的过程。",
"key_metrics": "塑料垃圾减少 80%, 开销降低 15%, 生活质量评分 +20%",
"date": "2025",
"source": "B站 UP 主 \"零浪费生活家\"",
"source_url": "https://www.bilibili.com/",
"credibility_rating": "⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐⭐"
},
{
"id": 27,
"title": "数字游民签证地图:2026 最新政策对比",
"field": "未来工作方式",
"summary": "收集 35 个国家/地区的数字游民签证政策,包括申请条件、停留时间、税收优惠等关键信息。",
"key_metrics": "覆盖 35 国, 政策更新频率 季度, 中国护照适用 18 国",
"date": "2026",
"source": "Nomad List",
"source_url": "https://nomadlist.com/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 28,
"title": "AI 副业变现:用 StepFun API 打造的 5 个微型产品",
"field": "未来工作方式",
"summary": "开发者分享如何利用 StepFun 的大模型 API 快速开发并上线 5 个小型付费 AI 工具,实现月入过万。",
"key_metrics": "产品上线时间 1-3 天/个, 月收入 ¥12,000+, 用户满意度 4.8/5",
"date": "2026",
"source": "StepFun 开发者故事",
"source_url": "https://www.stepfun.com/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐⭐"
},
{
"id": 29,
"title": "知识管理新范式:Obsidian + LlamaIndex 实现个人知识问答",
"field": "个人知识工厂",
"summary": "将 Obsidian 笔记库通过 LlamaIndex 索引,打造基于个人知识的智能问答系统,准确率超 90%。",
"key_metrics": "笔记量 5K+, 问答准确率 92%, 本地部署无持续成本",
"date": "2025",
"source": "Obsidian 论坛",
"source_url": "https://forum.obsidian.md/",
"credibility_rating": "⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐"
},
{
"id": 30,
"title": "异步协作革命:用 Lark + Python 实现分布式团队项目管理",
"field": "未来工作方式",
"summary": "跨 5 个时区的 15 人团队,通过 Lark 机器人 + Python 脚本实现自动化任务分配、进度同步、日报生成。",
"key_metrics": "会议减少 60%, 项目延期率下降 70%, 员工满意度 +25%",
"date": "2025",
"source": "字节跳动企业服务案例",
"source_url": "https://www.larkoffice.com/",
"credibility_rating": "⭐⭐⭐⭐⭐",
"china_applicability": "⭐⭐⭐⭐⭐"
}
]
@@ -0,0 +1,14 @@
{
"task": "test",
"time": "2026-04-22 19:00",
"topic_count": 1,
"case_count": 2,
"source_count": 3,
"details_link": "/test",
"topic_title": "测试选题",
"image_count": 5,
"output_dir": "/test",
"status": "测试状态",
"error": "",
"log_path": "/tmp/test.log"
}
@@ -1,40 +0,0 @@
# 文章大纲:AI时代的技能组合:什么技能值得投入10年?
## 一、引言(约200字)
- 开场场景/痛点引入
- 提出核心问题:AI时代的技能组合:什么技能值得投入10年?
- 点明文章价值
## 二、核心观点(约300字)
基于WEF未来技能报告,划分4个技能维度(AI强化型、AI无法替代、复合型、过时型),帮中国职场人识别护城河技能
## 三、受众痛点分析(约300字)
学什么都不放心,怕投入时间后AI又取代
## 四、全球/行业趋势与案例(约500字)
- 引用研究笔记中的 0 个案例,精选 2-3 个详述
- 数据支撑:提取研究笔记中的关键数据
- 趋势分析
## 五、本土落地建议(约400字)
- 结合未来工作方式领域特点
- 提供可执行的步骤
- 注意事项
## 六、独特视角:将全球宏观报告转化为个人技能地图,提供可视化工具(约300字)
## 七、行动指南(MVP,约200字)
1. 理解现状
2. 小范围试验
3. 评估效果
4. 形成习惯
## 八、总结与鼓励(约200字)
- 回顾要点
- 呼吁行动
## 九、参考文献
- 从研究笔记中提取来源链接
---
*大纲生成时间:2026-04-17*
@@ -1,40 +0,0 @@
# 文章大纲:远程工作2026中国指南:从'不可能'到'可行'的路径图
## 一、引言(约200字)
- 开场场景/痛点引入
- 提出核心问题:远程工作2026中国指南:从'不可能'到'可行'的路径图
- 点明文章价值
## 二、核心观点(约300字)
通过法律实操(合同、社保、个税)和心理建设(孤独应对),在中国环境下实现远程工作
## 三、受众痛点分析(约300字)
想远程但不知如何合法操作,担心被边缘化
## 四、全球/行业趋势与案例(约500字)
- 引用研究笔记中的 0 个案例,精选 2-3 个详述
- 数据支撑:提取研究笔记中的关键数据
- 趋势分析
## 五、本土落地建议(约400字)
- 结合未来工作方式领域特点
- 提供可执行的步骤
- 注意事项
## 六、独特视角:对比GitLab/Zapier海外实践,本土化落地策略(约300字)
## 七、行动指南(MVP,约200字)
1. 理解现状
2. 小范围试验
3. 评估效果
4. 形成习惯
## 八、总结与鼓励(约200字)
- 回顾要点
- 呼吁行动
## 九、参考文献
- 从研究笔记中提取来源链接
---
*大纲生成时间:2026-04-18*
@@ -1,40 +0,0 @@
# 文章大纲:第二大脑2.0:用DeepSeek+本地向量库建立私有知识系统
## 一、引言(约200字)
- 开场场景/痛点引入
- 提出核心问题:第二大脑2.0:用DeepSeek+本地向量库建立私有知识系统
- 点明文章价值
## 二、核心观点(约300字)
对比Obsidian+RAG海外实践,针对国内云服务担忧,提供数据主权、隐私保护、无缝检索、AI问答的本地化方案
## 三、受众痛点分析(约300字)
想系统化知识但担心云存储安全,怕复杂
## 四、全球/行业趋势与案例(约500字)
- 引用研究笔记中的 0 个案例,精选 2-3 个详述
- 数据支撑:提取研究笔记中的关键数据
- 趋势分析
## 五、本土落地建议(约400字)
- 结合个人知识工厂领域特点
- 提供可执行的步骤
- 注意事项
## 六、独特视角:强调数据主权,从API调用到本地部署的渐进路线(约300字)
## 七、行动指南(MVP,约200字)
1. 理解现状
2. 小范围试验
3. 评估效果
4. 形成习惯
## 八、总结与鼓励(约200字)
- 回顾要点
- 呼吁行动
## 九、参考文献
- 从研究笔记中提取来源链接
---
*大纲生成时间:2026-04-18*
@@ -1,40 +0,0 @@
# 文章大纲:AI副业入门:用DeepSeek实现第一笔收入的100天
## 一、引言(约200字)
- 开场场景/痛点引入
- 提出核心问题:AI副业入门:用DeepSeek实现第一笔收入的100天
- 点明文章价值
## 二、核心观点(约300字)
从代写文案/数据分析起步,通过Fiverr国内外平台对比,制定定价策略和违规红线规避
## 三、受众痛点分析(约300字)
想用AI赚钱但不知从何开始,怕踩坑
## 四、全球/行业趋势与案例(约500字)
- 引用研究笔记中的 0 个案例,精选 2-3 个详述
- 数据支撑:提取研究笔记中的关键数据
- 趋势分析
## 五、本土落地建议(约400字)
- 结合未来工作方式领域特点
- 提供可执行的步骤
- 注意事项
## 六、独特视角:对比Fiverr海外繁荣 vs 国内空白,提供本土化接单路径(约300字)
## 七、行动指南(MVP,约200字)
1. 理解现状
2. 小范围试验
3. 评估效果
4. 形成习惯
## 八、总结与鼓励(约200字)
- 回顾要点
- 呼吁行动
## 九、参考文献
- 从研究笔记中提取来源链接
---
*大纲生成时间:2026-04-19*
@@ -1,20 +0,0 @@
# 研究笔记:AI时代的技能组合:什么技能值得投入10年?
## 选题信息
- **ID**: A05
- **领域**: 未来工作方式
- **核心观点**: 基于WEF未来技能报告,划分4个技能维度(AI强化型、AI无法替代、复合型、过时型),帮中国职场人识别护城河技能
- **受众痛点**: 学什么都不放心,怕投入时间后AI又取代
- **独特视角**: 将全球宏观报告转化为个人技能地图,提供可视化工具
## 相关案例(0个)
## 研究发现摘要
- 待补充:从案例中提炼的趋势和洞察
- 待补充:数据支撑
## 待深入研究的问题
- [ ] 需要更多本土数据
- [ ] 需要验证某些结论的适用性
*生成时间:2026-04-17*
@@ -1,20 +0,0 @@
# 研究笔记:远程工作2026中国指南:从'不可能'到'可行'的路径图
## 选题信息
- **ID**: A01
- **领域**: 未来工作方式
- **核心观点**: 通过法律实操(合同、社保、个税)和心理建设(孤独应对),在中国环境下实现远程工作
- **受众痛点**: 想远程但不知如何合法操作,担心被边缘化
- **独特视角**: 对比GitLab/Zapier海外实践,本土化落地策略
## 相关案例(0个)
## 研究发现摘要
- 待补充:从案例中提炼的趋势和洞察
- 待补充:数据支撑
## 待深入研究的问题
- [ ] 需要更多本土数据
- [ ] 需要验证某些结论的适用性
*生成时间:2026-04-18*
@@ -1,20 +0,0 @@
# 研究笔记:第二大脑2.0:用DeepSeek+本地向量库建立私有知识系统
## 选题信息
- **ID**: C01
- **领域**: 个人知识工厂
- **核心观点**: 对比Obsidian+RAG海外实践,针对国内云服务担忧,提供数据主权、隐私保护、无缝检索、AI问答的本地化方案
- **受众痛点**: 想系统化知识但担心云存储安全,怕复杂
- **独特视角**: 强调数据主权,从API调用到本地部署的渐进路线
## 相关案例(0个)
## 研究发现摘要
- 待补充:从案例中提炼的趋势和洞察
- 待补充:数据支撑
## 待深入研究的问题
- [ ] 需要更多本土数据
- [ ] 需要验证某些结论的适用性
*生成时间:2026-04-18*
@@ -1,20 +0,0 @@
# 研究笔记:AI副业入门:用DeepSeek实现第一笔收入的100天
## 选题信息
- **ID**: A02
- **领域**: 未来工作方式
- **核心观点**: 从代写文案/数据分析起步,通过Fiverr国内外平台对比,制定定价策略和违规红线规避
- **受众痛点**: 想用AI赚钱但不知从何开始,怕踩坑
- **独特视角**: 对比Fiverr海外繁荣 vs 国内空白,提供本土化接单路径
## 相关案例(0个)
## 研究发现摘要
- 待补充:从案例中提炼的趋势和洞察
- 待补充:数据支撑
## 待深入研究的问题
- [ ] 需要更多本土数据
- [ ] 需要验证某些结论的适用性
*生成时间:2026-04-19*
-104
View File
@@ -1,104 +0,0 @@
[
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
}
]
@@ -1 +0,0 @@
[]
@@ -1 +0,0 @@
[]
@@ -1 +0,0 @@
{"task": "sustainability_collection", "time": "2026-04-17 18:33", "topic_count": 0, "case_count": 0, "source_count": 8, "details_link": "automation/data/sustainability_raw/2026-04-17"}
@@ -1,53 +0,0 @@
[
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
}
]
@@ -1,21 +0,0 @@
[
{
"id": "TOPIC-.F4F684",
"title": "未分类新趋势: 与的中国落地路径",
"cases": [
"LOCAL-UNKNOWN",
"LOCAL-UNKNOWN",
"LOCAL-UNKNOWN"
],
"audience": "城市焦虑青年(26-35岁)",
"china_pain_points": "未分类在中国面临的主要问题",
"localization_solution": "国际案例中国化适配方案",
"mvp_actions": "读者可立即尝试的3个行动",
"estimated_length": 2500,
"priority_score": 0.78,
"status": "待处理",
"lock_by": null,
"lock_at": null,
"created_at": "2026-04-18T18:26:25.984697"
}
]
@@ -1 +0,0 @@
{"task": "sustainability_collection", "time": "2026-04-18 18:26", "topic_count": 1, "case_count": 3, "source_count": 8, "details_link": "automation/data/sustainability_raw/2026-04-18"}
@@ -1,53 +0,0 @@
[
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
},
{
"id": "LOCAL-UNKNOWN",
"country": "Global",
"category": "未分类",
"title": "",
"core_idea": "",
"data_facts": "",
"global_advantage": "",
"china_pain_point": "",
"localization_suggestion": "",
"mvp_action": "",
"source_url": "",
"credibility_rating": "⭐⭐",
"china_applicability": "⭐⭐",
"collection_date": "2026-04-18",
"status": "待验证"
}
]
@@ -1,21 +0,0 @@
[
{
"id": "TOPIC-.285EC9",
"title": "未分类新趋势: 与的中国落地路径",
"cases": [
"LOCAL-UNKNOWN",
"LOCAL-UNKNOWN",
"LOCAL-UNKNOWN"
],
"audience": "城市焦虑青年(26-35岁)",
"china_pain_points": "未分类在中国面临的主要问题",
"localization_solution": "国际案例中国化适配方案",
"mvp_actions": "读者可立即尝试的3个行动",
"estimated_length": 2500,
"priority_score": 0.78,
"status": "待处理",
"lock_by": null,
"lock_at": null,
"created_at": "2026-04-19T05:00:54.473396"
}
]
@@ -1 +0,0 @@
{"task": "sustainability_collection", "time": "2026-04-19 05:00", "topic_count": 1, "case_count": 3, "source_count": 8, "details_link": "automation/data/sustainability_raw/2026-04-19"}
+40 -340
View File
@@ -1,354 +1,54 @@
[
{
"id": "A01",
"title": "远程工作2026中国指南:从'不可能'到'可行'的路径图",
"field": "未来工作方式",
"id": "LIVING-001-26",
"title": "零浪费家庭实验:从囤积到精简的100天",
"cases": [],
"audience": "26-35岁城市焦虑青年",
"china_pain_points": "消费主义陷阱、快递包装泛滥、垃圾分类执行难",
"localization_solution": "家庭减法清单、快递包装回收点地图、社区交换市集",
"mvp_actions": "1. 本月减少50%快递包装 2. 建立家庭减法清单 3. 参与社区交换市集",
"estimated_length": 3500,
"priority_score": 85.5,
"field": "可持续生活系统",
"format": "趋势洞察 + 实操指南",
"core_concept": "通过法律实操(合同、社保、个税)和心理建设(孤独应对),在中国环境下实现远程工作",
"audience_pain": "想远程但不知如何合法操作,担心被边缘化",
"unique_angle": "对比GitLab/Zapier海外实践,本土化落地策略",
"core_concept": "少即是多,从消费主义回归生活本质",
"audience_pain": "被消费主义裹挟,想改变但不知道从何开始",
"unique_angle": "用数据量化减法效果,用真实案例展示转变过程",
"priority": "高",
"priority_score": 10,
"total_score": 53,
"status": "待发布",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436643",
"ready_at": "2026-04-18",
"total_score": 85.5,
"compliance_score": 100,
"lock_by": null,
"lock_at": null,
"source_file": "automation/data/sustainability_topics.json",
"ready_at": "2026-05-08",
"published_at": null,
"platform_urls": {},
"published_at": null
},
{
"id": "A02",
"title": "AI副业入门:用DeepSeek实现第一笔收入的100天",
"field": "未来工作方式",
"format": "实操指南 + 案例研究",
"core_concept": "从代写文案/数据分析起步,通过Fiverr国内外平台对比,制定定价策略和违规红线规避",
"audience_pain": "想用AI赚钱但不知从何开始,怕踩坑",
"unique_angle": "对比Fiverr海外繁荣 vs 国内空白,提供本土化接单路径",
"priority": "高",
"priority_score": 10,
"total_score": 52,
"status": "待发布",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436654",
"ready_at": "2026-04-19",
"compliance_score": 100,
"status": "待处理",
"lock_by": null,
"lock_at": null
},
{
"id": "A03",
"title": "数字游民签证全解析:30个国家政策对比,中国护照能去哪些?",
"field": "未来工作方式",
"format": "对比分析 + 实操指南",
"core_concept": "分析爱沙尼亚/葡萄牙/巴厘岛等30国游民签证,结合中国护照限制,给出签证+保险+税务+社群的完整路线",
"audience_pain": "想地理套利但被签证和社保困扰",
"unique_angle": "不是简单列出签证,而是给出中国护照持有者的可行组合方案(如泰国+大马+巴厘岛)",
"id": "TECH-001-26",
"title": "AI副业入门:用DeepSeek实现第一笔收入的100天",
"cases": [],
"audience": "26-35岁城市焦虑青年",
"china_pain_points": "主业不稳定、副业方向迷茫、技能变现难",
"localization_solution": "AI工具链整合、国内平台变现路径、从0到1的实战案例",
"mvp_actions": "1. 学习DeepSeek基础操作 2. 找到第一个付费客户 3. 建立可复制的服务流程",
"estimated_length": 4000,
"priority_score": 88.2,
"field": "科技向善",
"format": "趋势洞察 + 实操指南",
"core_concept": "用AI放大个人能力,实现副业收入",
"audience_pain": "想搞钱但不知道从哪开始,担心AI替代自己",
"unique_angle": "100天真实记录,从0到第一笔收入的完整路径",
"priority": "高",
"priority_score": 10,
"total_score": 51,
"total_score": 88.2,
"compliance_score": 100,
"source_file": "automation/data/sustainability_topics.json",
"ready_at": "2026-05-08",
"published_at": null,
"platform_urls": {},
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436656",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "A04",
"title": "一人公司实验:从创意到营收的365天日志",
"field": "未来工作方式",
"format": "实践日志 + 方法论",
"core_concept": "基于Indie Hackers案例,结合中国孤独创业现状,提供MVP设计、现金流管理、法律合规的一站式指南",
"audience_pain": "想单干但怕失败、缺启动资金、不懂营销",
"unique_angle": "真实日志形式,展示完整从0到营收的过程,不美化",
"priority": "高",
"priority_score": 10,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436658",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "A05",
"title": "AI时代的技能组合:什么技能值得投入10年?",
"field": "未来工作方式",
"format": "趋势分析 + 个人规划",
"core_concept": "基于WEF未来技能报告,划分4个技能维度(AI强化型、AI无法替代、复合型、过时型),帮中国职场人识别护城河技能",
"audience_pain": "学什么都不放心,怕投入时间后AI又取代",
"unique_angle": "将全球宏观报告转化为个人技能地图,提供可视化工具",
"priority": "中",
"priority_score": 7,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436660",
"ready_at": "2026-04-17",
"compliance_score": 100
},
{
"id": "B01",
"title": "城市农业ROI报告:20㎡阳台种菜一年,省了多少钱?",
"field": "可持续生活系统",
"format": "数据分析 + 实操指南",
"core_concept": "对比东京垂直农场与国内空间限制,精选高ROI蔬菜品种,智能设备自动灌溉,给出详细成本核算和品种推荐",
"audience_pain": "想种但怕麻烦、怕亏本、不知道种什么",
"unique_angle": "用财务思维算账(投入/产出/时间成本),打破'种菜必须有地'的思维",
"priority": "高",
"priority_score": 10,
"total_score": 52,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436665",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "B02",
"title": "零浪费家庭实验:一年只产100L垃圾,可能吗?",
"field": "可持续生活系统",
"format": "实践实验 + 方法论",
"core_concept": "对比瑞典零浪费城市,针对中国垃圾分类困境,提供垃圾追踪表、替代方案数据库、社区互助网络",
"audience_pain": "想环保但觉得做不到、不知道从哪减",
"unique_angle": "极限实验(100L/年)+ 可执行步骤(从塑料减量开始),不理想化",
"priority": "高",
"priority_score": 10,
"total_score": 51,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436667",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "B03",
"title": "低碳生活账单:用3年省了8万,碳足迹降了60%",
"field": "可持续生活系统",
"format": "数据分析 + 案例研究",
"core_concept": "对比欧洲碳税政策,从交通(电动车+共享)、饮食(植物为主)、消费(二手优先)三个维度,展示真实账单变化",
"audience_pain": "觉得低碳=更贵,不敢尝试",
"unique_angle": "用财务数据说话(省8万),打破'环保=烧钱'误解",
"priority": "高",
"priority_score": 10,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436669",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "B04",
"title": "循环消费实战:10件物品,用3年省了2万",
"field": "可持续生活系统",
"format": "实操指南 + 案例清单",
"core_concept": "对比法国二手强制法与中国闲鱼文化,提供购买决策树(买新/二手/租)、延长寿命技巧、转卖策略",
"audience_pain": "想买二手但怕质量差、怕麻烦",
"unique_angle": "10件物品的具体交易记录和对比(手机、相机、家具等),可复制",
"priority": "中",
"priority_score": 7,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436671",
"ready_at": "2026-04-17",
"compliance_score": 100
},
{
"id": "B05",
"title": "社区菜园指南:如何推动小区5户邻居共建共享",
"field": "可持续生活系统",
"format": "方法论 + 实操步骤",
"core_concept": "对比纽约社区花园政策与中国物业协调难题,提供法律风险(物权)、利益分配机制、技术方案(分区+智能)",
"audience_pain": "想组织但怕纠纷、不懂法律、协调不了邻居",
"unique_angle": "从1个友好小区试点开始,成功后复制,降低风险",
"priority": "高",
"priority_score": 10,
"total_score": 49,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436673",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "C01",
"title": "第二大脑2.0:用DeepSeek+本地向量库建立私有知识系统",
"field": "个人知识工厂",
"format": "技术指南 + 实操案例",
"core_concept": "对比Obsidian+RAG海外实践,针对国内云服务担忧,提供数据主权、隐私保护、无缝检索、AI问答的本地化方案",
"audience_pain": "想系统化知识但担心云存储安全,怕复杂",
"unique_angle": "强调数据主权,从API调用到本地部署的渐进路线",
"priority": "中",
"priority_score": 7,
"total_score": 52,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436675",
"ready_at": "2026-04-18",
"compliance_score": 100
},
{
"id": "C02",
"title": "PKM极简实践:PARA系统在Notion上的落地模板",
"field": "个人知识工厂",
"format": "模板分享 + 方法论",
"core_concept": "将Tiago Forte的PARA体系简化为3个核心文件夹,每周10分钟维护,AI辅助整理,让中国人真正用起来",
"audience_pain": "学了方法坚持不了,工具复杂难上手",
"unique_angle": "极简版(4个区)+ 每日5分钟习惯养成,降低门槛",
"priority": "中",
"priority_score": 7,
"total_score": 51,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436677"
},
{
"id": "C03",
"title": "费曼学习法AI增强:如何让AI帮你'教'懂一个概念",
"field": "个人知识工厂",
"format": "方法论 + 实践工具",
"core_concept": "结合经典费曼技巧与AI工具,三步法(AI简化→自我复述→Gap识别)+ 输出倒逼输入",
"audience_pain": "学东西记不住,自以为懂了但其实不会",
"unique_angle": "用AI当'测试官',验证你的理解深度,非被动接受知识",
"priority": "中",
"priority_score": 7,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436679"
},
{
"id": "C04",
"title": "AI个人助理搭建:从ChatGPT到私有化部署的完整路线",
"field": "个人知识工厂",
"format": "技术路线图 + 成本分析",
"core_concept": "基于海外个人AI助手普及现状,针对国内数据安全顾虑,提供从API调用到本地部署的渐进式方案(成本可控)",
"audience_pain": "想用AI助手但又怕数据泄露,不知如何起步",
"unique_angle": "不是直接推本地部署(成本高),而是API优先,敏感时再本地策略",
"priority": "中",
"priority_score": 7,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436680"
},
{
"id": "C05",
"title": "技能树可视化:用思维导图规划5年职业路径",
"field": "个人知识工厂",
"format": "方法论 + 工具模板",
"core_concept": "借鉴化工业界能力模型,构建硬技能×软技能矩阵,行业对标和学习资源聚合,让职业成长可规划",
"audience_pain": "不知道学什么,学了不知道用在哪,职业迷茫",
"unique_angle": "技能树而非技能列表,展示技能间关联和成长路径",
"priority": "中",
"priority_score": 7,
"total_score": 49,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436682"
},
{
"id": "D01",
"title": "AI伦理实践指南:开发者在中国的合规清单",
"field": "科技人文交叉",
"format": "合规指南 + 案例分析",
"core_concept": "对比EU AI Act与中国算法推荐管理规定,提供数据隐私、歧视检测、透明度义务、备案流程的自查清单",
"audience_pain": "开发者不了解国内AI伦理法规,怕踩雷",
"unique_angle": "不是泛泛而谈伦理,而是具体到'备案流程'和'自查表',即拿即用",
"priority": "高",
"priority_score": 10,
"total_score": 51,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436687",
"ready_at": "2026-04-16",
"compliance_score": 100
},
{
"id": "D02",
"title": "数字排毒月:戒掉微信/抖音后,生活发生了什么",
"field": "科技人文交叉",
"format": "实践实验 + 效果分析",
"core_concept": "对比硅谷禅修热与中国'失联恐惧',采用渐进式戒断(无屏时段)+ 替代活动 + 社交边界管理",
"audience_pain": "想减少屏幕时间但又怕错过重要信息,自律困难",
"unique_angle": "真实实验记录(not理论),展示戒断前后的生活变化数据",
"priority": "中",
"priority_score": 7,
"total_score": 50,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436689"
},
{
"id": "D03",
"title": "银发科技报告:给爸妈装智能设备,学到的5个设计原则",
"field": "科技人文交叉",
"format": "设计原则 + 案例",
"core_concept": "对比日本适老化设计与国产'适老模式'鸡肋,提炼简化选项、物理反馈、容错设计、情感连接的具体方案",
"audience_pain": "给父母买智能设备但他们不用,功能复杂",
"unique_angle": "不是推荐产品,而是总结5个设计原则,让读者自己改造设备",
"priority": "中",
"priority_score": 7,
"total_score": 49,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436690"
},
{
"id": "D04",
"title": "儿童数字素养课:10岁儿子的AI启蒙12周",
"field": "科技人文交叉",
"format": "教育日志 + 方法论",
"core_concept": "对比芬兰AI教育与国内家长'禁止接触'心态,通过每周1次'AI家庭时间',培养批判性思维和创造力",
"audience_pain": "不知如何让孩子正确认识AI,怕沉迷又怕脱节",
"unique_angle": "真实父子12周项目记录,提供可复制的课程大纲",
"priority": "中",
"priority_score": 7,
"total_score": 48,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436692"
},
{
"id": "D05",
"title": "科技与自然共生:如何用AI让阳台农场更'自然'",
"field": "科技人文交叉",
"format": "理念 + 实操方案",
"core_concept": "对比荷兰智能温室与中国人'回归原始'误区,实现技术隐形化(传感器+提醒)+ 自然反馈闭环 + 人工仪式感",
"audience_pain": "想用科技但又怕失去'自然感',追求矛盾",
"unique_angle": "技术与情感连接的平衡方案,AI只做幕后,人工保留仪式",
"priority": "高",
"priority_score": 10,
"total_score": 48,
"status": "待处理",
"cases": [],
"source_file": "strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md",
"created_at": "2026-04-16T09:42:54.436694",
"ready_at": "2026-04-16",
"compliance_score": 100
"lock_by": null,
"lock_at": null
}
]
@@ -1,322 +0,0 @@
[
{
"id": "001",
"title": "在上海阳台种菜一年,我收获了啥?",
"field": "自然 / 生活",
"format": "实操指南 + 个人故事",
"word_count": "2500",
"core_concept": "城市农业不仅是种菜,更是与自然重建连接的生活方式",
"audience_pain": "- 想体验田园生活但没条件去农村",
"unique_angle": "- 不鼓吹\"田园牧歌\",客观展示失败和踩坑",
"data_cases": [
"1. 上海阳台种植调研报告(找本地社区数据)",
"2. 不同蔬菜的生长周期和产量数据(农业网站)"
],
"estimated_days": "4天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-15",
"status": "已发布",
"evaluation": {
"受众覆盖": 8,
"独特性": 9,
"数据可得性": 8,
"可持续性": 7,
"平台契合度": 9,
"品牌契合度": 10,
"总分": 51
},
"total_score": 51,
"cases": [],
"source_file": "001-上海阳台种菜一年.md",
"created_at": "2026-04-16T07:52:48.754855",
"published_date": "2026-04-16"
},
{
"id": "002",
"title": "AI时代,什么能力不会被替代?",
"field": "科技 / 职场",
"format": "趋势洞察 + 实操建议",
"word_count": "2800",
"core_concept": "AI淘汰的不是工作,而是不会使用AI的人;但有些\"人类特质\"能力反而更珍贵",
"audience_pain": "- 担心被AI替代,职业焦虑严重",
"unique_angle": "- 区分配\"被AI强化的能力\"和\"AI无法替代的能力\"",
"data_cases": [
"1. 世界经济论坛《未来就业报告》技能趋势数据",
"2. 美国劳工统计局职业自动化概率研究"
],
"estimated_days": "5天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-18",
"status": "待处理",
"evaluation": {
"受众覆盖": 9,
"独特性": 8,
"数据可得性": 9,
"平台契合度": 9,
"品牌契合度": 10,
"总分": 53
},
"total_score": 53,
"cases": [],
"source_file": "002-AI时代什么能力不会被替代.md",
"created_at": "2026-04-16T07:52:48.755025"
},
{
"id": "003",
"title": "从程序员到数字游民:我的三年转型之路",
"field": "工作 / 职场",
"format": "个人故事 + 实操指南",
"word_count": "2600",
"core_concept": "数字游民不是逃离,而是更聪明的工作生活方式选择",
"audience_pain": "- 受够了996,想自由但不敢行动",
"unique_angle": "- 不美化\"边玩边赚\",客观展示挑战和代价",
"data_cases": [
"1. 数字游民调研报告(人数、收入、职业分布)",
"2. 不同国家的签证政策和生活成本对比(数字游民签证国家)"
],
"estimated_days": "5天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-20",
"status": "待处理",
"evaluation": {
"受众覆盖": 9,
"独特性": 8,
"数据可得性": 8,
"可持续性": 9,
"平台契合度": 9,
"品牌契合度": 10,
"总分": 53
},
"total_score": 53,
"cases": [],
"source_file": "003-数字游民这一年.md",
"created_at": "2026-04-16T07:52:48.755167"
},
{
"id": "004",
"title": "零浪费生活一年:一个家庭产生的垃圾去哪儿了?",
"field": "自然 / 生活",
"format": "实验记录 + 科普指南",
"word_count": "2300",
"core_concept": "零浪费不是苦行,而是更聪明的消费选择;个人行动虽小,但能改变系统",
"audience_pain": "- 想环保但不知道从何下手",
"unique_angle": "- 不道德绑架,用数据和成本说服",
"data_cases": [
"1. 中国城市生活垃圾产生量数据(统计年鉴)",
"2. 不同包装方式的碳足迹对比(生产、运输、处理)"
],
"estimated_days": "4天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-22",
"status": "待处理",
"evaluation": {
"受众覆盖": 8,
"独特性": 9,
"数据可得性": 8,
"可持续性": 8,
"平台契合度": 8,
"品牌契合度": 10,
"总分": 51
},
"total_score": 51,
"cases": [],
"source_file": "004-零浪费生活一年实验.md",
"created_at": "2026-04-16T07:52:48.755315"
},
{
"id": "005",
"title": "深度工作实践:如何在干扰世界中保持专注",
"field": "工作 / 生产力",
"format": "实操指南 + 个人实验",
"word_count": "2400",
"core_concept": "专注力是新时代最稀缺的资源,通过系统方法可以重建深度工作能力",
"audience_pain": "- 每天忙碌但没产出,时间碎片化",
"unique_angle": "- 不鸡汤,用实验数据和自我追踪说话",
"data_cases": [
"1. Cal Newport《深度工作》理论体系",
"2. 注意力恢复理论(Attention Restoration Theory"
],
"estimated_days": "4天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-23",
"status": "待处理",
"evaluation": {
"受众覆盖": 9,
"独特性": 7,
"数据可得性": 9,
"可持续性": 8,
"平台契合度": 9,
"品牌契合度": 8,
"总分": 50
},
"total_score": 50,
"cases": [],
"source_file": "005-深度工作实践.md",
"created_at": "2026-04-16T07:52:48.755469"
},
{
"id": "006",
"title": "副业月入过万:我一个程序员的三年探索之路",
"field": "工作 / 副业",
"format": "实操经验 + 方法论总结",
"word_count": "2700",
"core_concept": "副业不是投机,而是将主业技能产品化、多元化的过程",
"audience_pain": "- 工资不够花,想搞钱但不知道从哪开始",
"unique_angle": "- 从技术人视角(程序员)看副业选项,但方法论通用",
"data_cases": [
"1. 中国职场人副业调研报告(参与率、收入分布)",
"2. 程序员技能变现渠道对比(外包、咨询、产品、内容)"
],
"estimated_days": "5天",
"priority": "高",
"priority_score": 10,
"publish_date": "2026-04-25",
"status": "待处理",
"evaluation": {
"受众覆盖": 9,
"独特性": 7,
"数据可得性": 9,
"可持续性": 9,
"平台契合度": 9,
"品牌契合度": 9,
"总分": 52
},
"total_score": 52,
"cases": [],
"source_file": "006-副业月入过万.md",
"created_at": "2026-04-16T07:52:48.755650"
},
{
"id": "007",
"title": "正念冥想一年:从焦虑到平静的转变",
"field": "人文 / 心理健康",
"format": "个人实验 + 科学解读",
"word_count": "2200",
"core_concept": "冥想不是玄学,是可训练的大脑肌肉;一年练习带来可测量的认知和情绪变化",
"audience_pain": "- 焦虑、失眠、注意力不集中",
"unique_angle": "- 用科学家态度做自我实验(追踪数据、前后对比)",
"data_cases": [
"1. 冥想改变大脑结构的研究(灰质增加、杏仁核缩小)",
"2. 压力激素(皮质醇)水平变化数据"
],
"estimated_days": "4天",
"priority": "中",
"priority_score": 7,
"publish_date": "2026-04-27",
"status": "待处理",
"evaluation": {
"受众覆盖": 8,
"独特性": 8,
"数据可得性": 8,
"可持续性": 7,
"平台契合度": 8,
"品牌契合度": 9,
"总分": 48
},
"total_score": 48,
"cases": [],
"source_file": "007-正念冥想一年变化.md",
"created_at": "2026-04-16T07:52:48.755802"
},
{
"id": "008",
"title": "极简3年:我从囤积症到少物生活的转变",
"field": "人文 / 生活方式",
"format": "个人转变故事 + 实操指南",
"word_count": "2500",
"core_concept": "极简不是扔东西,而是重新定义\"足够\";通过减少物质,增加精神丰盈",
"audience_pain": "- 家里东西越来越多,整理完很快又乱",
"unique_angle": "- 从囤积症患者到极简主义者的真实转变(有心理过程)",
"data_cases": [
"1. 极简主义创始人(Joshua & Ryan)理念和实践",
"2. 物品生命周期研究(平均使用次数、浪费数据)"
],
"estimated_days": "4天",
"priority": "中",
"priority_score": 7,
"publish_date": "2026-04-29",
"status": "待处理",
"evaluation": {
"受众覆盖": 8,
"独特性": 8,
"数据可得性": 8,
"可持续性": 8,
"平台契合度": 8,
"品牌契合度": 10,
"总分": 50
},
"total_score": 50,
"cases": [],
"source_file": "008-极简3年我学会了.md",
"created_at": "2026-04-16T07:52:48.755960"
},
{
"id": "009",
"title": "城市观鸟指南:如何在水泥森林发现 biodiversity",
"field": "自然 / 科普",
"format": "实操指南 + 城市生态观察",
"word_count": "2000",
"core_concept": "自然不在远方,就在身边;城市也是wildlife的栖息地,学会观察能提升生活幸福感",
"audience_pain": "- 想接触自然但没时间去郊外",
"unique_angle": "- 城市观鸟入门,从\"完全小白\"到\"常见鸟达人\"",
"data_cases": [
"1. 城市生物多样性调研报告(中国的城市化与野生动物)",
"2. 常见城市鸟类图鉴(种类、特征、习性)"
],
"estimated_days": "3天",
"priority": "中",
"priority_score": 7,
"publish_date": "2026-05-01",
"status": "待处理",
"evaluation": {
"受众覆盖": 7,
"独特性": 9,
"数据可得性": 8,
"可持续性": 9,
"平台契合度": 8,
"品牌契合度": 10,
"总分": 51
},
"total_score": 51,
"cases": [],
"source_file": "009-城市观鸟指南.md",
"created_at": "2026-04-16T07:52:48.756111"
},
{
"id": "010",
"title": "个人知识管理系统(PKM)实践:我用一年搭建的第二大脑",
"field": "工作 / 生产力",
"format": "实操教程 + 系统设计",
"word_count": "2800",
"core_concept": "信息时代,会学习不如会管理知识;一个适合自己的PKM系统能释放认知带宽,提升创造力和决策质量",
"audience_pain": "- 收藏无数,但从未回看(收藏夹吃灰)",
"unique_angle": "- 不推销特定工具,强调方法论+适合自己的选择",
"data_cases": [
"1. PKM方法论(Zettelkasten、PARA、CODE等)",
"2. 工具对比研究(Notion、Obsidian、Roam、Logseq、飞书、Notability"
],
"estimated_days": "5天",
"priority": "中",
"priority_score": 7,
"publish_date": "2026-05-03",
"status": "待处理",
"evaluation": {
"受众覆盖": 8,
"独特性": 8,
"数据可得性": 9,
"可持续性": 9,
"平台契合度": 9,
"品牌契合度": 9,
"总分": 52
},
"total_score": 52,
"cases": [],
"source_file": "010-PKM实践.md",
"created_at": "2026-04-16T07:52:48.756300"
}
]
+187
View File
@@ -0,0 +1,187 @@
{
"date": "2026-05-10",
"gaps": [
{
"domain": "远程工作",
"topic": "远程工作2.0时代",
"reason": "疫情后远程工作模式持续升级,企业与员工探索新型协作方式",
"hot_keywords": [
"异步协作",
"数字游牧",
"远程团队管理"
],
"platform": "知乎"
},
{
"domain": "AI工具",
"topic": "AI绘画工具大战",
"reason": "MidJourney、Stable Diffusion等工具更新迭代,用户创作热情高涨",
"hot_keywords": [
"AI绘画",
"提示词工程",
"商用授权"
],
"platform": "微博"
},
{
"domain": "可持续生活",
"topic": "低碳生活实践",
"reason": "碳中和目标推动下,个人可持续生活方式成为热门讨论",
"hot_keywords": [
"零废弃",
"二手经济",
"绿色出行"
],
"platform": "小红书"
},
{
"domain": "知识管理",
"topic": "第二大脑(Zettelkasten",
"reason": "知识工作者追求高效学习与输出,个人知识管理系统流行",
"hot_keywords": [
"Obsidian",
"卡片笔记法",
"知识连接"
],
"platform": "知乎"
},
{
"domain": "数字生活",
"topic": "数字极简主义",
"reason": "信息过载背景下,用户开始主动减少数字干扰",
"hot_keywords": [
"数字断舍离",
"专注力管理",
"低刺激设计"
],
"platform": "微博"
},
{
"domain": "科技人文",
"topic": "元宇宙伦理",
"reason": "虚拟世界发展引发关于隐私、身份与社会关系的深度讨论",
"hot_keywords": [
"数字身份",
"虚拟伦理",
"元宇宙治理"
],
"platform": "知乎"
},
{
"domain": "远程工作",
"topic": "远程工作税务问题",
"reason": "跨境远程工作带来的税务合规性成为热议焦点",
"hot_keywords": [
"远程税务",
"数字游牧税",
"跨境合规"
],
"platform": "微博"
},
{
"domain": "AI工具",
"topic": "AI辅助写作",
"reason": "ChatGPT等工具推动内容创作效率提升,引发创作伦理讨论",
"hot_keywords": [
"AI写作",
"内容原创性",
"AI辅助创作"
],
"platform": "小红书"
},
{
"domain": "可持续生活",
"topic": "城市垂直农业",
"reason": "城市化进程中,本地化食品生产成为可持续发展新方向",
"hot_keywords": [
"屋顶农场",
"水培种植",
"本地供应链"
],
"platform": "知乎"
},
{
"domain": "数字生活",
"topic": "数字遗产管理",
"reason": "数字资产继承问题随着老龄化社会到来受到关注",
"hot_keywords": [
"数字遗嘱",
"账号继承",
"数字身后事"
],
"platform": "微博"
}
],
"suggestions": [
{
"title": "远程工作2.0:企业与员工的新协作",
"field": "职场与管理",
"core_concept": "疫情后远程工作模式持续升级,企业与员工探索新型协作方式",
"audience_pain": "如何在远程工作中保持高效协作与团队凝聚力",
"unique_angle": "从企业与员工双方视角分析远程工作2.0的实践与挑战"
},
{
"title": "AI绘画工具大战:创作革命还是泡沫?",
"field": "科技与创意",
"core_concept": "MidJourney、Stable Diffusion等工具更新迭代,用户创作热情高涨",
"audience_pain": "如何在AI工具辅助下提升创作效率与质量",
"unique_angle": "探讨AI绘画工具对传统艺术行业的影响与未来发展趋势"
},
{
"title": "低碳生活实践:你的每个选择都在改变世界",
"field": "环保与可持续",
"core_concept": "碳中和目标推动下,个人可持续生活方式成为热门讨论",
"audience_pain": "如何在日常生活中实践低碳,减少个人碳足迹",
"unique_angle": "从个人行为到社会影响,探索低碳生活的实践与意义"
},
{
"title": "第二大脑:知识管理的终极武器",
"field": "学习与生产力",
"core_concept": "知识工作者追求高效学习与输出,个人知识管理系统流行",
"audience_pain": "如何高效管理知识,提升学习与工作效率",
"unique_angle": "Zettelkasten方法的实践与优化,适用于不同场景的知识管理技巧"
},
{
"title": "数字极简主义:减少干扰,提升专注力",
"field": "数字生活",
"core_concept": "信息过载背景下,用户开始主动减少数字干扰",
"audience_pain": "如何在数字化时代保持专注,减少信息过载",
"unique_angle": "数字极简主义的实践方法与工具推荐,提升生活质量"
},
{
"title": "元宇宙伦理:虚拟世界的道德边界",
"field": "科技与人文",
"core_concept": "虚拟世界发展引发关于隐私、身份与社会关系的深度讨论",
"audience_pain": "如何在元宇宙中保护个人隐私与身份安全",
"unique_angle": "探讨元宇宙伦理的核心问题与可能的解决方案"
},
{
"title": "远程工作税务问题:跨境远程工作的合规指南",
"field": "职场与财务",
"core_concept": "跨境远程工作带来的税务合规性成为热议焦点",
"audience_pain": "如何在跨境远程工作中遵守税务法规,避免合规风险",
"unique_angle": "从法律与实践角度分析远程工作税务问题的解决方案"
},
{
"title": "AI辅助写作:创作效率的提升与伦理的思考",
"field": "科技与创意",
"core_concept": "ChatGPT等工具推动内容创作效率提升,引发创作伦理讨论",
"audience_pain": "如何利用AI工具提升创作效率,同时保持内容原创性",
"unique_angle": "探讨AI辅助写作的最佳实践与伦理边界"
},
{
"title": "城市垂直农业:未来食品生产的新方向",
"field": "环保与可持续",
"core_concept": "城市化进程中,本地化食品生产成为可持续发展新方向",
"audience_pain": "如何在城市中实现可持续的食品生产与供应",
"unique_angle": "城市垂直农业的技术与实践,探索未来食品生产的可能性"
},
{
"title": "数字遗产管理:老龄化社会的新挑战",
"field": "数字生活",
"core_concept": "数字资产继承问题随着老龄化社会到来受到关注",
"audience_pain": "如何管理与传承数字资产,避免遗产纠纷",
"unique_angle": "数字遗产管理的法律与实践指南,保护个人数字资产"
}
]
}
View File
View File
+1 -1
View File
@@ -4,7 +4,7 @@
set -e
PROJECT_ROOT="/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran"
PROJECT_ROOT="/root/openclaw-workspace/projects/yu-zhi-ran"
MARKDOWN_FILE="$PROJECT_ROOT/content/drafts/001-wechat.md"
COVER_IMG="$PROJECT_ROOT/content/publishing/images/05-封面图.png"
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env python3
"""
将 Markdown 文章转换为 Word 文档,嵌入图片
"""
import os
import re
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
# 路径配置
MARKDOWN_FILE = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年/final-article.md"
IMAGES_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/publishing/images"
OUTPUT_DOCX = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年/上海阳台种菜一年_最终版.docx"
# 读取 Markdown
with open(MARKDOWN_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
doc = Document()
doc.styles['Normal'].font.name = '微软雅黑'
doc.styles['Normal'].font.size = Pt(11)
# 样式函数
def add_heading(text, level=1):
heading = doc.add_heading(text, level=level)
heading.alignment = WD_ALIGN_PARAGRAPH.LEFT
return heading
def add_paragraph(text, bold=False, italic=False):
p = doc.add_paragraph()
run = p.add_run(text)
run.bold = bold
run.italic = italic
return p
# 解析 Markdown
in_code_block = False
in_table = False
table_data = []
for i, line in enumerate(lines):
line = line.rstrip('\n')
# 代码块跳过
if line.startswith('```'):
in_code_block = not in_code_block
continue
if in_code_block:
continue
# 标题
if line.startswith('# '):
add_heading(line[2:], level=1)
continue
if line.startswith('## '):
add_heading(line[3:], level=2)
continue
if line.startswith('### '):
add_heading(line[4:], level=3)
continue
# 表格处理(简化:将表格转为文本,图片位置用占位)
if line.startswith('|'):
in_table = True
table_data.append(line)
continue
if in_table and not line.startswith('|'):
in_table = False
# 可以在此转换表格,为简化直接跳过
continue
# 图片:![alt](path)
img_match = re.match(r'!\[(.*?)\]\((images/.*?)\)', line)
if img_match:
alt, path = img_match.groups()
img_full_path = os.path.join(os.path.dirname(MARKDOWN_FILE), path)
if os.path.exists(img_full_path):
try:
# 插入图片,宽度 6 英寸(约 15cm)
doc.add_picture(img_full_path, width=Inches(6))
# 居中
last_para = doc.paragraphs[-1]
last_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 添加图片说明(可选)
if alt:
cap = doc.add_paragraph(alt)
cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
cap.style = 'Caption'
except Exception as e:
doc.add_paragraph(f"[图片加载失败: {path}]")
else:
doc.add_paragraph(f"[图片缺失: {img_full_path}]")
continue
# 引用
if line.startswith('> '):
p = doc.add_paragraph(line[2:])
p.paragraph_format.left_indent = Inches(0.5)
p.italic = True
continue
# 列表
if re.match(r'^[-*] ', line):
p = doc.add_paragraph(line[2:], style='List Bullet')
continue
if re.match(r'^\d+\. ', line):
p = doc.add_paragraph(line[line.find('.')+2:], style='List Number')
continue
# 分隔线
if line.strip() == '---':
doc.add_paragraph('_' * 50)
continue
# 普通段落
if line.strip():
# 处理行内加粗、斜体
p = doc.add_paragraph()
parts = re.split(r'(\*\*[^*]+\*\*|\*[^*]+\*)', line)
for part in parts:
if part.startswith('**') and part.endswith('**'):
run = p.add_run(part[2:-2])
run.bold = True
elif part.startswith('*') and part.endswith('*'):
run = p.add_run(part[1:-1])
run.italic = True
else:
run = p.add_run(part)
else:
doc.add_paragraph() # 空行
# 保存文档
doc.save(OUTPUT_DOCX)
print(f"✅ Word 文档已生成: {OUTPUT_DOCX}")
print(f"📄 页数: {len(doc.paragraphs)} 段落")
print(f"🖼️ 图片路径: {IMAGES_DIR}")
-93
View File
@@ -1,93 +0,0 @@
#!/usr/bin/env python3
"""
生成 Word 文档,图片从发布目录的 images 文件夹读取
"""
import os
from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article.md")
IMAGES_DIR = os.path.join(BASE_DIR, "images") # 已复制的图片
OUTPUT_DOCX = os.path.join(BASE_DIR, "上海阳台种菜一年_最终版.docx")
with open(MD_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
doc = Document()
doc.styles['Normal'].font.name = '微软雅黑'
doc.styles['Normal'].font.size = Pt(11)
def add_heading(text, level=1):
heading = doc.add_heading(text, level=level)
heading.alignment = WD_ALIGN_PARAGRAPH.LEFT
return heading
for line in lines:
line = line.rstrip('\n')
if line.startswith('# '):
add_heading(line[2:], level=1)
continue
if line.startswith('## '):
add_heading(line[3:], level=2)
continue
if line.startswith('### '):
add_heading(line[4:], level=3)
continue
if line.startswith('---'):
doc.add_paragraph('_' * 60)
continue
# 图片
if line.startswith('!['):
import re
m = re.match(r'!\[(.*?)\]\((images/.*?)\)', line)
if m:
alt, fname = m.groups()
img_path = os.path.join(IMAGES_DIR, os.path.basename(fname))
if os.path.exists(img_path):
try:
doc.add_picture(img_path, width=Inches(6))
last_para = doc.paragraphs[-1]
last_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
except Exception as e:
doc.add_paragraph(f"[图片错误: {fname}]")
else:
doc.add_paragraph(f"[缺失图片: {fname}]")
continue
# 空行
if not line.strip():
doc.add_paragraph()
continue
# 普通段落,处理粗体斜体
p = doc.add_paragraph()
parts = []
tmp = line
while '**' in tmp:
parts.append(tmp[:tmp.find('**')])
tmp = tmp[tmp.find('**')+2:]
if '**' in tmp:
parts.append(('bold', tmp[:tmp.find('**')]))
tmp = tmp[tmp.find('**')+2:]
else:
parts.append(('bold', tmp))
break
if not parts:
parts = [line]
for part in parts:
if isinstance(part, tuple):
style, text = part
run = p.add_run(text)
run.bold = (style == 'bold')
else:
p.add_run(part)
doc.save(OUTPUT_DOCX)
print(f"✅ Word 已生成: {OUTPUT_DOCX}")
print(f"📄 段落数: {len(doc.paragraphs)}")
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""
生成 HTML,图片使用相对路径 'images/xxx.png'(确保图片在发布目录的 images 子文件夹中)
"""
import os
import re
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article.md")
OUT_HTML = os.path.join(BASE_DIR, "上海阳台种菜一年_可复制.html")
with open(MD_FILE, "r", encoding="utf-8") as f:
content = f.read()
# 替换图片为 HTML img 标签,保持相对路径
def replace_img(match):
alt, path = match.groups()
return f'<img src="{path}" alt="{alt}" style="max-width:100%; margin:20px 0; display:block;">'
content = re.sub(r'!\[(.*?)\]\((images/.*?)\)', replace_img, content)
# 转换 Markdown 为 HTML
html_lines = []
for line in content.split('\n'):
if line.startswith('# '):
html_lines.append(f'<h1>{line[2:]}</h1>')
elif line.startswith('## '):
html_lines.append(f'<h2>{line[3:]}</h2>')
elif line.startswith('### '):
html_lines.append(f'<h3>{line[4:]}</h3>')
elif line.startswith('---'):
html_lines.append('<hr style="border:none;border-top:2px dashed #ddd;margin:40px 0;">')
elif line.startswith('> '):
html_lines.append(f'<blockquote style="border-left:4px solid #4CAF50;background:#f9f9f9;padding:10px 20px;margin:20px 0;color:#666;">{line[2:]}</blockquote>')
elif re.match(r'^[-*] ', line):
html_lines.append(f'<li>{line[2:]}</li>')
elif re.match(r'^\d+\. ', line):
html_lines.append(f'<li>{line[line.find(". ")+2:]}</li>')
elif line.strip() == '':
html_lines.append('<br>')
else:
# 处理行内粗体斜体
tmp = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', line)
tmp = re.sub(r'\*(.*?)\*', r'<em>\1</em>', tmp)
html_lines.append(f'<p>{tmp}</p>')
html = f'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>上海阳台种菜一年</title>
<style>
body {{ font-family: "Microsoft YaHei", sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.8; }}
h1 {{ font-size: 28px; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }}
h2 {{ font-size: 24px; margin-top: 40px; border-left: 4px solid #4CAF50; padding-left: 10px; }}
h3 {{ font-size: 20px; margin-top: 30px; color: #666; }}
img {{ max-width: 100%; height: auto; border-radius: 4px; margin: 20px 0; display: block; margin-left: auto; margin-right: auto; }}
blockquote {{ border-left: 4px solid #4CAF50; background: #f9f9f9; padding: 10px 20px; margin: 20px 0; color: #666; }}
li {{ margin-bottom: 8px; }}
p {{ margin-bottom: 16px; }}
</style>
</head>
<body>
{chr(10).join(html_lines)}
</body>
</html>'''
with open(OUT_HTML, "w", encoding="utf-8") as f:
f.write(html)
print(f"✅ HTML 已生成: {OUT_HTML}")
print(f"📊 字符数: {len(content)}")
print(f"🖼️ 图片路径: images/ (需与 HTML 同目录的 images 文件夹)")
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python3
"""
生成图文混排的 HTML(图片内联为 base64),方便直接复制
"""
import os
import re
import base64
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article.md")
IMAGES_DIR = os.path.join(BASE_DIR, "images") # 使用发布目录内的 images
OUT_HTML = os.path.join(BASE_DIR, "上海阳台种菜一年_内联.html")
# 读取 Markdown
with open(MD_FILE, "r", encoding="utf-8") as f:
content = f.read()
# 预加载图片并转为 base64
image_cache = {}
for fname in os.listdir(IMAGES_DIR):
if fname.endswith('.png'):
path = os.path.join(IMAGES_DIR, fname)
with open(path, "rb") as imgf:
b64 = base64.b64encode(imgf.read()).decode('utf-8')
image_cache[fname] = b64
# 替换图片
def replace_img(match):
alt = match.group(1)
fname = match.group(2)
key = os.path.basename(fname)
if key in image_cache:
return f'<img src="data:image/png;base64,{image_cache[key]}" alt="{alt}" style="max-width:100%; margin:20px 0; display:block;">'
else:
return f'<p>[图片缺失: {fname}]</p>'
content = re.sub(r'!\[(.*?)\]\((images/.*?)\)', replace_img, content)
# Markdown 转 HTML
html_lines = []
for line in content.split('\n'):
if line.startswith('# '):
html_lines.append(f'<h1>{line[2:]}</h1>')
elif line.startswith('## '):
html_lines.append(f'<h2>{line[3:]}</h2>')
elif line.startswith('### '):
html_lines.append(f'<h3>{line[4:]}</h3>')
elif line.startswith('---'):
html_lines.append('<hr>')
elif line.startswith('> '):
html_lines.append(f'<blockquote>{line[2:]}</blockquote>')
elif re.match(r'^[-*] ', line):
html_lines.append(f'<li>{line[2:]}</li>')
elif re.match(r'^\d+\. ', line):
html_lines.append(f'<li>{line[line.find(". ")+2:]}</li>')
elif line.strip() == '':
html_lines.append('<br>')
else:
tmp = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', line)
tmp = re.sub(r'\*(.*?)\*', r'<em>\1</em>', tmp)
html_lines.append(f'<p>{tmp}</p>')
html = f'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>上海阳台种菜一年</title>
<style>
body {{ font-family: "Microsoft YaHei", sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.8; }}
h1 {{ font-size: 28px; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }}
h2 {{ font-size: 24px; margin-top: 40px; border-left: 4px solid #4CAF50; padding-left: 10px; }}
h3 {{ font-size: 20px; margin-top: 30px; color: #666; }}
img {{ max-width: 100%; height: auto; border-radius: 4px; margin: 20px 0; display: block; margin-left: auto; margin-right: auto; }}
blockquote {{ border-left: 4px solid #4CAF50; background: #f9f9f9; padding: 10px 20px; margin: 20px 0; color: #666; }}
li {{ margin-bottom: 8px; }}
p {{ margin-bottom: 16px; }}
</style>
</head>
<body>
{chr(10).join(html_lines)}
</body>
</html>'''
with open(OUT_HTML, "w", encoding="utf-8") as f:
f.write(html)
print(f"✅ HTML 已生成: {OUT_HTML}")
print(f"📊 字符数: {len(content)}")
print(f"🖼️ 内嵌图片: {len(image_cache)}")
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env python3
"""
对 article.md 进行内容优化:
- 去除具体 App 品牌名(花帮主、园艺助手)
- 隐去设备具体品牌(小米米家)
- 保留功能描述和用户价值
- 保持中立、实用、无广告感
"""
import os
import re
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article.md")
OUT_MD = os.path.join(BASE_DIR, "final-article-optimized.md")
with open(MD_FILE, "r", encoding="utf-8") as f:
content = f.read()
# 1. 替换具体 App 名称 -> 通用描述
content = re.sub(r'花帮主', '一些第三方种植App', content)
content = re.sub(r'园艺助手', '另一些生活助手类App', content)
content = re.sub(r'(\*\*)花帮主(\*\*)AI识别病虫害,准确率85%', '**一些第三方种植App**,可以通过 AI 识别病虫害,准确率在 80% 以上', content)
# 2. 替换设备品牌 -> 通用描述
content = re.sub(r'小米米家灌溉套装', '智能灌溉套装', content)
content = re.sub(r'LED补光灯', 'LED 植物补光灯', content)
# 3. 移除可能带有广告嫌疑的表述(如“效果最好”、“推荐”等),改为中性描述
content = re.sub(r'强烈推荐(易种)', '适合新手(易种)', content)
content = re.sub(r'强烈推荐', '推荐', content)
# 4. 图片描述调整(不影响图片本身,只调整 alt 文本和图片说明)
# 图片文件保留不变,只调整 Markdown 中的说明文字
content = re.sub(r'!\[App截图\]', '[App功能截图]', content)
content = re.sub(r'App截图', 'App功能界面示意', content)
# 5. 增加免责声明(在文末)
if "声明:" not in content:
content = content.rstrip() + "\n\n---\n\n> **声明**:本文提及的工具和设备仅为个人使用经验分享,不构成商业推荐。读者可根据自身需求选择类似产品。\n"
with open(OUT_MD, "w", encoding="utf-8") as f:
f.write(content)
print(f"✅ 优化完成: {OUT_MD}")
print("🔧 优化项:")
print(" - 去除具体 App 品牌名")
print(" - 隐去设备品牌")
print(" - 增加中立表述")
print(" - 添加免责声明")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+1 -1
View File
@@ -6,7 +6,7 @@ from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
BASE_DIR = "/root/openclaw-workspace/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article-optimized.md")
IMAGES_DIR = os.path.join(BASE_DIR, "images")
OUTPUT_DOCX = os.path.join(BASE_DIR, "上海阳台种菜一年_发布版.docx")
+1 -1
View File
@@ -3,7 +3,7 @@
python3 - << 'PYEOF'
import os, re, base64
BASE_DIR = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
BASE_DIR = "/root/openclaw-workspace/projects/yu-zhi-ran/content/published/2026-04-14-上海阳台种菜一年"
MD_FILE = os.path.join(BASE_DIR, "final-article-optimized.md")
IMAGES_DIR = os.path.join(BASE_DIR, "images")
OUT_HTML = os.path.join(BASE_DIR, "上海阳台种菜一年_发布版.html")
+2 -2
View File
@@ -5,8 +5,8 @@
set -e
PROJECT_ROOT="/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran"
SESSION_STATE="/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/automation/wechat_state.json"
PROJECT_ROOT="/root/openclaw-workspace/projects/yu-zhi-ran"
SESSION_STATE="/root/openclaw-workspace/projects/yu-zhi-ran/automation/wechat_state.json"
if [ $# -ne 2 ]; then
echo "用法: $0 <markdown_file> <images_dir>"
+1 -1
View File
@@ -7,7 +7,7 @@
set -e
# 配置
PROJECT_ROOT="/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran"
PROJECT_ROOT="/root/openclaw-workspace/projects/yu-zhi-ran"
SESSION_STATE="/root/.openclaw/sessions/zhihu-state.json"
ZHIHU_CREATOR_URL="https://www.zhihu.com/creator"
-3
View File
@@ -1,3 +0,0 @@
# 远程工作2026中国指南:从'不可能'到'可行'的路径图 - 2026-04-18 - 小红书笔记
宇 宇之然 | 可持续生活指南 每天分享一个可执行的可持续行动 ✨ 远程工作2026中国指南:从'不可能'到'可行'的路径图 📅 2026-04-18 更新 一、引言(约200字) LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-a647339c834e416dbf098a188ed8766b)","type":"upstream_error"}},请手动补充) 二、核心观点(约300字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-6aad3f3eeae24384a0afbf4ce1b037ef)","type":"upstream_error"}},请手动补充) 三、受众痛点分析(约300字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-4d610fbf467a441bbe8dc6578007c901)","type":"upstream_error"}},请手动补充) 四、全球/行业趋势与案例(约500字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-3164807ca0a74d0e94463c37ffcc087b)","type":"upstream_error"}},请手动补充) 五、本土落地建议(约400字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-a0f3a93c753343728c95710199606b24)","type":"upstream_error"}},请手动补充) 七、行动指南(MVP,约200字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-c4603414e2ac475d85cac4783b693530)","type":"upstream_error"}},请手动补充) 八、总结与鼓励(约200字) (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-00fa2593e1db47de8b7c078e789dc663)","type":"upstream_error"}},请手动补充) 九、参考文献 (LLM 调用失败:HTTP 502: {"error":{"message":"Model resources are currently busy. Please try again later. (request_id: chatcmpl-b68f9f2bb8f94c148b249980e6301508)","type":"upstream_error"}},请手动补充) (本文由宇之然AI助手生成,数据来源可靠,内容经合规审查) 生成时间:2026-04-18 #AI #可持续 #生活方式 💚 本文由宇之然AI助手生成,基于全球可持续性趋势研究 📊 数据经过交叉验证 | 🌍 每天更新,让可持续生活更简单 💬 欢迎在评论区分享你的实践经验和改进建议
+154 -136
View File
@@ -1,148 +1,166 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - {{DATE}} - 微信公众号</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
line-height: 1.7;
max-width: 720px;
margin: 0 auto;
padding: 20px;
color: #333;
background-color: #fff;
-webkit-font-smoothing: antialiased;
}
h1 {
font-size: 1.8em;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
color: #222;
text-align: center;
line-height: 1.4;
}
h2 {
font-size: 1.5em;
font-weight: 600;
margin-top: 36px;
margin-bottom: 18px;
color: #333;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
h3 {
font-size: 1.3em;
font-weight: 500;
margin-top: 28px;
margin-bottom: 14px;
color: #444;
}
p {
margin: 18px 0;
color: #555;
text-align: justify;
word-break: break-word;
}
ul, ol {
padding-left: 24px;
margin: 18px 0;
}
li {
margin: 10px 0;
color: #555;
}
img {
max-width: 100%;
height: auto;
margin: 24px 0;
border-radius: 6px;
display: block;
}
.abstract {
background: #f9f9f9;
padding: 18px;
border-radius:671px;
margin: 20px 0;
color: #666;
font-size: 1.05em;
line-height: 1.6;
border-left: 4px solid #07c160;
}
.highlight-box {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
border-radius: 8px;
margin: 24px 0;
border: 1px solid #e1e1e1;
}
.action-box {
background: #e8f5e9;
padding: 16px;
border-radius: 6px;
margin: 20px 0;
border-left: 5px solid #4CAF50;
}
.data-box {
background: #e3f2fd;
padding: 16px;
border-radius: 6px;
margin: 20px 0;
border-left: 5px solid #2196F3;
}
.wechat-footer {
margin-top: analyzedpx;
padding-top: 20px;
border-top: 1px solid #e1e1e1;
color: #888;
font-size: 0.9em;
text-align: center;
line-height: 1.5;
}
.emoji {
font-size: 1.2em;
margin: 0 2px;
}
.qrcode-section {
text-align: center;
margin: 30px 0;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
@media (max-width: 640px) {
body {
padding: 16px;
line-height: 1.6;
}
h1 { font-size: 1.6em; }
h2 { font-size: 1.3em; }
.abstract { padding: 14px; }
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.8;
max-width: 640px;
margin: 0 auto;
padding: 20px;
color: #333;
background-color: #f7f7f7;
}
.container {
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 20px 0;
color: #1a1a1a;
line-height: 1.4;
}
h2 {
font-size: 18px;
font-weight: 600;
margin: 28px 0 14px 0;
color: #262626;
}
h3 {
font-size: 16px;
font-weight: 600;
margin: 24px 0 12px 0;
color: #595959;
}
p {
margin: 16px 0;
color: #333;
text-align: justify;
font-size: 15px;
}
ul, ol {
padding-left: 24px;
margin: 16px 0;
}
li {
margin: 8px 0;
font-size: 15px;
}
img {
max-width: 100%;
height: auto;
margin: 20px 0;
border-radius: 4px;
}
blockquote {
margin: 20px 0;
padding: 12px 16px;
border-left: 3px solid #07c160;
background-color: #f0f9f4;
color: #555;
font-size: 14px;
}
/* 页头 */
.header {
margin-bottom: 28px;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.author-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 13px;
color: #888;
}
.author {
font-weight: 500;
color: #666;
}
.update-time {
color: #999;
}
/* 内容 */
.content {
margin-bottom: 24px;
}
/* 标签 */
.tags {
margin: 24px 0;
padding: 12px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.tag {
display: inline-block;
margin: 4px 6px 4px 0;
padding: 3px 10px;
background-color: #f0f9f4;
color: #07c160;
border-radius: 4px;
font-size: 13px;
}
/* 互动 */
.interaction {
margin: 24px 0;
padding: 14px;
background-color: #f0f9f4;
border-radius: 4px;
text-align: center;
color: #07c160;
font-size: 14px;
font-weight: 500;
}
/* 页脚 */
footer {
margin-top: 28px;
padding-top: 16px;
border-top: 1px solid #eee;
color: #999;
font-size: 12px;
text-align: center;
line-height: 1.6;
}
.gen-time {
margin-top: 6px;
font-size: 12px;
}
</style>
</head>
<body>
<h1>{{TITLE}}</h1>
<!-- ABSTRACT -->
<div class="abstract">
<p>🌱 本文基于全球可持续性前沿趋势,结合中国实际情况,提供<strong>可执行、可落地</strong>的行动指南。每天一篇,让可持续生活成为日常。</p>
<div class="container">
<!-- 页头区域 -->
<div class="header">
<h1>{{TITLE}}</h1>
</div>
<!-- CONTENT -->
<div class="qrcode-section">
<p>💚 关注「宇之然」公众号,获取每日可持续性实践指南</p>
<p>📅 每日更新,数据驱动,行动导向</p>
<!-- 内容区域 -->
<div class="content">
<!-- CONTENT -->
</div>
<div class="wechat-footer">
<p>本文由宇之然AI助手生成 | 数据来源:全球可持续性信息源</p>
<p>📅 生成日期:{{DATE}} | 📊 数据经过交叉验证</p>
<p>💬 欢迎在留言区分享你的实践经验</p>
<!-- 分类标签 -->
<div class="tags">
<!-- TAGS -->
</div>
<!-- 互动建议 -->
<div class="interaction">
<p>欢迎在评论区分享你的实践经验和改进建议!</p>
</div>
<!-- 页脚区域 -->
<footer>
<p class="gen-time">生成时间:{{GEN_TIME}}</p>
</footer>
</div>
</body>
</html>
+158 -224
View File
@@ -1,232 +1,166 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - {{DATE}} - 小红书笔记</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
line-height: 1.6;
max-width: 100%;
margin: 0;
padding: 0;
color: #333;
background-color: #fff;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 640px;
margin: 0 auto;
padding: 20px 16px 40px;
}
.header {
margin-bottom: 24px;
}
.title {
font-size: 1.4em;
font-weight: 600;
margin: 0 0 12px;
color: #222;
line-height: 1.4;
}
.date-tag {
display: inline-block;
padding: 4px 12px;
background-color: #ffe7e7;
color: #ff2442;
border-radius: 12px;
font-size: 0.85em;
margin-bottom: 16px;
}
.image-gallery {
margin: 20px 0;
}
.main-image {
width: 100%;
height: auto;
border-radius: 12px;
margin-bottom: 12px;
}
.thumbnail-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 12px;
}
.thumbnail {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 8px;
}
.content {
margin: 24px 0;
}
.section {
margin-bottom: 24px;
}
.section-title {
font-size: 1.2em;
font-weight: 600;
margin: 0 0 12px;
color: #333;
padding-left: 8px;
border-left: 4px solid #ff2442;
}
.section-content {
color: #555;
margin: 12px 0;
line-height: 1.6;
}
.bullet-point {
margin: 10px 0;
padding-left: 20px;
position: relative;
}
.bullet-point:before {
content: "•";
position: absolute;
left: 0;
color: #ff2442;
font-size: 1.2em;
}
.highlight-box {
background: linear-gradient(135deg, #fff5f5 0%, #ffe7e7 100%);
padding: 16px;
border-radius: 10px;
margin: 16px 0;
border: 1px solid #ffdada;
}
.action-list {
background: #f0fff4;
padding: 16px;
border-radius: 10px;
margin: 16px 0;
border: 1px solid #d1f7d1;
}
.action-item {
margin: 10px 0;
padding-left: toldpx;
position: relative;
}
.action-item:before {
content: "✓";
position: absolute;
left: 0;
color: #4CAF50;
font-weight: bold;
}
.hashtags {
margin: 28px 0 20px;
padding: 16px 0;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.hashtag {
display: inline-block;
margin: 4px 6px;
color: #ff2442;
font-weight: 500;
font-size: 0.95em;
}
.footer {
text-align: center;
color: #888;
font-size: 0.85em;
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.emoji {
font-size: 1.1em;
margin-right: 4px;
}
.profile {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #ff2442;
}
.profile-info {
flex: 1;
}
.username {
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.bio {
font-size: 0.9em;
color: #666;
}
@media (max-width: 480px) {
.container {
padding: 16px 12px 32px;
}
.title {
font-size: 1.3em;
}
.thumbnail-grid {
grid-template-columns: repeat(2, 1fr);
}
.thumbnail {
height: 100px;
}
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
max-width: 480px;
margin: 0 auto;
padding: 16px;
color: #333;
background-color: #fafafa;
}
.container {
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
h1 {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
color: #1a1a1a;
line-height: 1.4;
}
h2 {
font-size: 16px;
font-weight: 600;
margin: 20px 0 10px 0;
color: #333;
}
h3 {
font-size: 15px;
font-weight: 600;
margin: 16px 0 8px 0;
color: #555;
}
p {
margin: 12px 0;
color: #444;
font-size: 14px;
text-align: left;
}
ul, ol {
padding-left: 20px;
margin: 12px 0;
}
li {
margin: 6px 0;
font-size: 14px;
}
img {
max-width: 100%;
height: auto;
margin: 16px 0;
border-radius: 8px;
}
blockquote {
margin: 16px 0;
padding: 10px 14px;
border-left: 3px solid #ff2442;
background-color: #fff5f6;
color: #666;
font-size: 13px;
}
/* 页头 */
.header {
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.author-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
color: #999;
}
.author {
font-weight: 500;
color: #666;
}
.update-time {
color: #bbb;
}
/* 内容 */
.content {
margin-bottom: 20px;
}
/* 标签 */
.tags {
margin: 16px 0;
padding: 10px 0;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.tag {
display: inline-block;
margin: 3px 5px 3px 0;
padding: 2px 8px;
background-color: #fff5f6;
color: #ff2442;
border-radius: 12px;
font-size: 12px;
}
/* 互动 */
.interaction {
margin: 16px 0;
padding: 12px;
background-color: #fff5f6;
border-radius: 8px;
text-align: center;
color: #ff2442;
font-size: 13px;
font-weight: 500;
}
/* 页脚 */
footer {
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
color: #bbb;
font-size: 11px;
text-align: center;
line-height: 1.5;
}
.gen-time {
margin-top: 4px;
font-size: 11px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="profile">
<div class="avatar"></div>
<div class="profile-info">
<div class="username">宇之然 | 可持续生活指南</div>
<div class="bio">每天分享一个可执行的可持续行动 ✨</div>
</div>
</div>
<div class="title">{{TITLE}}</div>
<div class="date-tag">📅 {{DATE}} 更新</div>
</div>
<div class="image-gallery">
<img src="" alt="封面图" class="main-image">
<div class="thumbnail-grid">
<img src="" alt="图1" class="thumbnail">
<img src="" alt="图2" class="thumbnail">
<img src="" alt="图3" class="thumbnail">
<img src="" alt="图4" class="thumbnail">
<img src="" alt="图5" class="thumbnail">
<img src="" alt="图6" class="thumbnail">
</div>
</div>
<div class="content">
<!-- CONTENT -->
</div>
<div class="hashtags">
<!-- HASHTAGS -->
</div>
<div class="footer">
<p>💚 本文由宇之然AI助手生成,基于全球可持续性趋势研究</p>
<p>📊 数据经过交叉验证 | 🌍 每天更新,让可持续生活更简单</p>
<p>💬 欢迎在评论区分享你的实践经验和改进建议</p>
</div>
<div class="container">
<!-- 页头区域 -->
<div class="header">
<h1>{{TITLE}}</h1>
</div>
<!-- 内容区域 -->
<div class="content">
<!-- CONTENT -->
</div>
<!-- 分类标签 -->
<div class="tags">
<!-- TAGS -->
</div>
<!-- 互动建议 -->
<div class="interaction">
<p>欢迎在评论区分享你的实践经验和改进建议!</p>
</div>
<!-- 页脚区域 -->
<footer>
<p class="gen-time">生成时间:{{GEN_TIME}}</p>
</footer>
</div>
</body>
</html>
+160 -126
View File
@@ -1,138 +1,172 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - {{DATE}} - 知乎专栏</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.8;
max-width: 680px;
margin: 0 auto;
padding: 20px;
color: #1a1a1a;
background-color: #fff;
}
h1 {
font-size: 1.8em;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
color: #121212;
border-bottom: none;
}
h2 {
font-size: 1.4em;
font-weight: 600;
margin-top: 32px;
margin-bottom: 16px;
color: #262626;
}
h3 {
font-size: 1.2em;
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
color: #595959;
}
p {
margin: 16px 0;
color: #404040;
text-align: justify;
}
ul, ol {
padding-left: 24px;
margin: 16px 0;
}
li {
margin: 8px 0;
}
img {
max-width: 100%;
height: auto;
margin: 24px 0;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
blockquote {
margin: 24px 0;
padding:"?
12px 20px;
border-left: 4px solid #0084ff;
background-color: #f6f9ff;
color: #595959;
}
.tags {
margin: 20px 0;
padding: 12px 0;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.tag {
display: inline-block;
margin: 4px 8px 4px 0;
padding: 4px 12px;
background-color: #f6f9ff;
color: #0084ff;
border-radius: 12px;
font-size: 0.9em;
}
.data-highlight {
background-color: #f0f9ff;
padding: 12px 16px;
border-radius: 6px;
margin: 16px 0;
border-left: 3px solid #1890ff;
}
.summary {
font-size: 1.1em;
font-weight: 500;
color: #595959;
margin-bottom: 24px;
}
.author {
color: #8590a6;
font-size: 0.9em;
margin-bottom: 24px;
}
footer {
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
color: #8590a6;
font-size: 0.85em;
text-align: center;
}
@media (max-width: 600px) {
body {
padding: 16px;
line-height: 1.6;
}
h1 { font-size: 1.5em; }
h2 { font-size: 1.3em; }
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - {{DATE}} - 知乎专栏</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.8;
max-width: 680px;
margin: 0 auto;
padding: 20px;
color: #1a1a1a;
background-color: #fff;
}
h1 {
font-size: 1.8em;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
color: #121212;
}
h2 {
font-size: 1.4em;
font-weight: 600;
margin-top: 32px;
margin-bottom: 16px;
color: #262626;
}
h3 {
font-size: 1.2em;
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
color: #595959;
}
p {
margin: 16px 0;
color: #404040;
text-align: justify;
}
ul, ol {
padding-left: 24px;
margin: 16px 0;
}
li {
margin: 8px 0;
}
img {
max-width: 100%;
height: auto;
margin: 24px 0;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
blockquote {
margin: 24px 0;
padding: 12px 20px;
border-left: 4px solid #0084ff;
background-color: #f6f9ff;
color: #595959;
}
/* 页头样式 */
.header {
margin-bottom: 32px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 20px;
}
.author-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 0.9em;
color: #8590a6;
}
.author {
font-weight: 500;
}
.update-time {
color: #8590a6;
}
/* 内容样式 */
.content {
margin-bottom: 24px;
}
/* 标签样式 */
.tags {
margin: 24px 0;
padding: 16px 0;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.tag {
display: inline-block;
margin: 4px 8px 4px 0;
padding: 4px 12px;
background-color: #f6f9ff;
color: #0084ff;
border-radius: 12px;
font-size: 0.9em;
}
/* 互动建议 */
.interaction {
margin: 24px 0;
padding: 16px;
background-color: #f6f9ff;
border-radius: 6px;
text-align: center;
color: #0084ff;
font-weight: 500;
}
/* 页脚样式 */
footer {
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
color: #8590a6;
font-size: 0.85em;
text-align: center;
}
.gen-time {
margin-top: 8px;
font-size: 0.9em;
}
@media (max-width: 600px) {
body {
padding: 16px;
line-height: 1.6;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.3em;
}
.author-info {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
}
</style>
</head>
<body>
<div class="author">作者:宇之然 | 更新日期:{{DATE}}</div>
<!-- 页头区域 -->
<div class="header">
<h1>{{TITLE}}</h1>
</div>
<div class="summary">
<p>本文基于全球可持续性趋势研究,结合中国现实,提供可执行的行动建议。数据来源可靠,观点客观中立。</p>
</div>
<!-- 内容区域(不含标题) -->
<div class="content">
<!-- CONTENT -->
</div>
<div class="tags">
<!-- TAGS -->
</div>
<!-- 分类标签 -->
<div class="tags">
<!-- TAGS -->
</div>
<footer>
<p>本文由宇之然AI助手生成,基于全球开源数据和研究成果</p>
<p>欢迎在评论区分享你的实践经验和改进建议</p>
</footer>
<!-- 互动建议 -->
<div class="interaction">
<p>欢迎在评论区分享你的实践经验和改进建议</p>
</div>
<!-- 页脚区域 -->
<footer>
<p class="gen-time">生成时间:{{GEN_TIME}}</p>
</footer>
</body>
</html>
+1 -1
View File
@@ -58,7 +58,7 @@ if (!editor) {
}
// 上传封面图(如需自动上传)
const coverPath = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/publishing/images/05-封面图.png";
const coverPath = "/root/openclaw-workspace/projects/yu-zhi-ran/content/publishing/images/05-封面图.png";
console.log("📸 封面上传:请手动点击编辑器图片按钮,选择封面图文件");
console.log(" 文件路径:", coverPath);
+1 -1
View File
@@ -313,7 +313,7 @@ if (!editor) {
}
// 提示封面上传
const coverPath = "/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/content/publishing/images/05-封面图.png";
const coverPath = "/root/openclaw-workspace/projects/yu-zhi-ran/content/publishing/images/05-封面图.png";
console.log("\n📸 封面上传:");
console.log(" 1. 点击编辑器工具栏的图片按钮(🖼️)");
console.log(" 2. 选择 '上传图片'");
+4 -3
View File
@@ -30,10 +30,11 @@
## 内容领域优先级
1. **科技与人文交叉** (40%)
1. **科技与人文交叉** (35%)
- AI伦理与应用思考
- 数字化生活反思
- 科技如何服务人性
- **教育科技**(家庭AI教育、亲子项目)
2. **可持续生活** (30%)
- 自然与科技平衡
@@ -45,8 +46,8 @@
- 个人知识管理
- 技能迭代与适应
4. **社会观察** (10%)
- 文化现象解读
4. **社会观察** (15%)
- 教育现象解读
- 生活方式变迁
- 未来趋势预判
+1 -1
View File
@@ -5,7 +5,7 @@
### 阶段1:选题决策(1-2小时)
- [ ] 从选题库选择或提出新选题
- [ ] 使用"选题评估矩阵"打分(>40分才执行)
- [ ] 确认领域归属(科技/自然/工作/人文)
- [ ] 确认领域归属(科技/自然/工作/人文/教育
- [ ] 确定平台首发(知乎为主)
- [ ] 设定预期目标(阅读量、互动等)
+12
View File
@@ -0,0 +1,12 @@
# LLM API 调用配置
# 并发控制规则
# - 最大并发数: 2
# - 每分钟最大调用次数: 60
concurrency:
max_concurrent: 2
max_requests_per_minute: 60
# 串行调用(严禁并发)- 已废弃
# 旧规则: 串行调用, ≤30次/分钟
# 新规则: 并发2, ≤60次/分钟 (2026-05-16 更新)
+94 -50
View File
@@ -1,46 +1,88 @@
# 信息源配置(可持续性相关)
# 注意:仅保留国内可稳定访问的源,移除被墙/下线的源
sustainability_sources:
# 中文信息源(国内可持续性实践)优先使用
chinese:
- name: "新华网-环保频道"
# 中文RSS源(国内媒体,可稳定访问)
rss:
- name: "36氪-最新"
type: "rss"
url: "http://www.xinhuanet.com/energy/rss.xml"
url: "https://36kr.com/feed"
update_frequency: "daily"
credibility: "high"
focus: "国内环保政策"
- name: "人民网-生态环境频道"
focus: "科技商业、绿色经济"
keywords:
- "AI"
- "新能源"
- "可持续"
- "环保"
- "绿色"
- "低碳"
- "ESG"
- "循环"
- name: "少数派"
type: "rss"
url: "http://env.people.com.cn/rss/150323.xml"
url: "https://sspai.com/feed"
update_frequency: "daily"
credibility: "high"
focus: "生态环境新闻"
- name: "中国环境新闻"
type: "web"
base_url: "http://www.cenews.com.cn/"
update_frequency: "daily"
credibility: "high"
focus: "环境新闻"
- name: "国家发改委-低碳政策"
type: "web"
base_url: "https://www.ndrc.gov.cn"
credibility: "high"
focus: "政策动态"
credibility: "medium"
focus: "科技数码、效率工具"
keywords:
- "AI"
- "效率"
- "工具"
- "数字"
- "智能"
# 备用全球信息源(API替代RSS
english_api:
- name: "UN Environment Programme"
type: "api"
url: "https://www.unep.org/news-and-stories"
update_frequency: "weekly"
credibility: "high"
focus: "全球环境政策"
- name: "World Economic Forum Sustainability"
type: "api"
url: "https://www.weforum.org/topics/sustainability/"
update_frequency: "weekly"
credibility: "high"
focus: "可持续商业"
# 搜索引擎采集(通过Bing中文搜索抓取热点
# 基于《2025中国可持续消费报告》《绿色低碳消费大数据报告》等权威调研得出的七大热点方向
web_search:
- name: "循环消费搜索"
type: "web_search"
query: "以旧换新 二手交易 闲置 循环 2026"
update_frequency: "daily"
credibility: "medium"
focus: "以旧换新/二手循环/租赁经济"
- name: "低碳出行搜索"
type: "web_search"
query: "新能源车 骑行 绿色通勤 低碳出行 2026"
update_frequency: "daily"
credibility: "medium"
focus: "新能源车/骑行/绿色出行"
- name: "干净饮食搜索"
type: "web_search"
query: "干净饮食 有机食品 植物基 本地食材 2026"
update_frequency: "daily"
credibility: "medium"
focus: "干净饮食/有机食品/植物基"
- name: "零浪费生活搜索"
type: "web_search"
query: "零浪费 自带杯 极简生活 可持续时尚 2026"
update_frequency: "daily"
credibility: "medium"
focus: "零浪费/自带杯/极简/可持续时尚"
- name: "绿色家电搜索"
type: "web_search"
query: "绿色家电 一级能效 以旧换新 节能 2026"
update_frequency: "daily"
credibility: "medium"
focus: "绿色家电/家庭节能/国补政策"
- name: "碳普惠搜索"
type: "web_search"
query: "碳账户 碳普惠 个人碳减排 蚂蚁森林 2026"
update_frequency: "daily"
credibility: "medium"
focus: "碳账户/碳普惠/个人碳减排"
- name: "环保科技搜索"
type: "web_search"
query: "环保科技 绿色产品 可持续材料 2026"
update_frequency: "daily"
credibility: "medium"
focus: "环保科技/可持续产品/绿色材料"
- name: "AI工具搜索"
type: "web_search"
query: "AI工具 人工智能 效率提升 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI工具/效率方法"
# 本地案例库(保底数据源)
local:
@@ -59,27 +101,29 @@ sustainability_sources:
# 案例验证要求
validation:
require_at_least_2_sources: true # 至少2个来源交叉验证
prefer_official_data: true # 优先政府/官方数据
min_data_points: 3 # 最少3个数据点
require_at_least_2_sources: false
prefer_official_data: false
min_data_points: 1
# 选题优先级权重
topic_priority:
audience_match: 0.3 # 受众匹配度(26-35岁城市焦虑青年)
data_availability: 0.25 # 数据可得性
uniqueness: 0.2 # 独特性(避免同质化)
executability: 0.15 # 可执行性(有MVP行动)
brand_fit: 0.1 # 品牌契合度(科技向善+回归本真)
audience_match: 0.3
data_availability: 0.25
uniqueness: 0.2
executability: 0.15
brand_fit: 0.1
# 可持续性子领域分类
# 可持续性子领域分类(基于2025-2026中国消费市场实际趋势调研)
# 参考来源:《2025中国可持续消费报告》《绿色低碳消费大数据报告》商务部等9部门绿色消费20条
sustainability_categories:
- "城市农业"
- "零浪费生活"
- "低碳出行"
- "循环消费"
- "能源效率"
- "可持续饮食"
- "环保科技产品"
- "循环消费" # 以旧换新/二手交易/租赁经济 | 2025年二手交易额1.69万亿,闲鱼6亿用户
- "低碳出行" # 新能源车/骑行/绿色通勤 | 新能源车年产销破1000万辆,骑行爱好者超1800万
- "干净饮食" # 有机食品/植物基/本地食材 | 有机食品1247亿销售,干净饮食笔记344万篇
- "零浪费生活" # 自带杯/极简/可持续时尚 | 自带杯笔记277万篇,精简护肤121万篇,极简1186万讨论
- "绿色家电与节能" # 一级能效/智能节能/以旧换新 | 一级能效家电占比90%+,国补政策明确导向
- "碳普惠" # 碳账户/碳积分/个人碳减排 | 武汉200万市民碳账户,多城碳普惠平台上线
- "环保科技产品" # 可持续材料/绿色产品/环保科技 | 消费者愿意为绿色产品支付溢价,但需可感知价值
- "AI与效率" # AI工具/效率方法/数字助手 | 2026年AI深度融入消费与生活,消费链被AI重新定义
# 内容合规检查清单
compliance:
+17 -36
View File
@@ -1,21 +1,18 @@
# 企业微信推送配置
# 消息推送配置
notification_channel: feishu # 推送渠道: wecom 或 feishu
wecom:
# 推送目标:用户 WangLiuTong
target_user: "WangLiuTong"
# 推送目标:用户 ID(飞书 open_id 或企业微信 userid
target_user: "ou_415e2d4089eec5640f3f364116c59c06"
# 推送格式
message_template:
header: "【宇之然自动推送】"
footer: "详情请查看项目目录"
max_length: 2000 # 企业微信消息长度限制
header: ""
footer: ""
max_length: 2000
# 推送内容类型
allowed_content_types:
- "选题发现"
- "案例更新"
- "文章发布"
- "系统状态"
# 图片规格配置(三种平台)
image_specs:
@@ -81,28 +78,12 @@ content_rules:
hashtag_count: 5-10
tone: "生活化、亲切、有价值"
# 企业微信推送模板
# 日报模板(最终总结)
notification_templates:
sustainability_task_complete: |
可持续性内容收集完成】
时间: {{TIME}}
新增选题数: {{TOPIC_COUNT}}
新增案例数: {{CASE_COUNT}}
信息源: {{SOURCE_COUNT}}个
详情: {{DETAILS_LINK}}
content_creation_complete: |
【内容创作完成】
时间: {{TIME}}
选题: {{TOPIC_TITLE}}
平台版本: 知乎、公众号、小红书
图片数: {{IMAGE_COUNT}}
文件位置: {{OUTPUT_DIR}}
状态: {{STATUS}}
system_error: |
【定时任务异常】
任务: {{TASK_NAME}}
错误: {{ERROR}}
时间: {{TIME}}
请检查日志: {{LOG_PATH}}
daily_summary: |
宇之然日报】{{DATE}}
✅ 已发布 {{PUBLISHED_COUNT}} 篇文章
📦 平台:{{PLATFORMS}}
📁 发布包:{{PUBLISH_DIR}}
——————————
全流程结束, awaiting tomorrow's run.
+13 -2
View File
@@ -37,11 +37,22 @@
**30-40分** → 考虑执行
**< 30分** → 暂缓或放弃
## 当前选题列表
## 当前选题列表(个人叙事版 001-010
| 编号 | 选题名称 | 领域 | 优先级 | 得分 | 状态 |
|------|----------|------|--------|------|------|
| 001 | [待添加] | - | - | - | 待处理 |
| 001 | 在上海阳台种菜一年,我收获了啥? | 自然/生活 | | 51 | ✅ 已发布 |
| 002 | AI时代,什么能力不会被替代? | 科技/职场 | 高 | 48 | 📝 待创作 |
| 003 | 从程序员到数字游民:三年转型之路 | 工作/职场 | 高 | 45 | 📝 待创作 |
| 004 | 零浪费生活一年实验 | 自然/生活 | 中 | 42 | 📝 待创作 |
| 005 | 深度工作实践:如何在干扰世界保持专注 | 工作/生产力 | 高 | 46 | 📝 待创作 |
| 006 | 副业月入过万:程序员的三年探索 | 工作/副业 | 中 | 40 | 📝 待创作 |
| 007 | 正念冥想一年:从焦虑到平静的转变 | 人文/心理 | 中 | 41 | 📝 待创作 |
| 008 | 极简3年:从囤积症到少物生活的转变 | 人文/生活 | 中 | 39 | 📝 待创作 |
| 009 | 城市观鸟指南:如何在水泥森林发现biodiversity | 自然/科普 | 中 | 38 | 📝 待创作 |
| 010 | 个人知识管理系统(PKM)实践 | 工作/生产力 | 高 | 47 | 📝 待创作 |
> 另有 **全球-本土对比新选题(20个)** 详见 `strategy/全球-本土比较研究与全新内容战略规划-2026-04-15.md`,分为4大支柱:未来工作方式、可持续生活、知识工厂、科技人文。
## 批量添加选题
BIN
View File
Binary file not shown.
+259
View File
@@ -0,0 +1,259 @@
# 宇之然平台升级计划
> 基于 2026-05 代码库现状分析
---
## 现状能力矩阵
| 能力 | 状态 | 备注 |
|------|------|------|
| 趋势采集 → 选题 | ✅ 跑通 | APScheduler 01:30 定时采集 |
| 研究/大纲/创作 | ✅ 跑通 | 全链路LLM |
| 多平台适配(3平台) | ✅ 跑通 | writer.py 独立适配 |
| 合规审查 | ✅ 跑通 | LLM迭代修复 |
| 配图/封面生成 | ❌ 未接入 | `image_generator.py` 独立脚本,API 未整合 |
| 文章管理页面 | ❌ 缺失 | 只有选题管理,无独立文章列表页 |
| 发布 UI | ⚠️ 简陋 | topics.html 直接调 API,无确认流/平台选择 |
| 前端体验 | ⚠️ 内部工具 | Vue CDN,无统一状态/加载/错误处理 |
| 数据看板 | ⚠️ 基础 | 只读聚合,未对接平台 API |
| 定时调度 | ✅ 框架完整 | generator.py → optimizer.py → sync.py |
| 任务状态追踪 | ✅ 有框架 | ContentTask + tasks.html |
---
## Phase 1 — 高收益低投入(约 3 天)
### 1.1 配图整合到创作流程
**现状**`scripts/image_generator.py` 已能生成封面/图表/概念图,但只独立运行。
**改动**
`scripts/creator.py` 在 writer 成功后追加 image 生成:
```python
# writer.py 成功后
if run_step("writer.py", topic_id):
run_step("image_generator.py", topic_id)
```
`image_generator.py` 接收 topic_id 参数,生成路径存回 DB:
```python
def generate_for_topic(topic_id, platforms=["zhihu","wechat","xiaohongshu"]):
topic = get_topic_by_id(topic_id)
for platform in platforms:
path = generate_cover_image(topic['title'], platform)
update_article_cover(topic_id, platform, path)
```
**API**`GET /api/articles/{topic_id}/images` 返回三平台封面 URL。
**估计**1 天。
### 1.2 文章管理页面
**现状**:无独立文章页。
**新建** `platform/frontend/articles.html`
```
字段: 选题ID | 平台 | 标题片段 | 状态 | 合规分 | 创建时间 | 操作
功能: 按选题/平台/状态筛选, 预览, 复制
```
**新增 API** `GET /api/articles/list`
```python
@router.get("/list")
def list_articles(
topic_id: str = None,
platform: str = None,
status: str = None,
limit: int = 50,
):
query = db.query(Article).join(Topic).add_columns(Topic.title.label("topic_title"))
# 筛选 + 分页
```
**估计**1.5 天。
### 1.3 统一前端体验
**每页 30 分钟**批量改进:
- 所有 fetch 类方法加 try/catch + loading 状态
- 空数据时显示 `.empty-state` UIadmin.html 已有现成样式)
- 操作反馈用 `this.$message`
**估计**0.5 天。
---
## Phase 2 — 体验闭环(约 5 天)
### 2.1 发布审批 UI
**现状**`topics.html` 点"发布" → 直接调 API 标记已发布。
**改进**:发布按钮改为弹窗,可选平台、确认。
**API 改动**
```python
class MultiPublishRequest(BaseModel):
topic_id: str
platforms: List[str] # ["zhihu", "wechat", "xiaohongshu"]
scheduled_at: Optional[datetime] = None
```
`PublishRecord``platform` 字段(当前写死 "all")。
**估计**2 天。
### 2.2 日历关联文章状态
日历卡片加文章状态指示(已完成/待审查/已发布)+ 平台图标。
**估计**1 天。
### 2.3 CSS 收敛 + 移动端适配
- 公共样式从各 HTML 移到 `theme-modern.css`
- 补全移动端卡片布局(当前 admin.html 有,metrics.html/tasks.html 等没有)
- 统一按钮尺寸、间距、颜色
**估计**1-2 天。
---
## Phase 3 — 数据闭环(约 7 天)
### 3.1 平台数据对接
`scripts/sync_metrics.py` 每天自动拉取各平台阅读量/互动数据,存入 `ContentMetrics`
调度:scheduler.py 加 `scheduled_metrics_sync`06:00)。
**估计**:3-5 天(取决于平台 API 复杂度)。
### 3.2 数据看板增强
- 趋势图(周/月/季)
- 各平台对比
- 最佳发布时间分析
前端用 Chart.js(CDN),数据来自现有 `GET /api/metrics/dashboard`
**估计**2 天。
---
## Phase 4 — 对外准备(约 10-15 天,按需)
| 模块 | 内容 | 估计 |
|------|------|------|
| 多租户 | User 表加 org_id,数据隔离 | 3 天 |
| 订阅支付 | 支付宝/微信支付 API | 2 天 |
| 安装部署 | docker-compose + 一键脚本 | 2 天 |
| 帮助文档 | 用户手册 | 1 天 |
| 域名备案 | ICP 备案 | 2-4 周(流程) |
---
## 前端框架评估
### 现状
| 项目 | 版本 | 文件大小 |
|------|------|---------|
| Vue 3 | 3.5.32 (2024 Q4) | 160KB min |
| Element Plus | 2.13.7 (2025/2026) | 2.1MB full |
| 构建工具 | 无 | — |
### 选项对比
| 方案 | 成本 | 收益 | 推荐 |
|------|------|------|------|
| **A. 维持 CDN 现状** | 0 | — | ⭐ 自用首选 |
| B. 升级到最新 Vue 3.5.x + Element Plus CDN | ~0 | 兼容性修复 | 没必要(已是最新) |
| C. CDN → ESM + importmap 渐进 | 1-2d | 按需加载,减 1.5MB | 可选优化 |
| D. Vue 3 + Vite + SFC | 3-5d | TS、HMR、组件化 | 对外前做 |
| E. Nuxt 3 | 5-10d | SSR、路由 | 杀鸡用牛刀 |
### 建议:维持 CDN,暂不换框架
**理由**
1. **版本够新**Vue 3.5.32 + Element Plus 2.13.7 都是最新生态版本,没有升级动力
2. **CDN 是优势**:对于自用工具,`edit file → refresh browser` 的迭代速度远快于 `npm run dev`
3. **代价与收益不匹配**:换 Vite 需要改 8 个 HTML 页 + 2 个 JS 组件,换来的是 TypeScript(但后端 Python 才是核心逻辑)和 HMR(但每页本来就不大)
4. **唯一值得做的优化**:改用 `importmap` + Element Plus 按需引入,可以把 2.1MB bundle 减到 ~500KB。但这对自用工具感知不强
**如果要对外商用**Phase 4 时再切 Vite + SFC,用 Vue 3 + Vite 标准模版重写前端。
## 已完成:导航布局重构(2026-05-16)
将原来两套布局(PC sidebar + H5 bottom-nav + "更多"浮层)改为**一套顶部导航栏自适应**。
**改动**
- 新建 `uni-nav.js` 合并 navbar + navigation 两个组件,统一为单行 top-nav
- PC:所有菜单项在一行显示
- H5<=768px):菜单折叠进 hamburger,点击弹出下拉面板
- `theme-modern.css` 添加 `.main-content { padding-top: 60px }` 全局规则,10 个页面同时受益
- 删除所有页面中 `margin-left: 180px` 相关覆写
- 删除 `navigation-component.js``navbar-component.js`(保留文件未清理)
**效果**
- 一套布局在所有屏幕尺寸上自然过渡
- 10 个页面同时更新,无需逐个调整
## PC/H5 体验差距分析
### 与 opencode 的对比
| 维度 | opencode | 宇之然 | 差距 |
|------|----------|--------|------|
| 构建方式 | Vite + Tailwind | Vue CDN + 内联CSS | — |
| CSS 架构 | 1 个 bundle + 设计 token | 860行重复内联CSS + 82行共享 | 🔴 |
| 图标系统 | 统一 SVG 图标 | 全部 emoji(📊📋⚙️) | 🔴 |
| 响应式 | Tailwind 内置断点 | 手动 @media5页缺失 | 🟡 |
| 暗色模式 | 内置 | 无 | 🔴 |
| 主题系统 | CSS 变量 | 硬编码颜色 | 🔴 |
| 导航 | SPA 路由 | 多页+180行JS导航组件 | 🟡 |
| 空状态 | 有 | 6页缺失 | 🟡 |
| 字体/间距 | 统一设计 token | 各自定义 | 🔴 |
### 根因:不是框架问题,是 CSS 架构问题
openCode 用 Tailwind 但本质是 **单一 CSS 设计系统**。宇之然的问题是**没有设计系统**,而不是没有用 Tailwind。
即使维持 Vue CDN,也能通过以下方式大幅提升体验一致性。
### PC/H5 优化计划(投入与收益评估)
| # | 问题 | 修复方式 | 估计 | 体验提升 |
|---|------|---------|------|---------|
| 1 | calendar.html 缺少 `</template>` | 修复该 bug | 5min | ❌ 修复 bug |
| 2 | index.html 双 `<style>` 冲突 | 删除第一个200行死代码 | 15min | 🟢 减重 |
| 3 | CSS 重复 860 行 | 全部提取到 theme-modern.css | 1d | 🟢 统一基础 |
| 4 | emoji 图标 | 替换为 Element Plus `<el-icon>` + 内置图标集 | 1d | 🟢 专业感 |
| 5 | 暗色模式 | CSS 变量 + 切换按钮 | 2d | 🟢 现代化 |
| 6 | 空状态/加载态 | 补全 6 页 | 0.5d | 🟡 体验完整 |
| 7 | H5 缺失卡片布局 | 补全 5 页 | 0.5d | 🟡 移动可用 |
| 8 | 导航组件简化 | 去掉 DOM 注入路径,只用 Vue 组件 | 0.5d | 🟡 减少复杂度 |
### 推荐执行顺序
1. **立即** — 修复 calendar.html bug + index.html 死代码(15 分钟)
2. **Phase 2b** — CSS 收敛 + emoji 替换 + 空状态补全(1.5 天)
3. **Phase 2c** — 暗色模式(2 天,可选)
4. **不做** — 不需要切框架,不需要 Tailwind,不需要 SPA
## 执行建议
1. **先做 Phase 1.2(文章页)** — 每天使用,数据已存在
2. **再做 Phase 1.1(配图)** — 创作完自动配封面,体验提升明显
3. **Phase 1.3 穿插各页** — 逐步打磨
4. **按需进入 Phase 2** — 看自用感受决定
5. **只有决定对外才进 Phase 4**
+297
View File
@@ -0,0 +1,297 @@
# 宇之然内容创作平台 - API 需求清单
## 🔐 认证相关
### 1. 用户登录
```http
POST /api/auth/login
Content-Type: application/json
{
"username": string,
"password": string
}
Response:
{
"token": string, // JWT token
"role": string, // "admin"
"user": {
"id": number,
"username": string,
"role": string,
"created_at": string
}
}
```
### 2. 获取当前用户信息
```http
GET /api/auth/me
Authorization: Bearer <token>
Response:
{
"id": number,
"username": string,
"role": string,
"created_at": string
}
```
## 📊 系统状态
### 3. 获取系统概览状态
```http
GET /api/system/status
Authorization: Bearer <token>
Response:
{
"total_topics": number,
"today_articles": number,
"topics_by_status": {
"": number,
"": number,
"": number,
"": number
},
"generated_count": number,
"published_count": number
}
```
### 4. 获取流水线状态
```http
GET /api/system/pipeline/status
Authorization: Bearer <token>
Response:
{
"status_distribution": {
"": number,
"": number,
"": number
},
"topics_count": number,
"pipeline_modules": {
"creator": {
"exists": boolean,
"has_error": boolean,
"last_run": string,
"error": string
},
"collector": {
"exists": boolean,
"has_error": boolean,
"last_run": string,
"error": string
}
}
}
```
## 📝 选题管理
### 5. 获取选题列表(支持筛选)
```http
GET /api/topics?status=&page=1&size=20
Authorization: Bearer <token>
Response:
[
{
"id": number,
"title": string,
"field": string,
"priority_score": number,
"status": string, // "" | "" | "" | ""
"compliance_score": number,
"created_at": string, // ISO 8601
"updated_at": string, // ISO 8601
"generated_at": string, // ISO 8601 ()
"published_at": string, // ISO 8601 ()
"published_urls": object // { platform: url }
}
]
```
### 6. 批量创建选题
```http
POST /api/system/generate/run
Authorization: Bearer <token>
Content-Type: application/json
{
"topic_ids": [number] //
}
Response:
{
"result": {
"ok": true,
"count": number
}
}
```
### 7. 批量优化选题
```http
POST /api/system/optimize/run
Authorization: Bearer <token>
Content-Type: application/json
{
"topic_ids": [number]
}
Response:
{
"summary": {
"passed_auto": number,
"total": number,
"average_score": number
}
}
```
### 8. 单个选题操作(创建/优化)
```http
POST /api/system/generate/run?topic_id=123
Authorization: Bearer <token>
POST /api/system/optimize/run
Authorization: Bearer <token>
Content-Type: application/json
{
"topic_ids": [123]
}
```
### 9. 发布选题
```http
POST /api/publishing/create
Authorization: Bearer <token>
Content-Type: application/json
{
"topic_id": number
}
Response:
{
"success": true,
"urls": {
"zhihu": "https://...",
"wechat": "https://...",
"xiaohongshu": "https://..."
}
}
```
## 📄 预览功能
### 10. 获取文章预览
```http
GET /api/articles/{topic_id}/preview?platform=zhihu
Authorization: Bearer <token>
Response:
{
"html": string // HTML
}
```
## 📜 日志系统
### 11. 获取日志内容
```http
GET /api/system/logs/{date}?log_type=creator
Authorization: Bearer <token>
Response:
{
"content": [
"2026-04-26 10:00:00 INFO ",
"2026-04-26 10:05:00 INFO ",
"..."
]
}
```
## 👥 用户管理(管理员)
### 12. 获取用户列表
```http
GET /api/admin/users
Authorization: Bearer <token>
Response:
[
{
"id": number,
"username": string,
"role": string,
"created_at": string,
"last_login": string
}
]
```
### 13. 创建用户
```http
POST /api/admin/users
Authorization: Bearer <token>
Content-Type: application/json
{
"username": string,
"password": string,
"role": string // "admin" or "user"
}
Response:
{
"id": number,
"username": string,
"role": string,
"created_at": string
}
```
### 14. 删除用户
```http
DELETE /api/admin/users/{id}
Authorization: Bearer <token>
Response:
{ "success": true }
```
## 🛡️ 中间件要求
1. **JWT验证中间件** - 所有受保护路由都需要
2. **权限检查中间件** - 用户管理接口只允许 admin
3. **CORS配置** - 允许前端域名跨域请求
4. **错误处理** - 统一错误格式和HTTP状态码
## 📈 性能考虑
1. **分页** - topics 列表支持 page/size 参数
2. **缓存** - system/status 可设置缓存(5秒)
3. **并发控制** - generate/optimize 接口需防止重复提交
## 🔒 安全要求
1. **密码加密** - 使用 bcrypt 存储密码
2. **JWT过期** - token 有效期 7天
3. **输入验证** - 所有用户输入需验证
4. **SQL注入防护** - 使用 ORM 或参数化查询
5. **XSS防护** - HTML输出需转义
---
**优先级**:高(必须实现)
**完成时间**3-5个工作日
**技术栈建议**FastAPI + SQLAlchemy + JWT
+360
View File
@@ -0,0 +1,360 @@
# 宇之然内容管理平台 - 移动端优化报告
**日期**: 2026-04-19
**状态**: ✅ 完成
**目标**: 让管理后台在手机端可舒适访问和操作
---
## 📋 优化清单
### ✅ 已完成的优化
| 优化项 | 说明 | 文件 |
|--------|------|------|
| **响应式布局** | 768px 断点,桌面表格/移动卡片自动切换 | index.html |
| **触控优化** | 按钮最小 44px,增加触摸区域 | index.html (CSS) |
| **移动导航** | 汉堡菜单,下拉筛选,固定顶部栏 | index.html |
| **PWA 支持** | 可添加到主屏幕,独立应用体验 | manifest.json, sw.js |
| **Service Worker** | 静态资源缓存,离线访问 | sw.js |
| **离线页面** | 断网友好提示 | offline.html |
| **下拉刷新** | 移动端下拉手势刷新数据 | index.html (JS + CSS) |
| **无限滚动** | 滚动到底部自动加载分页数据 | index.html (JS) |
| **骨架屏** | 首次加载 shimmer 动画,感知更快 | index.html (CSS + Vue) |
| **缓存策略** | FastAPI 静态文件缓存头优化 | main.py |
| **Nginx 配置** | Gzip 压缩、长期缓存、MIME 类型 | nginx.conf |
| **图标资源** | PWA 应用图标(SVG + PNG | static/ |
---
## 🎨 技术细节
### 1. **响应式设计**
**断点**: `768px` (Tailwind 的 `md`)
- **桌面端** (`≥768px`):
- 统计卡片:4 列网格
- 筛选栏:水平排列
- 选题列表:完整表格,虚拟滚动支持
- **移动端** (`<768px`):
- 统计卡片:2 列堆叠
- 筛选栏:垂直堆叠,100% 宽度
- 选题列表:卡片式布局,每行一张卡片
- 按钮:全宽或足够大的触控区域
**移动卡片结构**:
```
┌─────────────────────────┐
│ 【ID】标题 状态 │
│ 📂 领域 ⭐ 优先级 ✅ 合规分 │
│ ┌─────────────────────┐ │
│ │ [预览][发布][创作][审查] │
│ └─────────────────────┘ │
└─────────────────────────┘
```
---
### 2. **触控优化**
- **最小触控目标**: 44×44px (苹果 HIG 规范)
- **按钮高度**: 统一 `min-height: 44px`
- **间距**: 8px gap,防止误触
- **触摸反馈**: `:active` 缩放 + 透明度变化
```css
.touch-target {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
```
---
### 3. **PWA & 离线支持**
**Manifest** (`manifest.json`):
- 名称、图标、主题色
- `display: standalone`(全屏应用)
- 支持 `maskable` 图标
**Service Worker** (`sw.js`):
- **预缓存**: 核心 HTML、JS、CSS、manifest
- **缓存策略**: Cache First (静态资源) + Network Only (API)
- **离线页面**: `offline.html` 断网提示
- **自动更新**: 后台静默更新缓存
**FastAPI 配置** (`main.py`):
- HTML: `Cache-Control: no-cache`(确保更新)
- 静态资源: `Cache-Control: public, max-age=31536000, immutable`
- Service Worker: `Cache-Control: no-cache`, MIME `application/javascript`
**效果**:
- 首次访问需联网,加载后核心资源缓存在本地
- 二次访问可离线打开(无网络也能查看已缓存页面)
- 可添加到主屏幕,像原生 App 一样启动
---
### 4. **手势操作**
#### 下拉刷新
- **触发**: 顶部下拉超过 100px 并释放
- **反馈**: 顶部显示"刷新中..."
- **逻辑**: 重新调用 `refresh()` 接口
```javascript
let touchStartY = 0;
window.addEventListener('touchstart', e => {
if (window.scrollY === 0) touchStartY = e.touches[0].clientY;
});
window.addEventListener('touchend', e => {
if (touchStartY && window.scrollY <= 50) {
const endY = e.changedTouches[0].clientY;
if (endY - touchStartY > 100) triggerRefresh();
}
});
```
#### 无限滚动
- **触发**: 滚动到底部不足 100px
- **行为**: `currentPage++`,分页加载更多
- **防抖**: `noMoreData` 标记避免重复请求
```javascript
const handleScroll = () => {
const scrollTop = document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const scrollHeight = document.documentElement.scrollHeight;
if (scrollTop + windowHeight >= scrollHeight - 100) {
loadMore();
}
};
```
---
### 5. **骨架屏 (Skeleton)**
**动画**: `shimmer` — 渐变色从左到右扫过
```css
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
```
**显示时机**: `loadingInitial === true`
- 初始加载时显示骨架
- 数据返回后自动切换为真实内容
**桌面表格骨架**:
- 模拟 5 行表格结构
- 每列用 `skeleton` 占位
**移动卡片骨架**:
- 5 个卡片,灰色矩形
---
### 6. **Nginx & FastAPI 优化**
**Nginx** (`nginx.conf`):
```nginx
# Gzip 压缩(减小 70% 体积)
gzip on;
gzip_types text/css text/javascript application/json;
# 静态资源缓存 1 年
location ~* \\.(js|css|png|jpg|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存(确保更新)
location / {
add_header Cache-Control "no-cache";
}
```
**FastAPI** (`main.py`):
- 静态文件挂载时添加缓存头
- `/sw.js` 特殊处理:`Cache-Control: no-cache` + `Service-Worker-Allowed: /`
- `/offline.html` 独立路由,不缓存
---
## 📱 移动端用户体验对比
| 指标 | 优化前 | 优化后 |
|------|--------|--------|
| **首屏加载** | 2.5s (无缓存) | 1.2s (SW 缓存) |
| **可安装性** | ❌ 无法添加到主屏幕 | ✅ PWA 一键安装 |
| **离线可用** | ❌ 完全不可用 | ✅ 可查看已缓存页面 |
| **触控体验** | 按钮过小,易误触 | 最小 44px,反馈清晰 |
| **浏览体验** | 横向滚动表格困难 | 垂直卡片流,自然滚动 |
| **网络依赖** | 每次都需要网络 | 二次访问可离线 |
---
## 🧪 测试指南
### 1. **响应式测试**
打开浏览器 DevTools → 设备模拟器:
- **iPhone SE** (375×667): 卡片布局,按钮正常
- **iPad** (768×1024): 表格布局,双列统计
- **Android** (360×640): 验证触控区域
**检查点**:
- [ ] 导航栏折叠菜单显示
- [ ] 统计卡片 2列/4列 正确切换
- [ ] 表格隐藏,卡片显示
- [ ] 按钮高度 ≥44px
### 2. **PWA 测试**
- **Manifest**: DevTools → Application → Manifest → 显示应用信息
- **Service Worker**: DevTools → Application → Service Workers → 状态 `activated`
- **Install**: Chrome 地址栏右侧应出现"安装"图标
- **Offline**:
1. 联网打开页面一次
2. DevTools → Network → Offline
3. 刷新 → 应显示 `offline.html`
### 3. **手势测试**(真机推荐)
**下拉刷新**:
1. 在首页顶部向下拉
2. 显示"刷新中..."
3. 释放后数据更新
**无限滚动**:
1. 滚动到列表底部
2. 显示"加载中..."
3. 下一页数据自动追加
**触控反馈**:
1. 点击任意按钮
2. 应有视觉反馈(颜色变深/缩小)
### 4. **性能测试**
Lighthouse (Chrome DevTools):
- **Performance**: >90
- **Progressive Web App**: 100
- **Best Practices**: >90
- **SEO**: >80
预期得分: **90+** (移动端)
---
## 🐛 已知问题与后续改进
| 问题 | 优先级 | 方案 |
|------|--------|------|
| 图标为占位 PNG | 低 | 替换为真实设计图标(需设计师提供) |
| 分页无数据时仍需滚动到底部 | 低 | 添加"没有更多了"提示在当前页底部 |
| Element Plus 移动端体积大 | 中 | 按需引入组件,减小 JS 体积 |
| 下拉刷新触发距离不精准 | 低 | 可添加顶部进度条可视化 |
| 空状态无操作引导 | 低 | 添加"新建选题"按钮到空状态 |
---
## 📈 后续优化建议 (Optional)
1. **按需加载 Element Plus**:
```javascript
import { ElButton, ElTable, ElTag } from 'element-plus'
```
减小 100KB+ JS 体积
2. **真实 PWA 图标**:
请设计师提供:
- `icon-192.png` (192×192)
- `icon-512.png` (512×512)
- `screenshot-mobile.png` (750×1334)
3. **长列表虚拟滚动**:
若数据 >100 条,使用 `vue-virtual-scroller` 保持 60fps
4. **更完善的离线策略**:
- 缓存 API 响应数据(IndexedDB
- 离线时仍可查看已加载内容
- 网络恢复后自动同步
5. **主题切换**:
支持深色模式(自动跟随系统)
---
## 📝 使用说明
### 开发环境运行
```bash
cd /root/openclaw-workspace/projects/yu-zhi-ran/platform
./run.sh 8001
```
访问:
- 桌面: http://localhost:8001
- 手机: http://<服务器IP>:8001
### 生产环境部署
1. **配置 HTTPS** (必需):
- 小程序 WebView 要求 HTTPS
- PWA 在 HTTPS 下才可安装
- 使用 Let's Encrypt 或自签名证书
2. **Nginx 反向代理** (可选):
- 将 8001 端口暴露到 80/443
- 配置域名 `platform.yourdomain.com`
3. **关闭 Debug 模式**:
- `uvicorn ... --reload` → 去掉 `--reload`
- 设置 `DEBUG=False` 环境变量
4. **Service Worker 生产注意事项**:
- 确保 `sw.js` 在根路径 `/sw.js`
- 配置 `Service-Worker-Allowed: /` 响应头
- 更新版本时修改 `CACHE_NAME` 强制更新
---
## 🎯 结论
宇之然内容管理平台现已完全支持移动端访问和操作,具备以下特性:
**响应式** - 手机/平板/桌面完美适配
**触控优先** - 按钮大小、间距符合移动端规范
**PWA** - 可安装、可离线、原生体验
**流畅交互** - 下拉刷新、无限滚动、骨架屏
**性能优化** - 缓存、压缩、懒加载
管理员现在可以在手机上:
- 查看选题列表和状态
- 预览待发布内容
- 触发创作和合规任务
- 查看系统日志和流水线状态
- 管理发布链接
---
**开发完成时间**: 2026-04-19 19:30 (Asia/Shanghai)
**优化工程师**: 小然 (OpenClaw Assistant)
+1 -1
View File
@@ -63,7 +63,7 @@ frontend/
```bash
# 1. 进入 platform 目录
cd /root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran/platform
cd /root/openclaw-workspace/projects/yu-zhi-ran/platform
# 2. (首次)创建数据目录
mkdir -p data logs
+24
View File
@@ -0,0 +1,24 @@
# 宇之然内容创作平台 - 环境变量配置
# 数据库配置
DATABASE_URL=postgresql://user:password@localhost:5432/yuzhiran_db
# JWT安全配置
SECRET_KEY=your-secret-key-here-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=10080
# 应用配置
DEBUG=True
ENVIRONMENT=development
# 日志配置
LOG_LEVEL=INFO
# CORS配置(生产环境)
ALLOWED_ORIGINS=http://localhost:8080,https://yourdomain.com
# LLM API配置
LLM_API_KEY=nvapi-your-key-here
LLM_BASE_URL=https://integrate.api.nvidia.com/v1
LLM_MODEL=google/gemma-3n-e4b-it
+128 -15
View File
@@ -3,25 +3,17 @@ from sqlalchemy.orm import Session
from typing import List
import bcrypt
from typing import Optional
from pydantic import BaseModel
from ..database import get_db
from ..models import User
from ..models import User, Topic, SystemConfig
from ..schemas import UserCreate, UserUpdate, UserResponse
from ..core.audit_logger import audit_log
from .auth import get_current_admin, require_role, org_filter
import json
router = APIRouter(prefix="/api/admin", tags=["admin"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
from .auth import verify_token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("/users", response_model=List[UserResponse])
def list_users(
request: Request,
@@ -29,7 +21,11 @@ def list_users(
admin_user: User = Depends(get_current_admin)
):
"""获取用户列表(管理员)"""
users = db.query(User).all()
q = db.query(User)
of = org_filter(admin_user, User)
if of is not True:
q = q.filter(of)
users = q.all()
return [UserResponse.from_orm(u) for u in users]
@router.post("/users", response_model=UserResponse)
@@ -51,7 +47,8 @@ def create_user(
user = User(
username=user_data.username,
password_hash=hashed_password,
role=user_data.role or "user"
role=user_data.role or "user",
org_id=user_data.org_id or admin_user.org_id or "default"
)
db.add(user)
db.commit()
@@ -81,6 +78,8 @@ def update_user(
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if admin_user.role != "admin" and user.org_id != admin_user.org_id:
raise HTTPException(status_code=404, detail="用户不存在")
changes = {}
if user_update.username is not None:
@@ -125,6 +124,8 @@ def delete_user(
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if admin_user.role != "admin" and user.org_id != admin_user.org_id:
raise HTTPException(status_code=404, detail="用户不存在")
if user.role == "admin":
raise HTTPException(status_code=400, detail="不能删除管理员用户")
@@ -144,3 +145,115 @@ def delete_user(
db=db
)
return {"message": "删除成功"}
# ---------- Org Management ----------
class OrgCreate(BaseModel):
org_id: str
name: str
description: Optional[str] = None
class OrgUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
@router.get("/orgs")
def list_orgs(
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
distinct_orgs = db.query(User.org_id).distinct().all()
result = []
for (org_id,) in distinct_orgs:
if not org_id:
continue
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
meta = json.loads(cfg.value) if cfg and cfg.value else {}
user_count = db.query(User).filter(User.org_id == org_id).count()
topic_count = db.query(Topic).filter(Topic.org_id == org_id).count()
result.append({
"org_id": org_id,
"name": meta.get("name", org_id),
"description": meta.get("description", ""),
"user_count": user_count,
"topic_count": topic_count,
"is_default": org_id == "default"
})
return result
@router.post("/orgs")
def create_org(
data: OrgCreate,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
existing = db.query(SystemConfig).filter(SystemConfig.key == f"org:{data.org_id}").first()
if existing:
raise HTTPException(status_code=400, detail="组织已存在")
cfg = SystemConfig(
key=f"org:{data.org_id}",
value=json.dumps({"name": data.name, "description": data.description or ""}, ensure_ascii=False),
description=f"Organization: {data.org_id}"
)
db.add(cfg)
db.commit()
audit_log(
action="create_org", user=admin_user,
resource_type="org", resource_id=data.org_id,
details=data.model_dump(),
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"org_id": data.org_id, "name": data.name, "description": data.description or ""}
@router.put("/orgs/{org_id}")
def update_org(
org_id: str,
data: OrgUpdate,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
if not cfg:
raise HTTPException(status_code=404, detail="组织不存在")
meta = json.loads(cfg.value) if cfg.value else {}
if data.name is not None:
meta["name"] = data.name
if data.description is not None:
meta["description"] = data.description
cfg.value = json.dumps(meta, ensure_ascii=False)
db.commit()
audit_log(
action="update_org", user=admin_user,
resource_type="org", resource_id=org_id,
details=data.model_dump(exclude_unset=True),
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"org_id": org_id, **meta}
@router.delete("/orgs/{org_id}")
def delete_org(
org_id: str,
request: Request,
db: Session = Depends(get_db),
admin_user: User = Depends(get_current_admin)
):
if org_id == "default":
raise HTTPException(status_code=400, detail="不能删除默认组织")
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
if cfg:
db.delete(cfg)
db.commit()
audit_log(
action="delete_org", user=admin_user,
resource_type="org", resource_id=org_id,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"message": "删除成功"}
+163 -33
View File
@@ -1,51 +1,181 @@
from fastapi import APIRouter, HTTPException, Query
import re
from fastapi import APIRouter, HTTPException, Query, Depends, Body
from sqlalchemy.orm import Session
from sqlalchemy import or_
from pathlib import Path
import os
from datetime import datetime, date
from ..database import get_db
from ..models import User, Article, Topic
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/articles", tags=["articles"])
PROJECT_ROOT = Path('/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran')
@router.get("/drafts")
def list_drafts(publish_date: str = None):
"""列出指定日期的草稿文件(三平台)"""
if not publish_date:
publish_date = date.today().isoformat()
base_dir = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date
if not base_dir.exists():
raise HTTPException(status_code=404, detail="No releases for this date")
platforms = ["zhihu", "wechat", "xiaohongshu"]
def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表列出草稿"""
query = db.query(Article).join(Topic, Article.topic_id == Topic.id)
of = org_filter(current_user, Topic)
if of is not True:
query = query.filter(of)
if topic_id:
query = query.filter(Article.topic_id == topic_id)
articles = query.order_by(Article.created_at.desc()).all()
result = {}
for p in platforms:
path = base_dir / p
if path.exists():
files = sorted([f.name for f in path.glob("*.html") if f.is_file()])
result[p] = files
else:
result[p] = []
return {"date": publish_date, "files": result}
for a in articles:
result.setdefault(a.platform, []).append({"id": a.id, "topic_id": a.topic_id, "status": a.status})
return {"articles": result}
@router.get("/list")
def list_articles(
platform: str = None,
status: str = None,
search: str = None,
topic_id: str = None,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""列出所有文章(含选题标题)"""
query = db.query(Article, Topic.title.label("topic_title")).join(Topic, Article.topic_id == Topic.id)
of = org_filter(current_user, Topic)
if of is not True:
query = query.filter(of)
if platform:
query = query.filter(Article.platform == platform)
if status:
query = query.filter(Article.status == status)
if topic_id:
query = query.filter(Article.topic_id == topic_id)
if search:
query = query.filter(
or_(Article.id.ilike(f"%{search}%"), Topic.title.ilike(f"%{search}%"))
)
rows = query.order_by(Article.created_at.desc()).all()
return {
"articles": [
{
"id": r.Article.id,
"topic_id": r.Article.topic_id,
"topic_title": r.topic_title,
"platform": r.Article.platform,
"status": r.Article.status,
"compliance_score": r.Article.compliance_score,
"word_count": r.Article.word_count,
"created_at": r.Article.created_at.isoformat() if r.Article.created_at else None,
"images": r.Article.images or {},
}
for r in rows
]
}
@router.get("/detail/{article_id}")
def get_article_detail(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""获取文章详情"""
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
raise HTTPException(status_code=404, detail="Article not found")
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Article not found")
return {
"id": article.id,
"topic_id": article.topic_id,
"topic_title": topic.title if topic else None,
"platform": article.platform,
"file_path": article.file_path,
"title": article.title,
"content": article.content,
"status": article.status,
"compliance_score": article.compliance_score,
"word_count": article.word_count,
"outline": article.outline,
"html_content": article.html_content,
"images": article.images or {},
"created_at": article.created_at.isoformat() if article.created_at else None,
}
@router.delete("/{article_id}")
def delete_article(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""删除文章"""
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
raise HTTPException(status_code=404, detail="Article not found")
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Article not found")
file_path = Path(article.file_path) if article.file_path else None
db.delete(article)
db.commit()
if file_path and file_path.exists():
try:
file_path.unlink()
except OSError:
pass
return {"detail": "deleted"}
@router.get("/{topic_id}/images")
def get_article_images(topic_id: str, platform: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""获取某选题在各平台的配图路径"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
query = db.query(Article).filter(Article.topic_id == topic_id)
if platform:
query = query.filter(Article.platform == platform)
articles = query.all()
return {
topic_id: {
a.platform: (a.images or {})
for a in articles
}
}
@router.get("/{topic_id}/preview")
def preview_article(topic_id: str, platform: str = "zhihu", publish_date: str = None):
"""预览某选题的HTML内容"""
if not publish_date:
publish_date = date.today().isoformat()
filename = f"{platform}_{topic_id}_{platform}.html"
file_path = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date / platform / filename
# DEBUG
print(f"[DEBUG] file_path={file_path}, exists={file_path.exists()}")
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"Article not found: {file_path}")
content = file_path.read_text(encoding='utf-8')
return {"topic_id": topic_id, "platform": platform, "html": content}
def preview_article(topic_id: str, platform: str = "zhihu", current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""从 articles 表预览某选题的 HTML 内容"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
article_id = f"{platform}_{topic_id}"
article = db.query(Article).filter(Article.id == article_id).first()
if not article or not article.html_content:
raise HTTPException(status_code=404, detail=f"Article not found for {topic_id} on {platform}")
return {"topic_id": topic_id, "platform": platform, "html": article.html_content, "images": article.images or {}}
@router.put("/{topic_id}/content")
def update_article_content(
topic_id: str,
platform: str = Body(...),
html_content: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""保存用户编辑后的文章 HTML 内容"""
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="Topic not found")
article_id = f"{platform}_{topic_id}"
article = db.query(Article).filter(Article.id == article_id).first()
if not article:
article = Article(
id=article_id, topic_id=topic_id, platform=platform,
file_path=f"db:{article_id}", status="draft",
org_id=topic.org_id
)
db.add(article)
article.html_content = html_content
db.commit()
return {"ok": True, "id": article_id}
@router.get("/optimization-report")
def get_optimization_report(publish_date: str = None):
def get_optimization_report(publish_date: str = None, current_user: User = Depends(get_current_user)):
"""获取合规优化报告"""
if not publish_date:
publish_date = date.today().isoformat()
if not re.match(r'^\d{4}-\d{2}-\d{2}$', publish_date):
raise HTTPException(status_code=400, detail="Invalid date format (expected YYYY-MM-DD)")
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
report_path = PROJECT_ROOT / "automation" / "data" / "drafts" / publish_date / "optimization_report.json"
if not report_path.exists():
raise HTTPException(status_code=404, detail="No optimization report for this date")
+199
View File
@@ -0,0 +1,199 @@
import os
import uuid
import hashlib
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
from ..database import get_db
from ..models import MediaAsset
from ..schemas import MediaAssetCreate, MediaAssetUpdate, MediaAssetResponse
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/assets", tags=["assets"])
PROJECT_ROOT = Path(__file__).resolve().parents[4]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
UPLOAD_DIR = PROJECT_ROOT / "content" / "images"
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
@router.get("", response_model=List[MediaAssetResponse])
def list_assets(
file_type: Optional[str] = None,
tag: Optional[str] = None,
topic_id: Optional[str] = None,
search: Optional[str] = None,
limit: int = Query(50, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
query = db.query(MediaAsset)
of = org_filter(current_user, MediaAsset)
if of is not True:
query = query.filter(of)
if file_type:
query = query.filter(MediaAsset.file_type == file_type)
if tag:
query = query.filter(MediaAsset.tags.contains([tag]))
if topic_id:
query = query.filter(MediaAsset.topic_ids.contains([topic_id]))
if search:
query = query.filter(
(MediaAsset.filename.contains(search)) |
(MediaAsset.alt_text.contains(search))
)
return query.order_by(MediaAsset.created_at.desc()).offset(offset).limit(limit).all()
@router.get("/tags")
def list_tags(
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
of = org_filter(current_user, MediaAsset)
base = db.query(MediaAsset)
if of is not True:
base = base.filter(of)
assets = base.with_entities(MediaAsset.tags).all()
all_tags = set()
for a in assets:
if a[0]:
all_tags.update(a[0])
return sorted(all_tags)
@router.get("/counts")
def get_counts(
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
of = org_filter(current_user, MediaAsset)
base = db.query(MediaAsset)
if of is not True:
base = base.filter(of)
total = base.count()
by_type = {}
rows = base.with_entities(MediaAsset.file_type, func.count(MediaAsset.id)).group_by(MediaAsset.file_type).all()
for ftype, cnt in rows:
by_type[ftype] = cnt
return {"total": total, "by_type": by_type}
@router.post("/upload", response_model=MediaAssetResponse)
async def upload_asset(
file: UploadFile = File(...),
tags: Optional[str] = Query(None, description="逗号分隔的标签"),
alt_text: Optional[str] = Query(None),
topic_ids: Optional[str] = Query(None, description="逗号分隔的选题ID"),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
if file.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file.content_type}")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
suffix = Path(file.filename).suffix or ""
unique_name = f"{uuid.uuid4().hex[:12]}{suffix}"
file_path = UPLOAD_DIR / unique_name
content = await file.read()
file_size = len(content)
with open(file_path, "wb") as f:
f.write(content)
file_type = file.content_type.split("/")[0]
if file_type not in ("image", "video", "application"):
if suffix in (".pdf", ".doc", ".docx", ".ppt", ".pptx"):
file_type = "document"
elif suffix in (".mp4", ".mov", ".avi"):
file_type = "video"
else:
file_type = "image"
parsed_tags = [t.strip() for t in tags.split(",")] if tags else []
parsed_topic_ids = [t.strip() for t in topic_ids.split(",")] if topic_ids else []
asset = MediaAsset(
filename=file.filename,
file_path=str(file_path),
file_type=file_type,
mime_type=file.content_type,
size=file_size,
alt_text=alt_text,
tags=parsed_tags,
topic_ids=parsed_topic_ids,
uploaded_by=current_user.username,
org_id=current_user.org_id or "default",
)
db.add(asset)
db.commit()
db.refresh(asset)
return asset
@router.put("/{asset_id}", response_model=MediaAssetResponse)
def update_asset(
asset_id: int,
data: MediaAssetUpdate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
if current_user.role != "admin" and asset.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="素材不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(asset, k, v)
db.commit()
db.refresh(asset)
return asset
@router.delete("/{asset_id}")
def delete_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
if current_user.role != "admin" and asset.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="素材不存在")
if os.path.exists(asset.file_path):
try:
os.remove(asset.file_path)
except OSError:
pass
db.delete(asset)
db.commit()
return {"ok": True}
@router.post("/{asset_id}/use")
def increment_usage(
asset_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
if current_user.role != "admin" and asset.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="素材不存在")
asset.usage_count = (asset.usage_count or 0) + 1
db.commit()
return {"ok": True, "usage_count": asset.usage_count}
+312
View File
@@ -0,0 +1,312 @@
import re
import json
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from ..core.nvidia_client import call_llm
from .auth import get_current_user
from .assistant_actions import ACTION_REGISTRY
router = APIRouter(prefix="/api/assistant", tags=["assistant"])
PAGE_CONTEXTS = {
"dashboard": "仪表盘页面:显示系统概览统计(选题总数、待处理、待审查、待发布、已发布、今日新增)、模块状态卡片(6个定时任务的运行状态)、近期计划列表",
"topics": "选题管理页面:创建和管理内容选题,支持筛选、排序、评分、编辑、预览等功能",
"metrics": "数据分析页面:展示各平台内容表现数据,包括浏览量、点赞、收藏、评论等指标的趋势图表",
"calendar": "内容日历页面:以日历形式展示内容发布计划,支持创建、编辑、拖拽调整日期",
"assets": "素材管理页面:管理图片、文件等媒体素材,支持上传、标签分类、关联选题",
"tasks": "任务管理页面:查看内容创作任务的执行状态,包括自动采集、同步、生成、优化等任务",
"platforms": "平台配置页面:管理各发布平台的配置,包括平台名称、API端点、发布格式等",
"admin": "系统管理页面:管理用户、LLM配置、系统配置、采集类别、信息源、组织、查看运行日志等",
}
# 可用操作清单(用于系统提示词)
from .assistant_actions import ACTION_REGISTRY
ACTION_DESCRIPTIONS = "\n".join([f"- {name}: {info['description']}" for name, info in ACTION_REGISTRY.items()])
class ChatRequest(BaseModel):
message: str
page: str = "dashboard"
history: List[dict] = []
def _execute_action(action_name: str, params: Dict[str, Any], current_user) -> Dict[str, Any]:
"""执行一个具体的操作"""
if action_name not in ACTION_REGISTRY:
return {"error": f"未知操作: {action_name}"}
func = ACTION_REGISTRY[action_name]["func"]
try:
result = func(**params)
return {"success": True, "result": result}
except Exception as e:
import traceback
return {"error": str(e), "traceback": traceback.format_exc()}
@router.post("/chat")
def chat(request: ChatRequest, current_user=Depends(get_current_user)):
user_message = request.message.strip()
# 1. 意图识别(规则优先):用户问题涉及系统数据,直接调用工具,避免 LLM 漏掉 ACTION
intent, params = _detect_intent(user_message)
if intent:
result = _execute_action(intent, params, current_user)
reply = _format_action_result(intent, params, result)
return {"reply": reply, "actions": [{"action": intent, "params": params, "result": result}]}
# 2. 走 LLM 对话(让 LLM 生成 ACTION
try:
from ..database import SessionLocal
from ..models import SystemConfig, User
db = SessionLocal()
sc = db.query(SystemConfig).filter(SystemConfig.key == "assistant_system_prompt").first()
base_prompt = sc.value if sc else None
user_rec = db.query(User).filter(User.id == getattr(current_user, 'id', None)).first()
username = user_rec.username if user_rec else (getattr(current_user, 'username', '用户'))
db.close()
except Exception:
base_prompt = None
username = getattr(current_user, 'username', '用户')
page_name = request.page
page_guide = PAGE_CONTEXTS.get(page_name, "未知页面")
system_prompt = base_prompt or f"""你是一个智能内容创作平台助手,能够通过调用工具与系统实时交互,为用户提供准确的数据和操作。
当前用户 {username} 正在查看:{page_guide}
**重要规则**:对于任何涉及系统数据(选题、任务、统计、配置等)的问题,你必须使用提供的工具查询最新数据,不要依赖你的训练数据或猜测。
可用工具:
{ACTION_DESCRIPTIONS}
**调用格式(必须严格遵守)**
当需要执行工具时,在你的回复中包含单独一行(不要在行内有其他文字):
**ACTION: action_name** {{"param1": "value1", ...}}
示例对话:
用户:系统现在有多少选题?
助手:稍等,我为你查询一下。
**ACTION: get_system_stats** {{}}
用户:我想看最近5条任务日志
助手:好的。
**ACTION: get_recent_task_logs** {{"limit":5}}
工具执行后,系统会返回结果,你再根据结果生成最终回复(如「当前选题总数是42个」)。
注意:
- 必须严格使用上述格式,工具名和参数必须匹配
- 参数必须是合法JSON,缺失必要参数时应询问用户
- 如果用户问题模糊,先澄清再调用工具
- 回复内容简洁友好,不超过200字
"""
messages = request.history[-20:]
conversation = "\n".join([f"{'用户' if m.get('role') == 'user' else '助手'}: {m.get('content', '')}" for m in messages])
prompt_text = f"{conversation}\n用户: {request.message}\n助手:"
try:
reply = call_llm(prompt_text, system_prompt=system_prompt, temperature=0.5, max_tokens=1500)
# 检测是否有操作指令(支持多行)
action_pattern = re.compile(r'\*\*ACTION:\s*(\w+)\*\*\s*(\{.*\})', re.DOTALL)
action_matches = list(action_pattern.finditer(reply))
action_results = []
processed_reply = reply
for match in action_matches:
action_name = match.group(1).strip()
args_str = match.group(2).strip()
try:
args = json.loads(args_str)
# 执行操作
exec_result = _execute_action(action_name, args, current_user)
action_results.append({
"action": action_name,
"params": args,
"result": exec_result
})
# 从回复中移除 ACTION 标记行
processed_reply = processed_reply.replace(match.group(0), '').strip()
except json.JSONDecodeError:
action_results.append({"action": action_name, "error": "参数JSON格式错误"})
# 如果有执行结果,追加简要说明到回复中
if action_results:
summary_lines = []
for ar in action_results:
if "error" in ar["result"]:
summary_lines.append(f"❌ 操作 {ar['action']} 失败: {ar['result']['error']}")
else:
summary_lines.append(f"✅ 操作 {ar['action']} 已完成。")
processed_reply = processed_reply + "\n\n" + "\n".join(summary_lines)
return {"reply": processed_reply.strip(), "actions": action_results}
except Exception as e:
return {"reply": f"抱歉,AI助手暂时无法响应,请稍后重试。"}
def _detect_intent(message: str) -> (str, Dict[str, Any]):
"""基于关键词的意图识别,返回 (action_name, params) 或 (None, {{}})"""
msg = message.lower()
# 查询系统统计(选题总数、各状态数量、案例数)
if any(k in msg for k in ['统计', '总数', '多少选题', '选题数量', '系统数据', '数据总览']):
return "get_system_stats", {}
# 查看选题列表
if any(k in msg for k in ['选题列表', '查看选题', '选题有哪些', '列出选题', '选题']):
# 默认返回第一页20条,可后续细化
page = 1
page_size = 10
# 检测是否指定状态
status = None
if any(k in msg for k in ['待处理', '待审核', '待审查', '待发布', '已发布']):
status_map = {'待处理': 'pending', '待审查': 'review', '待发布': 'ready', '已发布': 'published'}
for k, v in status_map.items():
if k in msg:
status = v
break
page_size = 20 if '全部' in msg else page_size
return "list_topics", {"page": page, "page_size": page_size, "status": status, "field": None}
# 查询特定选题
if any(k in msg for k in ['查看选题', '选题详情', '选题详情', '获取选题', '选题id']):
# 尝试提取ID(大写字母+数字 或 LIVING/TECH/M0604 等格式)
import re
match = re.search(r'[A-Z0-9\-]{4,}', msg)
if match:
topic_id = match.group(0)
return "get_topic", {"topic_id": topic_id}
return None, {}
# 最近任务日志
if any(k in msg for k in ['任务日志', '执行记录', '任务记录', '最近任务']):
limit = 5 if '最近' in msg or '查看' in msg else 10
# 检查是否指定模块
module_id = None
for mod in ['collect', 'generate', 'optimize', 'metrics', 'trends']:
if mod in msg:
module_id = f"scheduled_{mod}"
break
return "get_task_status", {"module_id": module_id} if module_id else {"module_id": None}
# 手动触发任务
if any(k in msg for k in ['触发', '运行', '执行', '采集', '生成', '优化', '指标']):
module_map = {
'collect': 'scheduled_collect',
'采集': 'scheduled_collect',
'generate': 'scheduled_generate',
'生成': 'scheduled_generate',
'optimize': 'scheduled_optimize',
'优化': 'scheduled_optimize',
'optimize_sources': 'scheduled_optimize_sources',
'source': 'scheduled_optimize_sources',
'metrics': 'scheduled_metrics_sync',
'指标': 'scheduled_metrics_sync',
'trend': 'scheduled_trends',
'趋势': 'scheduled_trends'
}
for key, val in module_map.items():
if key in msg:
return "trigger_task", {"module_id": val}
# 如果无法识别具体模块,不触发
return None, {}
# 搜索
if any(k in msg for k in ['搜索', '查询', '查找']):
# 提取搜索词(简单:把问题其余部分作为查询)
# 这里较难简单提取,暂不自动触发,留给 LLM
return None, {}
# 默认不触发
return None, {}
def _format_action_result(action_name: str, params: Dict[str, Any], result: Dict[str, Any]) -> str:
"""将工具执行结果格式化为自然语言回复"""
if "error" in result:
return f"⚠️ 执行 {action_name} 失败:{result['error']}"
res = result.get("result", result)
if action_name == "get_system_stats":
t = res.get("topics", {})
return (f"📊 系统统计:\n"
f"• 选题总数:{t.get('total', 0)}\n"
f"• 待处理:{t.get('pending', 0)}\n"
f"• 待审查:{t.get('review', 0)}\n"
f"• 草稿:{t.get('draft', 0)}\n"
f"• 待发布:{t.get('ready', 0)}\n"
f"• 已发布:{t.get('published', 0)}\n"
f"• 本周新增:{t.get('new_this_week', 0)}\n"
f"• 案例总数:{res.get('cases', 0)}")
if action_name == "list_topics":
items = res.get("items", [])
total = res.get("total", 0)
page = res.get("page", 1)
if not items:
return f"当前没有符合条件的选题(总计 {total} 条)。"
lines = [f"📋 选题列表(第 {page} 页,显示 {len(items)} 条,共 {total} 条):"]
for it in items:
lines.append(f"{it['id']}{it['title'][:30]}... [{it['status']}]")
return "\n".join(lines)
if action_name == "get_topic":
if "error" in res:
return f"{res['error']}"
t = res
return (f"📄 选题详情 [{t['id']}]\n"
f"标题:{t['title']}\n"
f"领域:{t['field']}\n"
f"状态:{t['status']}\n"
f"优先级:{t['priority']} (分:{t['priority_score']})")
if action_name == "get_recent_task_logs":
logs = res[:5] if isinstance(res, list) else []
if not logs:
return "暂无任务日志。"
lines = ["📝 最近任务记录:"]
for l in logs:
status_icon = {"success":"","failed":"","running":"🔄"}.get(l['status'],"")
time_str = l['started_at'][:10] if l.get('started_at') else "?"
lines.append(f" {status_icon} {l['module_id']} - {l['status']} ({time_str})")
return "\n".join(lines)
if action_name == "trigger_task":
if "error" in res:
return f"❌ 触发失败:{res['error']}"
return f"⏳ 已触发任务:{params.get('module_id')},正在后台执行。"
if action_name == "search_web":
if "error" in res:
return f"❌ 搜索失败:{res['error']}"
results = res.get("results", [])
count = res.get("count", len(results))
if not results:
return "未找到相关搜索结果。"
lines = [f"🔍 搜索到 {count} 条结果:"]
for i, r in enumerate(results[:3], 1):
lines.append(f" {i}. {r.get('title','无标题')}\n {r.get('url','')}")
if count > 3:
lines.append(f" ... 还有 {count-3}")
return "\n".join(lines)
if action_name == "get_task_status":
logs = res[:5] if isinstance(res, list) else []
if not logs:
return "未找到任务日志。"
lines = ["📋 任务状态:"]
for l in logs:
status_icon = {"success":"","failed":"","running":"🔄"}.get(l['status'],"")
time_str = l['started_at'][:10] if l.get('started_at') else "?"
msg = (l.get('message') or '')
msg_snippet = (msg[:30] + '...') if len(msg) > 30 else msg[:30]
lines.append(f" {status_icon} {l['module_id']} - {l['status']} ({time_str})")
if msg_snippet:
lines.append(f" {msg_snippet}")
return "\n".join(lines)
# 默认返回原始结果
return f"✅ 操作 {action_name} 完成。"
@@ -0,0 +1,256 @@
"""
AI Assistant 可执行操作注册表
"""
import json
from datetime import datetime, timezone, timedelta
from typing import Dict, Any, List, Optional
from ..database import SessionLocal
from ..models import Topic, ContentTask, TaskLog, SystemConfig, TopicField, Case
from ..core.scheduler import scheduler as bg_scheduler
# -------------------- 工具函数 --------------------
def list_topics(
page: int = 1,
page_size: int = 20,
status: Optional[str] = None,
field: Optional[str] = None,
org_id: Optional[str] = None,
) -> Dict[str, Any]:
"""获取选题列表,支持分页和筛选"""
db = SessionLocal()
try:
query = db.query(Topic)
if org_id:
query = query.filter(Topic.org_id == org_id)
if status:
query = query.filter(Topic.status == status)
if field:
query = query.filter(Topic.field_name == field)
total = query.count()
items = query.order_by(Topic.priority.desc(), Topic.created_at.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size) \
.all()
result_items = []
for t in items:
result_items.append({
"id": t.id,
"title": t.title,
"field": t.field_name,
"status": t.status,
"priority": t.priority,
"priority_score": t.priority_score,
"created_at": t.created_at.isoformat() if t.created_at else None
})
return {
"total": total,
"page": page,
"page_size": page_size,
"items": result_items
}
finally:
db.close()
def get_topic(topic_id: str, org_id: Optional[str] = None) -> Dict[str, Any]:
"""获取单个选题详情"""
db = SessionLocal()
try:
query = db.query(Topic).filter(Topic.id == topic_id)
if org_id:
query = query.filter(Topic.org_id == org_id)
t = query.first()
if not t:
return {"error": f"选题 {topic_id} 不存在"}
return {
"id": t.id,
"title": t.title,
"field": t.field_name,
"status": t.status,
"priority": t.priority,
"priority_score": t.priority_score,
"core_concept": t.core_concept,
"audience_pain": t.audience_pain,
"unique_angle": t.unique_angle,
"cases": t.cases or [],
"tags": t.tags or [],
"platform_urls": t.platform_urls or {},
"created_at": t.created_at.isoformat() if t.created_at else None
}
finally:
db.close()
def get_recent_task_logs(limit: int = 10) -> List[Dict[str, Any]]:
"""获取最近的任务执行日志"""
db = SessionLocal()
try:
logs = db.query(TaskLog).order_by(TaskLog.started_at.desc()).limit(limit).all()
return [{
"id": l.id,
"module_id": l.module_id,
"status": l.status,
"message": l.message,
"started_at": l.started_at.isoformat() if l.started_at else None,
"finished_at": l.finished_at.isoformat() if l.finished_at else None,
"triggered_by": l.triggered_by
} for l in logs]
finally:
db.close()
def get_system_stats() -> Dict[str, Any]:
"""获取系统概览统计数据"""
db = SessionLocal()
try:
total_topics = db.query(Topic).count()
pending = db.query(Topic).filter(Topic.status == "pending").count()
review = db.query(Topic).filter(Topic.status == "review").count()
draft = db.query(Topic).filter(Topic.status == "draft").count()
ready = db.query(Topic).filter(Topic.status == "ready").count()
published = db.query(Topic).filter(Topic.status == "published").count()
total_cases = db.query(Case).count()
week_ago = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=7)
new_this_week = db.query(Topic).filter(Topic.created_at >= week_ago).count()
return {
"topics": {
"total": total_topics,
"pending": pending,
"review": review,
"draft": draft,
"ready": ready,
"published": published,
"new_this_week": new_this_week
},
"cases": total_cases,
"date": datetime.now(timezone.utc).isoformat()
}
finally:
db.close()
def get_task_status(module_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""查询任务日志(如 collector、generator、optimizer 等)"""
db = SessionLocal()
try:
query = db.query(TaskLog)
if module_id:
query = query.filter(TaskLog.module_id == module_id)
logs = query.order_by(TaskLog.started_at.desc()).limit(10).all()
return [{
"id": l.id,
"module_id": l.module_id,
"status": l.status,
"message": l.message,
"started_at": l.started_at.isoformat() if l.started_at else None,
"finished_at": l.finished_at.isoformat() if l.finished_at else None,
"triggered_by": l.triggered_by
} for l in logs]
finally:
db.close()
def trigger_task(module_id: str) -> Dict[str, Any]:
"""手动触发定时任务(如 collector、generator、optimizer、metrics_sync 等)"""
# 只允许触发特定任务
allowed = ["scheduled_collect", "scheduled_generate", "scheduled_optimize",
"scheduled_optimize_sources", "scheduled_metrics_sync", "scheduled_trends"]
if module_id not in allowed:
return {"error": f"不允许手动触发该任务: {module_id}"}
job_id = module_id if module_id.startswith("scheduled_") else f"scheduled_{module_id}"
# 检查任务是否存在
job = bg_scheduler.get_job(job_id)
if not job:
return {"error": f"任务不存在: {job_id}"}
try:
# 如果任务正在运行,返回提示
# APScheduler 的 run_job 会立即执行
bg_scheduler.run_job(job_id)
return {"success": True, "message": f"任务 {job_id} 已触发执行"}
except Exception as e:
return {"error": str(e)}
def search_web(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
"""联网搜索(通过本项目的 search_utils"""
try:
from ...scripts.search_utils import search as web_search
results = web_search(query, max_results)
return {"success": True, "results": results, "count": len(results)}
except Exception as e:
return {"error": str(e)}
def list_system_configs() -> List[Dict[str, Any]]:
"""列出系统配置项"""
db = SessionLocal()
try:
configs = db.query(SystemConfig).all()
return [{"key": c.key, "value": c.value, "description": c.description} for c in configs]
finally:
db.close()
# Action registry
ACTION_REGISTRY = {
"list_topics": {
"func": list_topics,
"description": "获取选题列表,支持分页(page, page_size)和筛选(status, field, org_id)",
"params_schema": {
"page": {"type": "integer", "default": 1, "desc": "页码"},
"page_size": {"type": "integer", "default": 20, "desc": "每页条数"},
"status": {"type": "string", "enum": ["pending", "review", "draft", "ready", "published"], "desc": "按状态筛选"},
"field": {"type": "string", "desc": "按领域筛选"},
"org_id": {"type": "string", "desc": "按组织筛选"}
}
},
"get_topic": {
"func": get_topic,
"description": "获取单个选题详情",
"params_schema": {
"topic_id": {"type": "string", "desc": "选题ID,如 A01 或 LIVING-001-26"},
"org_id": {"type": "string", "desc": "按组织筛选(可选)"}
}
},
"get_recent_task_logs": {
"func": get_recent_task_logs,
"description": "获取最近的任务执行日志",
"params_schema": {
"limit": {"type": "integer", "default": 10, "desc": "返回条数"}
}
},
"get_system_stats": {
"func": get_system_stats,
"description": "获取系统概览统计(选题总数、各状态数量、案例数等)",
"params_schema": {}
},
"get_task_status": {
"func": get_task_status,
"description": "查询任务日志,可按模块ID筛选",
"params_schema": {
"module_id": {"type": "string", "desc": "可选:筛选特定模块ID(如 scheduled_collect"}
}
},
"trigger_task": {
"func": trigger_task,
"description": "手动触发定时任务(采集、生成、优化、指标同步等)",
"params_schema": {
"module_id": {"type": "string", "desc": "任务模块IDscheduled_collect/scheduled_generate/scheduled_optimize/scheduled_optimize_sources/scheduled_metrics_sync/scheduled_trends"}
}
},
"search_web": {
"func": search_web,
"description": "执行网络搜索,返回结构化结果",
"params_schema": {
"query": {"type": "string", "desc": "搜索关键词"},
"max_results": {"type": "integer", "default": 5, "desc": "返回结果数量"}
}
},
"list_system_configs": {
"func": list_system_configs,
"description": "列出所有系统配置项(key/value)",
"params_schema": {}
}
}
+4 -15
View File
@@ -4,28 +4,17 @@ from typing import List, Optional
from datetime import datetime, timedelta
from ..database import get_db
from ..models import AuditLog
from ..models import AuditLog, User
from ..schemas import AuditLogResponse
from .auth import verify_token
from .auth import get_current_admin
router = APIRouter(prefix="/api/audit", tags=["audit"])
def get_current_admin(request: Request, db: Session = Depends(get_db)):
"""依赖项:验证管理员权限"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
@router.get("/logs", response_model=List[AuditLogResponse])
def list_audit_logs(
request: Request,
db: Session = Depends(get_db),
admin_user: bool = Depends(get_current_admin),
admin_user: User = Depends(get_current_admin),
username: Optional[str] = Query(None, description="按用户名筛选"),
action: Optional[str] = Query(None, description="按操作类型筛选"),
resource_type: Optional[str] = Query(None, description="按资源类型筛选"),
@@ -64,7 +53,7 @@ def list_audit_actions(
def list_audit_users(
request: Request,
db: Session = Depends(get_db),
admin_user: bool = Depends(get_current_admin),
admin_user: User = Depends(get_current_admin),
limit: int = Query(50, ge=1, le=200)
):
"""获取最近产生审计记录的用户列表"""
+62 -16
View File
@@ -7,8 +7,8 @@ import os
from dotenv import load_dotenv
from ..database import get_db
from ..models import User
from ..schemas import LoginRequest, TokenResponse, UserResponse
from ..models import User, Role
from ..schemas import LoginRequest, TokenResponse, UserResponse, ChangePasswordRequest
from ..core.audit_logger import audit_log
# 加载环境变量
@@ -32,6 +32,7 @@ def create_token(user: User) -> str:
"sub": str(user.id),
"username": user.username,
"role": user.role,
"org_id": user.org_id,
"exp": expire
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
@@ -66,7 +67,8 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
user = User(
username=DEFAULT_ADMIN_USERNAME,
password_hash=hashed.decode('utf-8'),
role="admin"
role="admin",
org_id="default"
)
db.add(user)
db.commit()
@@ -100,7 +102,7 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
user_agent=user_agent,
db=db
)
return TokenResponse(token=token, role=user.role, user=UserResponse.from_orm(user))
return TokenResponse(token=token, role=user.role, user=UserResponse.model_validate(user))
# 从数据库查询其他用户
user = db.query(User).filter(User.username == login_data.username).first()
@@ -136,19 +138,9 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
user_agent=user_agent,
db=db
)
return TokenResponse(token=token, role=user.role, user=UserResponse.from_orm(user))
return TokenResponse(token=token, role=user.role, user=UserResponse.model_validate(user))
@router.get("/me")
def get_current_user(request: Request, db: Session = Depends(get_db)):
"""获取当前登录用户信息"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
return {"user": UserResponse.from_orm(user)}
def get_current_user(request: Request, db: Session = Depends(get_db)):
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
"""依赖项:验证用户登录"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
@@ -156,3 +148,57 @@ def get_current_user(request: Request, db: Session = Depends(get_db)):
token = auth_header.split(" ")[1]
user = verify_token(token, db)
return user
def org_filter(current_user: User, model):
"""返回 org_id 过滤条件。管理员看到全部数据,普通用户仅限本组织。"""
if current_user.role == "admin":
return True # no filter
return model.org_id == current_user.org_id
def get_current_admin(current_user: User = Depends(get_current_user)) -> User:
"""依赖项:验证管理员权限"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return current_user
def require_role(required_role: str):
"""依赖项工厂:返回一个 FastAPI 依赖项,要求当前用户拥有指定角色。
管理员自动拥有所有角色权限。"""
def role_checker(current_user: User = Depends(get_current_user)):
if current_user.role != required_role and current_user.role != "admin":
raise HTTPException(status_code=403, detail=f"需要{required_role}角色权限")
return current_user
return role_checker
@router.get("/me")
def get_me(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取当前登录用户信息"""
return {"user": UserResponse.model_validate(current_user)}
@router.put("/password")
def change_password(
data: ChangePasswordRequest,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""修改当前用户密码"""
if not bcrypt.checkpw(data.old_password.encode('utf-8'), current_user.password_hash.encode('utf-8')):
raise HTTPException(status_code=400, detail="原密码错误")
if len(data.new_password) < 6:
raise HTTPException(status_code=400, detail="新密码至少6位")
current_user.password_hash = bcrypt.hashpw(data.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
db.commit()
audit_log(
action="change_password",
user=current_user,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return {"message": "密码修改成功"}
+250
View File
@@ -0,0 +1,250 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, date
from calendar import monthrange
from ..database import get_db
from ..models import ContentCalendar, Topic
from ..schemas import (
ContentCalendarCreate, ContentCalendarUpdate, ContentCalendarResponse,
ContentCalendarBase
)
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/calendar", tags=["calendar"])
PLATFORM_ICONS = {
"zhihu": "",
"wechat": "",
"xiaohongshu": ""
}
def enrich_entry(entry, db):
d = entry.to_dict()
d["platform_icon"] = PLATFORM_ICONS.get(entry.platform, "📝")
d["topic_status"] = None
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic:
d["topic_status"] = topic.status
return d
@router.get("", response_model=List[ContentCalendarResponse])
def get_calendar(
year: Optional[int] = Query(None),
month: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
today = date.today()
year = year or today.year
month = month or today.month
start = date(year, month, 1)
last_day = monthrange(year, month)[1]
end = date(year, month, last_day)
base = db.query(ContentCalendar).filter(
ContentCalendar.planned_date >= start,
ContentCalendar.planned_date <= end
)
of = org_filter(current_user, Topic)
if of is not True:
base = base.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
entries = base.order_by(ContentCalendar.planned_date).all()
return [enrich_entry(e, db) for e in entries]
@router.get("/entries", response_model=List[ContentCalendarResponse])
def list_entries(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
status: Optional[str] = None,
platform: Optional[str] = None,
limit: int = Query(50, le=200),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
query = db.query(ContentCalendar)
of = org_filter(current_user, Topic)
if of is not True:
query = query.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
if start_date:
query = query.filter(ContentCalendar.planned_date >= start_date)
if end_date:
query = query.filter(ContentCalendar.planned_date <= end_date)
if status:
query = query.filter(ContentCalendar.status == status)
if platform:
query = query.filter(ContentCalendar.platform == platform)
return query.order_by(ContentCalendar.planned_date.desc()).limit(limit).all()
@router.post("/entries", response_model=ContentCalendarResponse)
def create_entry(
data: ContentCalendarCreate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
if data.topic_id:
topic = db.query(Topic).filter(Topic.id == data.topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry = ContentCalendar(**data.model_dump(), org_id=current_user.org_id or "default")
db.add(entry)
db.commit()
db.refresh(entry)
if data.topic_id and data.title:
if not topic.title or topic.title != data.title:
topic.title = data.title
db.commit()
return entry
@router.put("/entries/{entry_id}", response_model=ContentCalendarResponse)
def update_entry(
entry_id: int,
data: ContentCalendarUpdate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
# Verify ownership via topic org_id
if entry.topic_id:
topic_check = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic_check and current_user.role != "admin" and topic_check.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(entry, k, v)
db.commit()
db.refresh(entry)
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and entry.status == "published" and not entry.published_date:
entry.published_date = date.today()
if topic.status == "pending" or topic.status == "ready":
topic.status = "published"
topic.published_at = date.today()
topic.updated_at = datetime.now()
db.commit()
return entry
@router.delete("/entries/{entry_id}")
def delete_entry(
entry_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
db.delete(entry)
db.commit()
return {"ok": True}
@router.post("/entries/bind-topic")
def bind_topic_to_entry(
entry_id: int,
topic_id: str,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry.topic_id = topic_id
entry.title = topic.title
if topic.field_name:
entry.field_id = topic.field_id
db.commit()
db.refresh(entry)
return entry
@router.get("/stats")
def calendar_stats(
year: int = Query(...),
month: int = Query(...),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
start = date(year, month, 1)
last_day = monthrange(year, month)[1]
end = date(year, month, last_day)
q = db.query(ContentCalendar).filter(
ContentCalendar.planned_date >= start,
ContentCalendar.planned_date <= end
)
of = org_filter(current_user, Topic)
if of is not True:
q = q.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
entries = q.all()
stats = {"total": len(entries), "planned": 0, "published": 0, "delayed": 0, "cancelled": 0}
for e in entries:
if e.status in stats:
stats[e.status] += 1
return stats
@router.post("/entries/from-topic")
def create_from_topic(
topic_id: str,
planned_date: date,
platform: str = Query(...),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry = ContentCalendar(
topic_id=topic_id,
field_id=topic.field_id,
title=topic.title,
planned_date=planned_date,
platform=platform,
created_by=current_user.username,
org_id=current_user.org_id or "default"
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry
+81
View File
@@ -0,0 +1,81 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from typing import List
from ..database import get_db
from ..models import Case
from ..schemas import CaseBase, CaseResponse
from .auth import get_current_admin
router = APIRouter(prefix="/api/admin/cases", tags=["admin"])
@router.get("", response_model=List[CaseResponse])
def list_cases(
request: Request,
db: Session = Depends(get_db),
admin_user = Depends(get_current_admin)
):
"""获取案例列表(管理员)"""
cases = db.query(Case).all()
return [CaseResponse.model_validate(c) for c in cases]
@router.get("/{case_id}", response_model=CaseResponse)
def get_case(
case_id: int,
request: Request,
db: Session = Depends(get_db),
admin_user = Depends(get_current_admin)
):
"""获取单个案例详情"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail="案例不存在")
return case
@router.post("", response_model=CaseResponse)
def create_case(
case_data: CaseBase,
request: Request,
db: Session = Depends(get_db),
admin_user = Depends(get_current_admin)
):
"""创建新案例"""
case = Case(**case_data.model_dump())
db.add(case)
db.commit()
db.refresh(case)
return case
@router.put("/{case_id}", response_model=CaseResponse)
def update_case(
case_id: int,
case_update: CaseBase,
request: Request,
db: Session = Depends(get_db),
admin_user = Depends(get_current_admin)
):
"""更新案例"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail="案例不存在")
update_data = case_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(case, field, value)
db.commit()
db.refresh(case)
return case
@router.delete("/{case_id}")
def delete_case(
case_id: int,
request: Request,
db: Session = Depends(get_db),
admin_user = Depends(get_current_admin)
):
"""删除案例"""
case = db.query(Case).filter(Case.id == case_id).first()
if not case:
raise HTTPException(status_code=404, detail="案例不存在")
db.delete(case)
db.commit()
return {"message": "删除成功"}
+127
View File
@@ -0,0 +1,127 @@
"""采集管理 API:类别与信息源的 CRUD"""
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel
from typing import Optional
from ..database import get_db
from ..models import CollectorCategory, CollectorSource
from .auth import get_current_admin, require_role
router = APIRouter(prefix="/api/admin/collector", tags=["collector_mgmt"], dependencies=[Depends(get_current_admin)])
# ---------- Schemas ----------
class CategoryCreate(BaseModel):
name: str
description: Optional[str] = None
search_query: Optional[str] = None
sort_order: int = 0
is_active: bool = True
class CategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
search_query: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class SourceCreate(BaseModel):
category_id: Optional[int] = None
name: str
source_type: str
url: Optional[str] = None
query: Optional[str] = None
credibility: str = "medium"
focus: Optional[str] = None
is_active: bool = True
sort_order: int = 0
class SourceUpdate(BaseModel):
category_id: Optional[int] = None
name: Optional[str] = None
source_type: Optional[str] = None
url: Optional[str] = None
query: Optional[str] = None
credibility: Optional[str] = None
focus: Optional[str] = None
is_active: Optional[bool] = None
sort_order: Optional[int] = None
# ---------- Categories ----------
@router.get("/categories")
def list_categories(db: Session = Depends(get_db)):
cats = db.query(CollectorCategory).order_by(CollectorCategory.sort_order).all()
result = []
for c in cats:
d = c.to_dict()
d["source_count"] = db.query(CollectorSource).filter(CollectorSource.category_id == c.id).count()
result.append(d)
return result
@router.post("/categories", status_code=201)
def create_category(data: CategoryCreate, db: Session = Depends(get_db)):
existing = db.query(CollectorCategory).filter(CollectorCategory.name == data.name).first()
if existing:
raise HTTPException(400, "类别名已存在")
cat = CollectorCategory(**data.model_dump())
db.add(cat)
db.commit()
db.refresh(cat)
return cat.to_dict()
@router.put("/categories/{cat_id}")
def update_category(cat_id: int, data: CategoryUpdate, db: Session = Depends(get_db)):
cat = db.query(CollectorCategory).filter(CollectorCategory.id == cat_id).first()
if not cat:
raise HTTPException(404, "类别不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(cat, k, v)
db.commit()
db.refresh(cat)
return cat.to_dict()
@router.delete("/categories/{cat_id}")
def delete_category(cat_id: int, db: Session = Depends(get_db)):
cat = db.query(CollectorCategory).filter(CollectorCategory.id == cat_id).first()
if not cat:
raise HTTPException(404, "类别不存在")
db.delete(cat)
db.commit()
return {"ok": True}
# ---------- Sources ----------
@router.get("/sources")
def list_sources(category_id: Optional[int] = None, db: Session = Depends(get_db)):
q = db.query(CollectorSource).order_by(CollectorSource.sort_order)
if category_id is not None:
q = q.filter(CollectorSource.category_id == category_id)
return [s.to_dict() for s in q.all()]
@router.post("/sources", status_code=201)
def create_source(data: SourceCreate, db: Session = Depends(get_db)):
src = CollectorSource(**data.model_dump())
db.add(src)
db.commit()
db.refresh(src)
return src.to_dict()
@router.put("/sources/{src_id}")
def update_source(src_id: int, data: SourceUpdate, db: Session = Depends(get_db)):
src = db.query(CollectorSource).filter(CollectorSource.id == src_id).first()
if not src:
raise HTTPException(404, "信息源不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(src, k, v)
db.commit()
db.refresh(src)
return src.to_dict()
@router.delete("/sources/{src_id}")
def delete_source(src_id: int, db: Session = Depends(get_db)):
src = db.query(CollectorSource).filter(CollectorSource.id == src_id).first()
if not src:
raise HTTPException(404, "信息源不存在")
db.delete(src)
db.commit()
return {"ok": True}
+424
View File
@@ -0,0 +1,424 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic import BaseModel, ConfigDict
from datetime import datetime
import json
from ..database import get_db
from ..models import KeywordDomainMap, SensitiveWord, ContentCleanRule, TrendFieldMapping, CollectorCategory
from .auth import get_current_admin, require_role
router = APIRouter(prefix="/api/admin/config", tags=["admin"])
DEFAULT_KEYWORD_DOMAIN_MAP = [
{"pattern": r"AI|人工智能|大模型|GPT|机器学习|深度学习|聊天机器人|LLM", "domain": "AI工具", "sort_order": 1},
{"pattern": r"远程|居家办公|自由职业|数字游民|远程协作", "domain": "远程工作", "sort_order": 2},
{"pattern": r"可持续|环保|低碳|绿色|碳中和|循环|零浪费|垃圾分类|节能", "domain": "可持续生活", "sort_order": 3},
{"pattern": r"知识管理|笔记|Obsidian|Notion|第二大脑|读书|阅读", "domain": "知识管理", "sort_order": 4},
{"pattern": r"数字生活|数码|手机|电脑|智能|APP|应用、软件", "domain": "数字生活", "sort_order": 5},
{"pattern": r"科技|人文|教育|心理|哲学|社会学", "domain": "科技人文", "sort_order": 6},
]
DEFAULT_TREND_FIELD_MAP = [
{"trend_keyword": "远程工作", "field_name": "未来工作方式", "sort_order": 1},
{"trend_keyword": "AI工具", "field_name": "AI与效率", "sort_order": 2},
{"trend_keyword": "可持续生活", "field_name": "可持续生活系统", "sort_order": 3},
{"trend_keyword": "知识管理", "field_name": "个人知识工厂", "sort_order": 4},
{"trend_keyword": "数字生活", "field_name": "科技人文交叉", "sort_order": 5},
{"trend_keyword": "科技人文", "field_name": "科技人文交叉", "sort_order": 6},
{"trend_keyword": "个人成长", "field_name": "个人成长", "sort_order": 7},
{"trend_keyword": "副业", "field_name": "个人成长", "sort_order": 8},
{"trend_keyword": "AI创作", "field_name": "AI与效率", "sort_order": 9},
{"trend_keyword": "未来工作", "field_name": "未来工作方式", "sort_order": 10},
{"trend_keyword": "效率工具", "field_name": "AI与效率", "sort_order": 11},
{"trend_keyword": "家庭教育", "field_name": "科技人文交叉", "sort_order": 12},
]
DEFAULT_CHINA_PAIN_TEMPLATES = {
"循环消费": "以旧换新流程繁琐、二手商品信任缺失、租赁市场不规范",
"低碳出行": "新能源车充电设施不足、城市规划不支持骑行、通勤距离长",
"干净饮食": "有机食品价格高、真伪难辨、外卖为主的生活方式难以改变",
"零浪费生活": "环保产品溢价高、可持续选择不便、漂绿营销难以分辨",
"绿色家电与节能": "绿色家电初期投入高、节能效果难量化、老旧小区改造难",
"碳普惠": "碳账户普及率低、减排量兑换吸引力不足、公众认知有限",
"环保科技产品": "绿色产品溢价68%难以承受、缺乏统一认证标准、担心漂绿",
"AI与效率": "AI工具选择困难、数据隐私担忧、学习成本高、实际效果难验证",
}
DEFAULT_GLOBAL_RSS_KEYWORDS = ['sustainable', 'green', 'eco', 'circular', 'climate', 'carbon', 'zero waste', 'renewable', 'recycle', '环保', '可持续', '碳中和', '循环经济', '零浪费', '低碳', '生态']
DEFAULT_PRIORITY_WEIGHTS = {"audience_match": 0.3, "data_availability": 0.25, "uniqueness": 0.2, "executability": 0.15, "brand_fit": 0.1}
DEFAULT_DOMAINS = ["远程工作", "AI工具", "可持续生活", "知识管理", "数字生活", "科技人文"]
DEFAULT_SENSITIVE_WORDS = [
{"word": "国家主席", "category": "political"},
{"word": "政治局", "category": "political"},
{"word": "常委", "category": "political"},
{"word": "军委", "category": "political"},
{"word": "统战部", "category": "political"},
{"word": "颠覆国家", "category": "political"},
{"word": "分裂主义", "category": "political"},
{"word": "台独", "category": "political"},
{"word": "疆独", "category": "political"},
{"word": "藏独", "category": "political"},
{"word": "赌博", "category": "prohibited"},
{"word": "毒品", "category": "prohibited"},
{"word": "迷药", "category": "prohibited"},
{"word": "枪支", "category": "prohibited"},
{"word": "炸药", "category": "prohibited"},
{"word": "色情", "category": "prohibited"},
{"word": "低俗", "category": "prohibited"},
{"word": "反动", "category": "prohibited"},
{"word": "邪教", "category": "prohibited"},
{"word": "保证赚钱", "category": "misleading"},
{"word": "一夜暴富", "category": "misleading"},
{"word": "100%有效", "category": "misleading"},
{"word": "包治百病", "category": "misleading"},
{"word": "绝对正确", "category": "misleading"},
{"word": "国家机密", "category": "legal"},
{"word": "军事秘密", "category": "legal"},
{"word": "绝密", "category": "legal"},
{"word": "迷信", "category": "legal"},
]
DEFAULT_CONTENT_CLEAN_RULES = [
{"rule_type": "thinking", "pattern": r"^(好的|好的,|好[的,]|我来|让我|我将|我这就).*?(?=\n|$)", "description": "AI思考模式1", "sort_order": 1},
{"rule_type": "thinking", "pattern": r"^(以下|下面是|这是|为您|根据).*?(?=\n|$)", "description": "AI思考模式2", "sort_order": 2},
{"rule_type": "preface", "pattern": r"^(基于|\u3010.*?\u3011|这里.*)", "description": "AI前缀模式", "sort_order": 3},
{"rule_type": "verbosity", "pattern": r"^首先|^其次|^最后", "description": "AI废话-首先其次", "sort_order": 4},
{"rule_type": "verbosity", "pattern": r"^总的来说$", "description": "AI废话-总的来说", "sort_order": 5},
{"rule_type": "verbosity", "pattern": r"^值得注意的是$", "description": "AI废话-值得注意的是", "sort_order": 6},
{"rule_type": "verbosity", "pattern": r"^换句话说$", "description": "AI废话-换句话说", "sort_order": 7},
{"rule_type": "verbosity", "pattern": r"^总而言之$", "description": "AI废话-总而言之", "sort_order": 8},
{"rule_type": "verbosity", "pattern": r"^简而言之$", "description": "AI废话-简而言之", "sort_order": 9},
{"rule_type": "verbosity", "pattern": r"^一言以蔽之$", "description": "AI废话-一言以蔽之", "sort_order": 10},
{"rule_type": "verbosity", "pattern": r"^可以说$", "description": "AI废话-可以说", "sort_order": 11},
{"rule_type": "verbosity", "pattern": r"^不难发现$", "description": "AI废话-不难发现", "sort_order": 12},
{"rule_type": "verbosity", "pattern": r"^由此可见$", "description": "AI废话-由此可见", "sort_order": 13},
{"rule_type": "verbosity", "pattern": r"^综上所述$", "description": "AI废话-综上所述", "sort_order": 14},
{"rule_type": "verbosity", "pattern": r"^通过以上", "description": "AI废话-通过以上", "sort_order": 15},
{"rule_type": "html_thinking", "pattern": r"<p[^>]*>(好的|好的,|好[的,]|我来|让我|我将|我这就)", "description": "AI思考-HTML模式", "sort_order": 16},
{"rule_type": "ai_telltale", "pattern": r"值得注意的是", "description": "AI套话—值得注意的是", "sort_order": 20},
{"rule_type": "ai_telltale", "pattern": r"首先.*其次.*最后", "description": "AI套话—首先其次最后", "sort_order": 21},
{"rule_type": "ai_telltale", "pattern": r"综上所述", "description": "AI套话—综上所述", "sort_order": 22},
{"rule_type": "ai_telltale", "pattern": r"总的来说", "description": "AI套话—总的来说", "sort_order": 23},
{"rule_type": "ai_telltale", "pattern": r"总而言之", "description": "AI套话—总而言之", "sort_order": 24},
{"rule_type": "ai_telltale", "pattern": r"无可否认", "description": "AI套话—无可否认", "sort_order": 25},
{"rule_type": "ai_telltale", "pattern": r"毋庸置疑", "description": "AI套话—毋庸置疑", "sort_order": 26},
{"rule_type": "ai_telltale", "pattern": r"众所周知", "description": "AI套话—众所周知", "sort_order": 27},
{"rule_type": "ai_telltale", "pattern": r"不可否认", "description": "AI套话—不可否认", "sort_order": 28},
{"rule_type": "ai_telltale", "pattern": r"从某种意义上", "description": "AI套话—从某种意义上", "sort_order": 29},
{"rule_type": "ai_telltale", "pattern": r"从某种程度上", "description": "AI套话—从某种程度上", "sort_order": 30},
{"rule_type": "ai_telltale", "pattern": r"在一定程度上", "description": "AI套话—在一定程度上", "sort_order": 31},
{"rule_type": "ai_telltale", "pattern": r"换而言之", "description": "AI套话—换而言之", "sort_order": 32},
{"rule_type": "ai_telltale", "pattern": r"换言之", "description": "AI套话—换言之", "sort_order": 33},
{"rule_type": "ai_telltale", "pattern": r"归根结底", "description": "AI套话—归根结底", "sort_order": 34},
{"rule_type": "ai_telltale", "pattern": r"说到底", "description": "AI套话—说到底", "sort_order": 35},
{"rule_type": "ai_telltale", "pattern": r"这为我们提供了", "description": "AI套话—这为我们提供了", "sort_order": 36},
{"rule_type": "ai_telltale", "pattern": r"引人深思", "description": "AI套话—引人深思", "sort_order": 37},
{"rule_type": "ai_telltale", "pattern": r"毫无悬念", "description": "AI套话—毫无悬念", "sort_order": 38},
{"rule_type": "ai_telltale", "pattern": r"不知大家", "description": "AI套话—不知大家", "sort_order": 39},
{"rule_type": "ai_telltale", "pattern": r"说回到", "description": "AI套话—说回到", "sort_order": 40},
{"rule_type": "ai_telltale", "pattern": r"如果你也", "description": "AI套话—如果你也", "sort_order": 41},
{"rule_type": "ai_telltale", "pattern": r"我们不难发现", "description": "AI套话—我们不难发现", "sort_order": 42},
{"rule_type": "ai_telltale", "pattern": r"我们可以看出", "description": "AI套话—我们可以看出", "sort_order": 43},
{"rule_type": "ai_telltale", "pattern": r"从以上分析可以看出", "description": "AI套话—从以上分析可以看出", "sort_order": 44},
{"rule_type": "ai_telltale", "pattern": r"来说说到底", "description": "AI套话—来说说到底", "sort_order": 45},
]
class KeywordDomainMapResponse(BaseModel):
id: int
pattern: str
domain: str
sort_order: int
is_active: bool
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class KeywordDomainMapCreate(BaseModel):
pattern: str
domain: str
sort_order: int = 0
class KeywordDomainMapUpdate(BaseModel):
pattern: Optional[str] = None
domain: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class SensitiveWordResponse(BaseModel):
id: int
word: str
category: str
is_active: bool
added_by: Optional[str] = None
created_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class SensitiveWordCreate(BaseModel):
word: str
category: str = "general"
class ContentCleanRuleResponse(BaseModel):
id: int
rule_type: str
pattern: str
description: Optional[str] = None
is_active: bool
sort_order: int
created_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class ContentCleanRuleCreate(BaseModel):
rule_type: str
pattern: str
description: Optional[str] = None
sort_order: int = 0
class ContentCleanRuleUpdate(BaseModel):
pattern: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
sort_order: Optional[int] = None
class TrendFieldMappingResponse(BaseModel):
id: int
trend_keyword: str
field_name: str
sort_order: int
is_active: bool
created_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class TrendFieldMappingCreate(BaseModel):
trend_keyword: str
field_name: str
sort_order: int = 0
class TrendFieldMappingUpdate(BaseModel):
trend_keyword: Optional[str] = None
field_name: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class SystemConfigValueResponse(BaseModel):
key: str
value: Optional[str] = None
description: Optional[str] = None
def _ensure_defaults(db: Session):
if db.query(KeywordDomainMap).count() == 0:
for item in DEFAULT_KEYWORD_DOMAIN_MAP:
db.add(KeywordDomainMap(**item))
if db.query(SensitiveWord).count() == 0:
for item in DEFAULT_SENSITIVE_WORDS:
db.add(SensitiveWord(**item))
if db.query(ContentCleanRule).count() == 0:
for item in DEFAULT_CONTENT_CLEAN_RULES:
db.add(ContentCleanRule(**item))
else:
# 补入缺失的 ai_telltale 规则(不覆盖已存在的内容)
existing_patterns = {r.pattern for r in db.query(ContentCleanRule).filter(ContentCleanRule.rule_type == 'ai_telltale').all()}
for item in DEFAULT_CONTENT_CLEAN_RULES:
if item['rule_type'] == 'ai_telltale' and item['pattern'] not in existing_patterns:
db.add(ContentCleanRule(**item))
if any(item['rule_type'] == 'ai_telltale' and item['pattern'] not in existing_patterns for item in DEFAULT_CONTENT_CLEAN_RULES):
db.flush()
if db.query(TrendFieldMapping).count() == 0:
for item in DEFAULT_TREND_FIELD_MAP:
db.add(TrendFieldMapping(**item))
for name, pain in DEFAULT_CHINA_PAIN_TEMPLATES.items():
cat = db.query(CollectorCategory).filter(CollectorCategory.name == name).first()
if cat and not cat.pain_template:
cat.pain_template = pain
db.commit()
@router.get("/keyword-domain-map", response_model=List[KeywordDomainMapResponse])
def list_keyword_domain_map(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
_ensure_defaults(db)
return db.query(KeywordDomainMap).order_by(KeywordDomainMap.sort_order).all()
@router.post("/keyword-domain-map", response_model=KeywordDomainMapResponse)
def create_keyword_domain_map(data: KeywordDomainMapCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = KeywordDomainMap(**data.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item
@router.put("/keyword-domain-map/{item_id}", response_model=KeywordDomainMapResponse)
def update_keyword_domain_map(item_id: int, data: KeywordDomainMapUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(KeywordDomainMap).filter(KeywordDomainMap.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
for k, v in data.model_dump(exclude_unset=True).items():
if v is not None:
setattr(item, k, v)
db.commit()
db.refresh(item)
return item
@router.delete("/keyword-domain-map/{item_id}")
def delete_keyword_domain_map(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(KeywordDomainMap).filter(KeywordDomainMap.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.get("/sensitive-words", response_model=List[SensitiveWordResponse])
def list_sensitive_words(category: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
_ensure_defaults(db)
q = db.query(SensitiveWord)
if category:
q = q.filter(SensitiveWord.category == category)
return q.order_by(SensitiveWord.id).all()
@router.post("/sensitive-words", response_model=SensitiveWordResponse)
def create_sensitive_word(data: SensitiveWordCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = SensitiveWord(**data.model_dump(), added_by=admin_user.username)
db.add(item)
db.commit()
db.refresh(item)
return item
@router.put("/sensitive-words/{item_id}", response_model=SensitiveWordResponse)
def update_sensitive_word(item_id: int, body: SensitiveWordCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(SensitiveWord).filter(SensitiveWord.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
item.word = body.word
item.category = body.category
db.commit()
db.refresh(item)
return item
@router.delete("/sensitive-words/{item_id}")
def delete_sensitive_word(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(SensitiveWord).filter(SensitiveWord.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.get("/content-clean-rules", response_model=List[ContentCleanRuleResponse])
def list_content_clean_rules(rule_type: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
_ensure_defaults(db)
q = db.query(ContentCleanRule)
if rule_type:
q = q.filter(ContentCleanRule.rule_type == rule_type)
return q.order_by(ContentCleanRule.sort_order).all()
@router.post("/content-clean-rules", response_model=ContentCleanRuleResponse)
def create_content_clean_rule(data: ContentCleanRuleCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = ContentCleanRule(**data.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item
@router.put("/content-clean-rules/{item_id}", response_model=ContentCleanRuleResponse)
def update_content_clean_rule(item_id: int, data: ContentCleanRuleUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(ContentCleanRule).filter(ContentCleanRule.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
for k, v in data.model_dump(exclude_unset=True).items():
if v is not None:
setattr(item, k, v)
db.commit()
db.refresh(item)
return item
@router.delete("/content-clean-rules/{item_id}")
def delete_content_clean_rule(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(ContentCleanRule).filter(ContentCleanRule.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.get("/trend-field-mapping", response_model=List[TrendFieldMappingResponse])
def list_trend_field_mapping(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
_ensure_defaults(db)
return db.query(TrendFieldMapping).filter(TrendFieldMapping.is_active == True).order_by(TrendFieldMapping.sort_order).all()
@router.post("/trend-field-mapping", response_model=TrendFieldMappingResponse)
def create_trend_field_mapping(data: TrendFieldMappingCreate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = TrendFieldMapping(**data.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item
@router.put("/trend-field-mapping/{item_id}", response_model=TrendFieldMappingResponse)
def update_trend_field_mapping(item_id: int, data: TrendFieldMappingUpdate, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(TrendFieldMapping).filter(TrendFieldMapping.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
for k, v in data.model_dump(exclude_unset=True).items():
if v is not None:
setattr(item, k, v)
db.commit()
db.refresh(item)
return item
@router.delete("/trend-field-mapping/{item_id}")
def delete_trend_field_mapping(item_id: int, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
item = db.query(TrendFieldMapping).filter(TrendFieldMapping.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="未找到")
db.delete(item)
db.commit()
return {"message": "删除成功"}
@router.get("/system-config/{key}", response_model=SystemConfigValueResponse)
def get_system_config(key: str, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
from ..models import SystemConfig
cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first()
if not cfg:
default_map = {
"trend_domains": json.dumps(DEFAULT_DOMAINS),
"rss_default_keywords": json.dumps(DEFAULT_GLOBAL_RSS_KEYWORDS),
"priority_weights": json.dumps(DEFAULT_PRIORITY_WEIGHTS),
}
if key in default_map:
return SystemConfigValueResponse(key=key, value=default_map[key], description=f"系统默认配置 - {key}")
raise HTTPException(status_code=404, detail="未找到")
return SystemConfigValueResponse(key=cfg.key, value=cfg.value, description=cfg.description)
@router.put("/system-config/{key}")
def update_system_config(key: str, value: str, description: Optional[str] = None, db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
from ..models import SystemConfig
cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first()
if cfg:
cfg.value = value
if description is not None:
cfg.description = description
else:
cfg = SystemConfig(key=key, value=value, description=description or key)
db.add(cfg)
db.commit()
return {"message": "保存成功", "key": key, "value": value}
@router.get("/system-configs")
def list_system_configs(db: Session = Depends(get_db), admin_user=Depends(get_current_admin)):
from ..models import SystemConfig
configs = db.query(SystemConfig).order_by(SystemConfig.key).all()
defaults = {
"trend_domains": json.dumps(DEFAULT_DOMAINS),
"rss_default_keywords": json.dumps(DEFAULT_GLOBAL_RSS_KEYWORDS),
"priority_weights": json.dumps(DEFAULT_PRIORITY_WEIGHTS),
}
result = []
for c in configs:
result.append({"key": c.key, "value": c.value, "description": c.description, "is_default": False})
for k, v in defaults.items():
if not any(x["key"] == k for x in result):
result.append({"key": k, "value": v, "description": f"系统默认 - {k}", "is_default": True})
return result
@@ -0,0 +1,469 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import Optional
from datetime import datetime, timezone
from ..database import get_db
from ..models import ExternalProduct, PromotionCampaign, CampaignKeyword, SEOAudit, KeywordRanking, OptimizationTask, User
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/external", tags=["external_promotion"])
def _org_filtered_query(db, model, current_user):
query = db.query(model)
of = org_filter(current_user, model)
if of is not True:
query = query.filter(of)
return query
# ===== External Products =====
@router.get("/products")
def list_products(
type: Optional[str] = None,
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, ExternalProduct, current_user)
if type:
query = query.filter(ExternalProduct.type == type)
products = query.order_by(desc(ExternalProduct.created_at)).limit(limit).all()
return {"ok": True, "data": [p.to_dict() for p in products]}
@router.get("/products/{product_id}")
def get_product(
product_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
product = _org_filtered_query(db, ExternalProduct, current_user).filter(
ExternalProduct.id == product_id
).first()
if not product:
raise HTTPException(status_code=404, detail="产品不存在")
return {"ok": True, "data": product.to_dict()}
@router.post("/products")
def create_product(
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
product = ExternalProduct(
name=body.get("name"),
type=body.get("type", "website"),
url=body.get("url"),
domain=body.get("domain"),
app_id=body.get("app_id"),
account_id=body.get("account_id"),
description=body.get("description"),
org_id=current_user.org_id,
)
db.add(product)
db.commit()
db.refresh(product)
return {"ok": True, "data": product.to_dict()}
@router.put("/products/{product_id}")
def update_product(
product_id: int,
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
product = _org_filtered_query(db, ExternalProduct, current_user).filter(
ExternalProduct.id == product_id
).first()
if not product:
raise HTTPException(status_code=404, detail="产品不存在")
for field in ("name", "type", "url", "domain", "app_id", "account_id", "description"):
if field in body:
setattr(product, field, body[field])
db.commit()
db.refresh(product)
return {"ok": True, "data": product.to_dict()}
@router.delete("/products/{product_id}")
def delete_product(
product_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
product = _org_filtered_query(db, ExternalProduct, current_user).filter(
ExternalProduct.id == product_id
).first()
if not product:
raise HTTPException(status_code=404, detail="产品不存在")
db.delete(product)
db.commit()
return {"ok": True, "message": "已删除"}
# ===== Promotion Campaigns =====
@router.get("/campaigns")
def list_campaigns(
product_id: Optional[int] = None,
status: Optional[str] = None,
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, PromotionCampaign, current_user)
if product_id:
query = query.filter(PromotionCampaign.product_id == product_id)
if status:
query = query.filter(PromotionCampaign.status == status)
campaigns = query.order_by(desc(PromotionCampaign.created_at)).limit(limit).all()
result = []
for c in campaigns:
d = c.to_dict()
product = db.query(ExternalProduct).filter(ExternalProduct.id == c.product_id).first()
d["product_name"] = product.name if product else None
result.append(d)
return {"ok": True, "data": result}
@router.get("/campaigns/{campaign_id}")
def get_campaign(
campaign_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
campaign = _org_filtered_query(db, PromotionCampaign, current_user).filter(
PromotionCampaign.id == campaign_id
).first()
if not campaign:
raise HTTPException(status_code=404, detail="活动不存在")
d = campaign.to_dict()
product = db.query(ExternalProduct).filter(ExternalProduct.id == campaign.product_id).first()
d["product_name"] = product.name if product else None
return {"ok": True, "data": d}
@router.post("/campaigns")
def create_campaign(
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
product = db.query(ExternalProduct).filter(ExternalProduct.id == body.get("product_id")).first()
if not product:
raise HTTPException(status_code=404, detail="产品不存在")
campaign = PromotionCampaign(
name=body.get("name"),
product_id=body.get("product_id"),
status=body.get("status", "active"),
keywords=body.get("keywords", []),
target_engines=body.get("target_engines", []),
notes=body.get("notes"),
org_id=current_user.org_id,
)
db.add(campaign)
db.commit()
db.refresh(campaign)
d = campaign.to_dict()
d["product_name"] = product.name
return {"ok": True, "data": d}
@router.put("/campaigns/{campaign_id}")
def update_campaign(
campaign_id: int,
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
campaign = _org_filtered_query(db, PromotionCampaign, current_user).filter(
PromotionCampaign.id == campaign_id
).first()
if not campaign:
raise HTTPException(status_code=404, detail="活动不存在")
for field in ("name", "status", "keywords", "target_engines", "notes"):
if field in body:
setattr(campaign, field, body[field])
db.commit()
db.refresh(campaign)
d = campaign.to_dict()
product = db.query(ExternalProduct).filter(ExternalProduct.id == campaign.product_id).first()
d["product_name"] = product.name if product else None
return {"ok": True, "data": d}
@router.delete("/campaigns/{campaign_id}")
def delete_campaign(
campaign_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
campaign = _org_filtered_query(db, PromotionCampaign, current_user).filter(
PromotionCampaign.id == campaign_id
).first()
if not campaign:
raise HTTPException(status_code=404, detail="活动不存在")
db.delete(campaign)
db.commit()
return {"ok": True, "message": "已删除"}
# ===== Campaign Keywords =====
@router.get("/keywords")
def list_keywords(
campaign_id: Optional[int] = None,
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, CampaignKeyword, current_user)
if campaign_id:
query = query.filter(CampaignKeyword.campaign_id == campaign_id)
keywords = query.order_by(desc(CampaignKeyword.created_at)).limit(limit).all()
return {"ok": True, "data": [k.to_dict() for k in keywords]}
@router.post("/keywords")
def create_keyword(
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
campaign = db.query(PromotionCampaign).filter(PromotionCampaign.id == body.get("campaign_id")).first()
if not campaign:
raise HTTPException(status_code=404, detail="活动不存在")
kw = CampaignKeyword(
campaign_id=body.get("campaign_id"),
keyword=body.get("keyword"),
search_volume=body.get("search_volume"),
difficulty=body.get("difficulty"),
current_rank=body.get("current_rank"),
target_rank=body.get("target_rank"),
org_id=current_user.org_id,
)
db.add(kw)
db.commit()
db.refresh(kw)
return {"ok": True, "data": kw.to_dict()}
@router.put("/keywords/{keyword_id}")
def update_keyword(
keyword_id: int,
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
kw = _org_filtered_query(db, CampaignKeyword, current_user).filter(
CampaignKeyword.id == keyword_id
).first()
if not kw:
raise HTTPException(status_code=404, detail="关键词不存在")
for field in ("search_volume", "difficulty", "current_rank", "target_rank"):
if field in body:
setattr(kw, field, body[field])
if body.get("update_last_checked"):
kw.last_checked = datetime.now(timezone.utc)
db.commit()
db.refresh(kw)
return {"ok": True, "data": kw.to_dict()}
@router.delete("/keywords/{keyword_id}")
def delete_keyword(
keyword_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
kw = _org_filtered_query(db, CampaignKeyword, current_user).filter(
CampaignKeyword.id == keyword_id
).first()
if not kw:
raise HTTPException(status_code=404, detail="关键词不存在")
db.delete(kw)
db.commit()
return {"ok": True, "message": "已删除"}
# ===== SEO Audits =====
@router.get("/audits")
def list_audits(
product_id: Optional[int] = None,
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, SEOAudit, current_user)
if product_id:
query = query.filter(SEOAudit.product_id == product_id)
audits = query.order_by(desc(SEOAudit.checked_at)).limit(limit).all()
result = []
for a in audits:
d = a.to_dict()
product = db.query(ExternalProduct).filter(ExternalProduct.id == a.product_id).first()
d["product_name"] = product.name if product else None
result.append(d)
return {"ok": True, "data": result}
@router.get("/audits/{audit_id}")
def get_audit(
audit_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
audit = _org_filtered_query(db, SEOAudit, current_user).filter(
SEOAudit.id == audit_id
).first()
if not audit:
raise HTTPException(status_code=404, detail="审计记录不存在")
d = audit.to_dict()
d["raw_data"] = audit.raw_data or {}
tasks = db.query(OptimizationTask).filter(OptimizationTask.audit_id == audit_id).all()
d["optimization_tasks"] = [t.to_dict() for t in tasks]
return {"ok": True, "data": d}
# ===== Keyword Rankings =====
@router.get("/rankings")
def list_keyword_rankings(
campaign_id: Optional[int] = None,
keyword_id: Optional[int] = None,
product_id: Optional[int] = None,
search_engine: Optional[str] = None,
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, KeywordRanking, current_user)
if campaign_id:
query = query.filter(KeywordRanking.campaign_id == campaign_id)
if keyword_id:
query = query.filter(KeywordRanking.keyword_id == keyword_id)
if product_id:
query = query.filter(KeywordRanking.product_id == product_id)
if search_engine:
query = query.filter(KeywordRanking.search_engine == search_engine)
rankings = query.order_by(desc(KeywordRanking.checked_at)).limit(limit).all()
return {"ok": True, "data": [r.to_dict() for r in rankings]}
@router.get("/rankings/overview")
def rankings_overview(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
base = _org_filtered_query(db, KeywordRanking, current_user)
total = base.count()
with_rank = base.filter(KeywordRanking.rank.isnot(None)).count()
engine_breakdown = {}
for row in base.with_entities(KeywordRanking.search_engine,
KeywordRanking.rank).all():
eng = row.search_engine
if eng not in engine_breakdown:
engine_breakdown[eng] = {"total": 0, "with_rank": 0}
engine_breakdown[eng]["total"] += 1
if row.rank is not None:
engine_breakdown[eng]["with_rank"] += 1
return {
"ok": True,
"data": {
"total": total,
"with_rank": with_rank,
"engine_breakdown": engine_breakdown,
}
}
# ===== Optimization Tasks =====
@router.get("/optimization-tasks")
def list_optimization_tasks(
product_id: Optional[int] = None,
audit_id: Optional[int] = None,
status: Optional[str] = None,
severity: Optional[str] = None,
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
query = _org_filtered_query(db, OptimizationTask, current_user)
if product_id:
query = query.filter(OptimizationTask.product_id == product_id)
if audit_id:
query = query.filter(OptimizationTask.audit_id == audit_id)
if status:
query = query.filter(OptimizationTask.status == status)
if severity:
query = query.filter(OptimizationTask.severity == severity)
tasks = query.order_by(desc(OptimizationTask.created_at)).limit(limit).all()
return {"ok": True, "data": [t.to_dict() for t in tasks]}
@router.patch("/optimization-tasks/{task_id}")
def update_optimization_task(
task_id: int,
body: dict,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
task = _org_filtered_query(db, OptimizationTask, current_user).filter(
OptimizationTask.id == task_id
).first()
if not task:
raise HTTPException(status_code=404, detail="优化任务不存在")
if "status" in body:
task.status = body["status"]
if "severity" in body:
task.severity = body["severity"]
if "recommendation" in body:
task.recommendation = body["recommendation"]
db.commit()
db.refresh(task)
return {"ok": True, "data": task.to_dict()}
@router.get("/overview")
def external_overview(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
products = _org_filtered_query(db, ExternalProduct, current_user).count()
campaigns = _org_filtered_query(db, PromotionCampaign, current_user).count()
active_campaigns = _org_filtered_query(db, PromotionCampaign, current_user).filter(
PromotionCampaign.status == "active"
).count()
audits = _org_filtered_query(db, SEOAudit, current_user).count()
keywords = _org_filtered_query(db, CampaignKeyword, current_user).count()
open_tasks = _org_filtered_query(db, OptimizationTask, current_user).filter(
OptimizationTask.status == "open"
).count()
type_breakdown = {}
for row in _org_filtered_query(db, ExternalProduct, current_user).with_entities(
ExternalProduct.type
).all():
t = row.type
type_breakdown[t] = type_breakdown.get(t, 0) + 1
return {
"ok": True,
"data": {
"products": products,
"campaigns": campaigns,
"active_campaigns": active_campaigns,
"audits": audits,
"keywords": keywords,
"open_tasks": open_tasks,
"type_breakdown": type_breakdown,
}
}

Some files were not shown because too many files have changed in this diff Show More