重构为单用户模式:去掉多客户架构 + 新增 Onboard/环境检查 + 修复 10 项问题
架构转变:从代运营多客户模式改为开源单用户模式。 - 去掉 clients/ 目录,style.yaml/history.yaml 扁平化到 skill root - Step 1 简化(不再提取客户名,直接读 style.yaml) - 新增 Step 0 环境检查(config/依赖/API 配置,降级标记传递到后续 Step) - Onboard 改为首次设置流程(交互式问答 + 支持"用默认的直接写") - 3 个脚本去掉 --client 参数,路径扁平化 - 修复 10 项 workflow 问题(降级传递、历史写入、wechat-constraints 引用等) - evals 更新为单用户模式的 3 个场景 - 新增 style.example.yaml 作为默认模板 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffcc186578
commit
dd1de0d1e9
9 changed files with 296 additions and 140 deletions
20
.gitignore
vendored
20
.gitignore
vendored
|
|
@ -1,10 +1,20 @@
|
||||||
# Credentials
|
# Credentials
|
||||||
config.yaml
|
config.yaml
|
||||||
|
|
||||||
# Generated output (keep directory structure)
|
# User data (generated at runtime, not tracked)
|
||||||
output/**/
|
style.yaml
|
||||||
|
history.yaml
|
||||||
|
playbook.md
|
||||||
|
corpus/
|
||||||
|
lessons/
|
||||||
|
|
||||||
|
# Generated output
|
||||||
|
output/
|
||||||
!output/.gitkeep
|
!output/.gitkeep
|
||||||
|
|
||||||
|
# Legacy client directories
|
||||||
|
clients/
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|
@ -19,9 +29,3 @@ build/
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Client data (demo is tracked as template)
|
|
||||||
clients/*/corpus/
|
|
||||||
clients/*/lessons/
|
|
||||||
clients/*/playbook.md
|
|
||||||
!clients/demo/
|
|
||||||
|
|
|
||||||
223
SKILL.md
223
SKILL.md
|
|
@ -3,8 +3,8 @@ name: wewrite
|
||||||
description: |
|
description: |
|
||||||
微信公众号内容全流程助手:热点抓取 → 选题 → 框架 → 写作 → SEO/去AI痕迹 → 视觉AI → 排版推送草稿箱。
|
微信公众号内容全流程助手:热点抓取 → 选题 → 框架 → 写作 → SEO/去AI痕迹 → 视觉AI → 排版推送草稿箱。
|
||||||
触发关键词:公众号、推文、微信文章、微信推文、草稿箱、微信排版、选题、热搜、
|
触发关键词:公众号、推文、微信文章、微信推文、草稿箱、微信排版、选题、热搜、
|
||||||
热点抓取、封面图、配图、客户配置名(如 demo/techbro)+ 写作任务。
|
热点抓取、封面图、配图、写公众号、写一篇。
|
||||||
也覆盖:markdown 转微信格式、学习用户改稿风格、文章数据复盘、新建客户配置。
|
也覆盖:markdown 转微信格式、学习用户改稿风格、文章数据复盘、风格设置。
|
||||||
不应被通用的"写文章"、blog、邮件、PPT、抖音/短视频、网站 SEO 触发——
|
不应被通用的"写文章"、blog、邮件、PPT、抖音/短视频、网站 SEO 触发——
|
||||||
需要有公众号/微信等明确上下文。
|
需要有公众号/微信等明确上下文。
|
||||||
---
|
---
|
||||||
|
|
@ -13,7 +13,7 @@ description: |
|
||||||
|
|
||||||
## 快速理解
|
## 快速理解
|
||||||
|
|
||||||
你是一个公众号内容编辑 Agent。用户给你一个客户名,你完成从热点抓取到草稿箱推送的全部工作。
|
你是用户的公众号内容编辑 Agent。用户让你写文章,你完成从热点抓取到草稿箱推送的全部工作。
|
||||||
|
|
||||||
**默认全自动**——不要中途停下来问用户选哪个选题、选哪个框架。自动选最优的,一口气跑完全流程。只在出错时才停下来。
|
**默认全自动**——不要中途停下来问用户选哪个选题、选哪个框架。自动选最优的,一口气跑完全流程。只在出错时才停下来。
|
||||||
|
|
||||||
|
|
@ -21,21 +21,60 @@ description: |
|
||||||
|
|
||||||
每一步都有降级方案,不要因为某一步失败就停下来。
|
每一步都有降级方案,不要因为某一步失败就停下来。
|
||||||
|
|
||||||
|
**降级标记**:Step 0 会检测哪些能力可用。如果某项 API 不可用,会设置降级标记。后续 Step 看到标记时直接走降级路径,不要重复报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 执行流程
|
## 执行流程
|
||||||
|
|
||||||
### Step 1: 确定客户
|
### Step 0: 环境检查(每次执行都跑,静默通过或引导修复)
|
||||||
|
|
||||||
从用户消息中提取客户名称,读取配置:
|
在做任何事之前,快速检查运行环境。**如果全部通过,不要输出任何内容,直接进 Step 1。** 只在发现问题时才停下来引导用户。
|
||||||
|
|
||||||
|
#### 0a. config.yaml
|
||||||
|
|
||||||
```
|
```
|
||||||
读取: {skill_dir}/clients/{client}/style.yaml
|
检查: {skill_dir}/config.yaml 是否存在
|
||||||
```
|
```
|
||||||
|
|
||||||
如果客户目录不存在,告诉用户:
|
- **存在** → 静默通过
|
||||||
- 参考 `{skill_dir}/references/style-template.md` 创建配置
|
- **不存在** → 告知用户:
|
||||||
- 或复制 `clients/demo/style.yaml` 作为模板
|
1. 复制 `config.example.yaml` 为 `config.yaml`
|
||||||
|
2. 必填:`wechat.appid` + `wechat.secret`(推送草稿箱需要)
|
||||||
|
3. 可选:`image.api_key`(AI 生图需要,不配也能跑,只是跳过生图)
|
||||||
|
4. 提供命令:`cp {skill_dir}/config.example.yaml {skill_dir}/config.yaml`
|
||||||
|
5. **如果用户说"先不配微信"** → 设置降级标记 `skip_publish = true`,继续流程
|
||||||
|
|
||||||
从 style.yaml 中提取:`topics`、`tone`、`voice`、`blacklist`、`theme`、`cover_style`、`author`、`content_style`。
|
#### 0b. Python 依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "import markdown, bs4, cssutils, requests, yaml, pygments, PIL" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
- **通过** → 静默继续
|
||||||
|
- **失败** → 告知用户缺少依赖,提供命令:`pip install -r {skill_dir}/requirements.txt`,等用户确认已安装后继续
|
||||||
|
|
||||||
|
#### 0c. API 配置检查(仅在 config.yaml 存在时)
|
||||||
|
|
||||||
|
读取 config.yaml,检查关键字段:
|
||||||
|
|
||||||
|
| 字段 | 缺失时处理 |
|
||||||
|
|------|-----------|
|
||||||
|
| `wechat.appid` + `wechat.secret` | 设置 `skip_publish = true`,警告"微信 API 未配置,本次将跳过推送,生成本地 HTML" |
|
||||||
|
| `image.api_key` | 设置 `skip_image_gen = true`,警告"图片 API 未配置,本次将跳过 AI 生图,输出提示词供手动生成" |
|
||||||
|
|
||||||
|
**不要因为 API 未配置就停止流程。** 设置降级标记,到对应 Step 时自动走降级路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: 加载风格配置
|
||||||
|
|
||||||
|
```
|
||||||
|
检查: {skill_dir}/style.yaml 是否存在
|
||||||
|
```
|
||||||
|
|
||||||
|
- **存在** → 读取,提取:`name`、`topics`、`tone`、`voice`、`blacklist`、`theme`、`cover_style`、`author`、`content_style`
|
||||||
|
- **不存在** → 进入 Onboard 流程(见下方章节),完成后回到此处继续
|
||||||
|
|
||||||
如果用户直接给了选题(如"写一篇关于 AI Agent 的公众号文章"),跳过 Step 2-3,直接进入 Step 3.5。
|
如果用户直接给了选题(如"写一篇关于 AI Agent 的公众号文章"),跳过 Step 2-3,直接进入 Step 3.5。
|
||||||
|
|
||||||
|
|
@ -58,12 +97,14 @@ python3 {skill_dir}/scripts/fetch_hotspots.py --limit 30
|
||||||
### Step 2.5: 历史读取 + SEO 数据
|
### Step 2.5: 历史读取 + SEO 数据
|
||||||
|
|
||||||
```
|
```
|
||||||
读取: {skill_dir}/clients/{client}/history.yaml
|
读取: {skill_dir}/history.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
提取已发布文章的 topic_keywords 列表,用于 Step 3 去重。
|
如果文件不存在或为空 → 跳过去重和偏好分析,直接进 Step 3。
|
||||||
|
|
||||||
如果 history.yaml 中有带 stats 的文章,提取表现最好的文章特征(框架类型、标题风格),作为偏好参考。
|
如果存在:
|
||||||
|
- 提取已发布文章的 topic_keywords 列表,用于 Step 3 去重
|
||||||
|
- 如果有带 stats 的文章,提取表现最好的文章特征(框架类型、标题风格),作为偏好参考
|
||||||
|
|
||||||
然后对热点中的关键词做 SEO 评分:
|
然后对热点中的关键词做 SEO 评分:
|
||||||
|
|
||||||
|
|
@ -73,6 +114,8 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
|
|
||||||
脚本返回每个关键词的 SEO 评分(0-10)和相关关键词,用于 Step 3 的 SEO 友好度评估。
|
脚本返回每个关键词的 SEO 评分(0-10)和相关关键词,用于 Step 3 的 SEO 友好度评估。
|
||||||
|
|
||||||
|
**降级**:如果 SEO 脚本报错,回退到 LLM 判断 SEO 友好度。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 3: 选题生成
|
### Step 3: 选题生成
|
||||||
|
|
@ -109,8 +152,8 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
|
|
||||||
```
|
```
|
||||||
读取: {skill_dir}/references/writing-guide.md
|
读取: {skill_dir}/references/writing-guide.md
|
||||||
读取: {skill_dir}/clients/{client}/playbook.md(如果存在)
|
读取: {skill_dir}/playbook.md(如果存在)
|
||||||
读取: {skill_dir}/clients/{client}/history.yaml(读取最近 3 篇的 dimensions 字段)
|
读取: {skill_dir}/history.yaml(读取最近 3 篇的 dimensions 字段)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4a. 维度随机化(写作前必须执行)
|
#### 4a. 维度随机化(写作前必须执行)
|
||||||
|
|
@ -122,6 +165,8 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
3. 对比 history.yaml 最近 3 篇的 dimensions 记录,如果完全重复则重新随机
|
3. 对比 history.yaml 最近 3 篇的 dimensions 记录,如果完全重复则重新随机
|
||||||
4. 将选中的维度作为**硬性写作约束**注入后续写作——不是建议,是必须贯穿全文的约束
|
4. 将选中的维度作为**硬性写作约束**注入后续写作——不是建议,是必须贯穿全文的约束
|
||||||
|
|
||||||
|
**降级**:如果 history.yaml 不存在或为空,跳过去重检查,直接随机。
|
||||||
|
|
||||||
**示例输出**:
|
**示例输出**:
|
||||||
```
|
```
|
||||||
本次激活维度:
|
本次激活维度:
|
||||||
|
|
@ -141,9 +186,9 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
- 避开 blacklist
|
- 避开 blacklist
|
||||||
- **去AI痕迹在此步执行,不是写完再改**——writing-guide.md 的 7 层规则必须在初稿阶段就全部生效
|
- **去AI痕迹在此步执行,不是写完再改**——writing-guide.md 的 7 层规则必须在初稿阶段就全部生效
|
||||||
|
|
||||||
**Playbook 优先**:如果 playbook.md 存在,其中的规则优先于 writing-guide.md 的通用规则。比如 playbook 说"从不用问句结尾"而 writing-guide 建议用反问句,以 playbook 为准。playbook 是客户的个性,writing-guide 是通用底线。
|
**Playbook 优先**:如果 playbook.md 存在,其中的规则优先于 writing-guide.md 的通用规则。比如 playbook 说"从不用问句结尾"而 writing-guide 建议用反问句,以 playbook 为准。playbook 是用户的个性,writing-guide 是通用底线。
|
||||||
|
|
||||||
保存到 `{skill_dir}/output/{client}/{date}-{slug}.md`
|
保存到 `{skill_dir}/output/{date}-{slug}.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -176,7 +221,7 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
7. **维度随机化层**:确认 Step 4a 选中的维度贯穿全文,不是只出现一两次
|
7. **维度随机化层**:确认 Step 4a 选中的维度贯穿全文,不是只出现一两次
|
||||||
8. **段落节奏**:无连续 2 个长度接近(±20字)的段落
|
8. **段落节奏**:无连续 2 个长度接近(±20字)的段落
|
||||||
|
|
||||||
**如果任何一项不通过**:定位具体段落,针对性重写该段落(不要全文重写),然后重新检查该项。
|
**如果任何一项不通过**:定位具体段落,针对性重写该段落(不要全文重写),然后重新检查该项。如果同一段落重写 3 次仍不通过,标注该项跳过,继续流程。
|
||||||
|
|
||||||
覆盖保存终稿。自动模式下选评分最高的标题作为最终标题。
|
覆盖保存终稿。自动模式下选评分最高的标题作为最终标题。
|
||||||
|
|
||||||
|
|
@ -184,6 +229,8 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
|
|
||||||
### Step 6: 视觉AI
|
### Step 6: 视觉AI
|
||||||
|
|
||||||
|
**如果 Step 0 设置了 `skip_image_gen = true`** → 跳过 6b,只执行 6a 生成提示词,输出供用户手动生成。
|
||||||
|
|
||||||
```
|
```
|
||||||
读取: {skill_dir}/references/visual-prompts.md
|
读取: {skill_dir}/references/visual-prompts.md
|
||||||
```
|
```
|
||||||
|
|
@ -209,13 +256,13 @@ python3 {skill_dir}/scripts/seo_keywords.py --json {从热点标题中提取的3
|
||||||
# 封面(2.35:1 微信封面比例)
|
# 封面(2.35:1 微信封面比例)
|
||||||
python3 {skill_dir}/toolkit/image_gen.py \
|
python3 {skill_dir}/toolkit/image_gen.py \
|
||||||
--prompt "{封面提示词}" \
|
--prompt "{封面提示词}" \
|
||||||
--output {skill_dir}/output/{client}/{date}-cover.png \
|
--output {skill_dir}/output/{date}-cover.png \
|
||||||
--size cover
|
--size cover
|
||||||
|
|
||||||
# 内文配图(16:9 横版)
|
# 内文配图(16:9 横版)
|
||||||
python3 {skill_dir}/toolkit/image_gen.py \
|
python3 {skill_dir}/toolkit/image_gen.py \
|
||||||
--prompt "{配图提示词}" \
|
--prompt "{配图提示词}" \
|
||||||
--output {skill_dir}/output/{client}/{date}-img{序号}.png \
|
--output {skill_dir}/output/{date}-img{序号}.png \
|
||||||
--size article
|
--size article
|
||||||
|
|
||||||
# 可通过 --provider 覆盖默认 provider(doubao/openai)
|
# 可通过 --provider 覆盖默认 provider(doubao/openai)
|
||||||
|
|
@ -229,6 +276,12 @@ python3 {skill_dir}/toolkit/image_gen.py \
|
||||||
|
|
||||||
### Step 7: 排版 + 推送草稿
|
### Step 7: 排版 + 推送草稿
|
||||||
|
|
||||||
|
**如果 Step 0 设置了 `skip_publish = true`** → 直接走降级路径(本地 HTML preview),不要尝试推送。
|
||||||
|
|
||||||
|
```
|
||||||
|
读取: {skill_dir}/references/wechat-constraints.md(排版时参考微信平台限制)
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 {skill_dir}/toolkit/cli.py publish {markdown_path} \
|
python3 {skill_dir}/toolkit/cli.py publish {markdown_path} \
|
||||||
--cover {cover_path} \
|
--cover {cover_path} \
|
||||||
|
|
@ -238,10 +291,10 @@ python3 {skill_dir}/toolkit/cli.py publish {markdown_path} \
|
||||||
|
|
||||||
如果有 cover 就加 `--cover`,没有就不加。
|
如果有 cover 就加 `--cover`,没有就不加。
|
||||||
|
|
||||||
**降级**:如果 publish 失败,改用 preview:
|
**降级**:如果 publish 失败或 `skip_publish = true`,改用 preview:
|
||||||
```bash
|
```bash
|
||||||
python3 {skill_dir}/toolkit/cli.py preview {markdown_path} \
|
python3 {skill_dir}/toolkit/cli.py preview {markdown_path} \
|
||||||
--theme {theme} --no-open -o {output_dir}/{slug}.html
|
--theme {theme} --no-open -o {skill_dir}/output/{slug}.html
|
||||||
```
|
```
|
||||||
告知用户本地 HTML 路径。
|
告知用户本地 HTML 路径。
|
||||||
|
|
||||||
|
|
@ -249,7 +302,7 @@ python3 {skill_dir}/toolkit/cli.py preview {markdown_path} \
|
||||||
|
|
||||||
### Step 7.5: 写入历史
|
### Step 7.5: 写入历史
|
||||||
|
|
||||||
发布成功后,向 `{skill_dir}/clients/{client}/history.yaml` 追加一条记录:
|
**不管是推送成功还是走了降级路径,都要写入历史。** 向 `{skill_dir}/history.yaml` 追加记录:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- date: "{今天日期}"
|
- date: "{今天日期}"
|
||||||
|
|
@ -257,12 +310,12 @@ python3 {skill_dir}/toolkit/cli.py preview {markdown_path} \
|
||||||
topic_source: "热点抓取" # 或 "用户指定"
|
topic_source: "热点抓取" # 或 "用户指定"
|
||||||
topic_keywords: ["{关键词1}", "{关键词2}"]
|
topic_keywords: ["{关键词1}", "{关键词2}"]
|
||||||
framework: "{使用的框架类型}"
|
framework: "{使用的框架类型}"
|
||||||
word_count: {字数}
|
word_count: {实际字数}
|
||||||
media_id: "{media_id}"
|
media_id: "{media_id}" # 降级时为 null
|
||||||
dimensions: # Step 4a 随机选中的维度,用于下次去重
|
dimensions: # Step 4a 实际选中的维度
|
||||||
- "叙事视角: 对话体"
|
- "{维度1}: {选项}"
|
||||||
- "主类比域: 烹饪"
|
- "{维度2}: {选项}"
|
||||||
- "节奏型: 快慢剧烈交替"
|
- "{维度3}: {选项}"
|
||||||
stats: null # 由 fetch_stats.py 后续回填
|
stats: null # 由 fetch_stats.py 后续回填
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -294,12 +347,83 @@ python3 {skill_dir}/toolkit/cli.py preview {markdown_path} \
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Onboard(首次设置)
|
||||||
|
|
||||||
|
**触发条件**:
|
||||||
|
- Step 1 发现 `style.yaml` 不存在
|
||||||
|
- 用户明确说"重新设置风格"、"修改配置"
|
||||||
|
|
||||||
|
### Phase 1: 收集信息(交互式问答)
|
||||||
|
|
||||||
|
通过对话收集以下信息,**不要一次性列出所有问题**——一轮问 1-2 个,像聊天一样:
|
||||||
|
|
||||||
|
**必问**(缺了无法运行):
|
||||||
|
|
||||||
|
| 顺序 | 问题 | 对应字段 | 示例引导 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| 1 | 你的公众号叫什么名字?主要做什么方向? | `name` + `industry` | "比如'零号AI',做科技/互联网" |
|
||||||
|
| 2 | 主要写哪几个方向的内容? | `topics` | "比如 AI、产品设计、效率工具" |
|
||||||
|
| 3 | 你希望文章是什么风格? | `tone` | "专业严肃?轻松有趣?毒舌犀利?像朋友聊天?" |
|
||||||
|
|
||||||
|
**选问**(有默认值,用户不答就用默认):
|
||||||
|
|
||||||
|
| 问题 | 对应字段 | 默认值 |
|
||||||
|
|------|---------|-------|
|
||||||
|
| 目标读者是谁? | `target_audience` | 从 industry 推断 |
|
||||||
|
| 用什么人称写? | `voice` | "第一人称,像一个懂行的朋友" |
|
||||||
|
| 有没有绝对不能出现的词或话题? | `blacklist` | 空 |
|
||||||
|
| 有没有想参考的公众号? | `reference_accounts` | 空 |
|
||||||
|
| 署名写什么? | `author` | name 字段值 |
|
||||||
|
| 偏好哪种排版风格? | `theme` | "professional-clean" |
|
||||||
|
| 封面风格偏好? | `cover_style` | 从 industry 推断 |
|
||||||
|
| 有没有固定封面模板? | `cover_template` | 不设置 |
|
||||||
|
|
||||||
|
**快捷路径**:
|
||||||
|
- 如果用户直接甩了一段描述(如"我做科技自媒体,风格像虎嗅"),直接从中提取所有能提取的字段,只补问缺的
|
||||||
|
- 如果用户说"不设置"、"用默认的"、"直接写" → 复制 `{skill_dir}/style.example.yaml` 为 `style.yaml`,跳过所有问答
|
||||||
|
|
||||||
|
```
|
||||||
|
参考: {skill_dir}/references/style-template.md(字段说明和可用主题列表)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 生成配置
|
||||||
|
|
||||||
|
用收集到的信息自动生成 `{skill_dir}/style.yaml`。
|
||||||
|
|
||||||
|
同时确保以下文件/目录存在(不存在则创建):
|
||||||
|
- `{skill_dir}/history.yaml` → 初始化为 `articles: []`
|
||||||
|
- `{skill_dir}/corpus/` → 空目录
|
||||||
|
- `{skill_dir}/lessons/` → 空目录
|
||||||
|
|
||||||
|
生成完成后,**把 style.yaml 的内容展示给用户看一遍**,问"这个配置 OK 吗?有什么要改的?"。用户确认后继续。
|
||||||
|
|
||||||
|
### Phase 3: Playbook(可选,不阻断)
|
||||||
|
|
||||||
|
问用户:"你有没有之前写过的公众号文章?如果有 20 篇以上,我可以从中学习你的写作风格,以后写出来的文章会更像你。"
|
||||||
|
|
||||||
|
- **用户有语料** → 告知将文章(.md 或 .txt)放入 `{skill_dir}/corpus/`,然后运行:
|
||||||
|
```bash
|
||||||
|
python3 {skill_dir}/scripts/build_playbook.py
|
||||||
|
```
|
||||||
|
按脚本输出逐批阅读文章,提取风格特征,生成 `playbook.md`。
|
||||||
|
|
||||||
|
- **用户没有语料 / 暂时不想弄** → 完全正常,跳过。告知用户:"没问题,先用通用风格写,后续你可以随时说'学习我的修改'来让我逐渐适应你的风格。"
|
||||||
|
|
||||||
|
### Phase 4: 试跑
|
||||||
|
|
||||||
|
Onboard 完成后,问用户:"配置好了,要不要现在试写一篇?"
|
||||||
|
|
||||||
|
- **是** → 回到 Step 1,执行完整流程
|
||||||
|
- **否** → 告知用户下次如何触发:"下次直接说'写一篇公众号文章'就行"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 效果复盘
|
## 效果复盘
|
||||||
|
|
||||||
当用户问"文章数据怎么样"、"效果复盘"、"看看表现"时:
|
当用户问"文章数据怎么样"、"效果复盘"、"看看表现"时:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 {skill_dir}/scripts/fetch_stats.py --client {client} --days 7
|
python3 {skill_dir}/scripts/fetch_stats.py --days 7
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本会:
|
脚本会:
|
||||||
|
|
@ -316,47 +440,19 @@ python3 {skill_dir}/scripts/fetch_stats.py --client {client} --days 7
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 客户 Onboard
|
|
||||||
|
|
||||||
当用户说"新建客户"、"导入历史文章"、"建 playbook"时:
|
|
||||||
|
|
||||||
### 1. 创建客户目录
|
|
||||||
|
|
||||||
```
|
|
||||||
{skill_dir}/clients/{client}/
|
|
||||||
├── style.yaml # 复制 demo 模板,让用户填写
|
|
||||||
├── corpus/ # 用户放入历史推文 .md 文件
|
|
||||||
├── history.yaml # 空初始化
|
|
||||||
└── lessons/ # 空目录
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 生成 Playbook
|
|
||||||
|
|
||||||
用户将历史推文放入 `corpus/` 后:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 {skill_dir}/scripts/build_playbook.py --client {client}
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本输出语料统计 + 分析指令。按指令逐批阅读文章,提取风格特征,生成 `playbook.md`。
|
|
||||||
|
|
||||||
建议至少 20 篇历史文章,50+ 篇效果更好。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 学习人工修改
|
## 学习人工修改
|
||||||
|
|
||||||
当用户说"我改了,学习一下"、"学习我的修改"时:
|
当用户说"我改了,学习一下"、"学习我的修改"时:
|
||||||
|
|
||||||
### 1. 获取 draft 和 final
|
### 1. 获取 draft 和 final
|
||||||
|
|
||||||
- draft:`output/{client}/` 下最新的 .md 文件
|
- draft:`output/` 下最新的 .md 文件
|
||||||
- final:用户提供修改后的版本(粘贴或指定文件路径)
|
- final:用户提供修改后的版本(粘贴或指定文件路径)
|
||||||
|
|
||||||
### 2. 运行 diff 分析
|
### 2. 运行 diff 分析
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 {skill_dir}/scripts/learn_edits.py --client {client} --draft {draft_path} --final {final_path}
|
python3 {skill_dir}/scripts/learn_edits.py --draft {draft_path} --final {final_path}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 分析并记录
|
### 3. 分析并记录
|
||||||
|
|
@ -377,7 +473,7 @@ python3 {skill_dir}/scripts/learn_edits.py --client {client} --draft {draft_path
|
||||||
每积累 5 次 lessons,脚本会提示更新 playbook:
|
每积累 5 次 lessons,脚本会提示更新 playbook:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 {skill_dir}/scripts/learn_edits.py --client {client} --summarize
|
python3 {skill_dir}/scripts/learn_edits.py --summarize
|
||||||
```
|
```
|
||||||
|
|
||||||
读取所有 lessons,找出反复出现的 pattern(≥2 次),将其固化到 `playbook.md` 的对应章节。
|
读取所有 lessons,找出反复出现的 pattern(≥2 次),将其固化到 `playbook.md` 的对应章节。
|
||||||
|
|
@ -390,10 +486,13 @@ python3 {skill_dir}/scripts/learn_edits.py --client {client} --summarize
|
||||||
|
|
||||||
| 步骤 | 降级 |
|
| 步骤 | 降级 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 环境检查(Step 0) | 逐项引导修复,设置降级标记,不阻断可降级的部分 |
|
||||||
| 热点抓取失败 | WebSearch 替代 |
|
| 热点抓取失败 | WebSearch 替代 |
|
||||||
| 选题为空 | 请用户手动给选题 |
|
| 选题为空 | 请用户手动给选题 |
|
||||||
| SEO 关键词查询失败 | 回退到 LLM 判断 |
|
| SEO 关键词查询失败 | 回退到 LLM 判断 |
|
||||||
| 封面生成失败 | 输出提示词,用户自行生成 |
|
| 维度随机化(Step 4a) | history.yaml 为空或损坏时跳过去重,直接随机 |
|
||||||
|
| 去AI验证(Step 5b) | 同一段落重写 3 次仍不通过,标注跳过该项,继续 |
|
||||||
|
| 封面/配图生成失败 | 输出提示词,用户自行生成 |
|
||||||
| 推送失败 | 生成本地 HTML,手动操作 |
|
| 推送失败 | 生成本地 HTML,手动操作 |
|
||||||
| 历史写入失败 | 警告但不阻断流程 |
|
| 历史写入失败 | 警告但不阻断流程 |
|
||||||
| 效果数据拉取失败 | 告知用户可能需要等 24h(微信数据有延迟) |
|
| 效果数据拉取失败 | 告知用户可能需要等 24h(微信数据有延迟) |
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# 文章发布历史 — Demo科技
|
|
||||||
# 由 SKILL.md Step 7 自动维护,每次发布后追加记录
|
|
||||||
# 选题时读取,避免重复 + 分析偏好
|
|
||||||
|
|
||||||
articles: []
|
|
||||||
|
|
||||||
# 每条记录格式:
|
|
||||||
# - date: "2026-03-23"
|
|
||||||
# title: "文章标题"
|
|
||||||
# topic_source: "热点抓取" # 热点抓取 / 用户指定
|
|
||||||
# topic_keywords: ["AI", "科技股"]
|
|
||||||
# framework: "热点解读型"
|
|
||||||
# word_count: 2000
|
|
||||||
# media_id: "xxx"
|
|
||||||
# stats: # 由 fetch_stats.py 回填
|
|
||||||
# read_count: 0
|
|
||||||
# share_count: 0
|
|
||||||
# like_count: 0
|
|
||||||
# read_rate: 0 # 阅读率 = 阅读数 / 送达数
|
|
||||||
|
|
@ -3,24 +3,95 @@
|
||||||
"evals": [
|
"evals": [
|
||||||
{
|
{
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"name": "topic-writing",
|
"name": "first-time-onboard",
|
||||||
"prompt": "用 demo 的配置,写一篇关于 AI Agent 正在取代传统 SaaS 的公众号文章",
|
"prompt": "我想用 WeWrite 写公众号文章。我的公众号叫「AI前哨站」,主要写 AI 和科技方向,风格偏轻松有趣,像跟朋友聊天。不用配微信 API,先不配,直接用默认的就行。",
|
||||||
"expected_output": "一篇 1500-2500 字的公众号文章 Markdown,包含 H1 标题、H2 结构、去 AI 痕迹、符合 demo 客户的 style.yaml 风格",
|
"expected_output": "完成 Onboard 流程:通过对话收集信息生成 style.yaml,检测到无 config.yaml 时设置降级标记而非报错停止",
|
||||||
"files": []
|
"files": [],
|
||||||
|
"assertions": [
|
||||||
|
{
|
||||||
|
"name": "creates_style_yaml",
|
||||||
|
"type": "file_exists",
|
||||||
|
"description": "应在 skill 目录下生成 style.yaml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "style_has_required_fields",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "style.yaml 包含 name、topics、tone 字段"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "no_client_directory",
|
||||||
|
"type": "negative_check",
|
||||||
|
"description": "不应创建 clients/ 子目录(旧模式已移除)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "graceful_no_config",
|
||||||
|
"type": "behavior_check",
|
||||||
|
"description": "检测到无 config.yaml 时应设置降级标记并继续,而非报错停止"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "markdown-convert",
|
"name": "topic-writing-anti-ai",
|
||||||
"prompt": "把 clients/demo/ 下的 style.yaml 看一下,然后帮我用 professional-clean 主题把这段 markdown 转成微信公众号格式的 HTML 并预览:\n\n# AI时代的效率革命\n\n## 为什么传统工具不够用了\n\n说实话,2026年还在用老一套工具的人,效率已经被甩开一个身位了。\n\n不是危言耸听。上周我帮一个朋友梳理他的工作流,发现他每天花 3 小时在重复性的文档整理上。\n\n## AI 工具的正确打开方式\n\n关键不是「用 AI」,而是「让 AI 做它擅长的事」。\n\n比如数据清洗、格式转换、初稿生成——这些重复性高、规则明确的任务,AI 做得比人快 10 倍。\n\n## 写在最后\n\n工具在进化,人也得跟上。",
|
"prompt": "写一篇公众号文章,选题:为什么 AI Agent 正在杀死传统 SaaS。不要用交互模式,全自动跑完。不用推送,生成本地预览就行。",
|
||||||
"expected_output": "一个微信兼容的内联样式 HTML 文件,正确提取 H1 为标题,H2 有主题样式,可在浏览器中预览",
|
"expected_output": "一篇 1500-2500 字的公众号 Markdown 文章,通过 7 层去 AI 痕迹验证,包含维度随机化记录,保存到 output/ 目录",
|
||||||
"files": []
|
"files": [],
|
||||||
|
"assertions": [
|
||||||
|
{
|
||||||
|
"name": "article_word_count",
|
||||||
|
"type": "range_check",
|
||||||
|
"description": "文章字数在 1500-2500 之间"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "no_banned_words",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "文章不包含禁用词:首先、其次、总之、综上所述、值得注意的是、不可否认、众所周知、至关重要、不言而喻"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "has_broken_sentences",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "文章包含至少 3 处破句/不完整句(如破折号中断、自我纠正、省略)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "has_specific_details",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "文章包含具体时间/地点/人物/非整数数字等细节,而非泛化表达"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dimensions_recorded",
|
||||||
|
"type": "behavior_check",
|
||||||
|
"description": "输出中展示了本次激活的随机维度(如叙事视角、类比域等)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "saved_to_output",
|
||||||
|
"type": "file_exists",
|
||||||
|
"description": "文章 Markdown 文件保存到 output/ 目录"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "client-onboard",
|
"name": "markdown-to-wechat",
|
||||||
"prompt": "帮我新建一个叫 techbro 的客户,做科技自媒体,面向程序员群体,风格偏吐槽幽默,参考少数派和 V2EX 的调性",
|
"prompt": "帮我把下面这段 markdown 转成微信公众号格式,用 professional-clean 主题预览一下:\n\n# 为什么你应该停止追求完美代码\n\n## 完美是个陷阱\n\n说实话,我写了十年代码,最大的领悟不是什么设计模式,而是——完美的代码根本不存在。\n\n你以为 Google 的代码库很优雅?去看看 Chromium 的源码,保证刷新你的三观。\n\n## 够用就是最好的\n\n我之前一个同事,重构一个模块重构了三周。结果呢?性能提升了 2%,但错过了整个 sprint 的交付。\n\n老板的原话是:「我要的是能跑的,不是能裱起来的。」\n\n## 最后说两句\n\n写代码跟做饭一样——你妈做的菜不一定摆盘好看,但你就是爱吃。",
|
||||||
"expected_output": "在 clients/techbro/ 下创建 style.yaml(含 topics/tone/voice/blacklist 等)和空的 history.yaml",
|
"expected_output": "生成微信兼容的内联样式 HTML 文件,H1 提取为独立标题,H2 有主题样式,可在浏览器预览",
|
||||||
"files": []
|
"files": [],
|
||||||
|
"assertions": [
|
||||||
|
{
|
||||||
|
"name": "html_generated",
|
||||||
|
"type": "file_exists",
|
||||||
|
"description": "生成了 HTML 预览文件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inline_styles_only",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "HTML 使用内联 style 属性,不包含 <style> 标签(微信限制)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "h1_extracted",
|
||||||
|
"type": "content_check",
|
||||||
|
"description": "H1 标题被提取为独立标题字段,不在正文 HTML 中"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
# 如何创建客户配置
|
# 风格配置说明
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
1. 复制 `clients/demo/style.yaml` 到 `clients/{客户名}/style.yaml`
|
1. 复制 `style.example.yaml` 为 `style.yaml`
|
||||||
2. 修改配置项
|
2. 修改配置项
|
||||||
3. 对 Agent 说:「用 {客户名} 的配置写一篇公众号文章」
|
3. 对 Agent 说:「写一篇公众号文章」
|
||||||
|
|
||||||
|
也可以跳过手动配置——首次使用时 Agent 会通过对话引导你自动生成 `style.yaml`。
|
||||||
|
|
||||||
## 必填字段
|
## 必填字段
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Build a client-specific writing playbook from historical articles.
|
Build a writing playbook from historical articles.
|
||||||
|
|
||||||
Reads all .md files in clients/{client}/corpus/, analyzes writing patterns
|
Reads all .md files in corpus/, analyzes writing patterns
|
||||||
in batches via LLM, and outputs a structured playbook.md.
|
in batches via LLM, and outputs a structured playbook.md.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 build_playbook.py --client demo
|
python3 build_playbook.py
|
||||||
python3 build_playbook.py --client demo --batch-size 10
|
python3 build_playbook.py --batch-size 10
|
||||||
|
|
||||||
Requires: ANTHROPIC_API_KEY or ARK API key in environment/config.
|
Requires: ANTHROPIC_API_KEY or ARK API key in environment/config.
|
||||||
This script outputs analysis prompts to stdout for the Agent (LLM) to process.
|
This script outputs analysis prompts to stdout for the Agent (LLM) to process.
|
||||||
|
|
@ -22,9 +22,9 @@ from pathlib import Path
|
||||||
SKILL_DIR = Path(__file__).parent.parent
|
SKILL_DIR = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
def load_corpus(client: str) -> list[dict]:
|
def load_corpus() -> list[dict]:
|
||||||
"""Load all markdown files from corpus directory."""
|
"""Load all markdown files from corpus directory."""
|
||||||
corpus_dir = SKILL_DIR / "clients" / client / "corpus"
|
corpus_dir = SKILL_DIR / "corpus"
|
||||||
if not corpus_dir.exists():
|
if not corpus_dir.exists():
|
||||||
print(f"Error: corpus directory not found: {corpus_dir}", file=sys.stderr)
|
print(f"Error: corpus directory not found: {corpus_dir}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -108,13 +108,12 @@ def output_analysis_prompt(articles: list[dict], stats: dict, batch_idx: int, to
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Build writing playbook from corpus")
|
parser = argparse.ArgumentParser(description="Build writing playbook from corpus")
|
||||||
parser.add_argument("--client", required=True, help="Client name")
|
|
||||||
parser.add_argument("--batch-size", type=int, default=10, help="Articles per batch")
|
parser.add_argument("--batch-size", type=int, default=10, help="Articles per batch")
|
||||||
parser.add_argument("--stats-only", action="store_true", help="Only show corpus stats")
|
parser.add_argument("--stats-only", action="store_true", help="Only show corpus stats")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Load corpus
|
# Load corpus
|
||||||
articles = load_corpus(args.client)
|
articles = load_corpus()
|
||||||
if not articles:
|
if not articles:
|
||||||
print("Error: no articles found in corpus/", file=sys.stderr)
|
print("Error: no articles found in corpus/", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -123,7 +122,7 @@ def main():
|
||||||
stats = compute_corpus_stats(articles)
|
stats = compute_corpus_stats(articles)
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f"CORPUS ANALYSIS — {args.client}")
|
print("CORPUS ANALYSIS")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(json.dumps(stats, ensure_ascii=False, indent=2))
|
print(json.dumps(stats, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ Uses WeChat Data Analytics API to pull article performance:
|
||||||
- /datacube/getarticletotal (cumulative)
|
- /datacube/getarticletotal (cumulative)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 fetch_stats.py --client demo
|
python3 fetch_stats.py
|
||||||
python3 fetch_stats.py --client demo --days 7
|
python3 fetch_stats.py --days 7
|
||||||
|
|
||||||
Requires: wechat appid/secret in config.yaml (skill root or toolkit dir)
|
Requires: wechat appid/secret in config.yaml (skill root or toolkit dir)
|
||||||
"""
|
"""
|
||||||
|
|
@ -89,11 +89,11 @@ def fetch_article_total(token: str, date: str) -> list[dict]:
|
||||||
return data["list"]
|
return data["list"]
|
||||||
|
|
||||||
|
|
||||||
def update_history(client: str, stats_list: list[dict]):
|
def update_history(stats_list: list[dict]):
|
||||||
"""Match stats to history.yaml entries and update."""
|
"""Match stats to history.yaml entries and update."""
|
||||||
history_path = SKILL_DIR / "clients" / client / "history.yaml"
|
history_path = SKILL_DIR / "history.yaml"
|
||||||
if not history_path.exists():
|
if not history_path.exists():
|
||||||
print(f"No history.yaml found for client: {client}")
|
print("No history.yaml found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(history_path, "r", encoding="utf-8") as f:
|
with open(history_path, "r", encoding="utf-8") as f:
|
||||||
|
|
@ -138,7 +138,6 @@ def update_history(client: str, stats_list: list[dict]):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Fetch WeChat article stats")
|
parser = argparse.ArgumentParser(description="Fetch WeChat article stats")
|
||||||
parser.add_argument("--client", required=True, help="Client name")
|
|
||||||
parser.add_argument("--days", type=int, default=3, help="Days to look back")
|
parser.add_argument("--days", type=int, default=3, help="Days to look back")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
@ -152,7 +151,7 @@ def main():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
token = _get_access_token(appid, secret)
|
token = _get_access_token(appid, secret)
|
||||||
print(f"Fetching stats for client '{args.client}', last {args.days} days...")
|
print(f"Fetching stats for last {args.days} days...")
|
||||||
|
|
||||||
all_stats = []
|
all_stats = []
|
||||||
for i in range(args.days):
|
for i in range(args.days):
|
||||||
|
|
@ -163,7 +162,7 @@ def main():
|
||||||
all_stats.extend(stats)
|
all_stats.extend(stats)
|
||||||
|
|
||||||
if all_stats:
|
if all_stats:
|
||||||
update_history(args.client, all_stats)
|
update_history(all_stats)
|
||||||
else:
|
else:
|
||||||
print("No stats data found for the specified period.")
|
print("No stats data found for the specified period.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
Learn from human edits by diffing AI draft vs published final.
|
Learn from human edits by diffing AI draft vs published final.
|
||||||
|
|
||||||
Compares the original AI-generated article with the human-edited version,
|
Compares the original AI-generated article with the human-edited version,
|
||||||
categorizes the changes, and saves lessons to clients/{client}/lessons/.
|
categorizes the changes, and saves lessons to lessons/.
|
||||||
|
|
||||||
When 5+ lessons accumulate, outputs a prompt for the Agent to update playbook.md.
|
When 5+ lessons accumulate, outputs a prompt for the Agent to update playbook.md.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 learn_edits.py --client demo --draft path/to/draft.md --final path/to/final.md
|
python3 learn_edits.py --draft path/to/draft.md --final path/to/final.md
|
||||||
python3 learn_edits.py --client demo --summarize # summarize all lessons
|
python3 learn_edits.py --summarize # summarize all lessons
|
||||||
|
|
||||||
The script does structural analysis; the Agent (LLM) interprets the diffs
|
The script does structural analysis; the Agent (LLM) interprets the diffs
|
||||||
and writes the lesson YAML + playbook updates.
|
and writes the lesson YAML + playbook updates.
|
||||||
|
|
@ -111,9 +111,9 @@ def compute_diff(draft: str, final: str) -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def save_diff_for_analysis(client: str, diff_result: dict, draft_path: str, final_path: str):
|
def save_diff_for_analysis(diff_result: dict, draft_path: str, final_path: str):
|
||||||
"""Save diff data for Agent to analyze and write lessons."""
|
"""Save diff data for Agent to analyze and write lessons."""
|
||||||
lessons_dir = SKILL_DIR / "clients" / client / "lessons"
|
lessons_dir = SKILL_DIR / "lessons"
|
||||||
lessons_dir.mkdir(parents=True, exist_ok=True)
|
lessons_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
@ -148,17 +148,17 @@ def save_diff_for_analysis(client: str, diff_result: dict, draft_path: str, fina
|
||||||
return diff_file
|
return diff_file
|
||||||
|
|
||||||
|
|
||||||
def count_lessons(client: str) -> int:
|
def count_lessons() -> int:
|
||||||
"""Count existing lesson files."""
|
"""Count existing lesson files."""
|
||||||
lessons_dir = SKILL_DIR / "clients" / client / "lessons"
|
lessons_dir = SKILL_DIR / "lessons"
|
||||||
if not lessons_dir.exists():
|
if not lessons_dir.exists():
|
||||||
return 0
|
return 0
|
||||||
return len(list(lessons_dir.glob("*-diff*.yaml")))
|
return len(list(lessons_dir.glob("*-diff*.yaml")))
|
||||||
|
|
||||||
|
|
||||||
def summarize_lessons(client: str):
|
def summarize_lessons():
|
||||||
"""Load all lessons and output for Agent to update playbook."""
|
"""Load all lessons and output for Agent to update playbook."""
|
||||||
lessons_dir = SKILL_DIR / "clients" / client / "lessons"
|
lessons_dir = SKILL_DIR / "lessons"
|
||||||
if not lessons_dir.exists():
|
if not lessons_dir.exists():
|
||||||
print("No lessons directory found.")
|
print("No lessons directory found.")
|
||||||
return
|
return
|
||||||
|
|
@ -181,14 +181,13 @@ def summarize_lessons(client: str):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Learn from human edits")
|
parser = argparse.ArgumentParser(description="Learn from human edits")
|
||||||
parser.add_argument("--client", required=True, help="Client name")
|
|
||||||
parser.add_argument("--draft", help="Path to AI draft")
|
parser.add_argument("--draft", help="Path to AI draft")
|
||||||
parser.add_argument("--final", help="Path to human-edited final")
|
parser.add_argument("--final", help="Path to human-edited final")
|
||||||
parser.add_argument("--summarize", action="store_true", help="Summarize all lessons")
|
parser.add_argument("--summarize", action="store_true", help="Summarize all lessons")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.summarize:
|
if args.summarize:
|
||||||
summarize_lessons(args.client)
|
summarize_lessons()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args.draft or not args.final:
|
if not args.draft or not args.final:
|
||||||
|
|
@ -232,20 +231,20 @@ def main():
|
||||||
print(f" + {line[:80]}")
|
print(f" + {line[:80]}")
|
||||||
|
|
||||||
# Save for Agent analysis
|
# Save for Agent analysis
|
||||||
diff_file = save_diff_for_analysis(args.client, diff_result, args.draft, args.final)
|
diff_file = save_diff_for_analysis(diff_result, args.draft, args.final)
|
||||||
print(f"\nDiff saved to: {diff_file}")
|
print(f"\nDiff saved to: {diff_file}")
|
||||||
|
|
||||||
# Check if playbook update should be triggered
|
# Check if playbook update should be triggered
|
||||||
lesson_count = count_lessons(args.client)
|
lesson_count = count_lessons()
|
||||||
print(f"Total lessons for {args.client}: {lesson_count}")
|
print(f"Total lessons: {lesson_count}")
|
||||||
|
|
||||||
if lesson_count >= 5 and lesson_count % 5 == 0:
|
if lesson_count >= 5 and lesson_count % 5 == 0:
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print("PLAYBOOK UPDATE TRIGGERED")
|
print("PLAYBOOK UPDATE TRIGGERED")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
print(f"{lesson_count} lessons accumulated. Agent should:")
|
print(f"{lesson_count} lessons accumulated. Agent should:")
|
||||||
print(f"1. Read all lessons: python3 learn_edits.py --client {args.client} --summarize")
|
print(f"1. Read all lessons: python3 learn_edits.py --summarize")
|
||||||
print(f"2. Read current playbook: clients/{args.client}/playbook.md")
|
print(f"2. Read current playbook: playbook.md")
|
||||||
print(f"3. Update playbook with recurring patterns from lessons")
|
print(f"3. Update playbook with recurring patterns from lessons")
|
||||||
|
|
||||||
# Output instructions for Agent
|
# Output instructions for Agent
|
||||||
|
|
@ -262,12 +261,12 @@ Read the draft and final versions, then analyze the edits:
|
||||||
- type: "用词替换" / "段落删除" / "段落新增" / "结构调整" / "标题修改" / "语气调整"
|
- type: "用词替换" / "段落删除" / "段落新增" / "结构调整" / "标题修改" / "语气调整"
|
||||||
- before: (original text)
|
- before: (original text)
|
||||||
- after: (edited text)
|
- after: (edited text)
|
||||||
- pattern: (what this tells us about the client's preference)
|
- pattern: (what this tells us about the user's preference)
|
||||||
|
|
||||||
4. Update {diff_file} with the edits and patterns lists.
|
4. Update {diff_file} with the edits and patterns lists.
|
||||||
|
|
||||||
5. If this is a recurring pattern (seen in previous lessons too),
|
5. If this is a recurring pattern (seen in previous lessons too),
|
||||||
consider updating clients/{args.client}/playbook.md.
|
consider updating playbook.md.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
# 客户配置 — Demo 科技媒体
|
# WeWrite 风格配置
|
||||||
|
# 复制为 style.yaml 并修改为你的公众号信息
|
||||||
|
# 或让 WeWrite 在首次使用时通过对话自动生成
|
||||||
|
|
||||||
name: "Demo科技"
|
name: "Demo科技"
|
||||||
industry: "科技/互联网"
|
industry: "科技/互联网"
|
||||||
Loading…
Reference in a new issue