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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Designed for **agent integration** — structured JSON output, MCP server suppor
- **CLI + MCP dual interface** — use as a command-line tool or as an MCP server for AI agents
- **Agent Skill** — `mpget init` injects a skill file into `.claude/skills/` with version sync
- **Multi-page search** — automatic pagination with configurable max pages and rate limiting
- **Article content extraction** — fetches and cleans WeChat article body text
- **Article content extraction** — fetches WeChat article body as Markdown (headings, lists, code blocks, images, quotes, tables)
- **Album article listing** — list all articles in an official account's album (with real mp.weixin.qq.com links)
- **Structured JSON output** — all results to stdout, errors to stderr with typed exit codes

Expand Down Expand Up @@ -104,7 +104,7 @@ All commands output JSON to stdout. Errors go to stderr as structured JSON.

```json
{
"content": "Article body text..."
"content": "### 小标题\n\n正文段落,**加粗**文字。\n\n![示意图](https://mmbiz.qpic.cn/mmbiz_png/abc/0?wx_fmt=png)\n\n- 条目一\n- 条目二"
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ CLI 工具与 MCP 服务,用于搜索和获取微信公众号文章(基于
- **CLI + MCP 双模式** — 命令行工具 + MCP Server,适配多种 AI Agent
- **Agent Skill** — `mpget init` 一键注入 Skill 到 `.claude/skills/`,带版本同步
- **多页搜索** — 自动分页,支持配置最大页数和请求间隔
- **文章内容提取** — 获取并清洗微信公众号文章正文
- **文章内容提取** — 正文按 Markdown 返回,保留标题、列表、代码块、图片、引用与表格
- **合集文章列表** — 拉取某公众号合集内全部文章(含真实 mp.weixin.qq.com 链接)
- **结构化 JSON 输出** — 结果输出到 stdout,错误输出到 stderr,带类型化退出码

Expand Down Expand Up @@ -104,7 +104,7 @@ mpget init

```json
{
"content": "文章正文..."
"content": "### 小标题\n\n正文段落,**加粗**文字。\n\n![示意图](https://mmbiz.qpic.cn/mmbiz_png/abc/0?wx_fmt=png)\n\n- 条目一\n- 条目二"
}
```

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
"email": "wszwc3721@163.com"
},
"dependencies": {
"@joplin/turndown-plugin-gfm": "^1.0.68",
"@modelcontextprotocol/sdk": "^1.29.0",
"cheerio": "^1.0.0",
"commander": "^12.0.0"
"commander": "^12.0.0",
"turndown": "^7.2.4"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"@types/turndown": "^5.0.6",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.4",
Expand Down
112 changes: 72 additions & 40 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/commands/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fetchArticleContent } from "../lib/fetcher.js"
export function setupContentCommand(program: Command): void {
program
.command("content <url>")
.description("获取微信公众号文章正文")
.description("获取微信公众号文章正文(Markdown)")
.option("-r, --referer <url>", "请求来源")
.action(async (url, opts) => {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function createMcpServer(): Server {
},
{
name: "content",
description: "获取微信公众号文章正文",
description: "获取微信公众号文章正文(Markdown)",
inputSchema: {
type: "object" as const,
properties: {
Expand Down
56 changes: 56 additions & 0 deletions src/lib/__test__/fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,60 @@ describe("fetchArticleContent", () => {
"获取文章内容失败: 未拿到有效的微信公众号文章链接",
)
})

it("正文按 Markdown 返回,保留标题、列表、代码块、图片、引用与表格", async () => {
const html = readFileSync(join(fixtureDir, "article-content.html"), "utf-8")
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes("https://mp.weixin.qq.com/s/a", html)))

const md = await fetchArticleContent("https://mp.weixin.qq.com/s/a")

expect(md).toContain("### 小标题")
expect(md).toContain("第一段**加粗**文字。")
expect(md).toContain("- 条目一")
expect(md).toContain("> 引用一段话")
expect(md).toContain("```\nnpm install -g mpget\n```")
expect(md).toContain("| 列 A | 列 B |")
expect(md).toContain("末尾*斜体*结束。")
})

it("图片取 data-src,跳过 data: 占位图", async () => {
const html = readFileSync(join(fixtureDir, "article-content.html"), "utf-8")
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes("https://mp.weixin.qq.com/s/b", html)))

const md = await fetchArticleContent("https://mp.weixin.qq.com/s/b")

expect(md).toContain("![示意图](https://mmbiz.qpic.cn/mmbiz_png/abc/0?wx_fmt=png)")
expect(md).not.toContain("data:image/gif")
})

it("图片地址与 alt 按 Markdown 语法转义", async () => {
const html = readFileSync(join(fixtureDir, "article-content.html"), "utf-8")
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes("https://mp.weixin.qq.com/s/e", html)))

const md = await fetchArticleContent("https://mp.weixin.qq.com/s/e")

// 地址含空格与圆括号时 turndown 会加尖括号并转义,alt 里的方括号也会被转义
expect(md).toContain("![图\\[1\\]](<https://mmbiz.qpic.cn/a/b \\(1\\).png>)")
})

it("排除 script/style 与 js_content 之外的内容", async () => {
const html = readFileSync(join(fixtureDir, "article-content.html"), "utf-8")
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes("https://mp.weixin.qq.com/s/c", html)))

const md = await fetchArticleContent("https://mp.weixin.qq.com/s/c")

expect(md).not.toContain("脚本内容不该出现在正文里")
expect(md).not.toContain("display: none")
expect(md).not.toContain("正文之外的噪声")
})

it("没有 js_content 时返回失败提示", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(mockRes("https://mp.weixin.qq.com/s/d", "<div>没有正文</div>")),
)
expect(await fetchArticleContent("https://mp.weixin.qq.com/s/d")).toBe(
"获取文章内容失败: 正文为空",
)
})
})
Loading
Loading