chore: rebuild dist/openclaw from source
This commit is contained in:
parent
e0a71be396
commit
a349dea556
4 changed files with 306 additions and 93 deletions
215
dist/openclaw/SKILL.md
vendored
215
dist/openclaw/SKILL.md
vendored
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: wewrite
|
||||
description: |
|
||||
微信公众号内容全流程助手:热点抓取 → 选题 → 框架 → 写作 → SEO/去AI痕迹 → 视觉AI → 排版推送草稿箱。
|
||||
微信公众号内容全流程助手:热点抓取 → 选题 → 框架 → 内容增强 → 写作 → SEO → 视觉AI → 排版推送草稿箱。
|
||||
触发关键词:公众号、推文、微信文章、微信推文、草稿箱、微信排版、选题、热搜、
|
||||
热点抓取、封面图、配图、写公众号、写一篇、主题画廊、排版主题、容器语法。
|
||||
也覆盖:markdown 转微信格式、学习用户改稿风格、文章数据复盘、风格设置、
|
||||
|
|
@ -36,30 +36,31 @@ description: |
|
|||
|
||||
**辅助功能**(按需加载,不在主管道内):
|
||||
- 用户说"重新设置风格" → `读取: {baseDir}/references/onboard.md`
|
||||
- 用户说"学习我的修改" → `读取: {baseDir}/references/learn-edits.md`
|
||||
- 用户说"学习我的修改" → `读取: {baseDir}/references/learn-edits.md`。支持两种来源:
|
||||
- **本地修改**(默认):用户在 `output/` 的 markdown 文件中修改
|
||||
- **微信草稿箱同步**:`python3 {baseDir}/scripts/learn_edits.py --from-wechat`,自动从草稿箱拉回最新内容,与本地原文做纯文本 diff
|
||||
- 用户说"看看文章数据" → `读取: {baseDir}/references/effect-review.md`
|
||||
- 用户说"诊断配置"/"检查反AI"/"为什么AI检测没过" → 执行以下流程:
|
||||
1. `python3 {baseDir}/scripts/diagnose.py --json`
|
||||
2. 如果有 fail 项 → 直接报告,建议修复
|
||||
3. 如果全 pass 或仅 warn → 继续 LLM 深度分析:
|
||||
- 读取 `style.yaml` 的 tone/voice 与 writing_persona,判断是否矛盾
|
||||
- 读取 `writing-config.yaml`(如存在),检查是否有 AI 特征参数(emotional_arc: flat、paragraph_rhythm: structured、closing_tendency: summary)
|
||||
- 读取 `history.yaml` 最近 5 篇,检查 persona 使用和 web_search 降级情况
|
||||
4. 综合输出自然语言报告 + 按优先级排序的改进建议
|
||||
- 用户说"优化写作参数"/"优化参数"/"跑优化" → 执行以下流程:
|
||||
1. 读取 `{baseDir}/writing-config.yaml`(不存在则从 `writing-config.example.yaml` 复制)
|
||||
2. 用户可指定迭代次数(默认 3),如"优化参数跑 5 轮"
|
||||
3. **迭代循环**(每轮):
|
||||
a. 用当前 writing-config.yaml 参数写一篇 500 字测试短文(主题:用户指定或"AI Agent 行业观察")
|
||||
b. 保存到 `{baseDir}/output/optimize-test.md`
|
||||
c. `python3 {baseDir}/scripts/humanness_score.py {baseDir}/output/optimize-test.md --json --tier3 {agent_tier3_score}`
|
||||
d. Agent 做 Tier 3 分析(读测试短文,评估风格漂移/密度波浪/连贯性打破/整体人感,输出 0-1 分数传入 --tier3)
|
||||
e. 解析 JSON 中 `param_scores`,找到得分最低的 1-2 个参数
|
||||
f. 调整 writing-config.yaml 中对应参数(方向:让该维度更"人类")
|
||||
g. 记录本轮:迭代编号、composite_score、调整的参数、旧值→新值
|
||||
4. 循环结束后,保留 composite_score 最低(最人类)的 writing-config.yaml
|
||||
5. 输出优化报告:起始分 → 最终分,每轮调整,最终参数
|
||||
6. 提示:"参数已优化。下次写文章时自动使用新参数。"
|
||||
- 用户说"检查一下"/"自检"/"这篇文章怎么样" → 对最近一篇生成的文章(或用户指定的文章)执行自检,输出生成报告:
|
||||
|
||||
**第一部分:生成档案**(告诉用户这篇文章是怎么来的)
|
||||
1. 读取 `history.yaml` 最近一条记录,提取:
|
||||
- 使用的框架类型 + 写作人格
|
||||
- 激活的维度随机化组合
|
||||
- 素材采集来源(web_search 还是降级到 LLM)
|
||||
- 内容增强策略(角度发现/密度强化/细节锚定/真实体感)
|
||||
- 范文风格库是否命中(用了哪几篇 exemplar,还是 fallback 到种子)
|
||||
- playbook 中生效的规则条数
|
||||
2. 如果 history.yaml 无记录或用户指定了外部文章 → 跳过此部分,提示"这篇文章不是 WeWrite 生成的,只做质量检查"
|
||||
|
||||
**第二部分:质量检查**(告诉用户哪里还能改)
|
||||
1. `python3 {baseDir}/scripts/humanness_score.py {article_path} --json`
|
||||
2. Agent 解读 JSON 中每项得分,翻译为用户可操作的建议,格式:
|
||||
- 每条建议定位到具体段落或句子("第 3 段连续 4 句长度接近")
|
||||
- 给出具体改法("建议把第 3 句拆成两个短句"、"这里可以加一句你自己的感受")
|
||||
- 按影响度排序,最多 5 条
|
||||
3. 如果所有项得分都不错 → "这篇文章质量不错,建议在编辑锚点处加入你的个人内容就可以发了。"
|
||||
|
||||
**输出格式**:自然语言报告,不输出 JSON 或分数(用户不需要看数字)
|
||||
- 用户说"更新"/"更新 WeWrite"/"升级" → 在 `{baseDir}` 执行 `git pull origin main`,完成后告知版本变化
|
||||
|
||||
---
|
||||
|
|
@ -133,7 +134,7 @@ python3 {baseDir}/scripts/fetch_hotspots.py --limit 30
|
|||
|
||||
**降级**:脚本报错 → web_search "今日热点 {topics第一个垂类}"
|
||||
|
||||
**2.2 历史去重 + SEO**:
|
||||
**2.2 历史分析 + SEO**:
|
||||
|
||||
```
|
||||
读取: {baseDir}/history.yaml(不存在则跳过)
|
||||
|
|
@ -143,18 +144,27 @@ python3 {baseDir}/scripts/fetch_hotspots.py --limit 30
|
|||
python3 {baseDir}/scripts/seo_keywords.py --json {关键词}
|
||||
```
|
||||
|
||||
**降级**:SEO 脚本报错 → LLM 判断
|
||||
历史分析(有 stats 数据时):
|
||||
- 统计哪种 `framework` 的文章表现最好(阅读量/分享率)→ 推荐框架时加权
|
||||
- 统计哪种 `enhance_strategy` 的文章表现最好 → 增强策略选择时参考
|
||||
- 近 7 天已写的关键词降分(去重)
|
||||
|
||||
**2.3 生成 10 个选题**:
|
||||
**降级**:SEO 脚本报错 → LLM 判断;history 无 stats → 跳过效果分析,仅做去重
|
||||
|
||||
**2.3 生成选题**:
|
||||
|
||||
```
|
||||
读取: {baseDir}/references/topic-selection.md
|
||||
```
|
||||
|
||||
每个选题含标题、评分、点击率潜力、SEO 友好度、推荐框架。近 7 天已写的关键词降分。
|
||||
生成 **10 个选题**,其中:
|
||||
- **7-8 个热点选题**:基于 2.1 的热点,按 topic-selection.md 规则评分
|
||||
- **2-3 个常青选题**:不依赖热点,从用户的 `topics` 领域生成长尾内容(教程/方法论/经验总结/工具推荐),标注为"常青"。适合 content_style 为干货型/测评型的用户
|
||||
|
||||
每个选题含标题、评分、点击率潜力、SEO 友好度、推荐框架。
|
||||
|
||||
- 自动模式 → 选最高分
|
||||
- 交互模式 → 展示 10 个,等用户选
|
||||
- 交互模式 → 展示全部,等用户选
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -166,20 +176,30 @@ python3 {baseDir}/scripts/seo_keywords.py --json {关键词}
|
|||
读取: {baseDir}/references/frameworks.md
|
||||
```
|
||||
|
||||
5 套框架(痛点/故事/清单/对比/热点解读),自动选推荐指数最高的。
|
||||
7 套框架(痛点/故事/清单/对比/热点解读/纯观点/复盘),自动选推荐指数最高的。
|
||||
|
||||
**3.2 素材采集(关键——决定能否通过 AI 检测)**:
|
||||
|
||||
纯 LLM 生成的内容无论技巧多好,底层 token 分布仍是 AI 的。通过检测的文章都建立在真实外部信息源之上。
|
||||
**3.2 素材采集 + 内容增强**(合并执行,共用搜索结果):
|
||||
|
||||
```
|
||||
web_search: "{选题关键词} site:36kr.com OR site:mp.weixin.qq.com OR site:zhihu.com"
|
||||
web_search: "{选题关键词} 数据 报告 2025 2026"
|
||||
读取: {baseDir}/references/content-enhance.md
|
||||
```
|
||||
|
||||
采集 5-8 条真实素材(具名来源 + 具体数据/引述/案例)。**禁止编造**。
|
||||
根据 3.1 选定的框架类型,一次搜索同时完成素材采集和内容增强:
|
||||
|
||||
**降级**:web_search 无结果或不可用 → 用 LLM 训练数据中可验证的公开信息。但需告知用户:"素材采集未能使用 web_search,文章的 AI 检测通过率会降低。建议在编辑锚点处多加入你自己的内容。"
|
||||
| 框架 | 搜索策略 | 从结果中提取 |
|
||||
|------|---------|-------------|
|
||||
| 热点解读 / 纯观点 | `"{关键词} site:mp.weixin.qq.com OR site:36kr.com"` + `"{关键词} 观点 OR 评论"` | 真实素材(数据/引述)**+** 已有文章的主流观点(供角度发现) |
|
||||
| 痛点 / 清单 | `"{关键词} 教程 OR 工具 OR 实操"` + `"{关键词} 数据 报告"` | 真实素材 **+** 具体工具名/步骤/参数(供密度强化) |
|
||||
| 故事 / 复盘 | `"{人物/事件} 采访 OR 专访 OR 细节"` + `"{关键词} 数据 报告"` | 真实素材 **+** 时间锚/数字锚/对话锚/感官锚(供细节锚定) |
|
||||
| 对比 | `"{方案A} vs {方案B} 评测 OR 体验"` + `"{方案A OR 方案B} 踩坑 OR 缺点 site:v2ex.com OR site:zhihu.com"` | 真实素材 **+** 真实用户评价和踩坑信息(供真实体感) |
|
||||
|
||||
每次搜索 2 轮,从结果中**同时**提取:
|
||||
1. **素材**:5-8 条真实素材(具名来源 + 具体数据/引述/案例)。**禁止编造**。
|
||||
2. **增强材料**:按 content-enhance.md 对应策略的要求提取(角度/密度要点/细节/用户声音)。
|
||||
|
||||
两者并入框架大纲,一起传入 Step 4 写作。
|
||||
|
||||
**降级**:web_search 不可用 → 用 LLM 训练数据中可验证的公开信息。但需告知用户:"素材采集未能使用 web_search,建议在编辑锚点处多加入你自己的内容。"密度强化不依赖搜索,始终执行。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -188,33 +208,36 @@ web_search: "{选题关键词} 数据 报告 2025 2026"
|
|||
```
|
||||
读取: {baseDir}/references/writing-guide.md
|
||||
读取: {baseDir}/playbook.md(如果存在,按 confidence 分级执行)
|
||||
读取: {baseDir}/writing-config.yaml(如果存在,作为写作参数)
|
||||
读取: {baseDir}/history.yaml(最近 3 篇的 dimensions 字段)
|
||||
读取: {baseDir}/history.yaml(最近 3 篇的 dimensions + closing_type 字段)
|
||||
读取: {baseDir}/references/exemplars/index.yaml(如果存在)
|
||||
```
|
||||
|
||||
**4.1 历史最佳参数参考**(有 history.yaml 且包含 composite_score 时执行):
|
||||
**4.1 维度随机化**:
|
||||
|
||||
读取 history.yaml 中有 `composite_score` 和 `writing_config_snapshot` 的文章,找到得分最低(最人类)的一篇。如果该篇得分比当前 writing-config.yaml 的默认参数对应的历史平均分更好,在写作时**参考**其参数组合(不是覆盖 writing-config.yaml,而是作为"上次这组参数效果好"的提示)。
|
||||
从以下维度池随机激活 2-3 个维度,让每篇文章的表达方式不同。如果 history.yaml 有最近 3 篇的 `dimensions` 字段,避免使用相同组合。
|
||||
|
||||
具体:如果历史最佳文章的某个参数值与当前 writing-config 不同,在写作时倾向使用历史最佳值。如果没有历史数据,跳过此步。
|
||||
| 维度 | 选项 |
|
||||
|------|------|
|
||||
| 叙事视角 | 第一人称亲历 / 旁观者分析 / 对话体 / 自问自答 |
|
||||
| 时间线 | 正序 / 倒叙 / 插叙 |
|
||||
| 类比域 | 体育 / 做饭 / 军事 / 恋爱 / 游戏 / 电影 / 建筑 / 医学 |
|
||||
| 情绪基调 | 克制冷静 / 热血激动 / 讽刺吐槽 / 温暖治愈 / 焦虑警示 |
|
||||
| 节奏 | 短句密集 / 长叙述慢推 / 长短急切交替 / 慢开头快收尾 |
|
||||
|
||||
**4.2 维度随机化**:从 writing-guide.md 规则 3.4 维度池随机激活 2-3 个维度,对比历史去重。
|
||||
|
||||
**4.3 加载写作人格**:
|
||||
**4.2 加载写作人格**:
|
||||
|
||||
```
|
||||
读取: {baseDir}/personas/{style.yaml 的 writing_persona 字段}.yaml
|
||||
如果 style.yaml 没有 writing_persona 字段 → 默认 midnight-friend
|
||||
```
|
||||
|
||||
人格文件定义了:语气浓度、数据呈现方式、情绪弧线、段落节奏、不确定性表达模板等。作为 4.4 的硬性约束执行。
|
||||
人格文件定义了:语气浓度、数据呈现方式、情绪弧线、段落节奏、不确定性表达模板等。作为写作的硬性约束执行。
|
||||
|
||||
**优先级**:playbook.md(confidence ≥ 5 的规则)> persona > 范文风格 > writing-guide.md。writing-guide 是底线(禁用词等),范文提供风格示范(句长节奏、情绪表达方式),persona 在此基础上特化风格参数(语气浓度、数据呈现),playbook 中高置信度规则是用户个性化的最终覆盖。playbook 中 confidence < 5 的规则作为软性参考。
|
||||
**优先级**:playbook.md(confidence ≥ 5 的规则)> persona > 范文风格 > writing-guide.md。writing-guide 是底线(基础写作规范),范文提供风格示范(句长节奏、情绪表达方式),persona 在此基础上特化风格参数(语气浓度、数据呈现),playbook 中高置信度规则是用户个性化的最终覆盖。playbook 中 confidence < 5 的规则作为软性参考。
|
||||
|
||||
**4.4 范文风格注入**(有 `references/exemplars/index.yaml` 时执行):
|
||||
**4.3 范文风格注入**(有 `references/exemplars/index.yaml` 时执行):
|
||||
|
||||
从 index.yaml 筛选 category 匹配当前框架类型的范文,按 humanness_score 升序(越低越人类)取 top 3。读取对应 .md 文件的片段内容。
|
||||
从 index.yaml 筛选 category 匹配当前框架类型的范文,取 top 3。读取对应 .md 文件的片段内容。
|
||||
|
||||
在写作 prompt 中注入:
|
||||
|
||||
|
|
@ -236,10 +259,10 @@ Category 映射规则:
|
|||
|
||||
| 框架类型 | exemplar category |
|
||||
|----------|-------------------|
|
||||
| 痛点型/深度解读 | tech-opinion |
|
||||
| 故事型 | story-emotional |
|
||||
| 清单型/对比型 | list-practical |
|
||||
| 热点解读型 | hot-take |
|
||||
| 痛点型 | tech-opinion |
|
||||
| 故事型 / 复盘型 | story-emotional |
|
||||
| 清单型 / 对比型 | list-practical |
|
||||
| 热点解读型 / 纯观点型 | hot-take |
|
||||
| 其他 | general |
|
||||
|
||||
如果匹配到的范文不足 3 篇,用 general category 补足。
|
||||
|
|
@ -258,26 +281,31 @@ Category 映射规则:
|
|||
|
||||
建库命令:`python3 {baseDir}/scripts/extract_exemplar.py article.md`
|
||||
|
||||
**4.5 写文章**:
|
||||
**4.4 写文章**:
|
||||
- H1 标题(20-28 字) + H2 结构,1500-2500 字
|
||||
- 真实素材锚定:Step 3.2 的素材分散嵌入各 H2 段落
|
||||
- **写作人格**:按 4.3 加载的人格参数写作(数据呈现方式、个人声音浓度、不确定性表达等)
|
||||
- **收尾方式**:persona 的 `closing_tendency` 仅作为倾向参考。根据文章内容和情绪弧线自行判断最自然的收尾方式(参见 writing-guide.md 收尾多样性表)。如果 history.yaml 中最近 3 篇有 `closing_type` 字段,避免使用相同的收尾类型
|
||||
- 3 层反检测规则(统计/语言/内容)在初稿阶段全部生效
|
||||
- **素材 + 增强约束**:Step 3.2 的素材和增强材料分散嵌入各 H2 段落。增强策略的核心输出(角度/密度要点/细节/用户声音)必须贯穿全文,不只装饰性出现一次
|
||||
- **写作人格**:按 4.2 加载的人格参数写作(数据呈现方式、个人声音浓度、不确定性表达等)
|
||||
- **收尾方式**:persona 的 `closing_tendency` 仅作为倾向参考。根据文章内容和情绪弧线自行判断最自然的收尾方式。如果 history.yaml 中最近 3 篇有 `closing_type` 字段,避免使用相同的收尾类型
|
||||
- **写作规范**:writing-guide.md 中的基础规则(禁用词、句长方差、词汇混用等)在初稿阶段生效
|
||||
- 2-3 个编辑锚点:`<!-- ✏️ 编辑建议:在这里加一句你自己的经历/看法 -->`
|
||||
- 可选容器语法:`:::dialogue`、`:::timeline`、`:::callout`、`:::quote`
|
||||
|
||||
保存到 `{baseDir}/output/{date}-{slug}.md`
|
||||
|
||||
**4.6 快速自检**(写完后立即执行,减少 Step 5 重写概率):
|
||||
**4.5 快速自检**(写完后立即执行,减少 Step 5 重写概率):
|
||||
|
||||
对初稿做 3 项最易不达标的快速扫描,**当场修复**,不留到 Step 5:
|
||||
对初稿做 5 项快速扫描,**当场修复**,不留到 Step 5:
|
||||
|
||||
1. **禁用词扫描**:检查 writing-guide.md 2.1 的禁用词列表,命中的直接替换(最常见的问题,修复成本最低)
|
||||
2. **句长方差检查**:粗略扫描是否有连续 3 句以上长度接近的段落,有则拆句或加短句
|
||||
3. **负面情绪检查**:全文是否有 ≥ 2 处真实负面表达,不够则在编辑锚点附近补充
|
||||
**写作层面**:
|
||||
1. **禁用词扫描**:检查 writing-guide.md 2.1 的禁用词列表,命中的直接替换
|
||||
2. **句长方差**:是否有连续 3 句以上长度接近的段落,有则拆句或加短句
|
||||
|
||||
这 3 项检查不需要调用脚本,LLM 自行完成即可。目标是让初稿在进入 Step 5 前已经消除最明显的问题。
|
||||
**内容层面**:
|
||||
3. **开头钩子**:前 3 句是否制造了悬念/冲突/好奇心?如果是平铺直叙的背景介绍,重写开头
|
||||
4. **增强贯穿**:增强策略的核心输出是否只出现在一段?如果是,在其他 H2 中补充
|
||||
5. **金句检查**:全文是否有至少 1 句可独立截图转发的句子?如果没有,在情绪高点处补一句
|
||||
|
||||
LLM 自行完成,不需要调用脚本。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -289,36 +317,43 @@ Category 映射规则:
|
|||
|
||||
**5.1 SEO**:3 个备选标题 + 摘要(≤54 字)+ 5 标签 + 关键词密度优化
|
||||
|
||||
**5.2 去 AI 逐层验证**(writing-guide.md 自检清单,每项必须通过):
|
||||
**5.2 质量验证**(两个维度,每项逐一检查):
|
||||
|
||||
| 层级 | 检查项 | 标准 | 规则 |
|
||||
|------|--------|------|------|
|
||||
| 统计 | 句长方差 | 最短与最长句相差 ≥ 30 字 | 1.1 |
|
||||
| 统计 | 词汇温度 | 任意 500 字 ≥ 3 种温度 | 1.2 |
|
||||
| 统计 | 段落节奏 | 无连续 2 个相近长度段落 | 1.3 |
|
||||
| 统计 | 情绪极性 | 负面情绪 ≥ 2 处,无平铺直叙 | 1.4 |
|
||||
| 统计 | 副词密度 | 无连续两句以副词开头 | 1.5 |
|
||||
| 统计 | 风格漂移 | 不同 H2 语气/正式度有差异 | 1.6 |
|
||||
| 语言 | 禁用词 | 命中数 = 0 | 2.1 |
|
||||
| 语言 | 破句 | ≥ 3 处 | 2.2 |
|
||||
| 语言 | 意外用词 | ≥ 1 处非常规但说得通的表达 | 2.3 |
|
||||
| 语言 | 连贯性 | ≥ 1 处跑题再拉回 | 2.4 |
|
||||
| 内容 | 真实锚定 | 每个 H2 ≥ 1 条真实素材,零编造 | 3.1 |
|
||||
| 内容 | 具体性 | 每 500 字 ≥ 2 处具体细节 | 3.2 |
|
||||
| 内容 | 密度波浪 | 高密度段后跟低密度段 | 3.3 |
|
||||
| 内容 | 维度贯穿 | 激活维度全文可见 | 3.4 |
|
||||
**A. 写作质量**(writing-guide.md 基础规则):
|
||||
|
||||
| 检查项 | 标准 | 规则 |
|
||||
|--------|------|------|
|
||||
| 句长方差 | 最短与最长句相差 ≥ 30 字 | 1.1 |
|
||||
| 词汇温度 | 任意 500 字 ≥ 3 种温度 | 1.2 |
|
||||
| 段落节奏 | 无连续 2 个相近长度段落 | 1.3 |
|
||||
| 情绪极性 | 负面情绪 ≥ 2 处,无平铺直叙 | 1.4 |
|
||||
| 禁用词 | 命中数 = 0 | 2.1 |
|
||||
| 真实锚定 | 每个 H2 ≥ 1 条真实素材,零编造 | 3.1 |
|
||||
| 具体性 | 每 500 字 ≥ 2 处具体细节 | 3.2 |
|
||||
|
||||
**B. 内容质量**(基于 Step 3.2 的增强策略检查):
|
||||
|
||||
| 检查项 | 标准 | 适用框架 |
|
||||
|--------|------|---------|
|
||||
| 增强贯穿 | 增强策略的核心输出(角度/密度/细节/体感)在全文可见,不只出现在一段 | 所有 |
|
||||
| 开头钩子 | 前 3 句能制造悬念、冲突或好奇心(不是背景铺垫) | 所有 |
|
||||
| 金句密度 | 至少 1 处可独立截图转发的句子 | 所有 |
|
||||
| 操作密度 | 每个 H2 有可操作要点(工具/步骤/参数) | 痛点/清单 |
|
||||
| 角度锐度 | 核心观点能引发同意或反对,不是"两面都有道理" | 热点解读/纯观点 |
|
||||
| 场景感 | 至少 2 处有时间/地点/对话等画面细节 | 故事/复盘 |
|
||||
| 真实声音 | 至少 1 处引用真实用户评价或体验 | 对比 |
|
||||
|
||||
不通过 → **定向修复**:只替换不达标的具体句子/段落,不动已通过的部分。每轮最多改 3 处,改完立即重新检查该项。2 轮仍不过 → 标注跳过,继续下一项。
|
||||
|
||||
**5.3 脚本验证**(补充逐项检查):
|
||||
**5.3 脚本辅助验证**(补充 5.2 的逐项检查):
|
||||
|
||||
Agent 在 5.2 逐项检查时同步完成 Tier 3 评估(风格漂移、密度波浪、连贯性打破、整体人感),产出 0-1 分数。
|
||||
Agent 在 5.2 检查过程中同步完成综合评估(各 H2 之间的语气差异度、信息密度的高低交替、段落间的节奏变化、整体阅读流畅度),产出 0-1 分数。
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/humanness_score.py {article_path} --json --tier3 {agent_tier3_score}
|
||||
```
|
||||
|
||||
解读 JSON 中 `composite_score`:
|
||||
解读 JSON 中 `composite_score`(0=质量高, 100=问题多):
|
||||
- < 30 → 通过,继续 Step 6
|
||||
- 30-50 → 查看 `param_scores` 中最低分的 1-2 项,只修复对应的具体句子(不重写整段),改完重新打分。1 轮即可
|
||||
- \> 50 → 取 `param_scores` 最低的 2-3 项,逐项定向修复(每项只改最相关的 1-2 处),最多 2 轮。仍 > 50 则标记 DONE_WITH_CONCERNS 继续
|
||||
|
|
@ -385,14 +420,16 @@ python3 {baseDir}/toolkit/cli.py preview {markdown} --theme {theme} --no-open -o
|
|||
title: "{标题}"
|
||||
topic_source: "热点抓取" # 或 "用户指定"
|
||||
topic_keywords: ["{词1}", "{词2}"]
|
||||
output_file: "{output 文件路径}" # e.g. output/2026-03-31-zhangxue-slow-accumulation.md
|
||||
framework: "{框架}"
|
||||
enhance_strategy: "{增强策略}" # angle_discovery/density_boost/detail_anchoring/real_feel
|
||||
word_count: {字数}
|
||||
media_id: "{id}" # 降级时 null
|
||||
writing_persona: "{人格名}"
|
||||
dimensions:
|
||||
- "{维度}: {选项}"
|
||||
closing_type: "{收尾类型}" # trailing_off/unanswered/scene_revert/abrupt_stop/anti_conclusion/image
|
||||
composite_score: {Step 5.3 的 composite_score} # 0=人类, 100=AI
|
||||
composite_score: {Step 5.3 的 composite_score} # 0=质量高, 100=问题多
|
||||
writing_config_snapshot: # 本次使用的关键参数(从 writing-config.yaml 提取)
|
||||
sentence_variance: {值}
|
||||
paragraph_rhythm: "{值}"
|
||||
|
|
@ -408,8 +445,7 @@ python3 {baseDir}/toolkit/cli.py preview {markdown} --theme {theme} --no-open -o
|
|||
**8.2 回复用户**:
|
||||
|
||||
- 最终标题 + 2 备选 + 摘要 + 5 标签 + media_id
|
||||
- 编辑建议:"文章有 2-3 个编辑锚点,建议花 3-5 分钟加入你自己的话,效果更好。"
|
||||
- 飞轮提示:"编辑完成后说**'学习我的修改'**,下次初稿会更接近你的风格。"
|
||||
- 编辑建议:"文章有 2-3 个编辑锚点,建议加入你自己的话。你可以在本地 markdown 里改,也可以直接在微信草稿箱改——改完后说**'学习我的修改'**,WeWrite 都能学到你的风格。"
|
||||
|
||||
**8.3 后续操作**:
|
||||
|
||||
|
|
@ -422,10 +458,9 @@ python3 {baseDir}/toolkit/cli.py preview {markdown} --theme {theme} --no-open -o
|
|||
| 看看有什么主题 | `python3 {baseDir}/toolkit/cli.py gallery` |
|
||||
| 换成 XX 主题 | 重新渲染 |
|
||||
| 看看文章数据 | `读取: {baseDir}/references/effect-review.md` |
|
||||
| 学习我的修改 | `读取: {baseDir}/references/learn-edits.md` |
|
||||
| 学习我的修改 | `读取: {baseDir}/references/learn-edits.md`。支持本地 markdown 修改和微信草稿箱同步(`--from-wechat`) |
|
||||
| 做一个小绿书/图片帖 | `python3 {baseDir}/toolkit/cli.py image-post img1.jpg img2.jpg -t "标题"` |
|
||||
| 诊断配置 / 检查反AI / 为什么AI检测没过 | `python3 {baseDir}/scripts/diagnose.py --json` + LLM 交叉分析 |
|
||||
| 优化写作参数 / 优化参数 | 迭代循环:写测试短文 → 打分 → 调参(见辅助功能) |
|
||||
| 检查一下 / 自检 / 这篇文章怎么样 | 生成报告(生成档案 + 质量检查,见辅助功能) |
|
||||
| 导入范文 / 建范文库 | `python3 {baseDir}/scripts/extract_exemplar.py article.md` |
|
||||
| 查看范文库 | `python3 {baseDir}/scripts/extract_exemplar.py --list` |
|
||||
|
||||
|
|
|
|||
2
dist/openclaw/VERSION
vendored
2
dist/openclaw/VERSION
vendored
|
|
@ -1 +1 @@
|
|||
1.3.3
|
||||
1.3.4
|
||||
|
|
|
|||
138
dist/openclaw/scripts/learn_edits.py
vendored
138
dist/openclaw/scripts/learn_edits.py
vendored
|
|
@ -16,7 +16,8 @@ The Agent uses this to write structured playbook.md rules.
|
|||
|
||||
Usage:
|
||||
python3 learn_edits.py --draft path/to/draft.md --final path/to/final.md
|
||||
python3 learn_edits.py --summarize # all lessons with confidence
|
||||
python3 learn_edits.py --from-wechat # auto-sync from WeChat draft box
|
||||
python3 learn_edits.py --summarize # all lessons with confidence
|
||||
python3 learn_edits.py --summarize --json # JSON output for agent
|
||||
"""
|
||||
|
||||
|
|
@ -48,6 +49,123 @@ def load_text(path: str) -> str:
|
|||
return Path(path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def markdown_to_plaintext(md: str) -> str:
|
||||
"""Strip markdown formatting to plain text for diff comparison."""
|
||||
text = md
|
||||
# Remove HTML comments (editing anchors etc.)
|
||||
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
||||
# Remove markdown headers markers
|
||||
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
|
||||
# Remove bold/italic markers
|
||||
text = re.sub(r"\*{1,3}(.*?)\*{1,3}", r"\1", text)
|
||||
# Remove inline code
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
# Remove link syntax [text](url) → text
|
||||
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||
# Remove image syntax
|
||||
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
||||
# Collapse whitespace
|
||||
text = re.sub(r"[ \t]+", " ", text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def fetch_wechat_draft() -> tuple[str, str, str]:
|
||||
"""
|
||||
Fetch the latest draft from WeChat and find the corresponding local file.
|
||||
Returns (draft_plaintext, final_plaintext, draft_path).
|
||||
"""
|
||||
# Load config
|
||||
config_path = SKILL_DIR / "config.yaml"
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError("config.yaml not found — need WeChat API credentials")
|
||||
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
wechat = config.get("wechat", {})
|
||||
appid = wechat.get("appid", "")
|
||||
secret = wechat.get("secret", "")
|
||||
if not appid or not secret:
|
||||
raise ValueError("config.yaml missing wechat.appid or wechat.secret")
|
||||
|
||||
# Load history to find latest article with media_id
|
||||
history_path = SKILL_DIR / "history.yaml"
|
||||
if not history_path.exists():
|
||||
raise FileNotFoundError("history.yaml not found — no articles to compare")
|
||||
|
||||
with open(history_path) as f:
|
||||
history = yaml.safe_load(f) or []
|
||||
|
||||
# Find most recent article with media_id
|
||||
latest = None
|
||||
for article in reversed(history):
|
||||
if article.get("media_id"):
|
||||
latest = article
|
||||
break
|
||||
|
||||
if not latest:
|
||||
raise ValueError("No article with media_id found in history.yaml")
|
||||
|
||||
media_id = latest["media_id"]
|
||||
title = latest.get("title", "")
|
||||
|
||||
# Find the local draft file
|
||||
# Priority: output_file field → title slug match → largest file
|
||||
date = latest.get("date", "")
|
||||
output_dir = SKILL_DIR / "output"
|
||||
draft_path = None
|
||||
|
||||
# First try: exact path from history
|
||||
output_file = latest.get("output_file", "")
|
||||
if output_file:
|
||||
candidate = SKILL_DIR / output_file if not Path(output_file).is_absolute() else Path(output_file)
|
||||
if candidate.exists():
|
||||
draft_path = candidate
|
||||
|
||||
if not draft_path and date:
|
||||
candidates = sorted(output_dir.glob(f"{date}-*.md"))
|
||||
if len(candidates) == 1:
|
||||
draft_path = candidates[0]
|
||||
elif len(candidates) > 1:
|
||||
# Multiple files on same date — try to match by title keywords
|
||||
title_lower = title.lower()
|
||||
for c in candidates:
|
||||
slug = c.stem.replace(date + "-", "").replace("-", " ")
|
||||
# Check if slug words appear in title
|
||||
if any(w in title_lower for w in slug.split() if len(w) > 1):
|
||||
draft_path = c
|
||||
break
|
||||
if not draft_path:
|
||||
# Fallback: use the largest file (likely the final version)
|
||||
draft_path = max(candidates, key=lambda p: p.stat().st_size)
|
||||
|
||||
if not draft_path or not draft_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Cannot find local draft for '{title}' (date={date}) in output/"
|
||||
)
|
||||
|
||||
# Get access token and fetch draft from WeChat
|
||||
sys.path.insert(0, str(SKILL_DIR / "toolkit"))
|
||||
from wechat_api import get_access_token
|
||||
from publisher import get_draft, html_to_plaintext
|
||||
|
||||
token = get_access_token(appid, secret)
|
||||
html = get_draft(token, media_id)
|
||||
wechat_text = html_to_plaintext(html)
|
||||
|
||||
# Convert local draft to plaintext
|
||||
local_md = load_text(str(draft_path))
|
||||
local_text = markdown_to_plaintext(local_md)
|
||||
|
||||
print(f"本地文件: {draft_path}")
|
||||
print(f"微信草稿: media_id={media_id}")
|
||||
print(f"文章标题: {title}")
|
||||
print(f"本地字数: {len(local_text)}, 微信字数: {len(wechat_text)}")
|
||||
|
||||
return local_text, wechat_text, str(draft_path)
|
||||
|
||||
|
||||
def split_sections(text: str) -> list[dict]:
|
||||
"""Split markdown into sections by H2 headers."""
|
||||
sections = []
|
||||
|
|
@ -276,6 +394,8 @@ def main():
|
|||
parser = argparse.ArgumentParser(description="Learn from human edits")
|
||||
parser.add_argument("--draft", help="Path to AI draft")
|
||||
parser.add_argument("--final", help="Path to human-edited final")
|
||||
parser.add_argument("--from-wechat", action="store_true",
|
||||
help="Auto-fetch edited version from WeChat draft box")
|
||||
parser.add_argument("--summarize", action="store_true", help="Summarize all lessons")
|
||||
parser.add_argument("--json", action="store_true", help="JSON output (with --summarize)")
|
||||
args = parser.parse_args()
|
||||
|
|
@ -284,8 +404,22 @@ def main():
|
|||
summarize_lessons(as_json=args.json)
|
||||
return
|
||||
|
||||
if args.from_wechat:
|
||||
local_text, wechat_text, draft_path = fetch_wechat_draft()
|
||||
if local_text == wechat_text:
|
||||
print("\n微信草稿与本地文件内容一致,没有修改。")
|
||||
return
|
||||
diff_result = compute_diff(local_text, wechat_text)
|
||||
# Save with special marker for wechat source
|
||||
lesson_file = save_lesson(diff_result, draft_path, f"wechat:{draft_path}")
|
||||
print(f"\nLesson saved to: {lesson_file}")
|
||||
print(f"\n检测到 {diff_result['lines_added']} 处新增, {diff_result['lines_deleted']} 处删除")
|
||||
print(f"字数变化: {diff_result['char_diff']:+d}")
|
||||
print(f"\nAgent 接下来读取 {draft_path} 和微信草稿内容,分析修改模式并写入 {lesson_file}")
|
||||
return
|
||||
|
||||
if not args.draft or not args.final:
|
||||
print("Error: --draft and --final required", file=sys.stderr)
|
||||
print("Error: --draft and --final required (or use --from-wechat)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
draft = load_text(args.draft)
|
||||
|
|
|
|||
44
dist/openclaw/toolkit/publisher.py
vendored
44
dist/openclaw/toolkit/publisher.py
vendored
|
|
@ -68,6 +68,50 @@ def create_draft(
|
|||
return DraftResult(media_id=data["media_id"])
|
||||
|
||||
|
||||
def get_draft(access_token: str, media_id: str) -> str:
|
||||
"""
|
||||
Get draft content from WeChat by media_id.
|
||||
API: POST https://api.weixin.qq.com/cgi-bin/draft/get
|
||||
Returns the HTML content of the first article.
|
||||
"""
|
||||
resp = requests.post(
|
||||
"https://api.weixin.qq.com/cgi-bin/draft/get",
|
||||
params={"access_token": access_token},
|
||||
json={"media_id": media_id},
|
||||
)
|
||||
resp.encoding = "utf-8"
|
||||
data = resp.json()
|
||||
|
||||
errcode = data.get("errcode", 0)
|
||||
if errcode != 0:
|
||||
errmsg = data.get("errmsg", "unknown error")
|
||||
raise ValueError(f"WeChat get_draft error: errcode={errcode}, errmsg={errmsg}")
|
||||
|
||||
articles = data.get("news_item", [])
|
||||
if not articles:
|
||||
raise ValueError(f"WeChat get_draft: no articles in draft {media_id}")
|
||||
|
||||
return articles[0].get("content", "")
|
||||
|
||||
|
||||
def html_to_plaintext(html: str) -> str:
|
||||
"""Extract plain text from WeChat HTML, stripping all tags and styles."""
|
||||
import re
|
||||
# Remove script/style blocks
|
||||
text = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
||||
# Replace block-level tags with newlines
|
||||
text = re.sub(r"<(br|p|div|section|h[1-6])[^>]*>", "\n", text, flags=re.IGNORECASE)
|
||||
# Remove all remaining tags
|
||||
text = re.sub(r"<[^>]+>", "", text)
|
||||
# Decode HTML entities
|
||||
import html as html_module
|
||||
text = html_module.unescape(text)
|
||||
# Collapse whitespace
|
||||
text = re.sub(r"[ \t]+", " ", text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def create_image_post(
|
||||
access_token: str,
|
||||
title: str,
|
||||
|
|
|
|||
Loading…
Reference in a new issue