Munder Difflin 的 /today 时间解析技能:用 when.mjs 把"今天"变成精确的日期窗口
【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin
/today是 Munder Difflin 这个本地多智能体(multi-agent)工具集中内置的一个只读时间技能(skill),它把口语化的"今天"解析成一组具体的 ISO 日期范围——既包含本地时区的自然日(civil date),也包含精确到毫秒的 UTC 时刻,供基于时间戳的查询直接使用。本文以 resources/skills/today/SKILL.md 为骨架,结合其底层解析器 resources/skills/temporal/when.mjs 的源码,讲清楚这个技能怎么用、输出字段是什么含义、底层是怎么算出来的,以及它如何在 Munder Difflin 的 hive worker(被派生的自主工作线程)中发挥作用。读完你不仅能熟练调用/today,还能理解整套时间窗口技能的设计哲学:解析器是唯一事实来源,Agent 绝不手工推算日期。
技能是什么:一个只读的"今天"解析器
/today是一个以 Claude Code Skills 格式定义的技能,声明在resources/skills/today/SKILL.md的 YAML frontmatter 中:
--- name: today description: | Resolve "today" to a concrete ISO date range relative to your run time — today. Returns inclusive civil dates plus exact UTC instants so you have temporal context without computing dates by hand. Read-only: no writes, no network. Use before any task scoped to today (today's logs, metrics, "what happened today"). allowed-tools: - Bash ---从 frontmatter 可以读出三个关键设计意图:
- 用途定位:任何以"今天"为时间边界的任务(今天的日志、指标、"今天发生了什么")在执行前都应先调用它;
- 能力约束:
allowed-tools只有Bash,且技能本身明确声明Read-only(只读):不写文件、不访问网络; - 单一事实来源:description 强调"don't computing dates by hand"——日期由解析器统一给出,Agent 不得自己推算。
/today其实不是一个独立的实现,而是调用同一套解析器的一个命名快捷方式。技能正文给出的调用方式如下:
node "$AGENT_DIR/.claude/skills/temporal/when.mjs" today其中$AGENT_DIR是 Munder Difflin 的 hive 注入给每个 worker 的环境变量,指向 worker 的私有工作区(包含identity.md、memory.md、inbox/、outbox/以及.claude/skills/),这一点在 resources/skills/capabilities/SKILL.md 的环境说明部分有完整定义。也就是说,每个被派生的 worker 都自带这份时间技能,随时可以解析日期。
输出字段:一份可直接用于查询的 JSON 记录
执行上面的命令后,解析器会打印一行人类可读的摘要,随后跟一条 JSON 记录。JSON 记录的字段如下(以/today为例):
| 字段 | 含义 | 示例 |
|---|---|---|
start/end | 本地时区下的包含式自然日日期(YYYY-MM-DD) | 2026-09-16/2026-09-16 |
startUtc/endExclusiveUtc | 同一窗口表示为半开区间[start, end)的精确 UTC 时刻 | 2026-09-16T00:00:00.000Z/2026-09-17T00:00:00.000Z |
days | 窗口跨越的自然日天数 | 1 |
timezone/tzOffsetMinutes | 解析时所在时区及相对 UTC 的偏移分钟数 | Asia/Shanghai/480 |
asOf | 本次解析发生的时刻(UTC ISO 字符串) | 2026-09-16T07:57:39.123Z |
window/label/inclusive | 请求的窗口关键字、人类可读标签、是否为包含式 | today/Today/true |
关键点在于两组日期各有用途:
start/end是给人类和自然语言任务看的:直接作为"今天的起止日期"使用;startUtc/endExclusiveUtc是给机器查询用的:对于createdAt、时间戳这类字段的过滤,使用半开区间[startUtc, endExclusiveUtc)可以避免"边界时刻到底算不算"的歧义——这正是技能文档强调[start, end)语义的原因。
源码级解析:when.mjs 是怎么算出"今天"的
/today背后真正的执行者是 resources/skills/temporal/when.mjs,一个纯 Node.js 标准库实现、无任何第三方依赖的脚本。理解它就能理解整个时间技能家族的实现原理。
本地自然日(civil day)的计算
脚本的核心是civ()与fmt()两个辅助函数:
const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); const civ = (y, m, d) => new Date(y, m, d); const fmt = (dt) => `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;civ(y, m, d)构造的是本地时区当天的午夜零点,而不是某个 UTC 时刻。这样"今天"就严格等于本地日历上的一天,跨时区运行时会各自解析出符合本时区习惯的日期。fmt()则用padStart把月份和日期补成两位,保证输出恒为YYYY-MM-DD格式。
today窗口的定义极其简单:
today: () => ({ label: 'Today', start: civ(Y, M, D), end: civ(Y, M, D) }),其中Y、M、D取自TODAY(civ(now.getFullYear(), now.getMonth(), now.getDate()))的三个字段。start 与 end 相同,都指向今天零点——因为"今天"只包含一个自然日。
半开区间与天数
resolve()函数负责把窗口加工成最终记录,其中最值得注意的是endExclusiveUtc和days的推导:
const endExclusive = civ(w.end.getFullYear(), w.end.getMonth(), w.end.getDate() + 1); const days = Math.round((endExclusive - w.start) / 86400000); return { window: key, label: w.label, start: fmt(w.start), end: fmt(w.end), inclusive: true, startUtc: w.start.toISOString(), endExclusiveUtc: endExclusive.toISOString(), days, asOf: now.toISOString(), timezone: TZ, tzOffsetMinutes: TZ_OFFSET_MIN };- 结束边界通过
end.getDate() + 1得到次日零点,因此无论是否跨月(比如 9 月 30 日加一天自动变成 10 月 1 日),都能正确得到半开区间的右端点; days由(endExclusive - start) / 86400000计算,本质上是两个本地零点之间的毫秒差换算成天数,再配合Math.round消除夏令时(DST)等导致的毫秒级偏移;- 时区信息通过
Intl.DateTimeFormat().resolvedOptions().timeZone获取,偏移分钟数使用-now.getTimezoneOffset()得到"东八区为 +480、UTC-7 为 -420"的惯用符号。
参数解析与错误处理
脚本的main()支持从命令行参数直接指定窗口关键字:
const keys = argv.filter((a) => !a.startsWith('--')); const requested = keys.length ? keys : ORDER; // 不带参数时输出全部窗口也就是说node when.mjs today只解析一个窗口;不带参数则把 14 个命名窗口全部打印出来(作为一个"时间上下文快照");如果传入了未知关键字,脚本会向 stderr 输出提示并设置退出码2,方便调用方感知错误。另有两个开关:--json只输出 JSON(机器可读),--list列出所有支持的窗口关键字。
为什么 Agent 不该手工推算日期
技能文档反复强调"Do not derive dates by hand — this resolver is the source of truth"(不要手工推算日期——解析器才是事实来源),这不是教条,而是有实际工程原因的:
- 时区歧义:Agent 运行环境所在时区决定了"今天"的边界。同一个时刻,在 UTC+8 和 UTC-8 可能分属两个不同的自然日。手工推算极易把"本地今天"和"UTC 今天"混淆。
- 边界语义:查询时间戳时,用"今天 23:59:59"作为右边界会漏掉最后一毫秒,用"明天 00:00:00"作为开区间右边界才是无歧义的做法。
startUtc/endExclusiveUtc直接把这个陷阱消解掉了。 - 月份/季度/年份的边界算术:
civ()依赖 JSDate对越界字段的自动归一化(如day 0自动回退到上个月最后一天、month 12自动进到下一年一月),这让lastMonth、lastQuarter这类窗口的起止计算既简短又正确,手工推算则很容易在 2 月、闰年、季度切换处出错。
在 Munder Difflin 的实际运行中,这套约定已经写进了 worker 的启动引导语:在 src/main/index.ts 中,hive 派生 worker 时会注入一段提示,明确要求"任何时间范围限定的工作,先用这些技能解析日期,而不是手工计算",并把/today、/last30Days、/lastQuarter等列为 worker 的时序技能。这说明时间解析是 worker 执行时间敏感任务(今日报告、周报、季度回顾)的前置标准动作。
从 /today 扩展到整套时间窗口
/today只是 resources/skills/temporal/SKILL.md 中定义窗口列表里的一个。temporal 技能是"所有窗口的单一事实来源",支持以下命名窗口:
| 关键字 | 含义 |
|---|---|
today/yesterday | 单个自然日 |
thisWeek/lastWeek | ISO 周(周一为起始);this= 周一到今天,last= 上一完整周(周一至周日) |
last7Days/last30Days/last90Days | 截至今天的滚动 N 天窗口(含今天) |
thisMonth/lastMonth | this= 1 号到今天;last= 上一完整自然月 |
thisQuarter/lastQuarter | this= 季度首日到今天;last= 上一完整季度 |
thisYear/lastYear | this= 1 月 1 日到今天(YTD);last= 上一完整年 |
last12Months | 截至今天的滚动 12 个月 |
统一约定:this*窗口是"周期起点 → 今天"(to-date);last*命名周期是"上一个完整周期"。此外还支持任意滚动窗口lastNdays/lastNweeks/lastNmonths(例如last45days、last2weeks、last6months),以及一组别名:ytd、qtd、mtd、wtd、7d、30d、90d、12m。这些别名和lastN通配形式的实现都集中在when.mjs的build()函数里,通过正则^last(\d+)days?$、^last(\d+)weeks?$、^last(\d+)months?$匹配并换算成对应的起始日期。
对应地,仓库里为每个常用窗口都单独放置了技能快捷方式,内容和/today完全同构——例如 resources/skills/last30Days/SKILL.md(滚动 30 天)、resources/skills/thisWeek/SKILL.md(周一到今天)、resources/skills/yesterday/SKILL.md(昨天)——它们内部都指向同一个when.mjs,只是参数不同。这也是为什么需要/temporal作为完整入口:当任务窗口不在快捷技能之列(如last90Days、last12Months或任意lastNdays)时,直接调用:
# 全部窗口一次输出(快速的时间上下文快照) node "$AGENT_DIR/.claude/skills/temporal/when.mjs" # 一次请求一个或多个窗口 node "$AGENT_DIR/.claude/skills/temporal/when.mjs" last30Days thisQuarter # 只输出 JSON(机器可读),或列出支持的关键字 node "$AGENT_DIR/.claude/skills/temporal/when.mjs" --json last7Days node "$AGENT_DIR/.claude/skills/temporal/when.mjs" --list如果$AGENT_DIR意外未设置,脚本在~/.claude/skills/temporal/when.mjs也有一份副本可调用——when.mjs的文件头注释里明确说明了这一点,作为兜底路径。
在 hive worker 中的典型用法
结合 resources/skills/capabilities/SKILL.md 的能力目录,一个典型的时间敏感任务流程是这样的:
- worker 启动时由 hive 注入
AGENT_ID、AGENT_NAME、AGENT_DIR、HIVE_ROOT等环境变量; - 接到任务后,若任务带时间范围("汇总今天的日志"、"报告上周指标"、"梳理本季度发布"),先执行
node "$AGENT_DIR/.claude/skills/temporal/when.mjs" <窗口>拿到start/end与startUtc/endExclusiveUtc; - 把解析出的具体 ISO 边界作为查询条件传给后续的数据源或集成调用——能力目录明确建议"先解析日期窗口,再把具体的 ISO 边界传给集成查询";
- 任务完成后,向 god 发送一条
"act":"done"的 outbox 消息汇报结果(worker 是临时进程,不直接推送远端,god 是唯一集成者)。
整个过程遵守严格的边界:时间技能只读(仅读取系统时钟并打印到 stdout,不写文件、不联网),外部集成调用则统一经由 hive 的 loopback broker 代理,worker 侧不持有任何凭据。这套"解析器单一事实来源 + 半开 UTC 区间 + 只读边界"的设计,让所有派生 worker 在时间语义上保持一致,从根上消除了多智能体协作中最常见的日期歧义问题。
小结
/today是一个只读、无网络依赖的 Claude Code 技能,通过node "$AGENT_DIR/.claude/skills/temporal/when.mjs" today把"今天"解析成包含式自然日 + 半开 UTC 区间的完整日期记录;- 输出同时覆盖人类可读的
YYYY-MM-DD与机器查询友好的startUtc/endExclusiveUtc,并附带days、timezone、asOf等上下文; - 底层实现 resources/skills/temporal/when.mjs 用本地午夜 + JS Date 越界归一化完成全部日期算术,无任何第三方依赖,随处可用;
- 需要更广窗口(周、月、季度、
lastNdays)时,升级到/temporal技能或直接调用同一个解析器,整套窗口列表与别名都以它为单一事实来源。
【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考