dsh-daily-calendar
v0.1.0
Published
Calendar read + controlled write plugin for the DeepSeek Harness (developer preview)
Readme
calendar
dsh(DeepSeek Harness)日历插件:读取优先的日历访问——统一 CalendarProvider 接口下接 Google Calendar(API v3)与通用 CalDAV(iCloud / Fastmail / Nextcloud 等,Basic Auth / App 专用密码),提供列表、详情、空闲时段查询三个只读工具;创建/修改事件属高风险写操作,需在配置中显式开启 writesEnabled 才会注册。
状态:developer preview。已通过
tsc --noEmit类型检查(对齐@deepseek-ai/[email protected]的工具签名)与 56 例纯函数/注入替身单测;尚未在真实 dsh 运行时与真实日历账号中验证,手动验证步骤见下文。
功能
Provider 分层
工具层只面向统一的 CalendarProvider 接口(listEvents / getEvent / freeBusy / createEvent / updateEvent),服务商差异全部下沉到实现:
- google:Google Calendar API v3。
singleEvents=true让服务端展开循环事件;freeBusy用官方 freeBusy 接口。OAuth2 采用务实方案(见下文「Google token 获取」):用户自备 access token,可选配置 refresh 三件套实现过期自动刷新。 - caldav:通用 CalDAV(RFC 4791)。读取用
REPORT calendar-query+ 自研轻量 ICS 解析器(折行展开、TEXT 转义、UTC/浮动/TZID/纯日期四种时间格式);freeBusy由事件列表推导(所有 CalDAV 服务器都支持 calendar-query,而 free-busy-query 支持参差)。配置了calendarUrl则直接使用;否则从serverUrl自动发现(current-user-principal → calendar-home-set → 第一个日历集合)。主要目标场景是 iCloud + App 专用密码。
新服务商(如 Microsoft Graph)实现同一接口即可接入;网络层(fetch)、环境变量读取均可注入,单测不触网。
工具
| 工具 | 作用 | 注册条件 |
| --- | --- | --- |
| calendar_list | 按时间范围列出事件(默认今天起 7 天),含起止、标题、地点、循环标记 | 始终 |
| calendar_get | 单个事件详情(含描述、链接) | 始终 |
| calendar_freebusy | 给定范围与所需时长,返回合并后的忙碌块与所有足够长的空档 | 始终 |
| calendar_create | 创建事件(描述中标注【高风险·L2 每次确认】) | 仅 writesEnabled: true |
| calendar_update | 修改事件标题/时间/地点/描述(同上标注) | 仅 writesEnabled: true |
所有工具返回 output.schema 声明的结构化 JSON(时间为 UTC ISO 8601),渲染文本按配置时区本地化;全部请求带超时(默认 10s)、响应 exec.signal 取消、网络错误与 5xx 自动重试一次、401/403 转为可读的"凭证过期"错误。
配置
- id: calendar
name: dsh-daily-calendar
config:
enabled: true
provider: google # google | caldav
writesEnabled: false # 默认关:开启才注册 calendar_create / calendar_update
defaultDays: 7 # calendar_list 默认范围长度(天)
timeZone: '' # IANA 时区,如 Asia/Shanghai;空 = 系统时区
timeoutMs: 10000
retry: true
google:
calendarId: primary
accessTokenEnv: GOOGLE_CALENDAR_TOKEN
refreshTokenEnv: GOOGLE_CALENDAR_REFRESH_TOKEN
clientIdEnv: GOOGLE_CALENDAR_CLIENT_ID
clientSecretEnv: GOOGLE_CALENDAR_CLIENT_SECRET
caldav:
serverUrl: '' # 如 https://caldav.icloud.com,用于自动发现
calendarUrl: '' # 日历集合 URL;设置了就跳过自动发现
usernameEnv: CALDAV_USERNAME
passwordEnv: CALDAV_PASSWORD所有叶子字段都有默认值;但凭证只从环境变量读取——配置里写的只是环境变量的名字,插件从不把凭证写进配置文件、日志或历史。
Google token 获取
首版不内置 OAuth 授权跳转流程(那需要一个本地回调服务与浏览器交互,超出插件边界),采用用户自备 token 的务实方案:
- 在 Google Cloud Console 创建项目,启用 Google Calendar API。
- 在「API 和服务 → 凭据」创建 OAuth 客户端 ID(类型选"桌面应用"),得到 client id / client secret。
- 打开 OAuth 2.0 Playground:右上角齿轮勾选"Use your own OAuth credentials"并填入上一步的 client id/secret;在左侧 scopes 中输入
https://www.googleapis.com/auth/calendar(读写)或.../calendar.readonly(只读);走完授权后点"Exchange authorization code for tokens",得到 access token 与 refresh token。 - 设置环境变量:
GOOGLE_CALENDAR_TOKEN=<access token>。如需过期自动刷新,再设GOOGLE_CALENDAR_REFRESH_TOKEN/GOOGLE_CALENDAR_CLIENT_ID/GOOGLE_CALENDAR_CLIENT_SECRET三件套。 - 没有 refresh 三件套时,access token(约 1 小时)过期后首次请求会报"凭证已过期,请重新生成"的可读错误。
权衡:这把 OAuth 的复杂度转嫁给了首次配置,但换来了零本地端口、零浏览器依赖、凭证完全由用户掌控;刷新流是可选增强,没有也能用。
CalDAV(iCloud 示例)
- Apple 账户开启双重认证后,在 appleid.apple.com 生成 App 专用密码。
- 设环境变量:
CALDAV_USERNAME=<Apple ID 邮箱>、CALDAV_PASSWORD=<App 专用密码>。 - 配置
caldav.serverUrl: https://caldav.icloud.com让插件自动发现第一个日历集合;或直接在 iCloud 日历网页版复制日历集合 URL 填caldav.calendarUrl(跳过发现,最稳)。
Fastmail(https://caldav.fastmail.com/)与 Nextcloud(https://<host>/remote.php/dav)同理,用户名/密码或应用密码即可。
权限透明
- 读写位置:无。插件不读写任何本地文件;唯一状态是进程内存中的"刷新后的 Google access token"(不落盘,重启即失效)。
- 网络域名:
provider: google时仅www.googleapis.com(Calendar API)与oauth2.googleapis.com(仅配置 refresh 三件套且 token 过期时);provider: caldav时仅你配置的serverUrl/calendarUrl那一台服务器。除此之外无任何网络请求。 - 凭证:全部从环境变量读取——Google 侧
accessTokenEnv(默认GOOGLE_CALENDAR_TOKEN)及可选 refresh 三件套;CalDAV 侧usernameEnv/passwordEnv(默认CALDAV_USERNAME/CALDAV_PASSWORD)。凭证值不出现在日志、工具输出与错误信息中;日志只记录"哪些环境变量名已配置"。注意 Google 侧 access token 会以Authorization: Bearer头发往 googleapis.com,CalDAV 侧以 HTTP Basic 头发往你的服务器——均仅发往对应域名。 - 确认点:插件自身不发起 ask。三个读工具是只读操作;
calendar_create/calendar_update在描述中标注【高风险·L2 每次确认】,若同时启用 approval-guard,未知工具默认 L2(每次 ask),与本约定一致。 - 日志内容:仅启动一行(provider、时区、写工具开关、已配置的凭证变量名)与请求失败时的警告(含状态码,不含凭证、不含事件内容)。事件内容只出现在工具返回值中。
已知限制
- RRULE 展开范围有限:CalDAV 侧只展开
FREQ=DAILY/FREQ=WEEKLY(支持INTERVAL/COUNT/UNTIL,按事件时区做 DST 安全的日历日推演);其余规则(MONTHLY/YEARLY/BYDAY 等)只标记recurring: true并返回基准次,不展开。EXDATE/RDATE/VTIMEZONE不解析。Google 侧无此限制(服务端展开)。 - 无会议邀请收发:创建/修改事件不处理 attendees,不会发出邀请邮件。
- 无删除事件工具:写面刻意保持最小(创建 + 修改)。
- Google 侧 token 刷新后的新 access token 只保存在进程内存:重启后若旧 token 已过期会再刷新一次(有 refresh 三件套时无感)。
- CalDAV 自动发现取 calendar-home-set 下第一个日历集合;多日历用户请直接配
calendarUrl。 - CalDAV 的
updateEvent重建整份 VEVENT:原事件里本插件不识别的属性(提醒 VALARM、自定义扩展等)会丢失。Google 侧是 PATCH 语义,无此问题。 - 浮动时间(无 TZID 的 ICS 本地时间)按插件
timeZone(默认系统时区)解释。
手动验证
CI 只覆盖类型检查与纯函数/注入替身单测。接入真实 dsh 与真实日历账号后请按以下步骤验证:
- 在 dsh 配置中启用
calendar并设好凭证环境变量,启动日志应出现calendar: loaded(provider=google,…,凭证环境变量已配置:GOOGLE_CALENDAR_TOKEN)。 - 让 agent 调用
calendar_list(不传参数)→ 返回今天起 7 天内的事件列表;传from/to/days验证范围参数。 - 用上一步返回的 id 调
calendar_get→ 返回详情(描述、链接)。 - 调
calendar_freebusy(durationMinutes: 60)→ 返回的空档与日历实际占用一致。 - 把 access token 改成一个错误值再调
calendar_list→ 报"凭证无效或已过期"的可读错误,且错误信息中不含 token 本身。 - 开启
writesEnabled: true后调calendar_create创建一个测试事件 → 在 Google 日历网页 / iCloud 日历中确认出现;再调calendar_update改标题 → 确认同步。验证完毕建议改回writesEnabled: false。 - CalDAV 路线:先只配
serverUrl验证自动发现;发现失败时报错会提示改配calendarUrl。
许可
MIT
