dsh-calendar 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +31 -4
- package/README.md +30 -6
- package/lib/caldav.d.ts +5 -5
- package/lib/caldav.js +42 -10
- package/lib/config.d.ts +1 -0
- package/lib/config.js +2 -2
- package/lib/ical.js +5 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/tools.d.ts +2 -2
- package/lib/tools.js +76 -15
- package/package.json +63 -61
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stardustlc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# dsh-calendar
|
|
2
2
|
|
|
3
|
+
   
|
|
4
|
+
|
|
5
|
+
[](https://awesome-dsh-plugin.com)
|
|
6
|
+
|
|
3
7
|
DSH community plugin: read/write calendar events via CalDAV. Provides 5 model-facing tools (calendar_list / calendar_create / calendar_update / calendar_delete / calendar_search), supporting Google / iCloud / Nextcloud and any CalDAV server. This round is a node half-body with no settings-page UI; all configuration goes through the profile's cordis.patch.yml.
|
|
4
8
|
|
|
9
|
+
## Compatibility
|
|
10
|
+
|
|
11
|
+
Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
|
|
12
|
+
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -10,6 +18,15 @@ dsh plugin --profile web add dsh-calendar
|
|
|
10
18
|
|
|
11
19
|
After installing, restart dsh. The plugin inserts a `calendar` config line into the profile (see this package's cordis.patch.yml). The default provider is custom with no credentials filled in; the plugin still loads, but tools throw a Chinese guidance error when called, prompting you to complete the configuration.
|
|
12
20
|
|
|
21
|
+
## Uninstall
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
dsh plugin --profile web remove dsh-calendar
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
|
|
28
|
+
|
|
29
|
+
|
|
13
30
|
## Configuration
|
|
14
31
|
|
|
15
32
|
All configuration lives in your profile's cordis.patch.yml; override the `calendar` line by id (overriding replaces that line's config wholesale). Common fields:
|
|
@@ -92,11 +109,11 @@ If your CalDAV server is not directly reachable from your network (some regional
|
|
|
92
109
|
|
|
93
110
|
## Tool reference
|
|
94
111
|
|
|
95
|
-
- `calendar_list`: list events in a time range (start/end, ISO 8601; defaults to the next 7 days). Recurring events are expanded by default (`expand` defaults to true, `maxOccurrences` defaults to 30, clamped to 1-200): each occurrence is a separate row with `isOccurrence: true` and `seriesStart`; non-recurring events keep `isOccurrence: false`. With `expand=false`, recurring events are returned as a single original entry with `rrule
|
|
96
|
-
- `calendar_create`: create an event (summary/start/end required; description/location/allDay optional)
|
|
97
|
-
- `calendar_update`: edit an event by uid (summary/start/end/description/location/allDay optional; omitted fields keep their original values)
|
|
112
|
+
- `calendar_list`: list events in a time range (start/end, ISO 8601; defaults to the next 7 days). Recurring events are expanded by default (`expand` defaults to true, `maxOccurrences` defaults to 30, clamped to 1-200): each occurrence is a separate row with `isOccurrence: true` and `seriesStart`; non-recurring events keep `isOccurrence: false`. With `expand=false`, recurring events are returned as a single original entry with `rrule`. Results are stably sorted by start time.
|
|
113
|
+
- `calendar_create`: create an event (summary/start/end required; description/location/allDay/rrule optional). Validates real calendar dates and `end >= start`.
|
|
114
|
+
- `calendar_update`: edit an event by uid (summary/start/end/description/location/allDay/rrule optional; omitted fields keep their original values, including the recurrence rule).
|
|
98
115
|
- `calendar_delete`: delete an event by uid
|
|
99
|
-
- `calendar_search`: search events by keyword (client-side filter over title/description/location/UID, case-insensitive)
|
|
116
|
+
- `calendar_search`: search events by keyword (client-side filter over title/description/location/UID, case-insensitive; `limit` defaults to 50, clamped to 1-200, and results are sorted by start time).
|
|
100
117
|
|
|
101
118
|
The stable event identifier `uid` is the CalDAV href (full object URL); `calendar_update` / `calendar_delete` use it.
|
|
102
119
|
|
|
@@ -104,6 +121,16 @@ The stable event identifier `uid` is the CalDAV href (full object URL); `calenda
|
|
|
104
121
|
|
|
105
122
|
Input and output are uniformly ISO 8601. Timed events are output in UTC (e.g. `2025-01-15T01:00:00Z`); all-day events output `YYYY-MM-DD`. Input may carry a timezone offset (e.g. `2025-01-15T09:00:00+08:00`); the plugin converts to UTC internally for storage.
|
|
106
123
|
|
|
124
|
+
|
|
125
|
+
## Changelog
|
|
126
|
+
|
|
127
|
+
- **0.4.0**: new `calendar_health` self-check (one-call CalDAV config health: endpoint/username/password/connection).
|
|
128
|
+
- **0.3.2**:
|
|
129
|
+
- Fix `calendar_update` dropping `rrule` while updating other fields.
|
|
130
|
+
- Validate `end >= start` and reject impossible dates such as `2025-02-30`.
|
|
131
|
+
- Sort `calendar_list` / `calendar_search` output by start time and clamp search `limit` to 1-200.
|
|
132
|
+
- Reset the cached CalDAV client after creation failure so the next tool call can retry.
|
|
133
|
+
|
|
107
134
|
## Known limitations
|
|
108
135
|
|
|
109
136
|
- Recurring event expansion: calendar_list expands RRULE by default via ICAL.RecurExpansion (`expand=true`), capped by `maxOccurrences`; calendar_search still returns the original series (not expanded).
|
package/README.md
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
   
|
|
8
8
|
|
|
9
|
+
[](https://awesome-dsh-plugin.com)
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
DSH 社区插件:通过 CalDAV 读写日历事件。提供 5 个面向模型的工具(calendar_list / calendar_create / calendar_update / calendar_delete / calendar_search),支持 Google / iCloud / Nextcloud 及任意 CalDAV 服务器。本轮为 node 半身,不含设置页 UI,配置全部走 profile 的 cordis.patch.yml。
|
|
11
13
|
|
|
14
|
+
## 兼容性
|
|
15
|
+
|
|
16
|
+
在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
|
|
17
|
+
|
|
12
18
|
## 安装
|
|
13
19
|
|
|
14
20
|
```bash
|
|
@@ -29,6 +35,15 @@ dsh plugin --profile web add dsh-calendar
|
|
|
29
35
|
- `calendarId`:google 专用,日历 ID(通常是你的邮箱)
|
|
30
36
|
- `host` / `user` / `calendar`:nextcloud 专用
|
|
31
37
|
|
|
38
|
+
|
|
39
|
+
## 卸载
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile web remove dsh-calendar
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中的对应插件行。
|
|
46
|
+
|
|
32
47
|
## 中国用户:特殊代理配置(Google / iCloud)
|
|
33
48
|
|
|
34
49
|
Google 与 iCloud 的 CalDAV 端点在中国大陆**不可直连**,需要配合你常用的梯子/特殊代理使用。插件内置 `proxyUrl` 配置:把 CalDAV 请求路由到**你本机代理客户端的端口**,不影响其他插件,也无需改任何系统设置。
|
|
@@ -119,11 +134,11 @@ iCloud:登录 appleid.apple.com → 登录与安全 → App 专用密码,生
|
|
|
119
134
|
|
|
120
135
|
## 工具清单
|
|
121
136
|
|
|
122
|
-
- `calendar_list`:列出某时间段事件(start/end,ISO 8601,缺省未来 7 天)。默认展开重复事件(`expand` 默认 true,`maxOccurrences` 默认 30、clamp 1-200):每个实例独立成行,带 `isOccurrence: true` 与 `seriesStart`;非重复事件保持 `isOccurrence: false`。`expand=false` 时重复事件按原始单条返回并带 `rrule
|
|
123
|
-
- `calendar_create`:新建事件(summary/start/end 必填,description/location/allDay
|
|
124
|
-
- `calendar_update`:按 uid 改事件(summary/start/end/description/location/allDay
|
|
137
|
+
- `calendar_list`:列出某时间段事件(start/end,ISO 8601,缺省未来 7 天)。默认展开重复事件(`expand` 默认 true,`maxOccurrences` 默认 30、clamp 1-200):每个实例独立成行,带 `isOccurrence: true` 与 `seriesStart`;非重复事件保持 `isOccurrence: false`。`expand=false` 时重复事件按原始单条返回并带 `rrule`。结果按开始时间稳定排序
|
|
138
|
+
- `calendar_create`:新建事件(summary/start/end 必填,description/location/allDay/rrule 可选)。严格校验真实日历日期与 `end >= start`
|
|
139
|
+
- `calendar_update`:按 uid 改事件(summary/start/end/description/location/allDay/rrule 可选,未提供保留原值,重复规则不再丢失)
|
|
125
140
|
- `calendar_delete`:按 uid 删事件
|
|
126
|
-
- `calendar_search`:按关键词搜事件(客户端过滤标题/描述/地点/UID
|
|
141
|
+
- `calendar_search`:按关键词搜事件(客户端过滤标题/描述/地点/UID,不区分大小写;`limit` 默认 50、clamp 1-200,结果按开始时间排序)
|
|
127
142
|
|
|
128
143
|
事件稳定标识 `uid` 为 CalDAV href(完整对象 URL),`calendar_update` / `calendar_delete` 使用它。
|
|
129
144
|
|
|
@@ -131,6 +146,16 @@ iCloud:登录 appleid.apple.com → 登录与安全 → App 专用密码,生
|
|
|
131
146
|
|
|
132
147
|
输入输出统一 ISO 8601。定时事件输出为 UTC(如 `2025-01-15T01:00:00Z`),全天事件输出 `YYYY-MM-DD`。输入可带时区偏移(如 `2025-01-15T09:00:00+08:00`),插件内部转 UTC 存储。
|
|
133
148
|
|
|
149
|
+
## 版本记录
|
|
150
|
+
|
|
151
|
+
- **0.4.0**:新增 `calendar_health` 自检(CalDAV 配置完整性一键体检:端点/账号/密码/连接)。
|
|
152
|
+
- **0.3.2**:
|
|
153
|
+
- 修复 `calendar_update` 更新其他字段时丢失 `rrule` 的问题。
|
|
154
|
+
- 更新与新建都会校验 `end >= start`,并拒绝 `2025-02-30` 这类不存在的日期。
|
|
155
|
+
- `calendar_list` / `calendar_search` 输出按开始时间稳定排序;搜索 `limit` clamp 到 1-200。
|
|
156
|
+
- CalDAV 客户端创建失败后清空缓存,下一次调用可自动重试,不再永久复用 rejected promise。
|
|
157
|
+
|
|
158
|
+
|
|
134
159
|
## 已知限制
|
|
135
160
|
|
|
136
161
|
- **国内网络**:Google 与 iCloud 的 CalDAV 端点在中国大陆不可直连;可用 `proxyUrl` 走本机梯子/特殊代理,或改用国内可直连的 CalDAV 端点(自建 Nextcloud/Radicale)。
|
|
@@ -157,5 +182,4 @@ pnpm test # 构建 + node --test
|
|
|
157
182
|
|
|
158
183
|
- [dsh-calendar](https://github.com/STARDUSTLC666/dsh-calendar) — CalDAV 日历五件套
|
|
159
184
|
- [dsh-slack](https://github.com/STARDUSTLC666/dsh-slack) — Slack 通知/收件箱
|
|
160
|
-
- [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
|
|
161
|
-
|
|
185
|
+
- [dsh-dingtalk](https://github.com/STARDUSTLC666/dsh-dingtalk) — 钉钉群通知(零依赖)
|
package/lib/caldav.d.ts
CHANGED
|
@@ -23,20 +23,20 @@ export declare class CalendarService {
|
|
|
23
23
|
list(startIso: string, endIso: string, options?: {
|
|
24
24
|
expand?: boolean;
|
|
25
25
|
maxOccurrences?: number;
|
|
26
|
-
}): Promise<CalendarEvent[]>;
|
|
26
|
+
}, signal?: AbortSignal): Promise<CalendarEvent[]>;
|
|
27
27
|
/** 列出全部事件(客户端过滤用)。 */
|
|
28
|
-
all(): Promise<CalendarEvent[]>;
|
|
28
|
+
all(signal?: AbortSignal): Promise<CalendarEvent[]>;
|
|
29
29
|
private toEvents;
|
|
30
30
|
/** 列出并展开:每个对象经 expandEventFromICal 展开为若干实例行。 */
|
|
31
31
|
private toExpandedEvents;
|
|
32
32
|
/** 按 uid(href)找到服务器对象(含 etag 与原始 data)。 */
|
|
33
33
|
private findObject;
|
|
34
34
|
/** 新建事件,返回带 href/uid 的事件。 */
|
|
35
|
-
create(fields: EventFields): Promise<CalendarEvent>;
|
|
35
|
+
create(fields: EventFields, signal?: AbortSignal): Promise<CalendarEvent>;
|
|
36
36
|
/** 按 uid 更新事件;未提供的字段保留原值。 */
|
|
37
|
-
update(uid: string, changes: Partial<EventFields
|
|
37
|
+
update(uid: string, changes: Partial<EventFields>, signal?: AbortSignal): Promise<CalendarEvent>;
|
|
38
38
|
/** 按 uid 删除事件。 */
|
|
39
|
-
delete(uid: string): Promise<{
|
|
39
|
+
delete(uid: string, signal?: AbortSignal): Promise<{
|
|
40
40
|
uid: string;
|
|
41
41
|
href: string;
|
|
42
42
|
}>;
|
package/lib/caldav.js
CHANGED
|
@@ -62,35 +62,45 @@ export class CalendarService {
|
|
|
62
62
|
return { url: this.collectionUrl };
|
|
63
63
|
}
|
|
64
64
|
/** 列出某时间段内的事件;expand 为 true 时在窗口内展开 RRULE。 */
|
|
65
|
-
async list(startIso, endIso, options) {
|
|
65
|
+
async list(startIso, endIso, options, signal) {
|
|
66
66
|
const expand = options?.expand !== false;
|
|
67
67
|
const maxOccurrences = options?.maxOccurrences ?? 30;
|
|
68
|
+
signal?.throwIfAborted();
|
|
68
69
|
try {
|
|
69
70
|
const client = await this.client();
|
|
71
|
+
signal?.throwIfAborted();
|
|
70
72
|
const objects = await client.fetchCalendarObjects({
|
|
71
73
|
calendar: this.calendar(),
|
|
72
74
|
timeRange: { start: startIso, end: endIso },
|
|
73
75
|
urlFilter: (url) => typeof url === 'string' && url.length > 0,
|
|
76
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
74
77
|
});
|
|
78
|
+
signal?.throwIfAborted();
|
|
75
79
|
return expand
|
|
76
80
|
? this.toExpandedEvents(objects, startIso, endIso, maxOccurrences)
|
|
77
81
|
: this.toEvents(objects);
|
|
78
82
|
}
|
|
79
83
|
catch (error) {
|
|
84
|
+
signal?.throwIfAborted();
|
|
80
85
|
throw translateError(error, '读取日历');
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
/** 列出全部事件(客户端过滤用)。 */
|
|
84
|
-
async all() {
|
|
89
|
+
async all(signal) {
|
|
90
|
+
signal?.throwIfAborted();
|
|
85
91
|
try {
|
|
86
92
|
const client = await this.client();
|
|
93
|
+
signal?.throwIfAborted();
|
|
87
94
|
const objects = await client.fetchCalendarObjects({
|
|
88
95
|
calendar: this.calendar(),
|
|
89
96
|
urlFilter: (url) => typeof url === 'string' && url.length > 0,
|
|
97
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
90
98
|
});
|
|
99
|
+
signal?.throwIfAborted();
|
|
91
100
|
return this.toEvents(objects);
|
|
92
101
|
}
|
|
93
102
|
catch (error) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
94
104
|
throw translateError(error, '读取日历');
|
|
95
105
|
}
|
|
96
106
|
}
|
|
@@ -112,29 +122,39 @@ export class CalendarService {
|
|
|
112
122
|
return events;
|
|
113
123
|
}
|
|
114
124
|
/** 按 uid(href)找到服务器对象(含 etag 与原始 data)。 */
|
|
115
|
-
async findObject(uid) {
|
|
125
|
+
async findObject(uid, signal) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
116
127
|
const client = await this.client();
|
|
128
|
+
signal?.throwIfAborted();
|
|
117
129
|
const target = normalizeUrl(uid);
|
|
118
130
|
const objects = await client.fetchCalendarObjects({
|
|
119
131
|
calendar: this.calendar(),
|
|
120
132
|
urlFilter: (url) => normalizeUrl(url) === target,
|
|
133
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
121
134
|
});
|
|
135
|
+
signal?.throwIfAborted();
|
|
122
136
|
return objects.find((object) => normalizeUrl(object.url) === target);
|
|
123
137
|
}
|
|
124
138
|
/** 新建事件,返回带 href/uid 的事件。 */
|
|
125
|
-
async create(fields) {
|
|
126
|
-
|
|
127
|
-
const
|
|
139
|
+
async create(fields, signal) {
|
|
140
|
+
signal?.throwIfAborted();
|
|
141
|
+
const icalUid = fields.icalUid ?? generateUid();
|
|
142
|
+
const iCalString = buildICalString({ ...fields, icalUid });
|
|
143
|
+
const filename = icalUid + '.ics';
|
|
128
144
|
try {
|
|
129
145
|
const client = await this.client();
|
|
146
|
+
signal?.throwIfAborted();
|
|
130
147
|
const response = await client.createCalendarObject({
|
|
131
148
|
calendar: this.calendar(),
|
|
132
149
|
iCalString,
|
|
133
150
|
filename,
|
|
151
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
134
152
|
});
|
|
153
|
+
signal?.throwIfAborted();
|
|
135
154
|
assertOk(response, '新建事件');
|
|
136
155
|
}
|
|
137
156
|
catch (error) {
|
|
157
|
+
signal?.throwIfAborted();
|
|
138
158
|
if (error instanceof CalDAVError)
|
|
139
159
|
throw error;
|
|
140
160
|
throw translateError(error, '新建事件');
|
|
@@ -146,12 +166,14 @@ export class CalendarService {
|
|
|
146
166
|
return event;
|
|
147
167
|
}
|
|
148
168
|
/** 按 uid 更新事件;未提供的字段保留原值。 */
|
|
149
|
-
async update(uid, changes) {
|
|
169
|
+
async update(uid, changes, signal) {
|
|
170
|
+
signal?.throwIfAborted();
|
|
150
171
|
let object;
|
|
151
172
|
try {
|
|
152
|
-
object = await this.findObject(uid);
|
|
173
|
+
object = await this.findObject(uid, signal);
|
|
153
174
|
}
|
|
154
175
|
catch (error) {
|
|
176
|
+
signal?.throwIfAborted();
|
|
155
177
|
throw translateError(error, '查找事件');
|
|
156
178
|
}
|
|
157
179
|
if (object === undefined) {
|
|
@@ -182,12 +204,16 @@ export class CalendarService {
|
|
|
182
204
|
const iCalString = buildICalString(merged);
|
|
183
205
|
try {
|
|
184
206
|
const client = await this.client();
|
|
207
|
+
signal?.throwIfAborted();
|
|
185
208
|
const response = await client.updateCalendarObject({
|
|
186
209
|
calendarObject: { url: object.url, etag: object.etag, data: iCalString },
|
|
210
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
187
211
|
});
|
|
212
|
+
signal?.throwIfAborted();
|
|
188
213
|
assertOk(response, '更新事件');
|
|
189
214
|
}
|
|
190
215
|
catch (error) {
|
|
216
|
+
signal?.throwIfAborted();
|
|
191
217
|
if (error instanceof CalDAVError)
|
|
192
218
|
throw error;
|
|
193
219
|
throw translateError(error, '更新事件');
|
|
@@ -198,12 +224,14 @@ export class CalendarService {
|
|
|
198
224
|
return event;
|
|
199
225
|
}
|
|
200
226
|
/** 按 uid 删除事件。 */
|
|
201
|
-
async delete(uid) {
|
|
227
|
+
async delete(uid, signal) {
|
|
228
|
+
signal?.throwIfAborted();
|
|
202
229
|
let object;
|
|
203
230
|
try {
|
|
204
|
-
object = await this.findObject(uid);
|
|
231
|
+
object = await this.findObject(uid, signal);
|
|
205
232
|
}
|
|
206
233
|
catch (error) {
|
|
234
|
+
signal?.throwIfAborted();
|
|
207
235
|
throw translateError(error, '查找事件');
|
|
208
236
|
}
|
|
209
237
|
if (object === undefined) {
|
|
@@ -212,12 +240,16 @@ export class CalendarService {
|
|
|
212
240
|
}
|
|
213
241
|
try {
|
|
214
242
|
const client = await this.client();
|
|
243
|
+
signal?.throwIfAborted();
|
|
215
244
|
const response = await client.deleteCalendarObject({
|
|
216
245
|
calendarObject: { url: object.url, etag: object.etag },
|
|
246
|
+
...(signal !== undefined ? { fetchOptions: { signal } } : {}),
|
|
217
247
|
});
|
|
248
|
+
signal?.throwIfAborted();
|
|
218
249
|
assertOk(response, '删除事件');
|
|
219
250
|
}
|
|
220
251
|
catch (error) {
|
|
252
|
+
signal?.throwIfAborted();
|
|
221
253
|
if (error instanceof CalDAVError)
|
|
222
254
|
throw error;
|
|
223
255
|
throw translateError(error, '删除事件');
|
package/lib/config.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare const NEXTCLOUD_PATH = "/remote.php/dav/calendars/";
|
|
|
47
47
|
export declare class ConfigError extends Error {
|
|
48
48
|
constructor(message: string);
|
|
49
49
|
}
|
|
50
|
+
export declare const CALENDAR_PROVIDERS: readonly CalendarProvider[];
|
|
50
51
|
/**
|
|
51
52
|
* 解析配置为可用的 caldavUrl + 凭证。配置缺失抛出 ConfigError(中文指引)。
|
|
52
53
|
* @param config - 插件 config(可能 undefined)。
|
package/lib/config.js
CHANGED
|
@@ -15,7 +15,7 @@ export class ConfigError extends Error {
|
|
|
15
15
|
this.name = 'ConfigError';
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
const
|
|
18
|
+
export const CALENDAR_PROVIDERS = ['google', 'icloud', 'nextcloud', 'custom'];
|
|
19
19
|
function nonEmpty(value) {
|
|
20
20
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
21
21
|
}
|
|
@@ -25,7 +25,7 @@ function trimTrailingSlash(value) {
|
|
|
25
25
|
function normalizeProvider(value) {
|
|
26
26
|
if (value === undefined || value === null || value === '')
|
|
27
27
|
return 'custom';
|
|
28
|
-
if (typeof value === 'string' &&
|
|
28
|
+
if (typeof value === 'string' && CALENDAR_PROVIDERS.includes(value)) {
|
|
29
29
|
return value;
|
|
30
30
|
}
|
|
31
31
|
throw new ConfigError('dsh-calendar 的 provider 取值不合法:只支持 google / icloud / nextcloud / custom。' +
|
package/lib/ical.js
CHANGED
|
@@ -179,7 +179,12 @@ export function expandEventFromICal(data, href, etag, rangeStart, rangeEnd, maxO
|
|
|
179
179
|
const rangeEndMs = isoToEpochMs(rangeEnd);
|
|
180
180
|
const occurrences = [];
|
|
181
181
|
try {
|
|
182
|
+
const totalIterationCap = Math.max(100000, maxOccurrences * 1000);
|
|
183
|
+
let iterations = 0;
|
|
182
184
|
while (occurrences.length < maxOccurrences) {
|
|
185
|
+
iterations += 1;
|
|
186
|
+
if (iterations > totalIterationCap)
|
|
187
|
+
break;
|
|
183
188
|
const next = expansion.next();
|
|
184
189
|
if (next === null || next === undefined)
|
|
185
190
|
break;
|
package/lib/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { type CalendarConfig } from './config.js';
|
|
11
11
|
import { type CalendarToolDefinition } from './tools.js';
|
|
12
12
|
/** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明注入,否则宿主会抛 cannot get property without inject。 */
|
|
13
|
+
export declare const name = "calendar";
|
|
13
14
|
export declare const inject: string[];
|
|
14
15
|
/** 插件所需的最小 ctx 面(社区插件不依赖宿主内部类型)。 */
|
|
15
16
|
export interface CalendarPluginContext {
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { resolveConfig } from './config.js';
|
|
11
11
|
import { buildCalendarTools } from './tools.js';
|
|
12
12
|
/** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明注入,否则宿主会抛 cannot get property without inject。 */
|
|
13
|
+
export const name = 'calendar';
|
|
13
14
|
export const inject = ['tools'];
|
|
14
15
|
/**
|
|
15
16
|
* 插件入口:惰性解析配置并注册五个日历工具。
|
package/lib/tools.d.ts
CHANGED
|
@@ -26,5 +26,5 @@ export interface CalendarToolDefinition {
|
|
|
26
26
|
execute(args: unknown, exec: unknown): Promise<unknown>;
|
|
27
27
|
timeoutMs?: number;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
export declare function buildCalendarTools(config: CalendarConfig | undefined): CalendarToolDefinition[];
|
|
29
|
+
/** 构建六个工具定义;每个 execute 惰性解析配置,缺失时抛出中文指引。 */
|
|
30
|
+
export declare function buildCalendarTools(config: CalendarConfig | undefined, env?: NodeJS.ProcessEnv): CalendarToolDefinition[];
|
package/lib/tools.js
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
* @module dsh-calendar/tools
|
|
6
6
|
*/
|
|
7
7
|
import { CalendarService } from './caldav.js';
|
|
8
|
-
import { resolveConfig } from './config.js';
|
|
8
|
+
import { CALENDAR_PROVIDERS, resolveConfig } from './config.js';
|
|
9
9
|
import { compileParameters } from './parameters.js';
|
|
10
10
|
const EVENT_SCHEMA = { type: 'object', additionalProperties: true };
|
|
11
11
|
const TIMEOUT_MS = 60000;
|
|
12
12
|
function asRecord(args) {
|
|
13
13
|
return typeof args === 'object' && args !== null ? args : {};
|
|
14
14
|
}
|
|
15
|
+
function executionSignal(exec) {
|
|
16
|
+
if (typeof exec !== 'object' || exec === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const signal = exec.signal;
|
|
19
|
+
return signal instanceof AbortSignal ? signal : undefined;
|
|
20
|
+
}
|
|
15
21
|
function optionalString(args, key) {
|
|
16
22
|
const value = args[key];
|
|
17
23
|
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
@@ -96,9 +102,9 @@ function buildSearchFilter(query) {
|
|
|
96
102
|
return haystack.includes(needle);
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
|
-
/**
|
|
100
|
-
export function buildCalendarTools(config) {
|
|
101
|
-
const service = () => new CalendarService(resolveConfig(config));
|
|
105
|
+
/** 构建六个工具定义;每个 execute 惰性解析配置,缺失时抛出中文指引。 */
|
|
106
|
+
export function buildCalendarTools(config, env = process.env) {
|
|
107
|
+
const service = () => new CalendarService(resolveConfig(config, env));
|
|
102
108
|
const list = {
|
|
103
109
|
name: 'calendar_list',
|
|
104
110
|
description: '列出某时间段内的日历事件(默认未来 7 天)。start/end 为 ISO 8601 时间(含时区偏移,如 2025-01-01T09:00:00+08:00),全天事件返回 YYYY-MM-DD。默认展开重复事件(RRULE):每个实例作为独立行返回,start/end 为该次发生时间,并带 isOccurrence=true 与 seriesStart(系列原开始时间);非重复事件保持单行且 isOccurrence=false。expand=false 时重复事件按原始单条返回并带 rrule 字段。maxOccurrences 为每个重复事件的展开次数上限。返回每个事件的稳定标识 uid,供 calendar_update / calendar_delete 使用。',
|
|
@@ -128,7 +134,7 @@ export function buildCalendarTools(config) {
|
|
|
128
134
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
129
135
|
},
|
|
130
136
|
},
|
|
131
|
-
async execute(args) {
|
|
137
|
+
async execute(args, exec) {
|
|
132
138
|
const input = asRecord(args);
|
|
133
139
|
const now = new Date();
|
|
134
140
|
const defaultEnd = new Date(now.getTime() + 7 * 24 * 3600 * 1000);
|
|
@@ -139,7 +145,7 @@ export function buildCalendarTools(config) {
|
|
|
139
145
|
assertTimeRange(start, end);
|
|
140
146
|
const expand = booleanWithDefault(input, 'expand', true);
|
|
141
147
|
const maxOccurrences = clampedInteger(input, 'maxOccurrences', 30, 1, 200);
|
|
142
|
-
const events = sortEvents(await service().list(start, end, { expand, maxOccurrences }));
|
|
148
|
+
const events = sortEvents(await service().list(start, end, { expand, maxOccurrences }, executionSignal(exec)));
|
|
143
149
|
return { count: events.length, start, end, events };
|
|
144
150
|
},
|
|
145
151
|
timeoutMs: TIMEOUT_MS,
|
|
@@ -168,7 +174,7 @@ export function buildCalendarTools(config) {
|
|
|
168
174
|
return [{ type: 'text', text: '已新建事件:' + formatEvent(event) }];
|
|
169
175
|
},
|
|
170
176
|
},
|
|
171
|
-
async execute(args) {
|
|
177
|
+
async execute(args, exec) {
|
|
172
178
|
const input = asRecord(args);
|
|
173
179
|
const summary = requiredString(input, 'summary', '事件标题');
|
|
174
180
|
const start = requiredString(input, 'start', '开始时间');
|
|
@@ -189,7 +195,7 @@ export function buildCalendarTools(config) {
|
|
|
189
195
|
...(allDay !== undefined ? { allDay } : {}),
|
|
190
196
|
...(rrule !== undefined ? { rrule } : {}),
|
|
191
197
|
};
|
|
192
|
-
const created = await service().create(fields);
|
|
198
|
+
const created = await service().create(fields, executionSignal(exec));
|
|
193
199
|
return { created };
|
|
194
200
|
},
|
|
195
201
|
timeoutMs: TIMEOUT_MS,
|
|
@@ -219,7 +225,7 @@ export function buildCalendarTools(config) {
|
|
|
219
225
|
return [{ type: 'text', text: '已更新事件:' + formatEvent(event) }];
|
|
220
226
|
},
|
|
221
227
|
},
|
|
222
|
-
async execute(args) {
|
|
228
|
+
async execute(args, exec) {
|
|
223
229
|
const input = asRecord(args);
|
|
224
230
|
const uid = requiredString(input, 'uid', '事件 uid');
|
|
225
231
|
const changes = {};
|
|
@@ -248,7 +254,7 @@ export function buildCalendarTools(config) {
|
|
|
248
254
|
const rrule = optionalString(input, 'rrule');
|
|
249
255
|
if (rrule !== undefined)
|
|
250
256
|
changes.rrule = rrule;
|
|
251
|
-
const updated = await service().update(uid, changes);
|
|
257
|
+
const updated = await service().update(uid, changes, executionSignal(exec));
|
|
252
258
|
return { updated };
|
|
253
259
|
},
|
|
254
260
|
timeoutMs: TIMEOUT_MS,
|
|
@@ -275,10 +281,10 @@ export function buildCalendarTools(config) {
|
|
|
275
281
|
return [{ type: 'text', text: '已删除事件:uid=' + result.uid }];
|
|
276
282
|
},
|
|
277
283
|
},
|
|
278
|
-
async execute(args) {
|
|
284
|
+
async execute(args, exec) {
|
|
279
285
|
const input = asRecord(args);
|
|
280
286
|
const uid = requiredString(input, 'uid', '事件 uid');
|
|
281
|
-
const result = await service().delete(uid);
|
|
287
|
+
const result = await service().delete(uid, executionSignal(exec));
|
|
282
288
|
return { deleted: true, uid: result.uid, href: result.href };
|
|
283
289
|
},
|
|
284
290
|
timeoutMs: TIMEOUT_MS,
|
|
@@ -309,15 +315,70 @@ export function buildCalendarTools(config) {
|
|
|
309
315
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
310
316
|
},
|
|
311
317
|
},
|
|
312
|
-
async execute(args) {
|
|
318
|
+
async execute(args, exec) {
|
|
313
319
|
const input = asRecord(args);
|
|
314
320
|
const query = requiredString(input, 'query', '搜索关键词');
|
|
315
321
|
const limit = clampedInteger(input, 'limit', 50, 1, 200);
|
|
316
|
-
const all = await service().all();
|
|
322
|
+
const all = await service().all(executionSignal(exec));
|
|
317
323
|
const matched = sortEvents(all).filter(buildSearchFilter(query)).slice(0, limit);
|
|
318
324
|
return { query, count: matched.length, events: matched };
|
|
319
325
|
},
|
|
320
326
|
timeoutMs: TIMEOUT_MS,
|
|
321
327
|
};
|
|
322
|
-
|
|
328
|
+
const health = {
|
|
329
|
+
name: 'calendar_health',
|
|
330
|
+
description: 'dsh-calendar 自检:检查 CalDAV 配置完整性(服务商/日历地址/账号/密码),不发起网络连接。遇到问题时先运行本工具定位。',
|
|
331
|
+
parameters: compileParameters({}),
|
|
332
|
+
output: {
|
|
333
|
+
schema: { type: 'object', additionalProperties: true },
|
|
334
|
+
render: (_args, value) => {
|
|
335
|
+
const rec = (value ?? {});
|
|
336
|
+
const rawChecks = Array.isArray(rec.checks) ? rec.checks : [];
|
|
337
|
+
const lines = ['dsh-calendar 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
|
|
338
|
+
for (const item of rawChecks) {
|
|
339
|
+
const c = (item ?? {});
|
|
340
|
+
lines.push('- ' + String(c.name) + ':' + (c.ok === true ? '✅ ' + String(c.detail ?? '') : '❌ ' + String(c.detail ?? '')));
|
|
341
|
+
}
|
|
342
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
async execute(_args, exec) {
|
|
346
|
+
executionSignal(exec)?.throwIfAborted();
|
|
347
|
+
const checks = [];
|
|
348
|
+
const rawProvider = config?.provider;
|
|
349
|
+
const provider = rawProvider === undefined || rawProvider === null || rawProvider === '' ? 'custom' : String(rawProvider);
|
|
350
|
+
const providerOk = CALENDAR_PROVIDERS.includes(provider);
|
|
351
|
+
checks.push({ name: '服务商', ok: providerOk, detail: providerOk ? provider : '不支持 ' + provider + ';只支持 ' + CALENDAR_PROVIDERS.join(' / ') });
|
|
352
|
+
let resolved;
|
|
353
|
+
try {
|
|
354
|
+
resolved = resolveConfig(config, env);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Each individual check below provides the actionable detail.
|
|
358
|
+
}
|
|
359
|
+
const hasUser = typeof config?.username === 'string' && config.username.trim() !== '';
|
|
360
|
+
const configuredPassword = typeof config?.password === 'string' && config.password.trim() !== '';
|
|
361
|
+
const envPassword = typeof env.DSH_CALENDAR_PASSWORD === 'string' && env.DSH_CALENDAR_PASSWORD.trim() !== '';
|
|
362
|
+
try {
|
|
363
|
+
const endpoint = resolved ?? resolveConfig({
|
|
364
|
+
...config,
|
|
365
|
+
username: hasUser ? config.username : '__health_check__',
|
|
366
|
+
password: configuredPassword ? config.password : (envPassword ? env.DSH_CALENDAR_PASSWORD : '__health_check__'),
|
|
367
|
+
}, env);
|
|
368
|
+
checks.push({ name: '日历地址', ok: true, detail: '已解析为 ' + endpoint.caldavUrl });
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
checks.push({ name: '日历地址', ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
372
|
+
}
|
|
373
|
+
checks.push({ name: '账号', ok: hasUser, detail: hasUser ? '已配置' : '未配置:请填 username(Google/iCloud 为账号邮箱)' });
|
|
374
|
+
checks.push({
|
|
375
|
+
name: '密码',
|
|
376
|
+
ok: configuredPassword || envPassword,
|
|
377
|
+
detail: configuredPassword ? '已在插件配置中提供' : envPassword ? '已通过环境变量 DSH_CALENDAR_PASSWORD 提供' : '未配置:请填 password 或环境变量 DSH_CALENDAR_PASSWORD(Google/iCloud 用应用专用密码)',
|
|
378
|
+
});
|
|
379
|
+
return { ok: resolved !== undefined, plugin: 'dsh-calendar', checks };
|
|
380
|
+
},
|
|
381
|
+
timeoutMs: TIMEOUT_MS,
|
|
382
|
+
};
|
|
383
|
+
return [list, create, update, remove, search, health];
|
|
323
384
|
}
|
package/package.json
CHANGED
|
@@ -1,61 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-calendar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 日历工具插件:通过 CalDAV 读写日历事件(Google / iCloud / Nextcloud / 自定义)",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"types": "lib/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./lib/index.d.ts",
|
|
11
|
-
"default": "./lib/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"lib",
|
|
18
|
-
"cordis.patch.yml",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-calendar",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "DSH 日历工具插件:通过 CalDAV 读写日历事件(Google / iCloud / Nextcloud / 自定义)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
25
|
+
"prepublishOnly": "pnpm run build"
|
|
26
|
+
},
|
|
27
|
+
"dsh": {
|
|
28
|
+
"bundle": {
|
|
29
|
+
"patch": "./cordis.patch.yml"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"dsh",
|
|
34
|
+
"deepseek-harness",
|
|
35
|
+
"plugin",
|
|
36
|
+
"calendar",
|
|
37
|
+
"caldav",
|
|
38
|
+
"ical"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ical.js": "^2.1.0",
|
|
46
|
+
"tsdav": "^2.3.1",
|
|
47
|
+
"undici": "^8.10.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^24.0.0",
|
|
51
|
+
"typescript": "^5.6.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/STARDUSTLC666/dsh-calendar.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/STARDUSTLC666/dsh-calendar/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/STARDUSTLC666/dsh-calendar#readme",
|
|
61
|
+
"author": "stardustlc",
|
|
62
|
+
"packageManager": "pnpm@11.7.0"
|
|
63
|
+
}
|