# geoAutoPublish
**Repository Path**: hackerSnail/geo-auto-publish
## Basic Information
- **Project Name**: geoAutoPublish
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-11-09
- **Last Updated**: 2026-11-12
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# GEO AI 内容创作与多平台分发系统
面向**单个运营者**的 GEO(Generative Engine Optimization)内容生产与分发工具:
`品牌资料/关键词 → 写作与来源核验 → 人工编辑审核 → 平台适配 → 授权草稿/人工发布 → 真实 URL 与效果记录`。
核心原则:**内容质量和可追溯事实高于发文数量**。GEO 的“被 AI 提及/引用”只能通过真实观测验证,
系统不会承诺效果,也不会伪造指标。
当前状态:**P0 + P1 + P2 + P3 + 公众号真实联调**。品牌库、mock / 本地 Ollama 写作流程、事实核查、
人工改稿与审批、来源核验、三平台适配、导出包、分发台账与效果台账均已可用;
**公众号草稿接口已真实联调通过(只建草稿,不发布)**,百家号/小红书仍为纯人工路径。
进度见 [`docs/IMPLEMENTATION_PLAN.md`](docs/IMPLEMENTATION_PLAN.md)。
---
## 1. 环境要求
- Python >= 3.11(开发环境实测 3.15.2)
- 无需数据库服务:默认 SQLite 单文件
- 可选:本机安装 [Ollama](https://ollama.com/)(本地模型,数据不出本机)
## 2. 安装
```powershell
# Windows PowerShell
cd <项目目录>
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
```
macOS / Linux:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e ".[dev]"
```
## 3. 配置
```powershell
copy .env.example .env # Windows
# cp .env.example .env # macOS / Linux
```
`.env` 已在 `.gitignore` 中,**不要提交**。默认配置即可直接运行(`LLM_PROVIDER=mock`,不需要任何 Key)。
关键开关见 `.env.example`:
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LLM_PROVIDER` | `mock` | `mock` / `ollama`;其他取值会明确报错(不静默降级) |
| `LLM_API_KEY` | 空 | 供应商正式 API Key,禁止用会话令牌代替 |
| `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | 本地模型服务地址 |
| `OLLAMA_MODEL` | 空 | 本机已安装的模型名(先 `ollama list` 查看) |
| `OLLAMA_THINK` | `false` | 思考型模型必须关,否则正文被思考内容占满而变空 |
| `WECHAT_APP_ID` / `WECHAT_APP_SECRET` | 空 | 微信公众号凭证,只放环境变量或密钥服务 |
| `WECHAT_DRAFT_ENABLED` | `false` | 允许调用“新建草稿”接口(还需账号具备该权限) |
| `WECHAT_PUBLISH_ENABLED` | `false` | 终局发布开关,默认关闭 |
| `REAL_PLATFORM_WRITES_ENABLED` | `false` | 全局真实写开关,单独打开**不会**触发任何对外写入 |
## 4. 初始化数据库与启动
```powershell
.\.venv\Scripts\python.exe -m alembic upgrade head
.\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000
```
打开:
- 管理界面:
- API 文档(Swagger):
- 健康检查:
## 5. 五分钟演示(不需要任何账号凭证)
1. 打开 ,创建一个品牌档案(标准名、别名、已核实事实、**禁用表述**)。
2. 打开 ,填主问题(例如“怎样选择适合中小企业的记账工具?”),点“生成草稿”。
3. 文章详情页会显示:意图分析、证据计划、事实核查结果、GEO 结构自检、来源台账与 Markdown 正文。
4. 注意:**不填来源时系统不会编造任何引用**,正文中的断言会被标记为 `【待核实:…】`,
版本状态为 `needs_verification=true`,不能进入分发。
### 分发台账与效果台账
- **分发台账**(`/distributions`):每个平台独立记录状态与真实链接。
状态机 `NOT_STARTED → READY → DRAFT_CREATED/MANUAL_PENDING → PUBLISHED/FAILED/UNKNOWN`;
幂等键 `version_hash + platform + account_id + operation`,重复提交不会产生第二条记录;
`PUBLISHED` 必须由授权接口真实成功或人工登记链接;链接未人工核验会标注「待核实」。
- **效果台账**(`/analytics`):**已发布 / 已被搜索引擎收录 / 被 AI 提及或引用**三件事分开统计,互不推导。
观测记录只写人工真实看到的;标记「有引用」必须给出可验证链接。
```bash
curl -X POST http://127.0.0.1:8000/api/platform-accounts -H "Content-Type: application/json" \
-d '{"platform":"wechat","account_name":"主号"}'
curl -X POST http://127.0.0.1:8000/api/articles//distributions -H "Content-Type: application/json" \
-d '{"platform":"wechat","account_id":"","operation":"manual_publish"}'
curl http://127.0.0.1:8000/api/analytics?brand_id=
```
### 微信公众号草稿(写操作,五道闸门)
```bash
# 1) 只读权限自检(取 token + 探测接口权限,不创建任何内容)
python -m app.cli check-wechat-api
# 2) 查出口 IP(加到公众号后台 IP 白名单)
python -m app.cli egress-ip
# 3) 在界面上:文章详情 →「微信公众号草稿」→ 运行自检 → 勾选确认 → 创建草稿
```
创建草稿必须同时满足五个条件(代码强制,缺一不可):
凭证与 `WECHAT_DRAFT_ENABLED` 就绪 → `REAL_PLATFORM_WRITES_ENABLED` 开启 →
最近一次权限自检通过 → 文章已审批且无待核实事实 → 用户明确勾选确认。
成功后台账状态为 `DRAFT_CREATED`(**草稿 ≠ 发布**),并持久化远端 `media_id`;
重复触发不会重复创建(幂等键 = 版本哈希 + 平台 + 账号 + 操作);
**超时不会自动重发**(非幂等写操作),而是置 `UNKNOWN` 等人工核实远端状态。
**正文内联图片**会在建草稿时自动转存:下载(SSRF 受控、仅图片类型、单图 ≤1MB)→ 上传微信素材库 →
替换正文 `src`。转存失败不阻断草稿,成功/失败张数写进台账 `extra.image_transfer`。
转存意味着图片会上传到微信素材库,只应处理你拥有授权的图片。
### 平台适配(本地适配已实现,真实账号能力未验证)
所有渠道统一走 `PlatformAdapter`:**只有适配器自己 `capabilities()` 宣告支持的操作才会执行**。
| 平台 | 本地产物 | 校验 |
| --- | --- | --- |
| 微信公众号 | `variants/wechat.html`(白名单清洗后的 HTML) | 标题 ≤64 字、正文图片需转存素材库、AI 标识、封面比例提示 |
| 百家号 | `variants/baijiahao.md` + 发布核对清单 | 标题 ≤30 字、摘要 ≤120 字、AI 标识、封面比例提示 |
| 小红书 | `variants/xiaohongshu.md`(标题/短文案/话题/配图清单/人工待办) | 标题 ≤20 字(按标点截断并提醒)、短文案 ≤1000 字、话题 ≤10 个、配图 3:4 |
```bash
curl http://127.0.0.1:8000/api/platforms # 各平台能力清单
curl -X POST http://127.0.0.1:8000/api/articles//render/wechat # 生成适配稿 + 校验问题
```
> `can_create_draft` / `can_publish_via_authorized_api` 目前一律为 `false`:
> **本系统不会发起任何真实平台写请求**,适配稿只供人工复制与发布。
> 字数与尺寸限制取自公开常见值,尚未用你的真实后台校验。
### 抓取来源正文(SSRF 受控)
文章详情页「来源」区可抓取公开网页正文,登记为**待核验**来源;核验后才绑定 claim。
```bash
curl -X POST http://127.0.0.1:8000/api/sources/fetch \
-H "Content-Type: application/json" \
-d '{"brand_id":"notelume","url":"https://hackersnail.github.io/NoteLume/"}'
```
防护规则(`app/services/fetching.py`):只允许 http/https、不允许 URL 内嵌凭据、只允许 80/443 端口、
解析域名后逐个检查 IP(禁止内网/回环/链路本地/保留地址)、手动处理重定向且每跳重新校验、
大小上限 + 内容类型白名单 + 超时。默认 `FETCH_ENABLED=false`,需显式开启。
### 运维命令行
```powershell
python -m app.cli list-templates # 查看预置固定问题集模板
python -m app.cli seed-prompts --brand notelume --template notelume # 播种问题集
python -m app.cli check-wechat # 公众号草稿就绪检查(不发任何请求)
python -m app.cli check-env # 配置摘要(不含密钥内容)
```
`check-wechat` 只读本地配置:检查 `.env`、`WECHAT_APP_ID` 格式、Secret 是否存在、
两个开关状态,并列出**必须由你在公众号后台确认**的两项(出口 IP 白名单、账号接口权限)。
它不会发起任何网络请求。
### 各平台「能否直接放入草稿箱」
```bash
python -m app.cli check-platform-drafts
```
| 平台 | 结论 | 依据 |
| --- | --- | --- |
| 微信公众号 | ✅ 可以 | `draft/add` 已真实联调(2026-11-12),`media_id` 已落库;终局发布不实现 |
| 小红书 | ⛔ 不支持 | 官方开放平台 11 个 API 分类全是电商链路,文档搜索「笔记/草稿/内容发布」零命中 |
| 百家号 | 🟡 待确认 | 需开发者资质 + 账号授权 + 接口审核;本机无法核实,待你后台确认 |
百家号/小红书因此只走「适配稿 + 一键复制 + 配图清单」的人工路径,**系统不代发**。
这两个平台没有可用的官方发布接口,系统**只帮你把人工发布要用的东西一次准备齐**:
- **一键复制发布包**:标题 + 摘要/正文 + 话题 + 配图清单 + 核对清单,整段粘到平台编辑器即可
(公众号还会附带清洗后的富文本 HTML,粘贴时保留格式)
- **下载配图清单**:张数、比例、每张图的内容建议、版权与 AI 标识提醒
```bash
curl http://127.0.0.1:8000/api/articles//publish-kit/xiaohongshu # 复制包 JSON
curl http://127.0.0.1:8000/api/articles//publish-kit/xiaohongshu/checklist.md # 配图清单
```
导出包里也会带上 `variants/<平台>.copy.txt` 与 `variants/<平台>.images.md`。
**系统不代发**:发布动作与责任都在你。
### 导出素材包
文章详情页点「生成 / 更新导出包」,会写出可交付目录(默认 `data/exports/`,可用 `EXPORT_DIR` 改):
```text
data/exports///
master.md # YAML 元数据 + 主稿,顶部横幅标明审批状态
sources.json # 来源 URL/访问时间/核验状态,以及每条主张的核销情况
review.json # 人审意见、修改记录、审批版本、核查结论、审计日志
assets/ # 授权素材目录(保留版权与 AI 标识)
export.json # 清单:文件列表、是否可发布、密钥扫描结果
.zip # 手动发布素材包
```
```bash
curl -X POST http://127.0.0.1:8000/api/articles//export
curl http://127.0.0.1:8000/api/articles//export/download # 下载 zip
```
两条硬约束:**未审批 / 审批已失效**的稿件会被显式标注「仅供内部审阅,不得对外发布」;
导出内容若检测到密钥会**直接拒绝导出**(不静默改写)。
### 人工审核闭环(同一页面内完成)
文章详情页底部的「人工审核」面板支持完整闭环:
1. **核验来源**:在来源区填写该来源支持的主张编号(如 `C1,C2`)→「标记为已核验」,
对应主张的待核实状态会立刻重算消失。
2. **提交审核** → **批准**(必须逐项勾选「事实准确 / 商业表述合规 / 有真实编辑贡献 / AI 内容标识正确」)。
3. **改稿即失效**:在「修改正文」里保存任何改动 → 自动生成新版本(v+1),
旧审批立即失效、状态回到待核查,必须重新提交与批准。
4. **退回**必须填写修改意见;所有动作都写入审批历史与审计日志(`GET /api/audit-events`)。
等价 API:
```bash
curl -X POST http://127.0.0.1:8000/api/brands \
-H "Content-Type: application/json" \
-d '{"id":"demo-brand","canonical_name":"示例科技","prohibited_claims":["行业第一"]}'
curl -X POST http://127.0.0.1:8000/api/articles/generate \
-H "Content-Type: application/json" \
-d '{"brand_id":"demo-brand","primary_question":"怎样选择适合中小企业的记账工具?","purpose":"行业科普"}'
# 改稿 → 新版本 → 旧审批失效
curl -X POST http://127.0.0.1:8000/api/articles//edit \
-H "Content-Type: application/json" \
-d '{"content_markdown":"# 修订后的正文\n\n本文由 AI 辅助撰写。\n","comment":"删除无来源段落"}'
# 批准(四项勾选缺一不可)
curl -X POST http://127.0.0.1:8000/api/articles//approve \
-H "Content-Type: application/json" \
-d '{"checks":{"facts_accurate":true,"claims_compliant":true,"has_human_editing":true,"ai_disclosure_ok":true}}'
```
## 6. 使用本地 Ollama(可选,数据不出本机)
```powershell
# 1) 确认本机模型
ollama list
# 2) 需要时拉取模型(示例)
ollama pull qwen3.5:9b
# 3) 写入 .env
# LLM_PROVIDER=ollama
# OLLAMA_MODEL=qwen3.5:9b
# OLLAMA_TIMEOUT_SECONDS=600
# 4) 重启服务后照常在界面生成文章
```
实测(Windows + `qwen3.5:9b`,CPU 推理):完整写作流程 4 次模型调用约 **50–65 秒**(含意图分析、证据计划、大纲、正文)。
本地模型接入后特别要注意的三点(均已在代码里处理并有回归测试):
| 现象 | 处理方式 |
| --- | --- |
| 思考型模型把预算花在 `thinking` 上,正文为空 | 强制 `OLLAMA_THINK=false`;仍为空时明确报错并提示调整 |
| 模型返回**空的主张清单** | 工作流回退到系统默认主张清单;「空清单」永远判为待核实,不会当成全部通过 |
| 模型自称「经人工审核校对」 | 事实核查按 `unsubstantiated_review_claim` 拦下(critical),生成阶段不存在人工审批 |
本地小模型会写出**看起来合理但没有来源**的细节(例如替品牌补出“基础版/同步方案”等官网并未写的内容)。
这不是 bug,而是这类模型的真实水平:因此系统的定位是**写作辅助**,事实必须由人核验来源后才能发布。
## 7. 测试与静态检查
```powershell
.\.venv\Scripts\python.exe -m pytest -q
.\.venv\Scripts\python.exe -m ruff check .
.\.venv\Scripts\python.exe -m ruff format --check .
```
测试全部使用临时 SQLite 与 mock provider(`conftest.py` 固定 `LLM_PROVIDER=mock`,不受本机 `.env` 影响);
同时有一条“阻断对外网络连接”的自动夹具,任何真实平台调用都会直接失败。
## 8. 目录结构
```text
app/
main.py # 应用入口、路由注册、全局错误兜底
web.py # 中文管理界面(Jinja2 服务端渲染)
api/ # REST 接口:health / brands / articles
core/ # 配置(.env)与日志脱敏
db.py # 引擎与会话(延迟创建)
models/ # ORM:brands / articles / article_versions / sources
schemas/ # Pydantic 请求响应模型
services/
writing/ # 写作工作流、LLMProvider 抽象、mock / ollama provider、GEO 自检
fact_check/ # 规则化事实核查(无来源即待核实)
distribution.py # 分发闸门(未审批/有待核实 → 拒绝分发)
platforms/ # P2 阶段实现:wechat / baijiahao / xiaohongshu
templates/ static/
migrations/ # Alembic 迁移
tests/ # pytest
docs/ # 实施方案、平台能力、运维说明、进度
data/ # 本地数据库与素材(不入 Git)
```
## 9. 已知限制与边界(重要)
- **百家号/小红书仍无真实写能力**:只做适配与导出,由你在后台人工发布。
- **公众号只做到草稿**:已真实联调通过(`draft/add` 验证、幂等验证、只读核验),
**终局发布(`freepublish/submit`)不实现**,也不做无人值守群发。
- **观测数据全靠人工**:GEO 效果台账只汇总人工登记的真实记录,系统不推测、不生成指标;
自动观测需先确认符合平台条款与授权。
- **小红书/百家号适配器尚未实现**:当前只有公众号适配器的设计位(P2)。
- **GEO 自检分数不是收录预测**:它只是内部编辑提示,不能作为“会被 AI 引用”的证据。
- **本系统不做无人值守批量发布**:真实写入必须同时满足账号权限、全局开关、用户明确确认、
已审批版本与适配器能力五个条件。
- **本地/云端大模型都可能编造细节**:系统的防线是「来源登记 + 事实核查 + 人工审批」,不是模型本身。
## 10. 常见问题
**Q:启动报 `ModuleNotFoundError: app`?**
先 `pip install -e ".[dev]"`,或在项目根目录执行(`alembic.ini` 与 `pyproject.toml` 已配置 `pythonpath`)。
**Q:数据库文件在哪?**
默认 `data/geo.db`(相对项目根目录解析,不随工作目录变化),该目录已被 `.gitignore` 忽略。
**Q:改了 ORM 模型怎么办?**
```powershell
.\.venv\Scripts\python.exe -m alembic revision --autogenerate -m "说明"
.\.venv\Scripts\python.exe -m alembic upgrade head
.\.venv\Scripts\python.exe -m alembic check # 校验模型与迁移是否一致
```
**Q:能不能接云端大模型?**
当前实现了 `mock` 与 `ollama`。设置 `LLM_PROVIDER=openai` 会明确报错而不是静默失败;
云厂商 provider 属 P1 范围,届时需要你提供供应商正式 API Key。
**Q:本地模型报「模型只返回了思考过程、没有正文」?**
模型是思考型且 `OLLAMA_THINK=true`,把预算耗在思考上了。改成 `OLLAMA_THINK=false` 并重启。
**Q:本地模型报 404 / 模型不存在?**
先 `ollama pull <模型名>`,再把 `OLLAMA_MODEL` 改成与 `ollama list` 完全一致的名称。
**Q:为什么文章里全是“待核实”?**
这是设计目标:没有可核验来源时,宁可标注也不要让模型编造参考文献。
## 11. 相关文档
- [`docs/CLAUDE.md`](docs/CLAUDE.md):项目持续工作指令与产品边界
- [`docs/GEO_实施方案.md`](docs/GEO_实施方案.md):可执行实施方案
- [`docs/IMPLEMENTATION_PLAN.md`](docs/IMPLEMENTATION_PLAN.md):开发清单与完成进度
- [`docs/PLATFORM_CAPABILITIES.md`](docs/PLATFORM_CAPABILITIES.md):各平台已验证能力清单
- [`docs/OPERATIONS.md`](docs/OPERATIONS.md):安全配置、备份、错误处理与凭证轮换