greenido/techmeme-2-slack:用 Gemini 抓取 Techmeme 并推送 Slack/Telegram 摘要
greenido/techmeme-2-slack
greenido/techmeme-2-slack 是一个用 Gemini 抓取 Techmeme 首页并生成每日摘要、推送到 Slack 和 Telegram 的开源工具。
How it works
- Scrape Techmeme's ranked homepage: one item per story cluster, with an excerpt and how many related articles cover it.
- Skip stories sent in the last 14 days (
data/sent-stories.json), and post at most one digest per day. - Pick and summarize with Gemini, which returns JSON: the headline number, a category, an emoji, a title and a summary.
- Validate: every link is taken from the scraped page (never from the model), and stories with numbers that don't appear in their headline are dropped.
- Post, grouped by category, as a Slack Block Kit message and Telegram HTML (split into parts if needed), then record the stories in the history file.
Setup
Requires Node.js 22+, a Gemini API key, and at least one delivery target.
git clone <your-repo-url> cd techmeme-cron npm install cp .env.example .env # then fill in your values
GEMINI_API_KEY=your_gemini_api_key_here GEMINI_MODEL=gemini-flash-latest # optional GEMINI_FALLBACK_MODEL=gemini-flash-lite-latest # optional; "none" disables # Configure at least one delivery target SLACK_BOT_TOKEN=xoxb-your-slack-bot-token SLACK_CHANNEL_ID=C1234567890 TELEGRAM_BOT_TOKEN=1234567890:your-telegram-bot-token TELEGRAM_CHAT_ID=@your_channel_username # or -1001234567890 for a private channel
Slack: bot token and channel ID
- Create an app at api.slack.com/apps, add the
chat:writebot scope, install it, and copy the Bot User OAuth Token (xoxb-…). - Invite the bot to the channel (
/invite @your-bot). - Right-click the channel → Copy link. The ID is the last part:
https://yourworkspace.slack.com/archives/C1234567890.
Telegram: bot, channel and chat ID
-
In @BotFather, run
/newbotand copy the token intoTELEGRAM_BOT_TOKEN. -
Create a channel, then add the bot under Administrators with permission to Post Messages.
-
Set
TELEGRAM_CHAT_IDto the channel, not the bot:- Public channel: its username, e.g.
@techmeme_digest. - Private channel: post a message in the channel, then run
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates"and copychannel_post.chat.id(it starts with-100).
- Public channel: its username, e.g.
-
Check that the bot can post:
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \ -H "Content-Type: application/json" \ -d '{"chat_id": "'"$TELEGRAM_CHAT_ID"'", "text": "Techmeme digest bot test message"}'
If both Slack and Telegram are configured, each run posts to both.
Usage
npm run start:dry-run # full run, prints the Slack and Telegram messages instead of posting npm start # post the digest (skipped if one was already sent today) npm start -- --force # post even if a digest was already sent today npm run start:telegram # post to Telegram only npm test # unit tests (no network, no credentials) npm run test:telegram # send a real test message to TELEGRAM_CHAT_ID node list-models.js # list the Gemini models your key can use
Deploying with GitHub Actions
daily-digest.yml runs daily at 10:00 UTC. GitHub often starts scheduled runs hours late, so the early time aims for delivery during the US morning. To run it manually, use Actions → Daily Tech Digest → Run workflow; tick force to post again on a day that already has a digest.
Add these under Settings → Secrets and variables → Actions:
| Secret | |
|---|---|
GEMINI_API_KEY |
Required |
SLACK_BOT_TOKEN, SLACK_CHANNEL_ID |
For Slack delivery |
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
For Telegram delivery |
GEMINI_MODEL, GEMINI_FALLBACK_MODEL |
Optional model overrides |
ALERT_TELEGRAM_CHAT_ID |
Optional. A separate chat that is messaged when a run fails |
Each run:
- runs the tests, then posts the digest
- shows the digest on the run's summary page
- commits the updated
data/sent-stories.json(one small bot commit per day). The daily commit also keeps the repository active, so GitHub doesn't disable the schedule after 60 days without commits.
ci.yml runs a syntax check, the tests and npm audit on Node 22 and 24 for every pull request and push to main. Dependabot opens monthly update PRs for npm packages and GitHub Actions.
Running from a local cron job instead
0 8 * * * cd /path/to/techmeme-cron && /path/to/node index.js >> cron.log 2>&1
Cron doesn't inherit your shell's PATH, so use the absolute path from which node. cron.log is ignored by git. If GitHub Actions is also posting, run only one of them, or set HISTORY_FILE to a path outside the repo for local runs.
Configuration
Models
| Model | |
|---|---|
gemini-flash-lite-latest |
Cheapest; the default fallback |
gemini-flash-latest |
Default: fast and balanced |
gemini-pro-latest |
Highest quality, slower |
The model must support structured JSON output. If the primary model fails after retries, or returns output that can't be used, GEMINI_FALLBACK_MODEL is tried once.
Story history
data/sent-stories.json(path set byHISTORY_FILE) keeps 14 days of delivered stories.- Repeat URLs are dropped before Gemini sees them.
- The last 3 days of titles are listed in the prompt as already covered.
- A run that finds a digest already sent today exits without posting, unless
--forceis passed. - Dry runs skip the same-day guard and never write the file.
Reliability
- Techmeme and Telegram: requests time out after 15s and are retried up to 3 times on network errors, 429s and 5xx responses. Telegram's
retry_afteris honored. - Gemini: each attempt times out after 90s, with up to 3 attempts per model.
- Slack: uses the SDK's five-retries-in-five-minutes policy.
- Delivery: each target is attempted even if another fails, and the run exits non-zero if any target failed.
Prompt and formatting
- Prompt:
buildPrompt()in digest.js sets the selection criteria. - Response shape:
DIGEST_SCHEMAin the same file. - Categories:
CATEGORIESin the same file. - Formatting: slack.js, telegram.js and markdown.js, so the prompt never needs to know about markup.
Troubleshooting
- "Invalid environment configuration":
GEMINI_API_KEYis missing, or a delivery target is only half configured. Each target needs both of its variables. - Telegram "chat not found" / "forbidden": the bot isn't an admin of the channel, or
TELEGRAM_CHAT_IDis the bot's username rather than the channel's. - "No items fetched from Techmeme": the page markup may have changed. The scraper falls back to plain
.iiitems. Runnpm testand compare the fixture with the live page. - "Dropped … numbers not in the headline": Gemini restated a figure (e.g. "1,300M" for "$1.3B"). The story is skipped, so that day's digest has fewer than 10 stories.
- Nothing posted, "already delivered today": the same-day guard. Use
--force, or force on a manual workflow run. - Scheduled runs stopped: check that the workflow is enabled in the Actions tab and that the secrets are set.
License
MIT
来源:GitHub · github.com