{"id":50468016,"url":"https://github.com/ckaznable/towa","last_synced_at":"2026-06-01T08:32:45.791Z","repository":{"id":349186634,"uuid":"1196275679","full_name":"ckaznable/towa","owner":"ckaznable","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-15T06:26:00.000Z","size":310,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T08:38:22.679Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ckaznable.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T14:40:07.000Z","updated_at":"2026-05-15T06:26:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ckaznable/towa","commit_stats":null,"previous_names":["ckaznable/towa"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ckaznable/towa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ckaznable%2Ftowa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ckaznable%2Ftowa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ckaznable%2Ftowa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ckaznable%2Ftowa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ckaznable","download_url":"https://codeload.github.com/ckaznable/towa/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ckaznable%2Ftowa/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33767435,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T08:32:45.070Z","updated_at":"2026-06-01T08:32:45.781Z","avatar_url":"https://github.com/ckaznable.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Towa RSS Reader\n\nTowa 是一個單機自架的 RSS / Atom reader，包含：\n\n- Axum 後端 API 與背景 worker\n- Vite 8 + Vue 前端\n- SQLite 文章儲存\n- 來源排程抓取、HTTP cache-aware freshness 判斷\n- Gemini Batch API 文章後處理\n- 永久收藏與 30 天保留策略\n- 同時保存 feed `summary` 與 `content`\n\n## Requirements\n\n- Rust toolchain\n- Node.js 25+\n- npm 11+\n\n## Project Layout\n\n- `src/`: Axum API、scheduler、LLM worker、SQLite access\n- `migrations/`: SQLite schema migrations\n- `web/`: Vite 8 + Vue frontend\n- `scripts/dev.sh`: 本地同時啟動後端與前端的開發腳本\n- `openspec/changes/build-rss-reader-platform/`: OpenSpec artifacts\n\n## OpenSpec Coverage\n\n目前主 `openspec/specs/` 已涵蓋這些核心能力：\n\n- `feed-source-management`\n- `feed-fetch-scheduling`\n- `article-storage-retention`\n- `llm-agent-post-processing`\n- `reader-api-and-web-ui`\n- `article-favorites`\n\n## Runtime Paths\n\n預設使用 XDG 目錄：\n\n- Config: `~/.config/towa/config.toml`\n- Database: `~/.local/share/towa/towa.db`\n\n可用環境變數覆蓋：\n\n- `TOWA_CONFIG`\n- `TOWA_DB_PATH`\n- `GEMINI_API_KEY`\n- `PORT`\n\n## Config Example\n\n```toml\n[llm]\napi_key = \"optional-inline-key\"\nbatch_poll_interval_seconds = 300\nbatch_submit_size = 16\nretry_limit = 3\n\n[[llm.agents]]\nid = \"gemini-brief\"\nlabel = \"Gemini Brief\"\nprovider = \"gemini\"\nmodel = \"gemini-2.5-flash\"\nsystem_prompt = \"Write a concise 3-5 sentence summary for a reader.\"\nbatch_enabled = true\n\n[[llm.agents]]\nid = \"gemini-deep-tech\"\nlabel = \"Gemini Deep Tech\"\nprovider = \"gemini\"\nmodel = \"gemini-2.5-flash\"\nsystem_prompt = \"Explain technical points, risks, and practical next actions.\"\nbatch_enabled = true\n```\n\n## Development\n\n安裝前端依賴：\n\n```bash\ncd web\nnpm install\n```\n\n只跑後端：\n\n```bash\ncargo run\n```\n\n只跑前端：\n\n```bash\ncd web\nnpm run dev\n```\n\n一起跑：\n\n```bash\n./scripts/dev.sh\n```\n\n開發時：\n\n- Axum API 預設在 `http://127.0.0.1:3000`\n- Vite dev server 預設在 `http://127.0.0.1:5173`\n- `web/vite.config.ts` 已把 `/api` proxy 到 Axum\n\n## Build\n\n後端：\n\n```bash\ncargo build --release\n```\n\n前端：\n\n```bash\ncd web\nnpm run build\n```\n\n## Test\n\n後端測試：\n\n```bash\ncargo test\n```\n\n前端型別檢查與 production build：\n\n```bash\ncd web\nnpm run build\n```\n\n## Scheduler Behavior\n\n每個來源會保存：\n\n- `ETag`\n- `Last-Modified`\n- `last_fetch_at`\n- `next_fetch_at`\n\n下一次抓取時間計算順序：\n\n1. `Cache-Control: s-maxage` / `max-age`\n2. `Expires`\n3. fallback interval\n\n這讓 scheduler 優先尊重來源的 cache / expiry header，而不是固定頻率暴力抓取。\n\nfallback interval 目前邊界是：\n\n- 最短 `1` 小時\n- 最長 `6` 小時\n- fetch 失敗時以最短間隔重試\n\n## Article Retention\n\n- 一般文章：預設保留 30 天\n- 收藏文章：永久保留\n\n背景 scheduler 每輪會先執行 retention cleanup，再抓取到期來源。\n\n## Gemini Post-Processing\n\n- 來源可指派單一 agent\n- 新文章若有 agent 指派，會進入 `pending`\n- worker 預設走 Gemini Batch API\n- 狀態模型：`pending` / `processing` / `done` / `failed`\n- 成功結果寫入 `llm_summary`\n- 錯誤寫入 `llm_error`\n- 依 `retry_limit` 自動重試\n- 原始文章內容會同時保存 `summary` 與 `content`；若 feed 有完整 `content.body`，系統會優先保存到 `content`\n\n若未設定 `GEMINI_API_KEY`，HTTP API 與前端仍可使用，但 LLM worker 不會啟動。\n\n## Frontend Scope\n\n目前前端已包含：\n\n- dark mode dashboard\n- 全來源 stream 與 favorites 視圖\n- 來源管理：新增、編輯、啟用/停用、刪除、agent 指派\n- 文章清單與文章詳情閱讀\n- LLM 狀態、摘要、錯誤顯示\n- 收藏切換與永久收藏檢視\n\n前端文章流目前只顯示 `llm_status = done` 的項目；若文章需經過 LLM，排序與顯示時間以 `available_at` 為準，也就是 batch 完成回寫時間，而不是原始抓取時間。\n\n## Favorites API\n\n後端已把收藏提升成正式能力，前端與其他 client 應優先使用：\n\n- `GET /api/favorites`\n- `PUT /api/articles/{id}/favorite`\n- `GET /api/articles?favorited=true`\n\n相容性考量下，舊的 bookmark alias 仍保留：\n\n- `GET /api/bookmarks`\n- `PUT /api/articles/{id}/bookmark`\n- `GET /api/articles?bookmarked=true`\n\nresponse 目前同時回傳：\n\n- `favorited`: 正式收藏語意\n- `bookmarked`: 舊欄位 alias\n\n## Deployment Notes\n\n單機部署最簡單：\n\n1. 在 `web/` 執行 `npm run build`\n2. 啟動 Axum binary\n3. Axum 會直接提供 `web/dist`，且所有非 `/api` 路徑都會 fallback 到前端 `index.html`\n4. 提供 `~/.config/towa/config.toml` 與 `GEMINI_API_KEY`\n\n如果只想先驗證抓取與 API，可不提供 `GEMINI_API_KEY`。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fckaznable%2Ftowa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fckaznable%2Ftowa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fckaznable%2Ftowa/lists"}