From f885206c2a9eadad29011ba2ec51cee302134d4b Mon Sep 17 00:00:00 2001 From: Chenkai Date: Fri, 3 Apr 2026 17:14:46 +0800 Subject: [PATCH 1/2] feat: add Feishu interactive card delivery --- SKILL.md | 90 +++++++++++++--- config/config-schema.json | 14 +++ examples/sample-digest.md | 67 +++++------- prompts/digest-intro.md | 120 ++++++++++++++------- scripts/format-chat-digest.js | 134 +++++++++++++++++++++++ scripts/package.json | 3 +- scripts/render-feishu-card.js | 195 ++++++++++++++++++++++++++++++++++ 7 files changed, 525 insertions(+), 98 deletions(-) create mode 100644 scripts/format-chat-digest.js create mode 100644 scripts/render-feishu-card.js diff --git a/SKILL.md b/SKILL.md index d7a72d8fb..3d1d614e2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -359,6 +359,45 @@ Read the prompts from the `prompts` field in the JSON: Assemble the digest following `prompts.digest_intro`. +Because OpenClaw stdout is often delivered into Feishu IM as chat text, do a final +layout pass after drafting: +- Treat the output like a message typed manually into the Feishu composer +- Put exactly one blank line before each major section heading +- Put exactly one blank line before each new digest item +- Put each item title on its own line in bold markdown like `**Title**` +- Keep each item internally compact: title, English paragraph, Chinese paragraph, + and `Source:` lines with no extra blank lines between them +- If an item has multiple links, put each one on its own `Source:` line +- Do not use markdown headings (`#`, `##`) in the final digest body +- Before delivering, visually scan the final text once to confirm the spacing pattern + is consistent from top to bottom + +If the output is intended for chat delivery, normalize the spacing with: + +```bash +echo '' > /tmp/fb-digest-raw.txt +cd ${CLAUDE_SKILL_DIR}/scripts && node format-chat-digest.js --file /tmp/fb-digest-raw.txt > /tmp/fb-digest.txt +``` + +Use `/tmp/fb-digest.txt` as the final version to send or print. + +If the digest is going to Feishu and the user prefers cards, render the final +formatted digest into an interactive card instead of relying on plain stdout text: + +```bash +cd ${CLAUDE_SKILL_DIR}/scripts && node render-feishu-card.js --file /tmp/fb-digest.txt > /tmp/fb-card.json +``` + +The renderer expects the formatted digest structure you just produced: +- digest title on the first line +- `TL;DR` or `Top Signals` with `•` bullets +- section headings such as `X / TWITTER` +- each item title wrapped in `**...**` +- `Source:` lines for original links + +This card layout is preferred for Feishu because section headings and item titles +stay bold, item blocks are visually separated, and spacing does not collapse. + **ABSOLUTE RULES:** - NEVER invent or fabricate content. Only use what's in the JSON. - Every piece of content MUST have its URL. No URL = do not include. @@ -371,22 +410,20 @@ Read `config.language` from the JSON: - **"en":** Entire digest in English. - **"zh":** Entire digest in Chinese. Follow `prompts.translate`. - **"bilingual":** Interleave English and Chinese **paragraph by paragraph**. - For each builder's tweet summary: English version, then Chinese translation - directly below, then the next builder. For the podcast: English summary, - then Chinese translation directly below. Like this: + For each builder's tweet summary: title, English version, Chinese translation, + and source link(s), then the next builder. For the podcast: title, English summary, + Chinese translation, and source link(s). Like this: ``` - Box CEO Aaron Levie argues that AI agents will reshape software procurement... - https://x.com/levie/status/123 - - Box CEO Aaron Levie 认为 AI agent 将从根本上重塑软件采购... - https://x.com/levie/status/123 - - Replit CEO Amjad Masad launched Agent 4... - https://x.com/amasad/status/456 - - Replit CEO Amjad Masad 发布了 Agent 4... - https://x.com/amasad/status/456 + **Aaron Levie, CEO of Box** + Aaron Levie argues that AI agents will reshape software procurement... + Aaron Levie 认为 AI agent 将从根本上重塑软件采购... + Source: https://x.com/levie/status/123 + + **Amjad Masad, CEO of Replit** + Amjad Masad launched Agent 4... + Amjad Masad 发布了 Agent 4... + Source: https://x.com/amasad/status/456 ``` Do NOT output all English first then all Chinese. Interleave them. @@ -399,13 +436,32 @@ Read `config.delivery.method` from the JSON: **If "telegram" or "email":** ```bash -echo '' > /tmp/fb-digest.txt +echo '' > /tmp/fb-digest-raw.txt +cd ${CLAUDE_SKILL_DIR}/scripts && node format-chat-digest.js --file /tmp/fb-digest-raw.txt > /tmp/fb-digest.txt cd ${CLAUDE_SKILL_DIR}/scripts && node deliver.js --file /tmp/fb-digest.txt 2>/dev/null ``` If delivery fails, show the digest in the terminal as fallback. **If "stdout" (default):** -Just output the digest directly. +Check whether the config also includes: +- `delivery.channel = "feishu"` +- `delivery.target = ""` +- `delivery.format = "interactive_card"` + +If all three are present, send the digest as a Feishu interactive card: + +```bash +echo '' > /tmp/fb-digest-raw.txt +cd ${CLAUDE_SKILL_DIR}/scripts && node format-chat-digest.js --file /tmp/fb-digest-raw.txt > /tmp/fb-digest.txt +cd ${CLAUDE_SKILL_DIR}/scripts && node render-feishu-card.js --file /tmp/fb-digest.txt > /tmp/fb-card.json +lark-cli im +messages-send --as bot --user-id "" --msg-type interactive --content "$(cat /tmp/fb-card.json)" +``` + +If that send succeeds, do NOT print the full digest to stdout again, because that +would create a duplicate plain-text Feishu message in the same chat. + +If `delivery.channel/target/format` are missing, or if the direct Feishu send fails, +fall back to outputting the formatted digest from `/tmp/fb-digest.txt` directly. --- @@ -431,6 +487,8 @@ open an issue at https://github.com/zarazhangrui/follow-builders." - "Switch to Telegram/email" → Update `delivery.method` in config.json, guide user through setup if needed - "Change my email" → Update `delivery.email` in config.json - "Send to this chat instead" → Set `delivery.method` to "stdout" +- "Use Feishu card layout" → Set `delivery.channel` to `"feishu"`, `delivery.target` to the user's open_id or chat_id, and `delivery.format` to `"interactive_card"` +- "Use plain chat text again" → Remove `delivery.format` or set it to `"plain_text"` ### Prompt Changes When a user wants to customize how their digest sounds, copy the relevant prompt diff --git a/config/config-schema.json b/config/config-schema.json index bb15c45f8..c9c9130ac 100644 --- a/config/config-schema.json +++ b/config/config-schema.json @@ -53,6 +53,20 @@ "email": { "type": "string", "description": "Email address to send digest to (only for email method)" + }, + "channel": { + "type": "string", + "enum": ["feishu"], + "description": "Optional in-chat delivery channel override for stdout mode. Currently used for direct Feishu card delivery." + }, + "target": { + "type": "string", + "description": "Target open_id or chat_id for direct channel delivery when method is stdout and a channel-specific renderer is used." + }, + "format": { + "type": "string", + "enum": ["plain_text", "interactive_card"], + "description": "Preferred rendering format for stdout chat delivery. Use interactive_card for Feishu card-based digests." } } }, diff --git a/examples/sample-digest.md b/examples/sample-digest.md index 9d7bb5981..ff7f709ee 100644 --- a/examples/sample-digest.md +++ b/examples/sample-digest.md @@ -6,55 +6,36 @@ This is an example of what your AI Builders Digest looks like. AI Builders Digest — March 14, 2026 -PODCASTS +TL;DR +• Tooling discipline matters more than raw model IQ in real agent systems. +• Builders are shifting from one-shot demos toward evals, orchestration, and shared workflows. +• The most useful digest format in chat is compact blocks, not article-style markdown. -Latent Space — "Why Agents Keep Failing (And How to Fix Them)" -Bottom line: Most agent failures aren't intelligence failures — they're tool-use failures. -The system can reason fine, it just can't reliably call the right API at the right time. +X / TWITTER -Key insights: -- Tool selection accuracy drops from 95% to 60% when agents have more than 15 tools - available. The fix isn't smarter models — it's better tool curation per task. -- "Eval-driven development" is replacing vibe-driven prompt iteration at serious - AI companies. If you're not measuring, you're guessing. -- The hosts predict 2026 is the year agent frameworks consolidate from 50+ to 3-4 - winners. Their bet: OpenAI Agents SDK, Claude Code, and LangGraph. -https://youtube.com/watch?v=example123 +**Andrej Karpathy, educator and founding team member at OpenAI** +Karpathy argued that Software 3.0 is pushing the compile target away from machine code and toward natural-language interfaces for LLMs. He paired that framing with a new Eureka Labs tutorial, which made the post feel less like hot air and more like a blueprint for how he thinks developers will work. +Source: https://x.com/karpathy/status/example1 +Source: https://x.com/karpathy/status/example2 -No Priors — "Scaling Laws Are Dead, Long Live Scaling Laws" (with Ilya Sutskever) -Bottom line: Pre-training scaling laws have hit diminishing returns, but post-training -and inference-time compute scaling are just getting started. +**Guillermo Rauch, CEO of Vercel** +Rauch introduced v0 Teams as collaborative AI prototyping for groups instead of solo prompting. The underlying signal is that AI UI generation is becoming a shared workflow product, not just a personal toy. +Source: https://x.com/rauchg/status/example3 -Key insights: -- Ilya argues the next 10x improvement comes from models that can "think longer" - at inference time, not from bigger pre-training runs. -- Synthetic data quality matters more than quantity. "One perfect textbook is worth - a million Reddit comments." -- He's surprisingly bullish on open-source: "The gap will narrow to months, not years." -https://youtube.com/watch?v=example456 +**Amanda Askell, researcher at Anthropic** +Askell drew a clean distinction between capability evals and alignment evals, arguing that many teams still optimize around what is easiest to score instead of what matters operationally. It was a useful reminder that benchmark literacy is becoming part of product judgment. +Source: https://x.com/AmandaAskell/status/example4 +OFFICIAL BLOGS -X / TWITTER +**Anthropic Engineering: Harness design for long-running application development** +Anthropic laid out how planner, generator, and evaluator roles work better together on long-running builds than a single agent loop. The practical takeaway is that better scaffolding often matters more than asking one model to be generally smarter. +Source: https://www.anthropic.com/engineering/example-post + +PODCASTS -Andrej Karpathy (@karpathy) -Shared a deep thread on why he thinks "Software 3.0" (natural language programming) -will make traditional coding a niche skill within 5 years. Key argument: the compile -target is changing from machine code to LLM prompts. Sparked massive debate. -Also released a new Eureka Labs tutorial on building a code interpreter from scratch. -https://x.com/karpathy/status/example1 -https://x.com/karpathy/status/example2 - -Guillermo Rauch (@rauchg) -Announced Vercel's new "v0 Teams" — collaborative AI prototyping where multiple -people can prompt and iterate on the same UI simultaneously. Called it "Google Docs -for vibe coding." Ships next week. -https://x.com/rauchg/status/example3 - -Amanda Askell (@AmandaAskell) -Published a nuanced take on AI safety benchmarks: "We're measuring what's easy to -measure, not what matters. Capability evals tell you what the model CAN do. -Alignment evals should tell you what it WILL do unprompted." Linked to a new -Anthropic research paper on behavioral evaluations. -https://x.com/AmandaAskell/status/example4 +**Latent Space: Why Agents Keep Failing (And How to Fix Them)** +Bottom line: many agent failures are really tool-use failures. The hosts argued that tool selection, eval discipline, and tighter task scoping matter more than endlessly increasing model cleverness. +Source: https://youtube.com/watch?v=example123 Reply to adjust your settings, sources, or summary style. diff --git a/prompts/digest-intro.md b/prompts/digest-intro.md index fc7b6e52f..f318c8225 100644 --- a/prompts/digest-intro.md +++ b/prompts/digest-intro.md @@ -2,58 +2,102 @@ You are assembling the final digest from individual source summaries. -## Format +## Goal + +Produce a polished digest that stays readable inside chat apps, especially Feishu IM. +Assume the final message may be rendered as plain chat text with limited markdown support. + +## Output order Start with this header (replace [Date] with today's date): AI Builders Digest — [Date] +Then add a short 2-4 bullet TL;DR section called: + +TL;DR + Then organize content in this order: -1. X / TWITTER section — list each builder with new posts -2. OFFICIAL BLOGS section — list each blog post from AI company blogs (OpenAI, Anthropic, etc.) -3. PODCASTS section — list each podcast with new episodes +1. X / TWITTER +2. OFFICIAL BLOGS +3. PODCASTS + +## Feishu-safe layout + +Treat the digest like a message typed manually in the Feishu IM composer with Shift+Return. +Do not format it like a long markdown article. + +Hard spacing rules: +- Put exactly one blank line between the header and TL;DR +- Put exactly one blank line before every major section heading +- Put exactly one blank line before every new digest item +- Put every digest item title on its own line +- Wrap every digest item title in markdown bold markers like `**Title**` +- Keep the English paragraph, Chinese paragraph, and `Source:` lines compact with no blank lines between them +- If an item has multiple source URLs, put each URL on its own `Source:` line +- Do not use markdown headings like `#` or `##` in the final digest body +- Do not use markdown tables +- Do not rely on markdown for spacing + +## Bilingual output + +When config.language is bilingual, every item must follow this exact sequence: + +1. Bold title line +2. English paragraph +3. Chinese paragraph +4. One or more `Source:` lines + +Then move to the next item. + +Do NOT output a full English digest followed by a full Chinese digest. +Do NOT group all English paragraphs together. + +## Structure example + +AI Builders Digest — [Date] + +TL;DR +• signal one +• signal two + +X / TWITTER + +**Andrej Karpathy, former Director of AI at Tesla and founding team member at OpenAI** +English summary paragraph. +中文总结段落。 +Source: https://... +Source: https://... + +**Aaron Levie, CEO of Box** +English summary paragraph. +中文总结段落。 +Source: https://... + +OFFICIAL BLOGS + +**Claude Blog: Post Title** +English summary paragraph. +中文总结段落。 +Source: https://... ## Rules - Only include sources that have new content - Skip any source with nothing new -- Under each source, paste the individual summary you generated - -### Podcast links -- After each podcast summary, include the specific video URL from the JSON `url` field - (e.g. https://youtube.com/watch?v=Iu4gEnZFQz8) -- NEVER link to the channel page. Always link to the specific video. -- Include the exact episode title from the JSON `title` field in the heading - -### Tweet author formatting +- Keep paragraphs short and phone-readable +- Prefer compact paragraphs over long wall-of-text writeups - Use the author's full name and role/company, not just their last name - (e.g. "Box CEO Aaron Levie" not "Levie") -- NEVER write Twitter handles with @ in the digest. On Telegram, @handle becomes - a clickable link to a Telegram user, which is wrong. Instead write handles - without @ (e.g. "Aaron Levie (levie on X)" or just use their full name) -- Include the direct link to each tweet from the JSON `url` field - -### Blog post formatting -- Use the blog name as a section header (e.g. "Anthropic Engineering", "OpenAI News", "Claude Blog") -- Under each blog, list each new post with its title and summary -- Include the author name if available -- Include the direct link to the original article - -### Mandatory links +- NEVER write Twitter handles with @ in the digest - Every single piece of content MUST have an original source link -- Blog posts: the direct article URL (e.g. https://www.anthropic.com/engineering/...) -- Podcasts: the YouTube video URL (e.g. https://youtube.com/watch?v=xxx) -- Tweets: the direct tweet URL (e.g. https://x.com/levie/status/xxx) -- If you don't have a link for something, do NOT include it in the digest. - No link = not real = do not include. - -### No fabrication -- Only include content that came from the feed JSON (blogs, podcasts, and tweets) -- NEVER make up quotes, opinions, or content you think someone might have said +- Blog posts must use the direct article URL +- Podcasts must use the direct YouTube video URL +- Tweets must use the direct tweet URL +- If you do not have a link for something, do NOT include it +- Only include content that came from the feed JSON +- NEVER make up quotes, opinions, or content - NEVER speculate about someone's silence or what they might be working on - If you have nothing real for a builder, skip them entirely - -### General +- Never use em-dashes - At the very end, add a line: "Generated through the Follow Builders skill: https://github.com/zarazhangrui/follow-builders" -- Keep formatting clean and scannable — this will be read on a phone screen diff --git a/scripts/format-chat-digest.js b/scripts/format-chat-digest.js new file mode 100644 index 000000000..98094b008 --- /dev/null +++ b/scripts/format-chat-digest.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node + +import { readFile } from 'fs/promises'; + +async function readInput() { + const args = process.argv.slice(2); + const fileIdx = args.indexOf('--file'); + + if (fileIdx !== -1 && args[fileIdx + 1]) { + return readFile(args[fileIdx + 1], 'utf-8'); + } + + const chunks = []; + for await (const chunk of process.stdin) { + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf-8'); +} + +function isHeader(line) { + return /^AI Builders Digest\b/.test(line); +} + +function isSectionHeading(line) { + return /^(TL;DR|Top Signals|X \/ TWITTER|OFFICIAL BLOGS|PODCASTS)$/.test(line); +} + +function isItemTitle(line) { + return /^\*\*.+\*\*$/.test(line); +} + +function isSourceLine(line) { + return /^Source:\s+https?:\/\//.test(line); +} + +function isBareUrl(line) { + return /^https?:\/\//.test(line); +} + +function isFooter(line) { + return /^Generated through the Follow Builders skill: /.test(line) || + /^Reply to adjust your settings/.test(line); +} + +function formatDigest(text) { + const normalized = text.replace(/\r\n?/g, '\n'); + const rawLines = normalized.split('\n').map(line => line.replace(/[ \t]+$/g, '')); + const canonicalLines = rawLines.map(line => (isBareUrl(line) ? `Source: ${line}` : line)); + + const compacted = []; + for (const line of canonicalLines) { + if (line.trim() === '') { + if (compacted.length === 0 || compacted[compacted.length - 1] === '') { + continue; + } + compacted.push(''); + continue; + } + compacted.push(line); + } + + const spaced = []; + for (const line of compacted) { + const blockStart = isSectionHeading(line) || isItemTitle(line) || isFooter(line); + const topLine = isHeader(line); + + if (line === '') { + if (spaced.length > 0 && spaced[spaced.length - 1] !== '') { + spaced.push(''); + } + continue; + } + + if (!topLine && blockStart && spaced.length > 0 && spaced[spaced.length - 1] !== '') { + spaced.push(''); + } + + if (isSourceLine(line) && spaced[spaced.length - 1] === '') { + spaced.pop(); + } + + spaced.push(line); + } + + const tightened = []; + for (let i = 0; i < spaced.length; i += 1) { + const line = spaced[i]; + const prev = tightened[tightened.length - 1]; + + if ( + line === '' && + isItemTitle(prev || '') + ) { + continue; + } + + if (line === '' && spaced[i + 1] && isSourceLine(spaced[i + 1])) { + continue; + } + + tightened.push(line); + } + + while (tightened[0] === '') tightened.shift(); + while (tightened[tightened.length - 1] === '') tightened.pop(); + + const finalLines = []; + for (let i = 0; i < tightened.length; i += 1) { + const line = tightened[i]; + const next = tightened[i + 1]; + + finalLines.push(line); + + if ((isHeader(line) || isSectionHeading(line)) && next && next !== '') { + finalLines.push(''); + } + } + + const deduped = []; + for (const line of finalLines) { + if (line === '' && (deduped.length === 0 || deduped[deduped.length - 1] === '')) { + continue; + } + deduped.push(line); + } + + while (deduped[0] === '') deduped.shift(); + while (deduped[deduped.length - 1] === '') deduped.pop(); + + return `${deduped.join('\n')}\n`; +} + +const input = await readInput(); +process.stdout.write(formatDigest(input)); diff --git a/scripts/package.json b/scripts/package.json index c8513db65..0e2e0176b 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "generate-feed": "node generate-feed.js", - "prepare-digest": "node prepare-digest.js" + "prepare-digest": "node prepare-digest.js", + "render-feishu-card": "node render-feishu-card.js" }, "dependencies": { "dotenv": "^16.4.0", diff --git a/scripts/render-feishu-card.js b/scripts/render-feishu-card.js new file mode 100644 index 000000000..ec04c53bb --- /dev/null +++ b/scripts/render-feishu-card.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node + +import { readFile } from 'fs/promises'; + +async function readInput() { + const args = process.argv.slice(2); + const fileIdx = args.indexOf('--file'); + + if (fileIdx !== -1 && args[fileIdx + 1]) { + return readFile(args[fileIdx + 1], 'utf-8'); + } + + const chunks = []; + for await (const chunk of process.stdin) { + chunks.push(chunk); + } + return Buffer.concat(chunks).toString('utf-8'); +} + +function stripTitleMarkers(line) { + return line.replace(/^\*\*/, '').replace(/\*\*$/, '').trim(); +} + +function isHeader(line) { + return /^AI Builders Digest\b/.test(line); +} + +function isSectionHeading(line) { + return /^(TL;DR|Top Signals|X \/ TWITTER|OFFICIAL BLOGS|PODCASTS)$/.test(line); +} + +function isItemTitle(line) { + return /^\*\*.+\*\*$/.test(line); +} + +function isSourceLine(line) { + return /^Source:\s+(https?:\/\/\S+)/.test(line); +} + +function isFooter(line) { + return /^Generated through the Follow Builders skill: /.test(line) || + /^Reply to adjust your settings/.test(line); +} + +function parseDigest(text) { + const lines = text.replace(/\r\n?/g, '\n').split('\n').map(line => line.trimRight()); + const title = lines.find(isHeader) || 'AI Builders Digest'; + const sections = []; + const topSignals = []; + + let currentSection = null; + let currentItem = null; + let inSummary = false; + + const flushItem = () => { + if (currentSection && currentItem) { + currentSection.items.push(currentItem); + } + currentItem = null; + }; + + const flushSection = () => { + flushItem(); + if (currentSection) { + sections.push(currentSection); + } + currentSection = null; + }; + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line || isHeader(line) || isFooter(line)) { + continue; + } + + if (line === 'TL;DR' || line === 'Top Signals') { + flushSection(); + inSummary = true; + continue; + } + + if (line.startsWith('• ')) { + if (inSummary) { + topSignals.push(line); + } else if (currentItem) { + currentItem.body.push(line); + } + continue; + } + + if (isSectionHeading(line)) { + inSummary = false; + flushSection(); + currentSection = { heading: line, items: [] }; + continue; + } + + if (isItemTitle(line)) { + inSummary = false; + flushItem(); + currentItem = { + title: stripTitleMarkers(line), + body: [], + sources: [] + }; + continue; + } + + const sourceMatch = line.match(/^Source:\s+(https?:\/\/\S+)/); + if (sourceMatch && currentItem) { + currentItem.sources.push(sourceMatch[1]); + continue; + } + + if (currentItem) { + currentItem.body.push(line); + } + } + + flushSection(); + + return { title, topSignals, sections }; +} + +function itemToMarkdown(item) { + const lines = [`**${item.title}**`, ...item.body]; + + item.sources.forEach((url, index) => { + const label = item.sources.length > 1 ? `Source ${index + 1}` : 'Source'; + lines.push(`[${label}](${url})`); + }); + + return lines.join('\n'); +} + +function buildCard(parsed) { + const elements = []; + + if (parsed.topSignals.length > 0) { + elements.push({ + tag: 'div', + text: { + tag: 'lark_md', + content: `**TL;DR**\n${parsed.topSignals.join('\n')}` + } + }); + } + + parsed.sections.forEach((section, sectionIndex) => { + if (elements.length > 0) { + elements.push({ tag: 'hr' }); + } + + elements.push({ + tag: 'div', + text: { + tag: 'lark_md', + content: `**${section.heading}**` + } + }); + + section.items.forEach((item, itemIndex) => { + if (itemIndex > 0) { + elements.push({ tag: 'hr' }); + } + + elements.push({ + tag: 'div', + text: { + tag: 'lark_md', + content: itemToMarkdown(item) + } + }); + }); + }); + + return { + config: { + wide_screen_mode: true, + enable_forward: true + }, + header: { + template: 'blue', + title: { + tag: 'plain_text', + content: parsed.title + } + }, + elements + }; +} + +const input = await readInput(); +const parsed = parseDigest(input); +process.stdout.write(`${JSON.stringify(buildCard(parsed))}\n`); From 9d37f8749bfcb46b38a26efa5396ac09dfd365ff Mon Sep 17 00:00:00 2001 From: Chenkai Date: Fri, 3 Apr 2026 17:14:46 +0800 Subject: [PATCH 2/2] trim README changes for upstream style --- README.md | 2 +- README.zh-CN.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2aedbb73a..42c4b2481 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ WhatsApp, etc.) with: - Full articles from official AI company blogs (Anthropic Engineering, Claude Blog) - Links to all original content - Available in English, Chinese, or bilingual +- Optional Feishu interactive cards for cleaner mobile reading ## Quick Start @@ -128,4 +129,3 @@ See [examples/sample-digest.md](examples/sample-digest.md) for what the output l ## License MIT - diff --git a/README.zh-CN.md b/README.zh-CN.md index 51224698d..9d26aaeff 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -15,6 +15,7 @@ - AI 公司官方博客的完整文章(Anthropic Engineering、Claude Blog) - 所有原始内容的链接 - 支持英文、中文或双语版本 +- 可选的飞书 interactive card 阅读模式,更适合移动端早报 ## 快速开始