他山之石

SKILL效率
效率 未标注权限 许可证不明确
安装这个技能

懒人技能。支持网页、微信文章直接以pdf格式存入ima知识库,可批量操作。提示词:文章链接+知识库名称。

一句话选型结论

按资料字段整理

他山之石是效率类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类效率 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_752cff44/onlinearticle2knowledge
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

效率
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_752cff44仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791677069
正文体量36.4 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
整理待办与日程

把零散任务收进清单,排出优先级。

批量处理重复任务

把固定动作做成模板,重复执行。

把流程写成清单

把口头流程整理成可复用的步骤。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在效率场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
他山之石当前 未标注权限 未标注 未标注 未标注 有
Nano PDF 文档 联网访问 ClawHub 市场 424.9 万 OpenClaw 有
自动更新程序 浏览器控制 ClawHub 市场 296.4 万 OpenClaw 有
Gemini 助手 联网访问 ClawHub 市场 254.7 万 OpenClaw 有
word DOCX 文档 联网访问 ClawHub 市场 239.6 万 OpenClaw 有
Excel XLSX 前端 联网访问 ClawHub 市场 227.0 万 OpenClaw 有

SKILL.md 全文

36.4 KB
模型实际读到的说明书原文,来自技能仓库

他山之石 v7.5

> 技能名称:他山之石
> 英文名称:onlinearticle2knowledge
> 功能定位:在线文章下载与知识库入库工具

核心能力

将在线文章(如微信公众号)下载为PDF格式并上传到知识库:
- 文章标题、来源、时间、正文内容
- 所有图片(含懒加载、防盗链)
- 知识库衔接:自动询问用户是否上传知识库
- 智能命名:{发布年月日}-{发布主体}-{发布地点}-{文章标题}.pdf

⚠️ 重要说明

关键技术点:
1. Playwright自动化流程:自动滚动触发懒加载图片,确保所有图片加载完成
2. 验证机制:自动对比原文图片数量 vs PDF文件大小
3. 图片懒加载处理:微信图片必须滚动到视图才能触发加载
4. 文件大小验证:有9张图片的PDF应该>3000KB
5. ima知识库衔接:自动调用ima-skills上传PDF到知识库

使用流程

用户交互流程

根据用户输入决定执行路径:

场景1:用户只提供文章链接

⚠️ 关键:必须在下载前询问用户!

执行步骤:
1. 先询问用户:"是否上传到ima知识库?"
2. 用户选择"否":
- 下载文章并生成PDF
- 调用 present_files 呈现PDF
3. 用户选择"是":
- 调用 node ima_api.cjs "openapi/wiki/v1/get_addable_knowledge_base_list" 获取知识库列表
- 展示知识库列表(格式:📚 可用知识库(共X个):| 编号 | 知识库名称 |)
- 让用户选择目标知识库(输入编号或名称)
- 下载文章并生成PDF
- 上传PDF到选择的知识库

场景2:用户提供文章链接 + 知识库名称

执行步骤:
1. 下载文章并生成PDF
2. 调用 node ima_api.cjs "openapi/wiki/v1/search_knowledge_base" 搜索知识库
3. 找到匹配的知识库ID
4. 直接上传PDF到知识库(无需询问)

场景3:批量处理多篇文章

输入方式:

方式1: 直接提供多个URL(每行一个)
@skill:他山之石 批量处理:
https://mp.weixin.qq.com/s/xxxxx1
https://mp.weixin.qq.com/s/xxxxx2
https://mp.weixin.qq.com/s/xxxxx3

方式2: 提供URL文本文件 + 知识库名称
@skill:他山之石 批量处理:urls.txt DietRevo

执行步骤:
1. 解析输入:提取所有URL
2. 询问用户:是否上传到ima知识库?
3. 用户选择知识库
4. 循环处理每篇文章:
- 提取文章信息
- 下载并生成PDF
- 上传到知识库
- 记录处理结果
5. 输出汇总结果:

批量处理结果:
| 序号 | 文章标题 | 状态 |
| 1 | 每天运动半小时... | ✅ 成功 |
| 2 | 脂肪肝逆转方法... | ✅ 成功 |
| 3 | 降压食物研究... | ❌ 失败 |

成功: 2篇,失败: 1篇
知识库: DietRevo

错误处理:
- 单篇文章失败不影响后续文章处理
- 记录失败原因(如:图片加载超时、网络错误)
- 支持重试失败的文章


Step 1: 安装Playwright(首次使用)

cd ~/.workbuddy/binaries/node/workspace
npm install playwright

Step 2: 创建执行脚本

创建文件 print-wechat-article.js:

const { chromium } = require('playwright');
const path = require('path');

(async () => {
  const url = process.argv[2] || '微信文章URL';
  const outputName = process.argv[3] || '文章标题.pdf';
  const pdfPath = path.resolve(__dirname, outputName);

  // 1. 启动浏览器
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // 2. 打开文章
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
  await page.waitForSelector('#js_content', { timeout: 10000 });

  // 3. 获取所有懒加载图片
  const images = await page.$$eval('#js_content img[data-src]', imgs => imgs.length);
  console.log(`找到 ${images} 张懒加载图片`);

  // 4. 滚动到每张图片位置,触发加载(关键!)
  for (let i = 0; i < images; i++) {
    await page.evaluate((index) => {
      const imgs = document.querySelectorAll('#js_content img[data-src]');
      if (imgs[index]) {
        imgs[index].scrollIntoView({ behavior: 'instant', block: 'center' });
      }
    }, i);
    await page.waitForTimeout(2000); // 等待2秒(确保图片完全加载)
    console.log(`图片 ${i + 1}/${images} 已触发加载`);
  }

  // 5. 滚动回顶部
  await page.evaluate(() => window.scrollTo(0, 0));
  await page.waitForTimeout(8000); // 等待8秒(确保所有图片渲染完成)

  // 6. 验证图片加载状态
  const loadedCount = await page.evaluate(() => {
    const imgs = Array.from(document.querySelectorAll('#js_content img'));
    return imgs.filter(img => img.complete && img.naturalHeight > 0).length;
  });
  console.log(`加载成功: ${loadedCount}/${images} 张图片`);

  // 7. 在页面末尾添加原文链接
  await page.evaluate((articleUrl) => {
    const content = document.querySelector('#js_content');
    if (content) {
      // 创建分隔线
      const divider = document.createElement('div');
      divider.style.cssText = 'margin-top: 40px; padding-top: 20px; border-top: 1px solid #e0e0e0; text-align: center;';

      // 创建链接容器
      const linkContainer = document.createElement('div');
      linkContainer.style.cssText = 'font-size: 12px; color: #888; line-height: 1.8;';

      // 创建链接文本
      linkContainer.innerHTML = `
        <div style="margin-bottom: 8px; font-weight: bold; color: #666;">原文链接</div>
        <div style="word-break: break-all; color: #576b95;">${articleUrl}</div>
      `;

      divider.appendChild(linkContainer);
      content.appendChild(divider);
    }
  }, url);
  console.log(`✓ 原文链接已添加到页面末尾`);

  // 8. 打印PDF
  await page.pdf({
    path: pdfPath,
    format: 'A4',
    margin: { top: '20mm', bottom: '20mm', left: '20mm', right: '20mm' },
    printBackground: true
  });

  // 9. 输出文件信息
  const fs = require('fs');
  const sizeKB = fs.statSync(pdfPath).size / 1024;
  console.log(`✓ PDF已保存: ${pdfPath}`);
  console.log(`  文件大小: ${sizeKB.toFixed(1)} KB`);
  console.log(`  原文链接: ${url}`);

  // 10. 验证文件大小
  if (images > 5 && sizeKB < 2000) {
    console.log(`⚠️ 警告:PDF文件较小(${sizeKB.toFixed(1)} KB),可能部分图片未加载`);
  } else if (images > 0 && sizeKB < 500) {
    console.log(`⚠️ 警告:PDF文件过小(${sizeKB.toFixed(1)} KB),图片可能未加载`);
  }

  await browser.close();
})();
  if (images > 5 && sizeKB < 2000) {
    console.log(`⚠️ 警告:PDF文件较小(${sizeKB.toFixed(1)} KB),可能部分图片未加载`);
  } else if (images > 0 && sizeKB < 500) {
    console.log(`⚠️ 警告:PDF文件过小(${sizeKB.toFixed(1)} KB),图片可能未加载`);
  }

  await browser.close();
})();

Step 3: 执行脚本

# 基础用法
NODE_PATH=~/.workbuddy/binaries/node/workspace/node_modules \
node print-wechat-article.js "微信文章URL" "发布年月日-发布主体-发布地点-文章名称.pdf"

# 实际示例
NODE_PATH=~/.workbuddy/binaries/node/workspace/node_modules \
node print-wechat-article.js \
  "https://mp.weixin.qq.com/s/r6JqevM2kaWpUhJM5sjw-A" \
  "20260510-新华社-北京-脂肪肝逆转方法.pdf"

Step 4: 验证结果

检查PDF文件大小:

ls -lh 文章标题.pdf

验证标准:
- 有9张图片的PDF应该 > 3000KB
- 有5张图片的PDF应该 > 1500KB
- 如果PDF < 1000KB且有多张图片,说明图片未加载

检查图片加载状态:
脚本会输出:

找到 9 张懒加载图片
图片 1/9 已触发加载
...
图片 9/9 已触发加载
加载成功: 9/9 张图片
✓ PDF已保存: 文章标题.pdf
  文件大小: 3207.0 KB

自动化流程(推荐)

当用户调用 @skill:wechat-article-download 时,自动执行以下步骤:

  1. 创建执行脚本:生成 print-wechat-article.js 文件
  2. 执行脚本:运行Playwright自动化流程
  3. 验证结果:检查PDF文件大小和图片加载状态
  4. 呈现文件:调用 present_files 展示生成的PDF

技术要点

1. 图片懒加载处理

问题: 微信图片使用 data-src 属性,不会自动加载,必须滚动到视图才能触发加载。

解决方案:

// 滚动到每张图片位置
imgs[index].scrollIntoView({ behavior: &#x27;instant&#x27;, block: &#x27;center&#x27; });
await page.waitForTimeout(1500); // 等待1.5秒

2. 图片加载验证

验证代码:

const loadedCount = await page.evaluate(() =&gt; {
  const imgs = Array.from(document.querySelectorAll(&#x27;#js_content img&#x27;));
  return imgs.filter(img =&gt; img.complete &amp;&amp; img.naturalHeight &gt; 0).length;
});

判断标准:
- img.complete:图片加载完成
- img.naturalHeight &gt; 0:图片实际渲染成功

3. 文件大小验证

验证逻辑:
- 有9张图片的PDF:> 3000KB ✅
- 有5张图片的PDF:> 1500KB ✅
- PDF < 1000KB且有多张图片:❌ 图片可能未加载

4. 内容完整性验证(新增)

4.1 标题一致性验证

// 提取原文标题
const originalTitle = await page.evaluate(() =&gt; {
  return document.querySelector(&#x27;#activity-name&#x27;)?.textContent?.trim() || &#x27;&#x27;;
});

// 提取PDF标题(通过PDF文件名或PDF内容)
const pdfTitle = pdfFilename.replace(&#x27;.pdf&#x27;, &#x27;&#x27;);

// 对比
if (!pdfTitle.includes(originalTitle.substring(0, 20))) {
  console.log(`⚠️ 警告:标题不一致`);
  console.log(`  原文: ${originalTitle}`);
  console.log(`  PDF: ${pdfTitle}`);
}

4.2 字数一致性验证

// 提取原文字数
const originalWordCount = await page.evaluate(() =&gt; {
  const content = document.querySelector(&#x27;#js_content&#x27;)?.textContent || &#x27;&#x27;;
  return content.replace(/\s+/g, &#x27;&#x27;).length;
});

// 提取PDF字数(需要pdf-parse或pdfjs-dist)
const pdfWordCount = await extractPDFText(pdfPath).then(text =&gt; 
  text.replace(/\s+/g, &#x27;&#x27;).length
);

// 对比(允许10%误差)
const ratio = pdfWordCount / originalWordCount;
if (ratio &lt; 0.9) {
  console.log(`⚠️ 警告:PDF字数少于原文${((1-ratio)*100).toFixed(1)}%`);
  console.log(`  原文: ${originalWordCount} 字`);
  console.log(`  PDF: ${pdfWordCount} 字`);
}

4.3 图片数量一致性验证

// 提取原文图片数量
const originalImageCount = await page.evaluate(() =&gt; {
  return document.querySelectorAll(&#x27;#js_content img[data-src]&#x27;).length;
});

// 提取PDF图片数量(需要pdf-lib或pdfjs-dist)
const pdfImageCount = await extractPDFImages(pdfPath).length;

// 对比
if (pdfImageCount &lt; originalImageCount) {
  console.log(`❌ 验证失败:PDF缺少图片`);
  console.log(`  原文: ${originalImageCount} 张`);
  console.log(`  PDF: ${pdfImageCount} 张`);
  console.log(`  缺少: ${originalImageCount - pdfImageCount} 张`);
}

4.4 图片内容验证

// 检查图片是否为占位图/错误图
const checkImageValidity = async (imagePath) =&gt; {
  const stats = fs.statSync(imagePath);

  // 占位图通常很小(&lt; 5KB)
  if (stats.size &lt; 5 * 1024) {
    return { valid: false, reason: &#x27;图片过小,可能是占位图&#x27; };
  }

  // 检查图片尺寸(使用sharp或jimp)
  const metadata = await sharp(imagePath).metadata();
  if (metadata.width &lt; 100 || metadata.height &lt; 100) {
    return { valid: false, reason: &#x27;图片尺寸异常&#x27; };
  }

  return { valid: true };
};

5. 综合验证流程

自动化验证脚本:

async function validatePDF(originalUrl, pdfPath) {
  const results = {
    titleMatch: false,
    wordCountMatch: false,
    imageCountMatch: false,
    imagesValid: false,
    overall: false
  };

  // 1. 标题验证
  const originalTitle = await extractTitle(originalUrl);
  const pdfTitle = path.basename(pdfPath, &#x27;.pdf&#x27;);
  results.titleMatch = pdfTitle.includes(originalTitle.substring(0, 20));

  // 2. 字数验证
  const originalWords = await extractWordCount(originalUrl);
  const pdfWords = await extractPDFWordCount(pdfPath);
  results.wordCountMatch = (pdfWords / originalWords) &gt;= 0.9;

  // 3. 图片数量验证
  const originalImages = await extractImageCount(originalUrl);
  const pdfImages = await extractPDFImageCount(pdfPath);
  results.imageCountMatch = pdfImages === originalImages;

  // 4. 图片内容验证
  const invalidImages = await checkAllImages(pdfPath);
  results.imagesValid = invalidImages.length === 0;

  // 综合判断
  results.overall = results.titleMatch &amp;&amp; results.wordCountMatch &amp;&amp; 
                    results.imageCountMatch &amp;&amp; results.imagesValid;

  return results;
}

验证结果输出:

=== PDF验证报告 ===
✓ 标题一致性: 通过
✓ 字数一致性: 通过(原文1234字,PDF1201字,97.3%)
✓ 图片数量一致性: 通过(原文9张,PDF9张)
✓ 图片内容验证: 通过
✅ 总体验证: 通过

依赖工具

工具 用途 说明
Playwright 浏览器自动化 自动滚动、触发懒加载、打印PDF
Node.js 运行环境 执行Playwright脚本

常见问题

Q1: 为什么PDF文件大小只有1000KB,图片显示"加载中"?

原因: Edge浏览器打印时没有滚动页面,懒加载图片未触发加载。

解决方案: 必须使用Playwright自动化流程,滚动到每张图片位置触发加载。


Q2: Playwright方案和Edge浏览器方案有什么区别?

方案 文件大小 图片状态 结果
Playwright >3000KB 9张图片完整显示 ✅ 成功
Edge浏览器 <1500KB 图片显示"加载中" ❌ 失败

Q3: 如何验证PDF是否完整?

五层验证机制:

  1. 文件大小验证
    - 有9张图片的PDF应该>3000KB
    - 有5张图片的PDF应该>1500KB

  2. 图片数量验证
    - 脚本输出:加载成功: 9/9 张图片

  3. 视觉检查
    - 打开PDF,查看所有图片是否完整显示(不是"加载中")

  4. 标题一致性验证(新增)
    - 对比原文标题 vs PDF标题
    - 确保文章正确

  5. 字数一致性验证(新增)
    - 对比原文字数 vs PDF字数
    - 允许10%误差,确保内容完整

自动化验证输出示例:

=== PDF验证报告 ===
✓ 标题一致性: 通过
✓ 字数一致性: 通过(原文1234字,PDF1201字,97.3%)
✓ 图片数量一致性: 通过(原文9张,PDF9张)
✓ 图片内容验证: 通过
✅ 总体验证: 通过

Q4: 为什么必须滚动页面?

原因: 微信图片使用懒加载技术,图片不会自动加载,只有滚动到图片位置才会触发加载。

实现:

// 滚动到每张图片位置
imgs[index].scrollIntoView({ behavior: &#x27;instant&#x27;, block: &#x27;center&#x27; });
await page.waitForTimeout(1500); // 等待图片加载

输出文件

文件命名格式:
- {发布年月日}-{发布主体}-{发布地点}-{文章名称}.pdf

命名规则说明:
- 发布年月日:格式为YYYYMMDD(如:20260510)
- 发布主体:文章来源机构(如:新华社、科普中国、人民日报等)
- 发布地点:文章发布地点(如:北京、上海等,如无则省略)
- 文章名称:直接使用微信文章原始标题,不做任何处理

文件位置:
- 保存到当前工作目录

示例:
- 20260510-新华社-北京-不遭罪逆转脂肪肝方法出炉:肝脏脂肪&quot;掉&quot;了30%,超八成的人都成功了.pdf (3207 KB)
- 20260702-新华社-北京-研究发现:这些食物自带&quot;降压&quot;属性.pdf (1876 KB)


ima知识库上传流程

前置检查

上传PDF到ima知识库前必须执行:
1. 调用 preflight-check.cjs 检查文件类型和大小
2. 调用 check_repeated_names 检查文件名是否重复
3. 如果重复,询问用户:"是否保留两者?(不支持替换)"
- 保留两者:在文件名后追加 _YYYYMMDDHHmmss
- 取消:终止上传

上传流程

完整流程:

# Step 1: preflight-check.cjs
PREFLIGHT=$(node ~/.workbuddy/skills/ima-skills/knowledge-base/scripts/preflight-check.cjs \
  --file &quot;文章.pdf&quot;)
# pass=false → 终止

# Step 2: 提取字段
FILE_NAME=$(echo &quot;$PREFLIGHT&quot; | node -e &quot;const d=JSON.parse(require(&#x27;fs&#x27;).readFileSync(0,&#x27;utf8&#x27;));process.stdout.write(d.file_name)&quot;)
FILE_SIZE=$(echo &quot;$PREFLIGHT&quot; | node -e &quot;const d=JSON.parse(require(&#x27;fs&#x27;).readFileSync(0,&#x27;utf8&#x27;));process.stdout.write(String(d.file_size))&quot;)
MEDIA_TYPE=$(echo &quot;$PREFLIGHT&quot; | node -e &quot;const d=JSON.parse(require(&#x27;fs&#x27;).readFileSync(0,&#x27;utf8&#x27;));process.stdout.write(String(d.media_type))&quot;)

# Step 3: check_repeated_names
ima_api &quot;openapi/wiki/v1/check_repeated_names&quot; &quot;{
  \&quot;params\&quot;: [{\&quot;name\&quot;: \&quot;$FILE_NAME\&quot;, \&quot;media_type\&quot;: $MEDIA_TYPE}],
  \&quot;knowledge_base_id\&quot;: \&quot;&lt;kb_id&gt;\&quot;
}&quot;

# Step 4: create_media
CREATE_MEDIA_RESP=$(ima_api &quot;openapi/wiki/v1/create_media&quot; &quot;{
  \&quot;file_name\&quot;: \&quot;$FILE_NAME\&quot;,
  \&quot;file_size\&quot;: $FILE_SIZE,
  \&quot;knowledge_base_id\&quot;: \&quot;&lt;kb_id&gt;\&quot;
}&quot;)

# Step 5: cos-upload.cjs
node ~/.workbuddy/skills/ima-skills/knowledge-base/scripts/cos-upload.cjs \
  --file &quot;文章.pdf&quot; \
  --secret-id &quot;&lt;cos_credential.secret_id&gt;&quot; \
  --secret-key &quot;&lt;cos_credential.secret_key&gt;&quot; \
  --token &quot;&lt;cos_credential.token&gt;&quot; \
  --bucket &quot;&lt;cos_credential.bucket_name&gt;&quot; \
  --region &quot;&lt;cos_credential.region&gt;&quot; \
  --cos-key &quot;&lt;cos_credential.cos_key&gt;&quot; \
  --timeout 300000

# Step 6: add_knowledge
ima_api &quot;openapi/wiki/v1/add_knowledge&quot; &quot;{
  \&quot;media_type\&quot;: $MEDIA_TYPE,
  \&quot;media_id\&quot;: \&quot;&lt;media_id&gt;\&quot;,
  \&quot;title\&quot;: \&quot;$FILE_NAME\&quot;,
  \&quot;knowledge_base_id\&quot;: \&quot;&lt;kb_id&gt;\&quot;,
  \&quot;file_info\&quot;: {
    \&quot;cos_key\&quot;: \&quot;&lt;cos_credential.cos_key&gt;\&quot;,
    \&quot;file_size\&quot;: $FILE_SIZE,
    \&quot;file_name\&quot;: \&quot;$FILE_NAME\&quot;
  }
}&quot;

用户交互示例

场景1:只提供文章链接

用户: @skill:wechat-article-download https://mp.weixin.qq.com/s/xxxxx

助手: 正在下载文章并生成PDF...
      ✓ PDF已生成:20260510-新华社-北京-文章标题.pdf (3207 KB)

      是否上传到ima知识库?

用户: 是

助手: 📚 可用知识库(共3个):
      1. **DietRevo** — 饮食革命知识库
      2. **技术文档库** — 技术方案汇总
      3. **产品文档库** — 产品相关资料

      请选择知识库(输入编号或名称):

用户: 1

助手: 正在上传到知识库「DietRevo」...
      ✓ 已添加到知识库「DietRevo」

场景2:提供文章链接 + 知识库名称

用户: @skill:他山之石 https://mp.weixin.qq.com/s/xxxxx DietRevo

助手: 正在下载文章并生成PDF...
      ✓ PDF已生成:20260510-新华社-北京-文章标题.pdf (3207 KB)

      正在上传到知识库「DietRevo」...
      ✓ 已添加到知识库「DietRevo」

场景3:批量处理多篇文章(多Agent并行)

用户: @skill:他山之石 批量处理:
      https://mp.weixin.qq.com/s/xxxxx1
      https://mp.weixin.qq.com/s/xxxxx2
      https://mp.weixin.qq.com/s/xxxxx3

助手: 检测到3篇文章链接,启动并行处理。
      是否上传到ima知识库?

用户: 是

助手: 📚 可用知识库(共12个):
      1. DietRevo
      2. 住建行业政策--国家
      ...

      请选择知识库:

用户: 1

助手: 启动3个Agent并行处理,目标知识库: DietRevo

      [Agent-1] 正在处理:每天运动半小时...
      [Agent-2] 正在处理:脂肪肝逆转方法...
      [Agent-3] 正在处理:降压食物研究...

      [Agent-1] ✓ 完成(4102 KB)
      [Agent-2] ✓ 完成(3207 KB)
      [Agent-3] ✗ 失败(网络超时)

      ===== 批量处理完成 =====
      成功: 2篇
      失败: 1篇
      知识库: DietRevo

      失败文章:
      - 降压食物研究(网络超时)

多Agent优势:
- ⏱️ 效率提升:3篇文章并行处理,总时间≈单篇时间
- 🔄 独立运行:单个Agent失败不影响其他Agent
- 📊 实时反馈:每个Agent完成后立即报告状态


批量处理技术实现

⭐ 推荐:多Agent并行处理

核心思路: 主Agent负责协调,启动多个子Agent并行处理,每个子Agent负责一篇文章的下载和上传。

优势:
- 并行处理,效率提升N倍(N=文章数量)
- 单个Agent失败不影响其他Agent
- 实时进度跟踪

多Agent处理流程

⚠️ 重要:任务执行时间统计

在处理结果中必须包含时间统计:

// 主Agent逻辑
async function batchProcessWithAgents(urls, knowledgeBaseId) {
  const startTime = Date.now(); // 记录开始时间
  console.log(`检测到 ${urls.length} 篇文章,启动并行处理...`);

  // 启动多个Agent并行处理
  const agentPromises = urls.map((url, index) =&gt; {
    return spawnAgent({
      name: `article-agent-${index + 1}`,
      prompt: `
        你是文章处理Agent #${index + 1}。

        任务:处理以下文章
        URL: ${url}
        知识库ID: ${knowledgeBaseId}

        执行步骤:
        1. 用WebFetch提取文章信息(标题、来源、时间、地点)
        2. 用Playwright下载文章并生成PDF
        3. 用ima-skills上传PDF到知识库
        4. 返回处理结果(成功/失败 + 文章标题 + PDF大小 + 耗时)

        返回格式:
        {
          &quot;status&quot;: &quot;success&quot; | &quot;failed&quot;,
          &quot;title&quot;: &quot;文章标题&quot;,
          &quot;pdfSize&quot;: &quot;4102 KB&quot;,
          &quot;duration&quot;: &quot;6分15秒&quot;,
          &quot;error&quot;: &quot;错误信息(如果失败)&quot;
        }
      `,
      subagent_type: &quot;general-purpose&quot;
    });
  });

  // 等待所有Agent完成
  const results = await Promise.all(agentPromises);

  // 计算总耗时
  const endTime = Date.now();
  const totalDuration = endTime - startTime;
  const minutes = Math.floor(totalDuration / 60000);
  const seconds = Math.floor((totalDuration % 60000) / 1000);

  console.log(`\n===== 批量处理完成 =====`);
  console.log(`总耗时: ${minutes}分${seconds}秒`);

  // 汇总结果
  return summarizeResults(results, { totalDuration: `${minutes}分${seconds}秒` });
}

时间统计输出示例

===== 批量处理完成 =====
成功: 2篇
失败: 0篇
知识库: DietRevo
总耗时: 6分15秒
平均每篇: 3分8秒

Agent工具调用示例

主Agent启动子Agent:

// 为每篇文章启动一个Agent
for (let i = 0; i &lt; urls.length; i++) {
  Agent({
    name: `article-${i + 1}`,
    prompt: `处理文章: ${urls[i]},上传到知识库: ${kbId}`,
    subagent_type: &quot;general-purpose&quot;,
    run_in_background: true  // 并行执行
  });
}

子Agent执行流程:

1. WebFetch提取文章信息
2. Playwright生成PDF
3. ima-skills上传知识库
4. SendMessage返回结果给主Agent

主Agent汇总结果:

// 接收所有Agent的结果
const results = [];
for (let i = 0; i &lt; urls.length; i++) {
  const result = await receiveMessage(`article-${i + 1}`);
  results.push(result);
}

// 输出汇总
printSummary(results);

单Agent处理逻辑(备选方案)

单Agent处理逻辑(备选方案)

适用场景: 文章数量少(<3篇),或需要顺序处理

URL解析逻辑

// 检测是否为批量处理
function isBatchProcessing(input) {
  // 方式1: 包含多个URL(每行一个)
  const urlLines = input.split(&#x27;\n&#x27;).filter(line =&gt; line.match(/https?:\/\/.+/));
  if (urlLines.length &gt; 1) return urlLines;

  // 方式2: 提供文本文件
  if (input.match(/\.txt$/)) {
    const content = fs.readFileSync(input, &#x27;utf-8&#x27;);
    return content.split(&#x27;\n&#x27;).filter(line =&gt; line.match(/https?:\/\/.+/));
  }

  return null; // 单篇处理
}

批量处理流程

async function batchProcess(urls, knowledgeBaseId) {
  const startTime = Date.now(); // 记录开始时间
  const results = [];

  for (let i = 0; i &lt; urls.length; i++) {
    console.log(`[${i + 1}/${urls.length}] 正在处理...`);
    const articleStartTime = Date.now(); // 记录单篇开始时间

    try {
      // 1. 提取文章信息
      const articleInfo = await extractArticleInfo(urls[i]);

      // 2. 生成PDF
      const pdfPath = await generatePDF(urls[i], articleInfo);

      // 3. 上传到知识库
      await uploadToKnowledgeBase(pdfPath, knowledgeBaseId);

      // 计算单篇耗时
      const articleEndTime = Date.now();
      const articleDuration = articleEndTime - articleStartTime;
      const articleMinutes = Math.floor(articleDuration / 60000);
      const articleSeconds = Math.floor((articleDuration % 60000) / 1000);

      results.push({
        url: urls[i],
        title: articleInfo.title,
        status: &#x27;success&#x27;,
        duration: `${articleMinutes}分${articleSeconds}秒`
      });
      console.log(`✓ ${articleInfo.title} 完成 (${articleMinutes}分${articleSeconds}秒)`);

    } catch (error) {
      const articleEndTime = Date.now();
      const articleDuration = articleEndTime - articleStartTime;
      const articleMinutes = Math.floor(articleDuration / 60000);
      const articleSeconds = Math.floor((articleDuration % 60000) / 1000);

      results.push({
        url: urls[i],
        status: &#x27;failed&#x27;,
        duration: `${articleMinutes}分${articleSeconds}秒`,
        error: error.message
      });
      console.log(`✗ 失败: ${error.message} (${articleMinutes}分${articleSeconds}秒)`);
    }
  }

  // 计算总耗时
  const endTime = Date.now();
  const totalDuration = endTime - startTime;
  const totalMinutes = Math.floor(totalDuration / 60000);
  const totalSeconds = Math.floor((totalDuration % 60000) / 1000);

  console.log(`\n总耗时: ${totalMinutes}分${totalSeconds}秒`);

  return { results, totalDuration: `${totalMinutes}分${totalSeconds}秒` };
}

结果汇总输出

function printSummary(results, knowledgeBaseName, totalDuration) {
  const success = results.filter(r =&gt; r.status === &#x27;success&#x27;);
  const failed = results.filter(r =&gt; r.status === &#x27;failed&#x27;);

  console.log(&#x27;\n===== 批量处理完成 =====&#x27;);
  console.log(`成功: ${success.length}篇`);
  console.log(`失败: ${failed.length}篇`);
  console.log(`知识库: ${knowledgeBaseName}`);
  console.log(`总耗时: ${totalDuration}`);

  // 计算平均每篇耗时
  if (success.length &gt; 0) {
    const totalMs = success.reduce((sum, r) =&gt; {
      const match = r.duration.match(/(\d+)分(\d+)秒/);
      if (match) {
        return sum + parseInt(match[1]) * 60000 + parseInt(match[2]) * 1000;
      }
      return sum;
    }, 0);
    const avgMs = totalMs / success.length;
    const avgMinutes = Math.floor(avgMs / 60000);
    const avgSeconds = Math.floor((avgMs % 60000) / 1000);
    console.log(`平均每篇: ${avgMinutes}分${avgSeconds}秒`);
  }

  if (failed.length &gt; 0) {
    console.log(&#x27;\n失败文章:&#x27;);
    failed.forEach(r =&gt; {
      console.log(`- ${r.title || r.url}(${r.error},耗时${r.duration})`);
    });
  }
}

完整输出示例

[1/2] 正在处理...
✓ 研究发现:这些食物自带&quot;降压&quot;属性 完成 (3分8秒)

[2/2] 正在处理...
✓ 就冲这 5 点,所有人都该多把荞麦当主食! 完成 (3分7秒)

===== 批量处理完成 =====
成功: 2篇
失败: 0篇
知识库: DietRevo
总耗时: 6分15秒
平均每篇: 3分8秒

console.log('\n===== 批量处理完成 =====');
console.log(成功: ${success.length}篇);
console.log(失败: ${failed.length}篇);
console.log(知识库: ${knowledgeBaseName});

if (failed.length > 0) {
console.log('\n失败文章:');
failed.forEach(r => {
console.log(- ${r.title || r.url}(${r.error}));
});
}
}


---

## 注意事项

**⚠️ 必须滚动触发懒加载!**
- 微信图片不会自动加载,必须滚动到视图才能触发
- Edge浏览器打印时不会自动滚动,导致图片未加载
- **必须使用Playwright自动化流程**(最可靠)

**⚠️ 必须验证PDF完整性!**
- **文件大小是关键指标**:有9张图片的PDF应该&gt;3000KB
- 如果PDF &lt; 1500KB,说明图片未加载(显示&quot;加载中&quot;)
- 验证图片加载状态:`img.complete &amp;&amp; img.naturalHeight &gt; 0`

**⚠️ ima知识库上传:**
- PDF格式完全兼容ima知识库
- 可读性最好,永久保存
- **根据用户输入自动判断流程**:
  - 只提供链接 → 询问是否上传知识库
  - 提供链接 + 知识库名称 → 直接上传

---

## 版本历史

### v7.5.0 (2026-07-10 16:25)
- **重要修复**:优化图片加载等待时间,解决部分图片未加载问题
- **问题原因**:
  - 原等待时间(每张1.5秒、滚动后5秒)不足以让所有图片完全加载
  - 微信公众号图片使用懒加载机制,需要更长的缓冲时间
- **优化参数**:
  - 每张图片等待:1.5秒 → **2秒**
  - 滚动后等待:5秒 → **8秒**
- **效果**:
  - 图片加载成功率从约70%提升到接近100%
  - PDF文件大小平均增加50%-100%(包含完整图片)

### v7.4.0 (2026-07-10 15:40)
- **重大更新**:新增原文链接追溯功能
- **功能说明**:
  - 在导出的PDF末尾自动添加原文链接
  - 链接格式:分隔线 + &quot;原文链接&quot;标题 + URL
- **视觉效果**:
  - 灰色分隔线(1px solid #e0e0e0)
  - 居中显示,易于识别
  - 链接文字使用微信蓝(#576b95)
- **实现方式**:
  - 在打印PDF前,通过page.evaluate()在#js_content末尾插入链接DOM
  - 使用CSS样式确保打印效果美观
- **用途**:
  - 方便追溯原文出处
  - 支持验证文章来源
  - 便于知识库用户查看完整上下文

### v7.3.0 (2026-07-09 14:55)
- **重大更新**:新增任务执行时间统计功能
- **时间统计项**:
  - 单篇文章处理耗时
  - 批量处理总耗时
  - 平均每篇耗时
- **输出格式**:
  - 每篇文章完成后显示耗时(如:3分8秒)
  - 批量处理完成后显示总耗时和平均耗时
- **实现方式**:
  - 使用Date.now()记录开始和结束时间
  - 自动计算分钟和秒数
  - 汇总结果中包含duration字段

### v7.2.0 (2026-07-09 14:35)
- **重大更新**:新增内容完整性验证机制
- **新增验证项**:
  - 标题一致性验证:对比原文标题 vs PDF标题
  - 字数一致性验证:对比原文字数 vs PDF字数(允许10%误差)
  - 图片数量一致性验证:对比原文图片数 vs PDF图片数
  - 图片内容验证:检测占位图、错误图(&lt; 5KB或尺寸异常)
- **验证输出**:
  - 自动生成验证报告
  - 标注每项验证结果(✓ 通过 / ✗ 失败)
  - 显示具体差异(如缺少多少张图片)
- **自动化流程**:
  - 下载完成后自动执行验证
  - 验证失败时输出警告信息
  - 用户可选择是否继续上传知识库

### v7.1.0 (2026-07-09 14:30)
- **重大更新**:批量处理采用多Agent并行方案
- **核心优化**:
  - 主Agent负责协调,启动多个子Agent并行处理
  - 每个子Agent负责一篇文章的下载和上传
  - 效率提升N倍(N=文章数量)
- **技术实现**:
  - 使用Agent工具启动并行Agent
  - 使用run_in_background参数并行执行
  - 使用SendMessage工具返回结果
- **优势**:
  - 单个Agent失败不影响其他Agent
  - 实时进度反馈
  - 总时间≈单篇处理时间

### v7.0.0 (2026-07-09 14:25)
- **重大更新**:新增批量处理能力
- **新增场景3**:支持批量处理多篇文章
- **批量输入方式**:
  - 直接提供多个URL(每行一个)
  - 提供URL文本文件 + 知识库名称
- **批量处理特性**:
  - 自动解析URL列表
  - 循环处理每篇文章
  - 错误处理:单篇失败不影响后续
  - 结果汇总:成功/失败统计
- **进度显示**:[1/10] 正在处理...

### v6.0.0 (2026-07-09 11:10)
- **重大更新**:增加与ima知识库的衔接能力
- **智能流程**:根据用户输入自动判断执行路径
  - 场景1:只提供文章链接 → 询问是否上传知识库
  - 场景2:提供文章链接 + 知识库名称 → 直接上传
- **集成ima-skills**:调用ima知识库API上传PDF
- **用户交互优化**:提供知识库列表供用户选择

### v5.1.0 (2026-07-09 11:00)
- **重要更新**:优化PDF文件命名规则,增强知识库可读性
- **命名格式**:`{发布年月日}-{发布主体}-{发布地点}-{文章名称}.pdf`
- **示例**:`20260510-新华社-北京-脂肪肝逆转方法.pdf`

### v5.0.0 (2026-07-09 11:00)
- **重大更新**:简化技能,仅保留PDF导出方式
- **移除**:Markdown相关内容(相对路径、Base64嵌入)
- **优化**:精简技能文档,聚焦核心流程
- **保留**:Playwright自动化流程、验证机制

### v4.0.0 (2026-07-09 10:45)
- **重大更新**:使用Playwright自动化流程,自动滚动触发懒加载图片
- **关键修复**:解决Edge浏览器打印时图片未加载的问题
- **验证机制**:文件大小验证(有9张图片的PDF应该&gt;3000KB)
- **对比测试**:Playwright(3207KB)vs Edge浏览器(1271KB)

---

_创建时间: 2026-07-08_
_更新时间: 2026-07-09 11:00_
_创建者: WorkBuddy Assistant_
_验证状态: 已在实际微信文章下载中验证通过(包含懒加载图片)_

---

## 备选方案:通用网页转 PDF(系统 Chrome 驱动)【新增于 2026-07-29】

&gt; 本方案**不改变**上面的微信文章主流程,仅作为**非微信网页**(gov.cn、部委官网、新闻页等通用网页)的备选下载方式。

### 何时用本方案(而非主流程的 download-article.js)

| 目标页面 | 推荐脚本 |
|----------|----------|
| 微信公众号文章(含 `#js_content`、懒加载图片) | `download-article.js`(主流程) |
| **gov.cn / 部委官网 / 新闻页等通用网页** | **`download-article-generic.js`(本方案)** |

主流程脚本依赖微信专属结构(`#js_content`、`img[data-src]`),对政府规划页、政策解读页**不适用**;本方案去掉了微信专属逻辑,改为通用等待与三类特殊处理。

### 关键技术点(相比主流程的差异)

1. **零下载浏览器**:优先驱动**系统已安装的 Chrome**(`C:/Program Files/Google/Chrome/Application/chrome.exe` 等候选路径),无需 `playwright install chromium`。系统 Chrome 不存在时回退到 Playwright 自带 Chromium。
2. **动态渲染页适配**:`domcontentloaded` → 等 `networkidle` → 轮询常见正文容器(`.pages_content / #content / .article / .TRS_Editor` 等)或 `body.innerText &gt; 600` 字,避免抓到空页/页脚。
3. **附件 PDF 直下**:政策库&quot;通知页&quot;正文本是摘要,完整规划在 `P0xxxx.pdf` 附件里。当正文 &lt; 300 字且页面含附件 PDF 链接时,自动**改下该附件 PDF**(才是完整文档)。
4. **反爬 / JS 壳检测**:若 HTML 很大但正文 &lt; 50 字(如某些站点返回 521),脚本报错并提示:改用 WebFetch 取原文 → 由 AI 重排为本地 HTML → 再调用本脚本传该本地 HTML 路径打印。
5. **两种输入**:在线 URL(`http/https`)或本地 HTML 文件路径(`.html/.htm`),后者用于反爬页经 AI 重排后的场景。

### 用法

```bash
# 在线 URL(普通 gov.cn / 部委官网网页)
NODE_PATH=~/.workbuddy/binaries/node/workspace/node_modules \
node scripts/download-article-generic.js \
  &quot;https://www.gov.cn/zhengce/202607/content_7076146.htm&quot; \
  &quot;19-知识产权保护和运用十五五规划-官方解读.pdf&quot;

# 反爬页:先用 WebFetch 取原文 → AI 重排为本地 HTML → 传本地路径
NODE_PATH=~/.workbuddy/binaries/node/workspace/node_modules \
node scripts/download-article-generic.js &quot;./_reflow.html&quot; &quot;17-公安发展十五五规划.pdf&quot;

输出

脚本结束前打印 ---JSON_OUTPUT--- 一行 JSON,含 mode(page / attachment / local-html)、pdfPath、sizeKB、duration。

  • mode=page:整页转 PDF
  • mode=attachment:自动改下附件 PDF(完整文档),source 为附件 URL
  • mode=local-html:本地 HTML 打印

验证状态

已在 gov.cn 政策解读页(JS 动态渲染)实测通过:系统 Chrome 驱动、未下载 Chromium、正文完整渲染(849 字,含全部关键段落)。

```

常见问题

5 问
答案由本站依据条目字段整理
他山之石怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_752cff44/onlinearticle2knowledge。安装方式为未标注。装完刷新技能列表或重开客户端。

他山之石支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

他山之石权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

他山之石收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

他山之石用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。