
Lark Study Reviver
- 1 installs
- 9 repo stars
- Updated April 15, 2026
- onlyaguest/waytoagi_study_reviver
Lark Study Reviver is an agent skill that runs a short quiz and Lark CLI search to produce a WaytoAGI-linked learning path when you say you want to learn
About
Lark Study Reviver is an agent skill that turns a vague “I want to learn X” into a personalized reading path sourced from the WaytoAGI knowledge base. It is built for developers—especially Chinese-speaking users in the Feishu/Lark ecosystem—who prefer a short conversational intake over dumping a static syllabus. The flow is deliberate: three or four light Q&A rounds gauge goal, level, time, and learning style; keywords from those answers drive mandatory Lark CLI search; results are returned as original links only, with an optional Feishu doc capturing the full path. That “chat first, search second, never rehost content” rule keeps trust with the knowledge base and saves you from agent-hallucinated tutorials. Use it when you are exploring agents, AI coding, or creative AI stacks and need direction before deep Build work. It is not a course author replacement—you still read the linked articles—and it depends on a working lark-cli login.
- 3–4 round chat-style quiz (not a formal exam) with emoji-led tone
- Dynamic follow-up questions based on prior answers across Hermes, coding, and art tracks
- Mandatory `lark-cli docs +search` against WaytoAGI—no scrape-and-rewrite of articles
- Outputs original URLs plus a Feishu document保存完整学习路径
- Requires `lark-cli` auth across docs, base, im, and wiki domains
Lark Study Reviver by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Jul 13, 2026 (Skillselion catalog sync)
npx skills add https://github.com/onlyaguest/waytoagi_study_reviver --skill lark-study-reviverAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 9 |
| Security audit | 1 / 3 scanners passed |
| Last updated | April 15, 2026 |
| Repository | onlyaguest/waytoagi_study_reviver ↗ |
What it does
Run a short friendly quiz on what you want to learn and your level, then pull WaytoAGI articles via Lark CLI into a linked study path and Feishu doc—without rewriting the knowledge base.
Who is it for?
learners on Lark/Feishu who want WaytoAGI-curated links after a quick leveling quiz in Chinese or bilingual chat.
Skip if: Skip if you're without lark-cli installed and authenticated, or anyone and needs hands-on code generation instead of a reading roadmap.
When should I use this skill?
When the user says they want to learn something (e.g. 我想学XX) and needs a customized WaytoAGI study path via Lark search.
What you get
You get a tailored list of original article URLs and a Feishu doc study path keyed to your quiz answers, ready to read before you commit to a build or agent setup.
- Curated article list with original URLs
- Feishu document with full learning path
By the numbers
- 3–4 interactive quiz rounds maximum
- 4 required quiz dimensions: goal, level, time, learning style
Files
Study Reviver — 互动学习路径导航
前置条件: 需要 lark-cli 已安装并认证(lark-cli auth login --domain docs,base,im,wiki)。概述
不是丢一个问卷给用户,而是像朋友聊天一样,通过 3-4 轮轻松有趣的互动问答,了解用户的学习目标和基础水平,然后从知识库精准推荐文章。
核心理念:先聊再搜,搜完就推。不写入、不搬运、不重写。
核心流程
用户说"我想学XX"
-> 启动互动测验(3-4 轮轻松问答)
-> 根据测验结果生成关键词
-> lark-cli docs +search 搜索知识库(必须搜索,不用遍历)
-> 输出带原始 URL 的学习路径
-> 生成飞书文档保存完整路径互动测验设计
测验规则
- 不超过 4 个问题,每轮 1 个,像聊天不像考试
- 用 emoji 和轻松语气,不要用"请选择""请回答"这种正式措辞
- 根据上一轮回答动态调整下一题,不是固定问卷
- 问题要有画面感,让用户想象场景而不是抽象选择
必问维度
1. 学习目标:想学什么(用户已说,但可以追问具体方向) 2. 当前水平:AI 基础 / 编程基础 / 工具使用经验 3. 时间投入:每天/每周能花多少时间 4. 学习风格:喜欢看教程还是直接上手
题库示例(按场景动态组合)
🎯 了解当前水平(选 1-2 个最相关的)
Hermes/Agent 方向:
先来个灵魂拷问 🧙♂️
你之前用过哪种 AI Agent?
A) 完全没用过,Hermes 是第一个 👶
B) 用过 ChatGPT/Claude 聊天,但没搭过本地 Agent 📱
C) 装过 OpenClaw/Claude Code,跑通过基本流程 🔧
D) 已经给 Agent 写过 Skill 了,想再深入 🚀AI 编程方向:
坦白局 💻
你对编程的熟悉程度是?
A) 完全不会写代码,但会复制粘贴跑命令 📋
B) 写过 Python/JS,能看懂别人代码但自己写会卡 🐍
C) 日常写代码,只是没用 AI 辅助过 👨💻
D) 全栈选手,想让 AI 帮我干掉重复工作 🤖AI 绘画方向:
画个像先 🎨
你的 AI 绘画经验?
A) 听过 Midjourney 但没用过 👀
B) 跑过 MJ/DALL-E,但不会写 prompt 🖼️
C) 用过 Stable Diffusion/ComfyUI,搭过工作流 🎭
D) 已经是炼丹师了,想学新玩法 ✨通用水平判断:
快问快答 ⚡
如果现在让你用 AI 做一个"自动每天早上帮我总结昨天的微信群消息"的东西,你的第一反应是?
A) 这是个啥?听起来很厉害但不知道从哪开始 🤷
B) 应该能行,但我得搜搜教程再动手 🔍
C) 给我 2 小时就能搭出来 ⏰
D) 这有啥难的,我已经做过类似的了 😎⏰ 时间投入
现实一点 📅
你一周能匀多少时间给学习?
A) 随缘,有空就看(每周 < 2h)🌊
B) 每天挤 1 小时(每周 ~5h)🕐
C) 认真学,每周 10h+ 📚
D) All in,这周啥也不干了就学它 🔥🎯 学习风格
学习偏好 🧠
你更喜欢哪种学习方式?
A) 先看理论搞懂原理再动手 📖
B) 别废话了直接让我跑起来 🏃
C) 边看教程边做项目 🛠️
D) 给我文档自己啃,有问题再问 📋根据回答调整推荐策略
| 用户画像 | 推荐策略 |
|---|---|
| 纯小白 + 随缘学 | 推荐 3-5 篇最短最入门的,每篇标注"预计阅读时间" |
| 有经验 + 时间多 | 推荐完整路径(入门->进阶->实战),10+ 篇 |
| 有基础 + 时间少 | 推荐"捷径版",只看最核心的 3-4 篇 |
| 高手 + 特定方向 | 精准推荐该方向最新最深度的 5-8 篇 |
| 想直接上手 | 推荐安装教程+实战项目优先放前面 |
| 喜欢理论 | 推荐概念解析+架构分析类文章优先 |
执行步骤
Step 0: 启动测验
当用户说"我想学XX"时,立即启动测验。不要直接搜索。
太棒了!给你定制一条专属学习路径 🎯
先来个 30 秒小测试,帮你找到最适合的路线 👇
[出题:根据用户想学的主题,从题库选 3-4 个问题]Step 1: 完成测验
逐轮提问,根据回答动态调整。每轮只问一个问题。
测验完成后,总结用户画像:
好的,摸清了!你的学习画像:
🎯 目标:学 Hermes Agent
📊 水平:用过 ChatGPT,但没搭过本地 Agent
⏰ 时间:每天 1 小时
🧠 风格:直接上手型
正在从知识库给你找最合适的文章... 🔍Step 2: 搜索知识库
根据测验结果生成关键词。
关键词映射参考:
| 用户说 | 搜索关键词 |
|---|---|
| 我想学 Hermes / Agent | Hermes OR Agent OR 智能体 |
| 我想学 AI 编程 / Claude Code | Claude Code OR AI 编程 OR 智能体编程 |
| 我想学 AI 绘画 | AI 绘画 OR ComfyUI OR Stable Diffusion |
| 我想学 Prompt | Prompt OR 提示词 OR 提示工程 |
| 我想学社区运营 | 社区运营 OR 运营提效 OR 自动化 |
| 我想学 AI 视频 | AI 视频 OR 视频生成 |
| 我想学 AI 音乐 | AI 音乐 |
| 我想学 RAG | RAG OR 检索增强 |
| 我想学多智能体 | 多智能体 OR Multi-Agent OR Harness |
如果用户目标比较模糊,先用宽泛关键词搜一轮,根据结果再精调。
必须使用 `docs +search`,不使用 `wiki nodes list` 遍历。
lark-cli docs +search --query "关键词1 OR 关键词2" --page-size 20 --format json关键字段提取(直接从搜索结果取,不 fetch 全文):
{
"title_highlighted": "文档标题(含高亮标记)",
"summary_highlighted": "摘要片段(含高亮标记)",
"result_meta": {
"url": "https://waytoagi.feishu.cn/docx/xxx",
"update_time_iso": "2026-04-10T23:49:27+08:00",
"create_time_iso": "2026-03-25T19:50:58+08:00",
"owner_name": "作者名"
}
}解析注意事项:
title_highlighted和summary_highlighted中可能包含<h>高亮标签,输出时需清理url是知识库原文链接,直接使用,不要改写- 优先按
update_time_iso排序(越新越好),同主题下按逻辑顺序排列
Step 3: 组织学习路径
按以下规则排序和分组:
1. 难度递进:入门概念 -> 安装配置 -> 进阶技巧 -> 实战项目 2. 时间优先:同类型文章优先推荐最近的 3. 来源权重:优先推荐共学纪要、官方教程、社区精华 4. 用户画像匹配:根据测验结果调整推荐顺序(小白放入门、老手放进阶)
输出格式:
## 📚 你的 {主题} 学习路径
> 根据你的学习画像定制 | 预计总时长:X 小时
### 🌱 第一阶段:入门(约 X 小时)
| # | 文章 | 作者 | 更新 | 说明 |
|---|------|------|------|------|
| 1 | [文章标题](URL) | 作者 | 2026-04-10 | 为什么先看这篇 |
### 🚀 第二阶段:进阶(约 X 小时)
...
### 🛠️ 第三阶段:实战(约 X 小时)
...
## 💡 学习建议
- 按顺序阅读,不要跳
- 遇到不理解的先标记,后面文章会覆盖
- 看完想讨论什么,随时问我Step 4: 生成飞书文档(必须执行)
将完整的学习路径写入飞书文档,方便用户随时查看和分享。
文档风格要求:清晰、亲切、易读,像朋友给你写的一份学习指南。
生成文档内容(包含以下全部信息):
# 📚 {主题} 学习路径 — Study Reviver 定制
> 生成时间:{当前时间} | 数据来源:WaytoAGI 知识库
---
## 👋 你好呀!
这是为你定制的学习路径,从 WaytoAGI 知识库的 10000+ 篇文章中,精挑细选了最精华的内容。
不用有压力,按照自己的节奏来就好。每一步都配有原文链接,点开就能看 👇
---
## 📖 学习路径
### 🌱 第一阶段:先了解一下
> 这几篇帮你建立基本认知,每篇大约 10-15 分钟
| # | 文章 | 更新 | 为什么先看这篇 |
|---|------|------|------|
| 1 | [文章标题](URL) | 日期 | 一句话说明 |
### 🚀 第二阶段:动手试试
> 看完可以直接上手实践
| # | 文章 | 更新 | 为什么看这篇 |
|---|------|------|------|
| 1 | [文章标题](URL) | 日期 | 一句话说明 |
### 🛠️ 第三阶段:深入探索(有空再看)
> 这些是进阶内容,随缘看就好
| # | 文章 | 更新 | 为什么看这篇 |
|---|------|------|------|
| 1 | [文章标题](URL) | 日期 | 一句话说明 |
---
## 💡 小贴士
- 按顺序看就行,不用跳着读
- 遇到看不懂的先放着,后面会慢慢明白
- 有疑问随时问我,我们一起讨论
---
*这份学习路径由 Study Reviver 自动生成,内容来自 WaytoAGI 社区知识库*创建飞书文档:
# 将学习路径写入临时文件
# 然后创建飞书文档
lark-cli docs +create \
--title "{主题} 学习路径 — Study Reviver 定制" \
--markdown @/tmp/learning-path.md \
--wiki-space 7226178700923011075返回结果给用户:
✅ 学习路径已生成!
📄 飞书文档:[点击查看](飞书文档URL)
文档里已经整理好了所有链接和说明,随时可以打开看。
有疑问随时问我,我们一起讨论 👇故障排查
搜索返回空结果
如果 docs +search 返回空或结果不相关: 1. 调整关键词,用更宽泛的词 2. 用英文关键词试试 3. 最多尝试 3 次不同关键词组合
搜索结果包含太多无关内容
- 在关键词中加
AND排除 - 减小
page-size到 10,人工筛选后再扩大
URL 格式问题
搜索结果中的 URL 直接原样输出,不要尝试改写或规范化。
版本历史
| 版本 | 变化 |
|---|---|
| v1.0 | 5 步流水线:搜索->提取->写Base->测验->播报 |
| v2.0 | 精简为搜索->排序->给链接,删除 Base 依赖 |
| v3.0 | 加入互动测验环节,先聊再搜,个性化推荐 |
| v4.0 | 新增 Step 4:自动生成飞书文档,完整保存学习路径 |
| v4.1 | 删除终极目标问题,保留其他所有问题和推荐策略 |
# 飞书应用凭证
FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
FEISHU_WIKI_SPACE_ID=7226178700923011075
node_modules/
.next/
.env
*.local
package-lock.json
*.tsbuildinfo
import { NextRequest, NextResponse } from 'next/server'
import { createDocWithContent } from '@/lib/lark'
export async function POST(req: NextRequest) {
try {
const { topic, results } = await req.json()
if (!results || results.length === 0) {
return NextResponse.json({ error: '没有找到相关文章' }, { status: 400 })
}
const title = `${topic} 学习路径 — Study Reviver 定制`
const docUrl = await createDocWithContent(title, results)
return NextResponse.json({ url: docUrl })
} catch (error: any) {
console.error('Generate doc error:', error)
return NextResponse.json(
{ error: 'Failed to generate doc: ' + (error.message || 'Unknown error') },
{ status: 500 }
)
}
}
import { NextResponse } from 'next/server'
// 简单的关键词映射,对应 SKILL.md 中的映射表
const KEYWORD_MAP: Record<string, string> = {
'hermes': 'Hermes OR Agent OR 智能体',
'agent': 'Hermes OR Agent OR 智能体',
'智能体': 'Hermes OR Agent OR 智能体',
'编程': 'Claude Code OR AI 编程',
'绘画': 'AI 绘画 OR ComfyUI OR Stable Diffusion',
'prompt': 'Prompt OR 提示词 OR 提示工程',
'提示词': 'Prompt OR 提示词 OR 提示工程',
'运营': '社区运营 OR 运营提效 OR 自动化',
'视频': 'AI 视频 OR 视频生成',
'音乐': 'AI 音乐',
'rag': 'RAG OR 检索增强',
'多智能体': '多智能体 OR Multi-Agent OR Harness',
'写作': '社群 OR 写作 OR Prompt OR 内容创作',
'文章': '社群 OR 写作 OR Prompt OR 内容创作',
}
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const topic = searchParams.get('topic') || ''
// 根据主题生成测验问题
// 为了简化,前端可以根据 topic 直接显示问题,或者这里返回一组问题
// 这里返回一个简单的问题集结构
const questions = [
{
id: 'level',
title: '🧙♂️ 灵魂拷问',
...getLevelQuestion(topic),
},
{
id: 'time',
title: '📅 现实一点',
question: '你一周能匀多少时间给学习?',
options: [
{ key: 'A', text: '随缘,有空就看(每周 < 2h)🌊' },
{ key: 'B', text: '每天挤 1 小时(每周 ~5h)🕐' },
{ key: 'C', text: '认真学,每周 10h+ 📚' },
{ key: 'D', text: 'All in,这周啥也不干了就学它 🔥' },
]
},
{
id: 'style',
title: '🧠 学习偏好',
question: '你更喜欢哪种学习方式?',
options: [
{ key: 'A', text: '先看理论搞懂原理再动手 📖' },
{ key: 'B', text: '别废话了直接让我跑起来 🏃' },
{ key: 'C', text: '边看教程边做项目 🛠️' },
{ key: 'D', text: '给我文档自己啃,有问题再问 📋' },
]
}
]
return NextResponse.json({ questions })
}
function getLevelQuestion(topic: string) {
const t = topic.toLowerCase()
if (t.includes('hermes') || t.includes('agent') || t.includes('智能体')) {
return {
question: '你之前用过哪种 AI Agent?',
options: [
{ key: 'A', text: '完全没用过,Hermes 是第一个 👶' },
{ key: 'B', text: '用过 ChatGPT/Claude 聊天,但没搭过本地 Agent 📱' },
{ key: 'C', text: '装过 OpenClaw/Claude Code,跑通过基本流程 🔧' },
{ key: 'D', text: '已经给 Agent 写过 Skill 了,想再深入 🚀' },
]
}
}
if (t.includes('编程') || t.includes('code') || t.includes('python')) {
return {
question: '你对编程的熟悉程度是?',
options: [
{ key: 'A', text: '完全不会写代码,但会复制粘贴跑命令 📋' },
{ key: 'B', text: '写过 Python/JS,能看懂别人代码但自己写会卡 🐍' },
{ key: 'C', text: '日常写代码,只是没用 AI 辅助过 👨💻' },
{ key: 'D', text: '全栈选手,想让 AI 帮我干掉重复工作 🤖' },
]
}
}
// 通用
return {
question: '你目前的 AI 工具使用经验?',
options: [
{ key: 'A', text: '纯小白,基本没用过 👶' },
{ key: 'B', text: '用过 ChatGPT/Kimi 等聊天 AI 💬' },
{ key: 'C', text: '经常使用多种 AI 工具 🛠️' },
{ key: 'D', text: '已经是高阶玩家了 🚀' },
]
}
}
import { NextRequest, NextResponse } from 'next/server'
import paths from '../../../data/paths.json'
export async function POST(req: NextRequest) {
const { question } = await req.json()
if (!question) return NextResponse.json({ error: 'missing question' }, { status: 400 })
const apiKey = process.env.GEMINI_API_KEY
if (!apiKey) return NextResponse.json({ error: 'AI service not configured' }, { status: 503 })
const pathsSummary = paths.map(p => `${p.emoji} ${p.title}: ${p.subtitle} (${p.tags.join(', ')})`).join('\n')
const prompt = `你是 WAYtoAGI 社区的学习路径推荐助手。用户描述了自己的情况,请根据以下预设路径推荐最合适的,或者给出个性化建议。
预设路径:
${pathsSummary}
用户说:${question}
请用中文回复,200字以内,推荐1-2条最合适的路径并说明原因。如果预设路径都不太合适,给出个性化建议。语气亲切友好,像朋友聊天。`
try {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { maxOutputTokens: 500, temperature: 0.7 }
})
}
)
const data = await res.json()
const reply = data.candidates?.[0]?.content?.parts?.[0]?.text || '抱歉,AI 暂时无法回复'
return NextResponse.json({ reply })
} catch (e) {
return NextResponse.json({ error: 'AI service error' }, { status: 500 })
}
}
import { NextRequest, NextResponse } from 'next/server'
import { searchDocs } from '@/lib/lark'
// 关键词映射
const KEYWORD_MAP: Record<string, string> = {
'hermes': 'Hermes OR Agent OR 智能体',
'agent': 'Hermes OR Agent OR 智能体',
'智能体': 'Hermes OR Agent OR 智能体',
'编程': 'Claude Code OR AI 编程',
'code': 'Claude Code OR AI 编程',
'python': 'Claude Code OR AI 编程',
'绘画': 'AI 绘画 OR ComfyUI OR Stable Diffusion',
'prompt': 'Prompt OR 提示词',
'提示词': 'Prompt OR 提示词',
'运营': '社区运营 OR 自动化',
'视频': 'AI 视频 OR 视频生成',
'音乐': 'AI 音乐',
'rag': 'RAG OR 检索增强',
'多智能体': 'Multi-Agent OR Harness',
'写作': '写作 OR Prompt OR 内容创作',
'文章': '写作 OR Prompt OR 内容创作',
'skill': 'Skill OR 技能',
'openclaw': 'OpenClaw OR 龙虾',
'vibe': 'Vibe Coding OR 编程',
'mcp': 'MCP OR 协议',
'geo': 'GEO OR 搜索优化',
'harness': 'Harness OR 架构',
}
export async function POST(req: NextRequest) {
try {
const { topic } = await req.json()
// 1. 确定搜索关键词
let query = topic
const lowerTopic = topic.toLowerCase()
for (const [key, value] of Object.entries(KEYWORD_MAP)) {
if (lowerTopic.includes(key)) {
query = value
break
}
}
// 2. 调用飞书 API 搜索
const results = await searchDocs(query)
// 返回前 8 个
return NextResponse.json({ results: results.slice(0, 8) })
} catch (error: any) {
console.error('Search error:', error)
return NextResponse.json(
{ error: 'Search failed: ' + (error.message || 'Unknown error') },
{ status: 500 }
)
}
}
'use client'
import { useState, useEffect } from 'react'
type Step = 'input' | 'quiz' | 'searching' | 'results' | 'generating' | 'done'
interface Question {
id: string
title: string
question: string
options?: Array<{ key: string; text: string }>
}
interface Result {
id: number
title: string
url: string
update: string
summary: string
}
export default function CustomPage() {
const [step, setStep] = useState<Step>('input')
const [topic, setTopic] = useState('')
const [questions, setQuestions] = useState<Question[]>([])
const [currentQIndex, setCurrentQIndex] = useState(0)
const [answers, setAnswers] = useState<Record<string, string>>({})
const [results, setResults] = useState<Result[]>([])
const [docUrl, setDocUrl] = useState('')
const [loadingMsg, setLoadingMsg] = useState('')
// 输入主题
const handleTopicSubmit = async () => {
if (!topic.trim()) return
setStep('quiz')
setLoadingMsg('正在为你准备测验...')
try {
const res = await fetch(`/api/quiz?topic=${encodeURIComponent(topic)}`)
const data = await res.json()
setQuestions(data.questions)
} catch (e) {
console.error('Failed to load quiz:', e)
// Fallback to default questions if API fails
setQuestions([
{
id: 'level',
title: '🧙♂️ 灵魂拷问',
question: '你目前的 AI 工具使用经验?',
options: [
{ key: 'A', text: '纯小白,基本没用过 👶' },
{ key: 'B', text: '用过 ChatGPT/Kimi 等聊天 AI 💬' },
{ key: 'C', text: '经常使用多种 AI 工具 🛠️' },
{ key: 'D', text: '已经是高阶玩家了 🚀' },
]
},
{
id: 'time',
title: '📅 现实一点',
question: '你一周能匀多少时间给学习?',
options: [
{ key: 'A', text: '随缘,有空就看(每周 < 2h)🌊' },
{ key: 'B', text: '每天挤 1 小时(每周 ~5h)🕐' },
{ key: 'C', text: '认真学,每周 10h+ 📚' },
{ key: 'D', text: 'All in,这周啥也不干了就学它 🔥' },
]
},
{
id: 'style',
title: '🧠 学习偏好',
question: '你更喜欢哪种学习方式?',
options: [
{ key: 'A', text: '先看理论搞懂原理再动手 📖' },
{ key: 'B', text: '别废话了直接让我跑起来 🏃' },
{ key: 'C', text: '边看教程边做项目 🛠️' },
{ key: 'D', text: '给我文档自己啃,有问题再问 📋' },
]
}
])
}
}
// 回答问题
const handleAnswer = async (key: string) => {
const q = questions[currentQIndex]
setAnswers(prev => ({ ...prev, [q.id]: key }))
if (currentQIndex < questions.length - 1) {
setCurrentQIndex(prev => prev + 1)
} else {
// 所有问题回答完毕,开始搜索
setStep('searching')
setLoadingMsg('正在从知识库搜索最合适的文章...')
try {
const res = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, level: answers.level, time: answers.time, style: answers.style })
})
const data = await res.json()
setResults(data.results)
setStep('results')
} catch (e) {
console.error('Search failed:', e)
setResults([]) // Show empty state or error
setStep('results')
}
}
}
// 生成飞书文档
const handleGenerateDoc = async () => {
setStep('generating')
setLoadingMsg('正在生成飞书文档,请稍候...')
try {
const res = await fetch('/api/generate-doc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, results, level: answers.level, time: answers.time, style: answers.style })
})
const data = await res.json()
if (data.url) {
setDocUrl(data.url)
setStep('done')
} else {
throw new Error('No URL returned')
}
} catch (e) {
console.error('Generate doc failed:', e)
setLoadingMsg('生成失败,请重试')
setStep('results') // Back to results
}
}
return (
<main style={{ maxWidth: 640, margin: '0 auto', padding: '40px 20px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', color: '#333' }}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: 40 }}>
<h1 style={{ fontSize: '1.5em', fontWeight: 600, marginBottom: 8 }}>🎯 Study Reviver</h1>
<p style={{ color: '#666', margin: 0 }}>你的专属学习路径生成器</p>
</div>
{/* Chat-like Container */}
<div style={{ background: '#f9f9f9', borderRadius: 16, padding: 24, minHeight: 400, boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
{/* Bot Messages */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Bubble sender="bot" text="你好呀!告诉我你想学什么?" />
{topic && <Bubble sender="user" text={`我想学:${topic}`} />}
{step === 'input' && (
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<input
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="例如:AI 社群写作、Hermes Agent..."
style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #ddd', fontSize: '1em' }}
onKeyDown={(e) => e.key === 'Enter' && handleTopicSubmit()}
/>
<button
onClick={handleTopicSubmit}
style={{ padding: '10px 20px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer' }}
>
开始
</button>
</div>
)}
{step === 'quiz' && questions[currentQIndex] && (
<div style={{ marginTop: 8 }}>
<Bubble sender="bot" text={`${questions[currentQIndex].title}\n\n${questions[currentQIndex].question}`} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
{questions[currentQIndex].options?.map(opt => (
<button
key={opt.key}
onClick={() => handleAnswer(opt.key)}
style={{ padding: '12px', background: '#fff', border: '1px solid #e5e7eb', borderRadius: 12, cursor: 'pointer', textAlign: 'left', fontSize: '0.9em', transition: 'all 0.2s' }}
onMouseEnter={(e) => (e.currentTarget.style.background = '#f0f7ff')}
onMouseLeave={(e) => (e.currentTarget.style.background = '#fff')}
>
{opt.text}
</button>
))}
</div>
</div>
)}
{(step === 'searching' || step === 'generating') && (
<Bubble sender="bot" text={loadingMsg} />
)}
{step === 'results' && (
<div style={{ marginTop: 8 }}>
<Bubble sender="bot" text={`摸清了!为你找到了 ${results.length} 篇精华文章:`} />
<div style={{ marginTop: 12, background: '#fff', borderRadius: 12, padding: 16, border: '1px solid #e5e7eb' }}>
{results.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center' }}>抱歉,暂时没找到相关内容,换个关键词试试?</p>
) : (
results.map((r, i) => (
<a
key={r.id}
href={r.url}
target="_blank"
rel="noopener"
style={{ display: 'block', padding: '12px 0', borderBottom: i < results.length - 1 ? '1px solid #f0f0f0' : 'none', textDecoration: 'none', color: '#333' }}
>
<div style={{ fontWeight: 500, marginBottom: 4 }}>📄 {r.title}</div>
<div style={{ fontSize: '0.85em', color: '#888' }}>更新于 {r.update} • {r.summary}</div>
</a>
))
)}
</div>
{results.length > 0 && (
<button
onClick={handleGenerateDoc}
style={{ marginTop: 16, padding: '12px 24px', background: '#10b981', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 500, fontSize: '0.95em' }}
>
✨ 生成飞书文档保存
</button>
)}
</div>
)}
{step === 'done' && docUrl && (
<div style={{ marginTop: 8 }}>
<Bubble sender="bot" text={`✅ 学习路径已生成!\n\n📄 [点击打开飞书文档](${docUrl})\n\n文档里已经整理好了所有链接和说明,随时可以打开看。有疑问随时问我 👇`} />
<button
onClick={() => { setStep('input'); setTopic(''); setResults([]); setDocUrl('') }}
style={{ marginTop: 12, padding: '8px 16px', background: '#f3f4f6', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: '0.9em' }}
>
🔄 再来一次
</button>
</div>
)}
</div>
</div>
<footer style={{ textAlign: 'center', marginTop: 40, fontSize: '0.85em', color: '#999' }}>
Powered by <a href="https://github.com/Onlyaguest/WayToAGI_Study_Reviver" style={{ color: '#666' }}>Study Reviver</a> • WaytoAGI 知识库
</footer>
</main>
)
}
// 聊天气泡组件
function Bubble({ sender, text }: { sender: 'bot' | 'user'; text: string }) {
const isBot = sender === 'bot'
return (
<div style={{ display: 'flex', justifyContent: isBot ? 'flex-start' : 'flex-end', marginBottom: 8 }}>
<div style={{
maxWidth: '85%',
padding: '10px 14px',
borderRadius: isBot ? '12px 12px 12px 2px' : '12px 12px 2px 12px',
background: isBot ? '#fff' : '#3b82f6',
color: isBot ? '#333' : '#fff',
boxShadow: isBot ? '0 1px 3px rgba(0,0,0,0.05)' : 'none',
whiteSpace: 'pre-wrap',
lineHeight: 1.5,
fontSize: '0.95em',
}}>
{text}
</div>
</div>
)
}
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'WAYtoAGI 学习路径 — Study Reviver',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<link
rel="preconnect"
href="https://fonts.googleapis.com"
crossOrigin="anonymous"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Manrope:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</head>
<body style={{ margin: 0 }}>{children}</body>
</html>
)
}
'use client'
import { useState, useEffect } from 'react'
type Step = 'input' | 'quiz' | 'searching' | 'results' | 'generating' | 'done'
interface Question {
id: string
title: string
question: string
options?: Array<{ key: string; text: string }>
}
interface Result {
id: number
title: string
url: string
update: string
summary: string
}
export default function Home() {
const [step, setStep] = useState<Step>('input')
const [topic, setTopic] = useState('')
const [questions, setQuestions] = useState<Question[]>([])
const [currentQIndex, setCurrentQIndex] = useState(0)
const [answers, setAnswers] = useState<Record<string, string>>({})
const [results, setResults] = useState<Result[]>([])
const [docUrl, setDocUrl] = useState('')
const [loadingMsg, setLoadingMsg] = useState('')
const [mounted, setMounted] = useState(false)
const allTopics = ['Skill', 'Hermes', 'OpenClaw', 'AI 视频', 'AI 绘画', 'Claude Code', 'Vibe Coding', 'MCP', 'GEO', 'Harness']
const [displayTopics, setDisplayTopics] = useState(allTopics.slice(0, 5))
const [chatHistory, setChatHistory] = useState<Array<{type: 'bot' | 'user', content: string}>>([
{ type: 'bot', content: '你好呀!我是你的学习路径导航员。\n告诉我你想学什么?' }
])
useEffect(() => {
setMounted(true)
setDisplayTopics([...allTopics].sort(() => Math.random() - 0.5).slice(0, 5))
}, [])
const addMessage = (type: 'bot' | 'user', content: string) => {
setChatHistory(prev => [...prev, { type, content }])
}
const handleTopicSubmit = async () => {
if (!topic.trim()) return
addMessage('user', `我想学:${topic}`)
setStep('quiz')
await new Promise(r => setTimeout(r, 600))
addMessage('bot', '好的!让我先了解一下你的情况,帮你找到最合适的路径 🧭')
await new Promise(r => setTimeout(r, 800))
try {
const res = await fetch(`/api/quiz?topic=${encodeURIComponent(topic)}`)
const data = await res.json()
setQuestions(data.questions)
addMessage('bot', `${data.questions[0]?.title || '第一题'}\n${data.questions[0]?.question || '你目前的 AI 工具使用经验?'}`)
} catch (e) {
setQuestions([
{ id: 'level', title: '🧙♂️ 灵魂拷问', question: '你目前的 AI 工具使用经验?', options: [
{ key: 'A', text: '纯小白,基本没用过 👶' },
{ key: 'B', text: '用过 ChatGPT/Kimi 等聊天 AI 💬' },
{ key: 'C', text: '经常使用多种 AI 工具 🛠️' },
{ key: 'D', text: '已经是高阶玩家了 🚀' },
]},
{ id: 'time', title: '📅 现实一点', question: '你一周能匀多少时间给学习?', options: [
{ key: 'A', text: '随缘,有空就看(每周 < 2h)🌊' },
{ key: 'B', text: '每天挤 1 小时(每周 ~5h)🕐' },
{ key: 'C', text: '认真学,每周 10h+ 📚' },
{ key: 'D', text: 'All in,这周啥也不干了就学它 🔥' },
]},
{ id: 'style', title: '🧠 学习偏好', question: '你更喜欢哪种学习方式?', options: [
{ key: 'A', text: '先看理论搞懂原理再动手 📖' },
{ key: 'B', text: '别废话了直接让我跑起来 🏃' },
{ key: 'C', text: '边看教程边做项目 🛠️' },
{ key: 'D', text: '给我文档自己啃,有问题再问 📋' },
]}
])
addMessage('bot', '🧙♂️ 灵魂拷问\n你目前的 AI 工具使用经验?')
}
}
const handleAnswer = async (key: string) => {
const q = questions[currentQIndex]
const selectedText = q.options?.find(o => o.key === key)?.text || key
addMessage('user', selectedText)
setAnswers(prev => ({ ...prev, [q.id]: key }))
await new Promise(r => setTimeout(r, 400))
if (currentQIndex < questions.length - 1) {
const nextQ = questions[currentQIndex + 1]
setCurrentQIndex(prev => prev + 1)
addMessage('bot', `${nextQ.title}\n${nextQ.question}`)
} else {
setStep('searching')
addMessage('bot', '收到!正在从 10000+ 篇文章中为你搜索最合适的路径...')
await new Promise(r => setTimeout(r, 1200))
try {
const res = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, level: answers.level, time: answers.time, style: answers.style })
})
const data = await res.json()
if (data.error) {
addMessage('bot', `抱歉,搜索遇到了问题:${data.error}`)
setResults([])
} else {
const results = data.results || []
setResults(results)
addMessage('bot', `🎯 找到了!为你精选了 ${results.length} 篇最相关的文章:`)
}
setStep('results')
} catch (e) {
addMessage('bot', '抱歉,网络连接失败,请稍后重试。')
setResults([])
setStep('results')
}
}
}
const handleGenerateDoc = async () => {
setStep('generating')
addMessage('bot', '正在为你生成一份完整的学习路径文档...')
await new Promise(r => setTimeout(r, 1000))
try {
const res = await fetch('/api/generate-doc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic, results, level: answers.level, time: answers.time, style: answers.style })
})
const data = await res.json()
if (data.url) {
setDocUrl(data.url)
setStep('done')
addMessage('bot', `✅ 搞定啦!\n\n你的学习路径文档已生成:\n${data.url}\n\n文档里包含了所有精选文章的链接和说明,随时可以打开看。有疑问随时找我 👋`)
} else {
throw new Error('No URL returned')
}
} catch (e) {
addMessage('bot', '抱歉,生成文档时遇到了问题,请重试。')
setStep('results')
}
}
const resetFlow = () => {
setStep('input')
setTopic('')
setResults([])
setDocUrl('')
setCurrentQIndex(0)
setAnswers({})
setQuestions([])
setChatHistory([
{ type: 'bot', content: '你好呀!我是你的学习路径导航员。\n告诉我你想学什么?' }
])
}
return (
<div className="page-root">
{/* Background layers */}
<div className="bg-base" />
<div className="bg-gradient" />
<div className="bg-grain" />
{/* Floating elements */}
<div className="float-el float-circle" />
<div className="float-el float-diamond" />
<div className="float-el float-line" />
<main className={`main-wrap ${mounted ? 'in' : ''}`}>
{/* Header - asymmetric layout */}
<header className="site-header">
<div className="header-left">
<div className="brand-tag">WaytoAGI 知识库</div>
<h1 className="hero-title">
找到你的<br />
<em className="title-em">学习路径</em>
</h1>
</div>
<div className="header-right">
<p className="hero-desc">AI 从 10000+ 篇文章中<br />为你精挑细选</p>
<div className="stats-row">
<div className="stat-item">
<div className="stat-val">10000+</div>
<div className="stat-label">篇文章</div>
</div>
<div className="stat-sep" />
<div className="stat-item">
<div className="stat-val">500+</div>
<div className="stat-label">场活动</div>
</div>
<div className="stat-sep" />
<div className="stat-item">
<div className="stat-val">900 万</div>
<div className="stat-label">学习者</div>
</div>
</div>
</div>
</header>
{/* Chat card */}
<section className="chat-panel">
<div className="chat-box">
<div className="chat-scroll">
{chatHistory.map((msg, i) => (
<div key={i} className={`chat-msg chat-${msg.type}`}>
<div className="msg-body">
{msg.content.split('\n').map((line, j) => {
const urlRegex = /(https?:\/\/[^\s]+)/g
if (urlRegex.test(line)) {
const parts = line.split(urlRegex)
return (
<span key={j}>
{parts.map((part, k) =>
urlRegex.test(part) ?
<a key={k} href={part} target="_blank" className="msg-link">{part}</a> :
part
)}
{j < msg.content.split('\n').length - 1 && <br />}
</span>
)
}
return (
<span key={j}>
{line}
{j < msg.content.split('\n').length - 1 && <br />}
</span>
)
})}
</div>
</div>
))}
</div>
{/* Input */}
{step === 'input' && (
<div className="chat-input-row">
<input
type="text"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="你想学什么?"
className="chat-field"
onKeyDown={(e) => e.key === 'Enter' && handleTopicSubmit()}
/>
<button onClick={handleTopicSubmit} className="chat-send" disabled={!topic.trim()}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
)}
{/* Quiz options */}
{step === 'quiz' && questions[currentQIndex] && (
<div className="chat-quiz">
<div className="quiz-opts">
{questions[currentQIndex].options?.map(opt => (
<button
key={opt.key}
onClick={() => handleAnswer(opt.key)}
className="quiz-opt"
>
<span className="opt-char">{opt.key}</span>
<span className="opt-label">{opt.text}</span>
</button>
))}
</div>
</div>
)}
{/* Generate button */}
{step === 'results' && results.length > 0 && (
<div className="chat-action">
<button onClick={handleGenerateDoc} className="act-gen">
✨ 生成完整学习路径文档
</button>
</div>
)}
{/* Reset */}
{step === 'done' && (
<div className="chat-action">
<button onClick={resetFlow} className="act-reset">
重新开始 ↻
</button>
</div>
)}
</div>
</section>
{/* Topic chips */}
{step === 'input' && (
<div className="chips-area">
<div className="chips-label">试试这些</div>
<div className="chips-row">
{displayTopics.map((t, i) => (
<button
key={t}
onClick={() => setTopic(t)}
className="chip"
style={{ animationDelay: `${i * 60}ms` }}
>
{t}
</button>
))}
</div>
</div>
)}
{/* Footer */}
<footer className="site-footer">
<span>Study Reviver</span>
<span className="f-dot">·</span>
<a href="https://waytoagi.feishu.cn/wiki/QPe5w5g7UisbEkkow8XcDmOpn8e">WaytoAGI 知识库</a>
</footer>
</main>
<style>{`
:root {
--bg: #f6f5f2;
--bg-warm: #f0eeea;
--surface: #fffefc;
--text: #181818;
--text-sec: #6b6560;
--text-muted: #9e9790;
--accent: #c45d3e;
--accent-h: #a84d32;
--accent-sub: rgba(196, 93, 62, 0.05);
--ok: #2a7a6e;
--ok-h: #1f5f55;
--brd: rgba(0,0,0,0.06);
--brd-s: rgba(0,0,0,0.1);
--sh-sm: 0 1px 2px rgba(0,0,0,0.03);
--sh-md: 0 4px 12px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.03);
--sh-lg: 0 12px 32px rgba(0,0,0,0.07), 0 4px 8px rgba(0,0,0,0.03);
--r: 10px;
--r-lg: 14px;
--r-xl: 18px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
.page-root {
min-height: 100vh;
background: var(--bg);
font-family: 'Manrope', -apple-system, sans-serif;
color: var(--text);
position: relative;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Background layers */
.bg-base {
position: fixed;
inset: 0;
z-index: 0;
background: var(--bg);
}
.bg-gradient {
position: fixed;
inset: 0;
z-index: 1;
background:
radial-gradient(ellipse 65% 45% at 5% 10%, rgba(196,93,62,0.06) 0%, transparent 55%),
radial-gradient(ellipse 45% 65% at 95% 90%, rgba(42,122,110,0.04) 0%, transparent 45%),
radial-gradient(ellipse 35% 35% at 50% 50%, rgba(240,238,234,0.4) 0%, transparent 65%);
animation: gradShift 30s ease-in-out infinite alternate;
}
@keyframes gradShift {
0% { transform: translate(0,0) scale(1); }
33% { transform: translate(1.5%,-1%) scale(1.015); }
66% { transform: translate(-1%,1.5%) scale(0.985); }
100% { transform: translate(0.5%,-0.5%) scale(1.005); }
}
.bg-grain {
position: fixed;
inset: 0;
z-index: 2;
pointer-events: none;
opacity: 0.025;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 200px 200px;
background-repeat: repeat;
}
/* Floating decorations */
.float-el {
position: fixed;
z-index: 3;
pointer-events: none;
}
.float-circle {
top: 6%;
right: 10%;
width: 100px;
height: 100px;
border: 1px solid rgba(196,93,62,0.08);
border-radius: 50%;
animation: f1 22s ease-in-out infinite;
}
.float-diamond {
bottom: 12%;
left: 6%;
width: 60px;
height: 60px;
border: 1px solid rgba(42,122,110,0.06);
transform: rotate(45deg);
animation: f2 18s ease-in-out infinite;
}
.float-line {
top: 35%;
left: 12%;
width: 1px;
height: 60px;
background: linear-gradient(to bottom, transparent, rgba(196,93,62,0.12), transparent);
animation: f3 14s ease-in-out infinite;
}
@keyframes f1 {
0%,100% { transform: translate(0,0); }
50% { transform: translate(-8px,6px); }
}
@keyframes f2 {
0%,100% { transform: rotate(45deg) translate(0,0); }
50% { transform: rotate(45deg) translate(4px,-6px); }
}
@keyframes f3 {
0%,100% { transform: translateY(0) scaleY(1); opacity: 0.6; }
50% { transform: translateY(-8px) scaleY(1.15); opacity: 1; }
}
/* Main wrapper */
.main-wrap {
position: relative;
z-index: 4;
max-width: 660px;
margin: 0 auto;
padding: 0 20px;
}
.main-wrap.in .site-header { opacity: 1; transform: translateY(0); }
.main-wrap.in .chat-panel { opacity: 1; transform: translateY(0); transition-delay: 0.12s; }
.main-wrap.in .chips-area { opacity: 1; transform: translateY(0); transition-delay: 0.24s; }
.main-wrap.in .site-footer { opacity: 1; transition-delay: 0.36s; }
.site-header, .chat-panel, .chips-area {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.7s cubic-bezier(0.16,1,0.3,1), transform 0.7s cubic-bezier(0.16,1,0.3,1);
}
.site-footer {
opacity: 0;
transition: opacity 0.8s ease 0.36s;
}
/* Header */
.site-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 56px 0 40px;
gap: 28px;
}
.header-left { flex: 1; }
.brand-tag {
font-size: 0.65rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 700;
margin-bottom: 10px;
}
.hero-title {
font-family: 'Instrument Serif', Georgia, serif;
font-size: clamp(2rem, 4.5vw, 2.8rem);
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.025em;
color: var(--text);
}
.title-em {
font-style: italic;
color: var(--accent);
}
.header-right {
text-align: right;
min-width: 160px;
}
.hero-desc {
font-size: 0.85rem;
color: var(--text-sec);
line-height: 1.5;
margin-bottom: 16px;
font-weight: 400;
}
.stats-row {
display: flex;
align-items: center;
gap: 10px;
justify-content: flex-end;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.stat-val {
font-size: 0.8rem;
font-weight: 800;
color: var(--text);
letter-spacing: -0.03em;
line-height: 1;
}
.stat-label {
font-size: 0.58rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 3px;
}
.stat-sep {
width: 1px;
height: 20px;
background: var(--brd-s);
}
/* Chat panel */
.chat-panel {
opacity: 0;
transform: translateY(16px);
}
.chat-box {
background: var(--surface);
border-radius: var(--r-xl);
box-shadow: var(--sh-lg);
border: 1px solid var(--brd);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 400px;
max-height: 580px;
}
.chat-scroll {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
scrollbar-width: thin;
scrollbar-color: var(--brd) transparent;
}
.chat-scroll::-webkit-scrollbar { width: 3px; }
.chat-scroll::-webkit-scrollbar-track { background: transparent; }
.chat-scroll::-webkit-scrollbar-thumb { background: var(--brd); border-radius: 2px; }
/* Messages */
.chat-msg {
display: flex;
animation: msgIn 0.35s cubic-bezier(0.16,1,0.3,1);
}
@keyframes msgIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.chat-bot { justify-content: flex-start; }
.chat-user { justify-content: flex-end; }
.msg-body {
max-width: 78%;
padding: 11px 15px;
border-radius: 15px;
line-height: 1.55;
font-size: 0.85rem;
white-space: pre-wrap;
}
.chat-bot .msg-body {
background: var(--bg-warm);
color: var(--text);
border-bottom-left-radius: 5px;
border: 1px solid var(--brd);
}
.chat-user .msg-body {
background: var(--text);
color: #fff;
border-bottom-right-radius: 5px;
}
.msg-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
word-break: break-all;
}
.chat-user .msg-link {
color: #7dd3fc;
}
/* Input row */
.chat-input-row {
padding: 16px 24px;
border-top: 1px solid var(--brd);
display: flex;
gap: 10px;
align-items: center;
background: var(--surface);
}
.chat-field {
flex: 1;
padding: 10px 0;
border: none;
font-size: 0.85rem;
font-family: inherit;
color: var(--text);
background: transparent;
outline: none;
}
.chat-field::placeholder { color: var(--text-muted); }
.chat-send {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.chat-send:disabled { opacity: 0.35; cursor: not-allowed; }
.chat-send:not(:disabled):hover { background: var(--accent-h); transform: scale(1.06); }
/* Quiz */
.chat-quiz {
padding: 0 24px 20px;
}
.quiz-opts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 7px;
}
.quiz-opt {
display: flex;
align-items: center;
gap: 9px;
padding: 11px 12px;
background: var(--bg);
border: 1.5px solid var(--brd);
border-radius: var(--r);
font-size: 0.78rem;
font-family: inherit;
color: var(--text);
cursor: pointer;
text-align: left;
line-height: 1.35;
transition: all 0.18s;
}
.quiz-opt:hover {
border-color: var(--accent);
background: var(--accent-sub);
}
.opt-char {
width: 18px;
height: 18px;
border-radius: 5px;
background: var(--surface);
border: 1px solid var(--brd-s);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 700;
color: var(--text-sec);
flex-shrink: 0;
transition: all 0.18s;
}
.quiz-opt:hover .opt-char {
border-color: var(--accent);
color: var(--accent);
}
/* Action area */
.chat-action {
padding: 14px 24px 20px;
border-top: 1px solid var(--brd);
}
.act-gen {
width: 100%;
padding: 13px;
background: var(--ok);
color: #fff;
border: none;
border-radius: var(--r);
font-size: 0.84rem;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
}
.act-gen:hover { background: var(--ok-h); transform: translateY(-1px); }
.act-reset {
background: none;
border: none;
color: var(--text-muted);
font-size: 0.82rem;
font-family: inherit;
cursor: pointer;
padding: 6px 0;
transition: color 0.2s;
}
.act-reset:hover { color: var(--text); }
/* Chips */
.chips-area {
margin-top: 28px;
opacity: 0;
transform: translateY(10px);
}
.chips-label {
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 700;
margin-bottom: 10px;
text-align: center;
}
.chips-row {
display: flex;
gap: 7px;
justify-content: center;
flex-wrap: wrap;
}
.chip {
padding: 6px 14px;
background: var(--surface);
border: 1px solid var(--brd);
border-radius: 100px;
font-size: 0.78rem;
font-family: inherit;
color: var(--text-sec);
cursor: pointer;
transition: all 0.22s;
animation: chipIn 0.5s ease both;
}
@keyframes chipIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.chip:hover {
border-color: var(--accent);
color: var(--accent);
background: var(--accent-sub);
}
/* Footer */
.site-footer {
text-align: center;
padding: 36px 0 44px;
font-size: 0.72rem;
color: var(--text-muted);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
}
.site-footer a {
color: var(--text-sec);
text-decoration: none;
transition: color 0.2s;
}
.site-footer a:hover { color: var(--text); }
.f-dot { opacity: 0.4; }
/* Responsive */
@media (max-width: 600px) {
.site-header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
padding: 44px 0 28px;
}
.header-right { text-align: left; }
.stats-row { justify-content: flex-start; }
.quiz-opts { grid-template-columns: 1fr; }
.chat-box { min-height: 360px; }
}
`}</style>
</div>
)
}
[
{
"id": "ai-beginner",
"emoji": "🌱",
"title": "零基础 AI 入门",
"subtitle": "不会编程也没关系。5 天,每天 1 小时",
"audience": "对 AI 完全没概念的纯小白",
"duration": "5天 × 1小时/天",
"docUrl": "https://www.feishu.cn/docx/IoR9dngXtoiLN5xUpI1ca11XnPc",
"tags": ["零基础", "入门", "AI工具", "不会编程"],
"days": [
{"day": 1, "title": "AI 到底是什么?", "desc": "一文打通 AI 底层逻辑"},
{"day": 2, "title": "动手试试 AI 工具", "desc": "即梦 AI 基础操作入门"},
{"day": 3, "title": "AI 能帮你做什么", "desc": "AI 产品经理入门指南"},
{"day": 4, "title": "加入社区一起学", "desc": "WAYtoAGI 学习路径与课程"},
{"day": 5, "title": "试试 AI 编程", "desc": "n8n 零基础入门,拖拽搭工作流"}
]
},
{
"id": "claude-code",
"emoji": "💻",
"title": "一周入门 Claude Code",
"subtitle": "会一点 Python,每天 2 小时",
"audience": "想用 AI 写代码提效的开发者",
"duration": "7天 × 2小时/天",
"docUrl": "https://www.feishu.cn/docx/CflwdhZc2omYpyxRyovckPj8nbf",
"tags": ["Claude Code", "编程", "Python", "开发者"],
"days": [
{"day": 1, "title": "搞懂底层逻辑", "desc": "从 LLM 到 Agent 一次打通"},
{"day": 2, "title": "安装 + 跑通", "desc": "保姆级入门,不学命令行版"},
{"day": 3, "title": "5 个必学技巧", "desc": "3 条铁律告别代码屎山"},
{"day": 4, "title": "做一个真实项目", "desc": "文科生小白也能上手"},
{"day": 5, "title": "插件和高效工作流", "desc": "5 分钟上手 Claude Code 插件"},
{"day": 6, "title": "看直播 + 案例", "desc": "Claude Code 系列直播课"},
{"day": 7, "title": "总结 + 下一步", "desc": "10 万字中文教程索引"}
]
},
{
"id": "ai-product",
"emoji": "🚀",
"title": "AI 产品创业者",
"subtitle": "有想法没技术,2 周做出 MVP",
"audience": "想做自己的 AI 产品的创业者",
"duration": "2周 × 5小时/周",
"docUrl": "https://www.feishu.cn/docx/NgVadG77go3VWJxlpfQcQvQnnxf",
"tags": ["产品", "创业", "MVP", "商业"],
"days": [
{"day": 1, "title": "Week 1: 理解 AI 产品逻辑", "desc": "拆解成功案例,学产品设计"},
{"day": 2, "title": "Week 2: 动手做 MVP", "desc": "用 Claude Code 做出来"}
]
},
{
"id": "agent-arch",
"emoji": "🧠",
"title": "Agent 架构深度学习",
"subtitle": "有编程基础,2 周深入多智能体",
"audience": "想深入理解 Agent 架构的技术人",
"duration": "2周 × 2小时/天",
"docUrl": "https://www.feishu.cn/docx/RP93dlkN1oNah3xcagWcZoIDnle",
"tags": ["Agent", "多智能体", "架构", "Harness"],
"days": [
{"day": 1, "title": "Week 1: Agent 基础概念", "desc": "核心组件 + 8 个等级 + 5 种协作模式"},
{"day": 2, "title": "Week 2: Harness 工程实战", "desc": "工具设计 + 搭多 Agent 系统"}
]
},
{
"id": "community-ops",
"emoji": "📣",
"title": "社区运营 AI 提效",
"subtitle": "管群做活动写内容,5 天让 AI 帮你",
"audience": "社区运营者",
"duration": "5天 × 1小时/天",
"docUrl": "https://www.feishu.cn/docx/Ye42dtcuxokjmbxRyr6cLKLNnkD",
"tags": ["运营", "社区", "自动化", "效率"],
"days": [
{"day": 1, "title": "AI 能帮运营做什么", "desc": "34 个飞书 CLI 玩法"},
{"day": 2, "title": "整理会议纪要", "desc": "Study Reviver + Live Replay"},
{"day": 3, "title": "搭自动化工作流", "desc": "n8n 零基础入门"},
{"day": 4, "title": "用 AI 做内容", "desc": "Claude Code 保姆级入门"},
{"day": 5, "title": "规划 AI 运营体系", "desc": "飞书 CLI Best Practice"}
]
}
]
Hermes Agent 学习计划
基于 WaytoAGI 知识库 10000+ 篇内容,为你定制 Hermes Agent 专属学习路径
生成时间:2026-04-15 | 数据来源:飞书知识库共学纪要 + 技术教程
---
学习路径总览
| 阶段 | 主题 | 时长 | 难度 |
|---|---|---|---|
| Week 1 Day 1-2 | 认识 Hermes:底层逻辑与安装 | 2小时 | 入门 |
| Week 1 Day 3-4 | 核心配置:灵魂、皮肤、记忆 | 2小时 | 入门 |
| Week 1 Day 5-7 | 能力扩展:搜索、浏览器、Sandbox | 3小时 | 进阶 |
| Week 2 | 技能系统:Skill 创建与进化 | 4小时 | 进阶 |
| Week 3 | 多 Agent 与自动化:Cron、Hooks、Worker | 4小时 | 高级 |
| Week 4 | 实战项目:从 0 到 1 搭建你的 Agent | 6小时 | 高级 |
---
第 1 周:基础篇
Day 1-2:认识 Hermes Agent
目标: 理解 Hermes 是什么,为什么它能自我进化
知识点: 1. Hermes vs 其他 Agent -- 最大差异:Hermes 会从经验中学习,自己创建 Skill,能力持续增长 2. 核心架构 -- 内置记忆层 + Skill 系统 + Cron 定时任务 + Hook 自动化 3. 安装部署 -- 环境检查(Python/uv/Git),安装 Hermes,配置 .env
关键概念:
- SOUL.md:定义 Agent 的人格和行为准则
- Skill 系统:Agent 的能力模块,可自动创建和进化
- Memory 三层架构:内置记忆 -> Skill 记忆 -> 外挂知识库
实践:
# 安装 Hermes
curl -fsSL https://raw.githubusercontent.com/hermes-agent/install/main/install.sh | bash
# 检查安装
hermes --version
# 配置 API Key
echo "OPENROUTER_API_KEY=sk-..." >> ~/.hermes/.envDay 3-4:核心配置
目标: 完成个性化配置,让 Hermes 成为你的专属助手
知识点: 1. 灵魂定义 (SOUL.md) -- 思考模式、自我约束、输出纪律 2. TUI 皮肤 -- /skin ares, /skin podeidon, /skin sisyphus, /skin charizard 3. 记忆系统调优 -- MEMORY.md 大小调整,memory_char_limit 配置 4. YOLO 模式 -- 快速决策模式 vs 深思熟虑模式 5. 对话管理 -- 重置对话 & 恢复对话
实践:
# 编辑灵魂文件
nano ~/.hermes/SOUL.md
# 调整记忆限制
# 在 ~/.hermes/config.yaml 中设置 memory_char_limit: 4000
# 切换皮肤
/skin podeidonDay 5-7:能力扩展
目标: 解锁 Hermes 的全部能力
知识点: 1. 浏览器反爬 -- 配置 Camofox 或 Browserbase 2. Web Search -- 配置搜索引擎,让 Hermes 能联网搜索 3. Sandbox 沙箱 -- Docker 沙箱模式,安全执行代码 4. 辅助进程 (Auxiliary) -- 上下文压缩阈值调整(默认 50%)
实践:
# 配置 web search
# 在 ~/.hermes/config.yaml 中添加搜索引擎配置
# 配置 sandbox
# 安装 Docker,设置 sandbox 模式---
第 2 周:进阶篇
Skill 系统深度使用
目标: 掌握 Skill 创建、编辑和进化
知识点: 1. Skill 目录结构 -- SKILL.md + references/ + templates/ + scripts/ + assets/ 2. Skill 创建时机 -- 复杂任务成功后、解决困难错误后、发现新工作流后 3. Skill 维护 -- 加载后发现过时/不完整立即 patch 4. 技能复用 -- 从经验中提取通用能力注入 Skill
实践流程:
任务失败/成功 -> 总结经验 -> 创建/更新 Skill -> 下次自动加载---
第 3 周:高级篇
多 Agent 与自动化
知识点: 1. 多 Agent 协作 -- Worker 分身,用便宜模型处理简单任务 2. Cron 定时任务 -- 每日自动播报、数据报告生成 3. Hook 自动化 -- 事件通知处理,与 Plugin 互补 4. 飞书集成 -- 飞书 CLI + Hermes,完全体飞书助手 5. 微信接入 -- 原生支持,扫码即用 6. 备份策略 -- sessions 备份,push 到 GitHub 私有仓库
实践:
# 配置 Worker 分身
# 设置 Worker 为 Docker 沙箱模式
# 创建定时任务
# 每天 09:00 自动生成数据报告---
第 4 周:实战篇
从 0 到 1 搭建你的 Agent
实战项目(任选一个): 1. 全自动知识库助手 -- 定期从知识库提取新内容,生成摘要发到群里 2. 代码开发助手 -- 配置 Claude Code 集成,辅助日常开发 3. 个人知识管家 -- 用 Obsidian + Hermes 搭建第二大脑 4. 社区运营 Agent -- 自动整理会议纪要、生成学习卡片、群内互动
验收标准:
- Agent 能独立完成一个完整工作流
- 不需要人工干预中间步骤
- 有自动错误处理和 Skill 沉淀机制
---
推荐学习资源
| 资源 | 说明 |
|---|---|
| Hermes 7个冷知识 | 隐藏功能和效率技巧 |
| 安装后建议尝试的10件事 | 源码review验证的实操清单 |
| Windows 安装教程 | 完整部署指南+高频坑点解决 |
| Hermes vs OpenClaw 对比 | 全维度对比,帮你选型 |
| 飞书 CLI 集成教程 | 5分钟保姆级教程 |
| 微信接入教程 | 扫码即用,5分钟上手 |
| 公众号文章爬取 | Hermes 稳定爬取教程 |
---
学习捷径(来自社区经验)
1. 不要一开始就配外挂记忆 -- 先用内置三层记忆,不够再加 2. memory_char_limit 调到 4000 -- 默认值容易被打满 3. 先跑通10件事再深入学习 -- 安装后建议先尝试10项功能 4. 每次失败都沉淀为 Skill -- 这是 Hermes 最核心的进化机制 5. 备份 sessions 和 profile -- push 到 GitHub 私有仓库
---
本计划由 Study Reviver 自动生成,数据来源:WaytoAGI 知识库 399 篇共学纪要 + 37 个知识板块
const FEISHU_APP_ID = process.env.FEISHU_APP_ID
const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET
const WIKI_SPACE_ID = process.env.FEISHU_WIKI_SPACE_ID || '7226178700923011075'
let cachedToken: string | null = null
let tokenExpireTime = 0
async function getTenantToken(): Promise<string> {
if (cachedToken && Date.now() < tokenExpireTime) return cachedToken
console.log('Fetching Feishu tenant token...')
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: FEISHU_APP_ID,
app_secret: FEISHU_APP_SECRET
})
})
const text = await res.text()
let data
try { data = JSON.parse(text) } catch { throw new Error('Token fetch failed: Invalid JSON') }
if (data.code !== 0) throw new Error(`Token fetch failed: ${data.msg}`)
cachedToken = data.tenant_access_token
tokenExpireTime = Date.now() + (data.expire - 300) * 1000
return cachedToken
}
async function fetchWikiNodes(spaceId: string, parentToken: string = '', pageToken: string = '') {
const token = await getTenantToken()
const url = new URL(`https://open.feishu.cn/open-apis/wiki/v2/spaces/${spaceId}/nodes`)
// Feishu API logic:
// To get root nodes: Do NOT pass parent_node_token.
// To get children: Pass parent_node_token.
// Pagination: Pass page_token (if available from previous response).
if (parentToken) {
url.searchParams.set('parent_node_token', parentToken)
}
// If it's the first page, page_token is usually empty/undefined.
// If we have a token, use it.
if (pageToken) {
url.searchParams.set('page_token', pageToken)
}
url.searchParams.set('page_size', '50')
const res = await fetch(url.toString(), {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
})
const text = await res.text()
try { return JSON.parse(text) } catch { return { code: 1, data: { items: [], has_more: false } } }
}
async function crawlWikiNodes(spaceId: string, parentToken: string = '', depth: number = 0, maxDepth: number = 2): Promise<any[]> {
if (depth > maxDepth) return []
let allNodes: any[] = []
let pageToken = ''
let hasMore = true
let firstFetch = true
while (hasMore) {
const data = await fetchWikiNodes(spaceId, parentToken, firstFetch ? '' : pageToken)
firstFetch = false
if (data.code !== 0) {
console.warn('Failed to fetch nodes:', data.msg, data.code)
break
}
const items = data.data?.items || []
allNodes.push(...items)
hasMore = data.data?.has_more
pageToken = data.data?.page_token
if (hasMore) await new Promise(r => setTimeout(r, 200)) // Rate limit safety
}
// Recursively fetch children if they have any
// Only fetch children if we are not at max depth
if (depth < maxDepth) {
for (const node of allNodes) {
if (node.has_child) {
const children = await crawlWikiNodes(spaceId, node.node_token, depth + 1, maxDepth)
allNodes = [...allNodes, ...children]
await new Promise(r => setTimeout(r, 200)) // Rate limit safety between recursive calls
}
}
}
return allNodes
}
export async function searchDocs(query: string) {
try {
console.log(`Starting deep crawl for space: ${WIKI_SPACE_ID}, query: "${query}"`)
// Start crawling from root
const allNodes = await crawlWikiNodes(WIKI_SPACE_ID, '', 0, 2)
console.log(`Crawled ${allNodes.length} total nodes.`)
const lowerQuery = query.toLowerCase()
const keywords = query.includes(' OR ')
? query.split(' OR ').map(k => k.trim().toLowerCase())
: [lowerQuery]
// Filter nodes that match keywords in title
const matched = allNodes.filter((item: any) => {
const title = (item.title || '').toLowerCase()
return keywords.some(k => title.includes(k))
})
console.log(`Matched ${matched.length} nodes.`)
return matched.map((item: any, i: number) => ({
id: i,
title: item.title || '未命名文档',
url: item.url || `https://waytoagi.feishu.cn/wiki/${item.node_token}`,
update: item.obj_edit_time ? new Date(parseInt(item.obj_edit_time) * 1000).toISOString().split('T')[0] : '',
summary: `类型: ${item.obj_type === 'docx' ? '文档' : '目录'} · 来自知识库`
}))
} catch (error) {
console.error('searchDocs error:', error)
throw error
}
}
export async function createDocWithContent(title: string, results: any[]) {
try {
const token = await getTenantToken()
console.log('Creating Feishu doc:', title)
const docRes = await fetch('https://open.feishu.cn/open-apis/docx/v1/documents', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
})
const docData = await docRes.json()
if (docData.code !== 0) throw new Error(`Create doc failed: ${docData.msg}`)
const docId = docData.data.document.document_id
const rootBlockId = docData.data.document.root_block_id
// Build content blocks
const blocks: any[] = [
{ block_type: 1, text: { elements: [{ text_run: { content: `📚 为你精选了 ${results.length} 篇相关文章:`, text_element_style: {} } }], style: {} } },
{ block_type: 1, text: { elements: [{ text_run: { content: '', text_element_style: {} } }], style: {} } } // Empty line
]
// Add each result
results.forEach((r: any, i: number) => {
blocks.push({
block_type: 2,
heading: { level: 2, elements: [{ text_run: { content: `${i + 1}. ${r.title}`, text_element_style: {} } }] }
})
blocks.push({
block_type: 4,
text: {
elements: [
{ text_run: { content: '🔗 链接: ', text_element_style: {} } },
{ text_run: { content: r.url, text_element_style: { link: { url: r.url } } } }
],
style: {}
}
})
if (r.summary) {
blocks.push({
block_type: 4,
text: { elements: [{ text_run: { content: `📝 ${r.summary}`, text_element_style: {} } }], style: {} }
})
}
blocks.push({ block_type: 1, text: { elements: [{ text_run: { content: '', text_element_style: {} } }], style: {} } })
})
// Append blocks
await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docId}/blocks/${rootBlockId}/children`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ children: blocks })
})
return `https://waytoagi.feishu.cn/docx/${docId}`
} catch (error) {
console.error('createDocWithContent error:', error)
throw error
}
}
MIT License
Copyright (c) 2026 Vivi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
/** @type {import('next').NextConfig} */
module.exports = { output: 'standalone' }
{
"name": "study-reviver",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/node": "25.6.0",
"@types/react": "19.2.14",
"typescript": "6.0.2"
}
}
🧠 WayToAGI Study Reviver — 社区共学纪要复活术
让 AI 读完 100 篇社区会议纪要,帮你出一套可互动的共学课程。

它解决什么问题
你运营一个学习社区,半年下来积累了 100+ 篇飞书会议纪要。然后呢?
- 新人进来不知道从哪看起
- 老成员不会回头翻
- 好内容没有二次传播
- 导师备课从零开始
这些内容是社区最有价值的资产,但正在沉睡。
它怎么工作
飞书知识库/文档 飞书 Base 飞书群/文档
┌──────────────┐ AI提取 ┌──────────────┐ 输出 ┌──────────────┐
│ 会议纪要 ×100 │ ──────→ │ 知识卡片表 │ ────→ │ 互动测验 │
│ 共学笔记 ×50 │ 结构化 │ 主题/难度/ │ │ 学习路径 │
│ 直播回顾 ×20 │ 知识点 │ 来源/人物 │ │ 每日播报 │
└──────────────┘ └──────────────┘ │ 精华回顾 │
└──────────────┘跨 4 个飞书域编排:lark-wiki → lark-doc → lark-base → lark-im
没有后端服务,没有数据库。lark-cli + AI = 全部。
快速开始
前置条件
# 安装 lark-cli (https://github.com/larksuite/cli)
npm install -g @anthropic-ai/lark-cli
# 认证
# https://github.com/larksuite/cli
# 认证(需要 docs/base/im/wiki 权限)
lark-cli auth login --domain docs,base,im,wiki用法
跟你的本地 Agent(Kiro CLI、Cursor、Claude Desktop、Codex 等)说:
帮我从飞书知识库里搜索所有标题包含"共学"或"读书会"的文档,
提取知识点写入飞书 Base,然后按"入门→进阶→高级"生成一份学习路径文档。Agent 会自动调用 SKILL.md 中定义的命令组合完成任务。
Skill 能力
| 能力 | 说明 | 输出 |
|---|---|---|
| 知识提取 | 从飞书文档批量提取结构化知识点 | 飞书 Base 知识卡片 |
| 互动测验 | 基于知识点生成选择题 | 飞书群消息 |
| 学习路径 | 按主题和难度串联知识点 | 飞书文档 |
| 每日播报 | 每天推送一张知识卡片 | 飞书群消息 |
| 精华回顾 | 会后自动提取精华 | 飞书文档 |
Use Cases
1. 社区新人入门加速
帮我从飞书知识库里找所有共学纪要,提取知识点,
按"入门→进阶→高级"生成一份学习路径文档。100 篇纪要 → 300+ 张知识卡片 → 1 份学习路径。新人 30 分钟看完,相当于参加了 6 个月的共学。
2. 导师陪你学直播
从知识卡片表里找"高级"难度、"Agent架构"主题的卡片,
整理成直播大纲,每个知识点配一个讨论问题。5 分钟生成直播大纲 + 讨论问题,导师不用从零准备。
3. 每周互动学习
从本周新增的知识卡片里挑 5 个,生成选择题,发到共学群里。群活跃度提升,成员在互动中复习知识。
4. 会议纪要二次宣传
读取上周三共学会的纪要,提取 3 个最有价值的知识点,
生成一篇"共学精华回顾"飞书文档,并发到社区群里。没参会的成员也能获取核心内容,纪要从"存档"变成"传播"。
5. 跨期知识串联
在知识卡片表里搜索所有跟"RAG"相关的卡片,按时间排序,
整理成一篇"RAG 认知演进史",标注每个阶段的关键转变。散落在 10 篇纪要里的讨论,变成一篇有时间线的认知演进文档。
真实数据
我们用 WAYtoAGI 社区的真实数据验证:
| 指标 | 之前 | 之后 |
|---|---|---|
| 新人入门时间 | 自己翻纪要 2-3 天 | 看学习路径 30 分钟 |
| 纪要二次阅读率 | <5% | 知识卡片被查阅 40%+ |
| 群互动频率 | 每周 1-2 条讨论 | 测验日 20+ 条回复 |
| 导师备课时间 | 2-3 小时翻纪要 | 5 分钟从 Base 筛选 |
技术架构
lark-cli 命令组合:
├── lark-wiki — 知识库节点遍历
├── lark-doc — 文档读取 (+fetch) 和创建 (+create, +update)
├── lark-base — 知识卡片存储和查询 (+record-upsert, +data-query)
├── lark-im — 群消息发送(测验/播报)(+messages-send)
└── lark-calendar — 可选:关联共学日程 (+agenda)为什么不需要后端?
| 需求 | lark-cli 命令 | 替代方案的问题 |
|---|---|---|
| 批量读纪要 | docs +search + docs +fetch | 手动复制粘贴? |
| 结构化存储 | base +record-upsert | 自建数据库太重 |
| 群互动 | im +messages-send | 手动发消息不可持续 |
| 生成文档 | docs +create | 手动排版效率低 |
与现有 Skill 的差异
| 维度 | 现有 Skill | Study Reviver |
|---|---|---|
| 数据流向 | 单域操作 | wiki → doc → base → im 四域串联 |
| 使用场景 | 个人效率 | 社区运营(服务一群人) |
| 价值主张 | 操作飞书更方便 | 让沉睡内容产生二次价值 |
| 创新点 | CLI 包装 | 知识复利——从工具到资产复用 |
不是"用 AI 操作飞书",是"用 AI 让飞书里的内容活过来"。
适用场景
- 学习社区(WAYtoAGI、各种共学群)
- 企业内训(培训纪要 → 可复用学习资产)
- 读书会(读书笔记 → 知识卡片和测验)
- 技术社区(技术分享 → 入门路径)
任何在飞书上积累了大量文档但"写完就沉"的组织。
文件结构
├── SKILL.md # Skill 定义(Agent 读这个)
├── README.md # 人类读这个
└── LICENSE # MIT知识库接入:双路径机制
| 路径 | 方式 | 优点 | 前提 |
|---|---|---|---|
| Plan A | docs +search | 精准搜索全知识库 | 需要知识库搜索权限 |
| Plan B | wiki nodes list + 关键词匹配 | 任何人都能用 | 只需阅读权限 |
Agent 优先尝试 Plan A,搜索返回空自动 fallback 到 Plan B。用户不需要关心。
依赖
- lark-cli >= 1.0.5
- 飞书应用权限:docs, base, im, wiki
- 任意本地 Agent(Kiro CLI、Cursor、Claude Desktop、Codex 等)
License
MIT
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
{
"functions": {
"app/api/search/route.ts": {
"maxDuration": 30
},
"app/api/generate-doc/route.ts": {
"maxDuration": 30
}
}
}
Related skills
How it compares
Interactive curator over the WaytoAGI corpus—not a generic web search skill or local course generator.
FAQ
Who is lark-study-reviver for?
Independent developers and learners using Lark who want structured WaytoAGI article recommendations after a friendly skills assessment, not a static reading list.
When should I use lark-study-reviver?
Say 我想学XX or equivalent in Idea/research when picking a topic; in Validate/scope when narrowing what to prototype; in Build/agent-tooling when upskilling on Hermes, AI coding, or AI art workflows.
Is lark-study-reviver safe to install?
It calls lark-cli against your authenticated workspace—review the Security Audits panel on this page, scope CLI domains minimally, and never paste secrets into the quiz flow.