mcpField
S
shabbat-aware-scheduler

shabbat-aware-scheduler

github.com/skills-il生产力26175 次安装2 周前更新
代码仓库

关于 shabbat-aware-scheduler

用于规划以色列日程的技能,结合希伯来历节日、安息日时段和当地营业时间评估日期与具体时刻。它还提供脚本和资料,并明确不提供宗教法律裁决。

主要功能

  1. 01查询 Hebcal 安息日与节日数据
  2. 02按以色列营业时间筛选
  3. 03识别节日及安息日时段
  4. 04支持定时任务的实时窗口检查
  5. 05附带日程脚本与参考资料

适用场景

  1. 01安排团队会议和活动
  2. 02规划避开安息日的部署
  3. 03配置跳过节日的定时任务
  4. 04选择合适的通知发送时间

MCP 相关

建议搭配 hebcal MCP 服务,以获取按地点计算的安息日和节日数据;有该 MCP 时,优先使用其工具,而非硬编码日期。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skills-il/localization --skill shabbat-aware-scheduler

安装后在 Claude Code 中输入/shabbat-aware-scheduler确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evidence.json
metadata.json
optimization-log.json
SKILL_HE.md
references/
domain-checklist.md
israeli-holiday-calendar.md
scripts/
check_shabbat.py

详细介绍

安息日感知调度器

此工具仅计算日历时间和业务排期,不发布哈拉卡(halachic)裁决;如有哈拉卡问题,请咨询拉比。

使用说明

第 1 步:确定调度场景
场景关键约束示例
会议安排以色列工作时间(周日至周四)、安息日、节日“安排下周的团队会议”
部署规划安息日、节日或节日前夕不得部署“这个版本什么时候可以部署?”
活动策划希伯来历限制、场地可用性“策划一场产品发布活动”
Cron/自动化周期性任务跳过安息日和节日“每天运行此任务,安息日除外”
通知时机安息日或深夜时段不发送“安排推送通知活动”
第 2 步:获取 Zmanim 和节日数据

使用 HebCal API 获取安息日时间和节日数据。 现成的实用工具见 scripts/check_shabbat.py。

查询 HebCal API 获取安息日时间: 优先使用 geonameid,而非原始坐标。 传入 geonameid 时,Hebcal 会为你应用该城市 自身的点烛(candle-lighting)minhag,不存在可能弄错的 b= 值。只有当某地点没有 geonameid 时,才使用纬度/经度加上手动选取的 b=。

import requests
from datetime import datetime, timedelta

CITY_GEONAMEID = {
    "jerusalem": 281184, "tel_aviv": 293397, "haifa": 294801,
    "beersheba": 295530, "tiberias": 293322, "safed": 293100,
}

def get_shabbat_times(date=None, geonameid=281184):
    """Candle lighting and havdalah for the Shabbat of the week containing `date`."""
    if date is None:
        date = datetime.now()

    # Anchor on the Friday of the CURRENT Shabbat week. Saturday belongs to the
    # Shabbat that started the evening before, so it looks BACK one day. Adding
    # (4 - weekday) % 7 unconditionally returns next week's Shabbat all Saturday,
    # which is 8 days wrong exactly when someone asks "when does Shabbat end?".
    if date.weekday() == 5:
        friday = date - timedelta(days=1)
    else:
        friday = date + timedelta(days=(4 - date.weekday()) % 7)

    response = requests.get("https://www.hebcal.com/shabbat", params={
        "cfg": "json",
        "gy": friday.year, "gm": friday.month, "gd": friday.day,
        "geonameid": geonameid,
        "M": "on",    # Havdalah at tzeit hakochavim (sun 8.5 deg below horizon)
        # Alternative: m=50 or m=72 for a fixed number of minutes after sunset
    })

    data = response.json()
    times = {}
    for item in data.get("items", []):
        if item["category"] == "candles":
            times["candle_lighting"] = item["date"]
        elif item["category"] == "havdalah":
            times["havdalah"] = item["date"]

    return times

Hebcal 的 b= 默认值为日落前 18 分钟,耶路撒冷为 40,海法和 Zikhron Ya'akov 为 30,且同一偏移量同样适用于节日前夕(Rosh Hashana、Yom Kippur、Sukkot、 Shmini Atzeret、Simchat Torah、Pesach、Shavuot),而不仅是安息日前夕。

获取一年的所有以色列节日:

def get_holidays(year):
    """Get all Israeli holidays for a given year."""
    response = requests.get("https://www.hebcal.com/hebcal", params={
        "v": 1,
        "cfg": "json",
        "year": year,
        "month": "x",  # All months
        "maj": "on",   # Major holidays
        "min": "on",   # Minor holidays
        "mod": "on",   # Modern holidays
        "mf": "on",    # Minor fasts. WITHOUT this, Tzom Gedaliah, Asara
                       # B'Tevet, Ta'anit Esther, Ta'anit Bechorot and
                       # Tzom Tammuz are absent from the response entirely.
        "i": "on",     # Israeli holidays (1-day yom tov)
        "nx": "off",
        "ss": "off"
    })

    data = response.json()
    holidays = []
    for item in data.get("items", []):
        if item["category"] in ["holiday", "roshchodesh"]:
            holidays.append({
                "title": item["title"],
                "date": item["date"],
                "category": item.get("subcat", item["category"]),
                "yomtov": item.get("yomtov", False),
                "memo": item.get("memo", "")
            })

    return holidays
第 3 步:实现调度逻辑

以色列工作时间:

日期时间备注
周日08:00-18:00以色列工作周的第一天
周一08:00-18:00常规工作日
周二08:00-18:00常规工作日
周三08:00-18:00常规工作日
周四08:00-18:00常规工作日
周五08:00-13:00半天(安息日前结束)
周六关闭安息日(不营业)

核心调度函数:

from datetime import datetime, timedelta, time
import pytz

IL_TZ = pytz.timezone("Asia/Jerusalem")

BUSINESS_HOURS = {
    6: (time(8, 0), time(18, 0)),  # Sunday
    0: (time(8, 0), time(18, 0)),  # Monday
    1: (time(8, 0), time(18, 0)),  # Tuesday
    2: (time(8, 0), time(18, 0)),  # Wednesday
    3: (time(8, 0), time(18, 0)),  # Thursday
    4: (time(8, 0), time(13, 0)),  # Friday (half day)
    5: None,                        # Saturday (Shabbat)
}

# Days the country does not work that Hebcal reports with yomtov=false.
# Filtering on yomtov alone marks every one of these an ordinary business day.
CLOSED_OBSERVANCES = {
    "Yom HaAtzma'ut": "closed",   "Yom HaZikaron": "closed",
    "Tish'a B'Av": "reduced",     "Erev Yom Kippur": "reduced",
    "Erev Rosh Hashana": "reduced", "Erev Pesach": "reduced",
    "Erev Sukkot": "reduced",     "Erev Shavuot": "reduced",
}
JERUSALEM_ONLY = {"Shushan Purim"}   # walled cities only

def _norm(title):
    # Hebcal emits typographic apostrophes, so "Yom HaAtzma\u2019ut" never matches
    # an ASCII-keyed table. This one character is the whole bug.
    return title.replace("\u2019", "'").strip()

def classify_day(date, holidays_cache=None, jerusalem=False):
    """Return (status, reason).

    closed         Shabbat, Yom Tov, or a national shutdown day
    reduced        morning only (erev chag, fast day)
    evening_closed a full working day whose EVENING is restricted: eve of
                   Yom HaZikaron, Yom HaShoah, Tisha B'Av. Distinct from
                   reduced on purpose, or the morning-only clamp would
                   block an ordinary Monday afternoon.
    open           ordinary business day
    """
    if date.weekday() == 5:
        return "closed", "Shabbat"
    if not holidays_cache:
        return "open", "Business day (no holiday data loaded)"

    date_str = date.strftime("%Y-%m-%d")
    status, reason = "open", "Business day"
    for h in holidays_cache:
        if not h["date"].startswith(date_str):
            continue
        title = _norm(h["title"])
        if h.get("yomtov", False):
            return "closed", f"Yom Tov: {title}"
        if title in JERUSALEM_ONLY:
            if jerusalem:
                return "closed", f"{title} (Jerusalem and walled cities)"
            continue
        level = CLOSED_OBSERVANCES.get(title)
        if level == "closed":
            return "closed", title
        if level == "reduced" and status == "open":
            status, reason = "reduced", f"{title} (morning only)"
    return status, reason

Chol ha-moed 归类为 open:在以色列它是法定工作日,尽管人手较少。 其他所有有歧义的情况一律判定为 closed。提出一个实际关闭的时段,代价是会议改期; 漏掉一次关闭,代价是会议本身。

第 4 步:感知节日的 Cron 任务

日期检查不等于时间检查。 classify_day() 回答的是关于某个日历日期的问题。 安息日和 Yom Tov 从前一天的傍晚点烛时开始,因此周五是营业日期,而周五 19:00 已经进入安息日。 任何在特定时刻触发的操作(cron、通知发送、部署)都必须额外检测实时时间窗口,否则它会 每周都在安息日期间运行,却报告“营业日”。

def is_shabbat_now(now, times):
    """times = get_shabbat_times(now). Fail closed if the API is unreachable."""
    start = datetime.fromisoformat(times["candle_lighting"])
    end = datetime.fromisoformat(times["havdalah"])
    return start <= now <= end
def should_run_today(holidays_cache=None, skip_friday=False, skip_erev_chag=False,
                     now=None, jerusalem=False, time_aware=True, rest_kwargs=None):
    """Determine if a scheduled job should run RIGHT NOW.

    rest_kwargs is passed to the live-window check (geonameid, or city and
    coordinates). Omitting it evaluates the window for Jerusalem, which is
    22 minutes early for a Tel Aviv caller.
    """
    today = datetime.now(IL_TZ).date()

    # Never run on Shabbat -- the live window, not just the weekday
    if is_shabbat_now(datetime.now(IL_TZ), get_shabbat_times()):
        return False, "Shabbat in progress"
    if today.weekday() == 5:
        return False, "Shabbat"

    # Check holidays
    if holidays_cache:
        date_str = today.strftime("%Y-%m-%d")
        status, reason = classify_day(today, holidays_cache)
        if status == "closed":
            return False, reason

        # Check if tomorrow is Yom Tov (today is Erev Chag)
        if skip_erev_chag:
            tomorrow = today + timedelta(days=1)
            tomorrow_str = tomorrow.strftime("%Y-%m-%d")
            for h in holidays_cache:
                if h["date"].startswith(tomorrow_str) and h["yomtov"]:
                    return False, f"Erev Chag: {h['title']} tomorrow"

    if skip_friday and today.weekday() == 4:
        return False, "Friday (half day)"

    return True, "Business day"
第 5 步:节假日前与季节性注意事项
时段日期(约)对排期的影响
安息日前夕(周五)每周根据季节在 13:00-15:00 前结束
犹太新年前夕约 9 月企业中午前歇业
犹太新年 + 赎罪日季节提斯利月 1-10 日10 天可用性降低
住棚节周提斯利月 15-22 日许多人休假, chol ha-moed
逾越节前一周尼散月 15 日前极度繁忙,打扫/采购
逾越节周尼散月 15-22 日许多人休假, chol ha-moed
三周(Bein HaMetzarim)塔模斯月 17 日至埃波月 9 日(约 7 月至 8 月初)不举办婚礼或庆祝活动;企业聚会通常推迟
圣殿被毁日埃波月 9 日(约 7 月底 / 8 月初)斋戒日;许多人视为半天或休息
书珊普珥节亚达月 15 日(普珥节次日)仅限耶路撒冷及其他历史上建有城墙的城市。全国工作日,耶路撒冷全市休息日
夏季(7-8 月)7 月至 8 月学校假期,业务减少
冬季安息日11 月至 2 月安息日较早(周五更早歇业)
夏季安息日5 月至 8 月安息日较晚(周五可用时间更多)

2026 年需要提前规划的重要节假日日期(以色列遵守):

节日公历(约)损失的工作日
逾越节2026 年 4 月 1 日至 4 月 8 日第一天和第七天为 Yom Tov;中间为 chol ha-moed
大屠杀纪念日2026 年 4 月 13 日至 4 月 14 日(傍晚至傍晚)纪念日;娱乐场所关闭
阵亡将士纪念日2026 年 4 月 20 日至 4 月 21 日纪念日;商业活动受限
独立日2026 年 4 月 21 日至 4 月 22 日独立日;大多数企业歇业
七七节2026 年 5 月 21 日至 5 月 22 日(傍晚至傍晚)以色列为期一天的 Yom Tov
圣殿被毁日2026 年 7 月 22 日至 7 月 23 日(傍晚至傍晚)斋戒日
犹太新年2026 年 9 月 11 日至 9 月 13 日第一天恰逢安息日(9 月 12 日周六)而非紧邻其旁,因此歇业跨度为两天,周六和周日,从周五傍晚点蜡烛时进入
赎罪日2026 年 9 月 20 日至 9 月 21 日(傍晚至傍晚)全国停摆
住棚节2026 年 9 月 25 日至 10 月 2 日住棚节前夕为 9 月 25 日;唯一的 Yom Tov 是 9 月 26 日的住棚节第一天。9 月 27 日至 10 月 2 日为 chol ha-moed,包括 10 月 2 日的 Hoshana Raba,该日为工作日
第八日集会(以色列为欢庆妥拉节)2026 年 10 月 2 日至 10 月 3 日住棚节周期收尾的 Yom Tov,在以色列为期一天。恰逢安息日,10 月 3 日
普珥节 / 书珊普珥节2026 年 3 月 3 日和 3 月 4 日全国性普珥节在 3 月 3 日;3 月 4 日的书珊普珥节仅在耶路撒冷和城墙城市遵守

日期已对照 Hebcal 2026(以色列遵守)核实。务必每年重新核对日历;希伯来历相对公历滑动 11 至 19 天。2026 年独立日落在其自然日期,即 5786 年以珥月 5 日(4 月 21 日至 22 日),没有推迟。在其他年份,当独立日或始终先于它的阵亡将士纪念日与安息日冲突时,可能会提前或推迟,因此绝不要直接计算以珥月 5 日。

示例

示例 1:安排会议

用户说:“为下周安排一次团队会议” 结果:检查以色列营业时间(周日至周四),确认没有 chagim,建议可用时段。避免周五,除非是上午,并确认不是 Erev Chag。

示例 2:部署窗口

用户说:“这周最安全的部署时间是什么时候?” 结果:找到周二或周三的时段(周中,距安息日缓冲最大),在营业时间内,且不在节假日前。建议上午部署,以便在安息日前有最大的回滚时间。

示例 3:感知节假日的 Cron

用户说:“设置一个跳过安息日和节假日的每日报告” 结果:提供带 should_run_today() 检查的 cron 配置,预加载当年的节假日缓存,并记录跳过的日期。

捆绑资源

脚本
  • scripts/check_shabbat.py:独立实用程序,可通过 Hebcal API 查询安息日时间、以色列节日和工作日状态。将任意日期分类为关闭 / 缩减 / 开放(涵盖非 Yom-Tov 的全国性关闭日和耶路撒冷的 Shushan Purim),为 cron 提供感知时间的 --is-rest-period-now 检查,覆盖 Yom Tov 开始以及安息日,并根据你搜索时的时间找到下一个可用的以色列工作时段。运行:python scripts/check_shabbat.py --help
参考资料
  • references/domain-checklist.md:本技能的覆盖契约。列出本技能必须处理的每一条以色列日历排期约束,按不同维度拆分(城市 minhag、havdalah 惯例、Yom Tov 与 chol ha-moed、非 Yom-Tov 的全国性关闭日、依赖城市的纪念日、erev 半天、DST、闰年、推迟规则),每行均附有引用来源。
  • references/israeli-holiday-calendar.md:完整的以色列节日日历,包含希伯来日期、公历近似日期、排期影响等级(高/中/低)、哀悼期限制、耶路撒冷按月份划分的季节性安息日点烛时间、2026 年关键日期,以及 HebCal API 端点参考。在围绕 chagim 做规划、确定季节性周五关闭时间,或检查某个事件是否与哀悼期冲突时查阅。

推荐的 MCP 服务

如需实时希伯来日历数据,请将此技能与以下内容搭配使用:

MCP 服务它提供的内容安装
hebcal通过官方 Hebcal API 提供犹太节日、安息日点烛时间、Havdalah 时间、Torah 诵读,以及希伯来—公历日期转换安装 hebcal

当 hebcal MCP 可用时,请使用其工具获取准确的安息日时间和节日日期,而不是硬编码值。该 MCP 可为任意以色列城市提供感知位置的点烛时间。

离线库

如果你在运行时无法访问 Hebcal API(CI、气隙环境、被限速为 90 req/10s),请使用本地库并跳过 HTTP 调用:

库语言说明
@hebcal/coreJavaScript / TypeScript积极维护(截至 2026 年 5 月为 v6.x)。纯 JS,无需网络。安装:npm i @hebcal/core
pyluachPython希伯来日历运算以及希伯来/公历转换。稳定但活跃度低(v2.3.0);适合日期转换,但无内置点烛时间。可与日落/zmanim 库搭配使用,或缓存预先计算的时间
hebcal-goGo由 Hebcal 团队维护

已弃用的 hebcal-js 包(NPM hebcal)是 @hebcal/core 的前身;不要在其上开始新工作。

注意事项

  • 在以色列,安息日点烛时间因城市而异。耶路撒冷为日落前 40 分钟,海法和 Zikhron Ya'akov 为 30 分钟,其他大多数城市为 18 分钟。对整个以色列使用同一个 b= 值会使周五的截止时间偏差 10 到 22 分钟。请将城市特定的偏移量传给 Hebcal API。
  • 以色列的节日(chagim)与安息日有不同的工作限制。大多数节日在以色列是一天,而在散居地是两天(Rosh Hashana 在两地都是两天)。使用散居地节日日历进行以色列的日程安排,会额外封锁实际上在以色列属于 chol ha-moed 的工作日。
  • 希伯来历有闰年,会多出一个月份(Adar II),在 19 年周期中出现 7 次。Agent 可能使用公历计算日期而完全错过这个月份。
  • 以色列的工作时间为周日至周四,周五为半天(至下午早些时候)。周六是每周的休息日,而不是周日。Agent 可能会安排周五下午的会议或周一早上的截止时间。
  • Yom HaAtzmaut 和 Yom HaZikaron 在其自然日期与安息日冲突时可能被推迟(nidcheh)。2026 年不适用偏移(5786 年 Iyyar 月 5 日落在 4 月 21 日至 22 日)。请始终从 Hebcal 读取日期,而不是直接计算 Iyyar 月 5 日。Hebcal 会自行应用推迟;i=on 是一个单独的标志,用于选择以色列的一天制 Yom Tov,并不是执行偏移的那个。
  • Hebcal 中 Havdalah 的默认值是 Tzeit HaKochavim(太阳位于地平线以下 8.5 度)。对照 Hebcal 自身的 zmanim 端点对耶路撒冷的测量,这在夏末约为日落后 37 分钟,在隆冬为 39 到 40 分钟,而不是常被引用的 50 分钟。更严格的 Rabbeinu Tam 遵循者使用 72 分钟。如果你的受众不是默认值,请选择正确的 m= 值。
  • Hebcal 仅用 yomtov: true 标记 Yom Tov。Yom HaAtzma'ut、Yom HaZikaron、Erev Yom Kippur、Tisha B'Av 以及每个 erev chag 都返回 yomtov: false,因此写成 if h["yomtov"] 的过滤器会把独立日当作普通的周三。请同时匹配标题,并注意 Hebcal 写入这些标题时使用排版撇号(Yom HaAtzma\u2019ut),因此以 ASCII 为键的查找会静默地永不触发。
  • 除非传入 mf=on,否则小斋日不可见。maj / min / mod 标志不会返回 Tzom Gedaliah、Asara B'Tevet、Ta'anit Esther、Ta'anit Bechorot 或 Tzom Tammuz。Tzom Gedaliah 落在 Rosh Hashana 的次日,正是有人安排补课会议的时候。
  • 当日历数据缺失时,请以 CLOSED 方式失败。Hebcal 有每 10 秒 90 次请求的速率限制,而一个在 429 时回答“工作日”的调度器会在 Yom Kippur 预订会议。将空的节日缓存视为未知因而关闭,绝不要视为开放。
  • Erev 状态应当被派生(“次日带有 yomtov: true”),而不是从硬编码列表中枚举。固定列表会遗漏像 Hoshana Raba 这样的情况,它在每一年都是 Shmini Atzeret 的 erev。
  • Shushan Purim(Adar 月 15 日)在耶路撒冷和其他历史上设有城墙的城市是完整节日,在其他地方则是正常工作日。全国性调度器不得封锁它;耶路撒冷调度器不得忽略它。这是唯一一个状态取决于城市而非日期的以色列节庆。
  • 以色列的夏令时规则自成一体:DST 在 3 月最后一个周日之前的周五开始,在 10 月最后一个周日结束,因此既不符合美国也不符合欧盟的转换。2026 年分别是 3 月 27 日和 10 月 25 日。两次转换都会在挂钟时间上使周五点烛时间移动一小时,而 10 月的结束是更尖锐的陷阱,因为安息日会突然提前一小时开始。请依赖 Asia/Jerusalem 时区数据库,而不是硬编码的偏移量。
  • Yom Kippur 在日程安排上被视为安息日(完全停摆,包括以色列的世俗商业、交通和广播媒体)。不要在这 25 小时的窗口内部署或安排任何事务。
  • 三周期间(Tammuz 月 17 日至 Av 月 9 日)是哀悼期;婚礼、音乐会和公司庆祝活动通常会被推迟。九日期间(Av 月 1 日至 9 日)更为严格。即使它不是 Yom Tov,也应视为“不得举办发布派对”的窗口。

故障排查

错误:"会议被安排在安息日期间"

原因:时区不匹配,服务运行在 UTC,而安息日时间按本地时间计算 解决方案:在检查之前,始终转换为 Asia/Jerusalem 时区。安息日时间因季节和地点而异。

错误:"未检测到节日"

原因:仅使用公历,没有希伯来历日期映射 解决方案:使用 HebCal API,它会处理希伯来历与公历之间的转换。每年缓存节日数据,并在犹太新年(Rosh Hashanah)时刷新。

错误:"周五会议时间太晚"

原因:无论季节如何,周五截止时间固定为 17:00 解决方案:在冬季,安息日最早可能在 16:00 开始。始终检查特定周五的实际点烛时间。

错误:"耶路撒冷的点烛时间不正确"

原因:在耶路撒冷坐标下传入 b=18(Hebcal 默认值),而不是耶路撒冷当地惯例的 40 分钟。 解决方案:当用户在耶路撒冷时始终传入 b=40,在海法和 Zikhron Ya'akov 传入 b=30,其他地方传入 b=18。Hebcal 采用同样的惯例。

错误:"哈夫达拉时间看起来差了 8-30 分钟"

原因:将 M=on(Tzeit HaKochavim,太阳位于地平线以下 8.5 度,在耶路撒冷全年测得为日落后 37 至 40 分钟)与硬编码的 "42 分钟" 或 "72 分钟" 假设混用。 解决方案:明确选择一种方法。M=on 是 Hebcal 的 tzeit hakochavim(太阳位于地平线以下 8.5 度,可见三颗小星星)。而 m=N 是日落后固定的 N 分钟,因此 m=50 和 m=72 是分钟数,不是星星数。记录你的调度器使用的是哪一种,以免下游 Agent 重复偏移。

错误:"会议被预订在 Yom HaAtzmaut / Tisha B'Av / erev chag"

原因:仅按 yomtov == true 过滤 Hebcal feed。经 2026 年以色列 feed 验证:Yom HaZikaron(4 月 21 日)、Yom HaAtzma'ut(4 月 22 日)、Erev Yom Kippur(9 月 20 日)和 Tisha B'Av(7 月 23 日)都返回 yomtov: false。 解决方案:除了标志之外,还要根据标题进行分类,并先规范化排版撇号。参见 scripts/check_shabbat.py 中的 classify_day()。

错误:"Cron 在周五 19:30 触发,正值安息日期间"

原因:守卫检查的是星期几,而周五在点烛之前仍属于工作日。 解决方案:将当前时刻与 Hebcal 返回的点烛至哈夫达拉时间窗口进行比较(is_shabbat_now()),并且如果 API 无法访问,则在周末按失败即拒绝处理。

评论

支持 Markdown