尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Memo is a Telegram bot + mini-app that acts as an intelligent personal diary. You send it text or voice messages about anything — food, workouts, expenses, feelings, thoughts — and it automatically classifies, stores, and analyzes everything. A built-in Telegram Mini App gives you a dashboard, knowledge graph, and retrospective reports.
Bot side (Telegram):
/report, /report daily, /report monthly)Mini App side (Telegram WebApp):
Telegram User
│
▼
Telegram Bot API
│ webhook POST
▼
/api/telegram/webhook (Edge runtime, grammY)
│
├─ classify() ← Gemini 2.5 Flash (intent + metrics extraction)
├─ handleTextMessage()
│ ├─ answerQuestion() ← RAG: embed → pgvector search → Gemini answer
│ ├─ handleAction() ← delete entries / create widget
│ └─ generateConverseReply() ← Gemini conversational reply
│
├─ handleVoiceMessage()
│ └─ classifyAudio() ← Gemini multimodal (transcribe + classify)
│
└─ embedEntry() [async, non-blocking]
├─ generateEmbedding() ← gemini-embedding-001 (768-dim)
├─ findSimilarEntries() ← pgvector cosine similarity RPC
├─ generateInsight() ← Gemini insight from similar entries
└─ generateConversationalReply() ← pattern-aware reply
Supabase (PostgreSQL + pgvector)
├─ profiles
├─ entries (+ vector index HNSW)
├─ insights
├─ categories
├─ reports
├─ reminders
└─ todos
Vercel Cron Jobs
├─ /api/cron/process (daily 00:00 UTC)
│ ├─ clusterEntries() ← pgvector union-find clustering
│ ├─ buildAndSaveWidgets() ← Gemini widget summaries
│ └─ autoIncrementStreaks()
└─ /api/cron/reports (daily 09:00 UTC)
Telegram Mini App
└─ Next.js pages under /miniapp/*
├─ /miniapp → Feed
├─ /miniapp/dashboard → Dashboard
├─ /miniapp/graph → Knowledge Graph
├─ /miniapp/reports → Retrospectives
└─ /miniapp/settings → Passcode / Privacy
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Bot library | grammY |
| AI / LLM | Google Gemini 2.5 Flash (gemini-2.5-flash) |
| Embeddings | Google gemini-embedding-001 (768 dimensions) |
| Database | Supabase (PostgreSQL) |
| Vector search | pgvector (HNSW index, cosine similarity) |
| Auth | Supabase Auth (synthetic email/password per Telegram user) |
| Frontend | React 18, Tailwind CSS, shadcn/ui, Radix UI |
| Graph visualization | D3.js (force simulation) |
| Validation | Zod |
| Deployment | Vercel (Edge + Node.js runtimes) |
| Cron | Vercel Cron Jobs |
src/
├── app/
│ ├── api/
│ │ ├── auth/telegram/route.ts # Telegram initData verification → Supabase JWT
│ │ ├── categories/route.ts # User categories CRUD
│ │ ├── entries/route.ts # Entries GET / PATCH / DELETE
│ │ ├── graph/route.ts # Knowledge graph payload
│ │ ├── reports/route.ts # Retrospective reports CRUD + generation
│ │ ├── cron/
│ │ │ ├── process/route.ts # Daily processing cron
│ │ │ └── reports/route.ts # Daily report cron
│ │ └── telegram/webhook/route.ts # Telegram bot webhook
│ └── miniapp/
│ ├── layout.tsx # Auth + tab bar + passcode lock
│ ├── page.tsx # Feed (entry list)
│ ├── dashboard/page.tsx # Metrics dashboard
│ ├── graph/page.tsx # D3 knowledge graph
│ ├── reports/page.tsx # Retrospective reports
│ └── settings/page.tsx # Passcode settings
├── components/ui/ # shadcn/ui components + custom
│ ├── edit-drawer.tsx # Slide-up entry editor
│ ├── lock-button.tsx # Manual lock trigger
│ └── passcode-screen.tsx # 4-digit PIN screen
└── lib/
├── bot/
│ ├── commands.ts # /start /help /report handlers
│ ├── conversational.ts # Pattern-aware reply (with similar entries)
│ ├── converse.ts # General conversational reply (tone learning)
│ ├── qa.ts # RAG question answering
│ ├── retrospective.ts # Report generation + DB persistence
│ └── handlers/
│ ├── action.ts # Action commands (delete, create widget)
│ ├── text.ts # Text message handler (main flow)
│ └── voice.ts # Voice message handler
├── processing/
│ ├── loop.ts # Clustering + streak auto-increment
│ └── widgets.ts # Dashboard widget generation
├── classifier.ts # Gemini classification (intent + metrics)
├── embedding.ts # Embedding generation + RAG pipeline trigger
├── env.ts # Zod-validated env vars (lazy proxy)
├── insight.ts # Insight generation + pgvector search
├── passcode.ts # PIN hash + lock timer (localStorage)
├── profile.ts # Supabase profile resolve/create
├── supabase/
│ ├── auth-context.tsx # React auth context (access token)
│ └── client.ts # Supabase browser client
└── utils.ts # cn(), sanitizeMarkdown()
supabase/migrations/ # Ordered SQL migrations
scripts/
└── set-webhook.ts # One-time webhook registration script
profilesStores one row per Telegram user. The id is the Supabase Auth UUID so RLS (auth.uid()) works for both bot and mini app.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | = Supabase Auth user UUID |
telegram_id | BIGINT UNIQUE | Telegram user ID |
username | TEXT | Telegram username |
settings | JSONB | Dashboard widgets, report schedule, pending deletes, etc. |
created_at / updated_at | TIMESTAMPTZ |
entriesCore diary entries. Every message that isn't a question, action, or smalltalk becomes an entry.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
content | TEXT | Cleaned entry text |
category | TEXT | Open-ended snake_case (thoughts, workout, sleep, etc.) |
metadata | JSONB | Structured data: dashboard_metrics[], goal_metrics[], expense amount/currency, food calories, bot_msg_id, etc. |
raw_media_url | TEXT | Reserved for future media attachments |
embedding | vector(768) | Gemini embedding for semantic search |
embedding_status | TEXT | pending / done / failed |
branch_id | UUID | Cluster ID assigned by the processing loop |
thread_id | UUID | Groups a conversation thread |
bot_reply | TEXT | Bot's reply stored alongside the entry |
reply_to_entry_id | UUID FK → entries | Thread parent link |
created_at / updated_at | TIMESTAMPTZ |
Indexes: user_id, category, branch_id, thread_id, reply_to_entry_id, HNSW vector index on embedding.
insightsAI-generated insights linking a new entry to semantically similar past entries.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
entry_id | UUID FK → entries | The entry that triggered this insight |
insight_text | TEXT | Generated insight text |
branch_id | UUID | Cluster this insight belongs to |
created_at | TIMESTAMPTZ |
categoriesPer-user category registry. Built-in categories are seeded on first use; new categories are created dynamically by the AI.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
name | TEXT | snake_case key (e.g. sleep, dreams) |
label_ua | TEXT | Human-readable label in user's language |
color | TEXT | Tailwind class string |
icon | TEXT | Lucide icon name |
reportsAI-generated agile retrospective reports.
| Column | Type | Notes |
|---|---|---|
id | UUID PK | |
user_id | UUID FK → profiles | |
period_type | TEXT | daily / weekly / monthly / custom |
period_from / period_to | TIMESTAMPTZ | Report date range |
content | TEXT | Full Telegram Markdown report |
summary | TEXT | 1-2 sentence TLDR |
went_well | TEXT | Retro section: what went well |
didnt_go_well | TEXT | Retro section: what didn't work |
start_stop_continue | TEXT | Retro section: actions |
experiment | TEXT | Retro section: next experiment |
lesson | TEXT | Retro section: main lesson |
insights | JSONB | Structured insight objects array |
reminders / todosStandard reminder and todo tables with RLS. Used for future bot features.
find_similar_entriesfind_similar_entries(p_user_id, p_embedding, p_exclude_id, p_top_k)
→ TABLE(id, content, category, created_at, similarity)
Returns top-K entries ordered by cosine similarity (1 - (embedding <=> p_embedding)).
src/lib/classifier.ts)Every incoming message goes through classify() (text) or classifyAudio() (voice). Uses Gemini 2.5 Flash with a detailed system prompt that returns a strict JSON schema validated by Zod.
Output fields:
intent: save_entry | question | converse | smalltalk | actioncategory: open-ended snake_case stringis_new_category: whether to create a new category rowcontent: cleaned textmetadata: structured data (expense amount, food calories, etc.)dashboard_metrics[]: all measurable quantities extracted and derived (e.g. 5km run → distance_km, kcal_burned, active_min, steps_count)goal_metrics[]: stated goals/targets (e.g. "want to run 100km this month")action_type + action_params: for action intentsThe classifier applies decomposition intelligence — from a single input like "ran 5km" it derives 4+ metrics using built-in formulas (running: 1km ≈ 80 kcal, 6 min, 1200 steps). Nutrition entries get full macro breakdowns.
src/lib/embedding.ts)After saving an entry, embedEntry() runs asynchronously (non-blocking):
gemini-embedding-001 to generate a 768-dimensional vectorentries.embedding with embedding_status = 'done'findSimilarEntries() via pgvector RPCsrc/lib/bot/qa.ts)RAG pipeline for intent = "question":
gemini-embedding-001src/lib/bot/converse.ts)For all saved entries, a natural reply is generated using:
src/lib/bot/retrospective.ts)Generates agile-style retrospectives with 5 structured sections:
Uses all entries in the period with their metrics. Output is structured JSON with both a full Telegram Markdown report and individual section fields for the mini app UI.
Entry point: src/app/api/telegram/webhook/route.ts (Edge runtime)
The bot uses grammY with a middleware that resolves/creates the user profile before any handler runs.
Commands:
/start — welcome message with feature overview/help — full command reference/report [daily|weekly|monthly] — generate retrospective with rotating status messagesMessage flow (text):
question → RAG answer (shows "thinking" message, edits it with answer)smalltalk → conversational reply, no saveaction → execute (delete entries / create widget), no savesave_entry / converse → resolve thread → save entry → generate reply → embed asyncMessage flow (voice):
classifyAudio() — Gemini multimodal transcription + classification in one callThread system:
bot_msg_id in entry.metadatareply_to_message.message_idbot_msg_id to find the threadThe mini app is a Next.js app served under /miniapp/*. It runs inside Telegram as a WebApp.
Layout (src/app/miniapp/layout.tsx):
window.Telegram.WebApp.ready() and .expand()/api/auth/telegram using initDataFeed (/miniapp):
Dashboard (/miniapp/dashboard):
dashboard_metrics from all entries in rangekcal_intake and kcal_burned existgoal_metrics targetsGraph (/miniapp/graph):
/api/graphReports (/miniapp/reports):
Settings (/miniapp/settings):
localStorageAll mini app routes require Authorization: Bearer <supabase_jwt>.
| Method | Path | Runtime | Description |
|---|---|---|---|
| POST | /api/telegram/webhook | Edge | grammY webhook handler |
| POST | /api/auth/telegram | Edge | Verify Telegram initData → return Supabase JWT |
| GET | /api/entries | Edge | List entries (pagination, category, date range filters) |
| PATCH | /api/entries | Edge | Update entry content/category (re-computes metrics via Gemini) |
| DELETE | /api/entries | Edge | Bulk delete by IDs |
| GET | /api/graph | Edge | Knowledge graph nodes + edges |
| GET | /api/reports | Node.js | List reports |
| POST | /api/reports | Node.js | Generate new report (60s timeout) |
| DELETE | /api/reports | Node.js | Delete report |
| GET | /api/categories | Edge | List user categories |
| GET | /api/cron/process | Node.js | Daily processing (cron, requires CRON_SECRET) |
| GET | /api/cron/reports | Node.js | Daily report delivery (cron, requires CRON_SECRET) |
Daily cron at 00:00 UTC — /api/cron/process:
clusterEntries(userId) — For each entry with an embedding, calls find_similar_entries RPC to find neighbors (similarity > 0.75). Builds a union-find graph and assigns shared branch_id UUIDs to clusters of ≥ 3 entries. Updates both entries.branch_id and insights.branch_id.
buildAndSaveWidgets(userId) — Generates dashboard widget configs and saves them to profiles.settings.dashboard_widgets:
calories_today — sum of today's calorie entriesexpenses_today — sum by currencymood_trend — Gemini analysis of feeling entries (improving / stable / declining)life_theme — per-cluster theme label generated by Geminidaily_summary / weekly_summary / monthly_summary — narrative summariesinsight_cluster — summarized insight clustersautoIncrementStreaks(userId) — Finds entries from yesterday with aggregate=last streak metrics (keys ending in _days). If no entry exists today for that category, creates a new entry with value+1.
Daily cron at 09:00 UTC — /api/cron/reports:
Delivers scheduled reports to users who have enabled auto-reports in profiles.settings.report_schedule.
The system uses a synthetic Supabase Auth account per Telegram user:
Bot side: resolveOrCreateProfile() in src/lib/profile.ts creates a Supabase Auth user with email telegram_<id>@memo.app and a deterministic password. The profile id is set to the Supabase Auth UUID so auth.uid() in RLS policies matches entries.user_id.
Mini App side: /api/auth/telegram verifies the Telegram initData HMAC-SHA256 signature (with 24-hour expiry check), then signs in with the same synthetic credentials to get a real Supabase session JWT. This JWT is stored in React context and sent as Authorization: Bearer on all API calls.
RLS: All tables have Row Level Security enabled. Policies use auth.uid() to ensure users can only access their own data.
# Telegram
TELEGRAM_BOT_TOKEN= # Bot token from @BotFather
# Supabase
SUPABASE_URL= # https://<project>.supabase.co
SUPABASE_SERVICE_ROLE_KEY= # Service role key (server-side only)
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Anon key (used in browser/edge with user JWT)
# Google AI
GEMINI_API_KEY= # Google AI Studio API key
# Cron security
CRON_SECRET= # Secret for /api/cron/* routes
All server-side vars are validated at request time via a Zod schema in src/lib/env.ts (lazy proxy — fails fast on first use if missing).
The project is deployed on Vercel.
Connect your GitHub repo to Vercel. Framework preset: Next.js.
In Vercel project settings → Environment Variables, add all vars from the section above.
Vercel auto-deploys on push. The build runs next build.
After first deploy, run the webhook registration script once:
TELEGRAM_BOT_TOKEN=<token> WEBHOOK_URL=https://<your-domain>/api/telegram/webhook npx ts-node scripts/set-webhook.ts
Or call the Telegram API directly:
https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://<your-domain>/api/telegram/webhook
vector extension in the SQL editor: CREATE EXTENSION IF NOT EXISTS vector;supabase/migrations/In @BotFather:
/newapp or edit existing bothttps://<your-domain>/miniappDefined in vercel.json:
{
"crons": [
{ "path": "/api/cron/process", "schedule": "0 0 * * *" },
{ "path": "/api/cron/reports", "schedule": "0 9 * * *" }
]
}
Vercel automatically calls these with Authorization: Bearer <CRON_SECRET> (set in env vars).
# Install dependencies
npm install
# Copy env file and fill in values
cp .env.example .env.local
# Run dev server
npm run dev
For local bot testing, use ngrok to expose localhost and register the webhook:
ngrok http 3000
# Then set webhook to https://<ngrok-id>.ngrok.io/api/telegram/webhook
The mini app requires Telegram context (window.Telegram.WebApp.initData) to authenticate. For local UI development you can mock the auth context in src/lib/supabase/auth-context.tsx.
# Lint
npm run lint
# Build
npm run build