Skip to content

Commit 108c5e4

Browse files
committed
feat(publisher): wechat credentials via env vars + .env.local auto-load
Security: - Remove app_id/app_secret from pipeline.yaml (env-only) - Settings page no longer saves wechat credentials to config - Add .env.local to .gitignore - Add scripts/output/ and *.bak.* to .gitignore UX: - Add .env.example with all configurable variables - start.sh auto-sources .env.local if present - Settings page shows env var setup instructions - Run detail page shows publisher status when credentials missing - README adds sections 5.3 (env.local setup) and 5.4 (IP whitelist prereqs) - README Publisher row notes draft-only behavior and credential requirements
1 parent 69399bb commit 108c5e4

8 files changed

Lines changed: 151 additions & 39 deletions

File tree

.env.example

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
# ContentPipe service
2-
CONTENTPIPE_HOST=0.0.0.0
3-
CONTENTPIPE_PORT=8765
4-
CONTENTPIPE_PUBLIC_BASE_URL=http://localhost:8765
5-
CONTENTPIPE_AUTH_TOKEN=change-me
6-
CONTENTPIPE_NOTIFY_CHANNEL=
7-
CONTENTPIPE_LOG_LEVEL=INFO
8-
9-
# OpenClaw Gateway
10-
OPENCLAW_GATEWAY_URL=http://host.docker.internal:18789
11-
12-
# Optional publishing credentials
13-
WECHAT_APPID=
14-
WECHAT_SECRET=
1+
# ContentPipe 环境变量配置示例
2+
# 复制此文件为 .env.local 并填入真实值,不要提交到仓库
3+
4+
# === 微信公众号发布配置(可选) ===
5+
# 如需自动创建公众号草稿,必须配置以下两项
6+
# 同时需要在公众号后台把当前服务器出口 IP 加入白名单
7+
WECHAT_APPID=wx_your_app_id_here
8+
WECHAT_SECRET=your_app_secret_here
9+
10+
# === LLM API Keys(根据你用的模型选填) ===
11+
# OPENAI_API_KEY=sk-...
12+
# ANTHROPIC_API_KEY=sk-ant-...
13+
# DASHSCOPE_API_KEY=sk-...
14+
15+
# === ContentPipe 服务配置 ===
16+
# CONTENTPIPE_PORT=8765
17+
# CONTENTPIPE_HOST=0.0.0.0
18+
# CONTENTPIPE_LOG_LEVEL=INFO
19+
20+
# === OpenClaw Gateway ===
21+
# OPENCLAW_GATEWAY_URL=http://localhost:18789

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Thumbs.db
2222

2323
# Secrets / local config
2424
.env
25+
.env.local
26+
scripts/output/
27+
*.bak.*
2528
.env.*
2629
!.env.example
2730
settings.local.yaml

README.md

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ContentPipe 是一个面向公众号 / 图文平台的内容生产系统。它
66

77
- **Scout**:选题与切入角度
88
- **Researcher**:事实核查与证据包
9-
- **Writer**:成稿生成
9+
- **Writer**唯一作者人格(成稿生成 + 审核聊天改稿)
1010
- **Director**:配图规划与视觉风格
1111
- **Image Gen**:生图
1212
- **Formatter**:排版与模板适配
@@ -60,16 +60,18 @@ scout → researcher → writer → director → image_gen → formatter → pub
6060
|---|---|---|
6161
| Scout | 热点扫描、选题提案、切入角度、writer brief ||
6262
| Researcher | 事实核查、数据点、风险与禁写项 ||
63-
| Writer | 生成文章草稿,结合 writer_context 三层结构 ||
63+
| Writer | 连续主 session 负责写稿/追问/改稿;fresh 结构 LLM 负责整理正文、反 AI 对抗与正式落盘,Python 做最终提交仲裁 ||
6464
| Director | 规划配图位置、风格、目的、描述 ||
6565
| Image Gen | 根据规划生成图片 | ⚙️ |
6666
| Formatter | 将 Markdown 转为平台 HTML,并插图 ||
6767
| Publisher | 公众号 / 小红书导出或发布 | ⚙️ |
6868

69+
> 注:微信公众号发布默认目标是**草稿箱**,不是直接群发。未配置 `WECHAT_APPID` / `WECHAT_SECRET` 时,Publisher 仅本地保存,不会真正调用微信发布接口。
70+
6971
### 2.2 关键特性
7072

7173
- **Per-node session**:每个节点独立会话,执行记录与审核聊天共享上下文
72-
- **实时同步**审核聊天中的明确修改,会自动同步回左侧结构化数据 / 文章正文
74+
- **逐轮提交仲裁**每个节点在执行/审核/重试后都要经过 Python 读回正式产物,完成最小提交判定、提交并刷新左侧结果
7375
- **图文精确匹配**:使用 `after_section` 定位,把图片插入指定段落下方
7476
- **模板适配**:支持深色/浅色模板的内联样式输出
7577
- **导向式审核**:在 Web UI 中查看节点输出卡片、文章、配图方案、预览
@@ -98,13 +100,21 @@ ContentPipe Service (FastAPI)
98100

99101
### 3.1.1 blank-agent 执行平面
100102

101-
`llm_mode=gateway` 下,ContentPipe 不再默认让节点直接消费聊天窗口里的文本输出,而是优先走一个低污染执行平面
103+
`llm_mode=gateway` 下,ContentPipe 通过一个低污染执行平面来承载节点的正式产物修改
102104

103105
- Gateway 请求显式路由到 `contentpipe-blank`
104-
- 每个节点仍保留独立 session key
105-
- blank-agent 负责把最终产物**直接写入正式项目目录**
106+
- 每个节点保留独立 session key
107+
- 同一个节点的初始执行与后续审核追问共享同一个主 session / 主提示词
108+
- 节点可以直接修改自己的正式产物文件(`edit``write` 都可以)
109+
- Python 在**每一轮 LLM 运行后**都负责读回正式产物,做最小提交判定、提交、更新 state 与刷新 UI
110+
- 若最小提交判定失败,Python 会把失败反馈送回对应节点继续修复
111+
- 更深层的内容 / schema 校验可作为下一阶段补齐
106112
- Pipeline 下游节点只消费正式产物文件,不依赖聊天解释文字
107113

114+
当前实现状态:
115+
- Scout / Researcher / Director / Formatter 审核聊天已接到“直接修改正式产物 → Python 读回提交”的主链路
116+
- Writer 使用“连续主 session + fresh 结构 helper”模式:主 session 负责改稿,结构 helper 落正式正文
117+
108118
当前正式约定:
109119

110120
- agent id: `contentpipe-blank`
@@ -248,7 +258,7 @@ pipeline:
248258
scout: "anthropic/claude-sonnet-4-6"
249259
researcher: "anthropic/claude-sonnet-4-6"
250260
writer: "openai-codex/gpt-5.4"
251-
de_ai_editor: "anthropic/claude-sonnet-4-6"
261+
de_ai_editor: "anthropic/claude-sonnet-4-6" # 内部 polish
252262
director: "anthropic/claude-opus-4-6"
253263
director_refine: "dashscope/qwen3.5-plus"
254264
```
@@ -273,12 +283,61 @@ DASHSCOPE_API_KEY=...
273283
ANTHROPIC_API_KEY=...
274284
```
275285

286+
> ⚠️ 如果你希望 **Publisher 真正把内容创建到微信公众号草稿箱**,必须显式配置环境变量:
287+
> - `WECHAT_APPID`
288+
> - `WECHAT_SECRET`
289+
>
290+
> 同时还需要把当前服务端出口 IP 加入微信公众号后台的 **IP 白名单**,否则会报 `invalid ip ... not in whitelist`
291+
>
292+
> 如果这两个环境变量未配置,ContentPipe 只能完成本地排版与产物保存,**不会真正发布到任何公众号后台**
293+
276294
说明:
277295
- `CONTENTPIPE_NOTIFY_CHANNEL` 为空时,不会发送 Discord 通知
278296
- `CONTENTPIPE_PUBLIC_BASE_URL` 用于 Discord 通知里的回链地址
279297
- `CONTENTPIPE_AUTH_TOKEN` 非空时,Web UI / API 会开启鉴权(浏览器登录或请求头 `X-ContentPipe-Token`
280298
- 发布相关密钥建议只通过环境变量或本地未跟踪配置注入
281299

300+
### 5.3 推荐配置方式(.env.local)
301+
302+
1. 复制示例文件:
303+
```bash
304+
cp .env.example .env.local
305+
```
306+
307+
2. 编辑 `.env.local`,填入真实值:
308+
```bash
309+
WECHAT_APPID=wx_your_app_id_here
310+
WECHAT_SECRET=your_app_secret_here
311+
```
312+
313+
3. `start.sh` 会自动加载 `.env.local`
314+
315+
> ⚠️ **安全提醒**
316+
> - `.env.local` 已加入 `.gitignore`,不会被提交
317+
> - 不要把真实凭证写进 `pipeline.yaml` 或代码
318+
> - 如果凭证已泄露,立即去公众号后台重置 AppSecret
319+
320+
### 5.4 微信公众号发布前置条件
321+
322+
除了配置 `WECHAT_APPID``WECHAT_SECRET`,还必须:
323+
324+
1. **添加 IP 白名单**
325+
- 登录公众号后台:`mp.weixin.qq.com`
326+
- 进入:**设置与开发 → 开发接口管理 → 基本配置 → IP 白名单**
327+
- 添加当前服务器出口 IP
328+
329+
2. **获取当前出口 IP**
330+
```bash
331+
curl -s https://api.ipify.org
332+
```
333+
334+
3. **验证白名单是否生效**
335+
```bash
336+
curl "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$WECHAT_APPID&secret=$WECHAT_SECRET"
337+
```
338+
- 成功返回:`{"access_token":"...","expires_in":7200}`
339+
- 失败返回:`{"errcode":40164,"errmsg":"invalid ip ... not in whitelist"}`
340+
282341
---
283342

284343
## 6. 启动方式
@@ -570,8 +629,8 @@ git push origin main
570629
### 已完成
571630

572631
- 交互式多节点 pipeline
573-
- 实时同步(聊天 → 结构化状态 / 文章
574-
- Writer 三层上下文
632+
- 实时同步(每轮 LLM 运行后都经 Python 读回 / 最小提交判定 / 刷新
633+
- Writer 三层上下文 + 连续主 session + fresh 结构 LLM
575634
- 图文精确匹配
576635
- Director 配图管理
577636
- 基础插件化(服务清单、健康检查、通知)

config/pipeline.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ scout:
3636
- "Agent"
3737
suggestions_count: 5
3838

39-
# 账号配置
39+
# 微信公众号发布配置
40+
wechat:
41+
author: "Mister Panda"
42+
43+
# 账号配置(兼容旧字段,待移除)
4044
accounts:
4145
wechat:
42-
author: "ContentPipe"
43-
# appid/secret 通过环境变量: WECHAT_APPID, WECHAT_SECRET
46+
author: "Mister Panda"

scripts/web/routes/pages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
from pathlib import Path
10+
import os
1011

1112
from fastapi import APIRouter, Form, Request
1213
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -98,11 +99,13 @@ async def run_detail(request: Request, run_id: str):
9899
run = get_run(run_id)
99100
if not run:
100101
return HTMLResponse("<h1>Run not found</h1>", status_code=404)
102+
env_ready = bool(os.getenv("WECHAT_APPID")) and bool(os.getenv("WECHAT_SECRET"))
101103
return templates.TemplateResponse("run_detail.html", {
102104
"request": request,
103105
"run": run,
104106
"nodes": PIPELINE_NODES,
105107
"page": "runs",
108+
"wechat_publish_ready": env_ready,
106109
})
107110

108111

scripts/web/templates/run_detail.html

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
▶ 继续
2323
</button>
2424
{% endif %}
25+
{% if run.status == 'running' and run.current_stage == 'image_gen' %}
26+
<button onclick="doActionRedirect('/api/runs/{{ run.run_id }}/rollback/image-gen-to-director', '图片生成较慢,回到 AI 导演审核态?这会清空当前已生成/待生成图片状态。', '/runs/{{ run.run_id }}/review?node=director')"
27+
class="px-3 py-1.5 bg-amber-600 text-white text-sm rounded-lg hover:bg-amber-500 transition">
28+
↩ 回到 AI 导演审核态
29+
</button>
30+
{% endif %}
2531
{% if run.status in ['running', 'review', 'pending'] %}
2632
<button onclick="doAction('/api/runs/{{ run.run_id }}/cancel', '确定暂停此 Run?')"
2733
class="btn-warn px-3 py-1.5 text-sm">
@@ -160,6 +166,12 @@
160166
{% if node.status == 'review' %}审核 →{% else %}💬 查看{% endif %}
161167
</a>
162168
{% endif %}
169+
{% if node.id == 'image_gen' and run.status == 'running' and run.current_stage == 'image_gen' %}
170+
<button onclick="doActionRedirect('/api/runs/{{ run.run_id }}/rollback/image-gen-to-director', '图片生成较慢,回到 AI 导演审核态?这会清空当前已生成/待生成图片状态。', '/runs/{{ run.run_id }}/review?node=director')"
171+
class="px-2 py-1 text-xs rounded bg-amber-600 text-white hover:bg-amber-500 transition">
172+
↩ 回导演
173+
</button>
174+
{% endif %}
163175

164176
{% if node.status == 'completed' %}
165177
<button onclick="toggleNodeOutput(this, '{{ run.run_id }}', '{{ node.id }}')"
@@ -198,19 +210,40 @@ <h3 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">📄
198210
<h3 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">🎨 配图方案</h3>
199211
<span class="text-xs text-slate-500">{{ run.visual_plan.style | default('') }}</span>
200212
</div>
201-
<p class="text-xs text-slate-400 mb-3">{{ run.visual_plan.global_tone | default('') | truncate(120) }}</p>
213+
<p class="text-xs text-slate-400 mb-3 whitespace-pre-wrap break-words">{{ run.visual_plan.global_tone | default('') }}</p>
202214
<div class="space-y-2">
203215
{% for p in run.visual_plan.placements | default([]) %}
204216
<div class="bg-slate-800/50 rounded p-3">
205217
<p class="text-xs text-sky-400 mb-1">配图 {{ loop.index }} · {{ p.purpose | default('') }}</p>
206-
<p class="text-sm text-slate-300">{{ p.description | default('') | truncate(150) }}</p>
218+
{% set desc = p.description | default('') %}
219+
{% if desc|length > 160 %}
220+
<details class="text-sm text-slate-300 group">
221+
<summary class="cursor-pointer list-none text-slate-300 hover:text-white">
222+
{{ desc[:160] }}…
223+
<span class="text-sky-400 ml-1 group-open:hidden">展开</span>
224+
<span class="text-sky-400 ml-1 hidden group-open:inline">收起</span>
225+
</summary>
226+
<p class="mt-2 whitespace-pre-wrap break-words">{{ desc }}</p>
227+
</details>
228+
{% else %}
229+
<p class="text-sm text-slate-300 whitespace-pre-wrap break-words">{{ desc }}</p>
230+
{% endif %}
207231
</div>
208232
{% endfor %}
209233
</div>
210234
</div>
211235
{% endif %}
212236

213237
<!-- 快捷操作 -->
238+
{% if run.current_stage == 'publisher' and not wechat_publish_ready %}
239+
<div class="card p-4 mt-4 border-amber-700/30 bg-amber-900/10">
240+
<h3 class="text-amber-300 text-sm font-semibold mb-2">📌 Publisher 当前不会真正发到公众号</h3>
241+
<p class="text-xs text-amber-100/90 leading-relaxed">
242+
当前环境未检测到 <code>WECHAT_APPID</code><code>WECHAT_SECRET</code>
243+
Publisher 只能本地保存产物,不会创建微信公众号草稿。
244+
</p>
245+
</div>
246+
{% endif %}
214247
{% if run.status == 'completed' %}
215248
<div class="mt-4 flex gap-3">
216249
<a href="/runs/{{ run.run_id }}/preview"

scripts/web/templates/settings.html

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,14 @@ <h2 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4">
5757
<h2 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-4">📱 微信公众号</h2>
5858

5959
{% set wechat = settings.get('wechat', {}) %}
60+
<div class="mb-3 p-3 rounded-lg bg-amber-900/20 border border-amber-700/30 text-xs text-amber-200 leading-relaxed">
61+
真正发布到微信公众号草稿箱需要在启动服务前配置环境变量:
62+
<br><code class="text-amber-100">export WECHAT_APPID=你的AppID</code>
63+
<br><code class="text-amber-100">export WECHAT_SECRET=你的AppSecret</code>
64+
<br><br>并且需要把当前服务器出口 IP 加入公众号后台的 <strong>IP 白名单</strong>
65+
<br>如果未配置,Publisher 只会本地保存 HTML / 图片产物,不会创建公众号草稿。
66+
</div>
6067
<div class="space-y-3">
61-
<div class="flex items-center gap-4">
62-
<label class="w-28 text-sm text-slate-300">AppID</label>
63-
<input type="text" name="wechat_app_id" value="{{ wechat.get('app_id', '') }}"
64-
placeholder="wx..."
65-
class="flex-1 bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-white text-sm focus:border-sky-500 focus:outline-none">
66-
</div>
67-
<div class="flex items-center gap-4">
68-
<label class="w-28 text-sm text-slate-300">AppSecret</label>
69-
<input type="password" name="wechat_app_secret" value="{{ wechat.get('app_secret', '') }}"
70-
class="flex-1 bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-white text-sm focus:border-sky-500 focus:outline-none">
71-
</div>
7268
<div class="flex items-center gap-4">
7369
<label class="w-28 text-sm text-slate-300">作者名</label>
7470
<input type="text" name="wechat_author" value="{{ wechat.get('author', 'ContentPipe') }}"

start.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ set -euo pipefail
66

77
SCRIPT_PATH="$(readlink -f "$0")"
88
PLUGIN_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
9+
10+
# 加载本地环境变量(如果存在)
11+
if [ -f "$PLUGIN_DIR/.env.local" ]; then
12+
set -a
13+
source "$PLUGIN_DIR/.env.local"
14+
set +a
15+
echo "✅ 已加载 .env.local"
16+
fi
917
APP_NAME="contentpipe"
1018
PORT="${CONTENTPIPE_PORT:-8765}"
1119
HOST="${CONTENTPIPE_HOST:-0.0.0.0}"

0 commit comments

Comments
 (0)