Cal.com 天气日历应用深度解析:用 Emoji 把 OpenWeatherMap 16 天预报装进你的日历
导读
Cal.com 自托管调度平台内置了一款名为 "Weather in your Calendar"(天气日历)的应用,它把 OpenWeatherMap 提供的本地天气预报以 ⛅️ 🌧️ ☀️ 🌨️ 等 Emoji 图标直接渲染进你日常使用的日历订阅源中,让你在规划外拍、花园派对或周末旅行时无需切换 App 即可一眼看到未来 16 天的天气。本文以该应用的官方描述文档 DESCRIPTION.md 为主体骨架,结合仓库中该模块的 config.json、api/add.ts 与 AppSettingsInterface.tsx 等源码,完整讲解其功能特性、安装原理、Webcal 订阅链接参数及二次配置方法。
桌面端日历周视图中天气 Emoji 展示效果
移动端日历视图中天气 Emoji 展示效果
一、这个应用解决什么问题
"把天气预报直接放进日历"是这款应用的核心理念。传统做法是:规划活动前打开天气 App 查预报,再回到日历确认日期——两个信息源互相割裂。该应用将两者合并为单一信息流:
- 天气数据直接嵌入日历的日期条目中,以 Emoji 图标 + 温度数值呈现;
- 预报周期为 16 天(桌面端截图展示周视图,移动端宣传文案则提及 14 天预报能力,不同终端展示粒度略有差异);
- 数据源为 OpenWeatherMap 的公开天气接口;
- 用户只需输入所在城市并完成订阅,即可在几乎所有支持日历订阅的客户端上看到天气。
这正是它被收录进 Cal.com App Store 的原因:Cal.com 生态以「调度基础设施」为核心,而天气日历作为一种日历增强类应用(variant: "other"、categories: ["other"]),在不改变调度逻辑的前提下丰富了日历的上下文信息,属于 packages/app-store 目录下众多可插拔应用中的一员。
二、功能特性拆解(对应官方描述)
官方 DESCRIPTION.md 用一段话概括了核心能力,展开后包含以下要点:
| 特性 | 说明 |
|---|---|
| 天气预报直入日历 | 无需额外打开天气 App,日历条目即天气条目 |
| Emoji 可视化 | 以 ⛅️ 🌧️ ☀️ 🌨️ 等图标区分晴、雨、多云、雪等天气状态 |
| 16 天预报 | 基于 OpenWeatherMap 的多日预报数据 |
| 城市自定义 | 输入你的城市即可生成专属订阅源 |
| 偏好可调 | 支持摄氏度/华氏度等参数调整(源码中体现为 units 参数) |
| 标准订阅协议 | 通过 webcal:// 链接完成日历订阅,兼容绝大多数日历客户端 |
两张官方效果图中:图 1 展示桌面端 Mac 风格日历(Week 视图),June 的每日条目旁出现 ☀️ 图标与 23°、14° 等温度,并标注了 Paris、New York 等城市;图 2 展示移动端日历(August 日期列表),条目旁同样带天气 Emoji 与温度,同时列出 "Search for your local city""Celsius and Fahrenheit (°C & °F)""Dark mode""14 day forecasts" 等能力点。两张图共同印证了「跨桌面/移动端、跨日历客户端」的通用订阅式工作方式。
三、应用元数据:从 config.json 看一个 App 的"身份证"
每个 Cal.com App 都由一个 config.json 描述其注册信息,天气日历的配置如下:
{
"name": "Weather in your Calendar",
"slug": "weather_in_your_calendar",
"type": "weather_in_your_calendar_other",
"logo": "icon.svg",
"url": "https://github.com/vejnoe",
"variant": "other",
"categories": ["other"],
"publisher": "Andreas Vejnø Andersen",
"email": "info@vejnoe.dk",
"description": "Get the local weather forecast with icons in your calendar",
"__createdUsingCli": true,
"isOAuth": false
}
字段语义与实现影响:
slug:全局唯一标识weather_in_your_calendar,文件头注释明确提示"Don't modify slug",如需变更必须通过 CLI 的 edit 命令,避免破坏既有安装记录;type:weather_in_your_calendar_other,用于在数据库Credential表中标记凭据类型(见下文安装原理);variant/categories:均为other,表示它不属于日历/视频/支付等标准分类,而是通用工具类应用;logo:指向 icon.svg,一个橙黄色渐变底、含太阳与云朵图形的方形图标;isOAuth: false:说明该应用不需要 OAuth 授权流程,是"零凭据"的纯订阅型应用;url、publisher、email:记录开发者归属信息(此处为第三方作者 Andreas Vejnø Andersen)。
该元数据被 App Store 的生成模块统一引用:在 apps.metadata.generated.ts 中可以看到 weather_in_your_calendar_config_json 被导入并注册进总元数据表,apps.browser.generated.tsx 与 apps.server.generated.ts 也同步收录,从而在用户端的 App 市场列表与服务端安装校验中同时生效。
四、安装原理:声明式 Handler 与"默认安装"
天气日历不需要任何 API Key 或 OAuth 凭据,因此其安装逻辑极简。入口 index.ts 只做了一件事:
export * as api from "./api";
而 api/add.ts 定义了一个声明式(Declarative)安装处理器:
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import { createDefaultInstallation } from "../../_utils/installation";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
export default handler;
关键点解读:
handlerType: "add"表明这是"点击安装"类型的处理器,api/index.ts 同目录的 api/index.ts 将其导出为add;supportsMultipleInstalls: false意味着每个用户只能安装一次(重复安装会被 installation.ts 中的checkInstalled以 HTTP 422 "Already installed" 拒绝);createDefaultInstallation来自通用工具 packages/app-store/_utils/installation.ts,它会向数据库写入一条Credential记录:type为weather_in_your_calendar_other,appId为 slug,key为空对象{},并关联当前userId或teamId。天气应用不需要存任何密钥,因此key为空即可。
这一设计体现了 Cal.com App Store 的通用抽象:无论应用多么简单(如本应用)或复杂(如 Stripe、Zoom 等含 OAuth 的支付/视频应用),都统一收敛到"安装时创建 Credential 记录"这一模式,业务差异完全由各 App 自己的 Handler 决定。
五、设置界面与 Webcal 订阅链接:参数逐个拆解
安装后,用户会在设置界面输入城市并生成订阅链接。前端实现位于 components/AppSettingsInterface.tsx:
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { TextField } from "@calcom/ui/components/form";
export default function AppSettings() {
const { t } = useLocale();
const unit = "metric";
const [location, setLocation] = useState("");
return (
<div className="stack-y-4 text-sm">
<TextField
placeholder="San Francisco"
value={location}
name="Enter City"
onChange={async (e) => {
setLocation(e.target.value);
}}
/>
<Button
href={`webcal://weather-in-calendar.com/cal/weather-cal.php?city=${location}&units=${unit}&temperature=day`}>
{t("add_to_calendar")}
</Button>
</div>
);
}
组件行为与参数说明:
| 参数 | 取值(源码现状) | 含义 |
|---|---|---|
city |
用户在输入框填写的城市名,占位符示例为 "San Francisco" | 决定天气数据的地理位置 |
units |
当前硬编码为 metric |
温度单位制,metric 即摄氏度;官方宣传中同时提及 °C 与 °F 两种支持 |
temperature |
固定为 day |
温度取值口径为白天气温 |
| 协议 | webcal:// |
标准日历订阅协议,用于把远程 iCalendar 源同步进本地日历 |
| 服务端点 | weather-in-calendar.com/cal/weather-cal.php |
第三方天气订阅服务端,负责把 OpenWeatherMap 数据转换为日历源 |
交互逻辑:用户在文本框中输入城市(如 Paris),点击 "Add to calendar"(文案来自 i18n 键 add_to_calendar)后,浏览器会打开类似下面的订阅链接:
webcal://weather-in-calendar.com/cal/weather-cal.php?city=Paris&units=metric&temperature=day
手机端(iOS/Android)会直接唤起系统日历订阅;桌面端日历客户端(macOS Calendar、Outlook 等)同样支持该协议,从而实现"订阅一次、长期自动更新"。由于天气是定时拉取的日历源,未来 16 天内的预报会随订阅源同步刷新,这正是 DESCRIPTION.md 所述"enter your city, adjust according to your preferences and subscribe to your calendar"的完整闭环。
六、从源码结构看可扩展点
虽然当前前端把 units 固定为 metric,但 URL 参数本身已预留单位切换能力(截图宣传文案也明确支持 °C / °F)。若需要华氏度,只需将订阅链接中的 units 改为 imperial 即可获得不同单位制的预报数据——这一结论可从 URL 构造逻辑直接推断。同理,temperature=day 之外理论上还存在其他取值口径,具体以第三方服务端 weather-in-calendar.com 的实现为准,仓库内未包含服务端代码。
从仓库整体视角看,该应用是 Cal.com "低门槛接入"理念的典型样本:一个 package.json(仅依赖 @calcom/lib)、一个 config.json、一个声明式安装 Handler、一个设置组件,即可完成从 App Store 上架到用户安装配置的完整链路。开发者若想为 Cal.com 贡献同类轻量应用,完全可以以此为最小模板——先复制天气日历的目录骨架,替换 config.json 的元数据与设置组件,再在 apps.metadata.generated.ts 等生成文件中注册,即可让新应用出现在 App 市场中。
七、小结
Weather in your Calendar 用最轻量的方式解决了"日历里看天气"的诉求:Emoji 降低阅读成本,16 天预报覆盖短中期规划需求,webcal:// 订阅协议保证跨客户端可用性。在 Cal.com 的 App Store 体系里,它展示了一条不需要 OAuth、不需要密钥的"声明式安装"路径,配合 installation.ts 的统一 Credential 落库逻辑,让第三方开发者可以用极低成本为调度生态贡献日历增强能力。如果你想在自己的 Cal.com 实例上体验:在 App Store 中找到 "Weather in your Calendar",安装后在设置中填入城市,点击订阅即可在你的日历客户端中看到属于那座城市的 ☀️ 与 ⛅️。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00