Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ openclaw/
# addon crews copied into crews/ at install time — not tracked
.pnpm-store/

# 本地凭据(relay 无状态多租户改造后,client 自管凭据)
crews/main/skills/wx-mp-publisher/accounts.json
# 本地凭据(relay 无状态多租户改造后,client 自管凭据,放源仓外 ~/.openclaw/ 下)
skills/wxwork-drive/spaces.json

# Claude Code 会话目录
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ metadata:
- `always: true` 的真实语义是"跳过 `requires` 二进制/env 检查直接判定 eligible"(见 `config-eval.ts:124`),**不是**"强制注入整个 SKILL.md"。如果 skill 没声明 `requires`,加 `always: true` 等于无意义,应删除。
- 加载阶段 openclaw 只把 `name` + `description` + SKILL.md 绝对路径塞进 system prompt 的 `<available_skills>` 块;agent 用到时才主动 read 全文。所以 frontmatter 写得再多也不会污染 system prompt,但反过来也意味着——除上述识别字段外,多余字段不会带来任何运行时收益。

## SKILL.md 内容书写规范

SKILL.md 是写给**执行时的智能体**看的操作手册,不是开发日志。内容只写指导性指令、正例、反例:

- ✅ 直接写"应该怎么做""不要怎么做",配 ✅ 正例 / ❌ 反例
- ❌ 不写排查故事、历史往来、开发经历、思路背景、踩坑全过程
- 经验教训要提炼成一条可执行的规则,而不是一段叙事

## skill 依赖打包规则

产品拆分后(D8)addons/ 结构已销毁,skill 只有两层:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ xiaobei 由Wiseflow (原AI首席情报官)作者 bigbrother666sh 开发。
>
> - YouTube:https://youtu.be/eK8aWWCNVZQ?si=K-MbWai-j6ydqCdy
> - bilibili:https://www.bilibili.com/video/BV1euMR6PEDh
> - 开发与使用心得: https://mp.weixin.qq.com/s/zxvWdCMUd0XquWxujvxkSg

- **🔄 数据闭环彻底打通,为自我进化奠基**:`content-calibrator`(打分+预测)与 `published-track`(数据复盘)机制全面完善,内容产出→发布→数据回流→下一轮策略调优的闭环不再有断点,为后续小贝自主迭代选题与打法提供燃料。现在主力四平台(微信视频号、微信公众号、小红书、抖音)全系支持内容自动生产、自动发布、自动取数、自动复盘
- **安装脚本大幅优化**:对比 5.6.0,install.sh / install-atomgit.sh / install.ps1 / install-atomgit.ps1 四脚本修了若干实测踩坑——tarball 下载原子写、Windows `.env`/`daemon.env` 换行与 BOM、技能 wrapper 在 Windows 用 `.cmd` shim 替代软链、`OPENCLAW_HOME` 在 `set -u` 下报 unbound、atomgit 国内线路默认跳过 gum spinner bootstrap 避免连 GitHub 超时、Windows 软链需要开发者模式等,产品稳定性显著提高。
Expand Down
2 changes: 1 addition & 1 deletion crews/main/HEARTBEAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@

1. 各平台数据更新情况(成功/跳过/失败数量)
2. **取数端 Cookie 失效列表**(如有):
> ⚠️ 以下**取数端**Cookie 已失效,数据未能更新。请白天使用 login-manager 技能重新登录
> ⚠️ 以下**取数端**Cookie 已失效,数据未能更新。请白天通知小贝重新登录
> - douyin(抖音)
> - xhs-browse(小红书浏览端)
> - wechat-channel(微信视频号)
Expand Down
46 changes: 46 additions & 0 deletions crews/main/skills/_shared/runtime_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""runtime_root.py — 把 Path(__file__).resolve() 算出的 ROOT 映射回 OpenClaw 运行时工作区。

背景:部署到 ``~/.openclaw/workspace-<crew>/skills/<name>`` 的 skill 是指向源仓
``~/wiseflow/crews/<crew>/skills/<name>`` 的 symlink。脚本的 ``Path(__file__).resolve()``
会跟随 symlink 跳进源仓,导致 ROOT 指向源仓而非运行时工作区——DB / prediction.md
等运行时数据都在工作区下,源仓里没有,于是全部 ``exists()=False``。

本 helper 按 marker 文件是否存在判定,把 ROOT 映射回 ``~/.openclaw/workspace-<crew>``。
"""
from __future__ import annotations

import os
from pathlib import Path


def resolve_runtime_root(
naive_root: Path,
marker: str = "db/published_track.db",
env_var: str = "PUBLISHED_TRACK_ROOT",
) -> Path:
"""返回运行时 ROOT(marker 文件所在的工作区)。

判定顺序:
1. ``env_var`` 显式指定 → 直接用(最高优先级,供部署/调试覆盖)。
2. ``naive_root`` 下 marker 存在 → 未 symlink / 源仓内直跑,用 naive_root。
3. 从 ``naive_root`` 路径里解析 ``crews/<crew>`` → ``~/.openclaw/workspace-<crew>``,
该目录下 marker 存在则用之(symlink 部署的常规情况)。
4. 都不命中 → 原样返回 naive_root(让调用方以清晰的 missing-DB 报错)。
"""
env = os.environ.get(env_var)
if env:
return Path(env).expanduser()

if (naive_root / marker).exists():
return naive_root

parts = naive_root.parts
for i, part in enumerate(parts):
if part == "crews" and i + 1 < len(parts):
crew = parts[i + 1]
cand = Path.home() / ".openclaw" / f"workspace-{crew}"
if (cand / marker).exists():
return cand
break

return naive_root
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "_shared"))
from runtime_root import resolve_runtime_root # noqa: E402

# ── 路径 ─────────────────────────────────────────────────────────────────────

ROOT = Path(
os.environ.get(
"PUBLISHED_TRACK_ROOT",
Path(__file__).resolve().parent.parent.parent.parent, # scripts/ → skill/ → skills/ → crew root
)
).expanduser()
ROOT = resolve_runtime_root(Path(__file__).resolve().parent.parent.parent.parent)
DB = ROOT / "db" / "published_track.db"

# ── 常量 ─────────────────────────────────────────────────────────────────────
Expand Down
10 changes: 4 additions & 6 deletions crews/main/skills/content-calibrator/scripts/validate-rubric.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "_shared"))
from runtime_root import resolve_runtime_root # noqa: E402

# ── 路径 ─────────────────────────────────────────────────────────────────────

ROOT = Path(
os.environ.get(
"PUBLISHED_TRACK_ROOT",
Path(__file__).resolve().parent.parent.parent.parent,
)
).expanduser()
ROOT = resolve_runtime_root(Path(__file__).resolve().parent.parent.parent.parent)
DB = ROOT / "db" / "published_track.db"

# ── 常量(与 detect-bump-signals.py 一致)─────────────────────────────────────
Expand Down
13 changes: 7 additions & 6 deletions crews/main/skills/published-track/scripts/query-retro-pending.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
from datetime import datetime, timedelta
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "_shared"))
from runtime_root import resolve_runtime_root # noqa: E402

# ── 路径 ─────────────────────────────────────────────────────────────────────

ROOT = Path(
os.environ.get(
"PUBLISHED_TRACK_ROOT",
Path(__file__).resolve().parent.parent.parent.parent, # scripts/ → skill/ → skills/ → crew root
)
).expanduser()
# skills/ 目录是 symlink(workspace-main/skills/<name> -> wiseflow/crews/main/skills/<name>),
# Path(__file__).resolve() 会跳进源仓,导致 ROOT 指向源仓而非运行时工作区。
# resolve_runtime_root 按 DB 是否存在判定,映射回 ~/.openclaw/workspace-<crew>。
ROOT = resolve_runtime_root(Path(__file__).resolve().parent.parent.parent.parent)
DB = ROOT / "db" / "published_track.db"

# ── 常量 ─────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion crews/main/skills/twitter-post/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ snapshot eval: document.querySelector('[data-testid="icon-verified"]') !== null
| Character limit exceeded (Premium 25K) | Trim or use thread |
| Media upload fails | Retry once; check file format and size |
| Upload strict mode violation (2 elements) | **用 `[data-testid=fileInput] >> nth=0` 消歧**(见 Workflow: Post with Image) |
| "Something went wrong, but don't fret" after Post | X 服务端瞬时错误。**自动重试**:reload compose 页 → retype → reupload → re-click Post,最多 3 次。3 次仍失败才报告用户。无需等待,立即重试。 |
| "Something went wrong, but don't fret" after Post | X 服务端瞬时错误。**优先精简正文**——这种情况大概率是字数超限(X 的字符计数规则与前端显示不完全一致,尤其 URL/emoji 计数偏差时实际超限但按钮未变灰)。先缩短正文再重试,而不是原样重发。重试流程:精简正文 → reload compose 页 → retype → reupload → re-click Post,最多 3 次。3 次仍失败才报告用户。 |
| Rate limit error | **Wait 30 min minimum** (not 15) + check frequency tracker |
| Post button greyed out | Content is empty or over limit — check before clicking |
| Frequency tracker warns high-risk | Ask user: continue or defer to tomorrow? |
Expand Down
5 changes: 5 additions & 0 deletions crews/main/skills/wx-channel-engagement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ wx-channel-engagement login # camoufox 无头截 QR PNG 落 /tmp/qr-wx
wx-channel-engagement login-confirm # 验登录就位 + close session(不导出 cookie/UA/token)
```

`login` 返回 `already_logged_in: true` 时无需扫码,直接执行后续命令。扫码失败 / 用错账户后,**不要直接重跑 `login`**——旧 cookie 污染 profile,重跑拿到的 QR 扫了也不生效。先带 `--reset` 清 profile 再重登:
```bash
wx-channel-engagement login --reset # 删 profile 目录 + 重新 open,从干净状态拿 QR
```

退出码:
- `0` 成功
- `1` 通用错误(参数错 / row 找不到 / 标题未匹配)
Expand Down
76 changes: 63 additions & 13 deletions crews/main/skills/wx-channel-engagement/scripts/fetch_engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import re
import sqlite3
import subprocess
import shutil
import sys
import time
from datetime import datetime, timedelta, timezone
Expand Down Expand Up @@ -66,6 +67,14 @@
FETCH_TIMEOUT_S = 30
SESSION_CLEANUP_ON_EXIT = True # 仅 close camoufox session,不动中央存储

# 视频号助手后台真路径(登录成功后 redirect 落点);裸 /platform/ 是入口 URL,不算就位
BACKEND_PATHS = (
"/platform/home", "/platform/post", "/platform/data",
"/platform/interaction", "/platform/live",
)
# 持久化 profile 目录(登录态落盘位置)
PROFILE_DIR = Path.home() / ".camoufox-cli" / "profiles" / SESSION_NAME

# 登录流程常量(本技能自管 wechat-channel session 的扫码登录)
QR_FILE = "/tmp/qr-wx-channel.png"
LOGIN_CONFIRM_POLL_MAX_S = 150
Expand Down Expand Up @@ -219,18 +228,47 @@ def cmd_login(args) -> None:

agent 拿 QR_FILE 发用户扫码,用户回复「已扫码」后调 login-confirm。

关键:camoufox-cli ``open`` 是「打开 + 立刻返回」,微信登录页的二维码是 JS
动态注入的 ``<img>``(src 是 base64 或 JS 拼的),页面 onload 后才填。
若 open 后立刻 screenshot,QR 还没注入完,截图空白。故 open 后先 wait
等二维码 ``<img>`` 出现(或兜底等固定时间),再 screenshot。
--reset:先 close session + 删 profile 目录再 open,用于上一次扫码污染了
cookie(如用错账户扫码)后强制从干净状态重来。不加 --reset 时不碰已有
登录态,避免把还有效的 session 清掉。

open 后先等 redirect 落点:落到后台路径 = 已登录,直接报 already_logged_in
不出 QR;落到登录页 = 正常出 QR。
"""
# --reset:清掉污染的 profile(close daemon + 删磁盘 profile 目录)
if getattr(args, "reset", False):
camoufox_close(SESSION_NAME)
if PROFILE_DIR.exists():
shutil.rmtree(PROFILE_DIR, ignore_errors=True)

# camoufox-cli open 视频号助手后台首页
camoufox_open(SESSION_NAME, CREATOR_CENTER_URL)

# 等二维码 <img> 出现:微信登录页 QR 是 JS 动态注入的 <img>,
# camoufox-cli open 是「打开 + 立刻返回」,QR 还没注入完就截图会空白。
# 不用 selector 锁 QR 元素(微信登录页结构不固定),直接轮询 eval
# 验证 <img> 元素已出现 + 有 src,再截图。最多等 10s。
# 等 redirect 落点(最多 8s),区分「已登录」vs「需扫码」
deadline = time.time() + 8
current_url = ""
while time.time() < deadline:
current_url = camoufox_get_url(SESSION_NAME)
if current_url and (
any(p in current_url for p in BACKEND_PATHS)
or "login" in current_url
or "scanloginqrcode" in current_url
):
break
time.sleep(0.5)

# 已落到后台路径 = 登录态有效,不出 QR
if current_url and any(p in current_url for p in BACKEND_PATHS):
camoufox_close(SESSION_NAME)
sys.stdout.write(json.dumps({
"ok": True,
"already_logged_in": True,
"message": "登录态有效,无需扫码,可直接执行后续命令",
}, ensure_ascii=False, indent=2))
sys.stdout.write("\n")
return

# 落到登录页 → 等二维码 <img> 出现再截图
deadline = time.time() + 10
qr_ready = False
while time.time() < deadline:
Expand Down Expand Up @@ -352,9 +390,16 @@ def _ensure_login() -> None:
# 显式跳登录页 → 真失效,不等
if "login" in current_url or "scanloginqrcode" in current_url:
break
# 跳到后台真路径(排除 login.html 后的 /platform/ 段)= 登录就位
if current_url and "/platform/" in current_url and "login" not in current_url:
# 跳到后台具体页面(/platform/home 等)= 登录就位
# 不再用 "/platform/" in url 判断,因为初始 URL 本身就含 /platform/
if any(p in current_url for p in BACKEND_PATHS):
return
# 登录成功后页面可能停在根路径 /platform/(wujie 已加载但 URL 未跳转)。
# 只要 wujie-app 已渲染且 URL 不含 login,即视为登录就位。
if "login" not in current_url:
probe = camoufox_eval(session, '(() => { try { return !!document.querySelector("wujie-app"); } catch(e) { return false; } })()')
if probe in (True, "true", "True"):
return
time.sleep(0.5)

sys.stderr.write(
Expand Down Expand Up @@ -492,11 +537,14 @@ def _cleanup_session(session: str) -> None:

def fetch_post_list(session: str) -> list[dict]:
"""打开作品管理页,eval JS 解析作品列表"""
# 1. 打开作品管理页
# 1. 先访问后台首页预热 wujie 微前端框架(直接跳作品管理页会导致 wujie-app 未初始化)
camoufox_open(session, CREATOR_CENTER_URL)
time.sleep(3)
# 2. 打开作品管理页
camoufox_open(session, POST_LIST_URL)
# 等页面加载(wujie 初始化 + shadow DOM 渲染)
time.sleep(5)
# 2. eval JS 解析 innerText
# 3. eval JS 解析 innerText
raw = camoufox_eval(session, _LIST_PARSE_JS)
if not raw:
return []
Expand Down Expand Up @@ -794,7 +842,9 @@ def build_parser() -> argparse.ArgumentParser:
sub.add_parser("probe", help="打开视频号助手后台 dump DOM/截图/innerText").set_defaults(func=cmd_probe)
sub.add_parser("list", help="列出后台所有视频 + 行内 metrics").set_defaults(func=cmd_list)

sub.add_parser("login", help="camoufox 无头截 QR PNG").set_defaults(func=cmd_login)
p_login = sub.add_parser("login", help="camoufox 无头截 QR PNG")
p_login.add_argument("--reset", action="store_true", help="先删 profile 目录再 open(上一次扫码污染 cookie 后强制重来)")
p_login.set_defaults(func=cmd_login)
sub.add_parser("login-confirm", help="确认登录 + close session").set_defaults(func=cmd_login_confirm)

p_fetch = sub.add_parser("fetch", help="抓单篇 engagement(按 title 在作品管理页匹配)")
Expand Down
4 changes: 2 additions & 2 deletions crews/main/skills/wx-mp-publisher/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

## 写入 accounts.json

Agent 收到 AppID + AppSecret 后,写入 `crews/main/skills/wx-mp-publisher/accounts.json`:
Agent 收到 AppID + AppSecret 后,写入 `~/.openclaw/wx-mp-publisher/accounts.json`(源仓之外的实例态目录,密钥不落源仓)

```json
{
Expand All @@ -39,5 +39,5 @@ Agent 收到 AppID + AppSecret 后,写入 `crews/main/skills/wx-mp-publisher/a

## 安全

- `accounts.json` 已在 `.gitignore` 中,不会进 git
- `accounts.json` 在源仓之外(`~/.openclaw/wx-mp-publisher/`),不会进 git / tarball / 备份
- AppSecret 等同于密码,不要贴到聊天群 / issue / 日志里
11 changes: 7 additions & 4 deletions crews/main/skills/wx-mp-publisher/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ metadata:

## 凭据与存储位置

- **公众号凭据**存放在本 skill 目录下的 `accounts.json`(已 gitignore,不进仓):
- **公众号凭据**存放在源仓之外的实例态目录(不跟源仓绑,软链 / tarball / 备份都不带密钥):
```
crews/main/skills/wx-mp-publisher/accounts.json
~/.openclaw/wx-mp-publisher/accounts.json
```
结构见 `accounts.example.json`。支持多账号,每条含 `alias` / `appId` / `appSecret`;多账号时 `default` 指向默认 alias。
结构见本 skill 同目录 `accounts.example.json`。支持多账号,每条含 `alias` / `appId` / `appSecret`;多账号时 `default` 指向默认 alias。
- **relay 身份** `OFB_KEY` + `RELAY_BASE_URL` 来自 `daemon.env`(由 entrypoint 注入环境变量)。

### 凭据缺失时 Agent 行为
Expand Down Expand Up @@ -55,6 +55,7 @@ wx-mp-publisher <markdown_file> [theme] [--account ALIAS]
脚本自动:
- 从 `accounts.json` 取目标账号凭据
- 从 Markdown 中提取本地图片路径,作为 `images` 字段一并上传(http/https 图片由 relay 自行抓取,不在此列)
- 发布前校验:图片引用必须纯文件名(不得带目录前缀 / 绝对路径)、frontmatter `author` ≤ 24 字节,违规直接报错退出
- POST multipart 到 `${RELAY_BASE_URL}/api/v1/wx-mp/publish`,带 `X-OFB-Key`
- 校验响应包络 `{ success, data, error }`

Expand Down Expand Up @@ -98,7 +99,7 @@ wx-mp-publisher <markdown_file> [theme] [--account ALIAS]
---
title: 文章标题
cover: ./cover.jpg # 可选,缺省自动取正文第一张图
author: 作者名称 # 可选
author: 作者名称 # 可选,≤ 8 个汉字 / 24 字节(超长报 45110)
source_url: https://... # 可选,原文链接
need_open_comment: true # 可选,是否开启评论(默认 false)
only_fans_can_comment: false # 可选,是否仅粉丝可评论(默认 false)
Expand Down Expand Up @@ -139,6 +140,8 @@ image_list:
| `OFB_KEY 未配置` | 让 IT engineer 在 `daemon.env` 配置 `OFB_KEY` 后重启实例 |
| `MISSING_APP_ID` / `MISSING_APP_SECRET`(relay 400) | accounts.json 中该账号凭据为空,补全 |
| `MISSING_MARKDOWN`(relay 400) | 检查 markdown 文件内容非空 |
| `45110: author size out of limit` | frontmatter `author` 超 8 汉字 / 24 字节,缩短后再发 |
| 图片 `ENOENT`(relay) | markdown 图片引用带目录前缀 / 绝对路径,改为纯文件名(basename) |
| relay 502 | relay 调微信失败,检查 AppSecret / IP 白名单(见 `REFERENCE.md`) |

---
Expand Down
Loading