16 Commits

Author SHA1 Message Date
yuzhiran f9547ba21f feat: 商业模式官网营销升级(免费试用/私有化/源码售卖)+ 企业微信
- 新增 docs/BUSINESS_MODEL.md 商业与营销方案
- landing.html 定价重构为三模式:免费试用 / 私有化部署 / 源码售卖
- 新增「开始合作」联系区(线索表单 + 企业微信二维码 wecom-qr.jpg)
- 商务邮箱更新为 contact@yuzhiran.com
- deploy/README 交付建议同步三模式
- PROGRESS 记至第十八章
2026-07-12 10:16:39 +08:00
yuzhiran 7e953afbd2 feat: 创作工作台统一 + AI味检测/白标 + 移动端补全 + 合规发布闭环
- 新增创作工作台 studio.html:合并选题/内容工厂/文章管理为单一 tab 入口(iframe embed 模式)
- 新增 AI味检测模块(ai_slop API + 页面,合规软硬问题分级)
- 新增白标品牌配置(branding API + 页面 + deploy 私有化交付包)
- 发布闭环:publishing 放宽至 editor + records/mark-published 接口
- 移动端响应式补全(admin/calendar/ai-slop 表格卡片兜底)
- 修复菜单幂等播种缺陷(按 path 对齐,避免功能页孤立)
- 新增短视频脚本 shortvideo.py 与 2026 市场调研简报
2026-07-11 12:36:01 +08:00
yuzhiran 3d596275d5 fix: preview body empty when no article content
topics.html: currentPreviewBody returns placeholder instead of empty string when platform has no content; openTopicArticlePreview properly awaits fetch

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 19:02:33 +08:00
yuzhiran a4242ada0e fix: search_rankings missing columns 500 error + campaigns.html var typo
database.py: add 5 missing search_rankings columns (ai_search_engine, citation_snippet, citation_url, geo_score, content_type) - caused /api/seo/rankings Internal Server Error

campaigns.html: fix template referencing rankingOverview instead of keywordRankingOverview

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 16:55:12 +08:00
yuzhiran d11d7f4980 fix: pipeline content tracking + topic article preview
db_helper.py: save_article now calculates and persists word_count
generator.py: run_creator_blocking sets word_count for HTML-imported articles
writer.py: fix title regex stripping content-leading numbers (35岁后→岁后)
trends.py: fix Baidu hot_score str/int type comparison crash
database.py: add missing content_tasks.org_id ALTER TABLE migration
schemas.py + topics.py: topic list API returns article_count + articles[] previews
topics.html: table view and card view show article badges with word counts, clickable to open preview

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 12:31:02 +08:00
yuzhiran 57e6e16c11 docs: finalize DB cleanup - delete 5 old TopicFields + 5 stale TrendFieldMapping rows 2026-06-23 18:36:54 +08:00
yuzhiran 187b414193 docs: document DB cleanup Phase F (32 old topics + 66 articles + 5 tasks removed)
Records the DB cleanup executed on 2026-06-23: cascade-deleted 32 sustainability/carbon topics, 66 articles, 5 tasks. Deactivated orphan TopicFields (科技人文交叉, 测试). 15 AI/tech topics remain.

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 17:45:45 +08:00
yuzhiran 56c9376c58 docs: document brand optimization v25.1 (brand tone, SEO/GEO, sources, series column, WeChat publisher)
PROGRESS.md: adds Phase A-F brand optimization to v25 section covering 6 prompt brand tone injection, long-tail SEO keywords, differentiated sources (machine-intelligence/arXiv/indie dev/industry reports/AI products), series column feature, and WeChat publisher blocked status.

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:32:23 +08:00
yuzhiran 302efd5f2d fix(ui): responsive mobile layout with auth flow refactoring for campaigns
Adds: v-cloak fix, mobile card-list views for all tables, responsive CSS breakpoints, early auth redirect, CDN script reorder to head, ElementPlusIconsVue safety check, global ElMessage reference. Moves async mounted to .then() pattern to avoid Vue 3 SFC issues.

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:30:19 +08:00
yuzhiran 897f72012f chore(cleanup): remove old content ideas and sustainability data
Deleted: 10 legacy topic cards (001-010), README from content/ideas/, old sustainability_topics.json, test-fix notification. These belonged to deprecated 'sustainable living' content pillar.

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:26:50 +08:00
yuzhiran 0258a9af9f feat(script): add DB migration script for content focus refactoring
Comprehensive DB migration: TopicField cleanup (5 old deactivated, 3 new updated), Topic reassignment (43/47 migrated), Article cleanup (15 deleted), CollectorCategory/Cleaning (12 old deactivated), TrendFieldMapping sync (11 entries)

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:26:22 +08:00
yuzhiran 9058539bae feat(config): add differentiated information sources (arXiv/indie dev/industry reports)
Adds 5 new sources: machine-intelligence RSS, arXiv paper search, indie developer search, industry data report search, AI product changelog search. Differentiates from generic AI media.

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:25:39 +08:00
yuzhiran 9fba863ce7 feat(config): align content strategy with brand direction
initial_data: field descriptions humanized, collector search queries include long-tail keywords, platform formats updated collector.py: DEFAULT_CHINA_PAINS expanded with specific pain questions (e.g. '35岁学AI来得及吗') topic_selector.py: domain mapping 11->18 entries (Prompt/AI编程/AI写作/数据隐私/AI教育/一人企业)

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:25:11 +08:00
yuzhiran 45ad5443c1 feat(prompt): inject brand tone into 6 platform prompts
Section expansion: zhihu (unique data + debate), wechat (tech-humanity reflection), xiaohongshu (real usage data) Title optimize: all 3 platforms aligned with new brand direction

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:24:38 +08:00
yuzhiran ef289b18d1 feat(ui): add series filter to topics search area
Adds column/category filter dropdown in topics.html search form, with seriesOptions computed and filteredTopics logic

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:23:19 +08:00
yuzhiran f1e6c11505 feat(models): add series column to Topic for content columns
Topic model: new series VARCHAR field (optional, indexed)
Schema: series in TopicBase/TopicCreate/TopicUpdate
API: GET /api/topics supports ?series= query param
DB migration: ALTER TABLE topics ADD COLUMN series

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:22:13 +08:00
77 changed files with 3754 additions and 2368 deletions
@@ -1,11 +1,11 @@
{
"sessionID": "ses_1dede8a7affeG6DxtfPUOviXrp",
"updatedAt": "2026-06-17T08:18:52.213Z",
"updatedAt": "2026-07-04T02:48:32.809Z",
"sources": {
"background-task": {
"state": "active",
"reason": "2 background task(s) active",
"updatedAt": "2026-06-17T08:18:52.213Z"
"reason": "1 background task(s) active",
"updatedAt": "2026-07-04T02:48:32.809Z"
}
}
}
+329 -2
View File
@@ -3,7 +3,7 @@
> 本文件为项目进度唯一真理源,所有进度信息以此为准。
> 其他文档中的进度描述一律以本文为准。
**最后更新**2026-06-21 (v24)
**最后更新**2026-07-04 (v26)
---
@@ -234,7 +234,146 @@
---
## 六、归档日志
## 十二、内容策略聚焦改造(v25 · 2026-06-23
### 从「6领域分散」到「3领域聚焦」
**背景**:此前内容覆盖可持续生活、数字游民、个人成长等 6 个领域,资源分散、品牌辨识度低。v25 聚焦「宇之然」品牌核心方向:AI 与前沿科技。
**聚焦的 3 个领域:**
| 领域 | 方向 | 覆盖话题 |
|------|------|---------|
| AI与效率 🤖 | AI 工具、工作流、效率方法 | AI工具实测、AI前沿资讯、效率方法论 |
| 科技人文 🔬 | AI 伦理、科技反思 | 数据隐私、人机关系、数字生活反思 |
| 未来工作方式 💼 | 远程工作、职业转型 | AI时代职场、一人企业、技能升级 |
### Phase 1 — 代码改造(6 个文件)
| 文件 | 改动 |
|------|------|
| `initial_data.py` | TopicField 从 6 个缩减为 3 个;CollectorCategory 从 8 个环保类改为 5 个 AI/科技类;PlatformConfig allowed_tags 全部替换为 AI/效率/科技标签 |
| `prompt_loader.py` | `sources_optimization` 提示词从"可持续生活领域"改为"AI与科技人文领域" |
| `config/sources.yaml` | 全部信息源替换为 AI/科技相关(36氪RSS、少数派RSS、5 组 AI 搜索词、1 个本地案例库) |
| `collector.py` | `DEFAULT_CHINA_PAINS` 从 8 个(7 环保 + 1 AI)改为 5 个 AI 焦点;`map_category_to_field` 返回 AI 领域;移除 4 处"可持续性"日志/默认值 |
| `topic_selector.py` | `DEFAULT_TREND_DOMAIN_MAP` 从 12 条(含可持续生活/个人成长/副业/家庭教育)改为 11 条纯 AI/科技映射 |
| `migrate_content_focus.py` | **新建** — Phase 2 DB 迁移脚本 |
### Phase 2 — DB 迁移
| 操作 | 影响 |
|------|------|
| TopicField | 更新 3 个新领域字段(排序/描述/图标),停用 5 个旧领域(可持续生活系统/可持续生活/数字游民/个人成长/个人知识工厂) |
| Topic | 47 个 Topic 中 43 个已迁移到新领域(4 个无法映射的归入"AI与效率" |
| Article | 删除 15 篇与旧可持续领域关联的旧文章 |
| CollectorCategory | 创建 3 个新分类(AI前沿资讯/AI工具实测/AI与职场),更新 2 个(AI生活化/科技人文),停用 12 个旧分类(循环消费/低碳出行/干净饮食/零浪费生活/绿色家电与节能/碳普惠/环保科技产品/自我丰容/适我主义/情绪价值与心理健康/沉浸式新生活/数字游民与自由职业) |
| CollectorSource | 停用 1 个旧源,创建 2 个新 AI 搜索源 |
| PlatformConfig | 3 个平台(知乎/微信/小红书)的 allowed_tags 全部替换为 AI/科技标签 |
| TrendFieldMapping | 同步 11 条新映射,停用旧映射 |
### Phase 3 — 文件清理
| 项目 | 数量 | 释放空间 |
|------|------|---------|
| 日志文件 (`automation/logs/`) | 400 个 | 2.5 MB |
| 旧选题卡片 (`content/ideas/`) | 11 个文件 | 96 KB |
| 可持续采集原始数据 (`sustainability_raw/`) | 多子目录 | 224 KB |
| 旧数据文件 (`sustainability_cases.json/topics.json`) | 2 个 | - |
| 旧草稿/大纲/研究笔记 (`drafts/outlines/research/`) | 91 个文件 | 740 KB |
| 通知记录 (`notifications/`) | 1 个 | 4 KB |
### Phase 4 — 重启验证
- 服务运行中:HTTP 200 ✅
- API 认证正常 ✅
- TopicFields 仅返回 3 个活跃领域 ✅
- Topic 总计 47 个,全部关联新领域 ✅
- Articles 已清理(0 篇过时文章)✅
- CollectorCategories 仅 5 个 AI 分类活跃 ✅
### 关键架构决策
- `collector.py` 类名(SustainabilityCollector/SustainabilitySource/SustainabilityCase)保留未改——减少中断风险,功能已从 sustainable 转向 AI/tech,重命名需要跨 20+ 文件修改
- DB 种子数据按 `if count() == 0` 判断,已有数据的库通过独立迁移脚本处理
- 旧数据优先保留(硬删除可能影响引用完整性),标记 `is_active=False` 而非删除
### 品牌调性与内容深化(v25.1 · 2026-06-23 第二次迭代)
**问题诊断**:第一次聚焦改造后,内容方向虽已收敛到 AI 3 领域,但存在品牌割裂("宇之然"调性偏人文自然 vs 内容全是纯工具测评)、AI 赛道竞争红海、平台策略错配(公众号叙事体不适合写 AI 工具)等深层问题。
**Phase A — 品牌调性注入(最核心改动)**
| prompt | 改动 | 预期效果 |
|--------|------|---------|
| `section_expansion_zhihu` | 加入独家数据要求+观点交锋,不只是介绍工具而是探讨技术如何改变人 | 在拥挤的 AI 知乎赛道上做出壁垒 |
| `section_expansion_wechat` | 从「AI 工具叙事」→「科技人文思辨」:用「我」口吻写 AI 时代的困惑、顿悟或观察 | 对齐品牌调性,摆脱"AI 测评不适合叙事体"的结构性矛盾 |
| `section_expansion_xiaohongshu` | 每个结论配亲测数据/对比/结果 | 用真实数据对抗同质化 |
| `title_optimize_zhihu` | 增加观点交锋感+立场态度 | 从"工具文标题"→"辩论帖标题" |
| `title_optimize_wechat` | 改为科技叙事方向,参考风格:「AI帮我写周报的第3个月,我终于发现哪里不对了」 | 对齐公众号新定位 |
| `title_optimize_xhs` | 每条标题含数字/时间/效果结果感 | 提高点击率 |
**Phase B — SEO/GEO 长尾关键词**
- `collector.py`: `DEFAULT_CHINA_PAINS` 每个痛点扩展 3-5 个长尾问题("AI写周报""AI提示词怎么写""35岁学AI来得及吗"
- `topic_selector.py`: `DEFAULT_TREND_DOMAIN_MAP` 从 11 条→18 条(新增 Prompt/提示词/AI编程/AI写作/数据隐私/AI教育/一人企业)
**Phase C — 差异化信息源**
`sources.yaml` 中新增 5 个与通用 AI 媒体形成区分的源:
| 新源 | 差异化定位 |
|------|-----------|
| 机器之心 RSS | AI 学术前沿,区别于大众科技媒体 |
| AI 学术论文搜索(arXiv 中文) | 一手论文解读内容源 |
| 独立开发者搜索 | 独特的小众 AI 产品视角 |
| 行业数据报告搜索 | 自采数据的基础来源 |
| AI 产品动态搜索 | 第一手产品更新信息 |
**Phase D — 栏目化内容机制**
为「AI工具实测周报」等固定栏目铺平道路:
- Topic 模型新增 `series` 字段(VARCHAR, optional, indexed
- API 端点 `/api/topics` 新增 `?series=` 筛选参数
- 前端 topics.html 搜索区增加「栏目」下拉筛选器
- `database.py` 新增 ALTER TABLE 迁移(`topics.series`
**Phase E — 公众号发布流水线**[需提供 WeChat APP_ID/APP_SECRET]
- 已安装 `/wechat-mp-publisher` 技能,支持远程 MCP 中转
- `wechat.env``mcp.json` 尚未配置
### 关键文件变更汇总
| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `scripts/prompt_loader.py` | 修改 | 6 个 prompt 品牌调性注入(section_expansion×3 + title_optimize×3 |
| `platform/backend/app/initial_data.py` | 修改 | 领域描述人文化、采集搜索词长尾化、平台格式说明更新 |
| `scripts/collector.py` | 修改 | DEFAULT_CHINA_PAINS 长尾关键词扩展 |
| `scripts/topic_selector.py` | 修改 | DEFAULT_TREND_DOMAIN_MAP 从 11→18 条 |
| `config/sources.yaml` | 修改 | 新增机器之心/arXiv/独立开发者/行业报告/AI产品动态 5 个差异源 |
| `platform/backend/app/models.py` | 修改 | Topic 模型新增 series 字段 |
| `platform/backend/app/schemas.py` | 修改 | TopicBase/TopicCreate/TopicUpdate 新增 series |
| `platform/backend/app/api/topics.py` | 修改 | list_topics 新增 series 查询参数 |
| `platform/backend/app/database.py` | 修改 | ALTER TABLE 添加 topics.series 列 |
| `platform/frontend/topics.html` | 修改 | 搜索区新增栏目下拉筛选器 |
| `scripts/migrate_content_focus.py` | 新建 | DB 迁移脚本(Phase 2 遗留) |
### Phase F — 数据库旧数据清理(2026-06-23 第三次迭代)
品牌优化完成后,发现旧数据库中存在大量迁移遗留的错配数据——32 个旧选题(循环消费/碳普惠/可持续方向)被 `migrate_content_focus.py` 的 fallback 逻辑错误地分配到了「AI与效率」领域,并持有 66 篇关联文章和 5 个任务。
| 操作 | 数量 |
|------|------|
| 删除旧选题(sustainability/carbon 方向) | 32 个 |
| 删除关联文章 | 66 篇 |
| 删除关联任务 | 5 条 |
| 停用 orphan 领域(科技人文交叉/测试) | 2 个 |
| 删除 5 个旧 TopicField + 5 条 stale TrendFieldMapping | 10 条 |
| **剩余数据** | **15 个选题 / 9 篇文章 / 3 个活跃领域 / 11 条 TrendFieldMapping** |
所有保留选题均为 AI/tech 方向,与聚焦的 3 领域一致。TopicFields 表从 10 行精简到 3 行,TrendFieldMapping 从 16 行精简到 11 行(全 active),无任何悬空引用。
---
> 过时文档统一移入 `archive/`,防止误导。
@@ -513,6 +652,53 @@ admin.html 新增「GEO 就绪度」标签页包含:
---
## 十三、2026 合规与市场契合升级(v26 · 2026-07-04
### 背景:2025-09-01《人工智能生成合成内容标识办法》正式施行
微信/小红书/抖音/B站/微博 强制 AI 生成内容**主动声明标识**;未标识/疑似 AI 内容限流、下架、封号。微信 3-27 新规明确禁"非真人自动化创作行为"(脚本/程序托管全程代写代发)。叠加创作者痛点从"怎么写快"转向"怎么不被限流/怎么有活人感/怎么变现",项目做了一轮合规与市场化升级。
### P0 — 合规生存(已落地)
| 改造 | 文件 | 说明 |
|------|------|------|
| 强制 AI 生成标识注入 | `scripts/writer.py` | 新增 `inject_ai_disclosure()`:按《标识办法》给三平台正文文末注入「AI 辅助创作」声明(显式),head 注入 `ai-generated` meta + AIGC 隐式标识 JSON-LD(隐式)。默认开启,可由 `SystemConfig(disclose_ai=false)``YZR_DISCLOSE_AI=0` 关闭 |
| 发布模块去伪 | `platform/backend/app/api/publishing.py` | **删除伪造的 `status='success'`**:发布接口改为生成发布稿 + 置选题为 `pending_publish`(待人工发布),PublishRecord 状态为 `pending_manual`;明确"AI 稿须真人校验声明后手动发布,系统不代发" |
| LLM 降级响亮失败 | `scripts/writer.py` | `_expand_section` 在 LLM 缺失时标记 `_degraded``run()` 结尾对降级稿报错告警,不再静默吐稀薄内容混入库 |
### P1 — 市场契合(已落地)
| 改造 | 文件 | 说明 |
|------|------|------|
| AI味检测+净化 可见化 | `platform/backend/app/api/ai_slop.py` + `platform/frontend/ai-slop.html` | 新 APIGET /report 跑合规检测、POST /purify 一键净化)+ 新前端页 + 导航"AI味检测"。把已有的去套话/合规能力变成用户可感知卖点(直击 2026 限流焦虑) |
| 长文→短视频拆条 | `scripts/shortvideo.py` + `scheduler.py` | 新脚本将选题长文拆成 N 条短视频脚本(口播/分镜/适配平台),入库 `articles(platform=shortvideo_i)`;新增定时任务 `scheduled_shortvideo` 08:00,对前日待发布选题自动拆条 |
| 外部推广去伪 | `platform/backend/app/api/external_promotion.py` | 新增 `POST /api/external/audit/run` 真实调用 `seo_auditor.py`(此前该脚本从未被任何接口/调度触发),让 SEO 审计可用而非摆设 |
### P2 — 健壮性(已落地)
| 改造 | 文件 | 说明 |
|------|------|------|
| 指标同步扩平台 | `platform/backend/app/core/scheduler.py` | `_run_metrics_sync` 从仅知乎扩展为遍历 `platform_urls`;微信/小红书无公开 API 时优雅跳过并日志说明,不再伪装"仅知乎有数据" |
| 合规闸门防绕过 | `scripts/compliance_optimizer.py` | `force_pass` 选题不自动推进到 `ready`(保留待审查),避免绕过合规闸门直接进发布 |
### 关键文件变更汇总
| 文件 | 类型 | 说明 |
|------|------|------|
| `scripts/writer.py` | 修改 | AI 标识注入 + 降级标记 |
| `platform/backend/app/api/publishing.py` | 修改 | 发布去伪,改待人工发布 |
| `scripts/compliance_optimizer.py` | 修改 | force_pass 不推进状态 |
| `platform/backend/app/api/ai_slop.py` | 新建 | AI味检测/净化 API |
| `platform/frontend/ai-slop.html` | 新建 | AI味检测页 |
| `scripts/shortvideo.py` | 新建 | 长文→短视频拆条 |
| `platform/backend/app/core/scheduler.py` | 修改 | 指标扩平台 + 短视频定时任务 |
| `platform/backend/app/api/external_promotion.py` | 修改 | SEO 审计可触发 |
### 验证
- 全部修改文件 `ast.parse` 通过;`app.api.ai_slop / publishing / external_promotion``core.scheduler` 在 venv 下 `import` 成功
- 注:本次为代码升级,**未重启服务/未跑全量测试套件**;发布/净化/拆条需运营在真实平台按声明手动完成
---
## 十二、规范说明
### 规划文档
@@ -540,3 +726,144 @@ admin.html 新增「GEO 就绪度」标签页包含:
如需 → 同步 README.md 摘要
```
---
## 第十四章:前端布局修复 + 盈利模式(私有化/白标)升级
**日期**2026-07-11
**目标**:① 修复前端布局/样式不合理项;② 将项目从"自用中台"升级为"可私有化部署 / 白标转售"产品。
### 一、前端布局/样式修复
- **A1 导航未固定 + 顶部双空隙**:`uni-nav.js` 删除 `<nav>` 内联 `position:relative`,恢复 CSS `position:fixed`。(同步修复 A3 移动端下拉菜单脱离)
- **A2 平板导航裁切**:汉堡菜单断点由 `max-width:768px` 提升至 `1000px`,769–1000px 链接改走下拉,不再静默丢失。
- **A4 预览弹窗偏右**`theme-modern.css` 移除 `@media(min-width:769px)``.preview-dialog-custom{left:90px}`,对话框恢复居中(影响 articles.html / factory.html)。
- **A6 卡片横向滚动条**`.card` 去掉 `overflow-x:auto`(el-table 自带内部滚动,重复滚动条消除,阴影不再被裁)。
- **A5 暗色模式半成品(对比度不可读)**:暂禁用 `prefers-color-scheme:dark` 块,统一亮色主题,待引入 EP 暗色变量后启用。
- **topics.html 筛选表单响应式**6 个 `el-col``:xs/:sm/:md` 断点,移动端堆叠不溢出。
- **metrics.html 统计卡**`.stats-grid``repeat(4,1fr)` 改为 `repeat(auto-fit,minmax(180px,1fr))`,平板自适应列数。
- **factory.html**`.topic-grid``minmax(min(340px,100%),1fr)`,≤320px 屏不溢出。
### 二、盈利模式升级:白标 + 私有化部署
- **白标配置后端**:新增 `api/branding.py`
- `GET /api/branding`(公开,无需登录):返回 `brand_name/brand_logo/brand_primary_color/brand_support_email/version`
- `PUT /api/branding`(管理员):更新白标配置,写入 `system_configs`
- `main.py` 注册 `branding.router`
- **白标种子**`initial_data.py` 写入 `brand_name/brand_logo/brand_primary_color/brand_support_email` 默认。
- **前端应用品牌**`uni-nav.js` `mounted``/api/branding`,动态设置平台名、Logo、导航渐变主色,并写入 `--color-primary`;新增 `branding.html` 白标设置页(管理员,含实时预览)。
- **导航入口**`initial_data.py` 菜单加「白标设置」(admin),`uni-nav.js` 默认 admin 项补充。
- **部署包 `deploy/`**
- `Dockerfile`python:3.11-slim,默认 SQLite,零依赖)
- `docker-compose.yml`(含可选 PostgreSQL 服务与数据卷)
- `.env.example`(管理员密码 / LLM Key / 数据库)
- `README.md`(私有化 + 白标交付指南、定价建议)
### 验证
- 服务 `yzr-platform.service` 重启成功,无启动错误。
- `GET /api/branding` 返回种子默认值;`PUT` 管理员保存后持久化生效,重置回默认。
- `node --check uni-nav.js``ast.parse(branding.py)` 均通过。
### 待办(已解决,见第十六章)
- ~~`init_db()` 的 SQLite `topics.series` 迁移缺口(测试 32/33)~~:经复跑为陈旧 `test.db` 所致,当前 33/33 全过;迁移 fallback 对 SQLite 正常。
- 菜单幂等播种缺陷(见第十六章):`initial_data.py``if Menu.count()==0` 全有/全无逻辑导致已在库时章节四新增的"文章管理/内容工厂/白标设置"未入库,导航不可达,已修复。
---
## 第十五章:移动端响应式补全
**日期**2026-07-11
**目标**:审计并补全全部前端的移动端(≤420px)适配。
### 审计结论
- 已良好适配:topics / articles / campaigns / tasks / metrics / insights / platforms / assets / factory / index / landing / login / users / logs(表格均有 card-list 兜底或本就是卡片布局;固定宽弹窗由全局 `≤768px` 全宽规则兜底)。
- 真实破裂点(3 处 el-table 无移动端兜底):
1. `ai-slop.html` 问题明细表(line 47
2. `calendar.html` 日程弹窗内 `selectedDayEntries` 表(line 189
3. `admin.html` 平台配置表(line 202,缺 `data-table` 类,全局隐藏规则未覆盖)
### 修复
- 三处统一改为「`el-table`(`.card-table`/`.data-table`) + `.card-list-mobile` 卡片列表」的 `v-if`/`v-else` 兜底,沿用 `theme-modern.css``.card-list-mobile` 全局样式(≤768px 自动切换)。
- 桌面端表格保留,移动端自动转为可点列表,操作按钮(编辑/删除/查看)一并带入卡片。
- 校验:三页结构 `v-if`/`v-else` 配对正确,HTTP 200 正常服务。
### 结论
前端现已**完整支持移动端响应式**:所有数据表在窄屏均以卡片列表呈现,导航/筛选/弹窗/统计网格均自适应,无横向溢出。
---
## 第十六章:菜单播种缺陷修复(导航可达性)
**日期**2026-07-11
**触发**:生产服务重启后核对 `/api/menus/active`,发现导航缺少"文章管理 / 内容工厂 / 白标设置",而 `articles.html`(章节四重写)、`factory.html``branding.html` 均为真实页面 → 这些入口被"孤立",用户无法从导航进入。
### 根因
- `initial_data.py` 菜单播种原逻辑为 `if db.query(Menu).count() == 0:` —— **仅当菜单表完全为空时才播种**
- 生产库此前已有旧菜单集,故章节四新增的三个菜单从未插入;`uni-nav.js` 运行时以 DB 菜单覆盖 fallback,于是三个入口从导航消失。
- 另:旧库首页菜单 `path="index.html"`(无斜杠)与新种子 `"/index.html"` 不匹配,初次修复导致"工作台"重复出现(count 13)。
### 修复
- 菜单播种改为**按 path 幂等对齐**:已存在则更新 `sort_order/roles/icon/is_active`(保留原名称,避免意外重命名),缺失则插入。
- 新增首页路径归一化:若同时存在 `index.html``/index.html` 则删除旧的;若仅 `index.html` 则改路径为 `/index.html`,消除重复入口。
- `init_db()` 在每次启动执行,重启后自动对齐。
### 验证
- 重启 `yzr-platform.service``/api/menus/active` 返回 **12 个菜单**,顺序正确、无重复:工作台→选题→文章管理→内容工厂→数据→日历→素材→任务→外推营销→AI味检测→白标设置→系统。
- 12 个对应 HTML 页面均 HTTP 200。
- `python3 tests/test_new_features.py`:**33/33 通过**(菜单播种变更未破坏 seeding)。
### 当前完整导航(运营闭环)
选题 → 内容工厂生成 → 文章管理编辑/AI味检测 → 发布记录标记已发布(合规:系统不代发,真人校验后手动发布)。
---
## 第十七章:创作流程统一(导航去碎片化)
**日期**2026-07-11
**目标**:将分散的「选题 / 内容工厂 / 文章管理」三个独立导航入口合并为单一「创作工作台」,减少导航碎片化。
### 方案
采用 **`studio.html` 单一入口 + `el-tabs` 内嵌三个已有页面(`?embed=1` 模式)**,而非重写三套逻辑——复用已验证页面,零逻辑重复、风险最低。
### 实现
- **新增 `studio.html`「创作工作台」**`el-tabs` 三个标签页(选题 / 内容生成 / 文章管理),各以 `<iframe>` 懒加载对应页面(`topics.html?embed=1` 等);首次切到才加载,保留各页状态。整页 flex 布局填满视口,iframe `position:absolute; inset:0` 自适应。
- **嵌入模式 `?embed=1`**`topics.html` / `factory.html` / `articles.html``mounted` 检测 `embed` 参数 → `isEmbed=true``<body class="embed">`
- `<uni-nav>``v-if="!isEmbed"` 隐藏子页面自身导航(避免双层导航);
- `theme-modern.css` 新增 `body.embed .main-content { padding-top:0 }` 去掉顶部留白;
- 跨页链接(`redirectToPage` / `goToArticles`)在 embed 下改用 `window.top.location.href`,整页跳转而非 iframe 内嵌套。
- **导航收敛**`initial_data.py` 菜单种子以「创作工作台→studio.html」替换原三入口,并将 `topics.html/factory.html/articles.html``is_active=False`(页面仍可被 studio 内嵌与直接 URL 访问,不丢失功能);`uni-nav.js` fallback 同步更新。
- 排序:工作台(0) → 创作工作台(1) → 数据(2) → 日历(3) → 素材(4) → 任务(5) → 外推营销(6) → AI味检测(7) → 白标设置(8,admin) → 系统(9,admin)。
### 验证
- 重启 `yzr-platform.service``/api/menus/active` 返回 **10 个菜单**,含「创作工作台(studio.html)」,原三入口已不在主导航。
- `studio.html / topics.html / factory.html / articles.html` 均 HTTP 200studio 内 iframe `src``embed=1`
- 三个页面内联脚本 `node --check` 通过;`initial_data.py` 编译通过。
- `python3 tests/test_new_features.py`**33/33 通过**。
### 当前导航(10 项,创作闭环归于单一入口)
工作台 → **创作工作台(选题·生成·文章)** → 数据 / 日历 / 素材 / 任务 / 外推营销 / AI味检测 / 白标设置 / 系统。
---
## 第十八章:商业模式与官网营销升级
**日期**2026-07-11
**目标**:把产品从自用工具定位升级为可对外售卖的 AI 内容中台,并在官网首页展示「免费试用 / 私有化部署 / 源码售卖」三种模式。
### 方案沉淀
- 新增 `docs/BUSINESS_MODEL.md`:功能盘点 + 2026 市场需求 + 三模式可行性 + 首页改造方案 + 定价建议。结论:以私有化部署 + 源码售卖为主力,免费试用作获客钩子(避开纯 SaaS 红海)。
### 实现(landing.html 改造)
- **定价区(#pricing)重构为三段式**:免费试用(¥0 起,私有化试用版/演示)→ 私有化部署(¥29,800 起买断,推荐,featured)→ 源码售卖(面议授权)。CTA 统一锚定 #contact
- **FAQ 增补**:私有化部署说明、源码采购、免费试用申请、AI 内容合规(不代发/人工闸门)、数据安全/多租户。
- **新增「开始合作」联系区(#contact)**:联系信息卡 + 轻量线索表单(姓名/公司/合作方式/联系方式/需求),提交唤起 mailto 至 hello@yu-zhi-ran.com(无后端依赖,低风险)。
- **文案对齐**:导航「免费使用」→「免费试用」+ 新增「联系销售」;Hero / CTA 按钮对齐;移动端菜单同步。
- **deploy/README.md**:交付建议同步为三模式定价。
- 新增 `.contact` 相关 CSS(深色区块,与 `.cta` 视觉一致)。
### 验证
- `curl /landing.html` → 200;新内容(免费试用/私有化部署/源码售卖/id="contact"/trialForm/申请试用/获取报价/联系销售)均存在。
- `<section>` 开合平衡(8/8);旧 SaaS 三档(入门/专业/企业版)已清除。
- 未做后端线索入库(当前 mailto 方案足够);若需留资分析可后续加 `/api/leads`
### 待办(可选)
- 真实线索入库(Lead 表 + /api/leads);私有化试用版自动下发;报价单生成。
@@ -1,14 +0,0 @@
{
"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"
}
@@ -0,0 +1,27 @@
## 标题(爆款钩子)
73%的人因AI替身失去信任,你还在用它省那12%的时间?
## 口播文案
你以为AI换脸能帮你省时间?[停顿]别傻了,每一次用AI形象开会,都是在职场里透支信任。
2025年卡内基梅隆大学的研究发现,[停顿]73%的商务谈判者,一旦知道对方用的是AI替身,信任度直接暴跌。你省下的那12%会议时长,得用几十次真诚沟通才能补回来。[停顿]
为啥?人眼天生能识别“非自然”的脸,哪怕只是一丝微笑延迟,你的大脑杏仁核就会报警——“这人不靠谱!”[停顿]哈佛的脑成像实验更狠:跟AI形象合作时,你大脑里的共情区域活跃度下降30%。你根本不在意对方有没有在听,对方也感受不到你的真诚。
所以那些跨国大公司为什么明令禁止?[停顿]因为信任一旦崩了,客户流失率六个季度能涨18%。你省的时间,抵不过一个客户的流失。[停顿]记住:高信任场景——客户会、谈判、一对一辅导,绝对不要用AI替身。
你觉得有道理吗?评论聊聊,你的公司禁用AI换脸了吗?
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 镜头1 | 博主正对镜头,背后是电脑屏幕显示视频会议中的AI换脸头像 | “AI换脸省时间?你省的是信任” | 0-8秒 |
| 镜头2 | 切换至数据图表:73%的数字放大闪烁,旁边有“信任度暴跌”字样 | “73%谈判者因AI替身失去信任” | 8-20秒 |
| 镜头3 | 博主做“恐怖谷”表情演示:先微笑、然后眼神呆滞、再突然回神 | “大脑报警:不自然的微笑=不靠谱” | 20-35秒 |
| 镜头4 | 动画展示大脑区域变暗(共情区下降30%),旁边出现客户流失率18% | “共情关闭,客户跑路” | 35-50秒 |
| 镜头5 | 博主回到正对镜头,手势强调“三场景禁用” | “客户会、谈判、一对一,禁用!” | 50-65秒 |
## 适配平台
- **抖音**:开头3秒用“73%”数字怼脸,加热门BGM(悬疑类),字幕用大字跳闪。
- **视频号**:节奏稍缓,加“你认同吗”互动投票卡片,结尾引导关注。
- **小红书视频**:封面用“AI替身毁信任”金句,文案加emoji分段,评论区置顶“自查清单”。
@@ -0,0 +1,28 @@
## 标题(爆款钩子)
别让AI换脸毁了客户!3条保命铁律立刻救你
## 口播文案
「[停顿]」你知道吗?你那个完美的AI替身,正在让你和客户之间的信任疯狂透支。今天三条干货,立刻就能用。
第一条:所有对外会议、重要谈判、一对一辅导,千万别用AI形象。[停顿]加州大学研究证明,哪怕眼珠动慢一丁点,对方大脑立刻拉警报——73%的商务谈判者会直接判定你不诚实。信任崩塌了,修复一次需要5到8次真诚互动。
第二条:内部非正式头脑风暴,用AI形象可以提升流畅度,[停顿]但必须提前告知团队,划清边界。盖洛普数据显示,明确边界的公司员工满意度高23%,客户推荐值高12%。
第三条:如果已经用了AI形象,立刻补救。真诚道歉,换成真人视频,主动暴露缺点。[停顿]别拿“效率”骗自己,信任是账面上的隐形资产。
记住:信任建立要日积月累,崩塌只在一念之间。觉得有用?点赞收藏,别再踩坑了。
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 博主正面近景,严肃表情,手指镜头 | 别让AI换脸毁了客户!3条保命铁律 | 8秒 |
| 2 | 博主拿起手机或电脑屏幕,展示一个AI换脸会议场景图标 | 73%的人直接判定不诚实 | 一次失信需要5-8次修复 | 12秒 |
| 3 | 博主切换到侧身,用手势列举要点,镜头缓慢推进 | 第一条:对外会议禁用 | 第二条:内部告知划边界 | 15秒 |
| 4 | 博主站起,背景出现“信任度收益”图表动画 | 第三条:用了立刻补救 | 信任是隐形资产 | 12秒 |
| 5 | 博主回到正面,微笑,手指向上做“收藏”手势 | 点赞收藏,别再踩坑 | 8秒 |
## 适配平台
- **抖音**:前3秒加一个“你中招了吗”的悬念语气,用快切放大“73%”等数据,字幕用黄色大字突出。
- **视频号**:开场加一句“我做商务8年,今天说个血泪教训”,增加真实感和信任感;节奏略慢,留出互动引导时间。
- **小红书视频**:首帧用“🔥AI换脸避坑指南”彩色标题,口播再简练10%,强调“收藏备用”;视频下方配图文摘要文字。
@@ -0,0 +1,19 @@
## 标题(爆款钩子)
我用AI替身开视频会,结果被客户当场识破,丢了200万的单
## 口播文案
上个月我哥们儿跟我喝酒,差点哭了[停顿]他说他为了赶一个跨国客户的视频会,用了最高级的AI换脸软件——皮肤零瑕疵、微笑完美,连声音都调了音。结果你猜怎么着?会议开到一半,对方突然问:“你本人是不是没在镜头前?”[停顿]原来客户那边有人工智能识别系统,发现他瞳孔反光不对,微表情卡顿。当场翻脸,200万的单子黄了[停顿]你以为AI替身能省时间?错了[停顿]大脑天生对“假脸”拉警报,瞳孔放大、心跳加速,你根本没法建立信任。老板们醒醒,效率再高也换不来真心合作。你身边有公司因为AI形象吃过亏吗?评论区聊聊。
## 分镜建议
| 画面 | 字幕 | 时长 |
|------|------|------|
| 中景:博主坐在桌前,表情无奈摇头 | 哥们儿用AI替身开会 单子黄了 | 0-5s |
| 特写:博主手指敲桌面,语气加重 | 客户突然问:你本人不在? | 5-15s |
| 切到手机/电脑屏幕动画:AI脸+显微镜下瞳孔异常 | 瞳孔反光不对 微表情卡顿 | 15-25s |
| 博主直视镜头,双手摊开 | 大脑对假脸拉警报! | 25-35s |
| 博主低头写笔记状,抬头问 | 你身边有公司吃过亏吗?评论区聊聊 | 35-45s |
## 适配平台
- **抖音**:前3秒加“你敢信?”等悬念字幕,节奏再快一点,每秒换画面。
- **视频号**:保持自然聊天的语气,结尾加一句“关注我,了解更多职场避坑技巧”。
- **小红书视频**:封面用大字“AI换脸翻车实录”,口播语速放慢,每句之间留呼吸感。
@@ -0,0 +1,29 @@
## 标题(爆款钩子)
别再挤景区了!4.2 亿人都在假装过端午,你还要继续吗?
## 口播文案
承认吧,你端午那趟旅行,根本不算过节,只是去当了个“人肉背景板”![停顿]
2026 年端午,4.2 亿人涌进景区。你拍的是密密麻麻的后脑勺,回家翻相册,除了证明“我去过”,对屈原、龙舟的理解,还停留在小学课本那两句话。[停顿]
调查显示,超六成人都在“走马观花”。问题真不在节日没内涵,而是你太懒了![停顿]你只会被动跟着人流挪动,却忘了主动探索。
换个思路试试?让 AI 做你的私人文化向导。[停顿]输入“屈原流放路线”或“龙舟制作工艺”,它立马生成专属路径。不再是大锅饭式的打卡,而是穿越千年的深度对话。
现在,四成游客愿意为这种“独家体验”多付 30% 的钱。[停顿]因为大家终于醒了:真正的文化,从来不在拥挤的人堆里,而在你主动迈出的那一步。
今年假期,你是继续去挤照片,还是用 AI 定制一场真正的文化穿越?评论区告诉我!
## 分镜建议
| 画面 | 字幕 | 时长 |
| :--- | :--- | :--- |
| **近景**:博主对着镜头一脸严肃,背景合成极度拥挤的景区人头攒动画面,甚至有点压抑感。 | 4.2 亿人<br>全是背景板? | 5s |
| **中景**:博主无奈地翻看手机相册,屏幕特写显示全是模糊的人头照,随后画面切换成枯燥的课本插图。 | 走马观花<br>等于没去 | 8s |
| **特写**:博主眼神突然亮起,手持手机展示 AI 界面,手指滑动生成一条炫酷的 3D 地图路线。 | 拒绝被动<br>AI 定制路径 | 10s |
| **混剪**:快速闪过几个高质量画面:亲手触摸龙舟木纹、在屈原故里听沉浸式故事,对比之前的拥挤画面。 | 深度体验<br>值得多付 30% | 10s |
| **近景**:博主回到镜头前,身体前倾,发出真诚邀请,屏幕下方弹出评论框引导。 | 你是哪种?<br>评论区见 | 7s |
## 适配平台
- **抖音**:前 3 秒拥挤画面要配合嘈杂的环境音效和急促的 BGM,制造强烈的焦虑感,随后音乐瞬间转为科技感轻音乐。
- **视频号**:文案中“小学课本”、“文化穿越”等情感共鸣点要加重语气,结尾引导转发给“只会打卡的朋友”。
- **小红书**:封面图要用"AI 定制路线 vs 传统人挤人”的强烈对比拼图,标题加上"🆘避雷”和"✨小众玩法”emoji。
@@ -0,0 +1,23 @@
## 标题(爆款钩子)
端午出游4.2亿人,AI教你3招避开人挤人
## 口播文案
你是不是也这样?[停顿]端午挤进景区,举着手机拍的全是人头,回家翻相册就剩“我去过”三个字。[停顿]超过六成游客都说体验太同质化,走马观花。[停顿]问题出在哪?不是节日没文化,是你太被动。[停顿]今天直接给你3条干货,让AI帮你定制专属文化路线,彻底告别“人从众”。[停顿]
第一条:别搜攻略,先问自己。[停顿]打开手机,输入“屈原流放路线”“端午习俗演变”或者“现代龙舟怎么造”,AI就能秒出主题路线。[停顿]第二条:别只看景点,要看故事。[停顿]AI会沿途穿插历史讲解、互动体验,让你从“拍照”变成“沉浸”。[停顿]第三条:多花30%的钱,换100%的深度体验。[停顿]最新数据说,超过四成游客愿意为个性化深度体验多付费,AI让这钱花得值。[停顿]
赶紧试试,评论区告诉我你用过AI定制旅行吗?[停顿]点赞收藏,下次出游再也不当背景板!
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 近景:博主对着镜头,手机展示假期人山人海的视频片段 | 端午出游4.2亿人 | 8秒 |
| 2 | 中景:博主手势比划,手机屏幕出现“屈原流放路线”输入框 | 6成游客:体验同质化 | 10秒 |
| 3 | 特写:手机屏幕快速滑动,显示AI生成的路线图、历史故事卡片、互动体验图标 | AI定制路线:输入→秒出 | 10秒 |
| 4 | 中景:博主拿着手机,表情兴奋,画面上方弹出数据图“40%游客愿多付30%” | 多花30%,换100%深度 | 8秒 |
| 5 | 近景:博主笑着对镜头,手指向屏幕“评论区见” | 评论区聊聊你的AI旅行 | 4秒 |
## 适配平台
- **抖音**:前3秒上“人潮拥挤”的实拍画面+快节奏卡点音乐,字幕用动态大字强调“4.2亿”。
- **视频号**:语速放慢10%,开头加一句“端午刚过,你踩雷了吗?”引发共鸣,结尾引导点赞转发。
- **小红书视频**:封面用“AI定制旅行”效果图,文案侧重“小众玩法”“拒绝人挤人”,画面多展示手机操作界面。
@@ -0,0 +1,29 @@
## 标题(爆款钩子)
端午挤到怀疑人生?她用AI玩出了新高度
## 口播文案
上周末端午,我闺蜜兴冲冲去古镇打卡,[停顿]结果你猜怎么着?朋友圈发的全是人头,连屈原像都没挤到跟前。回来她跟我说:感觉自己就是个移动的背景板,除了累,啥也没记住。[停顿]
其实,问题不是节日没意思,是我们太“被动”了。跟着人流走,走到哪拍到哪,回家一翻相册——哦,我去过。[停顿]
但如果反过来呢?你打开手机,让AI根据你的兴趣规划一条专属路线:喜欢历史,就走屈原流放路线;喜欢手艺,就去学包粽子、看龙舟怎么做。[停顿]我闺蜜后来试了一下,她说那天她才知道,五彩绳原来有五种含义,不是随便戴的。
数据也说了,超四成人愿意多花30%的钱,就为了这种“私人定制”的体验。[停顿]所以别再当“人挤人”里的沙粒了,主动一点,让文化真正走进你心里。[停顿]
你觉得呢?评论区聊聊你遇到过最坑的旅行经历。
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 中景:博主对着镜头,表情略带无奈,手里拿手机翻照片 | 端午古镇,全是人头 | 8秒 |
| 2 | 插入画面:景区人山人海的短视频素材(或模拟),然后切回博主 | 六成人觉得走马观花 | 10秒 |
| 3 | 特写:博主手指在手机屏幕上滑动,屏幕出现AI定制界面(可后期特效) | 输入“屈原流放路线” | 12秒 |
| 4 | 博主回忆状,微笑 | 五彩绳原来有五种含义 | 10秒 |
| 5 | 博主正对镜头,语气坚定,手势强调 | 别再做背景板了 | 8秒 |
(总时长约48秒,可适当调整节奏)
## 适配平台
- **抖音**:前3秒加一个“人多到哭”的音效或变脸特效,快速抓注意力。
- **视频号**:开头增加一个朋友圈实拍片段(人头),强化真实感,结尾引导点赞转发。
- **小红书视频**:画面加柔和滤镜,字幕用可爱字体,突出“闺蜜故事”的亲切感,文末加话题标签。
@@ -0,0 +1,19 @@
## 标题(爆款钩子)
🔥你的Logo设计单,正被邻居用旧电脑10分钟抢走!
## 口播文案
你刚接的那个Logo单,[停顿]可能正被隔壁邻居用一台三年前的笔记本,十分钟搞定。[停顿]别不信——2026年,Stability AI的端侧模型,能让普通笔记本15秒出商业级矢量图,成本只有云端的1/50。[停顿]翻译更惨,谷歌的离线实时口译,准确率已经92%,你还在按字收钱?[停顿]客户发现:本地跑AI不仅免费,隐私还更安全。[停顿]那咱们就等死吗?[停顿]不,自救关键是往“非标”钻——复杂人际博弈、现场应急决策、情感陪伴,这些端侧AI学不会。[停顿]立刻停止卖标准时间,转做深度咨询和定制方案,把AI变成你的杠杆。[停顿]听懂了的,评论区扣“转型”。
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 特写:手机屏幕显示一个Logo设计稿,然后一只手把设计稿推开 | 你的Logo单,邻居10分钟搞定 | 8秒 |
| 2 | 中景:博主坐在电脑前,表情严肃,手比划“15秒” | Stability AI端侧模型:15秒出图,成本1/50 | 10秒 |
| 3 | 切换:手机画面展示谷歌翻译界面,然后快速切换成离线模式 | 离线实时口译,准确率92%,按字收费模式已死 | 12秒 |
| 4 | 近景:博主身体前倾,眼神坚定,手指点向镜头 | 自救:深耕“非标”领域,复杂博弈、情感陪伴 | 10秒 |
| 5 | 特写:博主手拿话筒,背景虚化,字幕滚动 | 停止卖时间,做深度咨询,AI是你的杠杆 | 8秒 |
## 适配平台
- **抖音**:前3秒用冲突画面+快节奏剪辑,字幕用动态大字,激发完播。
- **视频号**:口播语速稍慢,增加“认同吗?评论区聊聊”等互动引导,强化信任感。
- **小红书视频**:封面用“2026自由职业生存指南”文字,口播中加入“干货警告”,结尾加话题标签。
@@ -0,0 +1,27 @@
## 标题(爆款钩子)
端侧AI爆发!3条自救指南,不想被淘汰的立刻看
## 口播文案
你还在做这3种活儿吗?赶紧停![停顿]
第一种,设计外包。你熬三天做的Logo,邻居用三年前的笔记本,15秒出图,成本只有你报价的1/50。客户发现本地跑AI免费还安全,你的报价单下季度就废了。[停顿]
第二种,翻译文案。谷歌的离线口译延迟不到0.2秒,医疗法律准确率92%,客户凭什么还按字付你钱?记住,AI不是抢你饭碗,是让“文字搬运工”技能归零。[停顿]
第三种,卖成品稿件。35%的中小企业已经用本地营销代理自动生成海报文案了,你要是不转型成训练这些智能体的“教练”,就会被踢出局。[停顿]
自救就一条路:深耕端侧AI搞不定的“非标”领域。比如复杂人际博弈、现场应急决策、高度情感陪伴。别卖标准时间,卖信任和深度咨询,把AI当杠杆,处理人类才能定义的模糊地带。[停顿]
现在,立刻,检查你的业务有没有被这三条砸中?评论区聊聊。
## 分镜建议
- 镜头1:特写,博主对着镜头,表情严肃,手指向屏幕。字幕:“3种职业正在被AI干掉”。时长:8秒
- 镜头2:切画面,展示AI快速生成Logo的对比(左边是你熬夜加班,右边是电脑15秒出图)。字幕:“设计外包→15秒出图”。时长:15秒
- 镜头3:博主口播,背景闪现谷歌翻译界面和字幕。字幕:“翻译文案→92%准确率”。时长:15秒
- 镜头4:博主口播,屏幕显示Gartner报告数据。字幕:“35%企业用AI代理→你该做教练”。时长:15秒
- 镜头5:博主面向镜头,语气坚定,手指点向观众。字幕:“自救:深耕非标领域,卖信任”。时长:10秒
## 适配平台
- **抖音**:开头加“别划走,3条干货救你饭碗”,语速更快,每句留1秒停顿。
- **视频号**:开头加“这条视频可能会被限流,但建议你看到最后”,语气更稳重,停顿稍长。
- **小红书视频**:开头加“姐妹们,别再踩坑了”,结尾加“关注我,只分享能用的干货”,画面更干净,字幕加emoji。
@@ -0,0 +1,17 @@
## 标题(爆款钩子)
你接的Logo单,邻居用旧电脑10分钟搞定|端侧AI来了
## 口播文案
上个月,一个朋友接了个Logo单子,报价3000块。[停顿]结果呢?他隔壁邻居,一个从没学过设计的大学生,用一台三年前的旧笔记本,装了最新的AI工具,十分钟出了三套方案。客户直接取消订单。[停顿]你可能会觉得夸张,但这就是2026年正在发生的事——端侧AI已经能本地跑商业级矢量图,15秒出图,成本是云端的五十分之一。[停顿]翻译、文案、设计这些自由职业者还在按字收费?客户已经发现:AI翻译带情感色彩和方言,能达到初级译员水平。[停顿]所以,如果你还在卖“时间”和“技能”,你的报价单下个季度就废了。[停顿]真正的出路不是跟AI比快,而是做AI做不了的事——比如复杂人际博弈、现场决策、情感陪伴。你得从“工具人”变成“教练”,帮客户调教本地AI,而不是被AI替代。[停顿]
## 分镜建议
- 镜头1(近景):博主对着镜头,表情略带无奈,手里拿着一张画着Logo的纸。字幕:“上周接了个Logo单子,报价3000。” 时长:8秒
- 镜头2(中景):切到博主旁边放一台旧笔记本电脑,屏幕快速闪过AI生成Logo的过程。字幕:“邻居大学生,十分钟出三套方案。” 时长:10秒
- 镜头3(近景):博主手指屏幕,语气加重。字幕:“15秒出图,成本是云端的1/50。” 时长:6秒
- 镜头4(全景):博主站起来,伸出双手做“停止”动作。字幕:“还在按字收费?客户已经不需要你了。” 时长:8秒
- 镜头5(特写):博主指向自己胸口,眼神坚定。字幕:“做AI做不了的,当教练。” 时长:8秒
## 适配平台
- **抖音**:前3秒用“你接的Logo单子”开头,快速钩住注意力;节奏紧凑,加上热门BGM。
- **视频号**:偏重情绪共鸣,可以放大“邻居大学生”的故事冲突,结尾多停留两秒引导点赞。
- **小红书视频**:封面用“3000块的单子被AI抢了”醒目大字,文案更干货,附上“你中招了吗”互动话题。
@@ -0,0 +1,17 @@
## 标题(爆款钩子)
做了8年SEO,流量却暴跌68%?你的文章正在给AI打工!
## 口播文案
老张上周在群里崩溃了,写了8年SEO,花三天打磨一篇爆款,结果流量掉了68%。[停顿] 他不信邪,搜自己标题——排在第一条的,不是他的文章,而是AI的摘要。[停顿] 你的完美SEO文章,正在变成AI搜索的免费数据源。这不是危言耸听。[停顿] 去年Google推出AI概览,直接在你内容里抽答案,用户根本不用点进来。你堆的关键词、铺的内链,全成了AI的养料。[停顿] 怎么办?别再写那种标准答案式的文章了。要写AI不会写的东西:你的个人故事、独家数据、争议观点。[停顿] 当你的内容不可替代时,AI只能把你当参考来源,而不是直接吃掉。[停顿] 记住,未来的SEO不是讨好算法,是成为AI不敢忽视的权威。评论“转型”,我教你具体做法。
## 分镜建议
- **镜头1**(0-8秒):博主面对镜头,表情严肃,背景是电脑显示SEO数据图表。字幕:“你的文章正在给AI打工”。时长8秒。
- **镜头2**(8-18秒):切换手机屏幕画面,展示一个AI搜索结果,红色圆圈圈出摘要框。字幕:“用户根本不用点进去”。时长10秒。
- **镜头3**(18-30秒):博主手比划,旁边放一块白板,画“旧SEO”和“新SEO”两个对比概念。字幕:“别再堆关键词”。时长12秒。
- **镜头4**(30-45秒):博主微笑自信,手指向自己胸前。字幕:“成为AI不敢忽视的权威”。时长15秒。
- **镜头5**(45-60秒):博主指向屏幕外,做出引导互动的动作。字幕:“评论‘转型’”。时长15秒。
## 适配平台
- **抖音**:前三秒必须情绪拉满,建议把“流量暴跌68%”做成动态大字炸屏,配合紧张BGM。
- **视频号**:增加1-2秒行业数据引用画面(如Google AI概览覆盖率),强化专业感和可信度。
- **小红书视频**:开头用“刷到这条视频你赚到了”代替直接冲突,结尾引导收藏并@好友,提高互动率。
@@ -0,0 +1,43 @@
## 标题(爆款钩子)
SEO已死?3招让AI主动抓你的文章,流量翻10倍!
## 口播文案
做SEO的老铁注意了![停顿]你花三天写的“完美文章”,标题塞满关键词,内链铺了12个,结果流量掉68%?原因很简单——AI搜索时代,你的文章被当免费数据喂给机器了![停顿]今天直接给三条能立刻上手的干货,让你成为AI的“必引内容”。
第一,别堆关键词了,改成结构化排版。用清晰的小标题、短段落、加粗重点、加列表。AI喜欢直接抓取“答案盒子”,你写成散文它理都不理。[停顿]
第二,疯狂建外链!让高权重网站引用你,AI搜索的排序算法超级看重反向链接。找个行业大号互推,或者去知乎、quora回答问题时顺手贴链接。[停顿]
第三,给你的文章加Schema标记。百度、谷歌都有免费工具,一键生成结构化数据标签,告诉AI这篇是“教程”还是“评测”。[停顿]
记住:以后你不是写给人看,是写给AI看的。AI先读,再推荐给你用户。三条立马去执行,下个月搜索广场就是你首页![停顿]
## 分镜建议
- 画面:博主对着镜头,表情焦虑,手里拿手机显示流量曲线暴跌图
字幕:SEO老张的惨痛教训
时长:5秒
- 画面:快速切换三个关键词卡片:结构化排版、外链引用、Schema标记,每个卡片停留1秒
字幕:3招立刻上手
时长:5秒
- 画面:博主手指屏幕,展示一个清晰的结构化文章示例(有小标题、列表)
字幕:AI喜欢“盒子内容”
时长:8秒
- 画面:博主做出打电话动作,画外显示“联系大V互推”字样
字幕:外链=流量密码
时长:6秒
- 画面:博主演示在搜索引擎工具页面点选Schema类型
字幕:一行代码让AI秒懂你
时长:6秒
- 画面:博主自信微笑,手指向镜头,文字弹出“现在就去改你的文章!”
字幕:行动才有流量
时长:5秒
## 适配平台
- 抖音:开头加个“你中招了吗”的钩子,字幕用更大字号,前3秒必须出现文题。
- 视频号:节奏稍慢,每招后加1秒表情特写,适合中年创业者观看习惯。
- 小红书视频:封面做对比图“旧SEO vs 新AI搜流”,正文精简到3个要点图,口播语速放慢。
@@ -0,0 +1,23 @@
## 标题(爆款钩子)
AI换脸开会?省了10分钟,丢了18%客户!
## 口播文案
你有没有碰到过这种场景?[停顿]打开视频会议,对面坐着个皮肤零瑕疵、眼神却发僵的“AI替身”。是不是瞬间觉得怪怪的?[停顿]别怪你太敏感,加州大学研究发现,见到这种“完美脸”,你的大脑会本能拉响警报——瞳孔放大,皮肤电反应飙升,这是进化留给你的保命本能。[停顿]
但很多人还在吹AI换脸多高效:会议时间缩短12%![停顿]可你知道另一面吗?同一家公司,员工抱怨“会议变陌生”“判断不了对方有没有在听”的比例,暴涨了41%。[停顿]更致命的是,一家欧洲金融科技公司用了AI形象后,客户流失率18个月内飙升18%——客户觉得你在“省成本”,而不是“在乎关系”。[停顿]
所谓效率,不过是在透支信任。[停顿]信任崩塌,比效率低更致命。你公司敢用AI换脸开会吗?评论区聊聊。[停顿]
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 主播正面特写,表情略带疑惑 | “AI换脸开会?省了10分钟,丢了18%客户!” | 5秒 |
| 2 | 画面插入数据对比图:左边“会议缩短12%”,右边“信任下降41%” | 会议缩短12% vs 信任下降41% | 8秒 |
| 3 | 主播用手比划大脑,背景出现杏仁核示意图 | 大脑拉响警报:瞳孔放大,皮肤电升高 | 10秒 |
| 4 | 切换场景:金融公司logo+客户流失曲线图 | 客户流失率18个月上升18% | 8秒 |
| 5 | 主播回到正面,直视镜头,摊手提问 | 你公司敢用AI换脸吗?评论区聊聊 | 7秒 |
## 适配平台
- **抖音**:前3秒用“你肯定遇到过”情绪钩子,节奏加快,字幕多用大号关键词。
- **视频号**:适当增加数据引用(如“盖洛普2026年报告”),语气更沉稳,强化信任与效率的理性对比。
- **小红书视频**:开头用“姐妹们,这事太离谱了”拉近距离,结尾引导收藏和“@你的老板来看”。
@@ -0,0 +1,42 @@
## 标题(爆款钩子)
AI换脸被职场封杀?这3个真相让你立刻关掉它
## 口播文案
你知道吗?用AI换脸开视频会议,73%的客户会直接降低对你的信任。[停顿]卡内基梅隆大学的研究实锤了——就算看起来再逼真,大脑杏仁核也会本能拉响警报,觉得“这人不对劲”。[停顿]
而且啊,你以为节省了时间?哈佛实验发现,和AI形象互动时,共情脑区活跃度整整下降30%。[停顿]员工匿名抱怨增加了41%——说“感觉会议变得陌生”。效率背后,是看不见的信任透支。[停顿]
更扎心的是,“完美形象”反而让人不舒服。斯坦福的研究说,AI永远模仿不了微表情的那些细微抖动,你的社交脑会陷入矛盾:看起来是人,直觉告诉你不是,持续消耗你的注意力。[停顿]
所以干货来了,三条立刻能用的:
第一条,对外客户会议、重要谈判、一对一辅导——绝对禁用AI脸。[停顿]
第二条,内部非正式头脑风暴可以用,但必须提前告知对方。[停顿]
第三条,每次想用AI形象前,先问自己三个问题:客户介意吗?团队能真正连接吗?如果因此失去一个客户,你接受吗?[停顿]
信任积累要几年,崩塌只需要一张AI脸。点赞收藏,别让效率毁了你的职场关系。
## 分镜建议
- 画面:博主面对镜头,表情严肃,手指指向屏幕上的“73%”数据字幕
- 字幕:73%的客户信任直线下降
- 时长:8秒
- 画面:快速切换大脑示意图/脑区动画,然后切回博主说话
- 字幕:共情脑区活跃度下降30%|员工抱怨+41%
- 时长:8秒
- 画面:展示一张完美AI人脸图片,然后手指划掉,出现“抖动的微表情”文字
- 字幕:完美 ≠ 真实|社交脑陷入矛盾
- 时长:8秒
- 画面:博主用手势比划“三”,然后逐条列出文字,配合手写动画
- 字幕:三条实操:①对外禁用②内部告知③三问自检
- 时长:12秒
- 画面:博主微笑总结,手势做“点赞”动作,结尾留钩子
- 字幕:信任积累几年,崩塌只需一张AI脸
- 时长:7秒
## 适配平台
- **抖音**:开头放5秒恐怖谷动画吸引眼球,全程快剪+高能字幕,结尾引导点赞收藏。
- **视频号**:节奏稍慢0.5倍,中间加入情绪停顿(比如叹息),适合熟人传播信任感。
- **小红书视频**:标题改成“打工人避坑!AI换脸会议避雷指南”,重点突出“亲测”“血泪教训”人设,评论区引导提问。
@@ -0,0 +1,22 @@
## 标题(爆款钩子)
就因为客户用了AI换脸,我200万的单子一夜黄了
## 口播文案
你知道吗?我有个做销售的朋友,去年年初谈一个200万的大单,[停顿]视频会议里客户全程用了一张AI换脸,皮肤完美、笑容标准,但眼神就是不太对劲。[停顿]他说自己全程浑身不舒服,最后报价都报得没底气。结果呢?客户反馈说“感觉你们公司不真诚”,直接选了别家。[停顿]
这不是个例啊。加州大学研究早就证明了,人看到“完美但空洞”的AI脸时,大脑会本能拉响警报——瞳孔放大、皮肤电反应升高,这是几百万年进化保留下来的警惕机制。[停顿]卡内基梅隆大学更狠:73%的商务谈判参与者,只要发现对方用了AI形象,信任直接打骨折。[停顿]
所以现在很多大公司开始明文封杀AI换脸,不是抵制效率,是害怕信任崩塌。[停顿]想想看,一场会议省了12%的时间,却要多花几十次真诚沟通才能挽回信用,这笔账你算得过来吗?[停顿]
你觉得AI形象该在职场用吗?评论区聊聊你的真实感受。[停顿]点赞收藏,别让更多人踩这个坑。
## 分镜建议
| 镜头 | 画面(景别/动作/场景) | 字幕(关键词) | 时长 |
|------|------------------------|----------------|------|
| 1 | 近景,主播在办公室对着镜头,表情略带懊恼 | “我朋友丢了200万的大单” | 8秒 |
| 2 | 插入视频会议画面,客户AI形象在微笑,眼神空洞,画面加恐怖谷特效 | “客户用的是AI换脸” | 6秒 |
| 3 | 切回主播近景,手指敲桌子加强语气 | “73%的人信任度下降” | 6秒 |
| 4 | 画面出现两个天平:左边是“省时间12%”,右边是“5-8次真诚沟通”,天平向右倾斜 | “效率 vs 信任,你选哪个?” | 6秒 |
| 5 | 主播直视镜头,做出询问表情,最后挥手 | “评论区等你故事” | 4秒 |
## 适配平台
- **抖音**:开头前3秒用“200万”+“黄了”制造强冲击,字幕加粗放大,节奏紧凑,可加#AI换脸职场封杀 话题标签。
- **视频号**:保留更多数据细节(73%、12%),结尾可引导关注账号获取“信任修复清单”,适合知识传播。
- **小红书视频**:封面用“❌AI换脸毁了我的大单”黄色大字,文案增加情绪词“气死了”“凭什么”,评论区引导互动共鸣。
@@ -0,0 +1,33 @@
## 标题(爆款钩子)
AI 预警快 15 分钟?别高兴太早,误报可能更要命!
## 口播文案
大家都觉得 AI 预警龙卷风是救星,能抢回救命 15 分钟?[停顿]
大错特错!
如果告诉你,这多出来的 15 分钟,可能是把你骗进死胡同的“催命符”,你信吗?[停顿]
传统雷达确实慢,要等风成型才报警。但 AI 太“敏感”了!
去年美国中部那场大乌龙还记得吗?[停顿]
AI 把一团普通云彩看成了超级龙卷风,几百万人半夜狂奔逃生,结果呢?连个风影都没见着。
这就是最可怕的“狼来了”效应。[停顿]
一旦误报多了,下次真龙卷风来了,你还会跑吗?
你会想:“又是 AI 抽风吧?”然后继续睡觉。
技术是快了,但信任没了,这才是最大的安全隐患。[停顿]
所以,别盲目吹捧 AI 万能。
在生死面前,快,不一定就是好。
你觉得,是宁可错杀一千,还是宁可慢那十几分钟?[停顿]
评论区告诉我你的选择。
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
| :--- | :--- | :--- | :--- |
| 1 | 主播怼脸特写,表情严肃甚至带点质疑,手做“停止”手势 | AI 预警=救星?<br>大错特错! | 5s |
| 2 | 快速剪辑:新闻画面中人群惊慌奔跑 vs 平静无风的街道(对比强烈) | 百万人大逃亡<br>结果:0 伤害 | 15s |
| 3 | 主播侧身,背景出现“狼来了”的卡通插图或警报器失灵特效 | 狼来了效应<br>信任崩塌 | 15s |
| 4 | 主播回到正脸,眼神诚恳,语速放缓,强调观点 | 快≠好<br>信任>速度 | 15s |
| 5 | 主播抛出问题,身体前倾,引导互动,屏幕出现投票条样式 | 宁可错报?<br>还是宁可慢? | 10s |
## 适配平台
- **抖音**:开头前 3 秒语速要更快,背景音乐用紧张的倒计时音效,强化冲突感。
- **视频号**:在“狼来了”段落可插入真实的新闻误报片段截图,增加中老年观众的信服力。
- **小红书**:封面图要用大字报风格写上“误报比迟到更可怕”,文案中增加 emoji 软化语气,引导讨论“安全感”。
@@ -0,0 +1,21 @@
## 标题(爆款钩子)
龙卷风预警提前15分钟?3个救命技巧别等误报后悔
## 口播文案
你知道吗?传统龙卷风预警平均只留给你13分钟![停顿]AI现在能提前15分钟,但误报率飙高。别慌,这3条干货,现在就能用上。
第一,别只盯手机短信。[停顿]气象局APP、本地应急广播全开,多个渠道多一重确认。第二,学会看天。漏斗云、旋转灰黑云团,就是死神敲门。别等警报响才跑,看到反常就撤。第三,提前找避难所。地下室、内部小房间,别躲车库或活动房。AI误报不怕,怕你麻痹大意。[停顿]记住,预警是提醒,自己预判才是保命符。评论区说说,你收到过假警报吗?
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 主播面对镜头,手指比划数字“13”和“15” | 13分钟 vs 15分钟,谁更快? | 5秒 |
| 2 | 切换手机预警通知截图,快速划过多个APP图标 | 多渠道预警,别只等短信 | 8秒 |
| 3 | 插入龙卷风来临前天空实拍(漏斗云旋转) | 漏斗云=死神敲门,看到就跑 | 10秒 |
| 4 | 主播手指向下指,背景出现地下室示意图 | 地下室最安全!别躲车库 | 8秒 |
| 5 | 主播回归镜头,微笑提问 | AI误报不可怕,麻痹才要命 | 7秒 |
## 适配平台
- **抖音**:前3秒用强冲突字幕“你的预警短信可能比死神晚到”,节奏更快,加热门音效。
- **视频号**:开头加“点赞收藏保命”引导互动,结尾带话题#极端天气自救指南
- **小红书视频**:封面用醒目大字“3条龙卷风保命干货”,口播语速稍慢,强调“提前准备”。
@@ -0,0 +1,32 @@
## 标题(爆款钩子)
龙卷风撕碎屋顶那刻,救命短信竟迟到了 13 分钟?
## 口播文案
2026 年 7 月,湖北那个下午,老张正修着屋顶。[停顿]
突然,天像被撕开个大口子,灰黑色的漏斗云直插下来。
就在瓦片满天飞、老张下意识抱头的那一秒,手机终于响了。
是一条红色预警短信。[停顿]
可这时候,跑还来得及吗?
这不是电影,是真实发生的“生死时间差”。
传统雷达太“慢热”了,非要等龙卷风完全成型才报警,平均晚整整 13 分钟!
对你我来说,这 13 分钟,就是生与死的距离。
有人寄希望于 AI 能快那 15 分钟,可去年美国那场大规模误报,又让几百万人白跑一趟,吓出心脏病。[停顿]
太快怕误报,太慢怕没命。
在极端天气面前,我们到底该怎么跑赢死神?
这道题,气象学家还在争,但留给我们的思考时间,真的不多了。[停顿]
你觉得,这命该交给算法,还是交给经验?评论区聊聊。
## 分镜建议
| 镜头 | 画面描述 | 字幕关键词 | 时长 |
| :--- | :--- | :--- | :--- |
| 1 | **特写 + 音效**:手机屏幕特写,显示时间跳动,背景音是呼啸的风声和瓦片碎裂声,突然弹出红色短信通知。 | 迟到的救命短信 | 5s |
| 2 | **中景/情景再现**:一个中年男子(老张)在屋顶惊慌失措,背景是合成特效的灰黑色漏斗云逼近,色调压抑。 | 生死 13 分钟 | 8s |
| 3 | **近景/口播**:博主面对镜头,神情严肃,手势比划出“时间差”的概念,背景可配一张传统雷达波形图。 | 传统雷达太“慢热” | 10s |
| 4 | **快剪/资料画面**:快速闪过美国新闻画面,人群惊慌奔跑,随后打上大大的红色问号“误报?”。 | AI 也会“狼来了”? | 8s |
| 5 | **特写/口播**:博主眼神直视镜头,抛出问题,画面逐渐变暗,只留眼神光。 | 你选速度还是准确? | 5s |
## 适配平台
- **抖音**:前 3 秒必须把“瓦片乱飞 + 短信迟到”的冲突画面剪在一起,配合急促的音效,完播率才高。
- **视频号**:文案结尾可加重“家人安全”的情感引导,评论区多引导中年用户分享身边的极端天气经历。
- **小红书**:封面图要用“漏斗云实拍 + 大字报标题”,文案中增加“避雷指南”或“自救干货”的标签属性。
@@ -0,0 +1,25 @@
## 标题(爆款钩子)
一道初中数学题,让GPT-4o集体死机:AI的数学智商到底有多差?
## 口播文案
你敢信?[停顿]那个能写论文、写代码、碾压围棋的AI,竟然被韦东奕的一道初中数学题给整破防了![停顿]
GPT-4o、Claude 3.5,还有所谓的“专用数学模型”……全给出错误答案,甚至直接放弃![停顿]可这道题,就是一个聪明的高中生花半小时也能做出来。[停顿]
为什么?因为AI根本没有“数学直觉”。[停顿]它只会疯狂穷举、暴力检索,遇到需要构造辅助线、灵光一现的题,立马变成傻子。[停顿]
说白了,人类最后一道防线不是算力,而是那一瞬间的“灵感”。[停顿]你还在替AI焦虑?先问问自己:你的直觉,还活着吗?
## 分镜建议
| 镜头 | 画面 | 字幕(关键词) | 时长 |
|------|------|----------------|------|
| 1 | 博主直视镜头,表情略带挑衅 | “一道初中数学题,AI集体死机” | 3秒 |
| 2 | 快速闪过韦东奕照片,叠加大模型logo(GPT-4o、Claude等) | “韦神出题 vs 四大AI” | 5秒 |
| 3 | 展示一张几何题图(辅助线题型)+红色叉号 | “全错!甚至直接放弃” | 4秒 |
| 4 | 博主手势比划“思路”,背景出现灯泡图标 | “人类数学直觉:灵光一现” | 5秒 |
| 5 | 博主摊手,嘴角微笑 | “你的直觉,还活着吗?” | 3秒 |
## 适配平台
- **抖音**:前3秒用“你敢信?一道初中题”直接引爆好奇心,全程语速加快,每句后留0.5秒反应时间。
- **视频号**:开头加一句“这可能是你今年听到最反认知的事”,结尾改成“转发给还在神化AI的朋友”增强社交互动。
- **小红书视频**:首页配图文标题“AI被初中题碾压?真相扎心了”,全程语速放缓,突出“数学直觉”关键词,适合知识收藏。
@@ -0,0 +1,27 @@
## 标题(爆款钩子)
韦神一道题让AI集体死机?3招保住你的数学直觉
## 口播文案
你听说了吗?[停顿]韦东奕出了一道初中数学题,结果GPT-4o、Claude 3.5全体翻车,直接“死机”[停顿]凭什么?因为AI没有数学直觉,它只会套公式,不会构造辅助线。今天给你3条干货,让你在AI时代也能守住人类最后的防线。
第一条:别让AI替你思考。[停顿]遇到难题,先自己画图、写步骤,逼自己找突破口。AI给答案太快,你根本没机会训练直觉。
第二条:刻意练习“开放式问题”。[停顿]平面几何、数论证明这类题,没有标准解法,需要你构造辅助线、猜规律。每天一道,大脑就会长出“直觉肌肉”。
第三条:把AI当陪练,别当老师。[停顿]做完题,再用AI验证思路,看它怎么错。你发现它卡在哪,你就在哪赢了。
这三招,今天就能用,别等脑子生锈。评论区告诉我,你遇到过AI算不出来的题吗?
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 近景:博主对着镜头,表情惊讶,手指比划 | 韦神一道题,AI集体死机? | 5秒 |
| 2 | 中景:博主拿出一张白纸,画一条辅助线,然后摇头 | 没有数学直觉,只会套公式 | 8秒 |
| 3 | 特写:博主用手势模仿思考,然后指向自己 | 别让AI替你思考,先自己画图 | 10秒 |
| 4 | 中景:博主展示一张几何题,用笔圈出关键点 | 每天一道开放式问题,训练直觉肌肉 | 10秒 |
| 5 | 近景:博主拿起手机,假装和AI对话,然后微笑 | 把AI当陪练,看它怎么错 | 8秒 |
## 适配平台
- **抖音**:前3秒用“韦神+AI翻车”标题+悬念剪辑,节奏更快,字幕加粗放大。
- **视频号**:增加“你同意吗?”结尾互动,结合朋友圈转发话术,强调“人类优势”。
- **小红书视频**:标题改为“AI翻车后我悟了:3个数学直觉训练法”,封面用几何题+红色感叹号。
@@ -0,0 +1,19 @@
## 标题(爆款钩子)
韦神一道初中题,AI全部“死机”——人类直觉到底有多强?
## 口播文案
前几天,韦东奕出了一道初中数学题。[停顿]结果你猜怎么着?那些号称能写论文、编代码、替人类打工的顶级AI,像GPT-4o、Claude 3.5,居然集体翻车了。[停顿]是真的“死机”,连思路都理不清。[停顿]我当时看到这个新闻,心里咯噔一下——原来AI再厉害,也学不会那种灵光一现的直觉。[停顿]数学靠的是巧劲儿,是十几年甚至几十年磨出来的感觉。而AI呢?它只会算,不会“想”。[停顿]所以别老焦虑自己被替代了。有些东西,机器就是学不会。比如你脑子里的那根“辅助线”,比如你看到问题突然冒出来的那个解法。[停顿]这,才是我们人类的最后防线。
## 分镜建议
| 镜头 | 画面 | 字幕 | 时长 |
|------|------|------|------|
| 1 | 镜头对准手机屏幕,展示韦东奕照片或新闻截图,快速拉近 | “韦神出题” | 5秒 |
| 2 | 切到电脑屏幕,显示AI回答错误或加载失败动画,配合音效 | “GPT-4o、Claude 3.5……全懵了” | 8秒 |
| 3 | 真人出镜,皱眉、摊手,语气惊讶 | “是不是觉得不可思议?” | 6秒 |
| 4 | 镜头切回真人,一边说一边比划“辅助线”的手势 | “数学直觉,才是人类最后的防线” | 10秒 |
| 5 | 真人微笑,屏幕下方弹出话题标签,结尾定格 | “有些东西,机器就是学不会” | 6秒 |
## 适配平台
- **抖音**:前3秒用快节奏切屏和音效冲突感,字幕加emoji,结尾引导点赞评论“你觉得自己直觉准吗?”
- **视频号**:适当放慢语速,保留停顿感,增加“你身边有没有这样的例子?”互动话术,适合中老年用户传播。
- **小红书视频**:封面用韦东奕照片+“AI翻车”大字,正文加#人类直觉 #韦东奕 标签,文案侧重“普通人如何训练自己的直觉”。
@@ -1,54 +0,0 @@
[
{
"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": "用数据量化减法效果,用真实案例展示转变过程",
"priority": "高",
"total_score": 85.5,
"compliance_score": 100,
"source_file": "automation/data/sustainability_topics.json",
"ready_at": "2026-05-08",
"published_at": null,
"platform_urls": {},
"status": "待处理",
"lock_by": null,
"lock_at": null
},
{
"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": "高",
"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": "待处理",
"lock_by": null,
"lock_at": null
}
]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

+73 -87
View File
@@ -1,4 +1,4 @@
# 信息源配置(可持续性相关
# 信息源配置(AI与科技人文领域
# 注意:仅保留国内可稳定访问的源,移除被墙/下线的源
sustainability_sources:
@@ -9,16 +9,14 @@ sustainability_sources:
url: "https://36kr.com/feed"
update_frequency: "daily"
credibility: "high"
focus: "科技商业、绿色经济"
focus: "科技商业、AI行业"
keywords:
- "AI"
- "新能源"
- "可持续"
- "环保"
- "绿色"
- "低碳"
- "ESG"
- "循环"
- "人工智能"
- "大模型"
- "科技"
- "数字化"
- "创业"
- name: "少数派"
type: "rss"
url: "https://sspai.com/feed"
@@ -31,99 +29,87 @@ sustainability_sources:
- "工具"
- "数字"
- "智能"
- "工作流"
- name: "机器之心"
type: "rss"
url: "https://jiqizhixin.com/rss"
update_frequency: "daily"
credibility: "high"
focus: "AI学术前沿、论文解读"
keywords:
- "AI"
- "论文"
- "大模型"
- "算法"
- "研究"
- "学术"
# 搜索引擎采集(通过Bing中文搜索抓取热点)
# 基于《2025中国可持续消费报告》《绿色低碳消费大数据报告》等权威调研得出的七大热点方向
web_search:
- name: "循环消费搜索"
- name: "AI前沿资讯搜索"
type: "web_search"
query: "以旧换新 二手交易 闲置 循环 2026"
query: "AI 人工智能 大模型 前沿 突破 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: "环保科技/可持续产品/绿色材料"
focus: "AI行业动态、技术突破"
- name: "AI工具搜索"
type: "web_search"
query: "AI工具 人工智能 效率提升 2026"
query: "AI工具 效率提升 AI工作流 实用 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI工具/效率方法"
focus: "AI工具评测、效率方法"
- name: "AI职场搜索"
type: "web_search"
query: "AI 裁员 职业转型 AI技能 远程工作 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI对就业影响、职业转型"
- name: "AI生活化搜索"
type: "web_search"
query: "AI陪伴 AI生活助手 AI心理咨询 智能体 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI人格化、生活场景应用"
- name: "科技人文搜索"
type: "web_search"
query: "AI伦理 数字生活 科技反思 人机关系 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI伦理、科技人文反思"
# ── 差异化源(与通用AI媒体形成区分)──
- name: "AI学术论文搜索"
type: "web_search"
query: "arXiv 论文 AI 最新研究 大模型 2026"
update_frequency: "weekly"
credibility: "high"
focus: "AI学术前沿、论文中文解读"
- name: "独立开发者搜索"
type: "web_search"
query: "独立开发者 AI产品 一人公司 出海工具 indie hacking 2026"
update_frequency: "daily"
credibility: "medium"
focus: "独立开发者生态、小众AI产品"
- name: "行业数据报告搜索"
type: "web_search"
query: "AI行业报告 数据统计 市场分析 AI应用渗透率 2026"
update_frequency: "daily"
credibility: "high"
focus: "行业数据、市场报告、AI应用统计"
- name: "AI产品动态搜索"
type: "web_search"
query: "AI产品发布 更新 changelog 新功能 OpenAI Claude 2026"
update_frequency: "daily"
credibility: "medium"
focus: "AI产品迭代、功能更新"
# 本地案例库(保底数据源)
local:
- name: "宇之然本地案例库"
- name: "宇之然AI案例库"
type: "local"
file: "strategy/全球案例数据库-v1.md"
file: "automation/data/ai_cases.json"
update_frequency: "weekly"
credibility: "medium"
focus: "全球-本土案例"
- name: "历史选题库"
type: "local"
file: "automation/data/sustainability_topics.json"
update_frequency: "daily"
credibility: "medium"
focus: "复用已有选题"
# 案例验证要求
validation:
require_at_least_2_sources: false
prefer_official_data: false
min_data_points: 1
# 选题优先级权重
topic_priority:
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重新定义
focus: "AI行业案例"
# 内容合规检查清单
compliance:
@@ -136,5 +122,5 @@ compliance:
required:
- "至少3个可靠引用"
- "数据标注来源"
- "包含MVP行动"
- "包含可操作建议"
- "中性客观语气"
@@ -1,123 +0,0 @@
# 在上海阳台种菜一年,我收获了啥?
**领域**:自然 / 生活
**形式**:实操指南 + 个人故事
**预估字数**2500
**核心观点**:城市农业不仅是种菜,更是与自然重建连接的生活方式
**受众痛点**
- 想体验田园生活但没条件去农村
- 担心食品安全,想自己种菜但不懂技术
- 阳台空间小,不知道能种什么
- 担心养不活植物,缺乏信心
- 城市生活压力大,寻求放松方式
**独特角度**
- 不鼓吹"田园牧歌",客观展示失败和踩坑
- 用数据说话(产量、成本、时间投入)
- 结合科技元素(智能种植、垂直农业)
- 从心理层面谈与植物建立连接的意义
- 延伸讨论城市食物系统和可持续生活
**数据/案例**(至少3个来源):
1. 上海阳台种植调研报告(找本地社区数据)
2. 不同蔬菜的生长周期和产量数据(农业网站)
3. 城市农业的心理学研究(减压、幸福感)
4. 自己一年的种植记录(日志、照片)
5. 智能种植设备对比(自动浇水、补光灯等)
**预估完成时间**4天
**优先级**:高
**预计发布时间**2026-04-15
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 8 | 城市青年有共鸣,但相对垂直 |
| 独特性 | 9 | 结合个人故事+科学种植+城市农业视角 |
| 数据可得性 | 8 | 自己有一年数据,可补充调研 |
| 可持续性 | 7 | 可写成系列(不同季节、不同植物) |
| 平台契合度 | 9 | 知乎生活/园艺话题,小红书也适合 |
| 品牌契合度 | 10 | 科技+自然+生活的完美结合 |
| **总分** | **51** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(300字)
- 场景:在上海20㎡阳台种出一个小菜园
- 问题:很多人觉得不可能/太麻烦
- 引子:一年的收获不仅是蔬菜,还有更多
### 主体(2000字)
**第一部分:为什么开始?(300字)**
- 疫情后的食物焦虑
- 对超市蔬菜的不信任
- 想找回"土地连接感"
- 初始期待(vs 现实)
**第二部分:阳台农业的真相(500字)**
- 空间利用:垂直种植+合理规划
- 品种选择:适合上海的蔬菜(番茄、辣椒、生菜、香草)
- 成本核算:投入 vs 产出(经济账和心理账)
- 时间投入:每天半小时,周末2小时
- 失败案例分享(死掉的植物+经验)
**第三部分:科技让种菜变简单(400字)**
- 智能灌溉系统(DIY vs 购买)
- 补光灯的应用(光照不足的冬天)
- 种植APP辅助(记录、提醒、社区)
- 垂直农业设备介绍(Aerogarden等)
- AI助手帮我分析植物健康(拍照识别病害)
**第四部分:收获的不仅是蔬菜(400字)**
- 产量记录(一年收获多少斤)
- 心理收获:减压、专注、成就感
- 家庭关系改善(和家人一起照料)
- 环保意义(减少碳足迹、包装垃圾)
- 生活方式的改变(更关注季节性、本地食物)
**第五部分:给新手的建议(400字)**
- 从简单的开始(香草、生菜)
- 不要一次买太多植物
- 必备工具清单(花盆、土、工具)
- 常见问题和解决方案
- 哪里获取信息和社区支持
### 结尾(200字)
- 城市农业不是回归原始,而是科技赋能的现代生活选择
- 哪怕只有一盆香草,也是与自然的连接
- 呼吁:从小事做起,重建人与自然的联系
- 预告:后续会分享具体种植技术细节
---
## 资源清单
### 数据来源(待收集)
- [ ] 上海市社区农业调查报告
- [ ] 阳台种植产量统计数据
- [ ] 城市农业心理健康研究论文
- [ ] 各蔬菜品种生长数据
### 图片素材(待准备)
- [ ] 一年种植时间线照片
- [ ] 不同生长阶段特写
- [ ] 收获成果展示
- [ ] 种植设施和工具照片
### 引用来源(待标注)
- [ ] 农业技术网站
- [ ] 学术数据库
- [ ] 种植社区讨论
- [ ] 科技产品官网
---
**决策**:这是一个高潜力选题,品牌契合度极高,预计能获得良好反响。建议优先执行。
@@ -1,143 +0,0 @@
# AI时代,什么能力不会被替代?
**领域**:科技 / 职场
**形式**:趋势洞察 + 实操建议
**预估字数**2800
**核心观点**:AI淘汰的不是工作,而是不会使用AI的人;但有些"人类特质"能力反而更珍贵
**受众痛点**
- 担心被AI替代,职业焦虑严重
- 不知道如何提升自己的竞争力
- 听说AI很强,但不知道学什么
- 想转行但方向迷茫
- 担心投入学习最后还是被淘汰
**独特角度**
- 区分配"被AI强化的能力"和"AI无法替代的能力"
- 从历史技术进步规律看职业演变(不是第一次自动化革命)
- 强调"审美、共情、意义构建"等软技能
- 给出具体的能力提升路径,不是泛泛而谈
- 结合宇之然"科技沟通万物,回归自然"的哲学
**数据/案例**(至少3个来源):
1. 世界经济论坛《未来就业报告》技能趋势数据
2. 美国劳工统计局职业自动化概率研究
3. AI能力边界研究(GPT-4o vs 人类能力对比)
4. 历史案例:工业革命、计算机革命后的职业变化
5. 访谈案例:哪些岗位在AI时代反而增值?
**预估完成时间**5天
**优先级**:高
**预计发布时间**2026-04-18
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 9 | 所有职场人士都关心,流量潜力大 |
| 独特性 | 8 | 避开"AI工具教程"红海,聚焦深度思考 |
| 数据可得性 | 9 | 大量研究报告和历史数据 |
| 可持续性 | 8 | 可写成系列(不同行业、不同能力维度) |
| 平台契合度 | 9 | 知乎职场/科技话题热门 |
| 品牌契合度 | 10 | 科技向善+人文关怀,契合价值观 |
| **总分** | **53** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(400字)
- 现象:朋友被AI工具震撼,担心失业
- 核心问题:AI时代,我们该学什么?
- 观点:AI淘汰的不是工作,而是不会使用AI的人;但有些"人类特质"能力反而更珍贵
- 预告:本文将分析AI的能力边界,并给出具体的技能提升建议
### 主体(2200字)
**第一部分:AI到底有多强?(400字)**
- 当前AI能力范围(文本、图像、代码、数据分析)
- AI的局限性:没有意识、没有真实体验、无法承担责任
- AI的依赖:需要人类提供高质量prompt、数据、验证
- 数据:哪些职业自动化概率高(根据研究报告)
- 事实:AI不是万能,只是工具
**第二部分:历史给我们的启示(300字)**
- 工业革命:机器取代体力劳动,但创造了新职业
- 计算机革命:打字员消失,但程序员、设计师兴起
- 每次技术革命都淘汰旧技能,但同时创造更多新技能
- 规律:重复性工作易被替代,创造性、社交性工作更难
- 结论:不恐惧技术,而是学会驾驭技术
**第三部分:AI无法替代的3类能力(800字)**
**A. 审美与创造力**
- AI生成的美术、音乐、文字缺少"灵魂"
- 真正的好设计需要理解人的情感和文化
- 创意工作者(作家、艺术家、设计师)的价值被提升而非降低
- 案例:AI辅助创作,但最终决策权在人
- 如何培养审美能力?(艺术欣赏、多元输入、实践创作)
**B. 共情与沟通**
- AI无法真正理解人类情感
- 医护、教育、管理、咨询等职业的"人味"不可替代
- 高情商沟通、冲突调解、心理支持
- 案例:AI心理咨询的局限性
- 如何提升共情能力?(倾听、换位思考、情绪识别)
**C. 意义构建与战略思维**
- AI能提供数据,但不能替你做价值判断
- 决策需要理解"为什么",而不仅是"怎么做"
- 战略规划、伦理判断、文化塑造
- 案例:企业战略制定,AI只能辅助分析
- 如何培养战略思维?(跨领域学习、哲学思考、长期视角)
**第四部分:AI会强化的能力(400字)**
- 学习能力(快速掌握新工具)
- 信息筛选与批判性思维(辨别AI输出质量)
- 工具使用能力(prompt engineering、AI工作流设计)
- 协同能力(人机协作,AI作为副驾驶)
- 快速迭代能力(AI加速实验和反馈)
**第五部分:具体能力提升路径(300字)**
- 初级(0-1年):掌握基础AI工具,提升学习效率
- 中级(1-3年):发展审美、沟通、批判性思维
- 高级(3-5年):建立战略思维,成为AI无法替代的决策者
- 学习资源推荐(书籍、课程、实践方法)
- 宇之然品牌如何帮助读者提升这些能力?(预告后续内容)
### 结尾(200字)
- AI不是敌人,而是工具
- 人类的优势在于"人性":审美、共情、意义构建
- 建议:拥抱AI提升效率,同时刻意训练人类特质能力
- 呼吁:不要焦虑,而是主动进化
- 互动:你在工作中感受到AI的哪些影响?欢迎评论区分享
---
## 资源清单
### 数据来源(待收集)
- [ ] WEF《未来就业报告》最新版
- [ ] 美国劳工统计局职业自动化概率表
- [ ] OpenAI/GPT能力边界研究论文
- [ ] 历史技术革命对就业影响的经济学研究
- [ ] AI能力与人类能力对比可视化数据
### 图片素材(待准备)
- [ ] AI能力范围示意图
- [ ] 自动化概率职业分布图
- [ ] 技能矩阵图(会被替代 vs 更珍贵)
- [ ] 能力提升路径时间线
### 引用来源(待标注)
- [ ] 世界经济论坛官网
- [ ] 学术期刊数据库
- [ ] 行业研究报告
- [ ] 知名专家观点
---
**决策**:这是一个必爆的热门选题,但内容必须深度且有数据支撑,不能流于表面。需要较强的信息整合能力。建议medium-high优先级执行。
-135
View File
@@ -1,135 +0,0 @@
# 从程序员到数字游民:我的三年转型之路
**领域**:工作 / 职场
**形式**:个人故事 + 实操指南
**预估字数**2600
**核心观点**:数字游民不是逃离,而是更聪明的工作生活方式选择
**受众痛点**
- 受够了996,想自由但不敢行动
- 不知道数字游民能做什么,收入从哪来
- 担心不稳定、没社保、老了怎么办
- 想远程但公司不让,不知如何说服老板
- 羡慕别人环游世界,自己却卡在工位
**独特角度**
- 不美化"边玩边赚",客观展示挑战和代价
- 从技术转型案例切入(程序员是最适合数字游民的职业之一)
- 用数据说话:收入对比、时间分配、成本核算
- 结合"宇之然"理念:科技让人自由,而非束缚
- 提供可操作的转型路线图(不是鸡汤)
**数据/案例**(至少3个来源):
1. 数字游民调研报告(人数、收入、职业分布)
2. 不同国家的签证政策和生活成本对比(数字游民签证国家)
3. 远程工作平台收入数据(Upwork、Toptal等)
4. 个人三年转型历程(时间线、收入变化、心路历程)
5. 成功/失败数字游民案例访谈
**预估完成时间**5天
**优先级**:高
**预计发布时间**2026-04-20
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 9 | 大量职场人向往自由工作,流量潜力巨大 |
| 独特性 | 8 | 避开"如何找远程工作"表层,深入转型战略 |
| 数据可得性 | 8 | 调研报告多,个人案例丰富 |
| 可持续性 | 9 | 可系列化(不同职业、不同国家、不同阶段) |
| 平台契合度 | 9 | 知乎职场/创业热门话题 |
| 品牌契合度 | 10 | 科技提升生活自由度,契合"科技沟通万物" |
| **总分** | **53** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(400字)
- 场景:三年前坐在格子间,加班到深夜,问自己"这就是我想要的生活吗?"
- 转折:发现程序员完全可以远程,开始转型尝试
- 现状:现在在清迈咖啡馆工作,月收入 comparable,但生活品质完全不同
- 核心观点:数字游民不是逃避,而是更聪明的生活方式选择
### 主体(2200字)
**第一部分:数字游民的真实生活(400字)**
- 破除神话(不是天天旅游、不是躺着赚钱)
- 真实时间表:上午工作、下午探索、晚上复盘
- 收入稳定性如何保障?(多个客户、项目制)
- 孤独感和社交挑战(社群、Co-working)
- 数据:平均收入、工作小时、留存率
**第二部分:为什么程序员适合?(300字)**
- 技能可远程交付(代码在哪都能写)
- 高需求(全球IT人才短缺)
- 收入高且相对稳定
- 学习资源丰富(可自学新技能)
- 本人的转型路径(从全职到自由职业)
**第三部分:转型前的准备(500字)**
- 财务准备:6-12个月紧急基金
- 技能盘点:哪些技能可远程变现?
- 作品集建设:GitHub、个人项目、案例
- 平台选择:Upwork、Toptal、电鸭、远程工作社区
- 先从兼职开始,不要裸辞
- 法律和社保:如何自己交?国际医保?
**第四部分:我的三年路线图(400字)**
- 第1年:保存+考证+接小项目(月入5k)
- 第2年:建立客户群+提价+过渡(月入15k)
- 第3年:稳定客户+被动收入+探索(月入20k+)
- 关键节点和决策点
- 踩过的坑(低价竞争、客户跑路、签证问题)
**第五部分:在哪里生活?(300字)**
- 数字游民热门城市对比(清迈、巴厘岛、里斯本、东京)
- 成本核算(住宿、交通、签证、保险)
- 网络和基础设施(稳定WiFi、Coworking
- 社区和社交(容易找到同类人)
- 安全和生活便利性
**第六部分:给想转型者的建议(300字)**
- 先试水:请假去远程工作一个月
- 不要孤注一掷:保持至少一个稳定收入源
- 建立系统:不只是接单,而是建立个人品牌
- 心理准备:自由带来责任,自律才能自由
- "宇之然"理念:科技让人自由,但自由需要能力支撑
### 结尾(200字)
- 数字游民不是适合所有人,但值得尝试
- 关键是找到适合自己的工作生活方式
- 鼓励读者思考:我想要什么样的生活?科技能帮我实现吗?
- 预告:后续分享具体技能提升路线(程序员转远程)
---
## 资源清单
### 数据来源(待收集)
- [ ] 数字游民数量增长数据( Nomad List 、相关报告)
- [ ] 程序员远程工作薪资调研
- [ ] 各国数字游民签证政策汇总
- [ ] Coworking 空间成本统计
- [ ] IT自由职业平台收入报告
### 图片素材(待准备)
- [ ] 三年转型时间线图
- [ ] 不同城市成本对比图表
- [ ] 收入变化曲线
- [ ] 世界各地工作地点照片(清迈咖啡馆等)
- [ ] Coworking 空间环境图
### 引用来源(待标注)
- [ ] Nomad List 官网数据
- [ ] 数字游民社区统计
- [ ] 各国签证政策官网
- [ ] 远程工作平台文档
---
**决策**:这是一个高流量选题,竞争也激烈,需要深度个人故事+系统化建议才能脱颖而出。建议 medium-high 优先级。
@@ -1,143 +0,0 @@
# 零浪费生活一年:一个家庭产生的垃圾去哪儿了?
**领域**:自然 / 生活
**形式**:实验记录 + 科普指南
**预估字数**2300
**核心观点**:零浪费不是苦行,而是更聪明的消费选择;个人行动虽小,但能改变系统
**受众痛点**
- 想环保但不知道从何下手
- 觉得"我一个人做有什么用"
- 担心成本太高,负担不起
- 嫌麻烦,步骤太多坚持不下去
- 家人不理解,无法统一战线
- 超市买买买停不下来,事后又后悔
**独特角度**
- 不道德绑架,用数据和成本说服
- 展示失败和妥协(不是完美零浪费,而是持续改进)
- 从家庭视角(夫妻+孩子)而非个人
- 连接科技:用App记录、智能采购、循环经济平台
- 强调经济收益(省了多少钱)而非道德优越
**数据/案例**(至少3个来源):
1. 中国城市生活垃圾产生量数据(统计年鉴)
2. 不同包装方式的碳足迹对比(生产、运输、处理)
3. 零浪费家庭采访或书籍(如Bea Johnson, 0 Waste Home
4. 自己一年的垃圾记录(称重、分类、分析)
5. 环保产品性价比对比(可重复使用 vs 一次性)
**预估完成时间**4天
**优先级**:高
**预计发布时间**2026-04-22
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 8 | 环保意识提升,但执行力不足的人多 |
| 独特性 | 9 | 家庭实验视角,真实数据,不教条 |
| 数据可得性 | 8 | 垃圾数据、环保报告、产品对比可获取 |
| 可持续性 | 8 | 可系列(厨余、包装、衣物、电子产品等) |
| 平台契合度 | 8 | 知乎生活/环保话题,小红书也适合 |
| 品牌契合度 | 10 | 自然+科技+生活,完美契合价值观 |
| **总分** | **51** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(300字)
- 场景:2025年1月1日,我在家门口放了一个垃圾桶,记录一整年的垃圾
- 问题:一个家庭一年会产生多少垃圾?它们最后去哪了?
- 引子:零浪费不是不吃不喝,而是 smarter consumption
- 预告:分享一年实验数据、踩坑经验、经济收益
### 主体(1800字)
**第一部分:我们这一年扔了啥?(300字)**
- 垃圾总量:XX公斤(对比全国人均)
- 分类数据:塑料、包装、厨余、其他
- 月度趋势(节假日更多,夏季更多)
- 最大来源:外卖包装、快递箱、食品包装
- 可视化:饼图+月度折线图
**第二部分:我们是怎么做到的?(400字)**
- 原则5R:Refuse(拒绝不需要的)、Reduce(减少)、Reuse(重复使用)、Recycle(回收)、Rot(堆肥)
- 购物习惯改变:
- 带袋子买菜(拒绝塑料袋)
- 散装优先(减少包装)
- 耐用品替代一次性(硅胶保鲜膜、可重复使用的咖啡杯)
- 二手优先(家具、电子产品、衣物)
- 厨余处理:堆肥桶+社区堆肥点
- 家庭共识:夫妻+孩子的参与,不是一个人负重前行
- 妥协:不可能100%,目标是逐年降低
**第三部分:科技让零浪费变简单(300字)**
- 零浪费购物App(包, 乐活等)
- 智能库存管理(避免买多导致浪费)
- 社区交换平台(闲鱼、小区群)
- 可降解产品追踪(哪些真的环保?)
- 碳足迹计算器(量化自己的贡献)
**第四部分:经济账(200字)**
- 初始投入:玻璃罐、棉布袋、不锈钢饭盒(XXX元)
- 每月节省:减少外卖/包装(XXX元)、二手购物(XXX元)
- 一年净节省:XXXX元(ROI 200%+
- 隐性收益:更健康饮食(少点外卖),更理性消费
- 环保不是富人游戏,穷人更应该
**第五部分:挑战与妥协(300字)**
- 家人不配合:老人舍不得扔旧物,孩子要零食包装
- 社交压力:聚会自带杯子被嘲笑
- 便利性:有时候真的没时间准备
- 产品缺失:市场上零浪费选择太少
- 我们没有做到的地方:电子产品循环难、医疗包装无法避免
- 心态调整:进步不是完美,可持续才是关键
**第六部分:我们的建议(300字)**
- 从最容易的3件事开始:带袋子、少点外卖、二手优先
- 设定合理目标(比如年度垃圾减量20%)
- 记录数据(否则不知道进步)
- 找到社区(一个人走不远)
- 关注政策(垃圾分类、再生资源回收)
- 科技赋能(用工具降低门槛)
- "宇之然"理念:零浪费不是回到原始,而是科技赋能的智能生活
### 结尾(200字)
- 一年垃圾量:XXX公斤(对比第一年减少XX%)
- 零浪费不是苦行,是更高质量的生活
- 每一个小行动都在影响系统
- 呼吁:不要追求完美,追求持续改进
- 互动:你的零浪费实践是什么?欢迎分享
---
## 资源清单
### 数据来源(待收集)
- [ ] 中国城市生活垃圾产生量年度数据(国家统计局)
- [ ] 各类包装碳足迹数据(学术论文)
- [ ] 零浪费家庭案例(国内外博客、书籍)
- [ ] 零浪费产品成本与一次性对比
- [ ] 社区堆肥项目案例
### 图片素材(待准备)
- [ ] 一年垃圾记录照片(分类展示)
- [ ] 零浪费购物装备(布袋、罐子、饭盒)
- [ ] 堆肥桶和成果
- [ ] 数据图表(垃圾量趋势、分类占比、经济收益)
- [ ] 前后对比(购物方式、垃圾桶状态)
### 引用来源(待标注)
- [ ] 零浪费运动创始人Bea Johnson
- [ ] 《零浪费生活》等书籍
- [ ] 环保组织报告(如WWF、Greenpeace
- [ ] 国内零 waste community
---
**决策**:实验性质内容,容易引发共鸣和讨论,互动率高。适合知乎和小红书。建议high优先级。
-166
View File
@@ -1,166 +0,0 @@
# 深度工作实践:如何在干扰世界中保持专注
**领域**:工作 / 生产力
**形式**:实操指南 + 个人实验
**预估字数**2400
**核心观点**:专注力是新时代最稀缺的资源,通过系统方法可以重建深度工作能力
**受众痛点**
- 每天忙碌但没产出,时间碎片化
- 容易被手机、消息、同事打断
- 知道要专注,但就是控制不住刷手机
- 工作1小时,实际只有30分钟在干活
- 想要心流状态但总进不去
- 下班累但没成就感
**独特角度**
- 不鸡汤,用实验数据和自我追踪说话
- 结合科技工具(Forest、番茄钟、白噪音)和传统方法
- 承认现代工作环境的现实(完全隔离不现实),提供折中方案
- 从神经科学和心理学角度解释why,不只是how
- 强调"深度休息"与"深度工作"的交替
**数据/案例**(至少3个来源):
1. Cal Newport《深度工作》理论体系
2. 注意力恢复理论(Attention Restoration Theory
3. 番茄工作法实证研究(效果数据)
4. 个人3个月实验数据(专注时间、任务完成量、压力水平)
5. 知识工作者专注力调研( interruptions cost
**预估完成时间**4天
**优先级**:高
**预计发布时间**2026-04-23
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 9 | 所有知识工作者都需要,流量潜力极大 |
| 独特性 | 7 | 话题热门,需要用数据和实验脱颖而出 |
| 数据可得性 | 9 | 大量研究文献和工具可用 |
| 可持续性 | 8 | 可系列(不同环境、不同工具、不同性格) |
| 平台契合度 | 9 | 知乎职场/效率热门,小红书打卡也适合 |
| 品牌契合度 | 8 | 科技辅助专注,但最终回归人的本质 |
| **总分** | **50** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(300字)
- 场景:上周写一份报告,计划4小时,实际花了8小时,中间刷了20次手机
- 问题:为什么我们越来越忙,产出却越来越少?
- 观点:不是时间不够,是注意力被碎片化
- 预告:分享3个月深度工作实验,数据+方法+工具
### 主体(1900字)
**第一部分:专注力危机(300字)**
- 数据:平均专注时长从26分钟降到8分钟(Microsoft研究)
- 知识工作者每天被中断多少次?(59次/天,平均)
- 切换成本:每次中断后需要23分钟回到深度状态
- 现代工作环境:开放式办公室、即时通讯、通知轰炸
- 后果:疲惫、低效、创造力下降
**第二部分:什么是深度工作?(200字)**
- Cal Newport定义:无干扰的认知挑战性工作
- 与肤浅工作的对比(容易回复的邮件、会议、行政事务)
- 为什么深度工作有价值?(高价值产出、技能增长、成就感)
- 神经科学解释:默认模式网络 vs 专注模式
- 个人实验目标:将每日深度工作时间从1h提升到4h
**第三部分:我的3个月实验设计(200字)**
- 基线测量:第一周不干预,记录专注情况(RescueTime数据)
- 变量引入:每周测试一种方法(时间块、地点、工具)
- 评估指标:深度工作时长、任务完成量、主观疲劳度
- 数据收集:自动追踪+每日日志
- 工具:Forest、番茄钟、白噪音、时间追踪App
**第四部分:7个有效方法(800字)**
1. **时间块法**:每天固定2-3个90分钟深度时段(早上最佳)
- 如何保护这些时段?(关闭通知、告知同事)
- 案例:我如何说服老板让我上午不参加会议
2. **地点隔离**:物理空间决定心理状态
- 家里打造深度工作角(哪怕只有一张桌子)
- 共享办公/图书馆vs咖啡馆(哪个更好?)
- 通勤时间利用(地铁上不适合深度,但可做浅层)
3. **工具辅助**:科技帮你专注,而不是分心
- Forest种树:游戏化保持专注
- Cold Turkey Blocker:屏蔽 distracting sites
- 白噪音/自然声:掩盖环境噪音
- 双显示器设置(主屏工作,副屏消息)
4. **仪式感建立**:让大脑进入"工作模式"
- 开始前:一杯茶+5分钟冥想+清单
- 结束时:总结+小奖励
- 服装:穿"工作装"即使在家
5. **批处理浅层工作**:集中处理邮件、消息、会议
- 每天2个固定时段(下午3点、5点)
- 其他时间绝不查看
- 快速回复模板
6. **注意力训练**:像肌肉一样训练专注
- 每天10分钟正念冥想(Headspace
- 单任务练习(吃饭不刷手机)
- 阅读长文章(训练持续注意力)
7. **深度休息**:专注需要高质量的休息
- 90分钟工作+15分钟休息(非屏幕活动)
- 午休必须离开工位
- 晚上完全断开(数字安息日)
- 睡眠优先(专注力的基础)
**第五部分:实验结果与数据(300字)**
- 深度工作时长变化:基线1.2h/天 → 实验后3.8h/天
- 任务完成量提升:每周完成项目数从2个→5个
- 主观疲劳度下降:从每天7分(10分制)降到4分
- 最有用的方法:时间块法(增加1.5h)+ 工具辅助(增加0.8h)
- 意外发现:地点隔离效果不明显(家里角落即可)
**第六部分:常见障碍与解决方案(200字)**
- 同事/老板随时找你怎么办?(设置"专注时段"通知)
- 自己忍不住刷手机怎么办?(物理隔离手机)
- 老板要求即时回复怎么办?(协商沟通规则)
- 创意型工作无法计划怎么办?(灵感记录+深度时间块)
- 适应期烦躁怎么办?(从1小时开始,逐步增加)
### 结尾(200字)
- 深度工作不是天赋,是可以训练的技能
- 关键不是完美,而是持续改进
- 科技让我们更容易分心,但也给了我们工具对抗
- "宇之然"理念:专注才能产生深度价值
- 挑战:从明天开始,尝试一个90分钟的时间块
---
## 资源清单
### 数据来源(待收集)
- [ ] Microsoft 注意力跨度研究报告
- [ ] RescueTime 知识工作者专注数据
- [ ] 番茄工作法效果 meta-analysis
- [ ] 神经科学关于注意力切换的研究
- [ ] 正念冥想对专注力的影响
### 图片素材(待准备)
- [ ] 深度工作时长趋势图(实验前后对比)
- [ ] 7种方法示意图
- [ ] 时间块安排示例(日历截图)
- [ ] 专注工具界面截图(Forest、白噪音)
- [ ] 实验数据表格
### 引用来源(待标注)
- [ ] Cal Newport《深度工作》
- [ ] 《倦怠社会》等社会学视角
- [ ] 心理学研究(注意力、心流)
- [ ] 工具官网和教程
---
**决策**:非常实用的主题,数据驱动,易引发收藏和转发。适合知乎收藏夹和小红书打卡。建议 high 优先级。
-183
View File
@@ -1,183 +0,0 @@
# 副业月入过万:我一个程序员的三年探索之路
**领域**:工作 / 副业
**形式**:实操经验 + 方法论总结
**预估字数**2700
**核心观点**:副业不是投机,而是将主业技能产品化、多元化的过程
**受众痛点**
- 工资不够花,想搞钱但不知道从哪开始
- 听说过很多副业(自媒体、电商、咨询),但不知哪个适合
- 时间有限,如何在主业之外有效投入
- 担心副业影响主业,被公司发现
- 交过学费买课,但没赚到钱
- 想开始但总在观望,不敢行动
**独特角度**
- 从技术人视角(程序员)看副业选项,但方法论通用
- 不鼓吹"月入十万"的毒鸡汤,诚实展示时间和金钱投入
- 强调"技能产品化"而非"时间换钱"(避免陷入低效劳务)
- 用数据说话:我的三年副业收入变化、时间分配、失败案例
- 结合长期主义:副业是为了人生更多选择,不仅是赚钱
**数据/案例**(至少3个来源):
1. 中国职场人副业调研报告(参与率、收入分布)
2. 程序员技能变现渠道对比(外包、咨询、产品、内容)
3. 时间投入与回报曲线(前期低效,后期复利)
4. 个人三年副业数据(2019-2021):时间投入vs收入产出
5. 成功/失败副业案例分析(为什么有些成了有些黄了)
**预估完成时间**5天
**优先级**:高
**预计发布时间**2026-04-25
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 9 | 大量职场人有副业需求,搜索和阅读量巨大 |
| 独特性 | 7 | 副业话题热,需要真实数据和深度分析才能出彩 |
| 数据可得性 | 9 | 调研报告多,个人案例可详细追踪 |
| 可持续性 | 9 | 可系列(不同阶段、不同技能、不同平台) |
| 平台契合度 | 9 | 知乎职场/赚钱热门,小红书副业也火 |
| 品牌契合度 | 9 | 科技赋能多元收入,增加人生选择权 |
| **总分** | **52** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(400字)
- 场景:2019年,我还是一枚996程序员,每月税后1.2万,觉得不够用
- 尝试:接过外包、做过咨询、写过公众号、卖过课程、尝试过跨境电商
- 现状:现在副业收入已超过主业,时间自由,有选择权
- 核心观点:副业不是一夜暴富,而是技能产品化和持续积累的过程
- 预告:分享三年探索的数据、踩坑、方法论
### 主体(2200字)
**第一部分:副业真相(破除幻想)(300字)**
- 不是所有副业都赚钱:80%的人副业收入<2000元
- 不是 Easily Money:前期投入巨大(时间、学习、试错)
- 不是可持续的劳务:用时间换钱必然后劲不足
- 数据:我的前18个月副业收入总计才8000元
- 真相:副业是"第二曲线"需要长期培育
**第二部分:副业类型全景图(300字)**
- **技能变现型**:外包、咨询、培训(适合技术人)
- 平台:Upwork、电鸭、知乎 Live、自有客户
- 特点:时间换钱,单价高但线性增长
- **产品型**:开发工具、课程、模板、SaaS
- 前期投入大,后期复利
- 例子:我做的 Chrome 插件年收入X万
- **内容型**:公众号、知乎、B站、小红书
- 长周期变现,但品牌效应强
- 例子:我的技术博客如何带来咨询客户
- **电商/贸易型**:跨境电商、二手、定制
- 资金和库存压力大,技术人不擅长
- 我失败的案例:淘宝店
- **投资型**:股票、基金、加密货币(不推荐作为主业外重点)
- **混合型**:我的组合:技能变现+产品+内容
**第三部分:程序员适合什么副业?(300字)**
- 优势:技术可远程交付、技能稀缺、学习能力强
- 最适合:
1. 技术外包/咨询(起步最快,但天花板低)
2. 开发小工具/SaaS(复利高,但需要产品思维)
3. 技术内容创作(博客、课程、咨询)
4. 技术团队组建(成为自由职业团队leader)
- 不适合:
1. 需要大量资金的(电商库存)
2. 纯体力或线下服务的
3. 需要法律资质的(金融咨询)
**第四部分:我的三年路线图(400字)**
- **第1年(2019**:探索+试错
- 目标:尝试3种副业,找到1-2个可行的
- 行动:接外包(赚了1.5万)、写博客(0收入)、尝试跨境电商(亏损2k)
- 学习:时间管理、客户沟通、基础营销
- 结果:发现外包可行但累,内容有潜力但慢
- 时间投入:每周10-15小时
- **第2年(2020**:聚焦+优化
- 目标:在技术外包+内容创作双线推进,月入稳定5000
- 行动:提高外包单价(从100$/h到150$/h),博客系统性写作
- 学习:个人品牌、定价策略、产品化
- 结果:外包月均3000,博客带来1个咨询项目(5000)
- 时间投入:每周15小时(主业压力大)
- **第3年(2021)**:产品化+规模化
- 目标:将知识产品化,打造"第二曲线"
- 行动:开发Chrome插件(年收入2万),出版电子书(1.5万),高单价咨询
- 学习:产品运营、用户增长、自动化
- 结果:副业月均1.2万,首次超过主业
- 时间投入:每周20小时(但其中50%是产品维护)
**第五部分:时间管理:主业+副业如何平衡?(200字)**
- 原则1:主业优先,不能因副业影响主业绩效(可能被开除)
- 原则2:副业时间固定(早上6-8点、晚上8-10点、周末4小时)
- 原则3:批量处理(每周日规划,减少切换成本)
- 原则4:学会说"不"(拒绝低价值副业请求)
- 我的时间分配:周总副业时间15h(低于20h安全线)
**第六部分:技能产品化:从时间换钱到复利增长(300字)**
- 外包(时间换钱):1h=150元,但做才有
- 产品(一次创造,多次销售):开发一个工具,持续销售
- 内容(建立品牌,带来被动机会):博客带来咨询,不需主动找
- 我的产品:Chrome插件(每月销售,收入稳定)
- 如何选择产品方向?(你解决自己的问题,可能也是别人的问题)
- 最小可行产品(MVP)快速验证
**第七部分:避坑指南(200字)**
- 坑1:低价竞争(不要接低于市场价的活,会拉低你的价值)
- 坑2:完美主义(先做出来,再优化;不要等完美才发布)
- 坑3:孤军奋战(找导师、加入社群、互相支持)
- 坑4:忽略法律(兼职合同、知识产权、个税)
- 坑5:忽视健康(连续熬夜接单,不可持续)
- 坑6:过度承诺(能力范围内再接,否则口碑崩塌)
**第八部分:给初学者的行动清单(200字)**
1. 盘点你的技能:什么可远程交付?什么可产品化?
2. 选择1-2个方向(不要贪多)
3. 用3个月探索:每方向投入100小时试水
4. 记录数据:时间投入、收入、满意度
5. 6个月后评估:哪个ROI最高?哪个最有潜力?
6. 聚焦+优化:放弃低ROI的,加注高潜力的
7. 建立系统:不是每次从零开始,而是有流程
8. 定期复盘(每月一次)
### 结尾(200字)
- 副业不只是赚钱,更是探索人生更多可能
- 我的现状:主业稳定,副业自由,有选择权
- "宇之然"理念:科技给人自由,副业是实现自由的一种路径
- 鼓励:从今晚开始,花1小时盘点你的技能
- 互动:你的副业探索经历如何?欢迎分享
---
## 资源清单
### 数据来源(待收集)
- [ ] 中国职场人副业现状调研(智联、脉脉等)
- [ ] 程序员收入与副业相关性数据
- [ ] 自由职业平台(Upwork、电鸭)收入报告
- [ ] 数字游民与副业的关系研究
- [ ] 时间投入与收入曲线(复利效应数据)
### 图片素材(待准备)
- [ ] 三年副业收入增长曲线
- [ ] 时间分配饼图(每天/每周)
- [ ] 副业类型对比矩阵(时间vs金钱vs长期价值)
- [ ] 技能产品化流程图
- [ ] 避坑指南信息图
### 引用来源(待标注)
- [ ] 《副业赚钱》等书籍
- [ ] 知名副业博主案例
- [ ] Upwork/Toptal 平台报告
- [ ] 个人博客和项目数据
---
**决策**:刚需话题,竞争激烈但需求巨大。真实数据和实操经验是关键。适合知乎和小红书。建议 high 优先级。
@@ -1,180 +0,0 @@
# 正念冥想一年:从焦虑到平静的转变
**领域**:人文 / 心理健康
**形式**:个人实验 + 科学解读
**预估字数**2200
**核心观点**:冥想不是玄学,是可训练的大脑肌肉;一年练习带来可测量的认知和情绪变化
**受众痛点**
- 焦虑、失眠、注意力不集中
- 想尝试冥想但觉得"太虚"、"静不下来"
- 工作压力大,找不到放松方法
- 情绪容易波动,想提升情绪调节能力
- 听说过冥想好处,但不知如何开始
- 试过几次没效果就放弃了
**独特角度**
- 用科学家态度做自我实验(追踪数据、前后对比)
- 破除玄学色彩,基于神经科学和心理学
- 展示失败和挣扎(不是一蹴而就)
- 结合科技:Meditation App、生物反馈设备
- 强调"微小习惯":每天10分钟胜过每周2小时
**数据/案例**(至少3个来源):
1. 冥想改变大脑结构的研究(灰质增加、杏仁核缩小)
2. 压力激素(皮质醇)水平变化数据
3. 注意力、情绪调节能力的心理测量
4. 个人一年追踪:每天10分钟,睡眠、焦虑评分、专注力自评
5. 其他冥想者案例(长期练习者)
**预估完成时间**4天
**优先级**:中
**预计发布时间**2026-04-27
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 8 | 压力大的都市人广泛需要,但转化率不确定 |
| 独特性 | 8 | 个人实验+数据追踪,破除玄学 |
| 数据可得性 | 8 | 神经科学研究多,自我追踪简单 |
| 可持续性 | 7 | 可系列(不同时长、不同技术、不同人群) |
| 平台契合度 | 8 | 知乎心理学/健康,小红书打卡风 |
| 品牌契合度 | 9 | 回归本心,科技辅助内心平静 |
| **总分** | **48** | 考虑执行 ✅ |
---
## 大纲草稿
### 开头(300字)
- 场景:2024年初,我焦虑到失眠,朋友建议"试试冥想"
- 抵触:觉得是玄学,静不下来,10分钟像几个小时
- 决定:用科学家态度做个实验,每天10分钟,追踪一年
- 结果:焦虑评分下降40%,睡眠质量提升,专注力改善
- 预告:分享这一年我的数据、方法、挣扎和收获
### 主体(1700字)
**第一部分:冥想是什么?(破除迷思)(200字)**
- 不是宗教,不是玄学,是注意力训练
- 不是停止思考,是观察思绪而不被带走
- 不是马上变平静,是练习"觉知"
- 科学解释:默认模式网络、前额叶皮层、神经可塑性
- 我的初始怀疑:一个程序员如何相信这个?
**第二部分:为什么现代人更需要冥想?(200字)**
- 信息过载:每天被数千条信息轰炸
- 多任务假象:切换导致注意力碎片化
- 压力激素持续分泌(皮质醇)
- 社交媒体比较和FOMO
- 缺乏"无所事事"的时间
- 后果:焦虑、失眠、注意力涣散、情绪不稳定
**第三部分:我的实验设计(200字)**
- 目标:一年后减少焦虑、改善睡眠、提升专注
- 方法:每天10分钟冥想(App指导:Headspace + Calm
- 追踪指标:
- 主观:焦虑评分(1-10)、睡眠质量(1-5)、专注时长
- 客观:皮质醇(唾液测试,每月1次)、心率变异性(HRV)
- 工具:Muse头环(脑电生物反馈)、Apple Watch睡眠追踪
- 记录:每日简记,每周复盘
**第四部分:前3个月的挣扎(200字)**
- 第1周:完全静不下来,中途放弃多次
- 第1月:平均每次5分钟就开始看时间
- 第2月:开始有一点点"空白时刻"
- 第3月:发现思绪模式(焦虑的思绪、工作的思绪)
- 关键:坚持每天做,不追求效果
- 工具帮助:App的引导声音让我stay
**第五部分:6个月时的变化(200字)**
- 主观感受:更容易进入状态(5分钟就能平静)
- 睡眠:入睡更快,夜间醒来减少(数据:平均入睡时间从30min→15min)
- 焦虑:明显降低(评分从7→4
- 专注:工作中更容易"心流"
- 意外:对情绪觉察力提升(能 early detect stress
- 生物反馈:HRV提升(从50ms→70ms
**第六部分:一年后的数据(200字)**
- 坚持率:85%365天,310天完成10min+
- 焦虑评分:从7.2降至4.1(下降43%)
- 睡眠质量:从2.8升至4.1(1-5分制)
- 专注时间:基线1.2h/天 → 2.5h/天( meas by RescueTime
- 皮质醇:早晨水平下降20%
- 大脑变化:Muse数据显示α波(放松)增加,β波(焦虑)减少
- ROI:每天10分钟,换来心理和生理健康提升,值吗?(一个数量级的健康价值)
**第七部分:关键技术/方法(300字)**
1. **呼吸冥想**:专注呼吸,是最基础也最有效的
- 方法:鼻吸鼻呼,数呼吸(1-10循环)
- 最佳时段:早晨起床后或晚上睡前
- 困难:思绪飘走正常,温柔拉回即可
2. **身体扫描**:释放身体紧张
- 方法:从脚到头,逐个部位觉察
- 适合:压力大时,身体紧绷
- 效果:发现很多平日忽略的肌肉紧张
3. **正念行走**:把步行变成冥想
- 方法:走路时觉察脚步、身体移动、呼吸
- 适合:久坐办公室,通勤路上
- 我:午休15分钟步行冥想,下午效率高
4. **3分钟呼吸空间**:快速冷静
- 方法:1分钟觉察,1分钟专注呼吸,1分钟觉察整体
- 适合:会议前、压力事件前
- 使用频率:平均每天2-3次
5. **科技辅助**
- Headspace/Calm:引导冥想(新手强烈推荐)
- Insight Timer:免费课程和统计
- Muse头环:生物反馈,可视化大脑状态
- Apple Watch:呼吸App,随时做1分钟
**第八部分:常见问题与解答(200字)**
- Q:静不下来怎么办?(接受它,坚持做,改变需要时间)
- Q:每天多久合适?(10分钟起步,30分钟最佳)
- Q:什么时间做最好?(早晨或睡前最易坚持)
- Q:必须坐垫子吗?(任何姿势,关键是舒适且清醒)
- Q:多久见效?(2-4周有初步感受,1年明显变化)
- Q:要加入宗教吗?(完全不需要, secular mindfulness
- Q:影响创造力吗?(反而提升,因为减少心理噪音)
### 结尾(200字)
- 冥想一年,是我给自己最好的投资之一
- 不是变成"无欲无求",而是更清晰、更平静、更有选择权
- "宇之然"理念:科技沟通万物,最终回归内心本真
- 鼓励:从今晚5分钟开始,用数据追踪自己的变化
- 互动:你有冥想经验吗?分享你的故事
---
## 资源清单
### 数据来源(待收集)
- [ ] 冥想改变大脑结构研究(哈佛、斯坦福等)
- [ ] 压力激素皮质醇与冥想关系研究
- [ ] 注意力、情绪调节能力测量工具(PANAS等)
- [ ] 心率变异性(HRV)与冥想数据
- [ ] 冥想对睡眠影响 meta-analysis
### 图片素材(待准备)
- [ ] 一年冥想坚持日历(打卡图)
- [ ] 追踪数据图表(焦虑、睡眠、专注趋势)
- [ ] 大脑结构变化示意图(灰质、杏仁核)
- [ ] HRV和脑电数据截图(如果使用设备)
- [ ] 冥想姿势和环境照片
### 引用来源(待标注)
- [ ] Headspace/Calm 科学页面
- [ ] 冥想研究综述(如《心理学前沿》)
- [ ] 《正念的奇迹》《禅与摩托车维修艺术》等书籍
- [ ] 神经科学教材
---
**决策**:心理健康内容需求大,但竞争也激烈。个人实验+数据追踪+科学解释是差异点。适合知乎和小红书。建议 medium 优先级。
-182
View File
@@ -1,182 +0,0 @@
# 极简3年:我从囤积症到少物生活的转变
**领域**:人文 / 生活方式
**形式**:个人转变故事 + 实操指南
**预估字数**2500
**核心观点**:极简不是扔东西,而是重新定义"足够";通过减少物质,增加精神丰盈
**受众痛点**
- 家里东西越来越多,整理完很快又乱
- 冲动购物,买了很多用不上的东西
- 感觉被物品包围,想要斷舍离但下不去手
- 想简化生活但不知从何开始
- 担心扔了后悔,觉得"万一以后用得上"
- 追求品质但预算有限,陷入"买 cheap stuff 然后浪费"
**独特角度**
- 从囤积症患者到极简主义者的真实转变(有心理过程)
- 不鼓吹"一无所有",而是"刚刚好"
- 量化成果:减少了70%物品,省了多少钱,时间如何变化
- 连接科技:智能家居、数字替代(电子书、云存储)、物品追踪
- 从物质极简到精神极简(信息、社交、目标)
**数据/案例**(至少3个来源):
1. 极简主义创始人(Joshua & Ryan)理念和实践
2. 物品生命周期研究(平均使用次数、浪费数据)
3. 消费心理学(冲动购物的神经机制)
4. 个人3年极简数据:物品数量变化、消费减少、时间分配
5. 多人案例采访(不同极简程度)
**预估完成时间**4天
**优先级**:中
**预计发布时间**2026-04-29
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 8 | 物质过剩的都市人有共鸣,但行动者少 |
| 独特性 | 8 | 转变故事有吸引力,数据驱动 |
| 数据可得性 | 8 | 个人追踪数据+消费研究+极简社区案例 |
| 可持续性 | 8 | 可系列(不同房间、不同人群、不同阶段) |
| 平台契合度 | 8 | 知乎生活方式/小红书极简话题 |
| 品牌契合度 | 10 | 回归自然本心,少物多思 |
| **总分** | **50** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(300字)
- 场景:2019年,我搬进新家,发现60%的物品两年没用过
- 触目惊心:衣柜塞满、抽屉爆炸、储物柜 hidden junk
- 决定:开始极简之路,目标是"每件东西都有用且愉悦"
- 3年变化:物品减少70%,消费减半,时间多出,焦虑下降
- 预告:分享转变过程、方法、数据和教训
### 主体(2000字)
**第一部分:从囤积到觉醒(300字)**
- 我的囤积症:舍不得扔,总觉得"会用到"
- 触发点:搬家发现大量未拆封的购物袋、过期的优惠券
- 消费主义陷阱:打折、社交媒体种草、身份焦虑
- 觉醒:物品在消耗我的注意力、空间、金钱
- 开始:先从小区域(一个抽屉)动手
**第二部分:极简不是数字,是心态(300字)**
- 误区:极简=苦行= empty room
- 真相:极简是"足够"哲学,留下真正重要和愉悦的
- 我的标准:每件物品是否在用?是否带来愉悦?是否可替代?
- 过程不是一次断舍离,而是持续优化
- 3年我扔了/捐了/卖了 1000+ 件物品
- 不是目标,是手段(为了时间、精力、金钱自由)
**第三部分:我的3年路线图(400字)**
- 第1年(2019):粗筛+快速清理
- 方法:一年未用的全扔(除了纪念品)
- 成果:物品减少40%,清空3个储物箱
- 挑战:扔东西的心痛感(浪费钱的心理)
- 第2年(2020):精细化+系统化
- 方法:One In, One Out(进一出一),严格购买决策流程
- 成果:物品再减少20%,消费减少35%
- 学习:识别真实需求 vs 虚假欲望
- 建立"30天清单":想买的东西放30天,90%会取消
- 第3年(2021):深化+扩展
- 方法:数字极简、信息极简、社交极简
- 成果:物品再减少10%,精神更清爽
- 维持:定期季度审查,防止反弹
- 极简成为习惯,不再需要"努力"
**第四部分:具体方法(500字)**
1. **分类清理法**
- 从易到难:抽屉→衣柜→书柜→储物间
- 4箱法:扔、捐、卖、留(保留)
- 纪念品处理:选一个盒子(大小限制),其他拍照留底
2. **购买决策流程**
- 问自己:我需要它吗?我有替代品吗?它带来长期愉悦吗?
- 30天等待期(除非是消耗品)
- 控制冲动:取消促销订阅、不逛购物App
- 质量优先:买少买好,耐用性优先
- 案例:少买了100件便宜T恤,买了5件优质耐穿的
3. **数字极简**
- 卸载不用的App,取消不看的订阅
- 照片整理:删除模糊、重复的,精选打印
- 文件存档:云存储+本地双备份,删临时文件
- 通知管理:只留必要的,减少干扰
4. **信息极简**
- RSS订阅精选(从200+减到20个高质量源)
- 社交媒体定时关闭(每天30min
- 不追热点,只深度阅读
- 信息输入输出平衡(少摄入,多创作)
5. **社交极简**
- 减少无效社交,保留高质量关系
- 会议只参加必要的,其他异步沟通
- 学会拒绝:时间是最稀缺资源
**第五部分:量化成果(200字)**
- 物品数量:从约1500件 → 450件(减少70%
- 消费:年均减少 40%(省下约3万/年)
- 时间:每周少花5小时整理、购物、找东西
- 空间:清空3个储物柜,家里更宽敞
- 心理:焦虑下降(从6→3分),决策更快(因为选择少)
- 环保:减少碳足迹(估算:每年少产生200kg垃圾)
**第六部分:挑战与妥协(200字)**
- 家人不理解:老婆/父母不配合,只能整理自己的部分
- 社会压力:节日送礼、人情往来难简化
- 实用性:有些"可能需要"的东西还是留了(工具、应急)
- 不是完美:还有10%的物品是"maybe future use"
- 转折点:当体验到"清爽"后,家人慢慢受影响
**第七部分:给初学者的建议(200字)**
- 从小开始:先整理一个抽屉或一个角落
- 不要一次扔太多:给缓刑期,减少心理阻力
- 拍照留念:纪念品不扔,但数字化
- 寻找社群:极简社区、Minimalism subreddit
- 接受不完美:目标是"足够好",不是空无一物
- 聚焦价值:极简是为了释放资源(时间、金钱、精力)做更重要的事
- "宇之然"理念:少物多思,回归内心真实需求
### 结尾(200字)
- 极简3年,生活更轻盈,更有方向
- 不是物质越多越幸福,而是"足够"才幸福
- 极简让我有更多资源去追求:深度阅读、健康生活、家人陪伴
- 呼吁:从今天开始,清理一个角落,体验清爽的感觉
- 互动:你的极简实践是什么?有什么困惑?
---
## 资源清单
### 数据来源(待收集)
- [ ] 极简主义书籍(Goodreads、豆瓣)
- [ ] 消费心理学研究(过度消费、囤积症)
- [ ] 物品生命周期和浪费数据(环保组织)
- [ ] 时间追踪数据(整理、购物时间)
- [ ] 极简对幸福感影响研究
### 图片素材(待准备)
- [ ] 整理前后对比图(同一区域)
- [ ] 物品数量变化图表
- [ ] 消费金额变化曲线
- [ ] 空间对比图(清空的储物柜)
- [ ] 极简生活场景(清爽的房间、精选的物品)
### 引用来源(待标注)
- [ ] Joshua Fields Millburn & Ryan Nicodemus
- [ ] 《极简主义》《断舍离》等书籍
- [ ] The Minimalists Podcast
- [ ] 消费主义批判文献
---
**决策**:生活方式的个人转变故事容易引发共鸣,适合叙事+方法论。小红书和知乎都合适。建议 medium 优先级。
-176
View File
@@ -1,176 +0,0 @@
# 城市观鸟指南:如何在水泥森林发现 biodiversity
**领域**:自然 / 科普
**形式**:实操指南 + 城市生态观察
**预估字数**2000
**核心观点**:自然不在远方,就在身边;城市也是wildlife的栖息地,学会观察能提升生活幸福感
**受众痛点**
- 想接触自然但没时间去郊外
- 觉得城市只有鸽子麻雀,没啥可看
- 想带孩子体验自然不知从何开始
- 对生物多样性有概念但不会识别
- 担心需要专业设备和大量学习
- 城市生活枯燥,寻求新鲜感和治愈
**独特角度**
- 城市观鸟入门,从"完全小白"到"常见鸟达人"
- 结合科技:观鸟App(Merlin、鸟哨识别)、社区数据平台(eBird)
- 强调"身边":小区、公园、甚至阳台上就能开始
- 用数据说话:上海/北京等城市的鸟类种类统计
- 连接宇之然理念:科技帮助人与自然的连接
**数据/案例**(至少3个来源):
1. 城市生物多样性调研报告(中国的城市化与野生动物)
2. 常见城市鸟类图鉴(种类、特征、习性)
3. eBird/懂鸟等平台用户提交数据(观测频率)
4. 个人城市观鸟记录(一年在X城市观测到XX种)
5. 观鸟对心理健康的研究(缓解压力、提升注意力)
**预估完成时间**3天
**优先级**:中
**预计发布时间**2026-05-01
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 7 | 相对垂直,但亲子、自然爱好者多 |
| 独特性 | 9 | 城市+观鸟结合,实用性强,科普价值高 |
| 数据可得性 | 8 | 鸟类数据、App工具、调研报告充足 |
| 可持续性 | 9 | 可系列(不同季节、不同城市、不同难度) |
| 平台契合度 | 8 | 知乎科普/小红书图文,适合短视频延伸 |
| 品牌契合度 | 10 | 科技+自然+生活,完美契合 |
| **总分** | **51** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(200字)
- 场景:在上海陆家嘴办公室,突然听到窗外各种鸟叫,一个想法:城市里有啥鸟?
- 意外:下载观鸟App后,发现小区里有20+种鸟
- 变化:现在走路会抬头,阳台常备望远镜,生活多了乐趣
- 核心观点:城市不是自然的对立面,而是生态系统一部分
- 预告:教你从零开始城市观鸟,需要什么、去哪看、如何识别
### 主体(1600字)
**第一部分:城市里有多少鸟?(200字)**
- 数据:上海已记录鸟类 400+ 种,北京 500+ 种
- 常见的是哪些?(麻雀、白头鹎、乌鸫、珠颈斑鸠、白鹭、喜鹊等)
- 季节性:候鸟迁徙(春秋季更多)
- 城市生态:公园、河流、小区绿地、屋顶都能看到
- 我的记录:在XX小区/公园一年观测到XX种
- 结论:比你想象的多得多
**第二部分:观鸟入门装备(200字)**
- **最低配置**(手机 + 免费App):
- 懂鸟/Merlin Bird ID(声音识别神器)
- eBird中国(记录、查看附近观测)
- 观鸟相机(手机即可,需要长焦建议入门望远镜2k-3k)
- **进阶配置**
- 望远镜(8x42 双筒,入门推荐)
- 鸟类图鉴(纸质书或App
- 记录本或电子表格
- **着装**:低调颜色(避免亮色惊鸟),舒适鞋子
- **.total cost*:零成本起步,深度5000元以内
**第三部分:去哪看?(200字)**
- **城市公园**:最佳入门地(有人维护、鸟类聚集)
- 上海:世纪公园、滨江森林公园、上海植物园
- 北京:奥林匹克森林公园、颐和园、圆明园
- **湿地/河流**:水鸟聚集(白鹭、翠鸟、水鸡)
- 例子:上海崇明东滩、北京野鸭湖
- **小区和校园**:你身边的第一站
- 树木多的老小区往往有惊喜
- 大学校园绿化好,鸟类多
- **城市高点**:观猛禽(迁徙季节)
- **App辅助**eBird Hotspots 查看附近观测点
**第四部分:如何识别?(300字)**
- 观鸟四要素:体型、颜色、行为、声音
- **步骤1**:先看大小和整体形状(像麻雀还是喜鹊?)
- **步骤2**:看颜色分布(头部、背部、腹部、翅膀)
- **步骤3**:看行为(在地上找食?树上鸣叫?水中游?)
- **步骤4**:听声音(Merlin识别超准)
- **步骤5**:对照图鉴或App
- **常见城市鸟快速指南**(选10种):
- 麻雀:灰褐色,群体,地面觅食,城市最常见
- 白头鹎:白色头顶,背部橄榄绿,叫"WA-DA-DI"
- 乌鸫:雄性黑褐色,黄色眼圈,鸣叫婉转
- 珠颈斑鸠:棕灰色,颈后白斑像珍珠,咕咕叫
- 白鹭:白色,长腿长颈,水边常见
- 喜鹊:黑白分明,长尾巴,聪明
- 灰喜鹊:蓝灰色,群居,山区更多
- 大山雀:蓝黄配色,叫"锌锌"
- 啄木鸟:斑纹红头,树洞敲击声
- 普通翠鸟:橙蓝色, waterside,极快闪
**第五部分:季节与迁徙(200字)**
- 春季(3-5月):候鸟北归,种类最多,常见莺类
- 夏季(6-8月):留鸟+繁殖季,早上鸣叫活跃
- 秋季(9-11月):候鸟南迁,猛禽(鹰、鸮)容易看到
- 冬季(12-2月):候鸟到南方越冬,水鸟多
- 迁徙高峰期:每年4月中、10月中,去湿地有惊喜
**第六部分:科技让观鸟更简单(200字)**
- **声音识别**Merlin Bird ID(免费的!),对着鸟叫录一下,马上指出种类和概率
- **图像识别**:懂鸟App上传照片,准确率90%+
- **社区数据**:eBird 查看附近谁看到了什么,热门 spots
- **记录与统计**:电子清单,个人记录,年度总结
- **社交**:加入城市观鸟社群,组团学习
- "宇之然":科技降低自然体验门槛
**第七部分:给新手的建议(200字)**
1. 从小区开始,别追求稀有鸟
2. 带双筒望远镜(非必需但提升体验)
3. 下载 Merlin 和懂鸟,声音识别是关键
4. 学习10种最常见的,再扩展
5. 晨起最佳(鸟活跃,人少)
6. 安静缓慢,避免惊鸟
7. 记录观测(App自动记录)
8. 加入本地观鸟群,老手带入门快
9. 长期坚持,你会发现城市生命的丰富
10. 保护鸟类:不打扰、不投喂、不破坏栖息地
### 结尾(200字)
- 观鸟一年,我在城市发现了20+种鸟,还记录到罕见的红嘴蓝鹊
- 自然不是远方的诗,是身边的小确幸
- 提升了观察力、耐心、对季节变化的敏感
- 带孩子观鸟是最佳的自然教育
- "宇之然":科技沟通万物,让我重新发现身边自然
- 鼓励:明天早上花15分钟,去小区听鸟叫、看鸟飞
- 互动:你所在城市常见什么鸟?分享你的发现
---
## 资源清单
### 数据来源(待收集)
- [ ] 中国主要城市鸟类名录(中国科学院、地方鸟类学会)
- [ ] eBird 中国数据(观测统计)
- [ ] 城市生态学研究(城市化对鸟类影响)
- [ ] 鸟类迁徙路线图(东亚-澳大利西亚迁飞区)
- [ ] 观鸟对心理健康益处研究
### 图片素材(待准备)
- [ ] 常见城市鸟类照片(自己拍摄或购买授权)
- [ ] 观鸟装备照片(望远镜、App截图)
- [ ] 城市观鸟地点(公园、湿地照片)
- [ ] 观鸟者视角(手持望远镜看鸟)
- [ ] 鸟类识别对比图(相似鸟区分)
### 引用来源(待标注)
- [ ] Cornell Lab Merlin Bird ID
- [ ] eBird 中国(https://ebird.org/china
- [ ] 中国观鸟组织(如:中国观鸟记录中心)
- [ ] 鸟类图鉴(如《中国鸟类观察手册》)
- [ ] 城市生态学文献
---
**决策**:入门级自然科普,大众接受度高,亲子群体尤其适合。内容轻量但实用,适合快速创作。建议 medium 优先级。
-231
View File
@@ -1,231 +0,0 @@
# 个人知识管理系统(PKM)实践:我用一年搭建的第二大脑
**领域**:工作 / 生产力
**形式**:实操教程 + 系统设计
**预估字数**2800
**核心观点**:信息时代,会学习不如会管理知识;一个适合自己的PKM系统能释放认知带宽,提升创造力和决策质量
**受众痛点**
- 收藏无数,但从未回看(收藏夹吃灰)
- 知识点散落在各处(笔记App、书签、微信收藏、纸质本)
- 需要时找不到,找到的又不完整
- 学会了方法但无法坚持(Roam太复杂、Notion不会用)
- 信息过载,不知道如何筛选和组织
- 想建立系统但被工具选择困住(工具 switcher)
**独特角度**
- 不推销特定工具,强调方法论+适合自己的选择
- 展示一年的实践数据和系统演变(不是一蹴而就)
- 强调"用"而非"建":系统为输出服务,不为收藏
- 结合AI辅助(自动标签、智能摘要、关联发现)
- 从程序员视角但内容通用
**数据/案例**(至少3个来源):
1. PKM方法论(Zettelkasten、PARA、CODE等)
2. 工具对比研究(Notion、Obsidian、Roam、Logseq、飞书、Notability
3. 个人一年数据:笔记数量、链接数、输出成果、时间节省
4. 知识工作者效率研究(信息检索耗时)
5. 成功案例:其他内容创作者如何用PKM支撑输出
**预估完成时间**5天
**优先级**:中
**预计发布时间**2026-05-03
**状态**:待处理
---
## 选题评估矩阵
| 维度 | 评分 | 说明 |
|------|------|------|
| 受众覆盖 | 8 | 知识工作者广泛需要,但执行率低 |
| 独特性 | 8 | 个人系统演变+数据追踪,避免工具论 |
| 数据可得性 | 9 | PKM方法论丰富,个人数据可量化 |
| 可持续性 | 9 | 可系列(不同工具、不同场景、进阶技巧) |
| 平台契合度 | 9 | 知乎效率/工具,小红书个人成长 |
| 品牌契合度 | 9 | 科技赋能知识管理,提升深度思考 |
| **总分** | **52** | 优先执行 ✅ |
---
## 大纲草稿
### 开头(400字)
- 场景:2024年,我发现自己有5个笔记App、2000+零散笔记、无数书签,但写文章时还是找不到资料
- 问题:信息时代,我们被信息淹没,却缺乏可用的知识
- 决定:用一年时间,搭建并完善个人知识管理系统(PKM)
- 结果:笔记整理到统一系统,知识关联起来了,写文章效率提升3倍
- 核心观点:PKM不是收藏信息,而是将信息转化为知识资产
- 预告:分享我的方法论、工具选择、系统设计、一年数据和教训
### 主体(2200字)
**第一部分:为什么你需要PKM?(300字)**
- 信息过载时代:每年产生信息量 vs 大脑处理能力
- 知识 ≠ 信息:信息是 raw data,知识是 processed and connected
- 不管理的代价:
- 重复学习(同一个知识点看过多次但没记住)
- 需要时找不到(搜索耗时>30分钟)
- 知识孤岛(孤立信息无法产生新想法)
- 决策质量低(缺乏完整背景)
- 研究:知识工作者平均每天花1.5小时找信息(麦肯锡)
- PKM的价值:提高检索效率、支持创造性连接、减少认知负荷
**第二部分:PKM方法论概览(200字)**
- **Zettelkasten**(卡片盒笔记法):原子化、链接、渐进式总结
- 核心:每个笔记一个想法,用链接建立连接
- 适合:深度思考者、研究者、作家
- **PARA**Tiago Forte):按可行动性组织(Projects, Areas, Resources, Archives
- 核心:不是按主题,而是按"使用状态"分类
- 适合:项目驱动、注重行动的人
- **CODE**Capture, Organize, Distill, Express):知识生命周期
- 核心:从收集到输出的完整流程
- 适合:内容创作者、学习型人才
- **选择建议**:没有最佳,只有最适合;可混合使用
- 我的选择:CODE框架 + Zettelkasten 链接 + PARA 分类
**第三部分:我的系统演变(3个阶段)(300字)**
- **第1阶段(混沌期,2019-2020**
- 工具:微信收藏 + Evernote + 书签
- 问题:信息散落,从未整理,基本不用
- 产出:0
- **第2阶段(工具迷恋,2021)**:
- 尝试:Roam Research(双链笔记)、Notion(数据库)、Obsidian(本地Markdown
- 问题:花大量时间折腾工具,而不是用系统
- 产出:少量笔记,但没形成知识网络
- **第3阶段(方法驱动,2022-2023**
- 选择:Notion(数据库+协作)+ 本地Markdown备份
- 方法论:CODE流程 + 原子笔记 + 强制链接
- 成果:整理笔记1500+条,链接1000+,支撑输出文章40+篇
- 关键:从"怎么建"转向"怎么用"
**第四部分:我的当前系统设计(400字)**
- **工具栈**
- 主库:Notion(数据库+关联视图+协作)
- 优点:全平台、强查询、美观
- 缺点:网络依赖、长期成本
- 本地备份:ObsidianMarkdown + 双链)
- 优点:数据自主、快、强链接
- 缺点:移动端弱、协作差
- 收集箱:Readwise + Instapaper + 微信收藏 + 书签
- AI辅助:使用LLM自动摘要、标签、提取关键点
- **结构(按CODE**
- **Capture**(收集):
- 快速收藏(Readwise高亮、微信收藏 → 每日清理到收集箱)
- 来源:文章、书、视频、对话、思考
- **Organize**(组织):
- 分类:PARAProjects/Areas/Resources/Archives
- 标签:主题(科技、自然、人文)+ 状态(草稿、完成)
- 链接:原子笔记之间建立连接(双向链接)
- **Distill**(提炼):
- 原子化:每笔记一个概念/想法/发现
- 渐进式总结:原始笔记 → 粗摘要 → 精要 → 综述
- MOC(内容地图):索引相关笔记,形成知识模块
- **Express**(输出):
- 创作模式:从PKM查询相关笔记 → 拼图式写作
- 模板:文章模板自动从PKM填充资料
- 发布后,将文章存入PKM作为新笔记
- **工作流**
- 每日:收集(30min + 整理(30min
- 每周:链接和MOC维护(1h
- 创作时:查询 → 连接 → 输出(PKM支撑)
**第五部分:量化结果(200字)**
- 笔记总量:1500+ 条(原子笔记)
- 笔记间链接:1200+ 个(网络密度 0.8)
- 知识领域:8个大类,50+个小类
- 时间节省:资料查找从平均30min → 5min83%
- 输出支撑:40+ 篇文章,其中80%从PKM直接提取资料
- 知识复用率:70%的文章引用已有笔记(不是每次都重研究)
- 意外收益:笔记价值随时间复利增长(旧笔记连接新笔记产生新洞见)
**第六部分:关键技巧(300字)**
1. **原子化**
- 每笔记一个概念、一个发现、一个问题
- 优点:易复用、易链接、易更新
- 例子:不要记整本书笔记,记"书的核心框架"+"3个关键观点"+"我的疑问"
2. **强制链接**
- 新笔记必须链接到至少2个已有笔记
- 链接要语义相关(不只是关键词匹配)
- 使用 Zettelkasten 的自下而上连接
- 工具:Obsidian的Graph View、Notion的Relation
3. **渐进式总结**
- 原始笔记(source原文高亮+简评)
- 一阶摘要(用自己的话)
- 二阶精要(关键点 bullet
- 三阶综合(多个笔记的整合+我的观点)
- 成果:长期使用,笔记越来越有价值
4. **MOC(内容地图)**
- 索引同一主题的所有笔记(类似目录)
- 动态更新,保持相关性
- 例子:"AI伦理" MOC 链接60+条笔记
- 创作时,先看MOC快速定位
5. **定期修剪**
- 每月审查:合并重复、删除过时、更新链接
- 保持系统健康,避免熵增
**第七部分:AI如何辅助PKM?(200字)**
- **自动摘要**:上传文章,LLM生成关键点(节省阅读时间)
- **智能标签**:自动建议标签,保持一致性
- **关联发现**:AI找语义相关笔记,建议链接
- **问答式检索**:自然语言查询("我怎么写关于Zettelkasten的文章?" → 相关笔记)
- **质量检查**:识别孤立笔记、断裂链接
- 工具:ChatGPT/Claude API + 自定义脚本
- "宇之然"AI时代,PKM是核心基础设施
**第八部分:常见问题与避坑(200字)**
- Q:工具太多,选哪个?(先用一个,习惯后再考虑迁移)
- Q:笔记分类 vs 标签?(分类是宏观,标签是微观,两者都需)
- Q:多久整理一次?(每天小整,每周大整)
- Q:隐私问题?(本地工具如Obsidian;云工具加密)
- Q:如何坚持?(绑定创作需求,系统为输出服务)
- 避坑:
- 不要过度组织(1-2小时整理1条笔记,效率低)
- 不要追求完美的分类(允许模糊)
- 不要忽视维护(熵增是必然,定期修剪)
- 不要只看不输出(PKM的价值在输出)
### 结尾(200字)
- PKM一年,我的知识从混沌到有序,从孤岛到网络
- 最大的收益:写文章不再从零开始,而是"拼图式创作"
- 知识管理是终身投资,越早开始复利越大
- "宇之然":科技沟通知识,让我更好地表达思想
- 行动:从今天开始,用Notation至少10个原子笔记
- 互动:你的知识管理实践如何?遇到的问题?
---
## 资源清单
### 数据来源(待收集)
- [ ] PKM方法论原始文献(Zettelkasten、PARA、CODE
- [ ] 知识管理工作效率研究(查找时间、复用率)
- [ ] 工具对比(Notion、Obsidian、Roam、Logseq、飞书)
- [ ] Notion API和自动化案例
- [ ] AI辅助知识管理工具(Memex、Rewind、Upabase
### 图片素材(待准备)
- [ ] 系统架构图(工具栈、数据流)
- [ ] Notion数据库截图(匿名化)
- [ ] Obsidian Graph View截图(展示连接密度)
- [ ] 工作流可视化(Capture → Organize → Distill → Express
- [ ] 量化结果图表(笔记数、链接数、时间节省)
- [ ] 渐进式总结示例(同一笔记的不同版本)
### 引用来源(待标注)
- [ ] Sönke Ahrens《How to Take Smart Notes》
- [ ] Tiago Forte《Building a Second Brain》
- [ ] Andy Matuschak & Michael Nielsen《How to Make a Networked Notebook That Explains Things》
- [ ] PARA方法论官网
- [ ] Zettelkasten.de社区
- [ ] AI知识管理工具(Mem.ai等)
---
**决策**:生产力领域刚需,受众广但竞争烈。结合个人系统演变数据+方法论+AI辅助,能做出差异化。适合知乎和小红书。建议 medium-high 优先级。
-71
View File
@@ -1,71 +0,0 @@
# 选题库管理
本目录收录所有选题卡片,按领域和优先级分类。
## 选题结构
每个选题文件格式:`YYYY-MM-DD-选题名称.md`
内容模板:
```markdown
# [选题名称]
**领域**:科技/自然/工作/人文
**形式**:观点/指南/趋势/故事
**预估字数**2000-3000
**核心观点**:(一句话说清楚)
**受众痛点**:(读者为什么关心?)
**独特角度**:(与其他人的区别)
**数据/案例**:(至少3个来源)
**预估完成时间**3-5天
**优先级**:高/中/低
**预计发布时间**YYYY-MM-DD
**状态**:待处理/研究中/写作中/已发布
```
## 选题评估得分
使用品牌手册中的"选题评估矩阵"打分:
- 受众覆盖:1-10分
- 独特性:1-10分
- 数据可得性:1-10分
- 可持续性:1-10分
- 平台契合度:1-10分
- 品牌契合度:1-10分
**总分 > 40分** → 优先执行
**30-40分** → 考虑执行
**< 30分** → 暂缓或放弃
## 当前选题列表(个人叙事版 001-010)
| 编号 | 选题名称 | 领域 | 优先级 | 得分 | 状态 |
|------|----------|------|--------|------|------|
| 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大支柱:未来工作方式、可持续生活、知识工厂、科技人文。
## 批量添加选题
新选题从以下来源获取:
- 行业趋势分析(research/trends-2026.md
- 日常观察记录
- 热点事件(冷静筛选)
- 读者问题反馈
- 竞品分析(差异化)
**每周选题会议**:每周日晚上20:00,评估新选题,确定下周发布计划。
---
**维护**:动态更新
**负责人**AI助手 + 用户协作
+39
View File
@@ -0,0 +1,39 @@
# ===== 数据库 =====
# 默认 SQLiteUSE_POSTGRES=false);生产设 true 并填 DATABASE_URL
USE_POSTGRES=false
# PG_HOST=db
# PG_PORT=5432
# PG_DATABASE=yzr_nr
# PG_USER=yzr
# PG_PASSWORD=change_me_strong
# DATABASE_URL=postgresql://yzr:yzr_password@db:5432/yzr_nr
# ===== 安全 =====
# 务必修改为随机长字符串
SECRET_KEY=change_this_to_a_long_random_secret_key
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_DAYS=7
# ===== 初始管理员 =====
DEFAULT_ADMIN_USERNAME=admin
DEFAULT_ADMIN_PASSWORD=admin123
# ===== 项目根(容器内固定为 /app =====
PROJECT_ROOT=/app
# ===== 主 LLM(按 provider 填写对应 Key =====
# provider 可选:nvidia / opencode-go / sensenova
LLM_PROVIDER=opencode-go
LLM_API_KEY=your_main_llm_key
LLM_MODEL=deepseek-v4-flash
LLM_BASE_URL=https://opencode.ai/zen/go/v1
# ===== 备用推理通道(opencode-go =====
OPENCODE_API_KEY=your_opencode_key
OPENCODE_BASE_URL=https://opencode.ai/zen/go/v1
OPENCODE_MODEL=deepseek-v4-flash
# ===== 配图生成(sensenova,可选) =====
SENSENOVA_API_KEY=
SENSENOVA_BASE_URL=https://token.sensenova.cn/v1
SENSENOVA_IMAGE_MODEL=sensenova-6.7-flash-lite
+20
View File
@@ -0,0 +1,20 @@
FROM python:3.11-slim
WORKDIR /app
# 安装依赖(先拷 requirements 享受层缓存)
COPY platform/backend/requirements.txt /app/platform/backend/requirements.txt
RUN pip install --no-cache-dir -r /app/platform/backend/requirements.txt
# 拷贝整个项目(保持 automation/ scripts/ 等相对路径)
COPY . /app
# 关键:让 initial_data.py 的 PROJECT_ROOT 指向 /app(与运行时一致)
ENV PROJECT_ROOT=/app
ENV USE_POSTGRES=false
ENV PYTHONUNBUFFERED=1
EXPOSE 8001
# 若使用 venv 可改为 ./start-platform.sh 8001
CMD ["sh", "-c", "cd /app/platform/backend && uvicorn app.main:app --host 0.0.0.0 --port 8001"]
+74
View File
@@ -0,0 +1,74 @@
# 宇之然内容创作平台 — 私有化 / 白标部署包
# YuZhiRan Content Platform — Private / White-label Deployment Package
本目录用于把「宇之然内容创作平台」作为**可私有化部署的 AI 内容中台**交付给客户(MCN、企业市场部、行业自媒体团队),
或作为白标产品转售。平台已内置白标能力:部署后管理员在「白标设置」页即可改平台名称 / Logo / 主色 / 支持邮箱。
## 交付形态
- 单容器即可运行(默认 SQLite,零外部依赖);生产建议挂 PostgreSQL(见下方 docker-compose 注释)。
- 全部为静态前端 + FastAPI 后端,无第三方 SaaS 依赖,可跑在内网 / 离线环境。
- 符合《AI 生成合成内容标识办法》:AI 稿默认注入显式 + 隐式声明,发布前必须由真人校验(系统不代发)。
## 一分钟启动(SQLite,零依赖)
```bash
# 1. 准备后端环境变量(含管理员密码、LLM Key)
cp .env.example ../platform/backend/.env
vim ../platform/backend/.env # 至少填 LLM_API_KEY / DEFAULT_ADMIN_PASSWORD
# 2. 构建并启动
docker compose up -d --build
# 3. 访问
# 前台: http://<服务器IP>:8001/
# 后台: http://<服务器IP>:8001/admin.html (默认 admin / 见 .env 中 DEFAULT_ADMIN_PASSWORD
```
## 生产部署(PostgreSQL
编辑 `docker-compose.yml`,取消 `db` 服务与 `web``USE_POSTGRES=true` / `DATABASE_URL` 的注释,
并删掉 `web``USE_POSTGRES=false` 那一行。数据库数据持久化在命名卷 `yzr_pgdata`
## 白标(转售 / 客户内网)
1. 用管理员登录后台 → 左侧「白标设置」。
2. 填入客户的平台名称、Logo URL、主色、支持邮箱 → 保存。
3. 前台导航与品牌色即时生效。无需改代码、无需重新构建镜像。
(底层读取 `system_configs` 表中 `brand_name / brand_logo / brand_primary_color / brand_support_email`
亦可直接 `UPDATE system_configs SET value='...' WHERE key='brand_name';` 批量初始化多客户。)
## 数据目录
容器内:
- 代码:`/app`
- SQLite 数据:`/app/platform/backend/yu_zhi_ran.db`
- 生成内容/图片:`/app/automation`(建议挂载卷持久化)
## 环境变量说明(platform/backend/.env
| 变量 | 说明 |
| --- | --- |
| USE_POSTGRES | true 用 PostgreSQLfalse 用 SQLite |
| PG_HOST/PG_PORT/PG_DATABASE/PG_USER/PG_PASSWORD | PostgreSQL 连接(USE_POSTGRES=true 时) |
| SECRET_KEY | JWT 签名密钥(**务必修改**) |
| DEFAULT_ADMIN_USERNAME / DEFAULT_ADMIN_PASSWORD | 初始管理员(首次启动创建) |
| LLM_API_KEY / LLM_MODEL / LLM_PROVIDER | 主 LLMnvidia / opencode-go / sensenova 等) |
| OPENCODE_API_KEY / OPENCODE_BASE_URL / OPENCODE_MODEL | 备用推理通道 |
| SENSENOVA_API_KEY | 配图生成(可选) |
## 目录结构(源码)
```
yu-zhi-ran/
├── platform/backend/ # FastAPI + SQLAlchemy 后端
├── platform/frontend/ # Vue3 CDN 前端(无需构建)
├── automation/ # 选题/案例/生成内容数据
├── scripts/ # 采集 / 创作 / 发布 脚本
├── tests/ # 集成测试
└── deploy/ # 本部署包
```
## 许可与交付建议
三种对外合作模式(详见官网 landing.html 的「合作模式 / 定价」区):
1. **免费试用**:私有化试用版(功能完整、限文章数)或预约 1v1 演示,零成本获客钩子。
2. **私有化部署(主力)**:每个客户一套独立部署(数据隔离),支持品牌白标。
- 建议定价:买断(¥29,800 起)+ 年运维/更新订阅(¥6,000/年起);或按实例数 / 坐席授权。
3. **源码售卖(高利润)**:完整源代码一次性授权 + 年订阅技术支持,可二次开发。
- AI 内容合规责任在内容发布方(客户),平台仅提供声明注入与发布前人工校验闸门(不代发)。
+39
View File
@@ -0,0 +1,39 @@
services:
web:
build:
context: ..
dockerfile: deploy/Dockerfile
container_name: yzr-platform
ports:
- "8001:8001"
environment:
# 默认 SQLite(零依赖)。生产请改用下方 PostgreSQL 配置:
- USE_POSTGRES=false
# - USE_POSTGRES=true
# - DATABASE_URL=postgresql://yzr:yzr_password@db:5432/yzr_nr
- PROJECT_ROOT=/app
- PYTHONUNBUFFERED=1
volumes:
# 持久化生成内容与数据
- app_automation:/app/automation
- app_content:/app/content
# 挂载后端 .env(含管理员密码、LLM Key
- ./backend.env:/app/platform/backend/.env:ro
restart: unless-stopped
# 生产用 PostgreSQL(取消注释并启用上方 USE_POSTGRES=true / DATABASE_URL
# db:
# image: postgres:16-alpine
# container_name: yzr-db
# environment:
# - POSTGRES_USER=yzr
# - POSTGRES_PASSWORD=yzr_password
# - POSTGRES_DB=yzr_nr
# volumes:
# - yzr_pgdata:/var/lib/postgresql/data
# restart: unless-stopped
volumes:
app_automation:
app_content:
yzr_pgdata:
+86
View File
@@ -0,0 +1,86 @@
# 宇之然 · 商业模式与营销方案(2026-07)
> 目标:把产品从"自用内部工具"升级为"可对外售卖的 AI 内容中台",并在官网首页(landing.html)展示三种商业模式。
---
## 一、项目功能梳理(能力盘点)
| 模块 | 能力 | 商业价值 |
|------|------|----------|
| 选题 topics | 热点采集 → 选题 → 审查 → 待发布状态机 | 解决"不知道写什么" |
| 内容工厂 factory / studio | 三平台差异化生成(知乎深度 / 公众号叙事 / 小红书干货) | 解决"写不出 / 质量低" |
| 文章管理 articles | 编辑 / AI 味检测 / 合规闸门 / 发布记录 | 解决"AI 味重 + 合规风险" |
| AI 味检测 ai_slop | 30+ 套话规则 + 软硬问题分级 | 差异化卖点(合规) |
| 搜索 / GEO metrics + search_rankings | Bing + AI 引用追踪、GEO 就绪度 | 解决"写了没流量" |
| 外推 / 素材 / 日历 / 任务 | 全流程运营闭环 | 一体化(vs 多工具拼凑) |
| 白标 branding + 私有化 deploy | 改品牌名 / Logo / 主色,Docker 一键部署 | **可直接当产品卖给客户** |
核心差异化:**三平台独立成稿 + AI 味检测 + GEO 获客 + 合规闸门**,且是"可私有化 / 可白标"的交付形态——这正是 to-B 卖点。
---
## 二、市场需求(2026
- **内容获客刚需**:公众号 / 知乎 / 小红书仍是中小团队低成本获客主阵地,但"日更 + 多平台 + 质量"人力成本高。
- **GEO 新窗口**ChatGPT / DeepSeek / Perplexity 等 AI 搜索兴起,品牌"被 AI 引用"成新需求,竞品少、溢价高。
- **合规强约束**:《AI 生成合成内容标识办法》要求 AI 稿真人校验后发布——产品内"不代发、人工闸门"正好切中。
- **私有化偏好上升**:数据不出域、品牌自主,是 MCN / 企业市场部 / 政务行业的硬需求(也是 SaaS 红海里能避开的价格带)。
---
## 三、三种商业模式可行性
| 模式 | 可行性 | 现状 | 要做的事 |
|------|--------|------|----------|
| **私有化部署** | ✅ 最成熟 | deploy/ Docker 包 + 白标已就绪 | 主要做营销包装 + 报价,几乎零研发 |
| **源码售卖** | ✅ 容易 | 整套代码已在 | 加 License / 授权条款 + 定价页(买断 / 年订阅 / 实例数) |
| **免费试用** | ⚠️ 需补研发 | 无自注册(仅 admin 建用户) | 以"私有化试用版 / 预约演示 / 限时体验"作为获客钩子,避开纯 SaaS 基础设施投入 |
**策略结论(沿用第二章)**:纯公有云 SaaS 是红海。真正壁垒是"可私有化 + 可白标 + 合规",因此以 **私有化部署 + 源码售卖为主力,免费试用作获客钩子(而非独立 SaaS)** 最稳。
---
## 四、首页营销展示方案(landing.html 改造)
官网营销首页 `landing.html` 已存在(Hero / 功能 / 流程 / 定价 / FAQ / CTA),但定价仍停留在"SaaS 月订阅"框架。改造内容:
### 4.1 定价区(#pricing)重构为三段式
1. **免费试用**(引流):私有化试用版 / 预约演示,限文章数,零成本体验完整流程。CTA → 申请试用(锚点 #contact)。
2. **私有化部署**(主力,推荐):买断 + 年运维订阅,数据不出域 + 白标 + 合规闸门。CTA → 获取报价(#contact)。
3. **源码售卖**(高利润):一次性授权 + 年订阅支持,可二次开发。CTA → 联系销售(#contact)。
### 4.2 FAQ 增补
- 什么是私有化部署?数据在哪里?
- 可以买源码自己改吗?授权方式?
- 免费试用怎么申请?
- 内容合规怎么保证?(人工闸门 / 不代发)
- 数据安全 / 多租户隔离。
### 4.3 新增"开始合作"联系区(#contact
- 联系信息(邮箱 / 微信 / 商务对接)
- 轻量线索表单(姓名 / 公司 / 需求 / 联系方式)→ mailto 提交(无后端依赖,低风险)
- 定价 CTA 统一锚定 #contact
### 4.4 其它文案对齐
- 导航"免费使用"→"免费试用";新增"联系销售"
- Hero CTA 对齐
- deploy/README 定价说明同步为三模式
---
## 五、落地优先级
1. **立即(本次实现)**landing.html 定价三段式 + FAQ + #contact 表单 + 文案对齐。
2. **可选增强**:真实线索入库(/api/leads + Lead 表)、私有化试用版自动下发、报价单生成。
3. **远期**:若决定做纯 SaaS 免费试用,再补"开放注册 + 额度"产品能力(当前非必需)。
---
## 六、定价建议(示例,可调整)
| 模式 | 参考报价 | 备注 |
|------|----------|------|
| 免费试用 | ¥0 | 私有化试用版 / 预约演示,限文章数 |
| 私有化部署 | ¥29,800 起(买断)+ 年运维订阅 ¥6,000/年起 | 按实例 / 坐席可选 |
| 源码售卖 | ¥XX,XXX(一次性授权)+ 年订阅支持 | 可二次开发,含部署指导 |
+206
View File
@@ -0,0 +1,206 @@
import sys
from pathlib import Path
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Depends, Body
from pydantic import BaseModel
from ..database import get_db
from ..models import User, Topic, Article
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/ai-slop", tags=["ai-slop"])
PROJECT_ROOT = Path(__file__).resolve().parents[4]
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
if not SCRIPTS_DIR.exists():
SCRIPTS_DIR = PROJECT_ROOT.parent / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
check_article = None
polish_with_llm = None
clean_html_content = None
strip_ai_preface = None
strip_thinking_html = None
try:
from compliance_checker import check_article
except Exception:
check_article = None
try:
from compliance_optimizer import polish_with_llm
except Exception:
polish_with_llm = None
try:
from content_cleaner import clean_html_content, strip_ai_preface, strip_thinking_html
except Exception:
clean_html_content = None
strip_ai_preface = None
strip_thinking_html = None
HARD_ISSUE_TYPES = ("敏感词", "法律法规", "平台规则", "品牌规范", "资源合规")
class IssueItem(BaseModel):
type: str
category: str = ""
detail: str = ""
suggestion: str = ""
severity: str = "medium"
class PlatformReport(BaseModel):
platform: str
score: int
passed: bool
issues: List[IssueItem]
html_preview: str = ""
class ReportResponse(BaseModel):
topic_id: str
platforms: List[PlatformReport]
class PurifyRequest(BaseModel):
topic_id: str
platform: str
class PurifyResponse(BaseModel):
ok: bool
platform: str
score_before: Optional[int] = None
score_after: Optional[int] = None
issues_before: List[IssueItem] = []
issues_after: List[IssueItem] = []
preview: str = ""
message: str = ""
def _normalize_issues(issues: list) -> List[IssueItem]:
result = []
for i in issues or []:
itype = i.get("type", "")
severity = "high" if itype in HARD_ISSUE_TYPES else "medium"
detail = i.get("detail") or i.get("suggestion") or i.get("word") or i.get("tag") or i.get("pattern") or ""
result.append(IssueItem(
type=itype,
category=i.get("category", ""),
detail=detail,
suggestion=i.get("suggestion", ""),
severity=severity,
))
return result
def _extract_title(html: str) -> str:
import re
m = re.search(r"<title>\s*([^<]+?)\s*</title>", html, re.IGNORECASE)
if not m:
m = re.search(r"<h1[^>]*>\s*([^<]+?)\s*</h1>", html, re.IGNORECASE)
return m.group(1).strip() if m else ""
def _extract_content(html: str) -> str:
import re
text = re.sub(r"<style.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r"<script.*?</script>", "", text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
return re.sub(r"\s+", " ", text).strip()
def _load_topic_data(db, topic_id: str) -> dict:
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
return {}
return {
"topic": {
"title": getattr(topic, "title", "") or "",
"field": getattr(topic, "field", "") or "",
"core_concept": getattr(topic, "core_concept", "") or "",
}
}
@router.get("/report", response_model=ReportResponse)
def get_report(topic_id: str, current_user: User = Depends(get_current_user), db=Depends(get_db)):
if check_article is None:
raise HTTPException(status_code=503, detail="合规检测模块不可用")
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
of = org_filter(current_user, Topic)
if of is not True and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
topic_data = _load_topic_data(db, topic_id)
from db_helper import get_articles_by_topic
articles = get_articles_by_topic(topic_id)
platforms = []
for art in articles:
platform = art.get("platform")
html = art.get("html_content") or ""
if not html:
continue
res = check_article(html, platform, topic_data=topic_data)
platforms.append(PlatformReport(
platform=platform,
score=res.get("score", 0),
passed=res.get("passed", False),
issues=_normalize_issues(res.get("issues", [])),
html_preview=html[:600],
))
return ReportResponse(topic_id=topic_id, platforms=platforms)
@router.post("/purify", response_model=PurifyResponse)
def purify(req: PurifyRequest, current_user: User = Depends(get_current_user), db=Depends(get_db)):
if check_article is None:
raise HTTPException(status_code=503, detail="合规检测模块不可用")
topic = db.query(Topic).filter(Topic.id == req.topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
of = org_filter(current_user, Topic)
if of is not True and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
from db_helper import get_articles_by_topic, save_article
articles = get_articles_by_topic(req.topic_id)
target = next((a for a in articles if a.get("platform") == req.platform), None)
if not target or not target.get("html_content"):
raise HTTPException(status_code=404, detail=f"未找到 {req.platform} 平台的文章")
html = target["html_content"]
topic_data = _load_topic_data(db, req.topic_id)
before = check_article(html, req.platform, topic_data=topic_data)
issues_before = _normalize_issues(before.get("issues", []))
raw_issues = before.get("issues", [])
polished_html, log_msg = (html, None)
if polish_with_llm is not None:
polished_html, log_msg = polish_with_llm(html, req.platform, remaining_issues=raw_issues)
cleaned = polished_html
if clean_html_content is not None:
cleaned = clean_html_content(cleaned)
if strip_ai_preface is not None:
cleaned = strip_ai_preface(cleaned)
if strip_thinking_html is not None:
cleaned = strip_thinking_html(cleaned)
title = _extract_title(cleaned)
content = _extract_content(cleaned)
save_article(req.topic_id, req.platform, cleaned, title=title, content=content, db=db)
after = check_article(cleaned, req.platform, topic_data=topic_data)
issues_after = _normalize_issues(after.get("issues", []))
message = "净化完成" + (f"{log_msg}" if log_msg else "(仅执行清洗,未调用 LLM")
return PurifyResponse(
ok=True,
platform=req.platform,
score_before=before.get("score"),
score_after=after.get("score"),
issues_before=issues_before,
issues_after=issues_after,
preview=cleaned[:600],
message=message,
)
+80
View File
@@ -0,0 +1,80 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Optional
from pydantic import BaseModel
from ..database import get_db
from ..models import SystemConfig
from .auth import get_current_admin
router = APIRouter(prefix="/api/branding", tags=["branding"])
BRAND_KEYS = {
"brand_name": "宇之然内容创作平台",
"brand_logo": "",
"brand_primary_color": "#2563eb",
"brand_support_email": "",
}
PUBLIC_RESP_DESC = "白标实例公开信息(供前端运行时应用品牌)"
class BrandingResponse(BaseModel):
brand_name: str
brand_logo: str
brand_primary_color: str
brand_support_email: str
version: str
class BrandingUpdate(BaseModel):
brand_name: Optional[str] = None
brand_logo: Optional[str] = None
brand_primary_color: Optional[str] = None
brand_support_email: Optional[str] = None
def _get(db: Session, key: str) -> str:
cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first()
if not cfg or cfg.value is None:
return BRAND_KEYS.get(key, "")
return cfg.value
@router.get("", response_model=BrandingResponse)
def get_branding(db: Session = Depends(get_db)):
"""公开接口:返回当前实例的白标配置(无需登录),用于前端运行时渲染品牌"""
try:
from ..main import app
version = getattr(app, "version", "0.1.0")
except Exception:
version = "0.1.0"
return BrandingResponse(
brand_name=_get(db, "brand_name"),
brand_logo=_get(db, "brand_logo"),
brand_primary_color=_get(db, "brand_primary_color"),
brand_support_email=_get(db, "brand_support_email"),
version=version,
)
@router.put("", response_model=BrandingResponse)
def update_branding(
payload: BrandingUpdate,
db: Session = Depends(get_db),
admin_user=Depends(get_current_admin),
):
"""管理员接口:更新白标配置(品牌名/Logo/主色/支持邮箱)"""
data = payload.model_dump(exclude_unset=True)
for key, value in data.items():
if key not in BRAND_KEYS:
continue
if value is None:
continue
cfg = db.query(SystemConfig).filter(SystemConfig.key == key).first()
if cfg:
cfg.value = str(value)
else:
db.add(SystemConfig(key=key, value=str(value), description="白标配置"))
db.commit()
return get_branding(db)
+56 -1
View File
@@ -1,4 +1,10 @@
import os
import sys
import subprocess
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import Optional
@@ -6,10 +12,18 @@ 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
from .auth import get_current_user, get_current_admin, org_filter
router = APIRouter(prefix="/api/external", tags=["external_promotion"])
PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent
SEO_AUDITOR_PATH = PROJECT_ROOT / "scripts" / "seo_auditor.py"
class AuditRunRequest(BaseModel):
product_id: Optional[str] = None
url: Optional[str] = None
def _org_filtered_query(db, model, current_user):
query = db.query(model)
@@ -467,3 +481,44 @@ def external_overview(
"type_breakdown": type_breakdown,
}
}
# ===== Run SEO Audit (delegate to scripts/seo_auditor.py) =====
@router.post("/audit/run")
def run_audit(
body: AuditRunRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin),
):
cmd = [sys.executable, str(SEO_AUDITOR_PATH)]
if body.product_id:
cmd += ["--product-id", str(body.product_id)]
elif body.url:
cmd += ["--url", body.url]
else:
raise HTTPException(status_code=400, detail="product_id 或 url 至少提供一个")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=408, detail="SEO 审计超时")
except Exception as e:
raise HTTPException(status_code=500, detail=f"审计执行失败: {str(e)}")
if result.returncode != 0:
raise HTTPException(
status_code=502,
detail={
"message": "SEO 审计脚本返回错误",
"stderr": result.stderr,
"stdout": result.stdout,
},
)
return {
"ok": True,
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
}
+104 -14
View File
@@ -2,13 +2,13 @@
"""发布管理 API"""
from fastapi import APIRouter, HTTPException, Depends, Request
from pydantic import BaseModel
from datetime import datetime, timezone, timedelta
from datetime import datetime, timezone, timedelta, date
from typing import Optional, List
from ..database import get_db
from ..models import Topic, PublishRecord, User
from sqlalchemy.orm import Session
from .auth import get_current_admin, org_filter
from .auth import get_current_user, org_filter
from ..core.audit_logger import audit_log
router = APIRouter(prefix="/api/publishing", tags=["publishing"])
@@ -40,9 +40,9 @@ async def create_publish_record(
req: MultiPublishRequest,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin)
current_user: User = Depends(get_current_user)
):
"""多平台发布选题(管理员"""
"""多平台发布选题(运营/管理员,按组织隔离"""
try:
q = db.query(Topic).filter(Topic.id == req.topic_id)
of = org_filter(current_user, Topic)
@@ -66,17 +66,17 @@ async def create_publish_record(
record = PublishRecord(
topic_id=req.topic_id,
platform=platform,
action='publish',
status='success',
action='stage',
status='pending_manual',
operator=operator,
description=f"选题 {req.topic_id} 发布到 {PLATFORM_LABELS.get(platform, platform)}",
description=f"选题 {req.topic_id} 已生成 {PLATFORM_LABELS.get(platform, platform)} 发布稿,待人工校验后发布",
org_id=topic.org_id
)
db.add(record)
results.append(PublishResult(
platform=platform,
platform_label=PLATFORM_LABELS.get(platform, platform),
status='success'
status='pending_manual'
))
except Exception as e:
results.append(PublishResult(
@@ -86,16 +86,17 @@ async def create_publish_record(
error_msg=str(e)
))
topic.status = '已发布'
# 诚实状态流转:本项目无真实自动发布器(且按《AI生成内容标识办法》AI 稿需真人把关),
# 故仅将选题置为「待人工发布」,待运营在对应平台校验声明后手动发布。
topic.status = 'pending_publish'
_now = datetime.now(timezone(timedelta(hours=8)))
topic.updated_at = _now
topic.published_at = _now.date()
db.commit()
db.expire_all()
db.refresh(topic)
audit_log(
action="publish",
action="publish_stage",
user=current_user,
resource_type="topic",
resource_id=req.topic_id,
@@ -105,15 +106,104 @@ async def create_publish_record(
db=db
)
success_count = sum(1 for r in results if r.status == 'success')
staged = sum(1 for r in results if r.status == 'pending_manual')
return MultiPublishResponse(
ok=success_count > 0,
ok=staged == len(results),
topic_id=req.topic_id,
results=results,
message=f"选题 {req.topic_id} 发布完成({success_count}/{len(results)} 平台成功)"
message=(
f"选题 {req.topic_id} 已生成 {staged}/{len(results)} 个平台的发布稿并置为「待人工发布」。"
f"AI 生成内容须由真人校验声明后手动发布,系统不代发。"
)
)
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/records")
def list_publish_records(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""列出发布记录(按组织隔离),用于运营查看「待人工发布 / 已发布」状态"""
q = db.query(PublishRecord)
of = org_filter(current_user, PublishRecord)
if of is not True:
q = q.filter(of)
recs = q.order_by(PublishRecord.created_at.desc()).all()
result = []
for r in recs:
topic = db.query(Topic).filter(Topic.id == r.topic_id).first()
result.append({
"id": r.id,
"topic_id": r.topic_id,
"topic_title": topic.title if topic else None,
"platform": r.platform,
"action": r.action,
"status": r.status,
"operator": r.operator,
"description": r.description,
"url": r.url,
"created_at": r.created_at.isoformat() if r.created_at else None,
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
})
return {"records": result}
class MarkPublishedResponse(BaseModel):
ok: bool
topic_id: str
message: str
@router.post("/{topic_id}/mark-published", response_model=MarkPublishedResponse)
def mark_published(
topic_id: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""运营在对应平台手动发布后,回填「已发布」闭环状态"""
q = db.query(Topic).filter(Topic.id == topic_id)
of = org_filter(current_user, Topic)
if of is not True:
q = q.filter(of)
topic = q.first()
if not topic:
raise HTTPException(status_code=404, detail=f"选题 {topic_id} 不存在")
if topic.status != 'pending_publish':
raise HTTPException(status_code=400, detail=f"选题 {topic_id} 当前状态为「{topic.status}」,无需标记发布")
_now = datetime.now(timezone(timedelta(hours=8)))
topic.status = 'published'
topic.published_at = date.today()
topic.updated_at = _now
recs = db.query(PublishRecord).filter(
PublishRecord.topic_id == topic_id,
PublishRecord.status == 'pending_manual'
)
for r in recs:
r.status = 'published'
r.updated_at = _now
db.commit()
audit_log(
action="publish_confirm",
user=current_user,
resource_type="topic",
resource_id=topic_id,
details={"operator": current_user.username},
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent", ""),
db=db
)
return MarkPublishedResponse(
ok=True,
topic_id=topic_id,
message=f"选题 {topic_id} 已标记为「已发布」,发布记录同步更新。"
)
+60 -1
View File
@@ -31,6 +31,7 @@ def list_topics(
status: Optional[str] = None,
today: Optional[bool] = None,
tag: Optional[str] = None,
series: Optional[str] = None,
search: Optional[str] = None,
sort_by: str = Query("priority_score", enum=["priority_score", "created_at", "title", "updated_at"]),
order: str = Query("desc", enum=["asc", "desc"]),
@@ -54,6 +55,8 @@ def list_topics(
query = query.filter(func.date(Topic.created_at) == date.today())
if tag:
query = query.filter(Topic.tags.contains([tag]))
if series:
query = query.filter(Topic.series == series)
if search:
query = query.filter(Topic.title.contains(search))
@@ -63,7 +66,63 @@ def list_topics(
else:
query = query.order_by(sort_col.asc())
return query.offset(offset).limit(limit).all()
topics = query.offset(offset).limit(limit).all()
# Enrich with article count and previews
from ..models import Article
topic_ids = [t.id for t in topics]
if topic_ids:
article_rows = (
db.query(Article.topic_id, Article.platform, Article.title, Article.word_count, Article.status)
.filter(Article.topic_id.in_(topic_ids))
.all()
)
articles_by_topic: Dict[str, list] = {}
for ar in article_rows:
articles_by_topic.setdefault(ar.topic_id, []).append({
"platform": ar.platform,
"title": ar.title,
"word_count": ar.word_count,
"status": ar.status,
})
else:
articles_by_topic = {}
result = []
for t in topics:
t_dict = {
"id": t.id,
"field_id": t.field_id,
"field_name": t.field_name,
"org_id": t.org_id,
"title": t.title,
"format": t.format,
"core_concept": t.core_concept,
"audience_pain": t.audience_pain,
"unique_angle": t.unique_angle,
"priority": t.priority,
"priority_score": t.priority_score,
"total_score": t.total_score,
"status": t.status,
"tags": t.tags or [],
"custom_data": t.custom_data or {},
"scoring_data": t.scoring_data or {},
"lock_by": t.lock_by,
"lock_at": t.lock_at,
"series": t.series,
"created_at": t.created_at,
"updated_at": t.updated_at,
"generated_at": t.generated_at,
"reviewed_at": t.reviewed_at,
"ready_at": t.ready_at,
"published_at": t.published_at,
"compliance_score": t.compliance_score,
"platform_urls": t.platform_urls or {},
"cases": [],
"article_count": len(articles_by_topic.get(t.id, [])),
"articles": articles_by_topic.get(t.id, []),
}
result.append(t_dict)
return result
@router.get("/stats")
+2 -1
View File
@@ -84,8 +84,9 @@ def run_creator_blocking(topic_id: str = None, timeout: int = 1800):
existing = db.query(Article).filter(Article.id == article_id).first()
if existing:
existing.html_content = html
existing.word_count = len(html)
else:
db.add(Article(id=article_id, topic_id=topic_id, platform=platform_dir, file_path=f"db:{article_id}", html_content=html, status="draft"))
db.add(Article(id=article_id, topic_id=topic_id, platform=platform_dir, file_path=f"db:{article_id}", html_content=html, word_count=len(html), status="draft"))
hf.unlink()
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
pdir = dd / platform_dir
+1 -2
View File
@@ -25,8 +25,7 @@ def _get_cmd(topic_ids: List[str] = None):
cmd = [str(venv_python), str(script_path)] if venv_python.exists() else ["python3", str(script_path)]
if topic_ids:
cmd.extend(["--topic-ids", ','.join(topic_ids)])
else:
cmd.append("--today-only")
# 不再使用 --today-only,避免三平台串行撰写时小红书文章错过审查窗口
return cmd
def run_optimizer(topic_ids: List[str] = None):
+54 -4
View File
@@ -58,6 +58,7 @@ MODULES = {
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*"},
"scheduled_rank_tracker": {"name": "🔍 搜索排名追踪", "cron": "07:00"},
"scheduled_geo_tracker": {"name": "🌐 AI 搜索引用追踪", "cron": "07:30"},
"scheduled_shortvideo": {"name": "🎬 短视频拆条", "cron": "08:00"},
}
LOG_FILE_MAP = {
@@ -71,6 +72,7 @@ LOG_FILE_MAP = {
"scheduled_task_monitor": "task_monitor",
"scheduled_rank_tracker": "rank_tracker",
"scheduled_geo_tracker": "geo_tracker",
"scheduled_shortvideo": "shortvideo",
}
def _log_to_file(module_id: str, status: str, message: str = None, error_trace: str = None):
@@ -188,6 +190,7 @@ class TaskScheduler:
("scheduled_reset_search_usage", self._run_reset_search_usage, "搜索用量重置"),
("scheduled_rank_tracker", self._run_rank_tracker, "搜索排名追踪"),
("scheduled_geo_tracker", self._run_geo_tracker, "AI 搜索引用追踪"),
("scheduled_shortvideo", self._run_shortvideo, "短视频拆条"),
]
for module_id, fn, name in MODULE_JOBS:
@@ -418,10 +421,11 @@ class TaskScheduler:
count = 0
for topic in topics:
platform_urls = topic.platform_urls or {}
zhihu_url = platform_urls.get("zhihu", "")
if not zhihu_url:
for platform, url in platform_urls.items():
if not url:
continue
m = re.search(r'zhuanlan\.zhihu\.com/p/(\d+)', zhihu_url)
if platform == "zhihu":
m = re.search(r'zhuanlan\.zhihu\.com/p/(\d+)', url)
if not m:
continue
post_id = m.group(1)
@@ -442,7 +446,7 @@ class TaskScheduler:
"comments": raw.get("comment_count", raw.get("comments_count", 0)),
"shares": raw.get("share_count", 0),
"last_fetched": datetime.now(),
"publish_url": zhihu_url,
"publish_url": url,
"data_snapshot": raw,
}
if existing:
@@ -453,6 +457,12 @@ class TaskScheduler:
count += 1
except Exception:
continue
elif platform in ("wechat_mp", "xiaohongshu"):
logger.info("平台 %s 无公开指标 API,跳过", platform)
continue
else:
logger.info("平台 %s 暂无指标同步支持,跳过", platform)
continue
if count:
db.commit()
logger.info("[Scheduled] Metrics sync completed: synced %d zhihu articles", count)
@@ -661,6 +671,46 @@ class TaskScheduler:
started_at=started, finished_at=datetime.now(timezone.utc))
logger.exception("[GeoTracker] AI 搜索引用追踪失败: %s", e)
def _run_shortvideo(self):
"""每日将前一日「待发布」选题的长文拆条为短视频脚本"""
started = datetime.now(timezone.utc)
log_id = _log_task("scheduled_shortvideo", "running", started_at=started)
try:
from ..database import SessionLocal
from ..models import Topic
db = SessionLocal()
try:
topics = db.query(Topic).filter(
Topic.status.in_(["ready", "待发布", "pending_publish"])
).order_by(Topic.ready_at.desc().nullslast()).limit(3).all()
finally:
db.close()
if not topics:
_log_task("scheduled_shortvideo", "success", log_id=log_id,
message="无待拆条选题", started_at=started, finished_at=datetime.now(timezone.utc))
return
import subprocess
total = 0
for t in topics:
res = subprocess.run(
[sys.executable, str(PROJECT_ROOT / "scripts" / "shortvideo.py"), "--topic-id", t.id, "--count", "3"],
capture_output=True, text=True, timeout=600
)
if res.returncode == 0:
total += 1
_log_task("scheduled_shortvideo", "success", log_id=log_id,
message=f"拆条完成: {total}/{len(topics)} 选题",
result_data={"topics": len(topics), "done": total},
started_at=started, finished_at=datetime.now(timezone.utc))
logger.info("[ShortVideo] 拆条完成: %d/%d", total, len(topics))
except Exception as e:
import traceback
_log_task("scheduled_shortvideo", "failed", log_id=log_id,
message=str(e),
error_trace=traceback.format_exc(),
started_at=started, finished_at=datetime.now(timezone.utc))
logger.exception("[ShortVideo] 拆条失败: %s", e)
def get_jobs(self):
"""返回当前所有定时任务的状态"""
jobs = []
+7
View File
@@ -129,8 +129,15 @@ def init_db():
("publish_records", "org_id", "VARCHAR DEFAULT 'default'"),
("content_metrics", "org_id", "VARCHAR DEFAULT 'default'"),
("search_rankings", "org_id", "VARCHAR DEFAULT 'default'"),
("search_rankings", "ai_search_engine", "VARCHAR"),
("search_rankings", "citation_snippet", "TEXT"),
("search_rankings", "citation_url", "VARCHAR"),
("search_rankings", "geo_score", "INTEGER DEFAULT 0"),
("search_rankings", "content_type", "VARCHAR"),
("geo_readiness_scores", "org_id", "VARCHAR DEFAULT 'default'"),
("articles", "org_id", "VARCHAR DEFAULT 'default'"),
("content_tasks", "org_id", "VARCHAR DEFAULT 'default'"),
("topics", "series", "VARCHAR"),
]:
try:
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}"))
+62 -30
View File
@@ -76,6 +76,17 @@ def import_initial_data():
for cfg in default_system_configs:
if db.query(SystemConfig).filter(SystemConfig.key == cfg["key"]).first() is None:
db.add(SystemConfig(**cfg))
# 白标默认值(私有化部署/卖给他人时可改)
default_branding = [
{"key": "brand_name", "value": "宇之然内容创作平台", "description": "白标:平台名称"},
{"key": "brand_logo", "value": "", "description": "白标:Logo URL(留空用文字)"},
{"key": "brand_primary_color", "value": "#2563eb", "description": "白标:主色(导航/按钮)"},
{"key": "brand_support_email", "value": "", "description": "白标:支持邮箱"},
]
for b in default_branding:
if db.query(SystemConfig).filter(SystemConfig.key == b["key"]).first() is None:
db.add(SystemConfig(**b))
if db.query(SystemConfig).filter(SystemConfig.key == "review_llm_id").first() is None:
first_llm = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
if first_llm:
@@ -105,7 +116,7 @@ def import_initial_data():
"default_format": "深度分析 3000-8000字,数据驱动",
"compliance_rules": {
"max_title_len": 100,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"allowed_tags": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"forbidden_patterns": ["加微信", "私聊", "付费咨询", "点击领取"]
},
"is_active": True,
@@ -124,7 +135,7 @@ def import_initial_data():
"default_format": "个人叙事 2000-4000字,对话感",
"compliance_rules": {
"max_title_len": 32,
"allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"],
"allowed_tags": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"forbidden_patterns": ["诱导分享", "朋友圈", "转发群"]
},
"is_active": True,
@@ -143,7 +154,7 @@ def import_initial_data():
"default_format": "精炼干货 400-1000字,实用优先",
"compliance_rules": {
"max_title_len": 50,
"allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"],
"allowed_tags": ["AI", "效率", "科技", "工具", "职场", "生活", "学习方法"],
"forbidden_patterns": ["私信", "加群", "导流"]
},
"is_active": True,
@@ -163,9 +174,9 @@ def import_initial_data():
else:
# 更新已有平台配置的字数要求(迁移:2026-06 内容质量升级)
platform_updates = {
"zhihu": {"min_words": 3000, "max_words": 8000, "default_format": "深度分析 3000-8000字,数据驱动"},
"wechat": {"min_words": 2000, "max_words": 4000, "default_format": "个人叙事 2000-4000字,对话感"},
"xiaohongshu": {"min_words": 400, "max_words": 1000, "default_format": "精炼干货 400-1000字,实用优先"},
"zhihu": {"min_words": 3000, "max_words": 8000, "default_format": "深度分析 3000-8000字,数据驱动+观点交锋,用一手数据和独特视角切入"},
"wechat": {"min_words": 2000, "max_words": 4000, "default_format": "科技人文叙事 2000-4000字,个人反思+情感共鸣,记录人与技术之间的故事"},
"xiaohongshu": {"min_words": 400, "max_words": 1000, "default_format": "精炼干货 400-1000字,亲测数据+实操结果,每个结论配真实对比"},
}
for p in db.query(PlatformConfig).all():
if p.platform in platform_updates:
@@ -178,41 +189,36 @@ def import_initial_data():
if db.query(TopicField).count() == 0:
fields = [
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程工作、零工经济、职业转型", "sort_order": 1},
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具、数字助手、效率方法", "sort_order": 2},
{"name": "可持续生活", "icon": "🌿", "color": "#67c23a", "description": "环保、低碳、自然生活方式", "sort_order": 3},
{"name": "数字游民", "icon": "🌍", "color": "#409eff", "description": "旅行、地理自由、海外生活", "sort_order": 4},
{"name": "个人成长", "icon": "📚", "color": "#e6a23c", "description": "学习、技能、认知升级", "sort_order": 5},
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "科技伦理、数字生活反思", "sort_order": 6},
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具实测对比、工作流效率方法、前沿资讯解读——关注技术如何改变人的工作方式", "sort_order": 1},
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "AI伦理困境、数字生活反思、人机关系探索——科技的温度与边界", "sort_order": 2},
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程协作实践、AI时代职业转型、一人企业模式——未来不是等来的", "sort_order": 3},
]
for f in fields:
db.add(TopicField(**f))
db.commit()
print("✅ 插入默认领域配置")
print("✅ 插入默认领域配置(聚焦AI×人文×未来工作)")
if db.query(CollectorCategory).count() == 0:
default_cats = [
{"name": "循环消费", "search_query": "以旧换新 二手交易 闲置 循环 2026", "description": "以旧换新/二手交易/租赁经济 | 2025年二手交易额1.69万亿", "sort_order": 1, "is_active": True},
{"name": "低碳出行", "search_query": "新能源车 骑行 绿色通勤 低碳出行 2026", "description": "新能源车/骑行/绿色出行 | 年产销破1000万辆", "sort_order": 2, "is_active": True},
{"name": "干净饮食", "search_query": "干净饮食 有机食品 植物基 本地食材 2026", "description": "有机食品/植物基/本地食材 | 有机食品1247亿", "sort_order": 3, "is_active": True},
{"name": "零浪费生活", "search_query": "零浪费 自带杯 极简生活 可持续时尚 2026", "description": "自带杯/极简/可持续时尚 | 自带杯笔记277万篇", "sort_order": 4, "is_active": True},
{"name": "绿色家电与节能", "search_query": "绿色家电 一级能效 以旧换新 节能 2026", "description": "一级能效/国补政策 | 一级能效占比90%+", "sort_order": 5, "is_active": True},
{"name": "碳普惠", "search_query": "碳账户 碳普惠 个人碳减排 蚂蚁森林 2026", "description": "碳账户/碳普惠/个人减排 | 武汉200万碳账户", "sort_order": 6, "is_active": True},
{"name": "环保科技产品", "search_query": "环保科技 绿色产品 可持续材料 2026", "description": "可持续材料/绿色产品 | 购买占比超32%", "sort_order": 7, "is_active": True},
{"name": "AI与效率", "search_query": "AI工具 人工智能 效率提升 2026", "description": "AI工具/效率方法/数字助手 | 2026年AI深度融入消费与生活", "sort_order": 8, "is_active": True},
{"name": "AI前沿资讯", "search_query": "AI 人工智能 大模型 2026 前沿 突破 论文解读", "description": "AI行业动态、大模型发布、技术突破 | 2026年AI全面嵌入产业", "sort_order": 1, "is_active": True},
{"name": "AI工具实测", "search_query": "AI工具 效率提升 工作流 prompt教程 Cursor Copilot 2026", "description": "AI工具评测、效率工作流、实操指南 | 2026年AI工具爆发", "sort_order": 2, "is_active": True},
{"name": "AI与职场", "search_query": "AI 裁员 职业转型 AI技能 远程工作 一人企业 2026", "description": "AI对就业影响、职业转型、技能升级 | 2026年AI重塑就业结构", "sort_order": 3, "is_active": True},
{"name": "AI生活", "search_query": "AI陪伴 AI心理咨询 生活助手 AI写作 智能体 2026", "description": "AI心理咨询/生活搭子/AI人格化 | 商业笔记互动增长263%", "sort_order": 4, "is_active": True},
{"name": "科技人文", "search_query": "AI伦理 数字生活 科技反思 人机关系 数据隐私 2026", "description": "AI伦理/数字生活反思/科技温度 | AI从工具到共生", "sort_order": 5, "is_active": True},
]
for cd in default_cats:
existing = db.query(CollectorCategory).filter(CollectorCategory.name == cd["name"]).first()
if not existing:
db.add(CollectorCategory(**cd))
db.commit()
print("✅ 插入默认采集类别和信息源")
print("✅ 插入默认采集类别(聚焦AI×科技人文)")
db.commit()
# 补充缺失的类别和信息源(对已有数据库的迁移)
# 补充缺失的采集源(对已有数据库的迁移)
for sd in [
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 人工智能 效率提升 2026", "credibility": "medium", "focus": "AI与效率", "sort_order": 99, "is_active": True},
{"name": "AI前沿搜索", "source_type": "web_search", "query": "AI 人工智能 大模型 前沿 2026", "credibility": "medium", "focus": "AI前沿资讯", "sort_order": 1, "is_active": True},
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 效率提升 AI工作流 2026", "credibility": "medium", "focus": "AI工具实测", "sort_order": 2, "is_active": True},
]:
if not db.query(CollectorSource).filter(CollectorSource.name == sd["name"]).first():
db.add(CollectorSource(**sd))
@@ -299,21 +305,47 @@ def import_initial_data():
db.commit()
print("✅ 插入默认角色")
# 初始化默认菜单(与 uni-nav.js 对齐)
if db.query(Menu).count() == 0:
# 归一化首页菜单路径,避免 index.html 与 /index.html 产生重复入口
idx_plain = db.query(Menu).filter(Menu.path == "index.html").first()
idx_slash = db.query(Menu).filter(Menu.path == "/index.html").first()
if idx_plain and not idx_slash:
idx_plain.path = "/index.html"
db.commit()
elif idx_plain and idx_slash:
db.delete(idx_plain)
db.commit()
# 初始化默认菜单:幂等按 path 对齐(已有时更新排序/角色,缺失时插入)。
# 运行时会用 DB 菜单覆盖 uni-nav.js 的 fallback,因此这里必须保证文章管理/内容工厂/白标设置等入口存在。
default_menus = [
{"name": "仪表盘", "path": "/", "icon": "IconHome", "sort_order": 0, "roles": ["admin", "editor"]},
{"name": "选题", "path": "topics.html", "icon": "IconTopic", "sort_order": 1, "roles": ["admin", "editor"]},
{"name": "工作台", "path": "/index.html", "icon": "IconHome", "sort_order": 0, "roles": ["admin", "editor"]},
{"name": "创作工作台", "path": "studio.html", "icon": "IconEdit", "sort_order": 1, "roles": ["admin", "editor"]},
{"name": "数据", "path": "metrics.html", "icon": "IconData", "sort_order": 2, "roles": ["admin", "editor"]},
{"name": "日历", "path": "calendar.html", "icon": "IconCalendar", "sort_order": 3, "roles": ["admin", "editor"]},
{"name": "素材", "path": "assets.html", "icon": "IconFolder", "sort_order": 4, "roles": ["admin", "editor"]},
{"name": "任务", "path": "tasks.html", "icon": "IconMenu", "sort_order": 5, "roles": ["admin", "editor"]},
{"name": "系统", "path": "admin.html", "icon": "IconSetting", "sort_order": 6, "roles": ["admin"]},
{"name": "外推营销", "path": "campaigns.html", "icon": "IconStar", "sort_order": 6, "roles": ["admin", "editor"]},
{"name": "AI味检测", "path": "ai-slop.html", "icon": "IconSearch", "sort_order": 7, "roles": ["admin", "editor"]},
{"name": "白标设置", "path": "branding.html", "icon": "IconSetting", "sort_order": 8, "roles": ["admin"]},
{"name": "系统", "path": "admin.html", "icon": "IconSetting", "sort_order": 9, "roles": ["admin"]},
]
for m in default_menus:
found = db.query(Menu).filter(Menu.path == m["path"]).first()
if found:
found.sort_order = m["sort_order"]
found.roles = m["roles"]
found.icon = m["icon"]
found.is_active = True
# 保留原名称,避免意外重命名
else:
db.add(Menu(**m))
# 原独立的选题/内容工厂/文章管理已合并进「创作工作台」,从主导航隐藏(页面仍可被 studio 内嵌与直接访问)
for legacy in ["topics.html", "factory.html", "articles.html"]:
legacy_menu = db.query(Menu).filter(Menu.path == legacy).first()
if legacy_menu:
legacy_menu.is_active = False
db.commit()
print("插入默认菜单")
print("✅ 菜单已对齐(创作工作台合并选题/内容工厂/文章管理)")
# 同步 PostgreSQL 自增序列
if os.getenv('USE_POSTGRES', 'true').lower() == 'true':
+3 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from .database import engine, get_db, init_db
from .models import Base
from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, task_configs, prompt_configs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config, collector_mgmt, assistant, config_items, role_configs, menu_configs, search_providers, search_rankings, external_promotion
from .api import topics, system, articles, publishing, auth, admin, audit, optimizer_logs, cases, task_logs, task_configs, prompt_configs, llm_configs, system_configs, topic_config, calendar, metrics, assets, tasks, platform_config, collector_mgmt, assistant, config_items, role_configs, menu_configs, search_providers, search_rankings, external_promotion, ai_slop, branding
from .initial_data import import_initial_data
from .core.scheduler import scheduler
@@ -106,6 +106,8 @@ app.include_router(menu_configs.public_router)
app.include_router(search_providers.router)
app.include_router(search_rankings.router)
app.include_router(external_promotion.router)
app.include_router(ai_slop.router)
app.include_router(branding.router)
# 挂载自动生成的图片(必须先于前端根挂载)
PROJECT_ROOT_DIR = Path(__file__).parent.parent.parent.parent
+1
View File
@@ -265,6 +265,7 @@ class Topic(Base):
platform_urls = Column(JSON, default=dict)
lock_by = Column(String, nullable=True)
lock_at = Column(DateTime, nullable=True)
series = Column(String, nullable=True, index=True, comment="所属栏目,如:AI工具实测周报、科技人文思辨")
field = relationship("TopicField", backref="topics")
+5
View File
@@ -76,6 +76,7 @@ class TopicBase(BaseModel):
scoring_data: Dict[str, Any] = {}
lock_by: Optional[str] = None
lock_at: Optional[datetime] = None
series: Optional[str] = None
class TopicCreate(BaseModel):
@@ -90,6 +91,7 @@ class TopicCreate(BaseModel):
tags: List[str] = []
custom_data: Dict[str, Any] = {}
scoring_data: Dict[str, Any] = {}
series: Optional[str] = None
class TopicAnalyzeRequest(BaseModel):
@@ -123,6 +125,7 @@ class TopicUpdate(BaseModel):
scoring_data: Optional[Dict[str, Any]] = None
priority_score: Optional[int] = None
total_score: Optional[float] = None
series: Optional[str] = None
class TopicResponse(TopicBase):
@@ -135,6 +138,8 @@ class TopicResponse(TopicBase):
compliance_score: Optional[int] = None
platform_urls: Dict[str, str] = {}
cases: List[Any] = []
article_count: int = 0
articles: List[Dict[str, Any]] = []
model_config = ConfigDict(from_attributes=True)
+18 -1
View File
@@ -199,7 +199,8 @@
<div class="empty-text">暂无平台配置</div>
</div>
</template>
<el-table v-else :data="platformConfigs" border stripe style="width:100%">
<template v-else>
<el-table class="data-table" :data="platformConfigs" border stripe style="width:100%">
<el-table-column prop="platform" label="标识" width="120"></el-table-column>
<el-table-column prop="name" label="名称" width="120"></el-table-column>
<el-table-column prop="icon" label="图标" width="100">
@@ -230,6 +231,22 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="pc in platformConfigs" :key="pc.platform" class="card-item">
<div class="card-row"><span class="card-label">标识</span><span class="card-value">{{ pc.platform }}</span></div>
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ pc.name }}</span></div>
<div class="card-row"><span class="card-label">图标</span><span class="card-value">{{ pc.icon || '—' }}</span></div>
<div class="card-row"><span class="card-label">网址</span><span class="card-value" style="word-break:break-all;">{{ pc.website_url || '—' }}</span></div>
<div class="card-row"><span class="card-label">字数</span><span class="card-value">{{ pc.min_words }} - {{ pc.max_words }}</span></div>
<div class="card-row"><span class="card-label">配图</span><span class="card-value">{{ pc.requires_image ? pc.image_count_min + '-' + pc.image_count_max + '张' : '不需要' }}</span></div>
<div class="card-row"><span class="card-label">激活</span><span class="card-value">{{ pc.is_active ? '是' : '否' }}</span></div>
<div class="card-actions">
<el-button size="small" @click="showPcDialogFn(pc)">编辑</el-button>
<el-button size="small" type="danger" @click="deletePlatformConfig(pc.platform)">删除</el-button>
</div>
</div>
</div>
</template>
<el-dialog v-model="showPcDialog" :title="pcDialogTitle" width="750px" :close-on-click-modal="false">
<el-form :model="pcForm" label-width="100px">
<el-row :gutter="16">
+150
View File
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - AI味检测</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<script src="uni-nav.js"></script>
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="AI味检测" :username="currentUser.username" :is-admin="isAdmin" current-page="ai-slop" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="main-content">
<main class="content-area">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconSearch /></el-icon> AI味检测 + 净化</h2>
</div>
<div class="filter-bar">
<el-input v-model="topicId" placeholder="输入选题 ID,如 B02" clearable size="default" style="width:280px;" @keyup.enter="runReport">
<template #prepend>选题</template>
</el-input>
<el-button type="primary" :loading="loadingReport" @click="runReport">检测</el-button>
</div>
<el-alert v-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" style="margin-bottom:12px;"></el-alert>
<div v-if="!loadingReport && report && report.platforms.length === 0" style="text-align:center;padding:40px 0;color:#909399;font-size:14px;">
该选题暂无文章,请先创作内容。
</div>
<div v-for="p in (report ? report.platforms : [])" :key="p.platform" style="margin-bottom:20px;">
<el-card shadow="hover">
<template #header>
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<strong style="font-size:16px;">{{ platformLabel(p.platform) }}</strong>
<el-tag :type="p.passed ? 'success' : 'danger'" size="small">{{ p.passed ? '通过' : '未通过' }}</el-tag>
<span style="font-size:13px;color:#909399;">合规分:<b :style="{color: p.score >= 80 ? '#67c23a' : (p.score >= 60 ? '#e6a23c' : '#f56c6c')}">{{ p.score }}</b></span>
<el-button size="small" type="warning" :loading="p.purifying" @click="purify(p)" style="margin-left:auto;">一键净化</el-button>
</div>
</template>
<div v-if="p.message" style="margin-bottom:10px;color:#67c23a;font-size:13px;">{{ p.message }}</div>
<div v-if="p.score_before !== null" style="margin-bottom:10px;font-size:13px;color:#606266;">
净化前分:<b>{{ p.score_before }}</b> &nbsp;&nbsp; 净化后分:<b :style="{color:'#67c23a'}">{{ p.score_after }}</b>
<span style="margin-left:12px;">问题数:{{ p.issues_before.length }} → {{ p.issues_after.length }}</span>
</div>
<div v-if="p.issues.length === 0" style="color:#67c23a;font-size:14px;">✅ 未检测到明显问题</div>
<template v-else>
<el-table class="card-table" :data="p.issues" size="small" stripe>
<el-table-column prop="type" label="类型" width="110">
<template #default="s"><el-tag :type="s.row.severity === 'high' ? 'danger' : 'warning'" size="small">{{ s.row.type }}</el-tag></template>
</el-table-column>
<el-table-column prop="category" label="类别" width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="detail" label="详情" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="suggestion" label="建议" min-width="180" show-overflow-tooltip></el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="iss in p.issues" :key="iss.type + iss.detail" class="card-item">
<div class="card-row"><span class="card-label">类型</span><span class="card-value"><el-tag :type="iss.severity === 'high' ? 'danger' : 'warning'" size="small">{{ iss.type }}</el-tag></span></div>
<div class="card-row"><span class="card-label">类别</span><span class="card-value">{{ iss.category }}</span></div>
<div class="card-row"><span class="card-label">详情</span><span class="card-value">{{ iss.detail }}</span></div>
<div class="card-row"><span class="card-label">建议</span><span class="card-value">{{ iss.suggestion }}</span></div>
</div>
</div>
</template>
</el-card>
</div>
</div>
</main>
</div>
</div>
<script src="vue.global.prod.js"></script>
<script src="icon-components.js"></script>
<script src="element-plus.full.js"></script>
<script>
const AiSlopApp = {
data() {
return {
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
topicId: '', loadingReport: false, report: null, errorMsg: ''
}
},
methods: {
getToken() { return localStorage.getItem('authToken'); },
async api(url, opts = {}) {
const token = this.getToken();
if (!token) { this.$message.error('请先登录'); setTimeout(() => window.location.href = '/login.html', 1500); return null; }
const res = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', ...(opts.headers || {}) }, ...opts });
if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.detail || `请求失败: ${res.status}`); }
return res.json();
},
async runReport() {
this.errorMsg = '';
if (!this.topicId.trim()) { this.$message.warning('请输入选题 ID'); return; }
this.loadingReport = true;
this.report = null;
try {
const data = await this.api('/api/ai-slop/report?topic_id=' + encodeURIComponent(this.topicId.trim()));
this.report = { topic_id: data.topic_id, platforms: (data.platforms || []).map(p => ({ ...p, purifying: false, message: '', score_before: null, score_after: null, issues_before: [], issues_after: [] })) };
} catch (error) {
this.errorMsg = error.message;
} finally { this.loadingReport = false; }
},
async purify(p) {
p.purifying = true; p.message = '';
try {
const data = await this.api('/api/ai-slop/purify', {
method: 'POST',
body: JSON.stringify({ topic_id: this.topicId.trim(), platform: p.platform })
});
p.score_before = data.score_before;
p.score_after = data.score_after;
p.issues_before = data.issues_before || [];
p.issues_after = data.issues_after || [];
p.issues = data.issues_after || [];
p.score = data.score_after;
p.passed = (data.score_after !== null && data.issues_after.length === 0) || (data.issues_after && data.issues_after.every(i => i.severity !== 'high')) || (p.score >= 80);
p.passed = data.score_after !== null ? (data.issues_after || []).every(i => i.severity !== 'high') : p.passed;
p.message = data.message || '净化完成';
this.$message.success('净化完成,合规分 ' + data.score_before + ' → ' + data.score_after);
} catch (error) {
this.$message.error('净化失败: ' + error.message);
} finally { p.purifying = false; }
},
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
platformLabel(p) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[p] || p; },
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
const params = new URLSearchParams(window.location.search);
const tid = params.get('topic_id');
if (tid) this.topicId = tid;
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true;
if (tid) this.runReport();
})
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
}
};
const app = Vue.createApp(AiSlopApp);
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
</body>
</html>
+129 -17
View File
@@ -24,10 +24,12 @@
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="文章管理" :username="currentUser.username" :is-admin="isAdmin" current-page="articles" @navigate="redirectToPage" @logout="handleLogout">
<uni-nav v-if="!isEmbed" title="文章管理" :username="currentUser.username" :is-admin="isAdmin" current-page="articles" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="main-content">
<main class="content-area">
<el-tabs v-model="activeTab">
<el-tab-pane label="文章管理" name="articles">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconDocument /></el-icon> 文章管理</h2>
@@ -65,17 +67,14 @@
<el-table-column prop="word_count" label="字数" width="70">
<template #default="scope">{{ scope.row.word_count ?? '-' }}</template>
</el-table-column>
<el-table-column label="配图" width="60">
<template #default="scope">
<span v-if="scope.row.images && scope.row.images.cover" style="cursor:pointer;font-size:16px;" title="有封面图"><el-icon style="vertical-align:-2px;"><IconPicture /></el-icon></span>
<span v-else style="color:#dcdfe6;"></span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<el-table-column label="操作" width="210" fixed="right">
<template #default="scope">
<div style="display: flex; gap: 4px;">
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
<el-button size="small" type="primary" @click="previewArticle(scope.row)" style="padding:5px 8px;">预览</el-button>
<el-button size="small" @click="editArticle(scope.row)" style="padding:5px 8px;">编辑</el-button>
<el-button size="small" type="warning" @click="openSlop(scope.row)" style="padding:5px 8px;">检测</el-button>
<el-button size="small" type="success" @click="goPublish()" style="padding:5px 8px;">发布</el-button>
<el-button size="small" type="danger" @click="deleteArticle(scope.row)" style="padding:5px 8px;">删除</el-button>
</div>
</template>
@@ -96,6 +95,9 @@
</div>
<div class="article-card-actions">
<el-button size="small" type="primary" @click="previewArticle(article)">预览</el-button>
<el-button size="small" @click="editArticle(article)">编辑</el-button>
<el-button size="small" type="warning" @click="openSlop(article)">检测</el-button>
<el-button size="small" type="success" @click="goPublish()">发布</el-button>
<el-button size="small" type="danger" @click="deleteArticle(article)">删除</el-button>
</div>
</div>
@@ -104,6 +106,48 @@
<el-icon><IconDocument /></el-icon> 暂无文章
</div>
</div>
</el-tab-pane>
<el-tab-pane label="发布记录" name="publish">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconUpload /></el-icon> 发布记录</h2>
<el-tag type="info" size="small">AI 内容需真人校验后发布,系统不代发</el-tag>
</div>
<el-table :data="publishRecords" stripe v-loading="loadingRecords">
<el-table-column prop="topic_id" label="选题 ID" width="90"></el-table-column>
<el-table-column prop="topic_title" label="选题标题" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="platform" label="平台" width="100">
<template #default="scope">{{ platformLabel(scope.row.platform) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="110">
<template #default="scope"><el-tag :type="scope.row.status === 'published' ? 'success' : 'warning'" size="small">{{ scope.row.status === 'published' ? '已发布' : '待人工发布' }}</el-tag></template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100"></el-table-column>
<el-table-column prop="created_at" label="生成时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button v-if="scope.row.status === 'pending_manual'" size="small" type="success" @click="markRecord(scope.row)" style="padding:5px 8px;">标记已发布</el-button>
<span v-else style="color:#909399;font-size:12px;"></span>
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile" v-if="publishRecords && publishRecords.length > 0">
<div v-for="rec in publishRecords" :key="rec.id" class="card-item">
<div class="card-row"><span class="card-label">选题</span><span class="card-value">{{ rec.topic_title || rec.topic_id }}</span></div>
<div class="card-row"><span class="card-label">平台</span><span class="card-value">{{ platformLabel(rec.platform) }}</span></div>
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="rec.status === 'published' ? 'success' : 'warning'" size="small">{{ rec.status === 'published' ? '已发布' : '待人工发布' }}</el-tag></span></div>
<div class="card-row"><span class="card-label">操作人</span><span class="card-value">{{ rec.operator || '-' }}</span></div>
<div class="card-actions" v-if="rec.status === 'pending_manual'">
<el-button size="small" type="success" @click="markRecord(rec)">标记已发布</el-button>
</div>
</div>
</div>
<div v-if="!loadingRecords && publishRecords.length === 0" style="text-align:center;padding:40px 0;color:#909399;font-size:14px;">
<el-icon><IconUpload /></el-icon> 暂无发布记录
</div>
</div>
</el-tab-pane>
</el-tabs>
</main>
</div>
<el-dialog v-model="previewVisible" title="文章预览" width="85%" :modal-props="{ closeOnClickModal: false }" :before-close="() => previewVisible = false" class="preview-dialog-custom" :fullscreen="previewFullscreen" close-on-press-escape :lock-scroll="false">
@@ -136,19 +180,31 @@
</div>
</template>
</el-dialog>
<el-dialog v-model="editVisible" title="编辑正文" width="85%" :close-on-click-modal="false">
<el-alert type="warning" :closable="false" show-icon style="margin-bottom:12px;">修改后保存即覆盖该平台文章,发布前请先完成 AI 味检测与人工校验。</el-alert>
<el-input type="textarea" :rows="20" v-model="editForm.html_content" style="font-family:monospace;"></el-input>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="savingEdit" @click="saveArticle">保存</el-button>
</template>
</el-dialog>
</div>
<script src="vue.global.prod.js"></script>
<script src="icon-components.js"></script>
<script src="element-plus.full.js"></script>
<script>
const ArticlesApp = {
const ArticlesApp = {
data() {
return {
isEmbed: false,
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
activeTab: 'articles',
loadingTable: false,
articles: [],
filterPlatform: '', filterStatus: '', searchQuery: '',
previewVisible: false, previewArticleData: null, previewFullscreen: false,
editVisible: false, savingEdit: false, editForm: { topic_id: '', platform: '', html_content: '' },
publishRecords: [], loadingRecords: false,
debounceTimer: null
}
},
@@ -164,6 +220,9 @@ const ArticlesApp = {
return list;
}
},
watch: {
activeTab(v) { if (v === 'publish') this.fetchRecords(); }
},
methods: {
getToken() { return localStorage.getItem('authToken'); },
async api(url, opts = {}) {
@@ -188,6 +247,16 @@ const ArticlesApp = {
this.articles = [];
} finally { this.loadingTable = false; }
},
async fetchRecords() {
this.loadingRecords = true;
try {
const data = await this.api('/api/publishing/records');
this.publishRecords = data.records || [];
} catch (error) {
this.$message.error('加载发布记录失败: ' + error.message);
this.publishRecords = [];
} finally { this.loadingRecords = false; }
},
debouncedSearch() {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => { this.fetchArticles(); }, 400);
@@ -204,6 +273,48 @@ const ArticlesApp = {
this.previewVisible = false;
}
},
async editArticle(article) {
this.editForm = { topic_id: article.topic_id, platform: article.platform, html_content: '' };
this.editVisible = true;
this.savingEdit = false;
try {
const data = await this.api('/api/articles/detail/' + article.id);
this.editForm.html_content = data.html_content || '';
} catch (error) {
this.$message.error('加载正文失败: ' + error.message);
this.editVisible = false;
}
},
async saveArticle() {
this.savingEdit = true;
try {
await this.api('/api/articles/' + encodeURIComponent(this.editForm.topic_id) + '/content', {
method: 'PUT',
body: JSON.stringify({ platform: this.editForm.platform, html_content: this.editForm.html_content })
});
this.$message.success('正文已保存');
this.editVisible = false;
await this.fetchArticles();
} catch (error) {
this.$message.error('保存失败: ' + error.message);
} finally { this.savingEdit = false; }
},
openSlop(article) {
window.open('ai-slop.html?topic_id=' + encodeURIComponent(article.topic_id), '_blank');
},
goPublish() { this.redirectToPage('topics.html'); },
async markRecord(rec) {
try {
await this.$confirm('确认已在各平台完成发布?', '标记发布', { type: 'warning' });
await this.api('/api/publishing/' + encodeURIComponent(rec.topic_id) + '/mark-published', { method: 'POST' });
this.$message.success('已标记为已发布');
await this.fetchRecords();
} catch (e) {
if (e === 'cancel') return;
const msg = e?.response?.data?.detail || e?.message || '未知错误';
this.$message.error('操作失败: ' + msg);
}
},
togglePreviewFullscreen() {
this.previewFullscreen = !this.previewFullscreen;
this.$nextTick(() => {
@@ -243,7 +354,7 @@ const ArticlesApp = {
} catch (e) { if (e !== 'cancel') this.$message.error('删除失败: ' + (e.message || '未知错误')); }
},
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
redirectToPage(page) { const url = page.startsWith('/') ? page : '/' + page; if (this.isEmbed) window.top.location.href = url; else window.location.href = url; },
platformLabel(p) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[p] || p; },
statusLabel(s) { return { draft: '草稿', reviewed: '已审查', published: '已发布' }[s] || s; },
statusTagType(s) { return { draft: 'warning', reviewed: 'success', published: 'info' }[s] || 'primary'; },
@@ -256,17 +367,18 @@ const ArticlesApp = {
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
if (new URLSearchParams(window.location.search).get('embed') === '1') { this.isEmbed = true; document.body.classList.add('embed'); }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; this.fetchArticles(); })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
}
};
const app = Vue.createApp(ArticlesApp);
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
};
const app = Vue.createApp(ArticlesApp);
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
<script src="ai-assistant.js"></script>
</body>
+123
View File
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 白标设置</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<script src="uni-nav.js"></script>
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="白标设置" :username="currentUser.username" :is-admin="isAdmin" current-page="branding" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="main-content">
<main class="content-area">
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconSetting /></el-icon> 白标 / 私有化品牌设置</h2>
<el-tag type="info" size="small">仅管理员可见</el-tag>
</div>
<el-alert type="success" :closable="false" show-icon style="margin-bottom:16px;">
<template #title>私有化交付说明</template>
修改以下配置即可将本平台改名为贵司品牌(名称 / Logo / 主色 / 支持邮箱),保存后前台导航与品牌色即时生效。适合部署到客户内网或作为白标产品交付。
</el-alert>
<el-form label-width="110px" style="max-width:640px;">
<el-form-item label="平台名称">
<el-input v-model="form.brand_name" placeholder="如:星辰内容中台" maxlength="20" show-word-limit></el-input>
</el-form-item>
<el-form-item label="Logo 地址">
<el-input v-model="form.brand_logo" placeholder="https 图片 URL,留空则显示文字名称"></el-input>
</el-form-item>
<el-form-item label="主色">
<el-color-picker v-model="form.brand_primary_color"></el-color-picker>
<el-input v-model="form.brand_primary_color" style="width:120px;margin-left:12px;" placeholder="#2563eb"></el-input>
</el-form-item>
<el-form-item label="支持邮箱">
<el-input v-model="form.brand_support_email" placeholder="support@yourcompany.com"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="save">保存并应用</el-button>
<el-button @click="reset">重置为当前</el-button>
</el-form-item>
</el-form>
<el-divider>实时预览</el-divider>
<div :style="{ background: 'linear-gradient(135deg,' + (form.brand_primary_color || '#2563eb') + ',' + (form.brand_primary_color || '#2563eb') + 'cc)', color:'#fff', padding:'10px 16px', borderRadius:'8px', display:'flex', alignItems:'center', gap:'8px', maxWidth:'480px' }">
<img v-if="form.brand_logo" :src="form.brand_logo" style="height:22px;border-radius:4px;" />
<strong>{{ form.brand_name || '平台名称预览' }}</strong>
<span style="margin-left:auto;font-size:12px;opacity:.85;">v{{ version }}</span>
</div>
</div>
</main>
</div>
</div>
<script src="vue.global.prod.js"></script>
<script src="icon-components.js"></script>
<script src="element-plus.full.js"></script>
<script>
const BrandingApp = {
data() {
return {
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
form: { brand_name: '', brand_logo: '', brand_primary_color: '#2563eb', brand_support_email: '' },
version: '0.1.0', saving: false,
}
},
methods: {
getToken() { return localStorage.getItem('authToken'); },
async load() {
try {
const data = await fetch('/api/branding').then(r => r.json());
this.form = {
brand_name: data.brand_name || '',
brand_logo: data.brand_logo || '',
brand_primary_color: data.brand_primary_color || '#2563eb',
brand_support_email: data.brand_support_email || '',
};
this.version = data.version || '0.1.0';
} catch (e) { this.$message.error('加载品牌配置失败'); }
},
async save() {
this.saving = true;
try {
const token = this.getToken();
const res = await fetch('/api/branding', {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
if (!res.ok) throw new Error((await res.json()).detail || '保存失败');
this.$message.success('已保存,刷新其他页面即可看到新品牌');
setTimeout(() => this.load(), 300);
} catch (e) {
this.$message.error('保存失败:' + e.message);
} finally { this.saving = false; }
},
reset() { this.load(); },
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
this.currentUser = data.user; this.isAdmin = data.user.role === 'admin';
if (!this.isAdmin) { this.$message.warning('需要管理员权限'); window.location.href = '/index.html'; return; }
this.load();
})
.catch(() => { window.location.href = '/login.html'; });
}
};
const app = Vue.createApp(BrandingApp);
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
</body>
</html>
+14 -1
View File
@@ -186,7 +186,8 @@
<div class="empty-text">该日暂无日程</div>
<p style="font-size:13px;color:#909399;margin:4px 0 0 0;">点击上方"添加日程"创建发布计划</p>
</div>
<el-table v-else :data="selectedDayEntries" stripe>
<template v-else>
<el-table class="card-table" :data="selectedDayEntries" stripe>
<el-table-column prop="title" label="标题" min-width="120"></el-table-column>
<el-table-column prop="platform" label="平台" width="100">
<template #default="scope">{{ scope.row._source === 'topic' ? (scope.row.status === 'published' ? '已发布' : '选题') : platformName(scope.row.platform) }}</template>
@@ -201,6 +202,18 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="entry in selectedDayEntries" :key="entry.id || entry.topic_id || entry.title" class="card-item">
<div class="card-row"><span class="card-label">标题</span><span class="card-value">{{ entry.title }}</span></div>
<div class="card-row"><span class="card-label">平台</span><span class="card-value">{{ entry._source === 'topic' ? (entry.status === 'published' ? '已发布' : '选题') : platformName(entry.platform) }}</span></div>
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="statusType(entry.status)" size="small">{{ statusLabel(entry.status) }}</el-tag></span></div>
<div class="card-actions">
<el-button v-if="entry._source === 'topic'" size="small" type="primary" @click="openEntryDialog(entry)">查看</el-button>
<el-button v-else size="small" @click="openEntryDialog(entry)">编辑</el-button>
</div>
</div>
</div>
</template>
</div>
</el-dialog>
+106 -35
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
[v-cloak] { display: none; }
.data-table { display: block; }
@media (max-width: 768px) { .data-table { display: none; } }
.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px; }
@@ -14,8 +15,18 @@
.stat-card .num { font-size: 28px; font-weight: 700; color: #2563eb; }
.stat-card .label { font-size: 12px; color: #909399; margin-top: 4px; }
.search-engine-tag { margin: 2px; }
@media (max-width: 768px) {
.page-header { flex-direction: column; align-items: flex-start !important; gap: 8px; }
.filter-bar { display: flex; flex-wrap: wrap; gap: 4px; }
.filter-bar .el-button { margin-left: 0 !important; }
.stat-cards { grid-template-columns: repeat(2, 1fr); }
.toolbar { flex-wrap: wrap; gap: 8px; }
}
</style>
<script src="uni-nav.js"></script>
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script src="icon-components.js"></script>
</head>
<body>
<div id="app" v-cloak>
@@ -89,6 +100,17 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="p in products" :key="p.id" class="card-item">
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ p.name }}</span></div>
<div class="card-row"><span class="card-label">类型</span><span class="card-value">{{ typeLabel(p.type) }}</span></div>
<div class="card-row"><span class="card-label">URL</span><span class="card-value"><a :href="p.url" target="_blank" style="color:#409eff;word-break:break-all;">{{ p.url || '-' }}</a></span></div>
<div class="card-actions">
<el-button size="small" @click="editProduct(p)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteProduct(p)">删除</el-button>
</div>
</div>
</div>
<div v-if="products.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="products.length" :page-size="pageSize" :current-page="productPage" @current-change="productPage = $event"></el-pagination>
</div>
@@ -129,6 +151,19 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="c in campaigns" :key="c.id" class="card-item">
<div class="card-row"><span class="card-label">活动</span><span class="card-value">{{ c.name }}</span></div>
<div class="card-row"><span class="card-label">产品</span><span class="card-value">{{ c.product_name || '-' }}</span></div>
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="c.status === 'active' ? 'success' : c.status === 'paused' ? 'warning' : 'info'" size="small">{{ { active: '进行中', paused: '已暂停', completed: '已完成' }[c.status] || c.status }}</el-tag></span></div>
<div class="card-row"><span class="card-label">引擎</span><span class="card-value"><el-tag v-for="e in (c.target_engines || [])" :key="e" size="small" class="search-engine-tag">{{ engineLabel(e) }}</el-tag></span></div>
<div class="card-actions">
<el-button size="small" @click="viewCampaignKeywords(c)">关键词</el-button>
<el-button size="small" @click="editCampaign(c)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCampaign(c)">删除</el-button>
</div>
</div>
</div>
<div v-if="campaigns.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="campaigns.length" :page-size="pageSize" :current-page="campaignPage" @current-change="campaignPage = $event"></el-pagination>
</div>
@@ -139,7 +174,8 @@
关键词详情:{{ selectedCampaign.name }}
<el-button size="small" type="primary" style="margin-left:8px;" @click="showKeywordDialog = true">添加关键词</el-button>
</h4>
<el-table v-if="campaignKeywords.length" :data="campaignKeywords" border stripe class="data-table" style="width:100%">
<template v-if="campaignKeywords.length">
<el-table :data="campaignKeywords" border stripe class="data-table" style="width:100%">
<el-table-column prop="keyword" label="关键词" min-width="150"></el-table-column>
<el-table-column prop="search_volume" label="搜索量" width="80"></el-table-column>
<el-table-column label="难度" width="70">
@@ -154,6 +190,18 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="kw in campaignKeywords" :key="kw.id" class="card-item">
<div class="card-row"><span class="card-label">关键词</span><span class="card-value">{{ kw.keyword }}</span></div>
<div class="card-row"><span class="card-label">搜索量</span><span class="card-value">{{ kw.search_volume || 0 }}</span></div>
<div class="card-row"><span class="card-label">难度</span><span class="card-value"><el-tag :type="(kw.difficulty || 0) > 0.7 ? 'danger' : (kw.difficulty || 0) > 0.4 ? 'warning' : 'success'" size="small">{{ kw.difficulty != null ? (kw.difficulty * 100).toFixed(0) + '%' : '-' }}</el-tag></span></div>
<div class="card-row"><span class="card-label">排名</span><span class="card-value">当前 {{ kw.current_rank || '-' }} 目标 {{ kw.target_rank || '-' }}</span></div>
<div class="card-actions">
<el-button size="small" type="danger" @click="deleteKeyword(kw)">删除</el-button>
</div>
</div>
</div>
</template>
<div v-else style="text-align:center;padding:20px;color:#909399;font-size:13px;">暂无关键词数据</div>
</div>
</div>
@@ -162,7 +210,7 @@
<div class="toolbar">
<span style="font-size:13px;color:#909399;">
共 {{ keywordRankings.length }} 条记录
<span v-if="rankingOverview.with_rank != null"> · 有排名 {{ rankingOverview.with_rank }} 条</span>
<span v-if="keywordRankingOverview.with_rank != null"> · 有排名 {{ keywordRankingOverview.with_rank }} 条</span>
</span>
</div>
<div v-if="loading" class="card-loading">加载中...</div>
@@ -177,7 +225,7 @@
</el-table-column>
<el-table-column label="排名" width="80">
<template #default="s">
<el-tag :type="s.row.rank != null && s.row.rank <= 10 ? 'success' : s.row.rank != null && s.row.rank <= 30 ? 'warning' : 'info'" size="small">{{ s.row.rank || '-' }}</el-tag>
<el-tag :type="s.row.rank == null ? 'info' : s.row.rank <= 10 ? 'success' : s.row.rank <= 30 ? 'warning' : 'info'" size="small">{{ s.row.rank || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="url_found" label="排名URL" min-width="200">
@@ -187,6 +235,14 @@
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="r in keywordRankings" :key="r.id" class="card-item">
<div class="card-row"><span class="card-label">关键词</span><span class="card-value">{{ r.keyword }}</span></div>
<div class="card-row"><span class="card-label">引擎</span><span class="card-value">{{ engineLabel(r.search_engine) }}</span></div>
<div class="card-row"><span class="card-label">排名</span><span class="card-value"><el-tag :type="r.rank == null ? 'info' : r.rank <= 10 ? 'success' : r.rank <= 30 ? 'warning' : 'info'" size="small">{{ r.rank || '-' }}</el-tag></span></div>
<div class="card-row"><span class="card-label">时间</span><span class="card-value">{{ r.checked_at ? new Date(r.checked_at).toLocaleString('zh-CN') : '-' }}</span></div>
</div>
</div>
<div v-if="keywordRankings.length > pageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="keywordRankings.length" :page-size="pageSize" :current-page="rankingPage" @current-change="rankingPage = $event"></el-pagination>
</div>
@@ -223,6 +279,15 @@
<template #default="s">{{ s.row.checked_at ? new Date(s.row.checked_at).toLocaleString('zh-CN') : '-' }}</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="a in audits" :key="a.id" class="card-item">
<div class="card-row"><span class="card-label">产品</span><span class="card-value">{{ a.product_name || '-' }}</span></div>
<div class="card-row"><span class="card-label">总分</span><span class="card-value"><el-tag :type="(a.overall_score || 0) >= 80 ? 'success' : (a.overall_score || 0) >= 60 ? 'warning' : 'danger'" size="small">{{ a.overall_score != null ? a.overall_score.toFixed(1) : '-' }}</el-tag></span></div>
<div class="card-row"><span class="card-label">评分</span><span class="card-value" style="font-size:12px;">M{{ a.meta_score || '-' }} H{{ a.heading_score || '-' }} C{{ a.content_score || '-' }} P{{ a.perf_score || '-' }} L{{ a.links_score || '-' }} Mob{{ a.mobile_score || '-' }}</span></div>
<div class="card-row"><span class="card-label">问题数</span><span class="card-value">{{ a.issues_found || 0 }}</span></div>
<div class="card-row"><span class="card-label">时间</span><span class="card-value">{{ a.checked_at ? new Date(a.checked_at).toLocaleString('zh-CN') : '-' }}</span></div>
</div>
</div>
</template>
</div>
@@ -260,6 +325,19 @@
</template>
</el-table-column>
</el-table>
<div class="card-list-mobile">
<div v-for="t in optimizationTasks" :key="t.id" class="card-item">
<div class="card-row"><span class="card-label">严重度</span><span class="card-value"><el-tag :type="t.severity === 'high' ? 'danger' : t.severity === 'medium' ? 'warning' : 'info'" size="small">{{ { high: '高', medium: '中', low: '低' }[t.severity] || t.severity }}</el-tag></span></div>
<div class="card-row"><span class="card-label">分类</span><span class="card-value">{{ { meta: 'Meta', heading: '标题', content: '内容', performance: '性能', links: '链接', mobile: '移动端' }[t.category] || t.category }}</span></div>
<div class="card-row"><span class="card-label">问题</span><span class="card-value" style="font-size:12px;">{{ t.issue }}</span></div>
<div class="card-row"><span class="card-label">建议</span><span class="card-value" style="font-size:12px;">{{ t.recommendation }}</span></div>
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag :type="t.status === 'resolved' ? 'success' : t.status === 'ignored' ? 'info' : 'warning'" size="small">{{ { open: '待处理', resolved: '已解决', ignored: '已忽略' }[t.status] || t.status }}</el-tag></span></div>
<div class="card-actions">
<el-button v-if="t.status === 'open'" size="small" type="success" @click="updateTaskStatus(t, 'resolved')">解决</el-button>
<el-button size="small" @click="updateTaskStatus(t, 'ignored')">忽略</el-button>
</div>
</div>
</div>
</template>
</div>
</div>
@@ -342,11 +420,13 @@
</el-dialog>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus"></script>
<script src="icon-components.js"></script>
<script>
const { createApp, ref, computed, onMounted, nextTick } = Vue;
const { ElMessage } = ElementPlus;
// Auth check — redirect early if no token
const authToken = localStorage.getItem('authToken') || '';
if (!authToken) { window.location.href = '/login.html'; }
const app = createApp({
data() {
@@ -380,7 +460,7 @@
showKeywordDialog: false,
keywordForm: { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 },
token: '',
token: authToken,
};
},
computed: {
@@ -435,7 +515,7 @@
await this.loadTasks();
}
} catch (e) {
ElementPlus.ElMessage.error(e.message);
ElMessage.error(e.message);
} finally {
this.loading = false;
}
@@ -464,7 +544,7 @@
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.productForm) });
const d = await r.json();
if (!d.ok) throw new Error(d.detail || '操作失败');
ElementPlus.ElMessage.success(this.editingProduct ? '已更新' : '已创建');
ElMessage.success(this.editingProduct ? '已更新' : '已创建');
this.showProductDialog = false;
this.editingProduct = null;
this.productForm = { name: '', type: 'website', url: '', domain: '', app_id: '', account_id: '', description: '' };
@@ -473,7 +553,7 @@
async deleteProduct(p) {
if (!confirm('确定删除产品「' + p.name + '」?')) return;
await this.fetchJson(this.api('/products/' + p.id), { method: 'DELETE', headers: this.headers() });
ElementPlus.ElMessage.success('已删除');
ElMessage.success('已删除');
this.products = await this.fetchJson(this.api('/products'));
},
@@ -502,7 +582,7 @@
const r = await fetch(url, { method, headers: this.headers(), body: JSON.stringify(this.campaignForm) });
const d = await r.json();
if (!d.ok) throw new Error(d.detail || '操作失败');
ElementPlus.ElMessage.success(this.editingCampaign ? '已更新' : '已创建');
ElMessage.success(this.editingCampaign ? '已更新' : '已创建');
this.showCampaignDialog = false;
this.editingCampaign = null;
this.campaignForm = { name: '', product_id: null, status: 'active', target_engines: [], keywords: [], notes: '' };
@@ -511,18 +591,18 @@
async deleteCampaign(c) {
if (!confirm('确定删除活动「' + c.name + '」?')) return;
await this.fetchJson(this.api('/campaigns/' + c.id), { method: 'DELETE', headers: this.headers() });
ElementPlus.ElMessage.success('已删除');
ElMessage.success('已删除');
this.campaigns = await this.fetchJson(this.api('/campaigns'));
},
// Keywords
async saveKeyword() {
if (!this.selectedCampaign) { ElementPlus.ElMessage.warning('请先选择活动'); return; }
if (!this.selectedCampaign) { ElMessage.warning('请先选择活动'); return; }
this.keywordForm.campaign_id = this.selectedCampaign.id;
const r = await fetch(this.api('/keywords'), { method: 'POST', headers: this.headers(), body: JSON.stringify(this.keywordForm) });
const d = await r.json();
if (!d.ok) throw new Error(d.detail || '添加失败');
ElementPlus.ElMessage.success('已添加');
ElMessage.success('已添加');
this.showKeywordDialog = false;
this.keywordForm = { keyword: '', search_volume: 0, difficulty: 0.5, target_rank: 10 };
this.loadCampaignKeywords(this.selectedCampaign.id);
@@ -530,14 +610,14 @@
async deleteKeyword(kw) {
if (!confirm('确定删除关键词「' + kw.keyword + '」?')) return;
await this.fetchJson(this.api('/keywords/' + kw.id), { method: 'DELETE', headers: this.headers() });
ElementPlus.ElMessage.success('已删除');
ElMessage.success('已删除');
if (this.selectedCampaign) this.loadCampaignKeywords(this.selectedCampaign.id);
},
// Optimization Tasks
async updateTaskStatus(task, status) {
await this.fetchJson(this.api('/optimization-tasks/' + task.id), { method: 'PATCH', headers: this.headers(), body: JSON.stringify({ status: status }) });
ElementPlus.ElMessage.success('已更新');
ElMessage.success('已更新');
this.loadTasks();
},
@@ -549,32 +629,23 @@
window.location.href = '/login.html';
},
},
async mounted() {
this.token = localStorage.getItem('authToken') || '';
if (!this.token) { window.location.href = '/login.html'; return; }
try {
const r = await fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + this.token } });
if (!r.ok) throw new Error('unauthorized');
const d = await r.json();
mounted() {
const token = this.token;
if (!token) { window.location.href = '/login.html'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function (r) { if (!r.ok) throw new Error('unauthorized'); return r.json(); })
.then(function (d) {
this.currentUser = d.user || d;
this.isAdmin = this.currentUser.role === 'admin';
window.installUniNav(this.$parent ? null : app).component('uni-nav', window.UniNav);
} catch (e) {
window.location.href = '/login.html';
}
}.bind(this))
.catch(function () { window.location.href = '/login.html'; });
this.loadTabData();
},
});
// Register Element Plus icons
const { Edit, Delete, Plus, Search, Setting, Monitor, Document, DataBoard, Folder, Menu as IconMenu } = ElementPlusIconsVue || {};
if (ElementPlusIconsVue) {
for (const [key, comp] of Object.entries(ElementPlusIconsVue)) {
app.component(key, comp);
}
}
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
</body>
+8 -4
View File
@@ -195,7 +195,7 @@
/* ========== Topic Cards (待创作) ========== */
.topic-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
gap: var(--spacing-md);
}
.topic-card {
@@ -565,7 +565,7 @@
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="内容工厂" :username="currentUser.username" :is-admin="isAdmin" current-page="factory" @navigate="redirectToPage" @logout="handleLogout">
<uni-nav v-if="!isEmbed" title="内容工厂" :username="currentUser.username" :is-admin="isAdmin" current-page="factory" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="main-content">
@@ -838,6 +838,7 @@
const FactoryApp = {
data() {
return {
isEmbed: false,
isLoggedIn: false,
isAdmin: false,
currentUser: { username: '' },
@@ -1037,7 +1038,7 @@ const FactoryApp = {
});
},
goToArticles() {
window.location.href = '/articles.html';
this.redirectToPage('/articles.html');
},
handleLogout() {
localStorage.removeItem('authToken');
@@ -1046,7 +1047,9 @@ const FactoryApp = {
window.location.href = '/login.html';
},
redirectToPage(page) {
window.location.href = page.startsWith('/') ? page : '/' + page;
const url = page.startsWith('/') ? page : '/' + page;
if (this.isEmbed) window.top.location.href = url;
else window.location.href = url;
}
},
mounted() {
@@ -1055,6 +1058,7 @@ const FactoryApp = {
window.location.href = '/login.html';
return;
}
if (new URLSearchParams(window.location.search).get('embed') === '1') { this.isEmbed = true; document.body.classList.add('embed'); }
fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + token }
})
+180 -46
View File
@@ -142,6 +142,32 @@
.cta a { display: inline-block; background: #fff; color: #2563eb !important; font-size: 16px; font-weight: 700; padding: 14px 40px; border-radius: 10px; transition: all 0.25s; position: relative; }
.cta a:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.15); }
/* ===== Contact / Trial ===== */
.contact { background: #0f172a; color: #fff; padding: 80px 24px; }
.contact .section-title { color: #fff; }
.contact .section-subtitle { color: rgba(255,255,255,0.7); }
.contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; max-width: 1000px; margin: 0 auto; align-items: start; }
.contact-info { display: flex; flex-direction: column; gap: 20px; }
.contact-info-item { display: flex; gap: 14px; align-items: flex-start; }
.ci-icon { width: 44px; height: 44px; border-radius: 12px; background: linear-gradient(135deg, #2563eb, #7c3aed); display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.ci-label { font-size: 0.8rem; color: rgba(255,255,255,0.6); }
.ci-value { font-size: 1rem; font-weight: 600; color: #fff; }
.ci-value a { color: #fff; }
.contact-form { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 28px; }
.contact-form h3 { font-size: 1.1rem; margin-bottom: 18px; color: #fff; }
.form-row { margin-bottom: 16px; }
.form-row label { display: block; font-size: 13px; color: rgba(255,255,255,0.7); margin-bottom: 6px; }
.form-row input, .form-row textarea, .form-row select { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: #fff; font-size: 14px; box-sizing: border-box; font-family: inherit; }
.form-row input:focus, .form-row textarea:focus, .form-row select:focus { outline: none; border-color: #2563eb; }
.form-row textarea { resize: vertical; min-height: 80px; }
.contact-form .landing-btn-primary { width: 100%; text-align: center; border: none; cursor: pointer; font-size: 15px; }
.contact-note { font-size: 0.8rem; color: rgba(255,255,255,0.5); margin-top: 12px; text-align: center; }
.contact-note a { color: #93c5fd; }
.wecom-qr { margin-top: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 16px; text-align: center; }
.wecom-qr img { width: 160px; height: 160px; object-fit: contain; border-radius: 12px; background: #fff; margin: 0 auto 10px; display: block; }
.wecom-qr-cap { font-size: 0.85rem; color: rgba(255,255,255,0.7); }
@media (max-width: 768px) { .contact-grid { grid-template-columns: 1fr; gap: 32px; } }
/* ===== Footer ===== */
.landing-footer { background: #0f172a; color: #94a3b8; padding: 64px 24px 32px; }
.landing-footer-inner { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; }
@@ -192,7 +218,8 @@
</div>
<div class="landing-nav-actions">
<a href="/login.html" class="landing-btn-login">登录</a>
<a href="/login.html" class="landing-btn-primary">免费使用 →</a>
<a href="#contact" class="landing-btn-login">联系销售</a>
<a href="#pricing" class="landing-btn-primary">免费试用 →</a>
<button class="landing-hamburger" id="hamburger" aria-label="菜单">
<span></span><span></span><span></span>
</button>
@@ -203,10 +230,11 @@
<a href="#features" class="mobile-link">产品功能</a>
<a href="#how-it-works" class="mobile-link">工作流程</a>
<a href="#platforms" class="mobile-link">支持平台</a>
<a href="#pricing" class="mobile-link">定价</a>
<a href="#pricing" class="mobile-link">合作模式</a>
<a href="#faq" class="mobile-link">常见问题</a>
<a href="#contact" class="mobile-link">联系销售</a>
<a href="/login.html" class="landing-btn-login" style="text-align:center;padding:12px;margin-top:8px;">登录</a>
<a href="/login.html" class="landing-btn-primary">免费使用 →</a>
<a href="#pricing" class="landing-btn-primary">免费用 →</a>
</div>
</nav>
@@ -218,8 +246,8 @@
<h1>让内容在 AI 时代<br><span>主动找到你的客户</span></h1>
<p>宇之然是一站式 AI 内容获客系统。自动生成知乎深度文、公众号叙事文、小红书干货贴——三平台差异化分发。通过 AI 搜索推荐(GEO)和独立站 SEO,让零品牌基础的团队也能持续获得精准流量。</p>
<div class="hero-actions">
<a href="/login.html" class="landing-btn-primary">免费开始使用 →</a>
<a href="#features" class="landing-btn-secondary">了解产品功能</a>
<a href="#pricing" class="landing-btn-primary">免费用 →</a>
<a href="#contact" class="landing-btn-secondary">预约演示 / 联系销售</a>
</div>
<div class="hero-stats">
<div class="hero-stat"><div class="hero-stat-num">3</div><div class="hero-stat-label">覆盖平台</div></div>
@@ -330,57 +358,53 @@
<!-- Pricing -->
<section id="pricing" class="pricing">
<div class="container">
<div style="text-align:center;"><span class="section-label">定价</span></div>
<h2 class="section-title">灵活的获客方案</h2>
<p class="section-subtitle">小规模试验到团队级运营,总有适合你的计划</p>
<div style="text-align:center;"><span class="section-label">合作模式</span></div>
<h2 class="section-title">免费试用 · 私有化部署 · 源码售卖</h2>
<p class="section-subtitle">零成本体验到整套买断,三种方式总有一种适合你</p>
<div class="pricing-grid">
<div class="pricing-card">
<div class="pricing-name">入门版</div>
<div class="pricing-price">¥299 <span>/月</span></div>
<div class="pricing-desc">个人创作者试水 AI 内容获客</div>
<div class="pricing-name">免费试用</div>
<div class="pricing-price">¥0 <span></span></div>
<div class="pricing-desc">零成本体验完整创作闭环</div>
<ul class="pricing-features">
<li>每月 20 篇文章生产</li>
<li>私有化试用版 / 预约 1v1 演示</li>
<li>限 5 篇 / 月文章生产</li>
<li>三平台差异化内容</li>
<li>基础 SEO 元数据注入</li>
<li>搜索排名追踪(Bing</li>
<li>GEO 就绪度基础评分</li>
<li>标准合规审查</li>
<li>基础 AI 味检测</li>
<li>基础 GEO 就绪度评分</li>
<li>社区与文档支持</li>
</ul>
<a href="/login.html" class="landing-btn-secondary">开始试用</a>
<a href="#contact" class="landing-btn-secondary">申请试用</a>
</div>
<div class="pricing-card featured">
<div class="pricing-badge">推荐</div>
<div class="pricing-name">专业版</div>
<div class="pricing-price">¥799 <span>/月</span></div>
<div class="pricing-desc">内容团队稳定获客的日常工具</div>
<div class="pricing-badge">主力推荐</div>
<div class="pricing-name">私有化部署</div>
<div class="pricing-price">¥29,800 <span>起 / 买断</span></div>
<div class="pricing-desc">一套独立部署,数据不出域,可白标</div>
<ul class="pricing-features">
<li>每月 100 篇文章生产</li>
<li>三平台差异化内容</li>
<li>高级 SEO + GEO 元数据</li>
<li>搜索排名追踪(Bing + AI</li>
<li>AI 搜索引用检测</li>
<li>GEO 就绪度完整评分(6维度)</li>
<li>全自动定时流水线</li>
<li>多用户协作</li>
<li>完整功能 + 无限文章生产</li>
<li>部署在你的自有服务器</li>
<li>品牌白标(名称 / Logo / 主色)</li>
<li>AI 味检测 + 合规人工闸门</li>
<li>GEO / SEO 全维度优化</li>
<li>多用户 / 多租户隔离</li>
<li>年运维与更新订阅</li>
</ul>
<a href="/login.html" class="landing-btn-primary">开始试用</a>
<a href="#contact" class="landing-btn-primary">获取报价</a>
</div>
<div class="pricing-card">
<div class="pricing-name">企业版</div>
<div class="pricing-price">¥2,499 <span>/</span></div>
<div class="pricing-desc">品牌化内容运营的完整方案</div>
<div class="pricing-name">源码售卖</div>
<div class="pricing-price">面议 <span>/ 授权</span></div>
<div class="pricing-desc">一次性授权,自主迭代可控</div>
<ul class="pricing-features">
<li>不限文章数量</li>
<li>所有平台差异化内容</li>
<li>全维度 SEO + GEO 优化</li>
<li>全平台搜索排名追踪</li>
<li>AI 搜索引用深度分析</li>
<li>数据看板 + 定制报告</li>
<li>专属 LLM 模型配置</li>
<li>API 接入</li>
<li>专属技术支持</li>
<li>完整源代码交付</li>
<li>永久授权 + 年订阅支持</li>
<li>可二次开发 / 集成</li>
<li>含私有化部署包</li>
<li>技术文档 + 部署指导</li>
<li>商务定制谈价</li>
</ul>
<a href="/login.html" class="landing-btn-secondary">联系销售</a>
<a href="#contact" class="landing-btn-secondary">联系销售</a>
</div>
</div>
</div>
@@ -417,6 +441,22 @@
<button class="faq-q">我的数据安全吗?</button>
<div class="faq-a">系统支持多租户隔离(组织级数据隔离),用户只能看到自己组织的数据。部署在你的自有服务器上,数据不出你的网络。支持 JWT 认证、HTTPS、管理员权限分级管理。</div>
</div>
<div class="faq-item">
<button class="faq-q">什么是私有化部署?和 SaaS 有什么区别?</button>
<div class="faq-a">私有化部署把整套平台装在你自己的服务器(或云主机)上,数据完全由你掌控、不出域,可彻底去掉"宇之然"品牌换成你自己的(白标)。相比公有云 SaaS,它更适合对数据合规、品牌自主有要求的 MCN、企业市场部和政务 / 行业机构。</div>
</div>
<div class="faq-item">
<button class="faq-q">可以买源码自己改吗?</button>
<div class="faq-a">可以。源码售卖提供完整源代码(后端 FastAPI + 前端 Vue3 CDN + 部署脚本),一次性授权后你拥有永久使用权,可自主二次开发、接入内部系统,并享受年度订阅技术支持与版本更新。</div>
</div>
<div class="faq-item">
<button class="faq-q">免费试用怎么申请?</button>
<div class="faq-a">无需信用卡。你可以申请私有化试用版(功能完整、限文章数)自行验证,或预约一次 1v1 在线演示,由我们带你走通选题→生成→AI 味检测→合规发布的完整流程。点击页面底部"申请试用"留下联系方式即可。</div>
</div>
<div class="faq-item">
<button class="faq-q">AI 生成的内容合规吗?</button>
<div class="faq-a">合规责任在内容发布方。平台内置"不代发"原则:所有 AI 稿必须经过真人校验、手动标记发布;同时提供 AI 套话检测与软硬问题分级,确保内容可读、可溯源、符合《AI 生成合成内容标识办法》的要求。</div>
</div>
</div>
</div>
</section>
@@ -425,8 +465,83 @@
<section class="cta">
<div class="container" style="position:relative;">
<h2>准备好了吗?</h2>
<p>立即开始使用宇之然,让 AI 为你的内容获取精准流量</p>
<a href="/login.html">免费开始使用</a>
<p>免费试用、私有化部署或源码采购——选一种方式,让 AI 为你的内容获取精准流量</p>
<a href="#contact">获取专属方案</a>
</div>
</section>
<!-- Contact / Trial -->
<section id="contact" class="contact">
<div class="container">
<div style="text-align:center;"><span class="section-label">开始合作</span></div>
<h2 class="section-title">选一种方式,和我们聊聊</h2>
<p class="section-subtitle">无论是免费试用、私有化部署还是源码采购,留下信息我们会在 1 个工作日内联系你</p>
<div class="contact-grid">
<div class="contact-info">
<div class="contact-info-item">
<div class="ci-icon"></div>
<div>
<div class="ci-label">商务邮箱</div>
<div class="ci-value"><a href="mailto:contact@yuzhiran.com">contact@yuzhiran.com</a></div>
</div>
</div>
<div class="contact-info-item">
<div class="ci-icon">💬</div>
<div>
<div class="ci-label">微信 / 商务对接</div>
<div class="ci-value">扫码上方企业微信二维码添加</div>
</div>
</div>
<div class="contact-info-item">
<div class="ci-icon">📦</div>
<div>
<div class="ci-label">交付形式</div>
<div class="ci-value">私有化实例 / 源码授权 / 试用版</div>
</div>
</div>
<div class="contact-info-item">
<div class="ci-icon">🛡</div>
<div>
<div class="ci-label">合规承诺</div>
<div class="ci-value">数据不出域 · 不代发 · 人工闸门</div>
</div>
</div>
<div class="wecom-qr">
<img src="/automation/images/wecom-qr.jpg" alt="企业微信二维码">
<div class="wecom-qr-cap">企业微信扫码添加 · 备注:私有化 / 源码</div>
</div>
</div>
<form class="contact-form" id="trialForm">
<h3>预约咨询 / 申请试用</h3>
<div class="form-row">
<label>姓名 *</label>
<input type="text" name="name" required placeholder="您的称呼">
</div>
<div class="form-row">
<label>公司 / 团队</label>
<input type="text" name="company" placeholder="选填">
</div>
<div class="form-row">
<label>合作方式 *</label>
<select name="need" required>
<option value="免费试用">免费试用(私有化试用版 / 演示)</option>
<option value="私有化部署">私有化部署(买断 + 运维)</option>
<option value="源码售卖">源码售卖(授权 + 支持)</option>
<option value="其他">其他 / 还不确定</option>
</select>
</div>
<div class="form-row">
<label>联系方式 *</label>
<input type="text" name="contact" required placeholder="邮箱 / 微信 / 电话">
</div>
<div class="form-row">
<label>需求说明</label>
<textarea name="remark" placeholder="简单描述您的场景与规模(选填)"></textarea>
</div>
<button type="submit" class="landing-btn-primary">提交,获取方案</button>
<div class="contact-note">提交将唤起邮件客户端发往 <a href="mailto:contact@yuzhiran.com">contact@yuzhiran.com</a></div>
</form>
</div>
</div>
</section>
@@ -447,7 +562,7 @@
<h4>支持</h4>
<a href="#faq">常见问题</a>
<a href="https://github.com" target="_blank">GitHub</a>
<a href="mailto:hello@yu-zhi-ran.com">联系我们</a>
<a href="mailto:contact@yuzhiran.com">联系我们</a>
</div>
<div>
<h4>法律</h4>
@@ -487,6 +602,25 @@
if (!isOpen) item.classList.add('open');
});
});
// Trial / contact form -> mailto
var trialForm = document.getElementById('trialForm');
if (trialForm) {
trialForm.addEventListener('submit', function(e) {
e.preventDefault();
var fd = new FormData(trialForm);
var name = (fd.get('name') || '').toString().trim();
var company = (fd.get('company') || '').toString().trim();
var need = (fd.get('need') || '').toString().trim();
var contact = (fd.get('contact') || '').toString().trim();
var remark = (fd.get('remark') || '').toString().trim();
var subject = '【宇之然】' + need + ' - ' + (name || '客户');
var body = '姓名:' + name + '\n公司/团队:' + (company || '(未填)') +
'\n合作方式:' + need + '\n联系方式:' + contact + '\n需求说明:' + (remark || '(未填)');
window.location.href = 'mailto:contact@yuzhiran.com?subject=' +
encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
});
}
</script>
</body>
</html>
+1 -1
View File
@@ -13,7 +13,7 @@
.stat-card.danger { background: linear-gradient(135deg, #f56c6c 0%, #f78989 100%); }
.stat-value { font-size: 32px; font-weight: 700; }
.stat-label { font-size: 14px; opacity: 0.9; margin-top: 4px; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 24px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; margin-bottom: 24px; }
.chart-container { background: white; border-radius: 12px; padding: 16px; margin-bottom: 24px; height: 320px; position: relative; }
.platform-chart { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
@media (max-width: 768px) {
+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 创作工作台</title>
<link rel="stylesheet" href="element-plus.css">
<link rel="stylesheet" href="theme-modern.css">
<style>
html, body { margin: 0; height: 100%; }
[v-cloak] { display: none; }
#app { display: flex; flex-direction: column; height: 100vh; }
.studio-body { flex: 1; display: flex; flex-direction: column; min-height: 0; padding-top: var(--nav-height); }
.studio-tabs { flex: 0 0 auto; padding: 0 16px; background: var(--color-bg-raised); border-bottom: 1px solid var(--color-border); }
.studio-tabs .el-tabs__header { margin-bottom: 0; }
.studio-iframe-wrap { flex: 1; position: relative; min-height: 0; background: var(--color-bg); }
.studio-iframe-wrap iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: none; }
@media (max-width: 768px) { .studio-tabs { padding: 0 8px; } }
</style>
<script src="uni-nav.js"></script>
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="创作工作台" :username="currentUser.username" :is-admin="isAdmin" current-page="studio" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="studio-body">
<div class="studio-tabs">
<el-tabs v-model="activeTab" @tab-change="onTabChange">
<el-tab-pane label="选题" name="topics"></el-tab-pane>
<el-tab-pane label="内容生成" name="factory"></el-tab-pane>
<el-tab-pane label="文章管理" name="articles"></el-tab-pane>
</el-tabs>
</div>
<div class="studio-iframe-wrap">
<iframe v-if="tabs.topics" src="topics.html?embed=1" frameborder="0"></iframe>
<iframe v-if="tabs.factory" src="factory.html?embed=1" frameborder="0"></iframe>
<iframe v-if="tabs.articles" src="articles.html?embed=1" frameborder="0"></iframe>
</div>
</div>
</div>
<script src="vue.global.prod.js"></script>
<script src="element-plus.js"></script>
<script src="element-plus-icons.js"></script>
<script>
const { createApp } = Vue;
const StudioApp = {
data() {
return {
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
activeTab: 'topics',
tabs: { topics: true, factory: false, articles: false },
};
},
methods: {
onTabChange(name) { this.tabs[name] = true; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
handleLogout() {
localStorage.removeItem('authToken');
localStorage.removeItem('userRole');
localStorage.removeItem('currentUser');
window.location.href = '/login.html';
},
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => { this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true; })
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
}
};
const app = Vue.createApp(StudioApp);
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
<script src="ai-assistant.js"></script>
</body>
</html>
+12 -5
View File
@@ -68,7 +68,10 @@
--content-max-width: 1400px;
}
/* ========== Dark Mode ========== */
/* ========== Dark Mode ==========
注意Element Plus 组件库为亮色主题未同步切换会导致弹窗/卡片内
自定义文字在深色变量下不可读当前明确采用亮色主题以保证对比度
待引入 EP 暗色变量(dark theme vars)后再启用完整暗色模式
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1a1a2e;
@@ -76,12 +79,10 @@
--color-bg-subtle: #1a1a3e;
--color-border: #2a2a4e;
--color-border-hover: #4a4a6e;
--color-text-primary: #e0e6ed;
--color-text-regular: #a0a6b0;
--color-text-secondary: #6b7280;
--color-text-placeholder: #4a4a5e;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
--shadow-md: 0 2px 12px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 28px rgba(0,0,0,0.4);
@@ -90,6 +91,7 @@
.card { background: var(--color-bg-raised); }
.el-table { --el-table-tr-bg: var(--color-bg-raised); --el-table-header-bg: var(--color-bg-subtle); }
}
*/
/* ========== Reset & Base ========== */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
@@ -119,6 +121,12 @@ body {
margin: 0 auto;
min-height: calc(100vh - var(--nav-height));
}
/* 嵌入模式(被创作工作台 iframe 加载时):去掉顶部导航留白 */
body.embed .main-content {
padding-top: 0 !important;
min-height: 100vh;
}
.content-area { flex: 1; padding: var(--spacing-md); overflow-y: auto; }
/* ========== Card ========== */
@@ -128,7 +136,6 @@ body {
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
box-shadow: var(--shadow-md);
overflow-x: auto;
transition: all 0.3s var(--ease-out);
}
@@ -405,7 +412,7 @@ body:has(.preview-dialog-custom.is-fullscreen) > [class*="el-overlay"] { z-index
}
@media (min-width: 769px) {
.preview-iframe { min-height: 400px; }
.preview-dialog-custom { position: relative; left: 90px; }
.preview-dialog-custom { position: relative; }
}
@media (max-width: 480px) {
+86 -21
View File
@@ -33,7 +33,7 @@
</head>
<body>
<div id="app" v-cloak>
<uni-nav title="选题管理" :username="currentUser.username" :is-admin="isAdmin" current-page="topics" @navigate="redirectToPage" @logout="handleLogout">
<uni-nav v-if="!isEmbed" title="选题管理" :username="currentUser.username" :is-admin="isAdmin" current-page="topics" @navigate="redirectToPage" @logout="handleLogout">
</uni-nav>
<div class="main-content">
<main class="content-area">
@@ -56,6 +56,7 @@
<el-button size="default" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'; fetchTopics()">待审查 ({{ statusStats.review }})</el-button>
<el-button size="default" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'; fetchTopics()">待发布 ({{ statusStats.ready }})</el-button>
<el-button size="default" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'; fetchTopics()">已发布 ({{ statusStats.published }})</el-button>
<el-button size="default" :type="filterStatus === 'pending_publish' ? 'primary' : ''" @click="filterStatus = 'pending_publish'; fetchTopics()">待人工发布 ({{ statusStats.pendingPublish }})</el-button>
</div>
<div style="margin-bottom:10px;">
<span class="search-toggle" @click="showSearch = !showSearch">🔍 {{ showSearch ? '收起搜索' : '展开搜索' }}</span>
@@ -64,29 +65,30 @@
<div v-if="showSearch" class="search-area">
<el-form :model="searchForm" size="small" label-width="70px" @submit.prevent>
<el-row :gutter="12">
<el-col :span="6"><el-form-item label="选题ID"><el-input v-model="searchForm.id" placeholder="如 A07" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :span="10"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="关键词" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :span="8"><el-form-item label="领域"><el-input v-model="searchForm.field" placeholder="如 科技前沿" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="12" :md="6"><el-form-item label="选题ID"><el-input v-model="searchForm.id" placeholder="如 A07" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="12" :md="8"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="关键词" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="12" :md="5"><el-form-item label="领域"><el-input v-model="searchForm.field" placeholder="如 科技前沿" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :xs="24" :sm="12" :md="5"><el-form-item label="栏目"><el-select v-model="searchForm.series" clearable placeholder="全部" style="width:100%;" @change="doSearch"><el-option v-for="s in seriesOptions" :key="s" :label="s" :value="s"></el-option></el-select></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="创建时间">
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="创建时间">
<el-date-picker v-model="searchForm.createdStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
<span style="margin:0 4px;color:#909399;">~</span>
<el-date-picker v-model="searchForm.createdEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
</el-form-item></el-col>
<el-col :span="12"><el-form-item label="创作时间">
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="创作时间">
<el-date-picker v-model="searchForm.generatedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
<span style="margin:0 4px;color:#909399;">~</span>
<el-date-picker v-model="searchForm.generatedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
</el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12"><el-form-item label="审查时间">
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="审查时间">
<el-date-picker v-model="searchForm.reviewedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
<span style="margin:0 4px;color:#909399;">~</span>
<el-date-picker v-model="searchForm.reviewedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
</el-form-item></el-col>
<el-col :span="12"><el-form-item label="发布时间">
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="发布时间">
<el-date-picker v-model="searchForm.publishedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
<span style="margin:0 4px;color:#909399;">~</span>
<el-date-picker v-model="searchForm.publishedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
@@ -108,6 +110,17 @@
<el-table-column prop="status" label="状态" width="90">
<template #default="scope"><span class="status-badge"><span class="status-dot" :class="scope.row.status"></span>{{ getStatusLabel(scope.row.status) }}</span></template>
</el-table-column>
<el-table-column label="文章" width="140">
<template #default="scope">
<div v-if="scope.row.article_count > 0" style="display:flex;gap:4px;flex-wrap:wrap;">
<el-tag v-for="a in (scope.row.articles || [])" :key="a.platform" :type="a.status === 'published' ? 'success' : 'warning'" size="small" style="cursor:pointer;" @click="openTopicArticlePreview(scope.row, a)">
{{ platformShort(a.platform) }}
<span v-if="a.word_count" style="margin-left:2px;opacity:0.7;">{{ fmtWordCount(a.word_count) }}</span>
</el-tag>
</div>
<span v-else style="color:#c0c4cc;font-size:12px;"></span>
</template>
</el-table-column>
<el-table-column prop="compliance_score" label="合规分" width="90">
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
</el-table-column>
@@ -122,6 +135,8 @@
<el-button size="small" type="success" :disabled="isStatus(scope.row, 'published')" @click="createTopic(scope.row)" style="padding:5px 8px;">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="reviewTopic(scope.row)" style="padding:5px 8px;">审查</el-button>
<el-button v-if="isStatus(scope.row, 'ready')" size="small" type="primary" @click="openPublishDialog(scope.row)" style="padding:5px 8px;">发布</el-button>
<el-button v-if="isStatus(scope.row, 'pending_publish')" size="small" type="success" @click="markPublished(scope.row)" style="padding:5px 8px;">标记发布</el-button>
<el-button size="small" :disabled="(scope.row.article_count || 0) === 0" @click="openSlop(scope.row)" style="padding:5px 8px;">检测</el-button>
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)" style="padding:5px 8px;">删除</el-button>
</div>
</template>
@@ -144,6 +159,10 @@
<div class="topic-card-tags">
<el-tag size="small" type="info">{{ topic.field }}</el-tag>
<el-tag size="small" type="warning">合规{{ topic.compliance_score }}</el-tag>
<el-tag v-for="a in (topic.articles || [])" :key="a.platform" size="small" :type="a.status === 'published' ? 'success' : 'warning'" style="cursor:pointer;" @click="openTopicArticlePreview(topic, a)">
{{ platformShort(a.platform) }}{{ a.word_count ? ' ' + fmtWordCount(a.word_count) : '' }}
</el-tag>
<el-tag v-if="topic.article_count === 0" size="small" type="info" effect="plain">无文章</el-tag>
</div>
<div class="topic-card-meta">
<div>创建: {{ formatDate(topic.created_at) }}</div>
@@ -156,6 +175,8 @@
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @click="createTopic(topic)">创作</el-button>
<el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="reviewTopic(topic)">审查</el-button>
<el-button v-show="isStatus(topic, 'ready')" size="small" type="primary" @click="openPublishDialog(topic)">发布</el-button>
<el-button v-if="isStatus(topic, 'pending_publish')" size="small" type="success" @click="markPublished(topic)">标记发布</el-button>
<el-button size="small" :disabled="(topic.article_count || 0) === 0" @click="openSlop(topic)">检测</el-button>
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
</div>
</div>
@@ -240,6 +261,10 @@
</template>
</el-dialog>
<el-dialog v-model="publishDialogVisible" title="发布确认" width="420px" :close-on-click-modal="false">
<el-alert type="warning" :closable="false" show-icon style="margin-bottom:16px;">
<template #title>AI 内容需真人校验后发布</template>
系统仅生成各平台发布稿并置为「待人工发布」,<b>不代发</b>。请在对应平台粘贴、核对 AI 声明与合规后手动发布,再回来点「标记已发布」。
</el-alert>
<div v-if="publishTopic">
<div style="margin-bottom:16px;">
<div style="font-size:14px;color:#606266;margin-bottom:8px;">选题:</div>
@@ -320,6 +345,7 @@
const TopicsApp = {
data() {
return {
isEmbed: false,
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
loadingTable: false, selectedTopicIds: [], filterStatus: '',
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
@@ -333,7 +359,7 @@ const TopicsApp = {
currentPage: 1, pageSize: 10,
sortField: 'created_at', sortOrder: 'ascending',
showSearch: false,
searchForm: { id: '', title: '', field: '', createdStart: null, createdEnd: null, generatedStart: null, generatedEnd: null, reviewedStart: null, reviewedEnd: null, publishedStart: null, publishedEnd: null },
searchForm: { id: '', title: '', field: '', series: '', createdStart: null, createdEnd: null, generatedStart: null, generatedEnd: null, reviewedStart: null, reviewedEnd: null, publishedStart: null, publishedEnd: null },
createDialogVisible: false, createStep: 1, analyzing: false, saving: false,
fieldOptions: [],
createForm: { raw_input: '', reference_links: '', field_id: null },
@@ -341,26 +367,32 @@ const TopicsApp = {
}
},
computed: {
seriesOptions() {
const s = new Set();
for (const t of this.topics) { if (t.series) s.add(t.series); }
return [...s].sort();
},
searchActive() {
const f = this.searchForm;
return !!(f.id || f.title || f.field || f.createdStart || f.createdEnd || f.generatedStart || f.generatedEnd || f.reviewedStart || f.reviewedEnd || f.publishedStart || f.publishedEnd);
return !!(f.id || f.title || f.field || f.series || f.createdStart || f.createdEnd || f.generatedStart || f.generatedEnd || f.reviewedStart || f.reviewedEnd || f.publishedStart || f.publishedEnd);
},
filteredTopics() {
let list;
if (this.filterStatus === 'today') list = this.todayTopics;
else if (!this.filterStatus) list = this.topics;
else {
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] };
const allowed = map[this.filterStatus] || [this.filterStatus];
list = this.topics.filter(t => allowed.includes(t.status));
}
// 搜索过滤
const sf = this.searchForm;
if (sf.id || sf.title || sf.field || sf.createdStart || sf.createdEnd || sf.generatedStart || sf.generatedEnd || sf.reviewedStart || sf.reviewedEnd || sf.publishedStart || sf.publishedEnd) {
if (sf.id || sf.title || sf.field || sf.series || sf.createdStart || sf.createdEnd || sf.generatedStart || sf.generatedEnd || sf.reviewedStart || sf.reviewedEnd || sf.publishedStart || sf.publishedEnd) {
list = list.filter(t => {
if (sf.id && !t.id.toLowerCase().includes(sf.id.toLowerCase())) return false;
if (sf.title && !t.title.toLowerCase().includes(sf.title.toLowerCase())) return false;
if (sf.field && !(t.field || '').toLowerCase().includes(sf.field.toLowerCase())) return false;
if (sf.series && (t.series || '') !== sf.series) return false;
const inRange = (val, start, end) => {
if (!val) return !start && !end;
const d = val.slice(0, 10);
@@ -400,8 +432,8 @@ const TopicsApp = {
return this.filteredTopics.slice(start, start + this.pageSize);
},
statusStats() {
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0 };
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0, pendingPublish: 0 };
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] };
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { s[k]++; break; } } });
return s;
},
@@ -422,8 +454,7 @@ const TopicsApp = {
},
currentPreviewBody() {
const html = this.platformContents[this.previewPlatform];
if (!html) return '';
if (html.length < 50) return '<div style="padding:40px;text-align:center;color:#909399;"><p style="font-size:36px;margin:0 0 12px;">📝</p><p>该平台内容暂未生成或内容为空</p><p style="font-size:13px;margin-top:8px;">可点击上方「批量创作」重新生成,或在编辑模式下手动添加内容</p></div>';
if (!html || html.length < 50) return '<div style="padding:40px;text-align:center;color:#909399;"><p style="font-size:36px;margin:0 0 12px;">📝</p><p>该平台内容暂未生成或内容为空</p><p style="font-size:13px;margin-top:8px;">可点击上方「批量创作」重新生成,或在编辑模式下手动添加内容</p></div>';
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
@@ -527,6 +558,24 @@ const TopicsApp = {
});
},
platformName(platform) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[platform] || platform; },
platformShort(platform) { return { zhihu: '知', wechat: '公', xiaohongshu: '红' }[platform] || platform; },
fmtWordCount(wc) { if (!wc) return ''; if (wc >= 1000) return (wc / 1000).toFixed(wc >= 10000 ? 0 : 1) + 'k'; return wc + ''; },
async openTopicArticlePreview(topic, article) {
this.previewTopic = topic;
this.previewPlatform = article.platform;
this.previewVisible = true;
this.platformContents = {};
this.previewImages = {};
const token = this.getToken();
if (!token) return;
try {
const r = await fetch(`/api/articles/${topic.id}/preview?platform=${article.platform}`, { headers: { 'Authorization': 'Bearer ' + token } });
if (r.ok) {
const d = await r.json();
if (d && d.html) { this.platformContents[article.platform] = d.html; if (d.images) this.previewImages[article.platform] = d.images; }
}
} catch (e) { console.error(`加载${article.platform}预览失败:`, e); }
},
copyContent(platform) {
const html = this.platformContents[platform];
if (!html) { this.$message.warning('暂无内容可复制'); return; }
@@ -619,13 +668,28 @@ const TopicsApp = {
method: 'POST',
body: JSON.stringify({ topic_id: this.publishTopic.id, platforms: selected })
});
this.$message.success(`发布完成: ${this.publishTopic.title}`);
this.$message.success(data.message || `已生成发布稿,请在各平台校验后手动发布`);
this.publishDialogVisible = false;
await this.fetchTopics();
await this.fetchTodayCount();
} catch (error) { this.$message.error(`发布失败: ${error.message}`); }
finally { this.publishing = false; }
},
openSlop(topic) {
window.open('ai-slop.html?topic_id=' + encodeURIComponent(topic.id), '_blank');
},
async markPublished(topic) {
try {
await this.$confirm('确认已在各平台完成发布?此操作将把选题标记为「已发布」并关闭发布记录。', '标记发布', { type: 'warning' });
await this.api('/api/publishing/' + encodeURIComponent(topic.id) + '/mark-published', { method: 'POST' });
this.$message.success('已标记为已发布');
await this.fetchTopics();
} catch (e) {
if (e === 'cancel') return;
const msg = e?.response?.data?.detail || e?.message || '未知错误';
this.$message.error('操作失败: ' + msg);
}
},
async deleteTopic(id) {
try {
await this.$confirm('确定删除选题 ' + id + '?将同时删除相关文章、发布记录等关联数据。', '提示', { type: 'warning' });
@@ -767,8 +831,8 @@ const TopicsApp = {
this.currentPage = 1;
},
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
getStatusLabel(status) { return { 'pending': '待处理', 'review': '待审查', 'ready': '待发布', 'published': '已发布' }[status] || status; },
redirectToPage(page) { const url = page.startsWith('/') ? page : '/' + page; if (this.isEmbed) window.top.location.href = url; else window.location.href = url; },
getStatusLabel(status) { return { 'pending': '待处理', 'review': '待审查', 'ready': '待发布', 'published': '已发布', 'pending_publish': '待人工发布' }[status] || status; },
toggleCheck(id) {
const idx = this.selectedTopicIds.indexOf(id);
if (idx >= 0) {
@@ -796,12 +860,13 @@ const TopicsApp = {
try { return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
catch (e) { return dateStr; }
},
getStatusType(status) { return { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info' }[status] || 'primary'; },
isStatus(row, status) { const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] }; return map[status] ? map[status].includes(row.status) : row.status === status; }
getStatusType(status) { return { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info', 'pending_publish': 'warning' }[status] || 'primary'; },
isStatus(row, status) { const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] }; return map[status] ? map[status].includes(row.status) : row.status === status; }
},
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
if (new URLSearchParams(window.location.search).get('embed') === '1') { this.isEmbed = true; document.body.classList.add('embed'); }
const params = new URLSearchParams(window.location.search);
const urlFilter = params.get('filter');
if (urlFilter) this.filterStatus = urlFilter;
+48 -15
View File
@@ -107,7 +107,7 @@
.uni-nav-dropdown-divider {\
height: 1px; background: #f0f0f0; margin: 4px 0;\
}\
@media (max-width: 768px) {\
@media (max-width: 1000px) {\
.uni-nav-items { display: none; }\
.uni-nav-username { display: none; }\
.uni-nav-hamburger { display: inline-flex; }\
@@ -121,20 +121,24 @@
function getCurrentPage() {
var path = window.location.pathname;
if (path === '/index.html') return 'workspace';
var m = path.match(/\/(\w+)\.html/);
return m ? m[1] : 'workspace';
if (path === '/' || path === '/index.html') return 'index.html';
var m = path.match(/\/([\w-]+)\.html$/);
return m ? m[1] + '.html' : 'index.html';
}
function navItems(isAdmin) {
var items = [
{ key: 'workspace', label: '工作台', page: 'index.html' },
{ key: 'factory', label: '内容工厂', page: 'factory.html' },
{ key: 'insights', label: '数据洞察', page: 'insights.html' },
{ key: 'assets', label: '资产库', page: 'assets.html' },
{ key: 'dashboard', label: '仪表盘', page: 'index.html' },
{ key: 'studio', label: '创作工作台', page: 'studio.html' },
{ key: 'insights', label: '数据', page: 'metrics.html' },
{ key: 'calendar', label: '日历', page: 'calendar.html' },
{ key: 'assets', label: '素材', page: 'assets.html' },
{ key: 'tasks', label: '任务', page: 'tasks.html' },
{ key: 'campaigns', label: '外推营销', page: 'campaigns.html' },
{ key: 'ai-slop', label: 'AI味检测', page: 'ai-slop.html' },
];
if (isAdmin) {
items.push({ key: 'branding', label: '白标设置', page: 'branding.html', admin: true });
items.push({ key: 'admin', label: '系统管理', page: 'admin.html', admin: true });
}
return items;
@@ -160,6 +164,9 @@
pwdForm: { old_password: '', new_password: '', confirm: '' },
pwdSubmitting: false,
serverMenus: null,
brandName: '',
brandLogo: '',
primaryColor: '',
};
},
computed: {
@@ -167,15 +174,19 @@
if (this.serverMenus) {
return this.serverMenus
.filter(function (m) { return m.is_active !== false; })
.map(function (m) { return { key: m.name, label: m.name, page: m.path }; });
.map(function (m) { return { key: m.name, label: m.name, page: (m.path || '').replace(/^\//, '') }; });
}
return navItems(this.isAdmin);
},
page: function () {
return this.currentPage || getCurrentPage();
return getCurrentPage();
},
showTitle: function () {
return this.title || '宇之然';
return this.brandName || this.title || '宇之然';
},
navStyle: function () {
if (!this.primaryColor) return {};
return { background: 'linear-gradient(135deg, ' + this.primaryColor + ', ' + this.primaryColor + 'cc)' };
},
},
methods: {
@@ -189,6 +200,24 @@
}.bind(this))
.catch(function () {});
},
fetchBranding: function () {
fetch('/api/branding')
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
.then(function (d) {
if (d.brand_name) this.brandName = d.brand_name;
if (d.brand_logo) this.brandLogo = d.brand_logo;
if (d.brand_primary_color) {
this.primaryColor = d.brand_primary_color;
document.documentElement.style.setProperty('--color-primary', d.brand_primary_color);
var s = document.getElementById('uni-nav-brand-style');
if (!s) { s = document.createElement('style'); s.id = 'uni-nav-brand-style'; document.head.appendChild(s); }
s.textContent = '.uni-nav-dropdown-item:hover,.uni-nav-dropdown-item.active{color:' + d.brand_primary_color + ';}' +
'.uni-nav-dropdown-item.active{background:' + d.brand_primary_color + '14;}' +
'.uni-nav-item.active{background:rgba(255,255,255,0.18);}';
}
}.bind(this))
.catch(function () {});
},
navigate: function (page) {
this.dropdownOpen = false; this.profileOpen = false;
if (this.onNavigate) { this.onNavigate(page); return; }
@@ -246,17 +275,21 @@
mounted: function () {
document.addEventListener('click', this.closeAll);
this.fetchMenus();
this.fetchBranding();
},
beforeUnmount: function () {
document.removeEventListener('click', this.closeAll);
},
template: '\
<nav class="uni-nav" style="position:relative;">\
<nav class="uni-nav" :style="navStyle">\
<div class="uni-nav-inner">\
<div class="uni-nav-brand">{{ showTitle }}</div>\
<div class="uni-nav-brand">\
<img v-if="brandLogo" :src="brandLogo" style="height:24px;width:auto;margin-right:6px;vertical-align:middle;border-radius:4px;" />\
<span>{{ showTitle }}</span>\
</div>\
<div class="uni-nav-items">\
<button v-for="item in items" :key="item.key"\
:class="[\'uni-nav-item\', { active: page === item.key }]"\
:class="['uni-nav-item', { active: page === item.page }]"\
@click="navigate(item.page)">{{ item.label }}</button>\
</div>\
<div class="uni-nav-right">\
@@ -281,7 +314,7 @@
</div>\
<div :class="[\'uni-nav-dropdown\', { open: dropdownOpen }]" @click.stop>\
<button v-for="item in items" :key="item.key"\
:class="[\'uni-nav-dropdown-item\', { active: page === item.key }]"\
:class="[\'uni-nav-dropdown-item\', { active: page === item.page }]"\
@click="navigate(item.page)">\
{{ item.label }}\
</button>\
+157
View File
@@ -0,0 +1,157 @@
# 宇之然 (YuZhiRan) 市场与竞争调研简报 — 2026
> 调研范围:中国 AI 内容创作工具格局、平台算法/政策、GEO/AI搜索、创作者变现痛点、未满足蓝海
> 方法:web_search 深度检索(2025–2026 公开报道、平台公告、行业白皮书、创作者社区帖)
> 注意:部分数据为行业机构/营销机构口径(GEO 服务商软文偏多),已在文中标注"存疑"。
---
## 一、关键趋势 (Key Trends)
1. **"AI 辅助"被鼓励,"AI 托管/批量"被严打** — 2026 年所有主流平台完成从"自愿声明"到"强制标识 + 全链路管控"的切换。9 月 1 日《人工智能生成合成内容标识办法》实施后,小红书、微信、抖音、快手、B站、视频号均上线 AI 内容主动声明/强制打标。微信 4 月新版《公众账号行为规范》明确:禁止用 AI/脚本/接口替代真人全流程运营,违者限流/删文/封号。
2. **"人味/原创增量"成为新的流量分配核心指标** — 平台不再简单打击"AI 率",而是打击"无人工痕迹+同质化+低价值增量"的内容。抖音把"原创识别率"纳入核心分发指标;重复率超 25% 即判低质。
3. **GEO(生成式引擎优化)从概念走向服务市场** — 豆包/文心/通义/DeepSeek/Kimi 已成决策入口。出现大量 GEO 服务商(Bugoo 布谷、余小铁、上海逐天、GEORANK 等),但行业鱼龙混杂、软文泛滥。
4. **一人公司 / Agent 化个体创业爆发** — 2026 被视为"一人公司狂飙"年,AI Agent 工作流(Dify/Coze/n8n/OpenClaw)使个体具备团队能力。
5. **"副业刚需"成为内容创作的核心驱动力** — 全球年轻人"多职劳动(Multiple Jobs)"激增,中国 26-35 岁焦虑青年把自媒体当"退路"与"睡后收入"来源。
6. **长文→短视频的转化工具成熟但割裂** — 即梦/可灵/剪映/即创打通"脚本→分镜→成片",但多为独立工具,少有"一篇文章自动拆成三平台+一个短视频"的一体化方案。
7. **本地生活 + 私域成为小红书/视频号新红利** — 小红书变"内容+交易"闭环,搜索流量(DSO)是低成本获客洼地,强调真实、就近、素人。
---
## 二、创作者痛点 (Top Pain Points, Ranked)
> 按"普遍度 × 严重程度 × 与宇之然定位的相关度"排序
1. **【最痛】AI 内容被限流/封号,且规则不清** — "写了 3 个月原创阅读不过千""AI 率 50%+ 被限流""小红书关于 AI 限流的笔记超 3 万篇"。平台边界模糊(哪些是 AI 托管、哪些算辅助),创作者"不知道红线在哪"。→ 宇之然的"合规检测/去套话/人味优化 + 三平台差异化"正好命中。
2. **同质化与"AI 套话"** — 同模型+同 prompt 产出高度雷同;平台同质化识别已迭代数轮。"同质化比 AI 检测更要命"。常见 AI 句式("首先/其次/最后""值得一提的是")被普遍反感。
3. **多平台分发但要"说人话且各说各话"** — 知乎/微信/小红书调性、字数、人称完全不同,手动改三遍成本极高。绝大多数工具只给"一篇通用稿",或只做简单改写。→ 宇之然"三平台独立展开"是差异化卖点。
4. **选题焦虑 + 流量焦虑** — 红海赛道拥挤、冷启动难、爆款不可复制。需要"有数据价值但竞争者少的细分方向"。
5. **变现难 + 账号权重脆弱** — 流量主/广告/知识付费门槛高;一个违规号可能前功尽弃。
6. **合规/敏感词/版权风险** — 《标识办法》+ 平台敏感词 + 版权模糊(AI 生成图/声音侵权案例频出,如配音演员维权)。
7. **长文→短视频的转化断层** — 公众号/知乎沉淀的长文,想转抖音/视频号缺少顺滑工作流;现有工具(即梦/剪映)需手动搬运。
8. **GEO/SEO 可见性失控** — 品牌/个人希望被豆包/DeepSeek 引用,但不知道"怎么喂、喂给谁、是否被引",缺乏可监测手段。
---
## 三、竞品动向 (Competitor Moves)
### AI 写作/创作工具(通用层,已饱和)
- **豆包(字节)**:月活 5.3 亿(2026.3 披露),日均 112 亿次对话;完全免费、多模态、生态闭环(抖音/头条/即梦)。2026 下半年可能启动付费。
- **DeepSeek**:月活 1.27 亿,中文写作性价比之王,开源,被引用逻辑偏"推理+技术链"。
- **Kimi(月之暗面)**:超长上下文(200 万字),长文档/资料整理最强;UGC/知乎信源占比高(约 70% 来自 UGC)。
- **通义千问(阿里)**:商务/文档/电商,引用偏权威媒体+阿里生态。
- **文心一言/文小言(百度)**:搜索基因,引用偏百度系(百家号/百科)+ 媒体软文。
- **秘塔写作猫**:中文纠错/润色/降重老牌,千万级用户,免费策略。
- **笔灵 AI**:模板化营销文案,批量标准化。
- **腾讯元宝**:微信生态闭环,公众号文章占信源约 50%。
### AI 视频/多模态(增长快,但非宇之然核心)
- 即梦(字节, Seedance 2.0)、可灵(快手)、LibTV、万镜一刻、剪映一键成片、即创(抖音带货)、腾讯智影、Runway/Sora(海外)。
- 趋势:文生视频从"玩具"变"生产力",但排队/成本/真人肖像限制仍痛。
### Agent/工作流平台(宇之然的间接竞品)
- **扣子 Coze(字节)**:运营人用得最多,抖音/小红书爆款生成工作流,Coze+DeepSeek/飞书联动。
- **Dify / n8n / OpenClaw**:个体创业者搭自动化内容生产线("一行指令出稿出图推送")。
### GEO 服务商(新兴市场,混乱)
- Bugoo 布谷、余小铁 GEO、上海逐天科技、GEORANK、熊猫出海 GEO、白杨 SEO 等。
- 打法:媒体发稿 + 自媒体宣发 + 信源投喂 + 引用率监测。报价中高,交付质量参差。
- **洞察**:GEO 目前高度"服务化/人力化",尚无标准化 SaaS;这正是宇之然可切入的"外部站 SEO/GEO"模块机会。
### 平台自身(既是合作方也是裁判)
- 小红书"点点"AI、微信元宝、抖音即梦——平台把 AI 工具内嵌进生态,目的是"把创作者绑在自己闭环里",同时严打跨平台批量搬运。
---
## 四、平台算法/政策 2025–2026 变化(要点)
| 平台 | 关键变化 | 对创作者的含义 |
|---|---|---|
| 全网 | 《AI生成合成内容标识办法》9/1 实施;显式+隐式标识,不得删除/篡改 | AI 内容必须可识别;标注≠免罚 |
| 小红书 | 强制 AI 标识(2025.8);封禁纯 AI 托管账号;"2万篇笔记种草公式"失效;转向"以人为本/超级小内容" | 批量素人宣发死亡;要真实、小需求、场景化 |
| 微信/公众号 | 4 月新规:禁 AI 全流程替代真人;限流/删文/封号 | AI 只能辅助选题/润色/查错,真人表达不可替代 |
| 抖音/快手 | "原创识别率"入核心指标;重复率>25% 判低质;禁批量同质化 | 原创增量是分发关键 |
| 知乎 | "知乎直答"存在感弱;Kimi/DeepSeek 大量引用知乎内容 | 知乎长文对 GEO 权重高(被 Kimi 引用 70% |
| 视频号 | 严打非真人出镜 AI 视频;限推荐/私信/评论 | 虚构真人/假图高风险 |
| 网文(晋江/起点/番茄) | 反 AI:起点/番茄禁 AI,晋江限边界;部分甲方拉黑用 AI 大纲者 | 虚构创作领域对 AI 最不友好 |
**"原创"的新定义**:不再是"查重率<X%",而是"有独特观点/可验证数据/真实案例/价值增量"。单纯换同义词、调语序仍被判低价值。平台建议 AI 辅助占比 ≤25–30%,每百字至少 1 个情绪钩子。
---
## 五、GEO / AI 搜索优化 2026
- **已成真,但是 B 端(企业)先行,C 端(个人创作者)滞后**。企业级 GEO 需求明确("不被 AI 引用=在 AI 世界失语"),服务商众多;个人创作者大多还停留在"SEO/发小红书"阶段。
- **中文术语认知**:GEO(生成式引擎优化)一词已被采用,但大众仍混用"AI搜索优化/AISEO"。 awareness 在营销圈高,在普通自媒体人里中等。
- **是否可 monetize**:对企业已是刚需(获客/品牌);对个人创作者尚处"锦上添花",但"被豆包/DeepSeek 提到"正成为个人 IP 的新护城河。
- **各平台引用逻辑差异(关键,决定 GEO 打法)**
- 豆包:字节生态优先(抖音/头条/抖音百科),外部补新闻媒;重生活化。
- DeepSeek:媒体+UGC(知乎类)为主,重推理/技术链。
- Kimi~70% UGC,知乎占比极高 → **知乎长文对 GEO 权重被低估**
- 文心/百度:百度系(百家号/百科)+媒体软文。
- 元宝/微信:公众号文章占 ~50% 信源。
- 通义:新闻门户(网易/腾讯/搜狐)+阿里生态。
- **实操共识**:结构化内容、跨平台信息一致、原创数据/白皮书、权威媒体分发、持续"投喂"。
- **数据存疑**GEO 服务商宣称"2026 Q1 中国 GEO 市场 480 亿、同比 11 倍""超 75% 用户决策首选 AI"等数字来自 IDC/艾瑞转引的软文,**未独立核实,谨慎引用**。
---
## 六、变现痛点 & 焦虑青年(26-35)画像
- **副业成刚需**:全球"多职劳动"激增(美国 5.3% 劳动力身兼数职,连续 21 个月>5%;Z 世代 70% 想保持≥2 收入源)。国内因 AI 失业焦虑+生活成本,副业从"兴趣"变"生存手段"。
- **26-35 岁核心恐惧**:35 岁职场危机、被 AI 替代、房贷/教育/医疗刚性支出、失业即断流。
- **他们要的内容**:① 可落地的副业/一人公司方法论;② AI 工具实操("用 AI 当外挂不被淘汰");③ 个人 IP 从 0 到 1;④ 健康/抗倦怠/数字排毒("职业倦怠高峰提前到 25 岁");⑤ 数字游民/地理套利;⑥ 可持续生活/智性消费(B站《2026 年轻人消费趋势报告》:"智性沸腾",不为标签为自我价值付费)。
- **内容调性偏好**:情绪价值、真实体验、具体数据、反套路、去 AI 味——与宇之然"三平台差异化+去套话"高度契合。
---
## 七、未满足的蓝海 (Underserved Niches)
> 综合"需求明确 × 现有 AI 工具少 × 与宇之然能力可衔接"
1. **【最强】三平台"真·差异化"自动生成** — 绝大多数工具只给通用稿或简单改写;能按知乎(数据分析/你)、微信(个人叙事/我)、小红书(干货/emoji)各自独立成稿且合规检测的产品稀缺。宇之然已具备,是核心壁垒。
2. **【强】GEO 化的内容生产(个人/中小品牌版)** — 企业 GEO 被服务商瓜分,但"帮个人创作者/小店产出能被豆包/DeepSeek 引用的结构化内容"尚无 SaaS。可把"外部站 SEO/GEO"做成标准模块。
3. **【强】长文→短视频脚本自动拆条** — 公众号/知乎长文一键生成抖音/视频号口播脚本+分镜+标题,目前需手动跨即梦/剪映。一体化缺口明显。
4. **本地生活商家内容** — 小红书/视频号"搜索流量+POI+素人"红利大,但中小门店缺内容生产能力;模板化"真实到店/本地避坑/套餐推荐"批量生成工具少。
5. **企业号/员工 KOS 内容** — "门店号+员工号双号运营"需求上升,但企业级多账号合规内容工厂稀缺。
6. **知识付费 IP 的内容中台** — 从选题→大纲→三平台→私域钩子→课程化,缺一站式。
7. **私域转化内容** — 公域内容如何带私域钩子(企微/社群话术),现有工具基本不碰。
8. **出海/跨境内容** — 小红书 2026 推跨境电商;英文/多语种内容生成+本地化是空白,且宇之然现有架构易扩展。
9. **合规/去标识安全层** — 随着《标识办法》落地,"发布前 AI 率+同质化+敏感词+暗限流预测"的检测与改写,会成为所有 AI 写作工具的标配前置环节(ContentAny 类已起量,但嵌入生成流的少)。
---
## 八、对宇之然的策略含义(一句话)
宇之然的"三平台差异化生成 + 去套话/合规检测 + 外部站 GEO + 长文转短视频"组合,正好卡在 2026 年最痛的三条裂缝上:**限流焦虑、同质化、跨平台+跨形态分发成本**。优先把"三平台真差异化+合规护城河"做成可感知的差异化卖点,再叠加 GEO 与短视频拆条作为第二增长曲线。
---
## 九、信息来源 (Sources)
- 互联网平台现状:鼓励AI,限制AI36氪/新莓)https://m.36kr.com/p/3259734457430277
- 小红书上线 AI 内容强制标识(网易/见实)https://www.163.com/dy/article/K86VAJIQ053144S4.html
- 从 AI 监管到内容生态重构:平台重新定价"真实"36氪/DoNewshttps://m.36kr.com/p/3765102752252424
- 警惕"AI托管"侵蚀内容生态(中国经济网/证券日报)http://m.ce.cn/bwzg/202603/t20260316_2829584.shtml
- 2026 AI 写作工具实测(AI-Pickhttps://ai-pick.cn/article-42.html
- 2026 AI 写作工具推荐 10 款(AI工具宝箱)https://www.aitoollab.cn/articles/ai-writing-tools-2026-guide
- Kimi、文心启动付费(澎湃)https://www.thepaper.cn/newsDetail_forward_33101613
- 2026 一人公司狂飙:AI Agent 重构个体创业(掘金)https://juejin.cn/post/7611761768110817332
- GEO2026 ChatGPT/DeepSeek/豆包等 AI 搜索优化(CSDN/DeepSeek 社区)https://deepseek.csdn.net/6a420c80662f9a54cb858452.html
- AI 搜索推荐机制拆解:豆包/通义/文心/DeepSeek 引用逻辑(CSDNhttps://blog.csdn.net/yunFAA667/article/details/159512346
- 豆包 AI 搜索优化 GEO 指南(cnblogs/GEORANKhttps://www.cnblogs.com/georank/p/20680805
- 上海 GEO 服务商评测(cnblogshttps://www.cnblogs.com/zwcxkj/articles/20039051
- DeepSeek/豆包/Kimi/通义 引用来源占比(白杨 SEO/淘江湖)https://jianghu.taobao.com/detail/47301_29034882
- 自媒体变现指南(U客直谈)https://www.ukezhitan.com/article-u8D5Au94B1u8D44u8BAF/12316
- 2026 小红书本地生活商家趋势(cnblogshttps://www.cnblogs.com/nbxc2021/p/19688862
- 一文讲透小红书 2026 平台趋势(数英)https://www.digitaling.com/articles/1443427.html
- B站 2026 年轻人消费趋势报告"智性沸腾" https://www.fxbaogao.com/detail/5235669
- 全球年轻人热衷于搞副业(新浪/新京报)https://finance.sina.cn/tech/2026-06-19/detail-inicxkay2951341.d.html
- 35 岁职场危机(新浪财经)https://cj.sina.com.cn/articles/view/7879848901/1d5acf3c501901iun6
- 即梦/可灵/剪映 AI 视频工具对比(老达 AI 博客)https://laodad.com/tools/6334.html
- 2026 国内外 19 款 AI 视频生成工具(知乎)https://zhuanlan.zhihu.com/p/684932281
- AI 写小红书/公众号为什么限流(阿里云开发者社区)https://developer.aliyun.com/article/1724689
- 公众号 AI 写作规则与生存指南(简书)https://www.jianshu.com/p/5e899e17bbd3
- 知乎写手避开 AI 检测(CSDNhttps://blog.csdn.net/2611_96189011/article/details/161697476
> 说明:GEO 市场规模/引用率等具体数字多出自 GEO 服务商软文,行业口径不一,建议内部使用时标注"行业预估,未独立核实"。2026 年部分平台月活/政策细节来自媒体报道,可能存在口径差异。
+28 -23
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
内容采集趋势抓取 选题生成 存入选题库
收集热点趋势信息经LLM分析后生成选题建议并存入数据库
内容采集管道趋势抓取 分析 选题生成 存入选题库
聚焦AI与科技人文领域从RSS搜索和Web搜索采集信息经LLM分析后生成选题
"""
import os
@@ -42,14 +42,11 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
DEFAULT_CHINA_PAINS = {
"循环消费": "以旧换新流程繁琐、二手商品信任缺失、租赁市场不规范",
"低碳出行": "新能源车充电设施不足、城市规划不支持骑行、通勤距离长",
"干净饮食": "有机食品价格高、真伪难辨、外卖为主的生活方式难以改变",
"零浪费生活": "环保产品溢价高、可持续选择不便、漂绿营销难以分辨",
"绿色家电与节能": "绿色家电初期投入高、节能效果难量化、老旧小区改造难",
"碳普惠": "碳账户普及率低、减排量兑换吸引力不足、公众认知有限",
"环保科技产品": "绿色产品溢价68%难以承受、缺乏统一认证标准、担心漂绿",
"AI与效率": "AI工具选择困难、数据隐私担忧、学习成本高、实际效果难验证"
"AI工具实测": "AI工具选择困难、免费版功能太受限、ChatGPT不如国产好用、学习曲线陡峭、不知道哪些AI工具真正值得付费、看了测评还是不知道怎么用",
"AI前沿资讯": "AI更新太快跟不上、论文太多看不完、不知道哪些是真突破、被营销号带偏、每天刷AI新闻但感觉什么都没学到",
"未来工作方式": "AI裁员焦虑加剧、远程协作效率打折、职业方向看不清、技能更新压力大、35岁学AI来得及吗、转行AI需要什么技能",
"科技人文反思": "AI伦理边界模糊、数据隐私被侵蚀、数字依赖越来越重、人机关系困惑、孩子该不该用AI写作业、AI生成的内容能信吗",
"AI职场应用": "AI替代担心、岗位技能要求变了、职场竞争加剧、缺乏AI实操指导、怎么用AI写周报、怎么用AI做数据分析、AI提示词怎么写才有效",
}
_cached_china_pains = None
@@ -87,22 +84,22 @@ def _get_china_pain(category: str) -> str:
@dataclass
class SustainabilitySource:
"""可持续性信息"""
"""内容采集"""
name: str
type: str # rss, web_search, web, api, local
url: Optional[str] = None # RSS URL 或通用链接
update_frequency: str = "daily"
credibility: str = "medium"
focus: str = "可持续性"
focus: str = "AI与科技"
keywords: Optional[List[str]] = None # 源特定关键词
query: Optional[str] = None # 搜索查询词(w eb_search类型用)
@dataclass
class SustainabilityCase:
"""可持续性案例"""
"""内容案例"""
id: str
country: str
category: str # 子领域:零浪费生活、循环消费
category: str # 类别:AI前沿资讯、AI工具实测
title: str
core_idea: str
data_facts: str
@@ -118,7 +115,7 @@ class SustainabilityCase:
@dataclass
class SustainabilityTopic:
"""可持续性选题"""
"""AI与科技选题"""
id: str
title: str
cases: List[str] # 关联的案例ID列表
@@ -128,7 +125,7 @@ class SustainabilityTopic:
mvp_actions: str
estimated_length: int
priority_score: float
field: str = "可持续生活系统" # 内容领域
field: str = "AI与效率" # 内容领域
format: str = "趋势洞察 + 实操指南" # 内容形式
core_concept: str = "" # 核心理念
audience_pain: str = "" # 受众痛点
@@ -150,7 +147,7 @@ class SustainabilityTopic:
self.platform_urls = {}
class SustainabilityCollector:
"""可持续性内容集器"""
"""AI与科技内容集器"""
def __init__(self):
self.load_config()
@@ -191,7 +188,7 @@ class SustainabilityCollector:
url=s.url or '',
query=s.query or '',
credibility=s.credibility or 'medium',
focus=s.focus or '可持续性',
focus=s.focus or 'AI与科技',
))
logger.info(f"从DB加载 {len(cats)} 个类别, {len(self.sources)} 个信息源")
db.close()
@@ -209,7 +206,7 @@ class SustainabilityCollector:
if 'base_url' in source_info and 'url' not in source_info:
source_info['url'] = source_info.pop('base_url')
source_info.setdefault('update_frequency', 'daily')
source_info.setdefault('focus', '可持续性')
source_info.setdefault('focus', 'AI与科技')
source_info.setdefault('keywords', None)
allowed_keys = {'name', 'type', 'url', 'update_frequency', 'credibility', 'focus', 'keywords', 'query'}
filtered_info = {k: v for k, v in source_info.items() if k in allowed_keys}
@@ -379,7 +376,7 @@ class SustainabilityCollector:
'source_name': source.name
})
logger.info(f"{source.name} 获取到 {len(articles)}可持续性文章")
logger.info(f"{source.name} 获取到 {len(articles)} 篇文章")
return articles
except Exception as e:
@@ -720,8 +717,16 @@ class SustainabilityCollector:
return topic
def map_category_to_field(self, category: str) -> str:
"""案例类别映射到内容领域的字段"""
return "可持续生活系统"
"""采集类别映射到内容领域"""
mapping = {
"AI前沿资讯": "AI与效率",
"AI工具实测": "AI与效率",
"AI趋势分析": "AI与效率",
"AI职场应用": "未来工作方式",
"科技人文反思": "科技人文",
"未来工作方式": "未来工作方式",
}
return mapping.get(category, "AI与效率")
def save_results(self):
"""保存收集结果"""
@@ -851,7 +856,7 @@ class SustainabilityCollector:
def run(self):
"""主运行流程"""
logger.info("开始可持续性内容收集")
logger.info("开始AI与科技内容收集")
existing_titles = self._get_existing_titles()
+3
View File
@@ -246,12 +246,14 @@ def save_article(topic_id: str, platform: str, html_content: str, *, title: str
from app.models import Article
article_id = f"{platform}_{topic_id}"
existing = db.query(Article).filter(Article.id == article_id).first()
word_count = len(content) if content else (len(html_content) if html_content else 0)
if existing:
existing.html_content = html_content
if title:
existing.title = title
if content:
existing.content = content
existing.word_count = word_count
else:
article = Article(
id=article_id,
@@ -261,6 +263,7 @@ def save_article(topic_id: str, platform: str, html_content: str, *, title: str
title=title,
content=content,
html_content=html_content,
word_count=word_count,
status="draft",
compliance_score=None
)
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""
内容聚焦迁移脚本Phase 2
目标将数据库从"6领域分散"迁移到"3领域聚焦(AI与效率、科技人文、未来工作方式)"
执行python3 scripts/migrate_content_focus.py
"""
import sys, json, logging, datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# 新领域配置(与 initial_data.py 对齐)
NEW_FIELDS = [
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具、AI工作流、效率方法、前沿资讯", "sort_order": 1},
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "AI伦理、科技反思、数字生活、人机关系", "sort_order": 2},
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程工作、AI时代职业转型、一人企业", "sort_order": 3},
]
# 旧领域名称 → 新领域名称映射
FIELD_MIGRATION_MAP = {
"可持续生活系统": "AI与效率",
"可持续生活": "AI与效率",
"数字游民": "未来工作方式",
"个人成长": "科技人文",
"个人知识工厂": "AI与效率",
"科技人文交叉": "科技人文",
}
# 新采集分类
NEW_CATEGORIES = [
{"name": "AI前沿资讯", "search_query": "AI 人工智能 大模型 2026 前沿", "description": "AI行业动态、大模型发布、技术突破", "sort_order": 1, "is_active": True},
{"name": "AI工具实测", "search_query": "AI工具 效率提升 AI工作流 2026", "description": "AI工具评测、效率工作流、实操指南", "sort_order": 2, "is_active": True},
{"name": "AI与职场", "search_query": "AI 裁员 职业转型 AI技能 远程工作 2026", "description": "AI对就业影响、职业转型、技能升级", "sort_order": 3, "is_active": True},
{"name": "AI生活化", "search_query": "AI陪伴 AI心理咨询 AI生活助手 2026", "description": "AI心理咨询/生活搭子/AI人格化", "sort_order": 4, "is_active": True},
{"name": "科技人文", "search_query": "AI伦理 数字生活 科技反思 人机关系 2026", "description": "AI伦理/数字生活反思/科技温度", "sort_order": 5, "is_active": True},
]
# 旧分类名称(待停用)
OLD_CATEGORY_NAMES = ["循环消费", "低碳出行", "干净饮食", "零浪费生活", "绿色家电与节能", "碳普惠", "环保科技产品"]
# 新平台 tags
NEW_PLATFORM_TAGS = {
"zhihu": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"wechat": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"xiaohongshu": ["AI", "效率", "科技", "工具", "职场", "生活", "学习方法"],
}
def migrate():
try:
from app.database import SessionLocal
from app.models import TopicField, Topic, Article, CollectorCategory, CollectorSource, PlatformConfig
except ImportError as e:
logger.error(f"导入失败(确保在项目根目录执行): {e}")
sys.exit(1)
db = SessionLocal()
try:
# ========== 1. 处理 TopicField ==========
logger.info("--- 处理 TopicField ---")
existing_fields = {f.name: f for f in db.query(TopicField).all()}
field_id_map = {} # name → id
for nf in NEW_FIELDS:
if nf["name"] in existing_fields:
f = existing_fields[nf["name"]]
f.icon = nf["icon"]
f.color = nf["color"]
f.description = nf["description"]
f.sort_order = nf["sort_order"]
f.is_active = True
field_id_map[nf["name"]] = f.id
logger.info(f" 更新领域: {nf['name']} (id={f.id})")
else:
f = TopicField(**nf)
db.add(f)
db.flush()
field_id_map[nf["name"]] = f.id
logger.info(f" 创建领域: {nf['name']} (id={f.id})")
# 停用旧领域
for old_name in ["可持续生活系统", "可持续生活", "数字游民", "个人成长", "个人知识工厂"]:
if old_name in existing_fields and old_name not in [nf["name"] for nf in NEW_FIELDS]:
f = existing_fields[old_name]
f.is_active = False
logger.info(f" 停用旧领域: {old_name} (id={f.id})")
db.commit()
# ========== 2. 迁移 Topic ==========
logger.info("--- 迁移 Topic ---")
topics = db.query(Topic).all()
topic_count = len(topics)
migrated_count = 0
for t in topics:
old_field = t.field_name or ""
new_field = None
for old_k, new_v in FIELD_MIGRATION_MAP.items():
if old_k in old_field or old_field in old_k:
new_field = new_v
break
if not new_field and t.field_id:
# 按名称猜测
for old_k, new_v in FIELD_MIGRATION_MAP.items():
if old_k[:4] in old_field or old_field[:4] in old_k:
new_field = new_v
break
if new_field and new_field in field_id_map:
t.field_id = field_id_map[new_field]
t.field_name = new_field
migrated_count += 1
elif t.field_id and t.field_name not in [nf["name"] for nf in NEW_FIELDS]:
# 无法映射的旧领域,统一设为"AI与效率"
if "AI与效率" in field_id_map:
t.field_id = field_id_map["AI与效率"]
t.field_name = "AI与效率"
migrated_count += 1
db.commit()
logger.info(f"{topic_count} 个 Topic,已迁移 {migrated_count}")
# ========== 3. 清理旧 Article ==========
logger.info("--- 清理旧 Article ---")
all_articles = db.query(Article).all()
deleted_articles = []
for a in all_articles:
topic = db.query(Topic).filter(Topic.id == a.topic_id).first()
if topic:
# 只保留 topic 仍然活跃且在新领域内的
tf = db.query(TopicField).filter(TopicField.id == topic.field_id).first()
if tf and tf.is_active and tf.name in [nf["name"] for nf in NEW_FIELDS]:
continue
# 该 Article 关联的 topic 已失效,删除
deleted_articles.append(a.id)
db.delete(a)
db.commit()
logger.info(f" 已删除 {len(deleted_articles)} 篇旧 Article")
# ========== 4. 清理 CollectorCategory ==========
logger.info("--- 清理 CollectorCategory ---")
existing_cats = {c.name: c for c in db.query(CollectorCategory).all()}
# 创建新分类
for nc in NEW_CATEGORIES:
if nc["name"] in existing_cats:
c = existing_cats[nc["name"]]
c.search_query = nc["search_query"]
c.description = nc["description"]
c.sort_order = nc["sort_order"]
c.is_active = True
logger.info(f" 更新采集分类: {nc['name']}")
else:
db.add(CollectorCategory(**nc))
logger.info(f" 创建采集分类: {nc['name']}")
# 停用旧分类
for old_name in OLD_CATEGORY_NAMES:
if old_name in existing_cats:
c = existing_cats[old_name]
c.is_active = False
logger.info(f" 停用旧采集分类: {old_name}")
# 删除旧 '可持续生活' 和 '可持续发展' 变体
for cname, cobj in existing_cats.items():
if cname not in [nc["name"] for nc in NEW_CATEGORIES] and cname not in OLD_CATEGORY_NAMES:
if any(kw in cname for kw in ["可持续", "环保", "绿色", "低碳", "零浪费", "循环"]):
cobj.is_active = False
logger.info(f" 停用旧采集分类: {cname}")
db.commit()
# ========== 5. 清理 CollectorSource ==========
logger.info("--- 清理 CollectorSource ---")
all_sources = db.query(CollectorSource).all()
deactivated_sources = 0
for s in all_sources:
# 停用非 AI/科技 类采集源
if s.focus and any(kw in s.focus for kw in ["可持续", "环保", "绿色", "低碳", "零浪费"]):
s.is_active = False
deactivated_sources += 1
# 旧 URL 不再可访问的源
if s.url and any(domain in s.url for domain in ["greenbiz.com", "sustainablebrands.com", "treehugger.com"]):
s.is_active = False
deactivated_sources += 1
# 检查 category_id 是否对应已停用的分类
if s.category_id:
cat = db.query(CollectorCategory).filter(CollectorCategory.id == s.category_id).first()
if cat and not cat.is_active:
s.is_active = False
deactivated_sources += 1
# 新增源(由 initial_data.py 的补充逻辑处理)
for new_src in [
{"name": "AI前沿搜索", "source_type": "web_search", "query": "AI 人工智能 大模型 前沿 2026", "credibility": "medium", "focus": "AI前沿资讯", "sort_order": 1, "is_active": True},
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 效率提升 AI工作流 2026", "credibility": "medium", "focus": "AI工具实测", "sort_order": 2, "is_active": True},
]:
existing = db.query(CollectorSource).filter(CollectorSource.name == new_src["name"]).first()
if not existing:
db.add(CollectorSource(**new_src))
logger.info(f" 创建采集源: {new_src['name']}")
db.commit()
logger.info(f" 已停用 {deactivated_sources} 个旧采集源")
# ========== 6. 更新 PlatformConfig tags ==========
logger.info("--- 更新 PlatformConfig tags ---")
for p in db.query(PlatformConfig).all():
if p.platform in NEW_PLATFORM_TAGS:
rules = p.compliance_rules or {}
if isinstance(rules, dict) and "allowed_tags" in rules:
rules["allowed_tags"] = NEW_PLATFORM_TAGS[p.platform]
p.compliance_rules = rules
logger.info(f" 更新平台标签: {p.platform}")
db.commit()
# ========== 7. 更新 TrendFieldMapping ==========
logger.info("--- 更新 TrendFieldMapping ---")
from app.models import TrendFieldMapping
new_mappings = [
{"trend_keyword": "AI工具", "field_name": "AI与效率", "sort_order": 1},
{"trend_keyword": "AI创作", "field_name": "AI与效率", "sort_order": 2},
{"trend_keyword": "AI职场", "field_name": "AI与效率", "sort_order": 3},
{"trend_keyword": "效率工具", "field_name": "AI与效率", "sort_order": 4},
{"trend_keyword": "未来工作", "field_name": "未来工作方式", "sort_order": 5},
{"trend_keyword": "远程工作", "field_name": "未来工作方式", "sort_order": 6},
{"trend_keyword": "AI就业", "field_name": "未来工作方式", "sort_order": 7},
{"trend_keyword": "科技人文", "field_name": "科技人文", "sort_order": 8},
{"trend_keyword": "数字生活", "field_name": "科技人文", "sort_order": 9},
{"trend_keyword": "AI伦理", "field_name": "科技人文", "sort_order": 10},
{"trend_keyword": "AI情感", "field_name": "AI与效率", "sort_order": 11},
]
existing_mappings = {m.trend_keyword: m for m in db.query(TrendFieldMapping).all()}
# 标记旧的映射为停用
for keyword, mapping in existing_mappings.items():
if keyword not in [nm["trend_keyword"] for nm in new_mappings]:
mapping.is_active = False
# 创建/更新新映射
for nm in new_mappings:
if nm["trend_keyword"] in existing_mappings:
m = existing_mappings[nm["trend_keyword"]]
m.field_name = nm["field_name"]
m.sort_order = nm["sort_order"]
m.is_active = True
else:
db.add(TrendFieldMapping(**nm))
db.commit()
logger.info(f" 已同步 {len(new_mappings)} 条 TrendFieldMapping")
logger.info("=" * 50)
logger.info("✅ DB 迁移完成")
logger.info("=" * 50)
except Exception as e:
import traceback
traceback.print_exc()
logger.error(f"迁移失败: {e}")
db.rollback()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
migrate()
+7 -7
View File
@@ -45,32 +45,32 @@ _PROMPT_DEFAULTS = {
"variables": ["gaps"],
},
"section_expansion_zhihu": {
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写400-800字,3-6段。每段围绕一个论点展开:先摆事实/数据,再分析原因,最后说清这跟读者有什么关系。\n\n注意:\n- 每个论点必须具体数据或案例(至少一个),不要空谈。引用研究、报告或行业事件\n- 用「你」称呼读者,像是在知乎回答里跟人讨论\n- 从具体问题切入,不要以「引言/总结/核心观点」这类标签开头\n- 回答「所以呢」——这条信息对读者有什么用\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些表达\n- 来源放在文末括号内\n- 纯段落,不要列表/标题/序号标记",
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写400-800字,3-6段。每段围绕一个论点展开:先摆独家数据/一手案例,再分析深层原因,最后说清这跟读者有什么关系。\n\n注意:\n- 每个论点必须具体数据或独家案例支撑——自采数据、一手实验对比、行业报告引用优先。不要只用公开信息\n- 用「你」称呼读者,像是在知乎回答里跟人讨论\n- 从具体问题或反直觉现象切入,不要以「引言/总结/核心观点」这类标签开头\n- 不只是介绍这个工具/技术,而是探讨它如何改变了人的行为方式或认知\n- 回答「所以呢」——这条信息对读者有什么用,有什么可操作的启发\n- 如果这个话题有争议点,不要回避,亮出你的立场\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些表达\n- 来源放在文末括号内\n- 纯段落,不要列表/标题/序号标记",
"temperature": 0.78, "max_tokens": 4000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"section_expansion_wechat": {
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写300-500字,用「我」的口吻叙述——像在跟朋友聊天,不是写文章\n\n注意:\n- 从个人经历或感受切入,不要讲道理\n- 每个观点背后要有一个具体故事或观察\n- 段与段之间有情绪的起承转合,不是逻辑推理\n- 读起来像一个人在说话,而不是一篇文章在论述\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要标题/列表/序号标记",
"content": "写「{topic_title}」文章中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写300-500字,用「我」的口吻写一个真实的困惑、顿悟或观察——不是测评工具,而是记录这个技术/趋势如何改变了你的一种习惯、认知或生活方式\n\n注意:\n- 从个人经历或感受切入,不是介绍工具功能,而是讲这个工具让你想到了什么、改变了什么\n- 每个观点背后要有一个具体故事或观察,虚实结合\n- 段与段之间有情绪的起承转合,不是逻辑推理\n- 读起来像一个人在深夜和朋友聊天时的反思,不是一篇产品测评\n- 可以有困惑和不确定——真实比正确重要\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要标题/列表/序号标记",
"temperature": 0.8, "max_tokens": 3000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"section_expansion_xiaohongshu": {
"content": "写「{topic_title}」笔记中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写100-200字,1-3小段。每段1-2句,直接给干货。\n\n注意:\n- 直接给结论或方法,不要铺垫\n- 可以用emoji(1个/段,不要堆砌)\n- 像博主在分享亲测有效的经验\n- 不要讲道理,只讲「是什么」和「怎么做」\n- 用「你」或直接省略主语\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要列表/标题/序号标记",
"content": "写「{topic_title}」笔记中「{section_title}」这一节。\n\n今天:{date}\n笔记要点:\n{content}\n\n写100-200字,1-3小段。每段1-2句,直接给干货。\n\n注意:\n- 直接给结论或方法,不要铺垫\n- 每个结论后面跟一个具体数据/对比/结果(亲测可用才有说服力)\n- 可以用emoji(1个/段,不要堆砌)\n- 像博主在分享亲测有效的经验,有真实的「我试过」感\n- 不要讲道理,只讲「是什么」和「怎么做」\n- 用「你」或直接省略主语\n- 避免「首先其次最后」「总的来说」「综上所述」「值得注意的是」这些套路\n- 纯段落,不要列表/标题/序号标记",
"temperature": 0.75, "max_tokens": 2000,
"variables": ["topic_title", "section_title", "date", "content"],
},
"title_optimize_zhihu": {
"content": "你是知乎老用户,经常上热榜。给这篇文章起3个标题候选:\n\n文章主题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n感觉像在跟人推荐一篇值得辩论的文。信息要密、20字内、有明确立场、让人想点进来说两句。不要「如何」开头。\n\n每行一个,不要编号。",
"content": "你是知乎老用户,经常上热榜。给这篇文章起3个标题候选:\n\n文章主题:{title}\n核心观点:{core}\n受众痛点:{pain}\n领域:{field}\n\n感觉像在跟人推荐一篇值得辩论的观点帖,不是在推荐一篇工具文。信息要密、20字内、有明确立场、有观点交锋感、让人想点进来说两句。不要「如何」开头。如果话题有争议性,在标题里亮出你的立场。\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "pain", "field"],
},
"title_optimize_wechat": {
"content": "你是公众号编辑。这篇文章面向{field}领域的读者,帮起3个标题:\n\n文章主题:{title}\n核心观点:{core}\n\n要有身份代入感(打工人/30岁+/普通上班族)、有情绪钩子(焦虑/反常识/后悔)、含微信搜索关键词。15-25字,口语化,越具体越好。参考这种风格:「用了AI效率反而更低了?73%的人掉进了这个坑\n\n每行一个,不要编号。",
"content": "你是公众号编辑,擅长写有温度、有人味的文章。这篇文章面向{field}领域的读者,帮起3个标题:\n\n文章主题:{title}\n核心观点:{core}\n\n标题方向是「科技时代的个人叙事」——不是推荐工具,而是分享一个真实的人与技术之间的故事。要有身份代入感(打工人/30岁+/普通上班族/家长)、有情绪钩子(困惑/反常识/顿悟/后悔)、含微信搜索关键词。15-25字,口语化,越具体越好。参考:「AI帮我写周报的第3个月,我终于发现哪里不对了\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core", "field"],
},
"title_optimize_xhs": {
"content": "你是小红书博主。给这篇笔记起3个标题:\n\n笔记主题:{title}\n核心:{core}\n\n每条18字以内,有具体场景和结果感。可以加1个精确emoji(不要🔥💥❌❓)。参考:「用了两年AI工具,推荐这5个就够了」这种风格。\n\n每行一个,不要编号。",
"content": "你是小红书博主,擅长写有数据、有结果感的干货笔记。给这篇笔记起3个标题:\n\n笔记主题:{title}\n核心:{core}\n\n每条18字以内,有具体场景和结果感(含数字/时间/效果)。可以加1个精确emoji(不要🔥💥❌❓)。参考:「两年换了8个AI工具,最后只留这3个」这种风格。\n\n每行一个,不要编号。",
"temperature": 0.8, "max_tokens": 1500,
"variables": ["title", "core"],
},
@@ -95,7 +95,7 @@ _PROMPT_DEFAULTS = {
"variables": ["html"],
},
"sources_optimization": {
"content": "你是一个内容策略分析师。分析当前中文互联网可持续生活领域的真实热点,与以下配置进行对比。\n\n当前配置的类别({n}个):\n{cat_names}\n\n当前配置的信息源({n2}个):\n{src_summary}\n\n请完成:\n1. 评估每个类别是否仍符合{year}年中国市场真实热点(保留/淘汰/合并)\n2. 评估每个信息源是否可在国内正常访问\n3. 建议新增的类别(最多2条)\n4. 建议新增的信息源搜索词(最多3条,含具体搜索词)\n\n输出 JSON\n{\n \"category_assessment\": [{\"name\": \"...\", \"status\": \"保留/淘汰/合并\", \"reason\": \"...\"}],\n \"source_assessment\": [{\"source\": \"...\", \"accessible\": true/false, \"note\": \"...\"}],\n \"new_categories\": [{\"name\": \"...\", \"reason\": \"...\"}],\n \"new_search_queries\": [{\"query\": \"...\", \"target\": \"...\"}]\n}\n只输出JSON。",
"content": "你是一个内容策略分析师。分析当前中文互联网AI与科技人文领域的真实热点,与以下配置进行对比。\n\n当前配置的类别({n}个):\n{cat_names}\n\n当前配置的信息源({n2}个):\n{src_summary}\n\n请完成:\n1. 评估每个类别是否仍符合{year}年中国市场真实热点(保留/淘汰/合并)\n2. 评估每个信息源是否可在国内正常访问\n3. 建议新增的类别(最多2条)\n4. 建议新增的信息源搜索词(最多3条,含具体搜索词)\n\n输出 JSON\n{\n \"category_assessment\": [{\"name\": \"...\", \"status\": \"保留/淘汰/合并\", \"reason\": \"...\"}],\n \"source_assessment\": [{\"source\": \"...\", \"accessible\": true/false, \"note\": \"...\"}],\n \"new_categories\": [{\"name\": \"...\", \"reason\": \"...\"}],\n \"new_search_queries\": [{\"query\": \"...\", \"target\": \"...\"}]\n}\n只输出JSON。",
"temperature": 0.5, "max_tokens": 3000,
"variables": ["n", "cat_names", "n2", "src_summary", "year"],
},
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
长文 短视频脚本改写器
读取某选题的长文知乎/公众号 markdown生成 N 条差异化短视频口播/分镜脚本
"""
import argparse
import datetime
import json
import logging
import sys
from pathlib import Path
from typing import Dict, List, Optional
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / 'platform' / 'backend'))
from db_helper import get_topic_by_id, get_articles_by_topic, save_article
from prompt_loader import get_prompt, get_prompt_params
try:
from app.core.nvidia_client import call_llm
HAVE_LLM = True
except ImportError:
HAVE_LLM = False
DATA_DIR = PROJECT_ROOT / "automation" / "data"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOGS_DIR / f"shortvideo_{TODAY}.log"),
logging.StreamHandler(),
],
)
logger = logging.getLogger(__name__)
SYSTEM_PROMPT = "你是短视频脚本编剧,擅长把长文改成高完播率口播脚本"
_VARIANT_ANGLES = [
("观点冲突型", "从一个反直觉/有争议的观点切入,制造认知冲突,让观众想反驳或认同"),
("干货清单型", "提炼 3-5 条可立刻上手的干货要点,节奏快、信息密度高"),
("故事共鸣型", "用一个真实人物/生活场景的故事开头,引发情绪共鸣,再带出观点"),
]
def _build_prompt(source_text: str, angle_name: str, angle_desc: str) -> str:
return f"""你是一个短视频脚本编剧。下面是一篇长文的内容,请把它改写成一条短视频脚本。
改写角度{angle_name}{angle_desc}
长文素材
{source_text}
请严格按以下结构输出 markdown各级标题不要省略
## 标题(爆款钩子)
1 15-25 字的短视频标题带强钩子悬念/冲突/数字/结果不要标题党到离谱
## 口播文案
60-90 秒的口语化口播稿 200-350 像真人对着镜头说话有停顿标记停顿标注换气/节奏点不要书面语不要首先其次最后套路
## 分镜建议
给出 3-5 个镜头每个镜头包含
- 画面具体拍什么景别/动作/场景
- 字幕屏幕上打出的关键词
- 时长几秒
用列表或表格呈现每个镜头一行
## 适配平台
说明这条脚本分别适配 抖音 / 视频号 / 小红书视频 时的微调建议 1 句话
只输出上面的 markdown不要其他说明"""
def _template_markdown(topic_title: str, angle_name: str) -> str:
return f"""## 标题(爆款钩子)
待生成{topic_title} - {angle_name}角度
## 口播文案
LLM 不可用使用模板占位请稍后手动补充 60-90 秒口播稿并用停顿标记节奏
## 分镜建议
- 画面占位 字幕占位 时长占位
## 适配平台
- 抖音占位
- 视频号占位
- 小红书视频占位
"""
def _load_source(topic: Dict, articles: List[Dict]) -> str:
for platform in ("zhihu", "wechat"):
for a in articles:
if a.get("platform") == platform and a.get("html_content"):
return a["html_content"]
if topic.get("core_concept"):
return f"# {topic.get('title', '')}\n\n{topic.get('core_concept')}"
return topic.get("title", "")
def _generate_one(source: str, idx: int, topic_title: str) -> str:
angle_name, angle_desc = _VARIANT_ANGLES[(idx - 1) % len(_VARIANT_ANGLES)]
if HAVE_LLM:
try:
params = get_prompt_params("shortvideo_script") or {}
temperature = params.get("temperature", 0.8)
max_tokens = params.get("max_tokens", 2000)
prompt = _build_prompt(source, angle_name, angle_desc)
result = call_llm(
prompt,
temperature=temperature,
max_tokens=max_tokens,
system_prompt=SYSTEM_PROMPT,
)
if result and result.strip():
return result.strip()
except Exception as e:
logger.warning("LLM 调用失败(variant %s):%s", idx, e)
logger.warning("LLM 不可用或失败,写入模板占位(variant %s", idx)
return _template_markdown(topic_title, angle_name)
def _extract_title(markdown: str, fallback: str) -> str:
for line in markdown.splitlines():
line = line.strip()
if line.startswith("## 标题") or line.startswith("# 标题"):
rest = line.split("标题", 1)[-1].strip("(): ")
if rest:
return rest[:60]
first = next((l.strip("# ").strip() for l in markdown.splitlines() if l.strip()), "")
return first[:60] if first else fallback
def main():
parser = argparse.ArgumentParser(description="长文转短视频脚本")
parser.add_argument("--topic-id", required=True, help="选题 ID")
parser.add_argument("--count", type=int, default=3, help="生成脚本数量(默认 3")
args = parser.parse_args()
topic_id = args.topic_id
count = max(1, args.count)
files: List[str] = []
out_dir = DATA_DIR / "shortvideos" / TODAY
out_dir.mkdir(parents=True, exist_ok=True)
topic = get_topic_by_id(topic_id)
if not topic:
logger.error("选题不存在:%s", topic_id)
print(json.dumps({"ok": False, "topic_id": topic_id, "count": 0, "files": []}, ensure_ascii=False))
sys.exit(1)
articles = get_articles_by_topic(topic_id)
source = _load_source(topic, articles)
topic_title = topic.get("title", topic_id)
ok = True
for i in range(1, count + 1):
markdown = _generate_one(source, i, topic_title)
title = _extract_title(markdown, f"{topic_title}_短视频{i}")
file_path = out_dir / f"{topic_id}_shortvideo_{i}.md"
try:
file_path.write_text(markdown, encoding="utf-8")
files.append(str(file_path))
logger.info("已写出脚本文件:%s", file_path)
except Exception as e:
logger.error("写文件失败:%s", e)
ok = False
continue
try:
save_article(topic_id, f"shortvideo_{i}", html_content=None, title=title, content=markdown)
logger.info("已写入 DBshortvideo_%s / %s", i, topic_id)
except Exception as e:
logger.warning("DB 保存失败(文件已落盘):%s", e)
summary = {"ok": ok, "topic_id": topic_id, "count": count, "files": files}
print(json.dumps(summary, ensure_ascii=False))
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
+16 -9
View File
@@ -24,18 +24,25 @@ TODAY = __import__('datetime').datetime.now().strftime("%Y-%m-%d")
logger = logging.getLogger(__name__)
DEFAULT_TREND_DOMAIN_MAP = {
"远程工作": "未来工作方式",
"AI工具": "AI与效率",
"可持续生活": "可持续生活系统",
"知识管理": "个人知识工厂",
"数字生活": "科技人文交叉",
"科技人文": "科技人文交叉",
"个人成长": "个人成长",
"副业": "个人成长",
"AI创作": "AI与效率",
"未来工作": "未来工作方式",
"AI职场": "AI与效率",
"效率工具": "AI与效率",
"家庭教育": "科技人文交叉",
"Prompt": "AI与效率",
"提示词": "AI与效率",
"AI编程": "AI与效率",
"AI写作": "AI与效率",
"未来工作": "未来工作方式",
"远程工作": "未来工作方式",
"AI就业": "未来工作方式",
"一人企业": "未来工作方式",
"副业": "未来工作方式",
"科技人文": "科技人文",
"数字生活": "科技人文",
"AI伦理": "科技人文",
"AI情感": "科技人文",
"数据隐私": "科技人文",
"AI教育": "科技人文",
}
_cached_trend_domain_map = None
+2 -1
View File
@@ -193,7 +193,8 @@ def fetch_baidu_hot() -> List[Dict]:
word = item.get("query", "").strip() or item.get("word", "").strip()
if not word:
continue
hot_score = item.get("hotScore", 0) or item.get("heat", 0)
hot_score_raw = item.get("hotScore", 0) or item.get("heat", 0)
hot_score = int(hot_score_raw) if hot_score_raw else 0
desc = item.get("desc", "")
results.append({
"domain": _guess_domain(word, desc),
+72 -3
View File
@@ -69,6 +69,33 @@ def _load_platform_config() -> dict:
PLATFORM_CONFIG = _load_platform_config()
_AI_DISCLOSURE_TEXT = {
"zhihu": "本文由 AI 辅助创作,核心观点与数据经宇之然人工审核校对。",
"wechat": "本文由 AI 辅助创作,核心观点与数据经宇之然人工审核校对。",
"xiaohongshu": "✨ 本文由 AI 辅助生成,内容已人工核对~",
}
def _load_ai_disclosure_enabled() -> bool:
"""根据《人工智能生成合成内容标识办法》(2025-09-01 施行),AI 生成内容须主动声明。
默认开启可通过 SystemConfig(disclose_ai=false) 或环境变量 YZR_DISCLOSE_AI=0 关闭"""
try:
if os.environ.get("YZR_DISCLOSE_AI", "").strip() == "0":
return False
from app.database import SessionLocal
from app.models import SystemConfig
db = SessionLocal()
try:
sc = db.query(SystemConfig).filter(SystemConfig.key == "disclose_ai").first()
if sc and str(sc.value).strip().lower() in ("false", "0", "no"):
return False
finally:
db.close()
except Exception:
pass
return True
AI_DISCLOSURE_ENABLED = _load_ai_disclosure_enabled()
PLATFORM_NAMES = {
"zhihu": "知乎专栏",
"wechat": "微信公众号",
@@ -235,10 +262,40 @@ def inject_geo_metadata(html: str, title: str, content: str, platform: str, tags
return html
def inject_ai_disclosure(html: str, platform: str) -> str:
"""按《人工智能生成合成内容标识办法》注入 AI 生成声明。
显式文首/文末标注AI 生成隐式head 注入 ai-generated meta AIGC 隐式标识字段"""
if not AI_DISCLOSURE_ENABLED:
return html
text = _AI_DISCLOSURE_TEXT.get(platform, _AI_DISCLOSURE_TEXT["zhihu"])
disclosure_block = f'\n<blockquote class="ai-disclosure">🤖 {text}</blockquote>\n'
# 文末声明(置于正文末尾,封面/互动钩子之前不影响阅读)
if "<!-- AI_DISCLOSURE -->" in html:
html = html.replace("<!-- AI_DISCLOSURE -->", disclosure_block.strip())
else:
# 在 </body> 前插入文末声明
html = html.replace("</body>", f"{disclosure_block}</body>")
# 隐式标识:head 注入声明 meta + AIGC 元数据字段(符合标识办法附录 E)
head_meta = (
'<meta name="ai-generated" content="true">\n'
'<meta name="ai-disclosure" content="本文含人工智能生成合成内容">\n'
'<script type="application/ld+json">\n'
'{"AIGC":{"Label":"1","ContentProducer":"宇之然","ProduceID":"yzr-' + platform + '",'
'"ContentPropagator":"宇之然","PropagateID":"yzr-' + platform + '"}}\n'
'</script>\n'
)
html = html.replace("</head>", head_meta + "</head>")
return html
class Writer:
def __init__(self, topic_id: str):
self.topic_id = topic_id
self.topic = self._load_topic()
self._degraded = False
outline_file = OUTLINE_DIR / TODAY / f"{topic_id}_outline.md"
if not outline_file.exists():
raise FileNotFoundError(f"Outline not found: {outline_file}")
@@ -334,6 +391,9 @@ class Writer:
except Exception as e:
logger.warning(f"LLM 扩写失败 [{platform}]: {e}")
# LLM 不可用或返回为空 → 降级为大纲要点拼接(非真正成稿)
self._degraded = True
logger.warning(f"⚠️ [{platform}] 章节「{section['title']}」LLM 扩写缺失,降级为大纲要点拼接(内容未达发布质量)")
lines = [l.strip() for l in content.split('\n') if not self._is_outline_noise(l)]
if lines:
sentences = []
@@ -557,7 +617,7 @@ class Writer:
line = line.strip()
if not line:
continue
line = re.sub(r'^\d+[.、)\s]+', '', line)
line = re.sub(r'^\d+[.、)]\s*', '', line)
line = line.strip('*#- \t"\'"''"')
# 跳过思考/建议类输出(如"不如:"、"或者:"、"建议方案"等)
if re.match(r'^(不如|或者|建议|推荐|参考|方案[一二三]|第[一二三]种|以[下是]|标题[一二三]|选项)', line):
@@ -608,8 +668,11 @@ class Writer:
html = html.replace("<!-- TAGS -->", tags_html)
else:
html = html.replace("<!-- TAGS -->", "")
html = inject_geo_metadata(html, title, adapted, platform, tags_html)
# 强制 AI 生成声明(合规要求,可配置关闭)
html = inject_ai_disclosure(html, platform)
return html
def save_html(self, html: str, platform: str, *, title: str = "", content: str = "") -> str:
@@ -634,8 +697,14 @@ class Writer:
html = self.generate_platform_html(markdown, platform)
results[platform] = str(self.save_html(html, platform, title=title, content=markdown))
self.mark_draft()
if self._degraded:
logger.error(
"❌ 撰写完成但存在 LLM 降级章节(大纲要点拼接,未达发布质量)。"
"选题保留为待审查,请检查 LLM 提供商连通性后重跑,勿直接发布降级稿。"
)
else:
logger.info(f"撰写完成,状态已更新为待审查")
return {"ok": True, "files": results}
return {"ok": True, "files": results, "degraded": self._degraded}
def main():
import argparse