自动抓取网页文章和 Twitter Article,完整归档到飞书文档。
1. 必须读取配置文件获取归档位置:
source ~/.openclaw/workspace/skills/article-archiver/config/feishu-locations.sh
# 使用 $DEFAULT_SPACE_ID 和 $DEFAULT_PARENT_NODE
2. 图片处理流程:
agent-browser 打开页面img.preview 或类似选择器过滤内容图片(排除头像、图标)/tmp/feishu_doc upload_image 并指定 after_block_id 插入到正确位置upload_image 支持 after_block_id 参数,可以精确控制图片位置3. 样式保留:
> 开头text- 或 1. ` 4. 内容传递方式:
read 工具读取文件$(cat file)5. 去重检查:
6. 防止乱码和格式问题:
html-to-markdown-fixed.js(而不是 html-to-markdown-final.js)\ufffd 和 ���# \n\n内容 格式为 # 内容\ufffd 或 ���7. 避免重复标题:
8. 元数据格式(重要):
```markdown
> 原始链接: https://example.com/article
> 归档时间: 2026-03-12 22:16:00
> 来源: example.com
> 作者: 张三 (@username)
---
```
> 开头(引用块)粗体 + 冒号--- 分开9. 图片位置精确控制:
feishu_doc upload_image 时,必须指定 after_block_idfeishu_doc list_blocks 获取所有 block_idafter_block_id 参数将图片插入到正确位置自动触发(无需用户明确说"归档"):
https://x.com/... 或 https://twitter.com/...https://mp.weixin.qq.com/...手动触发:
触发后的行为:
在开始归档前,必须先检查文档是否已存在:
# 1. 提取文章标题(使用修复版脚本)
TITLE=$(node scripts/html-to-markdown-fixed.js "$URL" "$(cat config/twitter-cookies.txt)" | jq -r '.title')
# 2. 在知识库中搜索同名文档
feishu_wiki nodes --space-id 7527734827164909572 --parent-node NqZvwBqMTiTEtkkMsRoc76rznce | \
jq -r '.nodes[] | select(.title == "'"$TITLE"'") | .node_token'
# 3. 如果找到同名文档,跳过归档
if [ -n "$EXISTING_NODE" ]; then
echo "⚠️ 文档已存在,跳过归档"
echo "已存在的文档:https://qingzhao.feishu.cn/wiki/$EXISTING_NODE"
exit 0
fi
去重规则:
实现方式:
if (url.includes('x.com') && url.includes('/status/')) {
// Twitter Article - 需要特殊处理
return 'twitter-article';
} else {
// 普通网页
return 'web-page';
}
Twitter Article(长文章 + 多图片):
# 使用修复版脚本(解决乱码和格式问题)
cd ~/.openclaw/workspace/skills/article-archiver
node scripts/html-to-markdown-fixed.js "$URL" "$(cat config/twitter-cookies.txt)" > /tmp/article.json
普通网页:
# 使用 web_fetch
web_fetch --url <url> --extract-mode markdown
⚠️ 重要:正确的文件内容传递方式
在 OpenClaw 会话中,不能使用 bash 命令替换 $(cat file) 直接传给 feishu_doc,因为会被当成字面字符串。
正确方法:
方法 1:使用 OpenClaw 的 read 工具(推荐)
// 1. 先用 read 工具读取文件
const content = await read({ path: '/tmp/content.txt' });
// 2. 然后传给 feishu_doc
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: content
});
方法 2:分批读取和上传(适合大文件)
// 读取文件总行数
const totalLines = parseInt(await exec({
command: 'wc -l < /tmp/article-body.txt'
}));
const BATCH_SIZE = 100;
let currentLine = 1;
while (currentLine <= totalLines) {
const endLine = Math.min(currentLine + BATCH_SIZE - 1, totalLines);
// 读取这批内容
const content = await read({
path: '/tmp/article-body.txt',
offset: currentLine,
limit: BATCH_SIZE
});
// 上传到飞书
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: content
});
currentLine = endLine + 1;
// 延迟,避免 API 限流
await new Promise(resolve => setTimeout(resolve, 300));
}
图片处理(关键步骤):
# 1. 使用 agent-browser 打开页面并提取图片
agent-browser open "https://example.com/article"
sleep 3
# 2. 滚动页面加载所有图片(懒加载)
agent-browser scroll down 5000
sleep 2
agent-browser scroll down 5000
sleep 2
# 3. 提取图片 URL(过滤头像和 SVG)
agent-browser eval 'JSON.stringify(
Array.from(document.querySelectorAll("img"))
.filter(img => !img.src.startsWith("data:") &&
!img.src.includes("avatar") &&
img.naturalWidth > 300)
.map(img => img.src)
)' > /tmp/image-urls.json
# 4. 下载并上传每张图片
cat /tmp/image-urls.json | jq -r '.[]' | while read url; do
curl -s -o /tmp/img.jpg "$url"
# 使用 feishu_doc upload_image 上传
# 注意:必须用 file_path 参数,不是 path
done
# 5. 关闭浏览器
agent-browser close
图片上传注意事项:
feishu_doc 的 upload_image actionfile_path,不是 path样式保留:
> 开头 ` 包裹- 或 1. 开头text关键原则:
标准格式(引用块 + 粗体字段名,参考 https://qingzhao.feishu.cn/wiki/ZVCFwN7bci1uyhknLpucA18FnSe):
// 提取元数据
const metadata = JSON.parse(await exec({
command: 'cat /tmp/article.json'
}));
const title = metadata.title;
const author = metadata.author;
const username = metadata.username;
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
// 判断来源
let source, authorText;
if (url.includes('x.com') || url.includes('twitter.com')) {
source = 'x.com (Twitter Article)';
authorText = `${author} (@${username})`;
} else if (url.includes('mp.weixin.qq.com')) {
source = '微信公众号';
authorText = author;
} else {
source = url.match(/https?:\/\/([^/]+)/)[1];
authorText = author;
}
// 写入元数据头部(引用块格式)
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: `> **原始链接:** ${url}
> **归档时间:** ${timestamp}
> **来源:** ${source}
> **作者:** ${authorText}
---`
});
关键点:
> 开头(引用块)粗体 + 冒号--- 分开使用 OpenClaw read 工具读取并写入:
// 读取正文内容
const content = await read({ path: '/tmp/article.json' });
const articleContent = JSON.parse(content).content;
// 写入正文
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: articleContent
});
飞书会自动处理:
!alt → 自动下载并转换为内部图片 `language ... ` → 渲染为代码块# 读取文档,检查块数
feishu_doc read --doc_token $DOC_TOKEN
# 验证:
# - 文字段落数 = 预期段落数
# - 图片数 = 预期图片数
# - 总块数 = 文字段落数 + 图片数
成功消息格式:
✅ 文章已归档到飞书
📄 标题:[文章标题]
🔗 原文:[原始链接]
📁 位置:学习资料抓取 → 待阅读
🔗 飞书:[飞书文档链接]
📊 统计:
- 内容:[行数] 行
- 图片:[数量] 张
- 格式:完整保留
失败消息格式:
❌ 归档失败
原因:[失败原因]
建议:[解决建议]
原文链接:[原始链接]
使用 message 工具发送:
// 成功通知
message({
action: "send",
target: "ou_a39e6975059f975a7ffd25892243b64a", // 威哥的 user_id
message: "✅ 文章已归档到飞书\n\n📄 标题:...\n🔗 飞书:..."
});
// 失败通知
message({
action: "send",
target: "ou_a39e6975059f975a7ffd25892243b64a",
message: "❌ 归档失败\n\n原因:...\n建议:..."
});
配置文件:config/feishu-locations.sh
# 默认归档位置
DEFAULT_SPACE_ID="7527734827164909572"
DEFAULT_PARENT_NODE="YziUwLVlBi9BX7kVtkJcf7nQns2" # 学习资料抓取 → 待阅读
# 可选的归档位置
LOCATION_TO_READ="7527734827164909572:YziUwLVlBi9BX7kVtkJcf7nQns2" # 待阅读
LOCATION_ROOT="7527734827164909572:Lfj1d3Pcmo0o2IxrHrOcMsffnZb" # 根目录
使用方法:
```bash
# 编辑配置文件
vim ~/.openclaw/workspace/skills/article-archiver/config/feishu-locations.sh
# 修改 DEFAULT_PARENT_NODE 为目标 node_token
DEFAULT_PARENT_NODE="YziUwLVlBi9BX7kVtkJcf7nQns2"
```
```bash
# 归档到指定节点
feishu_wiki create --parent-node
```
```bash
# 在配置文件中添加
LOCATION_ARCHIVE="7527734827164909572:XXX" # 归档目录
LOCATION_DRAFT="7527734827164909572:YYY" # 草稿目录
```
获取 node_token:
# 方法 1:从飞书 URL 中提取
# https://qingzhao.feishu.cn/wiki/YziUwLVlBi9BX7kVtkJcf7nQns2
# node_token = YziUwLVlBi9BX7kVtkJcf7nQns2
# 方法 2:使用 feishu_wiki 工具查询
feishu_wiki nodes --space-id 7527734827164909572
当前配置的位置:
YziUwLVlBi9BX7kVtkJcf7nQns2Lfj1d3Pcmo0o2IxrHrOcMsffnZbTwitter Article 需要登录 Cookie:
# Cookie 文件位置
~/.openclaw/workspace/skills/article-archiver/config/twitter-cookies.txt
# 格式
auth_token=xxx; ct0=yyy; twid=zzz
# 更新 Cookie
echo "auth_token=xxx; ct0=yyy; twid=zzz" > config/twitter-cookies.txt
# 学习资料抓取
# 2026-03
## 文章标题
> **原始链接**:https://example.com/article
>
> **归档时间**:2026-03-11 20:00:00
>
> **来源**:example.com
>
> **作者**:作者名 (@username)
文章内容...

更多内容...

---
URL 格式:
https://x.com/username/status/123456789https://twitter.com/username/status/123456789特点:
URL 格式:
https://mp.weixin.qq.com/s/...特点:
支持的网站:
特点:
web_fetch 抓取用户:https://x.com/mkdir700/status/2020652753190887566
系统行为:
```
✅ 文章已归档到飞书
📄 标题:月成本不到 100 元,如何实现 Token 自由
🔗 原文:https://x.com/mkdir700/status/2020652753190887566
📁 位置:学习资料抓取 → 待阅读
🔗 飞书:https://qingzhao.feishu.cn/wiki/XXX
📊 统计:
```
用户:https://x.com/mkdir700/status/2020652753190887566
系统行为:
```
⚠️ 文档已存在,跳过归档
📄 标题:月成本不到 100 元,如何实现 Token 自由
🔗 已存在的文档:https://qingzhao.feishu.cn/wiki/XXX
```
用户:https://mp.weixin.qq.com/s/abc123
系统行为:
用户:保存 https://example.com/article
系统行为:
用户:https://x.com/someuser/status/123(Cookie 过期)
系统行为:
```
❌ 归档失败
原因:Twitter Cookie 已过期
建议:
原文链接:https://x.com/someuser/status/123
```
从 Twitter Article HTML 转换为格式化 Markdown,修复了 UTF-8 乱码和标题格式问题:
node scripts/html-to-markdown-fixed.js <article_url> <cookie_string>
功能:
# \n\n内容 → # 内容)\ufffd 和 ���text) `shell ... ` )- item)与旧版本的区别:
html-to-markdown-final.js:旧版本,存在乱码和格式问题html-to-markdown-fixed.js:新版本,已修复所有已知问题处理长文章(600+ 行)+ 多图片(20+ 张):
cd ~/.openclaw/workspace/skills/article-archiver/scripts
./archive-long-article.sh <article_url> <doc_token>
功能:
原因:UTF-8 多字节字符在传输或序列化时被截断。
解决:
html-to-markdown-fixed.js 而不是 html-to-markdown-final.js原因:Markdown 格式 # \n\n内容 导致飞书渲染时标题为空。
解决:
html-to-markdown-fixed.js# 内容原因:先写完所有文字,再统一上传图片。
解决:按图片位置分段,交替写入文本和图片。
原因:长文章一次性写入失败。
解决:使用 archive-long-article.sh 脚本,分段处理。
原因:使用了简单的文本提取,没有保留 HTML 格式。
解决:使用 html-to-markdown.js 脚本,正确转换格式。
原因:手动逐个调用 feishu_doc 工具。
解决:用 bash 脚本批量处理,脚本执行不消耗 token。
症状:抓取 Twitter Article 失败,返回登录页面。
解决:
auth_token, ct0, twidconfig/twitter-cookies.txtarchive-long-article.sh 脚本最后更新:2026-03-11 20:55
维护者:影
article-archiver 现在使用业界标准的 turndown 库进行 HTML 到 Markdown 的转换,确保高质量的格式保留。
核心脚本:scripts/html-to-markdown-final.js
关键特性:
/ 标签,保留语言标注(bash、json、yaml、text 等)
!alt,飞书会自动下载并转换技术栈:
代码块处理:
// 自定义代码块规则:保留语言标注
turndownService.addRule('codeBlock', {
filter: function (node) {
return node.nodeName === 'PRE' && node.querySelector('code');
},
replacement: function (content, node) {
const code = node.querySelector('code');
const language = code.className.replace('language-', '') || '';
const text = code.textContent;
return '\n\n```' + language + '\n' + text + '\n```\n\n';
}
});
图片处理:
![]()
转换为 !alt 格式元数据头部:
> **原始链接:** https://x.com/username/status/123456789
> **归档时间:** 2026-03-11 22:50:00
> **来源:** x.com (Twitter Article)
> **作者:** 作者名 (@username)
---
测试案例:mkdir700 的 sub2api 教程
性能:
问题 1:代码块丢失
codeBlock 规则,显式处理 / 标签
问题 2:图片丢失
removeImages 规则,让 turndown 保留图片的 Markdown 语法问题 3:图片位置不对
问题 4:Cookie 过期
config/twitter-cookies.txt 文件cd ~/.openclaw/workspace/skills/article-archiver
npm install turndown turndown-plugin-gfm playwright
# 提取文章(包含代码块和图片)
node scripts/html-to-markdown-final.js \
"https://x.com/username/status/123456789" \
"$(cat config/twitter-cookies.txt)" \
> /tmp/article.json
# 检查提取结果
jq '.content' /tmp/article.json | head -50
# 统计代码块和图片
echo "代码块: $(jq -r '.content' /tmp/article.json | grep -c '```')"
echo "图片: $(jq -r '.content' /tmp/article.json | grep -c '!\[')"
问题:
识别方法:
// 检查是否为飞书嵌入文档
const firstParagraph = document.querySelector('article p')?.textContent;
if (firstParagraph?.includes('暂时无法在飞书文档外展示此内容')) {
// 这是飞书嵌入文档,无法抓取
}
解决方案:
问题:
表现:
解决方案:
问题:
https://pbs.twimg.com/media/...?format=jpg&name=small解决方案:
feishu_doc upload_image 逐个上传推荐流程:
重要规则:
feishu_wiki create)NqZvwBqMTiTEtkkMsRoc76rznce(学习资料抓取 → 待阅读)正确的创建方式:
feishu_wiki create \
--space-id 7527734827164909572 \
--parent-node NqZvwBqMTiTEtkkMsRoc76rznce \
--title "$TITLE" \
--obj-type docx
功能:
使用方法:
# 检查去重
RESULT=$(bash scripts/check-duplicate.sh "$URL")
# 解析结果
if [[ $RESULT == EXISTS:* ]]; then
NODE_TOKEN=$(echo "$RESULT" | cut -d: -f2)
TITLE=$(echo "$RESULT" | cut -d: -f3)
echo "⚠️ 文档已存在:https://qingzhao.feishu.cn/wiki/$NODE_TOKEN"
exit 0
fi
脚本位置:scripts/check-duplicate.sh
bash scripts/check-duplicate.sh "$URL"
node scripts/html-to-markdown-final.js "$URL" "$(cat config/twitter-cookies.txt)" > /tmp/article.json
TITLE=$(jq -r '.title' /tmp/article.json)
feishu_wiki create --space-id 7527734827164909572 --parent-node NqZvwBqMTiTEtkkMsRoc76rznce --title "$TITLE" --obj-type docx
// 提取元数据
const metadata = JSON.parse(await exec({ command: 'cat /tmp/article.json' }));
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
// 判断来源
let source, authorText;
if (url.includes('x.com') || url.includes('twitter.com')) {
source = 'x.com (Twitter Article)';
authorText = `${metadata.author} (@${metadata.username})`;
} else if (url.includes('mp.weixin.qq.com')) {
source = '微信公众号';
authorText = metadata.author;
} else {
source = url.match(/https?:\/\/([^/]+)/)[1];
authorText = metadata.author;
}
// 写入元数据头部
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: `> **原始链接:** ${url}
> **归档时间:** ${timestamp}
> **来源:** ${source}
> **作者:** ${authorText}
---`
});
// 读取正文内容
const content = await read({ path: '/tmp/article.json' });
const articleContent = JSON.parse(content).content;
// 写入正文
await feishu_doc({
action: 'append',
doc_token: DOC_TOKEN,
content: articleContent
});
feishu_doc list_blocks --doc_token $DOC_TOKEN | jq '.blocks | length'
问题 1:图片无法显示
问题 2:代码块格式错误
问题 3:文档创建在错误位置
feishu_wiki createparent_node 参数问题 4:重复归档
问题 5:元数据头部没有样式
> 开头(引用块)粗体 + 冒号问题 6:内容写入错误(显示命令而非内容)
$(cat /tmp/file.md) 而不是实际内容feishu_doc append --content "$(cat file.md)"read 工具读取文件,再传递内容```javascript
const content = await read({ path: '/tmp/file.md' });
await feishu_doc({ action: 'append', content: content });
```
问题 7:UTF-8 乱码(字符被截断)
那么��读和自媒体("阅"字显示为 ��)read 工具读取文件(自动处理编码)head -c)问题 8:归档位置错误
parent_node_tokenconfig/feishu-locations.shDEFAULT_PARENT_NODE 的值NqZvwBqMTiTEtkkMsRoc76rznce(学习资料抓取 → 待阅读)YziUwLVlBi9BX7kVtkJcf7nQns2(旧位置)共 1 个版本