Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.dev.vars
.env
.wrangler/
dist/
*.log
118 changes: 97 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,105 @@
# AI-Radar

AI-Radar is a zero-cost-first, Cloudflare-native workflow for discovering AI coding-plan, API, token, free-credit, limited-time offer, and price-change information.
AI-Radar is a zero-cost-first, Cloudflare-native workflow for discovering AI model launches, API availability, Coding Plan, Token, free-credit, limited-time offer, and price-change information.

## Product direction

- Cloud-first: runs on Cloudflare Workers without a local always-on machine.
- Zero-cost-first: the default architecture must stay within free tiers; paid APIs are not required.
- Low-token monitoring: HTTP/RSS/GitHub/page-change detection runs continuously; AI is only used after a meaningful change is found.
- Telegram-first delivery: P1 items are pushed immediately; P2/P3 are summarized into a daily report at 12:30 Asia/Shanghai.
- Public-ready: the public web view is separated from the admin/control plane from the beginning.

## MVP pipeline
## Discovery Radar v2 architecture

```text
Sources
-> collectors (RSS / Web / GitHub / future X adapter)
-> change detection + deduplication
-> deterministic rules
-> optional Workers AI enrichment
-> D1
-> P1 Telegram push
-> P2/P3 daily report
-> public web dashboard
Known sources (RSS / Web / GitHub)
├──────────────┐
│ │
↓ ↓
normal monitoring discovery feeds
(OpenRouter / Hugging Face /
Artificial Analysis)
│ │
└──────┬───────┘
change detection + source-entry dedup
deterministic event classifier
optional Workers AI enrichment
Cloudflare D1
↙ ↓ ↘
P1 Telegram dynamic source pool P2/P3 daily
immediate temporary → candidate 12:30 Asia/Shanghai
→ core
```

## Status
The discovery layer is designed to find previously unknown models/vendors instead of relying only on a manually maintained seed list. A newly discovered model page can be added automatically as a temporary source for 30 days. If it produces meaningful follow-up signals it is promoted to candidate, then to core after repeated useful hits. The active temporary/candidate pool is capped to avoid uncontrolled source growth.

## Event types

AI-Radar now treats model intelligence as first-class data rather than requiring a discount or free-credit signal:

- `new_model`
- `model_api_available`
- `model_open_source`
- `model_benchmark`
- `free_credit`
- `limited_offer`
- `price_drop`
- `price_change`
- `new_plan`

A clear new-model event has a deterministic P2 floor. New models with API availability, coding/agent relevance, or other high-value signals can be promoted to P1.

## Zero-cost rule

The default system does not require paid APIs or paid infrastructure. Discovery uses public web/API endpoints. Workers AI is enabled only after change detection, source-entry/global deduplication, and deterministic filtering. It is hard-capped at 50 calls per UTC day with at most 256 output tokens per call; quota exhaustion or any AI error falls back to the deterministic result.

## Current collectors

- RSS / Atom
- Ordinary HTML pages with conditional requests and content hashing
- GitHub repositories through public Atom feeds
- OpenRouter public model catalog discovery
- Hugging Face text-generation discovery set
- Artificial Analysis model-catalog discovery
- X adapter reserved; the zero-cost discovery layer is the current fallback for X-only announcements

## Source tiers

- `core`: stable long-term monitored source
- `discovery`: aggregator/catalog used to find unknown models and vendors
- `temporary`: auto-added page with a 30-day observation window
- `candidate`: temporary source that produced a meaningful update; observation window extends to 90 days
- candidate sources become `core` after three meaningful hits

## Cloudflare bindings

The Worker expects `DB` (D1), `AI` (Workers AI), and `ASSETS` (Workers Static Assets). Secrets: `ADMIN_TOKEN`, the dedicated AI-Radar `TELEGRAM_BOT_TOKEN`, and the dedicated group `TELEGRAM_CHAT_ID`. AI-Radar has no Telegram webhook or inbound command handler: the bot is outbound-only. Non-secret safety controls include `AI_DAILY_CALL_LIMIT` and `SOURCE_BATCH_SIZE`. Never commit secret values.

## Initial setup / upgrade

1. Create D1 database `ai-radar-db` if this is a fresh install.
2. Replace `REPLACE_WITH_D1_DATABASE_ID` in `wrangler.jsonc` if needed.
3. `npm install`
4. `npm run db:migrate:remote`
5. Configure secrets with `wrangler secret put ...`.
6. `npm run check`
7. `npm run deploy`

Migration `0006_discovery_radar.sql` adds source lifecycle fields and seeds OpenRouter, Hugging Face, Artificial Analysis, plus Multiverse Computing's official resources page.

Cloudflare Cron runs in UTC. `30 4 * * *` equals 12:30 Asia/Shanghai. The report window is the preceding 24 hours ending at 12:30 Beijing time.

## API

Public: `GET /api/health`, `GET /api/items`, `GET /daily/YYYY-MM-DD`, `GET /latest`.

Admin (Bearer `ADMIN_TOKEN`): `GET/POST /api/admin/sources`, `POST /api/admin/run-harvest`, `POST /api/admin/run-daily`.

## Delivery policy

- P1: immediate Telegram push.
- P2/P3: daily report at 12:30 Asia/Shanghai.
- First fetch establishes a baseline and does not flood Telegram.
- A new model does not get discarded merely because no discount/free-credit information is present.

## Branch status

Initial implementation in progress.
Discovery Radar v2 is implemented on `feature/discovery-radar-v2`, based on `ai-radar-mvp`. `main` is unchanged.
39 changes: 39 additions & 0 deletions docs/free-tier-capacity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# AI-Radar 免费层容量评估(2026-08-31)

## 官方上限

- Workers Free:100,000 个动态请求/日;每次 HTTP/Cron 调用 10 ms CPU;每次调用 50 个外部子请求;每账号最多 5 个 Cron Trigger。静态资源请求免费且不限量。[1][2]
- D1 Free:5,000,000 rows read/日、100,000 rows written/日、5 GB 账号总存储;单库 500 MB;单次 Worker 调用最多 50 条 D1 查询。[3][4]
- Workers AI:每天 10,000 Neurons 免费额度;AI-Radar 另设 50 次/UTC 日、每次最多 256 输出 token 的应用层硬上限。[5]
- 当前静态页面采用 Workers Static Assets 的 `assets.directory` + `ASSETS` binding + `run_worker_first`,符合官方当前全栈 Worker 推荐方式。[6]
- `ADMIN_TOKEN`、AI-Radar 专用 Telegram Bot Token 与独立群 Chat ID 只使用加密 Secret;不使用 Topic,且不与 Hermes 共享 Bot 凭据。官方推荐 `wrangler secret put`,Secret 值设置后不在 Wrangler 或 Dashboard 回显。[7]

## 估算假设

- 每个信源平均每 60 分钟抓取一次;Cron 每 5 分钟运行,另有 1 次日报 Cron,即 289 次 Worker 调用/日。
- 外部抓取 `fetch()` 是子请求,不计为入站 Worker 请求;每轮最多处理 10 个源,低于 50 子请求上限。
- D1 保守按每次源抓取 3 rows read、4 rows written 估算;另加 1,000 次/日公开 API 访问,每次最多读取 100 条。
- Workers AI 最坏按 50 次/日、约 4,300 Neurons/日估算,仍低于 10,000 免费额。
- 页面 HTML/CSS/JS 由 Static Assets 直接提供,不计动态请求;`/api/items`、`/latest`、`/daily/*` 才计动态请求。

## 结果

| 信源数 | 源抓取/日 | Worker 基础调用/日 | D1 read/日(含 1,000 次 API 浏览) | D1 write/日 | AI 上限 | 结论 |
|---:|---:|---:|---:|---:|---:|---|
| 20 | 480 | 289 + 动态页面/API 访问 | 101,729 | 2,020 | 4,300 Neurons | **SAFE** |
| 50 | 1,200 | 289 + 动态页面/API 访问 | 103,889 | 4,900 | 4,300 Neurons | **SAFE** |
| 100 | 2,400 | 289 + 动态页面/API 访问 | 107,489 | 9,700 | 4,300 Neurons | **WARNING** |

100 个源的 D1 与请求额度仍远低于免费上限,但平均每个 5 分钟周期会有约 8.3 个到期源,接近当前 `SOURCE_BATCH_SIZE=10`,并且复杂 HTML 的解析可能触碰 Free Cron 10 ms CPU 限制。因此 100 源不作为默认配置;必须先按源类型拆分频率、监控 `exceededCpu`,再决定是否扩容。

当前 9 个源按实际 60/120/180 分钟混合频率约 160 次抓取/日,明显低于 20 源模型,属于 **SAFE**。默认配置不会自动开通付费服务;额度耗尽时应失败或降级,而不是产生额外调用。

## Sources

[1] https://developers.cloudflare.com/workers/platform/pricing
[2] https://developers.cloudflare.com/workers/platform/limits
[3] https://developers.cloudflare.com/d1/platform/pricing
[4] https://developers.cloudflare.com/d1/platform/limits
[5] https://developers.cloudflare.com/workers-ai/platform/pricing
[6] https://developers.cloudflare.com/workers/static-assets
[7] https://developers.cloudflare.com/workers/configuration/secrets
37 changes: 37 additions & 0 deletions docs/x-zero-cost-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# X 零成本抓取候选方案(2026-08-31)

## 结论

本阶段不正式上线 X Collector。2026 年官方 X API 已转为按资源计费:读取 Post 为每条 $0.005,需购买 credits;“免费 xAI credits”是购买 X API credits 后的返利,不是免费读取额度。[8] 官方 changelog 仅说明经批准的 Public Utility Apps 可继续免费规模化访问、近期 Legacy Free 用户有一次性 $10 voucher,这两类都不是 AI-Radar 可长期默认依赖的公开免费层。[9]

因此继续保留 `XCollector` 接口,首选未来出现的官方免费 read-only 计划;当前不购买 X API、不使用付费代理、不部署自动登录脚本。

## 候选矩阵

| 方案 | 免费 | 需要登录 | 稳定性 | Cloudflare 可直接运行 | 主要风险 | 推荐级别 |
|---|---|---|---|---|---|---|
| 官方 X API Pay-per-use | 否 | 开发者账号 | 高 | 是 | 明确按读取资源收费;违反长期 0 元目标 | **不采用** |
| Public Utility Apps 特批 | 可能 | 需要申请与审核 | 高 | 是 | 非通用免费层,资格不确定 | **C:仅在官方批准后重评** |
| RSSHub 自托管 Twitter 路由 | 软件免费 | 通常需要 X Cookie/账号 | 低至中 | 不能直接作为纯 Worker 路由运行 | Cookie 失效、接口字段轮换、账号风控;2026 仍有时间线空结果问题。[10] | **D** |
| Nitter/公共实例 RSS | 表面免费 | 否 | 低 | 技术上可抓公开实例 | 上游 guest access、实例存活、法律与封禁风险;不能作为长期 SLA。[11] | **D** |
| X Syndication 单 Tweet endpoint | 免费且免登录 | 否 | 中 | 是 | 只能在已知 Tweet ID 后读取,不能发现新内容 | **C:只作详情补全** |
| 搜索引擎 `site:x.com` 线索 | 取决于搜索服务免费额度 | 否 | 低至中 | 取决于供应商 | 索引延迟、漏检、搜索 API 配额,不是完整时间线 | **C:只作发现补充** |
| OpenCLI/浏览器登录态 | 工具本身免费 | 是 | 中 | 否(依赖本机浏览器会话) | 电脑关闭即停止,不符合脱离本机要求 | **不用于生产** |
| 用户手工提交 X 链接 | 免费 | Worker 不需要登录 | 高 | 是 | 不是自动发现源 | **B:可作为补充入口** |

## 后续触发条件

仅在以下任一条件满足时重新评估并实现:

1. X 官方提供明确、可长期使用的免费只读/search 配额;
2. 获得 Public Utility Apps 免费资格且条款允许该监控场景;
3. 出现无需账户 Cookie、可在 Cloudflare 运行、连续验证至少 30 天的合法稳定公共源。

在此之前,AI-Radar 的 RSS/Web/GitHub 主链独立运行,X 不影响上线与日报。

## Sources

[8] https://docs.x.com/x-api/getting-started/pricing
[9] https://docs.x.com/changelog
[10] https://github.com/DIYgod/RSSHub/issues/22938
[11] https://github.com/zedeus/nitter
91 changes: 91 additions & 0 deletions migrations/0001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('rss','web','github','x')),
trust_level TEXT NOT NULL DEFAULT 'C' CHECK(trust_level IN ('A','B','C','D')),
enabled INTEGER NOT NULL DEFAULT 1,
interval_minutes INTEGER NOT NULL DEFAULT 15,
config_json TEXT,
etag TEXT,
last_modified TEXT,
content_hash TEXT,
next_fetch_at TEXT,
last_fetch_at TEXT,
last_success_at TEXT,
failure_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'new',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_sources_due ON sources(enabled, next_fetch_at);

CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER NOT NULL,
external_id TEXT,
fingerprint TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
summary TEXT,
url TEXT,
published_at TEXT,
discovered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
kind TEXT NOT NULL DEFAULT 'other',
priority TEXT NOT NULL DEFAULT 'P3' CHECK(priority IN ('P1','P2','P3')),
score INTEGER NOT NULL DEFAULT 0,
source_confidence TEXT NOT NULL DEFAULT 'low',
verification_status TEXT NOT NULL DEFAULT 'unverified',
vendor TEXT,
product TEXT,
previous_price REAL,
current_price REAL,
currency TEXT,
expires_at TEXT,
raw_excerpt TEXT,
ai_enriched INTEGER NOT NULL DEFAULT 0,
pushed_at TEXT,
daily_report_date TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_items_discovered ON items(discovered_at DESC);
CREATE INDEX IF NOT EXISTS idx_items_priority ON items(priority, discovered_at DESC);
CREATE INDEX IF NOT EXISTS idx_items_report ON items(daily_report_date, priority);

CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id INTEGER,
vendor TEXT,
product TEXT,
price REAL NOT NULL,
currency TEXT,
observed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(item_id) REFERENCES items(id) ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS daily_reports (
report_date TEXT PRIMARY KEY,
window_start TEXT NOT NULL,
window_end TEXT NOT NULL,
item_count INTEGER NOT NULL DEFAULT 0,
html TEXT NOT NULL,
generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
telegram_pushed_at TEXT
);

CREATE TABLE IF NOT EXISTS fetch_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER NOT NULL,
fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
status_code INTEGER,
changed INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER,
error TEXT,
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_fetch_logs_source ON fetch_logs(source_id, fetched_at DESC);
19 changes: 19 additions & 0 deletions migrations/0002_seed_sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'AI Coding Deals','https://github.com/codertesla/ai-coding-deals','github','B',10,
'{"githubOwner":"codertesla","githubRepo":"ai-coding-deals","githubMode":"commits","githubBranch":"main"}',CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://github.com/codertesla/ai-coding-deals');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'LLM Price Tracker','https://github.com/llerandi/llm-price-tracker','github','B',15,
'{"githubOwner":"llerandi","githubRepo":"llm-price-tracker","githubMode":"commits","githubBranch":"main"}',CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://github.com/llerandi/llm-price-tracker');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'Free LLM API Resources','https://github.com/cheahjs/free-llm-api-resources','github','B',15,
'{"githubOwner":"cheahjs","githubRepo":"free-llm-api-resources","githubMode":"commits","githubBranch":"main"}',CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://github.com/cheahjs/free-llm-api-resources');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'Coding Plan CN','https://github.com/xiaotiewinner/coding-plan','github','B',15,
'{"githubOwner":"xiaotiewinner","githubRepo":"coding-plan","githubMode":"commits","githubBranch":"main"}',CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://github.com/xiaotiewinner/coding-plan');
15 changes: 15 additions & 0 deletions migrations/0003_track_source_entries.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS source_entries (
source_id INTEGER NOT NULL,
external_id TEXT NOT NULL,
first_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (source_id, external_id),
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_source_entries_seen ON source_entries(first_seen_at);

UPDATE sources
SET enabled = 0,
status = 'disabled',
updated_at = CURRENT_TIMESTAMP
WHERE url = 'https://github.com/cheahjs/free-llm-api-resources';
33 changes: 33 additions & 0 deletions migrations/0004_production_sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
UPDATE sources
SET interval_minutes = 60,
updated_at = CURRENT_TIMESTAMP
WHERE url IN (
'https://github.com/codertesla/ai-coding-deals',
'https://github.com/llerandi/llm-price-tracker',
'https://github.com/xiaotiewinner/coding-plan'
);

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'DeepSeek API Pricing','https://api-docs.deepseek.com/quick_start/pricing/','web','A',120,NULL,CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://api-docs.deepseek.com/quick_start/pricing/');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'Zhipu BigModel Pricing','https://open.bigmodel.cn/pricing','web','A',120,NULL,CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://open.bigmodel.cn/pricing');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'MiniMax API Pricing','https://platform.minimaxi.com/docs/guides/pricing-paygo','web','A',180,NULL,CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://platform.minimaxi.com/docs/guides/pricing-paygo');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'Kimi API Pricing','https://platform.kimi.ai/docs/pricing/chat','web','A',180,NULL,CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://platform.kimi.ai/docs/pricing/chat');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'OpenCode Releases','https://github.com/opencode-ai/opencode','github','A',60,
'{"githubOwner":"opencode-ai","githubRepo":"opencode","githubMode":"releases","githubBranch":"main"}',CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://github.com/opencode-ai/opencode');

INSERT INTO sources(name,url,type,trust_level,interval_minutes,config_json,next_fetch_at)
SELECT 'HN AI Free Credits','https://hnrss.org/newest?q=AI%20API%20free%20credits','rss','C',60,NULL,CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM sources WHERE url='https://hnrss.org/newest?q=AI%20API%20free%20credits');
5 changes: 5 additions & 0 deletions migrations/0005_ai_daily_limit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS ai_daily_usage (
usage_date TEXT PRIMARY KEY,
calls INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Loading