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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The pipeline runs in four sequential phases, each implemented as a named async f
| `src/web.ts` | Sitemap-based web content fetching; state persisted to `digests/web-state.json` |
| `src/trending.ts` | GitHub Trending HTML scraper + Search API topic queries |
| `src/hn.ts` | Hacker News top AI stories via Algolia HN Search API |
| `src/generate-manifest.ts` | Generates `manifest.json` (sidebar data for Web UI) and `feed.xml` (RSS 2.0 feed) |
| `src/generate-manifest.ts` | Generates `manifest.json` (sidebar data for Web UI), `digests/search-index.json` (topic search data), and `feed.xml` (RSS 2.0 feed) |
| `src/topic-radar.ts` | Normalizes source data into an editorial topic pool, scores topics, and writes `ai-topic-radar.md` / `topic-pool.json` |
| `src/kr36.ts` | 36kr AI news articles fetched via RSS feed |
| `src/infoq-cn.ts` | InfoQ China AI articles fetched via internal API |
Expand Down Expand Up @@ -124,12 +124,15 @@ Files written to `digests/YYYY-MM-DD/`:
- GitHub issue label colors are defined in `LABEL_COLORS` in `src/github.ts`. Add new labels there.
- `sampleNote(total, sampled)` in `src/prompts.ts` formats the "(共 N 条,展示前 M 条)" note. Reuse it — do not inline the same string format.
- Web state (`digests/web-state.json`) is committed to git on every run. It is the source of truth for which URLs have been seen.
- Topic search state (`digests/search-index.json`) is generated from daily `topic-pool.json` files. It reads the current `candidates` field first and falls back to legacy `topics`.
- Topic candidates are URL-deduplicated. GitHub + Gitee repository candidates are capped at 20 items, and GitHub repository heat uses logarithmic scoring to avoid star-only dominance.

## Web UI & RSS Feed

- Web UI: `index.html` reads `manifest.json` to build the sidebar, then fetches `digests/YYYY-MM-DD/report.md` on demand.
- Search UI: `index.html` reads `digests/search-index.json`, which is generated in the same `pnpm manifest` step.
- RSS Feed: `feed.xml` at the repo root. Generated by `src/generate-manifest.ts` in the same `pnpm manifest` step. Contains the latest 30 items (newest first) across all report types. Item links use hash routing: `https://conradgui.github.io/AI-TREND-RADAR/#YYYY-MM-DD/report`.
- Both `manifest.json` and `feed.xml` are committed together in the "Commit manifest and feed" GHA step.
- `manifest.json`, `digests/search-index.json`, and `feed.xml` are committed together in the "Commit manifest and feed" GHA step.
- The `REPORT_LABELS` map in `src/i18n.ts` must be kept in sync with the `LABELS` object in `index.html` when adding new report types.

## Adding a new report type
Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
| `digests/YYYY-MM-DD/ai-topic-radar.md` | Markdown 版本 |
| `digests/YYYY-MM-DD/topic-pool.json` | 结构化选题池(分数、分类、动作、理由、证据) |
| `digests/YYYY-MM-DD/ai-china-tech.md` | 中文科技社区 AI 动态日报(36kr + InfoQ + Gitee + OSChina + 掘金) |
| `manifest.json` | 历史 Web UI 索引 |
| `manifest.json` | 历史 Web UI 侧边栏索引 |
| `digests/search-index.json` | 历史选题搜索索引 |
| `feed.xml` | RSS 订阅源 |

### 适合谁?
Expand Down Expand Up @@ -86,6 +87,17 @@ pnpm digest

某个来源失败不会中断日报,主报告会在"数据源状态与修复提示"里说明原因。

状态语义:

- `ok`:来源成功返回并产出内容。
- `empty`:来源成功访问,但本轮没有符合条件的新内容;不等同于故障。
- `skipped`:缺少可选 token 或配置,主动跳过。
- `error`:网络、接口结构或上游返回异常;HTTP 200 但返回拦截页、HTML 或异常 XML 也会标记为失败。

Gitee 是尽力而为来源,空结果或部分关键词失败不会阻断日报。

选题排序优先级默认按一手程度处理:OpenAI / Anthropic / Google DeepMind 官网优先,其次是产品、模型、开源和国际社区信号;国内媒体和国内开发者社区作为补充观察源。

## 功能模块总览

| 模块 | 能力 | 默认 |
Expand All @@ -95,6 +107,7 @@ pnpm digest
| 选题评分 | 商业影响 40、热度 30、新鲜度 20、可写性 10 | 是 |
| 内容分类 | 政策监管、模型突破、AI 产品、行业落地、标杆企业与商业格局 | 是 |
| 中文科技社区报告 | `ai-china-tech.md`(36kr + InfoQ + Gitee + OSChina + 掘金) | 是 |
| 搜索索引 | `digests/search-index.json`,从每日 `topic-pool.json` 生成 | 是 |
| 源级报告 | `ai-web.md`、`ai-hn.md`、`ai-arxiv.md` 等 | 默认关闭 |
| 英文报告 | `*-en.md` | 默认关闭 |
| 历史 Web UI | `index.html` + `manifest.json` | 是 |
Expand Down Expand Up @@ -122,6 +135,19 @@ pnpm digest
- 企业落地与行业应用
- 标杆企业动向、商业格局与投融资

选题池还有几条约束:

- `topic-pool.json` 的主数据字段是 `candidates`;历史版本的 `topics` 会继续被搜索索引兼容读取。
- 候选池会按 URL 去重。
- 官网一手内容会获得更高排序权重;HTTP 200 但结构异常的官网响应仍会标记为失败。
- GitHub + Gitee 仓库类候选最多保留 16 条,避免开源仓库长期挤满 60 个候选位。
- Gitee 单源最多 3 条。
- GitHub 仓库热度使用对数计分,高 star 仍有优势,但不会仅凭总 star 挤掉其他来源。
- 国内源拆分为国内媒体(InfoQ 中国、36kr)和国内开发者社区(开源中国、掘金);国内源合计最多 6 条。
- 掘金单源最多 2 条,定位为补充社区观察,不作为 Top 深挖主来源。
- Dev.to 和 Lobsters 社区内容会进入选题池,不只生成社区报告。
- 50-64 分是"观察项"。如果当天没有观察项,报告会显示说明文案;这不代表采集失败。

## 三种使用方式

### 方式一:本地使用
Expand Down Expand Up @@ -163,14 +189,14 @@ pnpm digest
| `pnpm serve` | 本地查看历史 Web UI |
| `pnpm weekly` | 生成周报 |
| `pnpm monthly` | 生成月报 |
| `pnpm test` | 单元测试(224 个) |
| `pnpm test` | 单元测试(274 个) |
| `pnpm typecheck` | TypeScript 类型检查 |

## 验证状态

- TypeScript typecheck: 通过
- ESLint: 通过
- 单元测试: 224/224 通过
- 单元测试: 274/274 通过

## 作品集边界

Expand Down
31 changes: 31 additions & 0 deletions docs/configuration.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,37 @@ GitHub Actions Secret 名称:
PRODUCTHUNT_TOKEN
```

## 数据源状态与选题规则

日报不会因为单个公开来源失败而中断。主报告里的"数据源状态与修复提示"使用以下语义:

| 状态 | 含义 |
| --- | --- |
| `ok` | 成功获取并产出内容 |
| `empty` | 成功访问,但本轮没有符合条件的新内容 |
| `skipped` | 缺少可选 token 或配置,主动跳过 |
| `error` | 网络、接口结构或上游返回异常 |

注意:

- `empty` 不是故障。官方内容源、Gitee 等来源在首次运行后没有新增内容是正常情况。
- ArXiv、36kr、OSChina 和官网 sitemap 会校验最小 XML 结构;HTTP 200 但返回 HTML 拦截页或异常 XML 时会标记为 `error`。
- Gitee 是尽力而为来源;空结果、部分关键词失败或接口短暂不可用不会阻断日报。

选题池规则:

- 每日结构化选题池写入 `digests/YYYY-MM-DD/topic-pool.json`,主字段为 `candidates`。
- 搜索索引 `digests/search-index.json` 由 `pnpm manifest` 生成,优先读取 `candidates`,并兼容旧版 `topics`。
- 候选会按 URL 去重。
- OpenAI / Anthropic / Google DeepMind 官网一手内容拥有最高排序优先级。
- GitHub + Gitee 仓库类候选最多保留 16 条,避免仓库信号长期挤满候选池。
- Gitee 单源最多 3 条。
- GitHub 仓库热度使用对数计分,高 star 项目仍可入池,但不会仅凭总 star 压过其他来源。
- 国内源拆分为国内媒体(InfoQ 中国、36kr)和国内开发者社区(开源中国、掘金);国内源合计最多 6 条。
- 掘金单源最多 2 条,定位为补充社区观察,不作为 Top 深挖主来源。
- Dev.to 和 Lobsters 社区内容会进入选题池,同时也可用于社区报告。
- 50-64 分是"观察项";如果观察项为空,报告会显示解释文案,这不代表采集失败。

## Telegram

需要两个值:
Expand Down
16 changes: 10 additions & 6 deletions docs/getting-started.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pnpm digest
- 终端最后看到 `Done!`
- 出现 `digests/YYYY-MM-DD/ai-topic-radar.html`
- 出现 `manifest.json`
- 出现 `digests/search-index.json`
- 出现 `feed.xml`

打开报告:
Expand All @@ -70,6 +71,7 @@ digests/YYYY-MM-DD/ai-topic-radar.html
- `Missing DEEPSEEK_API_KEY`:当前终端没有成功 export key,重新执行上面的 `read -r -s` 命令。
- GitHub API 403:说明匿名 GitHub API 限流。可以先忽略,也可以配置 `GITHUB_TOKEN`。
- Product Hunt skipped:没有配置 `PRODUCTHUNT_TOKEN`,不影响主报告。
- 数据源显示 `empty`:表示来源成功访问但本轮没有新内容,不等同于失败。

## 第 2 步:启用 GitHub Actions 自动日报

Expand Down Expand Up @@ -105,6 +107,7 @@ Actions -> Daily AI Topic Radar -> Run workflow
- workflow 绿色通过
- 仓库出现新的 `digests/YYYY-MM-DD/`
- `manifest.json` 和 `feed.xml` 被更新
- `digests/search-index.json` 被更新,并从每日 `topic-pool.json` 的 `candidates` 生成历史搜索数据
- 如果配置了通知 token,Telegram / 飞书收到消息

常见失败:
Expand All @@ -124,9 +127,7 @@ Settings -> Pages
推荐配置:

```text
Source: Deploy from a branch
Branch: main
Folder: / (root)
Source: GitHub Actions
```

访问历史 Web UI:
Expand Down Expand Up @@ -157,6 +158,8 @@ https://conradgui.github.io/AI-TREND-RADAR/feed.xml

- 404:Pages 还没部署完成,等 1-3 分钟;或仓库路径大小写写错。
- Web UI 空白:确认 `manifest.json` 已提交,且 `digests/YYYY-MM-DD/ai-topic-radar.md` 存在。
- 搜索没有结果:确认 `digests/search-index.json` 已提交,且对应日期的 `topic-pool.json` 里有 `candidates`。
- 观察项暂无条目:表示当天没有 50-64 分候选;不代表数据源采集失败。
- 通知里的链接打不开:确认 workflow 的 `PAGES_URL` 使用 `https://conradgui.github.io/AI-TREND-RADAR`。

## 第 4 步:配置 Telegram 和飞书通知
Expand Down Expand Up @@ -234,9 +237,10 @@ REPORT_LANGS=zh,en SAVE_SOURCE_REPORTS=1 pnpm digest
3. `pnpm manifest`
4. 打开 `digests/YYYY-MM-DD/ai-topic-radar.html`
5. 检查 `manifest.json`
6. 检查 `feed.xml`
7. 检查 GitHub Actions Secrets
8. 检查 GitHub Pages URL 大小写
6. 检查 `digests/search-index.json`
7. 检查 `feed.xml`
8. 检查 GitHub Actions Secrets
9. 检查 GitHub Pages URL 大小写

## 安全提醒

Expand Down
131 changes: 130 additions & 1 deletion src/__tests__/topic-radar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,137 @@ describe("buildTopicRadar", () => {
const repoCount = candidates.filter(
(candidate) => candidate.source.startsWith("GitHub Search") || candidate.source === "Gitee",
).length;
const giteeCount = candidates.filter((candidate) => candidate.source === "Gitee").length;

expect(repoCount).toBeLessThanOrEqual(20);
expect(repoCount).toBeLessThanOrEqual(16);
expect(giteeCount).toBeLessThanOrEqual(3);
});

it("prioritizes fresh official sources over high-heat Juejin articles", () => {
const input = baseInput();
input.webResults = [
{
site: "openai",
siteName: "OpenAI",
isFirstRun: false,
totalDiscovered: 3,
status: status("web-openai", "ok"),
newItems: Array.from({ length: 3 }, (_, index) => ({
url: `https://openai.com/index/official-${index}`,
title: `OpenAI enterprise product launch ${index}`,
lastmod: "2026-05-20",
content:
"OpenAI launches a new enterprise AI product with pricing, customer rollout, and platform availability.",
site: "openai",
category: "product",
})),
},
];
input.chinaSourcesData = {
kr36: { articles: [], fetchSuccess: true, status: status("kr36") },
infoqCn: { articles: [], fetchSuccess: true, status: status("infoq-cn") },
gitee: { projects: [], fetchSuccess: true, status: status("gitee") },
oschina: { news: [], fetchSuccess: true, status: status("oschina") },
juejin: {
articles: Array.from({ length: 8 }, (_, index) => ({
id: `juejin-${index}`,
title: `掘金 AI 编程爆款文章 ${index}`,
url: `https://juejin.cn/post/${index}`,
brief: "AI 编程、企业用户和产品体验相关的高热度社区文章,适合观察但不是一手官方信号。",
author: "掘金作者",
diggCount: 10_000 - index,
viewCount: 100_000 - index,
tags: ["AI编程", "前端"],
publishTime: "2026-05-20T00:00:00Z",
})),
fetchSuccess: true,
status: status("juejin"),
},
};

const candidates = buildTopicRadar(input).candidates;
const officialScores = candidates
.filter((candidate) => candidate.source === "OpenAI")
.map((candidate) => candidate.score);
const juejinScores = candidates
.filter((candidate) => candidate.source === "掘金")
.map((candidate) => candidate.score);

expect(candidates.slice(0, 3).every((candidate) => candidate.source === "OpenAI")).toBe(true);
expect(Math.min(...officialScores)).toBeGreaterThan(Math.max(...juejinScores));
});

it("caps domestic media and domestic community candidates separately", () => {
const input = baseInput();
input.chinaSourcesData = {
kr36: {
articles: Array.from({ length: 6 }, (_, index) => ({
id: `kr36-${index}`,
title: `36kr AI 企业落地新闻 ${index}`,
url: `https://36kr.com/p/${index}`,
summary: "AI 企业落地、商业化和用户增长相关报道,来自国内媒体。",
publishedAt: "2026-05-20T00:00:00Z",
author: "36kr",
})),
fetchSuccess: true,
status: status("kr36"),
},
infoqCn: {
articles: Array.from({ length: 6 }, (_, index) => ({
id: `infoq-${index}`,
title: `InfoQ 中国 AI 架构实践 ${index}`,
url: `https://www.infoq.cn/article/${index}`,
summary: "企业 AI 架构、模型应用和工程落地经验。",
author: "InfoQ",
publishTime: "2026-05-20T00:00:00Z",
topics: ["AI", "架构"],
})),
fetchSuccess: true,
status: status("infoq-cn"),
},
gitee: { projects: [], fetchSuccess: true, status: status("gitee") },
oschina: {
news: Array.from({ length: 6 }, (_, index) => ({
id: `oschina-${index}`,
title: `开源中国 AI 产品动态 ${index}`,
url: `https://www.oschina.net/news/${index}`,
body: "AI 产品、用户入口和开发者生态相关内容。",
author: "开源中国",
pubDate: "2026-05-20T00:00:00Z",
})),
fetchSuccess: true,
status: status("oschina"),
},
juejin: {
articles: Array.from({ length: 8 }, (_, index) => ({
id: `juejin-${index}`,
title: `掘金 AI 编程文章 ${index}`,
url: `https://juejin.cn/post/${index}`,
brief: "AI 编程、前端开发和开发者工具相关社区内容。",
author: "掘金作者",
diggCount: 8_000 - index,
viewCount: 80_000 - index,
tags: ["AI编程", "前端"],
publishTime: "2026-05-20T00:00:00Z",
})),
fetchSuccess: true,
status: status("juejin"),
},
};

const candidates = buildTopicRadar(input).candidates;
const domesticMediaCount = candidates.filter((candidate) =>
["36kr", "InfoQ 中国"].includes(candidate.source),
).length;
const domesticCommunityCount = candidates.filter((candidate) =>
["开源中国", "掘金"].includes(candidate.source),
).length;
const juejinCount = candidates.filter((candidate) => candidate.source === "掘金").length;

expect(domesticMediaCount + domesticCommunityCount).toBeLessThanOrEqual(6);
expect(domesticMediaCount).toBeLessThanOrEqual(4);
expect(domesticCommunityCount).toBeLessThanOrEqual(2);
expect(juejinCount).toBeLessThanOrEqual(2);
});

it("keeps source warnings without blocking topic pool generation", () => {
Expand Down
Loading
Loading