[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"similar-ridafkih--keeper.sh":3,"tool-ridafkih--keeper.sh":65},[4,23,32,40,49,57],{"id":5,"name":6,"github_repo":7,"description_zh":8,"stars":9,"difficulty_score":10,"last_commit_at":11,"category_tags":12,"status":22},2268,"ML-For-Beginners","microsoft\u002FML-For-Beginners","ML-For-Beginners 是由微软推出的一套系统化机器学习入门课程，旨在帮助零基础用户轻松掌握经典机器学习知识。这套课程将学习路径规划为 12 周，包含 26 节精炼课程和 52 道配套测验，内容涵盖从基础概念到实际应用的完整流程，有效解决了初学者面对庞大知识体系时无从下手、缺乏结构化指导的痛点。\n\n无论是希望转型的开发者、需要补充算法背景的研究人员，还是对人工智能充满好奇的普通爱好者，都能从中受益。课程不仅提供了清晰的理论讲解，还强调动手实践，让用户在循序渐进中建立扎实的技能基础。其独特的亮点在于强大的多语言支持，通过自动化机制提供了包括简体中文在内的 50 多种语言版本，极大地降低了全球不同背景用户的学习门槛。此外，项目采用开源协作模式，社区活跃且内容持续更新，确保学习者能获取前沿且准确的技术资讯。如果你正寻找一条清晰、友好且专业的机器学习入门之路，ML-For-Beginners 将是理想的起点。",84991,2,"2026-04-05T10:45:23",[13,14,15,16,17,18,19,20,21],"图像","数据工具","视频","插件","Agent","其他","语言模型","开发框架","音频","ready",{"id":24,"name":25,"github_repo":26,"description_zh":27,"stars":28,"difficulty_score":29,"last_commit_at":30,"category_tags":31,"status":22},3128,"ragflow","infiniflow\u002Fragflow","RAGFlow 是一款领先的开源检索增强生成（RAG）引擎，旨在为大语言模型构建更精准、可靠的上下文层。它巧妙地将前沿的 RAG 技术与智能体（Agent）能力相结合，不仅支持从各类文档中高效提取知识，还能让模型基于这些知识进行逻辑推理和任务执行。\n\n在大模型应用中，幻觉问题和知识滞后是常见痛点。RAGFlow 通过深度解析复杂文档结构（如表格、图表及混合排版），显著提升了信息检索的准确度，从而有效减少模型“胡编乱造”的现象，确保回答既有据可依又具备时效性。其内置的智能体机制更进一步，使系统不仅能回答问题，还能自主规划步骤解决复杂问题。\n\n这款工具特别适合开发者、企业技术团队以及 AI 研究人员使用。无论是希望快速搭建私有知识库问答系统，还是致力于探索大模型在垂直领域落地的创新者，都能从中受益。RAGFlow 提供了可视化的工作流编排界面和灵活的 API 接口，既降低了非算法背景用户的上手门槛，也满足了专业开发者对系统深度定制的需求。作为基于 Apache 2.0 协议开源的项目，它正成为连接通用大模型与行业专有知识之间的重要桥梁。",77062,3,"2026-04-04T04:44:48",[17,13,20,19,18],{"id":33,"name":34,"github_repo":35,"description_zh":36,"stars":37,"difficulty_score":29,"last_commit_at":38,"category_tags":39,"status":22},519,"PaddleOCR","PaddlePaddle\u002FPaddleOCR","PaddleOCR 是一款基于百度飞桨框架开发的高性能开源光学字符识别工具包。它的核心能力是将图片、PDF 等文档中的文字提取出来，转换成计算机可读取的结构化数据，让机器真正“看懂”图文内容。\n\n面对海量纸质或电子文档，PaddleOCR 解决了人工录入效率低、数字化成本高的问题。尤其在人工智能领域，它扮演着连接图像与大型语言模型（LLM）的桥梁角色，能将视觉信息直接转化为文本输入，助力智能问答、文档分析等应用场景落地。\n\nPaddleOCR 适合开发者、算法研究人员以及有文档自动化需求的普通用户。其技术优势十分明显：不仅支持全球 100 多种语言的识别，还能在 Windows、Linux、macOS 等多个系统上运行，并灵活适配 CPU、GPU、NPU 等各类硬件。作为一个轻量级且社区活跃的开源项目，PaddleOCR 既能满足快速集成的需求，也能支撑前沿的视觉语言研究，是处理文字识别任务的理想选择。",74913,"2026-04-05T10:44:17",[19,13,20,18],{"id":41,"name":42,"github_repo":43,"description_zh":44,"stars":45,"difficulty_score":46,"last_commit_at":47,"category_tags":48,"status":22},3215,"awesome-machine-learning","josephmisiti\u002Fawesome-machine-learning","awesome-machine-learning 是一份精心整理的机器学习资源清单，汇集了全球优秀的机器学习框架、库和软件工具。面对机器学习领域技术迭代快、资源分散且难以甄选的痛点，这份清单按编程语言（如 Python、C++、Go 等）和应用场景（如计算机视觉、自然语言处理、深度学习等）进行了系统化分类，帮助使用者快速定位高质量项目。\n\n它特别适合开发者、数据科学家及研究人员使用。无论是初学者寻找入门库，还是资深工程师对比不同语言的技术选型，都能从中获得极具价值的参考。此外，清单还延伸提供了免费书籍、在线课程、行业会议、技术博客及线下聚会等丰富资源，构建了从学习到实践的全链路支持体系。\n\n其独特亮点在于严格的维护标准：明确标记已停止维护或长期未更新的项目，确保推荐内容的时效性与可靠性。作为机器学习领域的“导航图”，awesome-machine-learning 以开源协作的方式持续更新，旨在降低技术探索门槛，让每一位从业者都能高效地站在巨人的肩膀上创新。",72149,1,"2026-04-03T21:50:24",[20,18],{"id":50,"name":51,"github_repo":52,"description_zh":53,"stars":54,"difficulty_score":46,"last_commit_at":55,"category_tags":56,"status":22},2234,"scikit-learn","scikit-learn\u002Fscikit-learn","scikit-learn 是一个基于 Python 构建的开源机器学习库，依托于 SciPy、NumPy 等科学计算生态，旨在让机器学习变得简单高效。它提供了一套统一且简洁的接口，涵盖了从数据预处理、特征工程到模型训练、评估及选择的全流程工具，内置了包括线性回归、支持向量机、随机森林、聚类等在内的丰富经典算法。\n\n对于希望快速验证想法或构建原型的数据科学家、研究人员以及 Python 开发者而言，scikit-learn 是不可或缺的基础设施。它有效解决了机器学习入门门槛高、算法实现复杂以及不同模型间调用方式不统一的痛点，让用户无需重复造轮子，只需几行代码即可调用成熟的算法解决分类、回归、聚类等实际问题。\n\n其核心技术亮点在于高度一致的 API 设计风格，所有估算器（Estimator）均遵循相同的调用逻辑，极大地降低了学习成本并提升了代码的可读性与可维护性。此外，它还提供了强大的模型选择与评估工具，如交叉验证和网格搜索，帮助用户系统地优化模型性能。作为一个由全球志愿者共同维护的成熟项目，scikit-learn 以其稳定性、详尽的文档和活跃的社区支持，成为连接理论学习与工业级应用的最",65628,"2026-04-05T10:10:46",[20,18,14],{"id":58,"name":59,"github_repo":60,"description_zh":61,"stars":62,"difficulty_score":10,"last_commit_at":63,"category_tags":64,"status":22},3364,"keras","keras-team\u002Fkeras","Keras 是一个专为人类设计的深度学习框架，旨在让构建和训练神经网络变得简单直观。它解决了开发者在不同深度学习后端之间切换困难、模型开发效率低以及难以兼顾调试便捷性与运行性能的痛点。\n\n无论是刚入门的学生、专注算法的研究人员，还是需要快速落地产品的工程师，都能通过 Keras 轻松上手。它支持计算机视觉、自然语言处理、音频分析及时间序列预测等多种任务。\n\nKeras 3 的核心亮点在于其独特的“多后端”架构。用户只需编写一套代码，即可灵活选择 TensorFlow、JAX、PyTorch 或 OpenVINO 作为底层运行引擎。这一特性不仅保留了 Keras 一贯的高层易用性，还允许开发者根据需求自由选择：利用 JAX 或 PyTorch 的即时执行模式进行高效调试，或切换至速度最快的后端以获得最高 350% 的性能提升。此外，Keras 具备强大的扩展能力，能无缝从本地笔记本电脑扩展至大规模 GPU 或 TPU 集群，是连接原型开发与生产部署的理想桥梁。",63927,"2026-04-04T15:24:37",[20,14,18],{"id":66,"github_repo":67,"name":68,"description_en":69,"description_zh":70,"ai_summary_zh":71,"readme_en":72,"readme_zh":73,"quickstart_zh":74,"use_case_zh":75,"hero_image_url":76,"owner_login":77,"owner_name":78,"owner_avatar_url":79,"owner_bio":80,"owner_company":81,"owner_location":82,"owner_email":83,"owner_twitter":77,"owner_website":84,"owner_url":85,"languages":86,"stars":113,"forks":114,"last_commit_at":115,"license":116,"difficulty_score":29,"env_os":117,"env_gpu":118,"env_ram":118,"env_deps":119,"category_tags":128,"github_topics":129,"view_count":29,"oss_zip_url":80,"oss_zip_packed_at":80,"status":22,"created_at":146,"updated_at":147,"faqs":148,"releases":177},468,"ridafkih\u002Fkeeper.sh","keeper.sh","Open-source calendar sync tool & universal calendar MCP server. Aggregate, sync and control calendars on Google, Outlook, Office 365, iCloud, CalDAV or ICS.","keeper.sh 是一个开源的日历同步工具，支持从 Google、Outlook、iCloud 等主流平台及 ICS 文件聚合日历事件，并通过统一的 MCP 协议服务器实现跨平台事件同步与管理。它解决了用户在多日历系统中手动同步导致的事件冲突、时间错位等问题，尤其适合需要同时管理个人、工作及业务日历的用户。\n\n对于开发者或自托管爱好者，keeper.sh 提供了基于 AGPL-3.0 协议的完整源码，支持本地部署与 HTTPS 安全环境搭建。其核心亮点包括：  \n1. **智能同步机制**：通过源-目标事件映射自动处理新增、删除与更新操作，避免冗余事件残留；  \n2. **MCP 协议支持**：为 AI 代理提供标准化的日历访问接口，便于集成自动化场景；  \n3. **跨平台兼容性**：覆盖主流日历服务及 CalDAV\u002FICS 格式，无需依赖特定厂商生态。  \n\n普通用户可通过图形界面快速配置同步规则，开发者则可利用其模块化架构（PostgreSQL 数据库 + Redis 缓存 + 微服务架构）进行二次开发。若你正被多平台日历同步的低效与不稳定性困扰，keeper.sh 提供了一个","keeper.sh 是一个开源的日历同步工具，支持从 Google、Outlook、iCloud 等主流平台及 ICS 文件聚合日历事件，并通过统一的 MCP 协议服务器实现跨平台事件同步与管理。它解决了用户在多日历系统中手动同步导致的事件冲突、时间错位等问题，尤其适合需要同时管理个人、工作及业务日历的用户。\n\n对于开发者或自托管爱好者，keeper.sh 提供了基于 AGPL-3.0 协议的完整源码，支持本地部署与 HTTPS 安全环境搭建。其核心亮点包括：  \n1. **智能同步机制**：通过源-目标事件映射自动处理新增、删除与更新操作，避免冗余事件残留；  \n2. **MCP 协议支持**：为 AI 代理提供标准化的日历访问接口，便于集成自动化场景；  \n3. **跨平台兼容性**：覆盖主流日历服务及 CalDAV\u002FICS 格式，无需依赖特定厂商生态。  \n\n普通用户可通过图形界面快速配置同步规则，开发者则可利用其模块化架构（PostgreSQL 数据库 + Redis 缓存 + 微服务架构）进行二次开发。若你正被多平台日历同步的低效与不稳定性困扰，keeper.sh 提供了一个轻量且可靠的开源解决方案。","![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fridafkih_keeper.sh_readme_74b3ff9502a5.png)\n\n# About\n\nKeeper is a simple & open-source calendar syncing tool. It allows you to pull events from remotely hosted iCal or ICS links, and push them to one or many calendars so the time slots can align across them all.\n\n# Features\n\n- Aggregating calendar events from remote sources\n- Event content agnostic syncing engine\n- Push aggregate events to one or more calendars\n- MCP (Model Context Protocol) server for AI agent calendar access\n- Open source under AGPL-3.0\n- Easy to self-host\n- Easy-to-purge remote events\n\n# Bug Reports & Feature Requests\n\nIf you encounter a bug or have an idea for a feature, you may [open an issue on GitHub](https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues) and it will be triaged and addressed as soon as possible.\n\n# Contributing\n\nHigh-value and high-quality contributions are appreciated. Before working on large features you intend to see merged, please open an issue first to discuss beforehand.\n\n## Local Development\n\nThe dev environment runs behind HTTPS at `https:\u002F\u002Fkeeper.localhost` using a [Caddy](https:\u002F\u002Fcaddyserver.com\u002F) reverse proxy with automatic TLS. The `.localhost` TLD resolves to `127.0.0.1` automatically per [RFC 6761](https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6761) — no `\u002Fetc\u002Fhosts` entry is needed.\n\n### Prerequisites\n\n- [Bun](https:\u002F\u002Fbun.sh\u002F) (v1.3.5+)\n- [Docker](https:\u002F\u002Fdocs.docker.com\u002Fget-started\u002F) & Docker Compose\n\n### Getting Started\n\n```bash\nbun install\n```\n\n#### Generate and Trust a Root CA\n\nThe dev environment runs behind HTTPS via Caddy. You need to generate a local root certificate authority and trust it so your browser accepts the certificate.\n\n```bash\nmkdir -p .pki\nopenssl req -x509 -new -nodes \\\n  -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\\n  -keyout .pki\u002Froot.key -out .pki\u002Froot.crt \\\n  -days 3650 -subj \"\u002FCN=Keeper.sh CA\"\n```\n\nThen trust it on your platform:\n\n**macOS**\n\n```bash\nsudo security add-trusted-cert -d -r trustRoot \\\n  -k \u002FLibrary\u002FKeychains\u002FSystem.keychain .pki\u002Froot.crt\n```\n\n**Linux**\n\n```bash\nsudo cp .pki\u002Froot.crt \u002Fusr\u002Flocal\u002Fshare\u002Fca-certificates\u002Fkeeper-dev-root.crt\nsudo update-ca-certificates\n```\n\n#### Start the Dev Environment\n\n```bash\nbun dev\n```\n\nThis starts PostgreSQL, Redis, and a Caddy reverse proxy via Docker Compose, along with the API, web, MCP, and cron services locally. Once running, open `https:\u002F\u002Fkeeper.localhost`.\n\n### Architecture\n\n| Service  | Local Port | Accessed Via                         |\n| -------- | ---------- | ------------------------------------ |\n| Caddy    | 443        | `https:\u002F\u002Fkeeper.localhost`           |\n| Web      | 5173       | Proxied by Caddy                     |\n| API      | 3000       | Proxied by Web at `\u002Fapi`             |\n| MCP      | 3001       | Proxied by Web at `\u002Fmcp`             |\n| Postgres | 5432       | `postgresql:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fpostgres` |\n| Redis    | 6379       | `redis:\u002F\u002Flocalhost:6379`             |\n\n# Qs\n\n## Why does this exist?\n\nBecause I needed it. Ever since starting [Sedna](https:\u002F\u002Fsedna.sh\u002F)—the AI governance platform—I've had to work across three calendars. One for my business, one for work, and one for personal.\n\nMeetings have landed on top of one-another a frustratingly high number of times.\n\n## Why not use _this other service_?\n\nI've probably tried it. It was probably too finicky, ended up making me waste hours of my time having to delete stale events it didn't seem to want to track anymore, or just didn't sync reliably.\n\n## How does the syncing engine work?\n\n- If we have a local event but no corresponding \"source → destination\" mapping for an event, we push the event to the destination calendar.\n- If we have a mapping for an event, but the source ID is not present on the source any longer, we delete the event from the destination.\n- Any events with markers of having been created by Keeper, but with no corresponding local tracking, we remove it. This is only done for backwards compatibility.\n\nEvents are flagged as having been created by Keeper either using a `@keeper.sh` suffix on the remote UID, or in the case of a platform like Outlook that doesn't support custom UIDs, we just put it in a `\"keeper.sh\"` category.\n\n# Considerations\n\n1. **Keeper tracks timeslots, not event details**, summaries, descriptions, etc., for now. If you need that I would recommend [OneCal](https:\u002F\u002Fonecal.io\u002F).\n2. **Keeper only sources from remote and publicly available iCal\u002FICS URLs** at the moment, so that means that if your security policy does not permit these, another solution may suit you better.\n3. **The MCP server provides read-only access** to calendar data. AI agents can list calendars and query events but cannot create, modify, or delete them.\n\n# Cloud Hosted\n\nI've made Keeper easy to self-host, but whether you simply want to support the project or don't want to deal with the hassle or overhead of configuring and running your own infrastructure cloud hosting is always an option.\n\nHead to [keeper.sh](https:\u002F\u002Fkeeper.sh) to get started with the cloud-hosted version. Use code `README` for 25% off.\n\n|                       | Free       | Pro (Cloud-Hosted) | Pro (Self-Hosted) |\n| --------------------- | ---------- | ------------------ | ----------------- |\n| **Monthly Price**     | $0 USD     | $5 USD             | $0                |\n| **Annual Price**      | $0 USD     | $42 USD (-30%)     | $0                |\n| **Refresh Interval**  | 30 minutes | 1 minute           | 1 minute          |\n| **Source Limit**      | 2          | ∞                  | ∞                 |\n| **Destination Limit** | 1          | ∞                  | ∞                 |\n\n# Self Hosted\n\nBy hosting Keeper yourself, you get all premium features for free, can guarantee data governance and autonomy, and it's fun. If you'll be self-hosting, please consider supporting me and development of the project by sponsoring me on GitHub.\n\nThere are seven images currently available, two of them are designed for convenience, while the five are designed to serve the granular underlying services.\n\n> [!NOTE]\n>\n> **Migrating from a previous version?** If you are upgrading from the older Next.js-based release, see the [migration guide](https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F140) for environment variable changes. The new web server will also print a migration notice at startup if it detects old environment variables.\n\n## Environment Variables\n\n| Name                           | Service(s)    | Description                                                                                                                                                         |\n| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DATABASE_URL                   | `api`, `cron`, `worker`, `mcp` | PostgreSQL connection URL.\u003Cbr>\u003Cbr>e.g. `postgres:\u002F\u002Fuser:pass@postgres:5432\u002Fkeeper`                                                                                  |\n| REDIS_URL                      | `api`, `cron`, `worker` | Redis connection URL. Must be the same Redis instance across all services.\u003Cbr>\u003Cbr>e.g. `redis:\u002F\u002Fredis:6379`                                                        |\n| WORKER_JOB_QUEUE_ENABLED       | `cron`        | Required. Set to `true` to enqueue sync jobs to the worker queue, or `false` to disable. If unset, the cron service will exit with a migration notice.              |\n| BETTER_AUTH_URL                | `api`, `mcp`  | The base URL used for auth redirects.\u003Cbr>\u003Cbr>e.g. `http:\u002F\u002Flocalhost:3000`                                                                                           |\n| BETTER_AUTH_SECRET             | `api`, `mcp`  | Secret key for session signing.\u003Cbr>\u003Cbr>e.g. `openssl rand -base64 32`                                                                                               |\n| API_PORT                       | `api`         | Port the Bun API listens on. Defaults to `3001` in container images.                                                                                                |\n| ENV                            | `web`         | Optional. Runtime environment. One of `development`, `production`, or `test`. Defaults to `production`.                                                             |\n| PORT                           | `web`         | Port the web server listens on. Defaults to `3000` in container images.                                                                                             |\n| VITE_API_URL                   | `web`         | The URL the web server uses to proxy requests to the Bun API.\u003Cbr>\u003Cbr>e.g. `http:\u002F\u002Fapi:3001`                                                                         |\n| COMMERCIAL_MODE                | `api`, `cron` | Enable Polar billing flow. Set to `true` if using Polar for subscriptions.                                                                                          |\n| POLAR_ACCESS_TOKEN             | `api`, `cron` | Optional. Polar API token for subscription management.                                                                                                              |\n| POLAR_MODE                     | `api`, `cron` | Optional. Polar environment, `sandbox` or `production`.                                                                                                             |\n| POLAR_WEBHOOK_SECRET           | `api`         | Optional. Secret to verify Polar webhooks.                                                                                                                          |\n| ENCRYPTION_KEY                 | `api`, `cron`, `worker` | Key for encrypting CalDAV credentials at rest.\u003Cbr>\u003Cbr>e.g. `openssl rand -base64 32`                                                                                |\n| RESEND_API_KEY                 | `api`         | Optional. API key for sending emails via Resend.                                                                                                                    |\n| PASSKEY_RP_ID                  | `api`         | Optional. Relying party ID for passkey authentication.                                                                                                              |\n| PASSKEY_RP_NAME                | `api`         | Optional. Relying party display name for passkeys.                                                                                                                  |\n| PASSKEY_ORIGIN                 | `api`         | Optional. Origin allowed for passkey flows (e.g., `https:\u002F\u002Fkeeper.example.com`).                                                                                    |\n| GOOGLE_CLIENT_ID               | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration.                                                                                                                 |\n| GOOGLE_CLIENT_SECRET           | `api`, `cron`, `worker` | Optional. Required for Google Calendar integration.                                                                                                                 |\n| MICROSOFT_CLIENT_ID            | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration.                                                                                                               |\n| MICROSOFT_CLIENT_SECRET        | `api`, `cron`, `worker` | Optional. Required for Microsoft Outlook integration.                                                                                                               |\n| POSTGRES_PASSWORD              | `standalone`  | Optional. Custom password for the internal PostgreSQL database in `keeper-standalone`. If unset, defaults to `keeper`. The database is not exposed outside the container, so this is low risk, but can be set for defense in depth. |\n| BLOCK_PRIVATE_RESOLUTION       | `api`, `cron` | Optional. Set to `true` to block outbound fetches (ICS subscriptions, CalDAV servers) from resolving to private\u002Freserved network addresses. Prevents SSRF. Defaults to `false` for backward compatibility with self-hosted setups that use local CalDAV\u002FICS servers. |\n| PRIVATE_RESOLUTION_WHITELIST          | `api`, `cron` | Optional. When `BLOCK_PRIVATE_RESOLUTION` is `true`, this comma-separated list of hostnames or IPs is exempt from the restriction.\u003Cbr>\u003Cbr>e.g. `192.168.1.50,radicale.local,10.0.2.12` |\n| TRUSTED_ORIGINS                | `api`         | Optional. Comma-separated list of additional trusted origins for CSRF protection.\u003Cbr>\u003Cbr>e.g. `http:\u002F\u002F192.168.1.100,http:\u002F\u002Fkeeper.local,https:\u002F\u002Fkeeper.example.com` |\n| MCP_PUBLIC_URL                 | `api`, `mcp`  | Optional. Public URL of the MCP resource. Enables OAuth on the API and identifies the MCP server to clients.\u003Cbr>\u003Cbr>e.g. `https:\u002F\u002Fkeeper.example.com\u002Fmcp`           |\n| VITE_MCP_URL                   | `web`         | Optional. Internal URL the web server uses to proxy `\u002Fmcp` requests to the MCP service.\u003Cbr>\u003Cbr>e.g. `http:\u002F\u002Fmcp:3002`                                              |\n| MCP_PORT                       | `mcp`         | Optional. Port the MCP server listens on.\u003Cbr>\u003Cbr>e.g. `3002`                                                                                                       |\n| OTEL_EXPORTER_OTLP_ENDPOINT    | `api`, `cron`, `worker`, `mcp`, `web` | Optional. When set, enables forwarding structured logs to an OpenTelemetry collector via [`pino-opentelemetry-transport`](https:\u002F\u002Fgithub.com\u002FVunovati\u002Fpino-opentelemetry-transport). The transport runs in a dedicated worker thread and does not affect application performance.\u003Cbr>\u003Cbr>e.g. `https:\u002F\u002Fotel-collector.example.com:4318` |\n| OTEL_EXPORTER_OTLP_PROTOCOL    | `api`, `cron`, `worker`, `mcp`, `web` | Optional. Protocol used by the OTLP exporter. Defaults to `http\u002Fprotobuf` per the OpenTelemetry spec.\u003Cbr>\u003Cbr>e.g. `http\u002Fprotobuf`, `grpc`, `http\u002Fjson` |\n| OTEL_EXPORTER_OTLP_HEADERS     | `api`, `cron`, `worker`, `mcp`, `web` | Optional. Headers sent with every OTLP export request. Use this for authentication (e.g. Basic auth or API keys).\u003Cbr>\u003Cbr>e.g. `Authorization=Basic dXNlcjpwYXNz` |\n\nThe following environment variables are baked into the web image at **build time**. They are pre-configured in the official Docker images and only need to be set if you are building from source.\n\n| Name                              | Description                                                        |\n| --------------------------------- | ------------------------------------------------------------------ |\n| VITE_COMMERCIAL_MODE              | Toggle commercial mode in the web UI (`true`\u002F`false`).             |\n| POLAR_PRO_MONTHLY_PRODUCT_ID      | Optional. Polar monthly product ID to power in-app upgrade links.  |\n| POLAR_PRO_YEARLY_PRODUCT_ID       | Optional. Polar yearly product ID to power in-app upgrade links.   |\n| VITE_VISITORS_NOW_TOKEN           | Optional. [visitors.now](https:\u002F\u002Fvisitors.now) token for analytics |\n| VITE_GOOGLE_ADS_ID                | Optional. Google Ads conversion tracking ID (e.g., `AW-123456789`) |\n| VITE_GOOGLE_ADS_CONVERSION_LABEL  | Optional. Google Ads conversion label for purchase tracking        |\n\n> [!NOTE]\n>\n> - `keeper-standalone` auto-configures everything internally — both the web server and Bun API sit behind a single Caddy reverse proxy on port `80`.\n> - `keeper-services` runs the web, API, cron, and worker services inside one container. The web server proxies `\u002Fapi` requests internally, so only port `3000` needs to be exposed.\n> - For individual images, only the `web` container needs to be exposed. The API is accessed internally via `VITE_API_URL`.\n\n## Images\n\n| Tag                        | Description                                                                                                                                              | Included Services                                                                        |\n| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `keeper-standalone:2.9`    | The \"standalone\" image is everything you need to get up and running with Keeper with as little configuration as possible.                                | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker`, `redis`, `postgresql`, `caddy` |\n| `keeper-services:2.9`      | If you'd like for the Redis & Database to exist outside of the container, you can use the \"services\" image to launch without them included in the image. | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker`                                 |\n| `keeper-web:2.9`           | An image containing the Vite SSR web interface.                                                                                                          | `keeper-web`                                                                              |\n| `keeper-api:2.9`           | An image containing the Bun API service.                                                                                                                 | `keeper-api`                                                                              |\n| `keeper-cron:2.9`          | An image containing the Bun cron service. Requires `keeper-worker` for destination syncing.                                                              | `keeper-cron`                                                                             |\n| `keeper-worker:2.9`        | An image containing the BullMQ worker that processes calendar sync jobs enqueued by `keeper-cron`.                                                       | `keeper-worker`                                                                           |\n| `keeper-mcp:2.9`           | An image containing the MCP server for AI agent calendar access. Optional — only needed if using MCP clients.                                            | `keeper-mcp`                                                                              |\n\n> [!TIP]\n>\n> Pin your images to a major.minor version tag (e.g., `2.9`) rather than `latest`. This prevents breaking changes from automatically applying when you pull new images.\n\n## Prerequisites\n\n### Docker & Docker Compose\n\nIn order to install Docker Compose, please refer to the [official Docker documentation.](https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Finstall\u002F).\n\n### Google OAuth Credentials\n\n> [!TIP]\n>\n> This is optional, although you will not be able to set Google Calendar as a destination without this.\n\nReference the [official Google Cloud Platform documentation](https:\u002F\u002Fsupport.google.com\u002Fcloud\u002Fanswer\u002F15549257) to generate valid credentials for Google OAuth. You must grant your consent screen the `calendar.events`, `calendar.calendarlist.readonly`, and `userinfo.email` scopes.\n\nOnce this is configured, set the client ID and client secret as the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables at runtime.\n\n### Microsoft Azure Credentials\n\n> [!TIP]\n>\n> Once again, this is optional. If you do not configure this, you will not be able to configure Microsoft Outlook as a destination.\n\nMicrosoft does not appear to do documentation well, the best I could find for non-legacy instructions on configuring OAuth is this [community thread.](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fanswers\u002Fquestions\u002F4705805\u002Fhow-to-set-up-oauth-2-0-for-outlook). The required scopes are `Calendars.ReadWrite`, `User.Read`, and `offline_access`. The client ID and secret for Microsoft go into the `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` environment variables respectively.\n\n## Standalone Container\n\nWhile you'd typically want to run containers granularly, if you just want to get up and running, a convenience image `keeper-standalone:2.9` has been provided. This container contains the `cron`, `worker`, `web`, `api` services as well as a configured `redis`, `database`, and `caddy` instance that puts everything behind the same port. While this is the easiest way to spin up Keeper, it is not recognized as best-practice.\n\n### Generate `keeper-standalone` Environment Variables\n\nThe following will generate a `.env` file that contains the key used to generate sessions, as well as the key that is used to encrypt CalDAV credentials at rest.\n\n> [!IMPORTANT]\n>\n> If you plan on accessing Keeper from a URL _other than_ http:\u002F\u002Flocalhost,\n> you will need to set the `TRUSTED_ORIGINS` environment variable. This should\n> be a comma-delimited list of protocol-hostname inclusive origins you will be using.\n>\n> Here is an example where we would be accessing Keeper from the LAN IP and where we\n> are routing Keeper through a reverse proxy that hosts it at https:\u002F\u002Fkeeper.example.com\u002F\n>\n> ```bash\n> TRUSTED_ORIGINS=http:\u002F\u002F10.0.0.2,https:\u002F\u002Fkeeper.example.com\n> ```\n>\n> Without this, you will fail CSRF checks on the `better-auth` package.\n\n```bash\ncat > .env \u003C\u003C EOF\n# BETTER_AUTH_SECRET and ENCRYPTION_KEY are required.\n# TRUSTED_ORIGINS is required if you plan on accessing Keeper from an\n# origin other than http:\u002F\u002Flocalhost\u002F\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nTRUSTED_ORIGINS=\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### Run `keeper-standalone` with Docker\n\nIf you'd like to just run using the Docker CLI, you can use the following command. I would however recommend [using a compose.yaml](#run-standalone-with-docker-compose) file.\n\n```bash\ndocker run -d \\\n  -p 80:80 \\\n  -v keeper-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata \\\n  --env-file .env \\\n  ghcr.io\u002Fridafkih\u002Fkeeper-standalone:2.9\n```\n\n### Run `keeper-standalone` with Docker Compose\n\nIf you'd prefer to use a `compose.yaml` file, the following is an example. Remember to [populate your .env file first](#generate-keeper-standalone-environment-variables).\n\n```yaml\nservices:\n  keeper:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-standalone:2.9\n    ports:\n      - \"80:80\"\n    volumes:\n      - keeper-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    environment:\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      TRUSTED_ORIGINS: ${TRUSTED_ORIGINS}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n\nvolumes:\n  keeper-data:\n```\n\nOnce that's configured, you can launch Keeper using the following command.\n\n```bash\ndocker compose up -d\n```\n\nWith all said and done, you can access Keeper at http:\u002F\u002Flocalhost\u002F. You can use a reverse-proxy like Nginx or Caddy to put Keeper behind a domain on your network.\n\n## Collective Services Image\n\nIf you'd like to bring your own Redis and PostgreSQL, you can use the `keeper-services` image. This contains the `cron`, `web` and `api` services in one.\n\n### Generate `keeper-services` Environment Variables\n\n```bash\ncat > .env \u003C\u003C EOF\n# DATABASE_URL and REDIS_URL are required.\n# *_CLIENT_ID and *_CLIENT_SECRET are optional.\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nDATABASE_URL=postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\nREDIS_URL=redis:\u002F\u002Fredis:6379\nBETTER_AUTH_URL=http:\u002F\u002Flocalhost:3000\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### Run `keeper-services` with Docker Compose\n\nOnce you've populated your environment variables, you can choose to run `redis` and `postgres` alongside the `keeper-services` image to get up and running.\n\n```yaml\nservices:\n  postgres:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: keeper\n      POSTGRES_PASSWORD: keeper\n      POSTGRES_DB: keeper\n    volumes:\n      - postgres-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keeper -d keeper\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    volumes:\n      - redis-data:\u002Fdata\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  keeper:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-services:latest\n    environment:\n      DATABASE_URL: ${DATABASE_URL}\n      REDIS_URL: ${REDIS_URL}\n      BETTER_AUTH_URL: ${BETTER_AUTH_URL}\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    ports:\n      - \"3000:3000\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\nvolumes:\n  postgres-data:\n  redis-data:\n```\n\nOnce that's configured, you can launch Keeper using the following command.\n\n```bash\ndocker compose up -d\n```\n\n## Individual Service Images\n\nWhile running services individually is considered best-practice, it is verbose and more complicated to configure. Each service is hosted in its own image.\n\n### Generate Individual Service Environment Variables\n\n```bash\ncat > .env \u003C\u003C EOF\n# The only optional variables are *_CLIENT_ID, *_CLIENT_SECRET\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nVITE_API_URL=http:\u002F\u002Fapi:3001\nPOSTGRES_USER=keeper\nPOSTGRES_PASSWORD=keeper\nPOSTGRES_DB=keeper\nREDIS_URL=redis:\u002F\u002Fredis:6379\nBETTER_AUTH_URL=http:\u002F\u002Flocalhost:3000\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### Configure Individual Service `compose.yaml`\n\n```yaml\nservices:\n  postgres:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: ${POSTGRES_DB}\n    volumes:\n      - postgres-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keeper -d keeper\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    volumes:\n      - redis-data:\u002Fdata\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  api:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-api:latest\n    environment:\n      API_PORT: 3001\n      DATABASE_URL: postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\n      REDIS_URL: redis:\u002F\u002Fredis:6379\n      BETTER_AUTH_URL: ${BETTER_AUTH_URL}\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\n  cron:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-cron:latest\n    environment:\n      DATABASE_URL: postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\n      REDIS_URL: redis:\u002F\u002Fredis:6379\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\n  web:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-web:latest\n    environment:\n      VITE_API_URL: ${VITE_API_URL}\n      PORT: 3000\n    ports:\n      - \"3000:3000\"\n    depends_on:\n      api:\n        condition: service_started\n\nvolumes:\n  postgres-data:\n  redis-data:\n```\n\nOnce that's configured, you can launch Keeper using the following command.\n\n```bash\ndocker compose up -d\n```\n\n# MCP (Model Context Protocol)\n\nKeeper includes an optional MCP server that lets AI agents (such as Claude) access your calendar data through a standardized protocol. The MCP server authenticates via OAuth 2.1 with a consent flow hosted by the web application.\n\n## Available Tools\n\n| Tool              | Description                                                                                          |\n| ----------------- | ---------------------------------------------------------------------------------------------------- |\n| `list_calendars`  | List all calendars connected to Keeper, including provider name and account.                          |\n| `get_events`      | Get calendar events within a date range. Accepts ISO 8601 datetimes and an IANA timezone identifier. |\n| `get_event_count` | Get the total number of calendar events synced to Keeper.                                            |\n\n## Connecting an MCP Client\n\nTo connect an MCP-compatible client (e.g. Claude Code, Claude Desktop), point it at your MCP server URL. The client will be guided through the OAuth consent flow to authorize read access to your calendar data.\n\nExample Claude Code MCP configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"keeper\": {\n      \"type\": \"url\",\n      \"url\": \"https:\u002F\u002Fkeeper.example.com\u002Fmcp\"\n    }\n  }\n}\n```\n\n## Self-Hosted MCP Setup\n\n> [!NOTE]\n>\n> MCP is fully optional. All MCP-related environment variables are optional across every service and image. If they are not set, Keeper starts normally without MCP functionality. Existing self-hosted deployments are unaffected.\n\nThe MCP server is proxied through the web service at `\u002Fmcp`, the same way the API is proxied at `\u002Fapi`. MCP is **not** bundled in the `keeper-standalone` or `keeper-services` convenience images — run the `keeper-mcp` image as a separate container alongside them.\n\nTo enable MCP on a self-hosted instance:\n\n1. Run the `keeper-mcp` container with `MCP_PORT`, `MCP_PUBLIC_URL`, `DATABASE_URL`, `BETTER_AUTH_SECRET`, and `BETTER_AUTH_URL`.\n2. Set `MCP_PUBLIC_URL` on the `api` service to the same value (e.g. `https:\u002F\u002Fkeeper.example.com\u002Fmcp`).\n3. Set `VITE_MCP_URL` on the `web` service to the internal URL of the MCP container (e.g. `http:\u002F\u002Fmcp:3002`).\n\n# Modules\n\n## Applications\n\n1. [@keeper.sh\u002Fapi](.\u002Fapplications\u002Fapi)\n2. [@keeper.sh\u002Fcron](.\u002Fapplications\u002Fcron)\n3. [@keeper.sh\u002Fmcp](.\u002Fapplications\u002Fmcp)\n4. [@keeper.sh\u002Fweb](.\u002Fapplications\u002Fcanary-web)\n5. @keeper.sh\u002Fcli _(Coming Soon)_\n6. @keeper.sh\u002Fmobile _(Coming Soon)_\n7. @keeper.sh\u002Fssh _(Coming Soon)_\n\n## Modules\n\n1. [@keeper.sh\u002Fauth](.\u002Fpackages\u002Fauth)\n1. [@keeper.sh\u002Fauth-plugin-username-only](.\u002Fpackages\u002Fauth-plugin-username-only)\n1. [@keeper.sh\u002Fbroadcast](.\u002Fpackages\u002Fbroadcast)\n1. [@keeper.sh\u002Fbroadcast-client](.\u002Fpackages\u002Fbroadcast-client)\n1. [@keeper.sh\u002Fcalendar](.\u002Fpackages\u002Fcalendar)\n1. [@keeper.sh\u002Fconstants](.\u002Fpackages\u002Fconstants)\n1. [@keeper.sh\u002Fdata-schemas](.\u002Fpackages\u002Fdata-schemas)\n1. [@keeper.sh\u002Fdatabase](.\u002Fpackages\u002Fdatabase)\n1. [@keeper.sh\u002Fdate-utils](.\u002Fpackages\u002Fdate-utils)\n1. [@keeper.sh\u002Fencryption](.\u002Fpackages\u002Fencryption)\n1. [@keeper.sh\u002Fenv](.\u002Fpackages\u002Fenv)\n1. [@keeper.sh\u002Ffixtures](.\u002Fpackages\u002Ffixtures)\n1. [@keeper.sh\u002Fkeeper-api](.\u002Fpackages\u002Fkeeper-api)\n1. [@keeper.sh\u002Foauth](.\u002Fpackages\u002Foauth)\n1. [@keeper.sh\u002Foauth-google](.\u002Fpackages\u002Foauth-google)\n1. [@keeper.sh\u002Foauth-microsoft](.\u002Fpackages\u002Foauth-microsoft)\n1. [@keeper.sh\u002Fpremium](.\u002Fpackages\u002Fpremium)\n1. [@keeper.sh\u002Fprovider-caldav](.\u002Fpackages\u002Fprovider-caldav)\n1. [@keeper.sh\u002Fprovider-core](.\u002Fpackages\u002Fprovider-core)\n1. [@keeper.sh\u002Fprovider-fastmail](.\u002Fpackages\u002Fprovider-fastmail)\n1. [@keeper.sh\u002Fprovider-google-calendar](.\u002Fpackages\u002Fprovider-google-calendar)\n1. [@keeper.sh\u002Fprovider-icloud](.\u002Fpackages\u002Fprovider-icloud)\n1. [@keeper.sh\u002Fprovider-outlook](.\u002Fpackages\u002Fprovider-outlook)\n1. [@keeper.sh\u002Fprovider-registry](.\u002Fpackages\u002Fprovider-registry)\n1. [@keeper.sh\u002Fpull-calendar](.\u002Fpackages\u002Fpull-calendar)\n1. [@keeper.sh\u002Fsync-calendar](.\u002Fpackages\u002Fsync-calendar)\n1. [@keeper.sh\u002Fsync-events](.\u002Fpackages\u002Fsync-events)\n1. [@keeper.sh\u002Ftypescript-config](.\u002Fpackages\u002Ftypescript-config)\n","![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fridafkih_keeper.sh_readme_74b3ff9502a5.png)\n\n# 关于\n\nKeeper 是一款简单且开源的日历同步工具。它允许您从远程托管的 iCal 或 ICS 链接中拉取事件，并将其推送到一个或多个日历中，以便所有日历的时间段都能对齐。\n\n# 功能特性\n\n- 聚合来自远程源的日历事件\n- 与事件内容无关的同步引擎\n- 将聚合后的事件推送到一个或多个日历\n- MCP（模型上下文协议）服务器，用于 AI 代理访问日历\n- 基于 AGPL-3.0 开源\n- 易于自行托管\n- 易于清理远程事件\n\n# 错误报告与功能请求\n\n如果您遇到错误或有任何功能想法，可以在 [GitHub 上提交问题](https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues)，我们将尽快进行筛选和处理。\n\n# 贡献\n\n我们欢迎高价值和高品质的贡献。在着手开发您希望合并的大型功能之前，请先开启一个问题进行讨论。\n\n## 本地开发\n\n开发环境通过带有自动 TLS 的 [Caddy](https:\u002F\u002Fcaddyserver.com\u002F) 反向代理运行在 `https:\u002F\u002Fkeeper.localhost` 的 HTTPS 后面。根据 [RFC 6761](https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6761)，`.localhost` 顶级域名会自动解析为 `127.0.0.1` —— 无需 `\u002Fetc\u002Fhosts` 条目。\n\n### 前置条件\n\n- [Bun](https:\u002F\u002Fbun.sh\u002F) (v1.3.5+)\n- [Docker](https:\u002F\u002Fdocs.docker.com\u002Fget-started\u002F) 和 Docker Compose\n\n### 入门指南\n\n```bash\nbun install\n```\n\n#### 生成并信任根证书颁发机构 (CA)\n\n开发环境通过 Caddy 运行在 HTTPS 后面。您需要生成一个本地根证书颁发机构 (Certificate Authority, CA) 并信任它，以便您的浏览器接受该证书。\n\n```bash\nmkdir -p .pki\nopenssl req -x509 -new -nodes \\\n  -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\\n  -keyout .pki\u002Froot.key -out .pki\u002Froot.crt \\\n  -days 3650 -subj \"\u002FCN=Keeper.sh CA\"\n```\n\n然后在您的平台上信任它：\n\n**macOS**\n\n```bash\nsudo security add-trusted-cert -d -r trustRoot \\\n  -k \u002FLibrary\u002FKeychains\u002FSystem.keychain .pki\u002Froot.crt\n```\n\n**Linux**\n\n```bash\nsudo cp .pki\u002Froot.crt \u002Fusr\u002Flocal\u002Fshare\u002Fca-certificates\u002Fkeeper-dev-root.crt\nsudo update-ca-certificates\n```\n\n#### 启动开发环境\n\n```bash\nbun dev\n```\n\n这将通过 Docker Compose 启动 PostgreSQL、Redis 和 Caddy 反向代理，以及本地的 API、Web、MCP 和 cron 服务。运行后，打开 `https:\u002F\u002Fkeeper.localhost`。\n\n### 架构\n\n| 服务 | 本地端口 | 访问方式 |\n| -------- | ---------- | ------------------------------------ |\n| Caddy | 443 | `https:\u002F\u002Fkeeper.localhost` |\n| Web | 5173 | 由 Caddy 代理 |\n| API | 3000 | 由 Web 在 `\u002Fapi` 处代理 |\n| MCP | 3001 | 由 Web 在 `\u002Fmcp` 处代理 |\n| Postgres | 5432 | `postgresql:\u002F\u002Fpostgres:postgres@localhost:5432\u002Fpostgres` |\n| Redis | 6379 | `redis:\u002F\u002Flocalhost:6379` |\n\n# 常见问题\n\n## 为什么会有这个项目？\n\n因为我需要它。自从开始使用 [Sedna](https:\u002F\u002Fsedna.sh\u002F)——AI 治理平台——以来，我不得不在三个日历之间工作。一个用于商务，一个用于工作，一个用于个人。\n\n会议冲突的情况频繁发生，令人沮丧。\n\n## 为什么不使用其他服务？\n\n我可能已经尝试过了。它可能太挑剔了，最终浪费了我几个小时的时间去删除那些它似乎不再想跟踪的陈旧事件，或者根本无法可靠地同步。\n\n## 同步引擎是如何工作的？\n\n- 如果我们有一个本地事件，但没有对应的事件“源 → 目标”映射，我们将该事件推送到目标日历。\n- 如果我们有事件的映射，但源 ID 在源上不再存在，我们从目标中删除该事件。\n- 任何带有由 Keeper 创建标记但没有相应本地跟踪的事件，我们都会将其移除。这仅为了向后兼容而执行。\n\n事件会被标记为由 Keeper 创建，方法是在远程 UID 上使用 `@keeper.sh` 后缀，或者对于像 Outlook 这样不支持自定义 UID 的平台，我们将其放入 `\"keeper.sh\"` 类别中。\n\n# 注意事项\n\n1. **Keeper 目前仅跟踪时间段，而非事件详情**、摘要、描述等。如果您需要这些功能，我推荐 [OneCal](https:\u002F\u002Fonecal.io\u002F)。\n2. **Keeper 目前仅从远程且公开可用的 iCal\u002FICS URL 获取数据**，这意味着如果您的安全策略不允许这样做，其他解决方案可能更适合您。\n3. **MCP 服务器仅提供只读访问权限**。AI 代理可以列出日历和查询事件，但不能创建、修改或删除它们。\n\n# 云托管\n\n我已经让 Keeper 易于自行托管，但无论您是单纯想支持该项目，还是不想处理配置和运行自己基础设施的麻烦和开销，云托管始终是一个选项。\n\n前往 [keeper.sh](https:\u002F\u002Fkeeper.sh) 开始使用云托管版本。使用代码 `README` 可享受 25% 折扣。\n\n| | 免费 | 专业版 (云托管) | 专业版 (自行托管) |\n| --------------------- | ---------- | ------------------ | ----------------- |\n| **月费** | $0 USD | $5 USD | $0 |\n| **年费** | $0 USD | $42 USD (-30%) | $0 |\n| **刷新间隔** | 30 分钟 | 1 分钟 | 1 分钟 |\n| **源限制** | 2 | ∞ | ∞ |\n| **目标限制** | 1 | ∞ | ∞ |\n\n# 自行托管\n\n通过自行托管 Keeper，您可以免费获得所有高级功能，能够保证数据治理和自主权，而且很有趣。如果您将自行托管，请考虑通过在 GitHub 上赞助我来支持我和项目的开发。\n\n目前有七种镜像可用，其中两种设计为方便使用，另外五种设计用于服务于细粒度的底层服务。\n\n> [!NOTE]\n>\n> **从旧版本迁移？** 如果您是从较旧的基于 Next.js 的版本升级，请参阅 [迁移指南](https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F140) 以了解环境变量更改。如果检测到旧的环境变量，新的 Web 服务器也会在启动时打印迁移通知。\n\n## 环境变量\n\n| 变量名                           | 服务 | 说明                                                                                                                                                         |\n| ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DATABASE_URL                   | `api`, `cron`, `worker`, `mcp` | PostgreSQL 连接 URL。\u003Cbr>\u003Cbr>例如：`postgres:\u002F\u002Fuser:pass@postgres:5432\u002Fkeeper`                                                                                  |\n| REDIS_URL                      | `api`, `cron`, `worker` | Redis 连接 URL。所有服务必须使用相同的 Redis 实例。\u003Cbr>\u003Cbr>例如：`redis:\u002F\u002Fredis:6379`                                                        |\n| WORKER_JOB_QUEUE_ENABLED       | `cron`        | 必填。设置为 `true` 将同步任务加入 worker 队列，或设置为 `false` 禁用。如果未设置，cron 服务将退出并显示迁移提示。              |\n| BETTER_AUTH_URL                | `api`, `mcp`  | 用于身份验证重定向的基础 URL。\u003Cbr>\u003Cbr>例如：`http:\u002F\u002Flocalhost:3000`                                                                                           |\n| BETTER_AUTH_SECRET             | `api`, `mcp`  | 会话签名的密钥。\u003Cbr>\u003Cbr>例如：`openssl rand -base64 32`                                                                                               |\n| API_PORT                       | `api`         | Bun API 监听的端口。容器镜像中默认为 `3001`。                                                                                                |\n| ENV                            | `web`         | 可选。运行时环境。可以是 `development`、`production` 或 `test`。默认为 `production`。                                                             |\n| PORT                           | `web`         | Web 服务器监听的端口。容器镜像中默认为 `3000`。                                                                                             |\n| VITE_API_URL                   | `web`         | Web 服务器用于代理请求到 Bun API 的 URL。\u003Cbr>\u003Cbr>例如：`http:\u002F\u002Fapi:3001`                                                                         |\n| COMMERCIAL_MODE                | `api`, `cron` | 启用 Polar 计费流程。如果使用 Polar 进行订阅管理，请设置为 `true`。                                                                                          |\n| POLAR_ACCESS_TOKEN             | `api`, `cron` | 可选。用于订阅管理的 Polar API 令牌。                                                                                                              |\n| POLAR_MODE                     | `api`, `cron` | 可选。Polar 环境，`sandbox` 或 `production`。                                                                                                             |\n| POLAR_WEBHOOK_SECRET           | `api`         | 可选。用于验证 Polar webhooks 的密钥。                                                                                                                          |\n| ENCRYPTION_KEY                 | `api`, `cron`, `worker` | 用于静态加密 CalDAV 凭证的密钥。\u003Cbr>\u003Cbr>例如：`openssl rand -base64 32`                                                                                |\n| RESEND_API_KEY                 | `api`         | 可选。通过 Resend 发送邮件的 API 密钥。                                                                                                                    |\n| PASSKEY_RP_ID                  | `api`         | 可选。通行密钥认证的依赖方 ID。                                                                                                              |\n| PASSKEY_RP_NAME                | `api`         | 可选。通行密钥的依赖方显示名称。                                                                                                                  |\n| PASSKEY_ORIGIN                 | `api`         | 可选。允许通行密钥流的源（例如：`https:\u002F\u002Fkeeper.example.com`）。                                                                                    |\n| GOOGLE_CLIENT_ID               | `api`, `cron`, `worker` | 可选。Google 日历集成所需。                                                                                                                 |\n| GOOGLE_CLIENT_SECRET           | `api`, `cron`, `worker` | 可选。Google 日历集成所需。                                                                                                                 |\n| MICROSOFT_CLIENT_ID            | `api`, `cron`, `worker` | 可选。Microsoft Outlook 集成所需。                                                                                                               |\n| MICROSOFT_CLIENT_SECRET        | `api`, `cron`, `worker` | 可选。Microsoft Outlook 集成所需。                                                                                                               |\n| POSTGRES_PASSWORD              | `standalone`  | 可选。`keeper-standalone` 内部 PostgreSQL 数据库的自定义密码。如果未设置，默认为 `keeper`。数据库未在容器外暴露，因此风险较低，但可设置以实现纵深防御。 |\n| BLOCK_PRIVATE_RESOLUTION       | `api`, `cron` | 可选。设置为 `true` 以阻止出站获取（ICS 订阅、CalDAV 服务器）解析到私有\u002F保留网络地址。防止 SSRF (服务器端请求伪造)。默认为 `false`，以便与使用本地 CalDAV\u002FICS 服务器的自托管设置向后兼容。 |\n| PRIVATE_RESOLUTION_WHITELIST          | `api`, `cron` | 可选。当 `BLOCK_PRIVATE_RESOLUTION` 为 `true` 时，此逗号分隔的主机名或 IP 列表不受限制。\u003Cbr>\u003Cbr>例如：`192.168.1.50,radicale.local,10.0.2.12` |\n| TRUSTED_ORIGINS                | `api`         | 可选。用于 CSRF (跨站请求伪造) 保护的额外受信任源的逗号分隔列表。\u003Cbr>\u003Cbr>例如：`http:\u002F\u002F192.168.1.100,http:\u002F\u002Fkeeper.local,https:\u002F\u002Fkeeper.example.com` |\n| MCP_PUBLIC_URL                 | `api`, `mcp`  | 可选。MCP 资源的公共 URL。在 API 上启用 OAuth (授权协议) 并向客户端标识 MCP 服务器。\u003Cbr>\u003Cbr>例如：`https:\u002F\u002Fkeeper.example.com\u002Fmcp`           |\n| VITE_MCP_URL                   | `web`         | 可选。Web 服务器用于代理 `\u002Fmcp` 请求到 MCP 服务的内部 URL。\u003Cbr>\u003Cbr>例如：`http:\u002F\u002Fmcp:3002`                                              |\n| MCP_PORT                       | `mcp`         | 可选。MCP 服务器监听的端口。\u003Cbr>\u003Cbr>例如：`3002`                                                                                                       |\n| OTEL_EXPORTER_OTLP_ENDPOINT    | `api`, `cron`, `worker`, `mcp`, `web` | 可选。设置后，启用通过 [`pino-opentelemetry-transport`](https:\u002F\u002Fgithub.com\u002FVunovati\u002Fpino-opentelemetry-transport) 将结构化日志转发到 OpenTelemetry (开放遥测) 收集器。传输运行在专用工作线程中，不影响应用性能。\u003Cbr>\u003Cbr>例如：`https:\u002F\u002Fotel-collector.example.com:4318` |\n| OTEL_EXPORTER_OTLP_PROTOCOL    | `api`, `cron`, `worker`, `mcp`, `web` | 可选。OTLP 导出器使用的协议。根据 OpenTelemetry 规范默认为 `http\u002Fprotobuf`。\u003Cbr>\u003Cbr>例如：`http\u002Fprotobuf`, `grpc`, `http\u002Fjson` |\n| OTEL_EXPORTER_OTLP_HEADERS     | `api`, `cron`, `worker`, `mcp`, `web` | 可选。随每个 OTLP 导出请求发送的头部。用于身份验证（例如 Basic auth 或 API 密钥）。\u003Cbr>\u003Cbr>例如：`Authorization=Basic dXNlcjpwYXNz` |\n\n以下环境变量在**构建时**被固化到 Web 镜像中。它们在官方 Docker (Docker 容器引擎) 镜像中已预配置，仅当您从源代码构建时才需要设置。\n\n| Name                              | Description                                                        |\n| --------------------------------- | ------------------------------------------------------------------ |\n| VITE_COMMERCIAL_MODE              | 切换 Web UI 中的商业模式（`true`\u002F`false`）。             |\n| POLAR_PRO_MONTHLY_PRODUCT_ID      | 可选。用于支持应用内升级链接的 Polar 月度产品 ID。  |\n| POLAR_PRO_YEARLY_PRODUCT_ID       | 可选。用于支持应用内升级链接的 Polar 年度产品 ID。   |\n| VITE_VISITORS_NOW_TOKEN           | 可选。[visitors.now](https:\u002F\u002Fvisitors.now) 分析令牌 |\n| VITE_GOOGLE_ADS_ID                | 可选。Google Ads 转化跟踪 ID（例如 `AW-123456789`） |\n| VITE_GOOGLE_ADS_CONVERSION_LABEL  | 可选。用于购买跟踪的 Google Ads 转化标签        |\n\n> [!NOTE]\n>\n> - `keeper-standalone` (独立容器) 自动内部配置所有内容——Web 服务器和 Bun (JavaScript 运行时) API (应用程序接口) 都位于端口 `80` 上的单个 Caddy (Web 服务器) 反向代理之后。\n> - `keeper-services` 在一个容器内运行 Web、API、cron (定时任务) 和 worker (工作进程) 服务。Web 服务器在内部代理 `\u002Fapi` 请求，因此只需暴露端口 `3000`。\n> - 对于独立镜像，只需暴露 `web` 容器。API 通过 `VITE_API_URL` 内部访问。\n\n\n\n## 镜像\n\n| Tag                        | Description                                                                                                                                              | Included Services                                                                        |\n| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `keeper-standalone:2.9`    | “独立”镜像包含了您启动 Keeper 所需的一切，且配置尽可能少。                                | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker`, `redis` (内存数据库), `postgresql` (关系型数据库), `caddy` |\n| `keeper-services:2.9`      | 如果您希望 Redis 和数据库存在于容器外部，可以使用“服务”镜像来启动，而不将它们包含在镜像中。 | `keeper-web`, `keeper-api`, `keeper-cron`, `keeper-worker`                                 |\n| `keeper-web:2.9`           | 包含 Vite (前端构建工具) SSR (服务端渲染) Web 界面的镜像。                                                                                                          | `keeper-web`                                                                              |\n| `keeper-api:2.9`           | 包含 Bun API 服务的镜像。                                                                                                                 | `keeper-api`                                                                              |\n| `keeper-cron:2.9`          | 包含 Bun cron 服务的镜像。需要 `keeper-worker` 进行目标同步。                                                              | `keeper-cron`                                                                             |\n| `keeper-worker:2.9`        | 包含处理由 `keeper-cron` 排队的日历同步作业的 BullMQ (消息队列) worker 的镜像。                                                       | `keeper-worker`                                                                           |\n| `keeper-mcp:2.9`           | 包含用于 AI 代理日历访问的 MCP (模型上下文协议) 服务器的镜像。Optional — 仅在需要使用 MCP 客户端时必需。                                            | `keeper-mcp`                                                                              |\n\n> [!TIP]\n>\n> 将您的镜像锁定到主版本。次版本标签（例如 `2.9`），而不是 `latest`。这可以防止在拉取新镜像时自动应用破坏性更改。\n\n## 前置条件\n\n### Docker 与 Docker Compose\n\n为了安装 Docker Compose (编排工具)，请参阅 [官方 Docker 文档。](https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002Finstall\u002F)。\n\n### Google OAuth 凭据\n\n> [!TIP]\n>\n> 这是可选的，尽管没有这个您将无法设置 Google Calendar 作为目的地。\n\n参考 [官方 Google Cloud Platform 文档](https:\u002F\u002Fsupport.google.com\u002Fcloud\u002Fanswer\u002F15549257) 生成有效的 Google OAuth (开放授权协议) 凭据。您必须授予同意屏幕 `calendar.events`、`calendar.calendarlist.readonly` 和 `userinfo.email` 权限范围。\n\n一旦配置完成，将客户端 ID 和客户端密钥设置为运行时的 `GOOGLE_CLIENT_ID` 和 `GOOGLE_CLIENT_SECRET` 环境变量。\n\n### Microsoft Azure 凭据\n\n> [!TIP]\n>\n> 同样，这是可选的。如果您不配置此项，将无法配置 Microsoft Outlook 作为目的地。\n\nMicrosoft 似乎不太擅长编写文档，我找到的关于配置 OAuth 的非旧版最佳说明是此 [社区讨论帖。](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fanswers\u002Fquestions\u002F4705805\u002Fhow-to-set-up-oauth-2-0-for-outlook)。所需的权限范围是 `Calendars.ReadWrite`、`User.Read` 和 `offline_access`。Microsoft 的客户端 ID 和密钥分别放入 `MICROSOFT_CLIENT_ID` 和 `MICROSOFT_CLIENT_SECRET` 环境变量中。\n\n## 独立容器\n\n虽然通常您可能希望以细粒度运行容器，但如果您只是想快速启动，我们提供了一个便捷镜像 `keeper-standalone:2.9`。此容器包含 `cron`、`worker`、`web`、`api` 服务，以及一个配置的 `redis`、`database` 和 `caddy` 实例，将所有内容置于同一端口之后。虽然这是启动 Keeper 最简单的方法，但它不被视为最佳实践。\n\n### 生成 `keeper-standalone` 环境变量\n\n以下内容将生成一个 `.env` 文件，其中包含用于生成会话的密钥，以及用于加密静态 CalDAV (日历数据访问协议) 凭据的密钥。\n\n> [!IMPORTANT]\n>\n> 如果您计划从 http:\u002F\u002Flocalhost 以外的 URL 访问 Keeper，\n> 您需要设置 `TRUSTED_ORIGINS` 环境变量。这应该\n> 是您将要使用的协议 - 主机名包含起源的逗号分隔列表。\n>\n> 这是一个示例，我们将通过局域网 IP 访问 Keeper，并且我们\n> 将通过托管在 https:\u002F\u002Fkeeper.example.com\u002F 的反向代理路由 Keeper\n>\n> ```bash\n> TRUSTED_ORIGINS=http:\u002F\u002F10.0.0.2,https:\u002F\u002Fkeeper.example.com\n> ```\n>\n> 如果没有这个，您将无法通过 `better-auth` (认证库) 包的 CSRF (跨站请求伪造) 检查。\n\n```bash\ncat > .env \u003C\u003C EOF\n# BETTER_AUTH_SECRET and ENCRYPTION_KEY are required.\n\n```\n# TRUSTED_ORIGINS is required if you plan on accessing Keeper from an\n# origin other than http:\u002F\u002Flocalhost\u002F\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nTRUSTED_ORIGINS=\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### 使用 Docker 运行 `keeper-standalone`\n\n如果您只想使用 Docker CLI（命令行界面）运行，可以使用以下命令。但我建议[使用 compose.yaml（编排文件）](#run-standalone-with-docker-compose)。\n\n```bash\ndocker run -d \\\n  -p 80:80 \\\n  -v keeper-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata \\\n  --env-file .env \\\n  ghcr.io\u002Fridafkih\u002Fkeeper-standalone:2.9\n```\n\n### 使用 Docker Compose 运行 `keeper-standalone`\n\n如果您更倾向于使用 `compose.yaml` 文件，以下是示例。记得先[填充您的 .env 文件](#generate-keeper-standalone-environment-variables)。\n\n```yaml\nservices:\n  keeper:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-standalone:2.9\n    ports:\n      - \"80:80\"\n    volumes:\n      - keeper-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    environment:\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      TRUSTED_ORIGINS: ${TRUSTED_ORIGINS}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n\nvolumes:\n  keeper-data:\n```\n\n配置完成后，您可以使用以下命令启动 Keeper。\n\n```bash\ndocker compose up -d\n```\n\n一切设置完毕后，您可以在 http:\u002F\u002Flocalhost\u002F 访问 Keeper。您可以使用 Nginx 或 Caddy 等反向代理（reverse-proxy）将 Keeper 部署在您网络中的域名后面。\n\n## 集成服务镜像\n\n如果您想自带 Redis 和 PostgreSQL，可以使用 `keeper-services` 镜像。它在一个镜像中包含了 `cron`、`web` 和 `api` 服务。\n\n### 生成 `keeper-services` 环境变量\n\n```bash\ncat > .env \u003C\u003C EOF\n# DATABASE_URL and REDIS_URL are required.\n# *_CLIENT_ID and *_CLIENT_SECRET are optional.\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nDATABASE_URL=postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\nREDIS_URL=redis:\u002F\u002Fredis:6379\nBETTER_AUTH_URL=http:\u002F\u002Flocalhost:3000\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### 使用 Docker Compose 运行 `keeper-services`\n\n填充好环境变量后，您可以选择与 `keeper-services` 镜像一起运行 `redis` 和 `postgres` 以快速启动。\n\n```yaml\nservices:\n  postgres:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: keeper\n      POSTGRES_PASSWORD: keeper\n      POSTGRES_DB: keeper\n    volumes:\n      - postgres-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keeper -d keeper\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    volumes:\n      - redis-data:\u002Fdata\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  keeper:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-services:latest\n    environment:\n      DATABASE_URL: ${DATABASE_URL}\n      REDIS_URL: ${REDIS_URL}\n      BETTER_AUTH_URL: ${BETTER_AUTH_URL}\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    ports:\n      - \"3000:3000\"\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\nvolumes:\n  postgres-data:\n  redis-data:\n```\n\n配置完成后，您可以使用以下命令启动 Keeper。\n\n```bash\ndocker compose up -d\n```\n\n## 独立服务镜像\n\n虽然单独运行服务被认为是最佳实践（best-practice），但它比较冗长且配置更复杂。每个服务都托管在各自的镜像中。\n\n### 生成独立服务的环境变量\n\n```bash\ncat > .env \u003C\u003C EOF\n# The only optional variables are *_CLIENT_ID, *_CLIENT_SECRET\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nENCRYPTION_KEY=$(openssl rand -base64 32)\nVITE_API_URL=http:\u002F\u002Fapi:3001\nPOSTGRES_USER=keeper\nPOSTGRES_PASSWORD=keeper\nPOSTGRES_DB=keeper\nREDIS_URL=redis:\u002F\u002Fredis:6379\nBETTER_AUTH_URL=http:\u002F\u002Flocalhost:3000\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\nMICROSOFT_CLIENT_ID=\nMICROSOFT_CLIENT_SECRET=\nEOF\n```\n\n### 配置独立服务的 `compose.yaml`\n\n```yaml\nservices:\n  postgres:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: ${POSTGRES_USER}\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n      POSTGRES_DB: ${POSTGRES_DB}\n    volumes:\n      - postgres-data:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U keeper -d keeper\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  redis:\n    image: redis:7-alpine\n    volumes:\n      - redis-data:\u002Fdata\n    healthcheck:\n      test: [\"CMD\", \"redis-cli\", \"ping\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\n  api:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-api:latest\n    environment:\n      API_PORT: 3001\n      DATABASE_URL: postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\n      REDIS_URL: redis:\u002F\u002Fredis:6379\n      BETTER_AUTH_URL: ${BETTER_AUTH_URL}\n      BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\n  cron:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-cron:latest\n    environment:\n      DATABASE_URL: postgres:\u002F\u002Fkeeper:keeper@postgres:5432\u002Fkeeper\n      REDIS_URL: redis:\u002F\u002Fredis:6379\n      ENCRYPTION_KEY: ${ENCRYPTION_KEY}\n      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}\n      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}\n      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}\n      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}\n    depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy\n\n  web:\n    image: ghcr.io\u002Fridafkih\u002Fkeeper-web:latest\n    environment:\n      VITE_API_URL: ${VITE_API_URL}\n      PORT: 3000\n    ports:\n      - \"3000:3000\"\n    depends_on:\n      api:\n        condition: service_started\n\nvolumes:\n  postgres-data:\n  redis-data:\n```\n\n配置完成后，您可以使用以下命令启动 Keeper。\n\n```bash\ndocker compose up -d\n```\n\n# MCP（模型上下文协议）\n\nKeeper 包含一个可选的 MCP（模型上下文协议）服务器，允许 AI 代理（例如 Claude）通过标准化协议访问您的日历数据。该 MCP 服务器通过 Web 应用程序托管的 OAuth 2.1 同意流程进行身份验证。\n\n## 可用工具\n\n| 工具 | 描述 |\n| ----------------- | ---------------------------------------------------------------------------------------------------- |\n| `list_calendars` | 列出连接到 Keeper 的所有日历，包括提供商名称和账户。 |\n| `get_events` | 获取指定日期范围内的日历事件。接受 ISO 8601 日期时间和 IANA 时区标识符。 |\n| `get_event_count` | 获取同步到 Keeper 的日历事件总数。 |\n\n## 连接 MCP 客户端\n\n要连接兼容 MCP 的客户端（例如 Claude Code、Claude Desktop），请将其指向您的 MCP 服务器 URL。客户端将引导您完成 OAuth 同意流程，以授权读取您的日历数据。\n\n示例 Claude Code MCP 配置：\n\n```json\n{\n  \"mcpServers\": {\n    \"keeper\": {\n      \"type\": \"url\",\n      \"url\": \"https:\u002F\u002Fkeeper.example.com\u002Fmcp\"\n    }\n  }\n}\n```\n\n## 自托管 MCP 设置\n\n> [!NOTE]\n>\n> MCP 完全是可选的。所有与 MCP 相关的环境变量在所有服务和镜像中均为可选。如果未设置，Keeper 将正常启动且不包含 MCP 功能。现有的自托管部署不会受到影响。\n\nMCP 服务器通过 Web 服务在 `\u002Fmcp` 路径进行代理，方式与 API 在 `\u002Fapi` 路径代理相同。MCP **不**包含在 `keeper-standalone` 或 `keeper-services` 便捷镜像中 —— 请将 `keeper-mcp` 镜像作为独立容器与它们一同运行。\n\n要在自托管实例上启用 MCP：\n\n1. 使用 `MCP_PORT`、`MCP_PUBLIC_URL`、`DATABASE_URL`、`BETTER_AUTH_SECRET` 和 `BETTER_AUTH_URL` 运行 `keeper-mcp` 容器。\n2. 在 `api` 服务上设置 `MCP_PUBLIC_URL` 为相同的值（例如 `https:\u002F\u002Fkeeper.example.com\u002Fmcp`）。\n3. 在 `web` 服务上设置 `VITE_MCP_URL` 为 MCP 容器的内部 URL（例如 `http:\u002F\u002Fmcp:3002`）。\n\n# 模块\n\n## 应用程序\n\n1. [@keeper.sh\u002Fapi](.\u002Fapplications\u002Fapi)\n2. [@keeper.sh\u002Fcron](.\u002Fapplications\u002Fcron)\n3. [@keeper.sh\u002Fmcp](.\u002Fapplications\u002Fmcp)\n4. [@keeper.sh\u002Fweb](.\u002Fapplications\u002Fcanary-web)\n5. @keeper.sh\u002Fcli _(即将推出)_\n6. @keeper.sh\u002Fmobile _(即将推出)_\n7. @keeper.sh\u002Fssh _(即将推出)_\n\n## 模块\n\n1. [@keeper.sh\u002Fauth](.\u002Fpackages\u002Fauth)\n2. [@keeper.sh\u002Fauth-plugin-username-only](.\u002Fpackages\u002Fauth-plugin-username-only)\n3. [@keeper.sh\u002Fbroadcast](.\u002Fpackages\u002Fbroadcast)\n4. [@keeper.sh\u002Fbroadcast-client](.\u002Fpackages\u002Fbroadcast-client)\n5. [@keeper.sh\u002Fcalendar](.\u002Fpackages\u002Fcalendar)\n6. [@keeper.sh\u002Fconstants](.\u002Fpackages\u002Fconstants)\n7. [@keeper.sh\u002Fdata-schemas](.\u002Fpackages\u002Fdata-schemas)\n8. [@keeper.sh\u002Fdatabase](.\u002Fpackages\u002Fdatabase)\n9. [@keeper.sh\u002Fdate-utils](.\u002Fpackages\u002Fdate-utils)\n10. [@keeper.sh\u002Fencryption](.\u002Fpackages\u002Fencryption)\n11. [@keeper.sh\u002Fenv](.\u002Fpackages\u002Fenv)\n12. [@keeper.sh\u002Ffixtures](.\u002Fpackages\u002Ffixtures)\n13. [@keeper.sh\u002Fkeeper-api](.\u002Fpackages\u002Fkeeper-api)\n14. [@keeper.sh\u002Foauth](.\u002Fpackages\u002Foauth)\n15. [@keeper.sh\u002Foauth-google](.\u002Fpackages\u002Foauth-google)\n16. [@keeper.sh\u002Foauth-microsoft](.\u002Fpackages\u002Foauth-microsoft)\n17. [@keeper.sh\u002Fpremium](.\u002Fpackages\u002Fpremium)\n18. [@keeper.sh\u002Fprovider-caldav](.\u002Fpackages\u002Fprovider-caldav)\n19. [@keeper.sh\u002Fprovider-core](.\u002Fpackages\u002Fprovider-core)\n20. [@keeper.sh\u002Fprovider-fastmail](.\u002Fpackages\u002Fprovider-fastmail)\n21. [@keeper.sh\u002Fprovider-google-calendar](.\u002Fpackages\u002Fprovider-google-calendar)\n22. [@keeper.sh\u002Fprovider-icloud](.\u002Fpackages\u002Fprovider-icloud)\n23. [@keeper.sh\u002Fprovider-outlook](.\u002Fpackages\u002Fprovider-outlook)\n24. [@keeper.sh\u002Fprovider-registry](.\u002Fpackages\u002Fprovider-registry)\n25. [@keeper.sh\u002Fpull-calendar](.\u002Fpackages\u002Fpull-calendar)\n26. [@keeper.sh\u002Fsync-calendar](.\u002Fpackages\u002Fsync-calendar)\n27. [@keeper.sh\u002Fsync-events](.\u002Fpackages\u002Fsync-events)\n28. [@keeper.sh\u002Ftypescript-config](.\u002Fpackages\u002Ftypescript-config)","# keeper.sh 快速上手指南\n\n**Keeper** 是一款简单且开源的日历同步工具。它支持从远程托管的 iCal 或 ICS 链接拉取事件，并将其推送到一个或多个日历中，实现时间槽的对齐。支持 MCP 协议供 AI 代理访问，易于自托管。\n\n## 环境准备\n\n在开始之前，请确保您的开发环境满足以下要求：\n\n- **操作系统**: macOS \u002F Linux \u002F Windows (WSL)\n- **运行时**: [Bun](https:\u002F\u002Fbun.sh\u002F) (版本 v1.3.5+)\n- **容器化**: [Docker](https:\u002F\u002Fdocs.docker.com\u002Fget-started\u002F) & Docker Compose\n- **网络**: 本地开发环境通过 Caddy 反向代理运行在 HTTPS 下\n\n## 安装步骤\n\n### 1. 克隆项目\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh.git\ncd keeper.sh\n```\n\n### 2. 安装依赖\n```bash\nbun install\n```\n\n### 3. 生成并信任根证书\n本地开发环境使用 HTTPS，需要生成本地根证书授权（CA）并信任它，以便浏览器接受证书。\n\n**生成证书：**\n```bash\nmkdir -p .pki\nopenssl req -x509 -new -nodes \\\n  -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\\n  -keyout .pki\u002Froot.key -out .pki\u002Froot.crt \\\n  -days 3650 -subj \"\u002FCN=Keeper.sh CA\"\n```\n\n**信任证书：**\n\n*macOS*\n```bash\nsudo security add-trusted-cert -d -r trustRoot \\\n  -k \u002FLibrary\u002FKeychains\u002FSystem.keychain .pki\u002Froot.crt\n```\n\n*Linux*\n```bash\nsudo cp .pki\u002Froot.crt \u002Fusr\u002Flocal\u002Fshare\u002Fca-certificates\u002Fkeeper-dev-root.crt\nsudo update-ca-certificates\n```\n\n### 4. 启动开发环境\n```bash\nbun dev\n```\n该命令将通过 Docker Compose 启动 PostgreSQL、Redis、Caddy 反向代理以及 API、Web、MCP 和 Cron 服务。\n\n## 基本使用\n\n### 访问界面\n服务启动后，在浏览器中打开以下地址：\n```text\nhttps:\u002F\u002Fkeeper.localhost\n```\n\n### 核心功能配置\n1. **日历聚合**: 在界面中添加远程 iCal\u002FICS 链接作为数据源。\n2. **事件同步**: 系统会自动将聚合后的事件推送到指定的目标日历。\n3. **AI 集成**: 可通过 MCP 服务器 (`\u002Fmcp`) 为 AI 代理提供只读的日历数据访问权限。\n\n### 生产部署提示\n若需自行部署生产环境，建议使用 Docker 镜像。请根据需求配置环境变量（如 `DATABASE_URL`, `REDIS_URL`, `ENCRYPTION_KEY` 等），具体参考官方文档中的环境变量列表。","一位负责多个项目的独立开发者同时管理着工作 Gmail、Outlook 客户日历以及个人 iCloud 日程，每天需要频繁在不同平台间协调会议时间。\n\n### 没有 keeper.sh 时\n- 需要在三个不同的日历应用间反复切换查看，极易漏看冲突时段导致会议撞车。\n- 删除了某个会议后，其他平台仍残留无效事件，导致主视图长期被垃圾数据污染，影响判断。\n- 无法让 AI 助手直接读取所有分散的日程，难以实现基于真实时间的自动化会议安排建议。\n- 手动同步耗时且不可靠，经常出现某一方更新后另一方未生效的情况，造成沟通误会。\n\n### 使用 keeper.sh 后\n- keeper.sh 自动聚合所有来源事件，提供统一视图，彻底消除跨平台切换的麻烦。\n- 内置智能清理机制，自动移除已失效的远程事件，保持日历视图始终清爽准确。\n- 通过 MCP 协议直接暴露日程接口给 AI Agent，实现基于真实时间的自动化排期。\n- 双向同步引擎确保任意端的修改即刻更新到所有关联日历，杜绝信息不同步。\n\nkeeper.sh 实现了跨平台日程的统一管控与自动化流转，彻底消除了多日历管理的摩擦成本。","https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fridafkih_keeper.sh_74b3ff95.png","ridafkih","Rida F'kih","https:\u002F\u002Foss.gittoolsai.com\u002Favatars\u002Fridafkih_082cc8b4.png",null,"@wealthsimple ","Calgary, Alberta, Canada","github.com@rida.dev","rida.dev","https:\u002F\u002Fgithub.com\u002Fridafkih",[87,91,95,98,102,106,109],{"name":88,"color":89,"percentage":90},"TypeScript","#3178c6",98.2,{"name":92,"color":93,"percentage":94},"Dockerfile","#384d54",0.6,{"name":96,"color":97,"percentage":94},"MDX","#fcb32c",{"name":99,"color":100,"percentage":101},"CSS","#663399",0.3,{"name":103,"color":104,"percentage":105},"Shell","#89e051",0.1,{"name":107,"color":108,"percentage":105},"JavaScript","#f1e05a",{"name":110,"color":111,"percentage":112},"HTML","#e34c26",0,962,28,"2026-04-05T04:00:31","AGPL-3.0","Linux, macOS","未说明",{"notes":120,"python":118,"dependencies":121},"该工具为日历同步服务而非 AI 模型，无需 GPU 计算资源。本地开发需生成并信任本地根证书以启用 HTTPS。运行环境基于 Bun 而非 Python。支持 Docker 容器化部署及自托管。",[122,123,124,125,126,127],"Bun >= 1.3.5","Docker","Docker Compose","PostgreSQL","Redis","Caddy",[18],[130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145],"bun","calendar","ical","nextjs","sync","typescript","ics","caldav","calendar-sync","google-calendar","icloud","mcp","mcp-server","open-source","outlook","self-hosted","2026-03-27T02:49:30.150509","2026-04-06T07:12:56.653105",[149,154,158,163,168,173],{"id":150,"question_zh":151,"answer_zh":152,"source_url":153},1832,"应该选择 `keeper-standalone` 还是 `keeper-services` 镜像进行部署？","推荐根据需求选择：\n1. 如果想以最少的努力实现自托管，可使用 `keeper-standalone:latest` 镜像，它包含 web、cron、api、caddy、postgres 和 redis 所有组件。\n2. 如果不想代理链式结构，或希望管理自己的 Redis 和 Postgres 实例并将 Keeper 组件隔离在独立容器中，请使用 `keeper-services:latest` 镜像。","https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F52",{"id":155,"question_zh":156,"answer_zh":157,"source_url":153},1833,"如何使用外部数据库配置 Keeper？","使用 `keeper-services:latest` 时，需在 `compose.yaml` 中单独定义 postgres 和 redis 服务。例如：\n- Postgres 需设置 `POSTGRES_USER`、`POSTGRES_PASSWORD`、`POSTGRES_DB` 并挂载数据卷。\n- Redis 需挂载数据卷并配置健康检查。\n- Keeper 服务通过环境变量连接这些外部实例，而不是依赖内置数据库。",{"id":159,"question_zh":160,"answer_zh":161,"source_url":162},1834,"注册账号时无错误提示但无法完成注册是什么原因？","这通常是因为用户名长度不足。系统要求用户名必须超过两个字符。如果用户名太短，界面可能不会显示明确的错误信息，导致看似无响应。请尝试使用至少三个字符的用户名。","https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F72",{"id":164,"question_zh":165,"answer_zh":166,"source_url":167},1835,"访问 API 时出现 502 错误或 Bun 崩溃该如何解决？","日志显示 Bun 发生段错误（Segmentation fault），这可能与 CPU 架构（如 x86 与 ARM）或 Bun 版本 Bug 有关。解决方案包括：\n1. 确认是否已更新至修复该问题的版本（如 Issue #130 提及的修复）。\n2. 如果是 x86 系统，可能是特定 CPU 指令集问题，尝试重启容器可能暂时恢复。\n3. 关注官方关于 Bun 崩溃的后续修复公告。","https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F125",{"id":169,"question_zh":170,"answer_zh":171,"source_url":172},1836,"如何在 Docker 中将 Redis 绑定到桥接网络？","可以在配置文件中将 Redis 绑定到 Docker 桥接网络（bridge network）来限制访问范围。这有助于在保持 Redis 安全的同时允许容器间通信。注意，绑定网络配置需要与 Redis 的保护模式设置配合调整。","https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fissues\u002F53",{"id":174,"question_zh":175,"answer_zh":176,"source_url":172},1837,"Redis 的 `protected-mode` 应该如何配置？","`bind` 和 `protected-mode` 并不完全关联。如果 Redis 位于 VPS 后端，可以将其绑定到 Docker 桥接网络，但如果需要允许非 localhost 地址访问，可能仍需关闭 `protected-mode`。因为开启时它仅在 localhost 地址上绕过保护。请根据实际网络环境权衡安全性与连通性。",[178,183,188,193,198,203,208,213,218,223,228,233,238,243,248,253,258,263,268,273],{"id":179,"version":180,"summary_zh":181,"released_at":182},101296,"v2.9.27","## What's Changed\r\n* Disable exponential backoff by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F354\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.26...v2.9.27","2026-04-01T09:03:53",{"id":184,"version":185,"summary_zh":186,"released_at":187},101297,"v2.9.26","## What's Changed\r\n* chore: add lefthook by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F352\r\n* chore: sample errors for visibility by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F353\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.25...v2.9.26","2026-04-01T08:23:04",{"id":189,"version":190,"summary_zh":191,"released_at":192},101298,"v2.9.25","## What's Changed\r\n* docs: remove README.md considerations by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F350\r\n* feat: add conflict resolution by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F351\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.24...v2.9.25","2026-04-01T07:53:00",{"id":194,"version":195,"summary_zh":196,"released_at":197},101299,"v2.9.24","## What's Changed\r\n* fix: deadlock caused by conflicting mappings by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F349\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.23...v2.9.24","2026-04-01T06:00:53",{"id":199,"version":200,"summary_zh":201,"released_at":202},101300,"v2.9.23","## What's Changed\r\n* feat(digest-fetch): rebuild digest-fetch to still be bad, but less by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F348\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.22...v2.9.23","2026-04-01T03:32:52",{"id":204,"version":205,"summary_zh":206,"released_at":207},101301,"v2.9.22","## What's Changed\r\n* fix: js-sha256 is so unfortunate by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F347\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.21...v2.9.22","2026-04-01T03:16:40",{"id":209,"version":210,"summary_zh":211,"released_at":212},101302,"v2.9.21","## What's Changed\r\n* fix: mark digest-fetch as external by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F346\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.19.10...v2.9.21","2026-04-01T03:03:26",{"id":214,"version":215,"summary_zh":216,"released_at":217},101303,"v2.9.20","## What's Changed\r\n* fix: add js-sha-256 to external array by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F345\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.19.9...v2.9.20","2026-04-01T02:50:16",{"id":219,"version":220,"summary_zh":221,"released_at":222},101304,"v2.9.18","## What's Changed\r\n* fix: lark breaks because it does not support \"expand\" flag by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F332\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.17...v2.9.18","2026-03-30T00:42:20",{"id":224,"version":225,"summary_zh":226,"released_at":227},101305,"v2.9.17","## What's Changed\r\n* fix: pass ua to prevent rate limiting from google calendar ICS links by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F329\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.16...v2.9.17","2026-03-26T15:18:41",{"id":229,"version":230,"summary_zh":231,"released_at":232},101306,"v2.9.16","## What's Changed\r\n* fix: handle mail as null from outlook by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F328\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.15...v2.9.16","2026-03-26T14:10:42",{"id":234,"version":235,"summary_zh":236,"released_at":237},101307,"v2.9.15","## What's Changed\r\n* refactor: move tests into reflection dirs by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F318\r\n* fix: stale state store reference by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F325\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.14...v2.9.15","2026-03-25T19:43:40",{"id":239,"version":240,"summary_zh":241,"released_at":242},101308,"v2.9.14","## What's Changed\r\n* fix: be less strict for caldav needs reauthentication trips by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F316\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.13...v2.9.14","2026-03-22T16:40:16",{"id":244,"version":245,"summary_zh":246,"released_at":247},101309,"v2.9.13","## What's Changed\r\n* feat(web): add logging output to otel collector by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F312\r\n* fix: always exclude working location events by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F313\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.12...v2.9.13","2026-03-22T15:58:57",{"id":249,"version":250,"summary_zh":251,"released_at":252},101310,"v2.9.12","## What's Changed\r\n* feat(otelemetry): use line as fallback in otel by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F311\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.11...v2.9.12","2026-03-22T00:27:48",{"id":254,"version":255,"summary_zh":256,"released_at":257},101311,"v2.9.11","## What's Changed\r\n* fix: add log forwarding otel package by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F310\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.10...v2.9.11","2026-03-22T00:02:42",{"id":259,"version":260,"summary_zh":261,"released_at":262},101312,"v2.9.10","## What's Changed\r\n* fix: set pino-opentelemetry-transport as external by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F308\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.9...v2.9.10","2026-03-21T22:19:11",{"id":264,"version":265,"summary_zh":266,"released_at":267},101313,"v2.9.9","## What's Changed\r\n* feat: add pino-opentelemetry-transport support for OTEL log collection by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F305\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.8...v2.9.9","2026-03-21T21:33:33",{"id":269,"version":270,"summary_zh":271,"released_at":272},101314,"v2.9.8","## What's Changed\r\n* fix(web): include auth state in cache key for landing by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F297\r\n* fix(cron): filter out disabled calendars in ingest job by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F298\r\n* fix(sync): do not enqueue push jobs on dead accounts and track caldav failures for disablement by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F296\r\n* fix(sync, calendar): add credential lock on destination sync by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F295\r\n* fix(calendar): add exponential backoff as recommended by google by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F294\r\n* fix(web): use serialization for optimistic\u002Fserialized fetch to prevent stale values by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F299\r\n* fix(web): map microsoft-365 to outlook on web by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F300\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.7...v2.9.8","2026-03-19T01:02:59",{"id":274,"version":275,"summary_zh":276,"released_at":277},101315,"v2.9.7","## What's Changed\r\n* fix: give postgres user permission over PGDATA by @ridafkih in https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fpull\u002F293\r\n\r\n\r\n**Full Changelog**: https:\u002F\u002Fgithub.com\u002Fridafkih\u002Fkeeper.sh\u002Fcompare\u002Fv2.9.6...v2.9.7","2026-03-18T17:50:59"]