一个用于获取和解析 RSS 2.0 订阅源(支持 content:encoded 命名空间)的静态 Web 应用,以统一格式展示每日报告条目。内置跨日对比视图,可按工具名称和日期聚合条目,展示讨论热度图、版本时间线、热门话题排行和反复出现的问题追踪。
专为追踪 AI 开发者工具动态而设计,支持中文界面。
-
克隆仓库:
git clone git@github.com:wang2032/multica.git cd multica -
在浏览器中打开
index.html即可运行,无需安装任何依赖或构建步骤。 -
默认加载项目内的
feed.xml示例文件。你也可以输入任何 RSS 2.0 订阅源的 URL 来加载。
npm install
npm run build然后在你的 TypeScript/JavaScript 项目中引入:
import { ingestFeed, normalizeContent } from "multica";Multica 包含两个独立的层级:
multica/
├── 浏览器端(静态 Web 应用)
│ ├── index.html # 单页面入口
│ ├── styles.css # 样式表
│ ├── main.js # UI 控制器
│ ├── feed-parser.js # RSS 解析器(使用 DOMParser)
│ ├── cross-day.js # 跨日对比引擎
│ └── feed.xml # 示例 RSS 数据
│
├── Node.js 端(TypeScript 库)
│ └── src/
│ ├── index.ts # 模块导出
│ ├── types.ts # Block 类型定义
│ ├── normalize.ts # HTML 内容规范化
│ ├── normalize.test.ts # 规范化测试
│ └── feed/
│ ├── index.ts # Feed 模块导出 + ingestFeed()
│ ├── types.ts # FeedItem / FeedResult 接口
│ ├── parser.ts # RSS 解析器(基于正则)
│ ├── parser.test.ts # 解析器测试
│ └── fetcher.ts # HTTP/本地文件获取器
│
├── package.json
├── tsconfig.json
└── jest.config.js
- 浏览器端:纯 JavaScript 文件,通过
<script>标签直接加载,无需构建步骤。使用浏览器原生DOMParser解析 XML。 - Node.js 端:TypeScript 编写的可复用库,提供 RSS 获取、解析和 HTML 内容规范化功能。
- 打开页面后,顶部输入框默认填入
feed.xml(本地示例文件)。 - 你可以替换为任何 RSS 2.0 订阅源的 URL,例如
https://example.com/feed.xml。 - 点击 Load Feed 按钮或按回车键加载。
加载成功后,页面会显示频道信息和所有条目。
条目列表以卡片形式展示,每张卡片包含:
- 标题:RSS 条目的
<title>值 - 发布日期:来自
<pubDate>或<dc:date>,以中文格式显示 - 摘要:来自
<description>的简短描述
加载订阅源后,页面顶部会出现筛选栏,提供以下筛选方式:
| 筛选项 | 说明 |
|---|---|
| 关键词搜索 | 在标题、摘要和 HTML 内容中进行全文搜索 |
| 起始日期 | 筛选发布日期 >= 该日期的条目 |
| 截止日期 | 筛选发布日期 <= 该日期的条目 |
| 工具筛选 | 从下拉菜单选择特定工具名称,仅显示该工具的条目 |
| Clear 按钮 | 清除所有筛选条件 |
工具下拉菜单的选项会根据已加载的订阅源数据自动填充。筛选栏右侧会显示当前匹配的条目数量。
- 在列表中点击任意条目卡片,进入详情视图。
- 详情页展示条目的完整 HTML 内容(来自
<content:encoded>)。 - 点击 Show raw XML 按钮可以查看该条目的原始 XML 代码。
- 点击 ← Back to list 按钮返回列表视图。
点击 7 Day Comparison 按钮,进入跨日对比视图。该视图会按工具名称和日期聚合所有条目,展示以下内容:
页面顶部显示 5 张统计卡片:
| 统计项 | 说明 |
|---|---|
| 工具数量 | 识别到的不同工具总数 |
| 覆盖天数 | 数据跨越的天数 |
| 话题总数 | 所有工具的话题合计 |
| 版本更新 | 所有工具的版本更新合计 |
| 反复出现的问题 | 在 2 天以上出现的问题数量 |
SVG 面积图,展示每个工具随时间的讨论热度变化。横轴为日期,纵轴为讨论量。不同工具以不同颜色的面积区域表示。
每个工具一个独立面板,包含:
- 版本时间线:按时间顺序列出该工具的所有版本变更
- 热门话题排行:横条图展示该工具按出现频率排列的热门话题
- 反复出现的问题:以警告色高亮显示在多个日期反复出现的问题列表
npm install安装的依赖包括:
- 运行时依赖:
node-html-parser(用于 HTML 解析) - 开发依赖:
typescript、jest、ts-jest、类型定义
// 从 URL 或本地文件获取并解析 RSS 订阅源
import { ingestFeed } from "./src/feed";
const result = await ingestFeed("https://example.com/feed.xml");
// 或使用本地文件
// const result = await ingestFeed("./feed.xml");
console.log(result.feedTitle); // 订阅源标题
console.log(result.feedDescription); // 订阅源描述
console.log(result.items.length); // 条目数量
console.log(result.fetchedAt); // 获取时间 (Date)
for (const item of result.items) {
console.log(item.title); // 条目标题
console.log(item.link); // 条目链接
console.log(item.pubDate); // 发布日期 (Date)
console.log(item.summary); // 摘要
console.log(item.htmlContent); // HTML 内容
}也可以分开调用获取和解析:
import { fetchFeed, parseRssFeed } from "./src/feed";
// 单独获取原始 XML
const xml = await fetchFeed("https://example.com/feed.xml");
// 单独解析 XML 字符串
const result = parseRssFeed(xml, "https://example.com/feed.xml");将 RSS 条目中的 HTML 内容解析为结构化数据块:
import { normalizeContent, parseHtml, extractMetadata } from "./src";
const html = "<h2>2026-04-11: Claude</h2><p>New features released.</p>";
// 完整规范化:解析 + 提取元数据
const normalized = normalizeContent(html);
console.log(normalized.blocks); // Block[] 结构化内容块
console.log(normalized.metadata); // { date: "2026-04-11", topic: "claude" }
// 仅解析 HTML 为 Block 数组
const blocks = parseHtml(html);
// 仅从已有的 Block 数组中提取元数据
const metadata = extractMetadata(blocks);| 函数 | 签名 | 说明 |
|---|---|---|
ingestFeed |
(source: string) => Promise<FeedResult> |
一步完成获取和解析。source 可以是 HTTP/HTTPS URL 或本地文件路径 |
fetchFeed |
(source: string) => Promise<string> |
获取原始 XML 文本。URL 使用 HTTP(S) 获取,其他路径视为本地文件 |
parseRssFeed |
(xml: string, sourceUrl?: string) => FeedResult |
解析 RSS 2.0 XML 字符串,支持 CDATA、content:encoded 命名空间 |
| 函数 | 签名 | 说明 |
|---|---|---|
normalizeContent |
(htmlContent: string) => NormalizedContent |
主入口:解析 HTML 并提取元数据,返回 { blocks, metadata } |
parseHtml |
(html: string) => Block[] |
将 HTML 解析为结构化 Block 数组 |
extractMetadata |
(blocks: Block[]) => ContentMetadata |
从 Block 数组中提取日期和话题信息 |
// Feed 类型
interface FeedItem {
id: string; // 唯一标识(来自 <guid>,回退到 <link>)
title: string; // 条目标题
link: string; // 条目链接
pubDate: Date; // 发布日期
summary: string; // 纯文本摘要
htmlContent: string; // 完整 HTML 内容
rawXml: string; // 原始 <item> XML
}
interface FeedResult {
sourceUrl: string; // 订阅源 URL
feedTitle: string; // 订阅源标题
feedDescription: string; // 订阅源描述
items: FeedItem[]; // 解析后的条目数组
fetchedAt: Date; // 获取时间
}
// Normalize 类型
interface InlineText {
text: string; // 文本内容
bold?: boolean; // 粗体标记
italic?: boolean; // 斜体标记
code?: boolean; // 行内代码标记
href?: string; // 链接 URL
}
type Block =
| HeadingBlock // 标题(h1-h6),含 level 和 children
| ParagraphBlock // 段落
| ListBlock // 列表(有序/无序),含 items 二维数组
| TableBlock // 表格,含可选 header 和 rows
| QuoteBlock // 引用块
| LinkBlock; // 独立链接
interface NormalizedContent {
blocks: Block[]; // 结构化内容块
metadata: ContentMetadata; // 提取的元数据
}
interface ContentMetadata {
date?: string; // 内容中提取的日期(YYYY-MM-DD 格式)
topic?: string; // 从标题或链接中提取的话题标识
}Multica 支持标准 RSS 2.0 格式的订阅源,并额外支持以下扩展:
content:encoded命名空间(用于完整 HTML 内容)dc:date命名空间(作为<pubDate>的替代)
每个解析后的条目包含以下字段:
| 字段 | 数据来源 | 说明 |
|---|---|---|
id |
<guid> 或 <link> |
唯一标识 |
title |
<title> |
条目标题 |
link |
<link> |
条目链接 |
pubDate |
<pubDate> 或 <dc:date> |
发布日期 |
summary |
<description> |
纯文本摘要 |
htmlContent |
<content:encoded> |
完整 HTML 内容(无则回退到 description) |
rawXml |
序列化的 <item> 节点 |
原始 XML |
若要使用跨日对比功能,条目标题应遵循以下命名规范:
<工具名称> YYYY-MM-DD
例如:Claude 2026-04-11、Cursor 2026-04-10
条目的 HTML 内容(<content:encoded>)应使用以下标题划分段落:
| 中文标题 | 英文标题 | 用途 |
|---|---|---|
| 版本动态 | Version | 版本更新信息 |
| 热门话题 | Topics | 热门讨论话题 |
| 反复出现的问题 | Issues | 反复出现的问题 |
示例内容结构:
<h2>版本动态</h2>
<ul>
<li>v3.5 新增代码补全功能</li>
<li>修复了内存泄漏问题</li>
</ul>
<h2>热门话题</h2>
<ul>
<li>多语言支持需求增加</li>
<li>性能优化成为关注焦点</li>
</ul>
<h2>反复出现的问题</h2>
<ul>
<li>大文件处理时偶发崩溃</li>
</ul>| 文件 | 说明 |
|---|---|
index.html |
单页面 HTML 入口,包含页面结构和 UI 元素 |
styles.css |
全部 CSS 样式(737 行),深色主题,卡片布局,响应式设计 |
main.js |
UI 控制器(854 行):管理列表、详情、跨日对比三个视图,处理搜索筛选,生成 SVG 图表 |
feed-parser.js |
RSS 2.0 解析器(128 行):使用浏览器 DOMParser 解析 XML,支持 content:encoded 命名空间 |
cross-day.js |
跨日对比引擎(287 行):按工具聚合条目,构建版本时间线、热门排行和问题追踪 |
feed.xml |
示例 RSS 订阅源数据(509 行),用于测试 |
src/index.ts |
模块导出:重新导出 normalize 模块的函数和类型 |
src/types.ts |
Block 类型系统定义:InlineText、HeadingBlock、ParagraphBlock 等 |
src/normalize.ts |
HTML 内容规范化器(311 行):解析 HTML 为结构化 Block 数组并提取元数据 |
src/feed/index.ts |
Feed 模块入口:导出获取、解析函数和 ingestFeed() 便捷方法 |
src/feed/types.ts |
FeedItem 和 FeedResult 接口定义 |
src/feed/parser.ts |
RSS 2.0 解析器(基于正则,无 DOM 依赖) |
src/feed/fetcher.ts |
HTTP(S)/本地文件获取器(55 行),支持重定向跟随(最多 5 次) |
| 类别 | 技术 |
|---|---|
| 前端 | 原生 JavaScript(无框架),HTML5,CSS3 |
| 后端/库 | TypeScript,Node.js |
| HTML 解析 | node-html-parser(Node.js 端)/ DOMParser(浏览器端) |
| 构建工具 | TypeScript 编译器 (tsc) |
| 测试框架 | Jest + ts-jest |
| 图表 | 自生成 SVG(无第三方图表库) |
| 国际化 | 中文界面,日期使用 zh-CN locale |
编译 TypeScript 源码到 dist/ 目录:
npm run build编译配置(tsconfig.json):
- 输入目录:
src/ - 输出目录:
dist/ - 模块系统:CommonJS
- 目标版本:ES2022
- 启用严格模式
- 生成 source map 和声明文件(
.d.ts)
运行全部测试:
npm test测试覆盖范围:
src/feed/parser.test.ts:RSS 解析测试,包括频道元数据、条目提取、CDATA 处理、XML 实体、边界情况src/normalize.test.ts:HTML 规范化测试,包括标题、段落、列表、表格、引用、行内格式、元数据提取、集成测试
测试配置(jest.config.js):
- 预设:
ts-jest - 环境:
node - 测试文件匹配:
src/**/*.test.ts
RSS 2.0 原始字段 → 统一字段
─────────────────────────────────────
<guid> → id
<title> → title
<link> → link
<pubDate> / <dc:date> → pubDate
<description> → summary
<content:encoded> → htmlContent
原始 <item> 节点 → rawXml
HTML 元素 → Block 类型
────────────────────────────────
<h1> ~ <h6> → HeadingBlock(含 level: 1-6)
<p> → ParagraphBlock
<ul> / <ol> → ListBlock(含 ordered: boolean)
<table> → TableBlock(含可选 header)
<blockquote> → QuoteBlock
独立 <a> → LinkBlock
<div>/<section>/... → 递归解析子元素
其他元素 → 作为 ParagraphBlock 处理
HTML 标签 → InlineText 属性
─────────────────────────────────────
<strong> / <b> → bold: true
<em> / <i> → italic: true
<code> → code: true
<a href="..."> → href: "..."
<br> → text: "\n"