dsh-calendar 0.1.0
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/README.md +118 -0
- package/cordis.patch.yml +19 -0
- package/lib/caldav.d.ts +38 -0
- package/lib/caldav.js +212 -0
- package/lib/config.d.ts +50 -0
- package/lib/config.js +83 -0
- package/lib/ical.d.ts +53 -0
- package/lib/ical.js +135 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +41 -0
- package/lib/parameters.d.ts +38 -0
- package/lib/parameters.js +36 -0
- package/lib/tools.d.ts +30 -0
- package/lib/tools.js +266 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# dsh-calendar
|
|
2
|
+
|
|
3
|
+
DSH 社区插件:通过 CalDAV 读写日历事件。提供 5 个面向模型的工具(calendar_list / calendar_create / calendar_update / calendar_delete / calendar_search),支持 Google / iCloud / Nextcloud 及任意 CalDAV 服务器。本轮为 node 半身,不含设置页 UI,配置全部走 profile 的 cordis.patch.yml。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dsh plugin --profile web add dsh-calendar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
安装后重启 dsh。插件会向 profile 插入一行 id 为 `calendar` 的配置行(见本包的 cordis.patch.yml)。默认 provider 为 custom 且未填任何凭证,此时插件照常加载,但工具在调用时会抛出中文指引错误,提示你补全配置。
|
|
12
|
+
|
|
13
|
+
## 配置
|
|
14
|
+
|
|
15
|
+
所有配置都在你的 profile 的 cordis.patch.yml 里,按 id 覆盖 `calendar` 行(覆盖整行的 config)。通用字段:
|
|
16
|
+
|
|
17
|
+
- `provider`:google | icloud | nextcloud | custom
|
|
18
|
+
- `caldavUrl`:完整日历集合 URL(custom / icloud 必填;google / nextcloud 也可手填覆盖预设)
|
|
19
|
+
- `username`:CalDAV 账号(Google / iCloud 为账号邮箱)
|
|
20
|
+
- `password`:密码;Google / iCloud 请用应用专用密码。推荐用环境变量 `DSH_CALENDAR_PASSWORD`,避免明文入库。
|
|
21
|
+
- `calendarId`:google 专用,日历 ID(通常是你的邮箱)
|
|
22
|
+
- `host` / `user` / `calendar`:nextcloud 专用
|
|
23
|
+
|
|
24
|
+
### Google 示例
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
- id: calendar
|
|
28
|
+
name: dsh-calendar
|
|
29
|
+
config:
|
|
30
|
+
provider: google
|
|
31
|
+
username: you@gmail.com
|
|
32
|
+
calendarId: you@gmail.com
|
|
33
|
+
# password 推荐用环境变量 DSH_CALENDAR_PASSWORD
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Google 的 CalDAV 集合 URL 由插件拼成:`https://apidata.googleusercontent.com/caldav/v2/<calendarId>/events`。
|
|
37
|
+
|
|
38
|
+
### iCloud 示例
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- id: calendar
|
|
42
|
+
name: dsh-calendar
|
|
43
|
+
config:
|
|
44
|
+
provider: icloud
|
|
45
|
+
username: you@icloud.com
|
|
46
|
+
caldavUrl: https://caldav.icloud.com/123456789/calendars/<日历ID>/
|
|
47
|
+
# password 推荐用环境变量 DSH_CALENDAR_PASSWORD
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
iCloud 需要完整日历集合 URL(含你的用户 ID 与日历 ID),在 icloud.com 的日历 CalDAV 设置里可找到具体日历地址。
|
|
51
|
+
|
|
52
|
+
### Nextcloud 示例
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
- id: calendar
|
|
56
|
+
name: dsh-calendar
|
|
57
|
+
config:
|
|
58
|
+
provider: nextcloud
|
|
59
|
+
username: alice
|
|
60
|
+
host: https://cloud.example.com
|
|
61
|
+
user: alice
|
|
62
|
+
calendar: personal
|
|
63
|
+
# password 推荐用环境变量 DSH_CALENDAR_PASSWORD
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
插件会拼成:`https://cloud.example.com/remote.php/dav/calendars/alice/personal/`。
|
|
67
|
+
|
|
68
|
+
### 自定义 CalDAV 示例
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
- id: calendar
|
|
72
|
+
name: dsh-calendar
|
|
73
|
+
config:
|
|
74
|
+
provider: custom
|
|
75
|
+
caldavUrl: https://dav.example.com/calendars/me/work/
|
|
76
|
+
username: me
|
|
77
|
+
# password 推荐用环境变量 DSH_CALENDAR_PASSWORD
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 应用专用密码指引
|
|
81
|
+
|
|
82
|
+
Google:登录 myaccount.google.com → 安全 → 两步验证(需先开启)→ 应用专用密码,选择「其他」生成 16 位密码,填到 `password` 或 `DSH_CALENDAR_PASSWORD`。不能用你的 Google 登录密码。
|
|
83
|
+
|
|
84
|
+
iCloud:登录 appleid.apple.com → 登录与安全 → App 专用密码,生成后填到 `password` 或 `DSH_CALENDAR_PASSWORD`。不能用你的 Apple ID 密码。
|
|
85
|
+
|
|
86
|
+
若调用报 401/403,多半是密码不对(用了登录密码而非应用专用密码),插件会返回中文提示。
|
|
87
|
+
|
|
88
|
+
## 工具清单
|
|
89
|
+
|
|
90
|
+
- `calendar_list`:列出某时间段事件(start/end,ISO 8601,缺省未来 7 天)
|
|
91
|
+
- `calendar_create`:新建事件(summary/start/end 必填,description/location/allDay 可选)
|
|
92
|
+
- `calendar_update`:按 uid 改事件(summary/start/end/description/location/allDay 可选,未提供保留原值)
|
|
93
|
+
- `calendar_delete`:按 uid 删事件
|
|
94
|
+
- `calendar_search`:按关键词搜事件(客户端过滤标题/描述/地点/UID,不区分大小写)
|
|
95
|
+
|
|
96
|
+
事件稳定标识 `uid` 为 CalDAV href(完整对象 URL),`calendar_update` / `calendar_delete` 使用它。
|
|
97
|
+
|
|
98
|
+
## 时间与时区
|
|
99
|
+
|
|
100
|
+
输入输出统一 ISO 8601。定时事件输出为 UTC(如 `2025-01-15T01:00:00Z`),全天事件输出 `YYYY-MM-DD`。输入可带时区偏移(如 `2025-01-15T09:00:00+08:00`),插件内部转 UTC 存储。
|
|
101
|
+
|
|
102
|
+
## 已知限制
|
|
103
|
+
|
|
104
|
+
- 重复事件(RRULE)不展开:calendar_list / calendar_search 返回的是原始事件时间与 `rrule` 字段,重复实例需要模型自行按 rrule 计算。
|
|
105
|
+
- 不做 OAuth:仅支持 Basic 认证(应用专用密码),不支持 Google / iCloud 的 OAuth 登录流程。
|
|
106
|
+
- 时区规则:带 TZID(命名时区)的事件输出会转成 UTC(Z);全天边界、夏令时等复杂时区规则不做精细化处理。
|
|
107
|
+
- 无设置页 UI:本轮为 node 半身,配置只走 cordis.patch.yml,不提供 Web 设置页。
|
|
108
|
+
- 日历发现:iCloud 需手动填完整日历集合 URL;不做 principal 自动发现与多日历选择。
|
|
109
|
+
- 取消/超时:工具依赖 timeoutMs(60 秒)做整体超时,不在单次网络请求上透传 AbortSignal。
|
|
110
|
+
|
|
111
|
+
## 开发
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm install
|
|
115
|
+
pnpm test # 构建 + node --test
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
构建产物在 `lib/`,测试在 `test/*.test.mjs`(不依赖真实账号)。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# dsh-calendar 组合包补丁:把日历工具插件行插入 profile。
|
|
2
|
+
# 本补丁按 id 覆盖;用户应在自己 profile 的 cordis.patch.yml 里覆盖本行的整个
|
|
3
|
+
# config(见 README 的配置示例)。配置缺失时插件仍会加载,但每个工具在调用时
|
|
4
|
+
# 会抛出带中文指引的错误,提示补全配置后重启。
|
|
5
|
+
#
|
|
6
|
+
# 示例(用户 profile 的 cordis.patch.yml):
|
|
7
|
+
# - id: calendar
|
|
8
|
+
# name: 'dsh-calendar'
|
|
9
|
+
# config:
|
|
10
|
+
# provider: google # google | icloud | nextcloud | custom
|
|
11
|
+
# username: you@gmail.com # CalDAV 账号(Google/iCloud 用账号邮箱)
|
|
12
|
+
# calendarId: you@gmail.com # google:日历 ID(通常是你的邮箱地址)
|
|
13
|
+
# # password: xxxx # 应用专用密码;推荐改用环境变量 DSH_CALENDAR_PASSWORD
|
|
14
|
+
#
|
|
15
|
+
- insert:
|
|
16
|
+
- id: calendar
|
|
17
|
+
name: 'dsh-calendar'
|
|
18
|
+
config:
|
|
19
|
+
provider: custom
|
package/lib/caldav.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CalDAV 访问层:用 tsdav 客户端对单个日历集合做 查询/新建/更新/删除。
|
|
3
|
+
* 直接用集合 URL 操作(createDAVClient 不带 defaultAccountType,跳过服务发现)。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/caldav
|
|
6
|
+
*/
|
|
7
|
+
import type { ResolvedConfig } from './config.js';
|
|
8
|
+
import { type CalendarEvent, type EventFields } from './ical.js';
|
|
9
|
+
/** CalDAV 操作错误:带中文指引。 */
|
|
10
|
+
export declare class CalDAVError extends Error {
|
|
11
|
+
readonly status?: number;
|
|
12
|
+
constructor(message: string, status?: number);
|
|
13
|
+
}
|
|
14
|
+
/** 对单个日历集合的封装。 */
|
|
15
|
+
export declare class CalendarService {
|
|
16
|
+
private readonly config;
|
|
17
|
+
private readonly collectionUrl;
|
|
18
|
+
private clientPromise?;
|
|
19
|
+
constructor(config: ResolvedConfig);
|
|
20
|
+
private client;
|
|
21
|
+
private calendar;
|
|
22
|
+
/** 列出某时间段内的事件(不展开 RRULE)。 */
|
|
23
|
+
list(startIso: string, endIso: string): Promise<CalendarEvent[]>;
|
|
24
|
+
/** 列出全部事件(客户端过滤用)。 */
|
|
25
|
+
all(): Promise<CalendarEvent[]>;
|
|
26
|
+
private toEvents;
|
|
27
|
+
/** 按 uid(href)找到服务器对象(含 etag 与原始 data)。 */
|
|
28
|
+
private findObject;
|
|
29
|
+
/** 新建事件,返回带 href/uid 的事件。 */
|
|
30
|
+
create(fields: EventFields): Promise<CalendarEvent>;
|
|
31
|
+
/** 按 uid 更新事件;未提供的字段保留原值。 */
|
|
32
|
+
update(uid: string, changes: Partial<EventFields>): Promise<CalendarEvent>;
|
|
33
|
+
/** 按 uid 删除事件。 */
|
|
34
|
+
delete(uid: string): Promise<{
|
|
35
|
+
uid: string;
|
|
36
|
+
href: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
package/lib/caldav.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CalDAV 访问层:用 tsdav 客户端对单个日历集合做 查询/新建/更新/删除。
|
|
3
|
+
* 直接用集合 URL 操作(createDAVClient 不带 defaultAccountType,跳过服务发现)。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/caldav
|
|
6
|
+
*/
|
|
7
|
+
import { createDAVClient } from 'tsdav';
|
|
8
|
+
import { buildICalString, generateUid, parseEventFromICal, } from './ical.js';
|
|
9
|
+
/** CalDAV 操作错误:带中文指引。 */
|
|
10
|
+
export class CalDAVError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'CalDAVError';
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function ensureTrailingSlash(value) {
|
|
19
|
+
return value.endsWith('/') ? value : value + '/';
|
|
20
|
+
}
|
|
21
|
+
function normalizeUrl(value) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
|
|
24
|
+
}
|
|
25
|
+
/** 把底层错误翻译成中文指引;识别 401/403 提示应用专用密码。 */
|
|
26
|
+
function translateError(error, action) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
const status = error?.status;
|
|
29
|
+
if (status === 401 || status === 403 || /401|403/.test(message)) {
|
|
30
|
+
return new CalDAVError('账号认证失败(401/403):请检查应用专用密码是否正确生成——' +
|
|
31
|
+
'Google 需在账号安全里创建「应用专用密码」,iCloud 需在 appleid.apple.com 创建 app 专用密码,' +
|
|
32
|
+
'不能用登录密码。请在 profile 的 cordis.patch.yml 覆盖 calendar 行或设置环境变量 DSH_CALENDAR_PASSWORD 后重启。', status ?? (/401/.test(message) ? 401 : 403));
|
|
33
|
+
}
|
|
34
|
+
return new CalDAVError(action + ' 失败:' + message, status);
|
|
35
|
+
}
|
|
36
|
+
/** 对单个日历集合的封装。 */
|
|
37
|
+
export class CalendarService {
|
|
38
|
+
config;
|
|
39
|
+
collectionUrl;
|
|
40
|
+
clientPromise;
|
|
41
|
+
constructor(config) {
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.collectionUrl = ensureTrailingSlash(config.caldavUrl);
|
|
44
|
+
}
|
|
45
|
+
client() {
|
|
46
|
+
this.clientPromise ??= createDAVClient({
|
|
47
|
+
serverUrl: this.config.caldavUrl,
|
|
48
|
+
credentials: { username: this.config.username, password: this.config.password },
|
|
49
|
+
authMethod: 'Basic',
|
|
50
|
+
});
|
|
51
|
+
return this.clientPromise;
|
|
52
|
+
}
|
|
53
|
+
calendar() {
|
|
54
|
+
return { url: this.collectionUrl };
|
|
55
|
+
}
|
|
56
|
+
/** 列出某时间段内的事件(不展开 RRULE)。 */
|
|
57
|
+
async list(startIso, endIso) {
|
|
58
|
+
try {
|
|
59
|
+
const client = await this.client();
|
|
60
|
+
const objects = await client.fetchCalendarObjects({
|
|
61
|
+
calendar: this.calendar(),
|
|
62
|
+
timeRange: { start: startIso, end: endIso },
|
|
63
|
+
urlFilter: (url) => typeof url === 'string' && url.length > 0,
|
|
64
|
+
});
|
|
65
|
+
return this.toEvents(objects);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw translateError(error, '读取日历');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** 列出全部事件(客户端过滤用)。 */
|
|
72
|
+
async all() {
|
|
73
|
+
try {
|
|
74
|
+
const client = await this.client();
|
|
75
|
+
const objects = await client.fetchCalendarObjects({
|
|
76
|
+
calendar: this.calendar(),
|
|
77
|
+
urlFilter: (url) => typeof url === 'string' && url.length > 0,
|
|
78
|
+
});
|
|
79
|
+
return this.toEvents(objects);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw translateError(error, '读取日历');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
toEvents(objects) {
|
|
86
|
+
const events = [];
|
|
87
|
+
for (const object of objects) {
|
|
88
|
+
const event = parseEventFromICal(String(object.data ?? ''), object.url, object.etag);
|
|
89
|
+
if (event !== null)
|
|
90
|
+
events.push(event);
|
|
91
|
+
}
|
|
92
|
+
return events;
|
|
93
|
+
}
|
|
94
|
+
/** 按 uid(href)找到服务器对象(含 etag 与原始 data)。 */
|
|
95
|
+
async findObject(uid) {
|
|
96
|
+
const client = await this.client();
|
|
97
|
+
const target = normalizeUrl(uid);
|
|
98
|
+
const objects = await client.fetchCalendarObjects({
|
|
99
|
+
calendar: this.calendar(),
|
|
100
|
+
urlFilter: (url) => normalizeUrl(url) === target,
|
|
101
|
+
});
|
|
102
|
+
return objects.find((object) => normalizeUrl(object.url) === target);
|
|
103
|
+
}
|
|
104
|
+
/** 新建事件,返回带 href/uid 的事件。 */
|
|
105
|
+
async create(fields) {
|
|
106
|
+
const iCalString = buildICalString(fields);
|
|
107
|
+
const filename = (fields.icalUid ?? generateUid()) + '.ics';
|
|
108
|
+
try {
|
|
109
|
+
const client = await this.client();
|
|
110
|
+
const response = await client.createCalendarObject({
|
|
111
|
+
calendar: this.calendar(),
|
|
112
|
+
iCalString,
|
|
113
|
+
filename,
|
|
114
|
+
});
|
|
115
|
+
assertOk(response, '新建事件');
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof CalDAVError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw translateError(error, '新建事件');
|
|
121
|
+
}
|
|
122
|
+
const href = new URL(filename, this.collectionUrl).href;
|
|
123
|
+
const event = parseEventFromICal(iCalString, href);
|
|
124
|
+
if (event === null)
|
|
125
|
+
throw new CalDAVError('新建事件失败:生成的 iCal 无法解析');
|
|
126
|
+
return event;
|
|
127
|
+
}
|
|
128
|
+
/** 按 uid 更新事件;未提供的字段保留原值。 */
|
|
129
|
+
async update(uid, changes) {
|
|
130
|
+
let object;
|
|
131
|
+
try {
|
|
132
|
+
object = await this.findObject(uid);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw translateError(error, '查找事件');
|
|
136
|
+
}
|
|
137
|
+
if (object === undefined) {
|
|
138
|
+
throw new CalDAVError('找不到 uid 对应的事件:请用 calendar_list 或 calendar_search 重新获取最新 uid,' +
|
|
139
|
+
'该事件可能已被删除或 uid 已过期。');
|
|
140
|
+
}
|
|
141
|
+
const existing = parseEventFromICal(String(object.data ?? ''), object.url, object.etag);
|
|
142
|
+
const start = changes.start ?? existing?.start;
|
|
143
|
+
const end = changes.end ?? existing?.end;
|
|
144
|
+
if (start === undefined || end === undefined) {
|
|
145
|
+
throw new CalDAVError('无法确定事件的开始/结束时间:请同时提供 start 与 end。');
|
|
146
|
+
}
|
|
147
|
+
const merged = {
|
|
148
|
+
summary: changes.summary ?? existing?.summary ?? '',
|
|
149
|
+
start,
|
|
150
|
+
end,
|
|
151
|
+
...(changes.description !== undefined ? { description: changes.description } : existing?.description !== undefined ? { description: existing.description } : {}),
|
|
152
|
+
...(changes.location !== undefined ? { location: changes.location } : existing?.location !== undefined ? { location: existing.location } : {}),
|
|
153
|
+
allDay: changes.allDay ?? existing?.allDay,
|
|
154
|
+
...(existing?.icalUid !== undefined ? { icalUid: existing.icalUid } : {}),
|
|
155
|
+
};
|
|
156
|
+
const iCalString = buildICalString(merged);
|
|
157
|
+
try {
|
|
158
|
+
const client = await this.client();
|
|
159
|
+
const response = await client.updateCalendarObject({
|
|
160
|
+
calendarObject: { url: object.url, etag: object.etag, data: iCalString },
|
|
161
|
+
});
|
|
162
|
+
assertOk(response, '更新事件');
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof CalDAVError)
|
|
166
|
+
throw error;
|
|
167
|
+
throw translateError(error, '更新事件');
|
|
168
|
+
}
|
|
169
|
+
const event = parseEventFromICal(iCalString, object.url, object.etag);
|
|
170
|
+
if (event === null)
|
|
171
|
+
throw new CalDAVError('更新事件失败:生成的 iCal 无法解析');
|
|
172
|
+
return event;
|
|
173
|
+
}
|
|
174
|
+
/** 按 uid 删除事件。 */
|
|
175
|
+
async delete(uid) {
|
|
176
|
+
let object;
|
|
177
|
+
try {
|
|
178
|
+
object = await this.findObject(uid);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw translateError(error, '查找事件');
|
|
182
|
+
}
|
|
183
|
+
if (object === undefined) {
|
|
184
|
+
throw new CalDAVError('找不到 uid 对应的事件:请用 calendar_list 或 calendar_search 重新获取最新 uid,' +
|
|
185
|
+
'该事件可能已被删除或 uid 已过期。');
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const client = await this.client();
|
|
189
|
+
const response = await client.deleteCalendarObject({
|
|
190
|
+
calendarObject: { url: object.url, etag: object.etag },
|
|
191
|
+
});
|
|
192
|
+
assertOk(response, '删除事件');
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof CalDAVError)
|
|
196
|
+
throw error;
|
|
197
|
+
throw translateError(error, '删除事件');
|
|
198
|
+
}
|
|
199
|
+
return { uid: object.url, href: object.url };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** 校验 HTTP 响应,把 401/403 与其它非 2xx 转成中文错误。 */
|
|
203
|
+
function assertOk(response, action) {
|
|
204
|
+
if (response.status === 401 || response.status === 403) {
|
|
205
|
+
throw new CalDAVError('账号认证失败(' + response.status + '):请检查应用专用密码是否正确生成——' +
|
|
206
|
+
'Google 需在账号安全里创建「应用专用密码」,iCloud 需在 appleid.apple.com 创建 app 专用密码,' +
|
|
207
|
+
'不能用登录密码。请在 profile 的 cordis.patch.yml 覆盖 calendar 行或设置环境变量 DSH_CALENDAR_PASSWORD 后重启。', response.status);
|
|
208
|
+
}
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new CalDAVError(action + ' 失败:服务器返回 ' + response.status + ' ' + response.statusText, response.status);
|
|
211
|
+
}
|
|
212
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日历插件配置解析:provider 预设 + 手填 URL,全部在调用时惰性解析。
|
|
3
|
+
* 配置缺失绝不阻止插件加载,只在工具 execute 时抛出带中文指引的错误。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/config
|
|
6
|
+
*/
|
|
7
|
+
/** 支持的 provider 预设。 */
|
|
8
|
+
export type CalendarProvider = 'google' | 'icloud' | 'nextcloud' | 'custom';
|
|
9
|
+
/** 插件配置:可在 profile 的 cordis.patch.yml 覆盖 calendar 行的整个 config。 */
|
|
10
|
+
export interface CalendarConfig {
|
|
11
|
+
/** provider 预设;默认 custom。 */
|
|
12
|
+
provider?: CalendarProvider;
|
|
13
|
+
/** 完整日历集合 URL;custom / icloud 必填,google / nextcloud 可由此手填覆盖预设。 */
|
|
14
|
+
caldavUrl?: string;
|
|
15
|
+
/** CalDAV 账号(Google / iCloud 用账号邮箱)。 */
|
|
16
|
+
username?: string;
|
|
17
|
+
/** 密码;支持环境变量 DSH_CALENDAR_PASSWORD。Google / iCloud 请用应用专用密码。 */
|
|
18
|
+
password?: string;
|
|
19
|
+
/** google:日历 ID(通常是你的邮箱地址)。 */
|
|
20
|
+
calendarId?: string;
|
|
21
|
+
/** nextcloud:主机,如 https://cloud.example.com。 */
|
|
22
|
+
host?: string;
|
|
23
|
+
/** nextcloud:用户名。 */
|
|
24
|
+
user?: string;
|
|
25
|
+
/** nextcloud:日历名。 */
|
|
26
|
+
calendar?: string;
|
|
27
|
+
}
|
|
28
|
+
/** 解析完成、可直接建客户端的配置。 */
|
|
29
|
+
export interface ResolvedConfig {
|
|
30
|
+
provider: CalendarProvider;
|
|
31
|
+
caldavUrl: string;
|
|
32
|
+
username: string;
|
|
33
|
+
password: string;
|
|
34
|
+
}
|
|
35
|
+
/** 预设端点常量。 */
|
|
36
|
+
export declare const GOOGLE_CALDAV_PREFIX = "https://apidata.googleusercontent.com/caldav/v2/";
|
|
37
|
+
export declare const ICLOUD_CALDAV_URL = "https://caldav.icloud.com";
|
|
38
|
+
export declare const NEXTCLOUD_PATH = "/remote.php/dav/calendars/";
|
|
39
|
+
/** 配置错误:带中文指引,模型和用户都能读。 */
|
|
40
|
+
export declare class ConfigError extends Error {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析配置为可用的 caldavUrl + 凭证。配置缺失抛出 ConfigError(中文指引)。
|
|
45
|
+
* @param config - 插件 config(可能 undefined)。
|
|
46
|
+
* @param env - 环境变量来源(测试可注入)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveConfig(config: CalendarConfig | undefined, env?: NodeJS.ProcessEnv): ResolvedConfig;
|
|
49
|
+
/** 依据 provider 预设或手填字段拼出日历集合 URL。 */
|
|
50
|
+
export declare function buildCaldavUrl(config: CalendarConfig, provider: CalendarProvider): string;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日历插件配置解析:provider 预设 + 手填 URL,全部在调用时惰性解析。
|
|
3
|
+
* 配置缺失绝不阻止插件加载,只在工具 execute 时抛出带中文指引的错误。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/config
|
|
6
|
+
*/
|
|
7
|
+
/** 预设端点常量。 */
|
|
8
|
+
export const GOOGLE_CALDAV_PREFIX = 'https://apidata.googleusercontent.com/caldav/v2/';
|
|
9
|
+
export const ICLOUD_CALDAV_URL = 'https://caldav.icloud.com';
|
|
10
|
+
export const NEXTCLOUD_PATH = '/remote.php/dav/calendars/';
|
|
11
|
+
/** 配置错误:带中文指引,模型和用户都能读。 */
|
|
12
|
+
export class ConfigError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ConfigError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const PROVIDERS = ['google', 'icloud', 'nextcloud', 'custom'];
|
|
19
|
+
function nonEmpty(value) {
|
|
20
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
21
|
+
}
|
|
22
|
+
function trimTrailingSlash(value) {
|
|
23
|
+
return value.replace(/\/+$/, '');
|
|
24
|
+
}
|
|
25
|
+
function normalizeProvider(value) {
|
|
26
|
+
if (value === undefined || value === null || value === '')
|
|
27
|
+
return 'custom';
|
|
28
|
+
if (typeof value === 'string' && PROVIDERS.includes(value)) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
throw new ConfigError('dsh-calendar 的 provider 取值不合法:只支持 google / icloud / nextcloud / custom。' +
|
|
32
|
+
'请在 profile 的 cordis.patch.yml 覆盖 calendar 行的 provider 并重启。');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析配置为可用的 caldavUrl + 凭证。配置缺失抛出 ConfigError(中文指引)。
|
|
36
|
+
* @param config - 插件 config(可能 undefined)。
|
|
37
|
+
* @param env - 环境变量来源(测试可注入)。
|
|
38
|
+
*/
|
|
39
|
+
export function resolveConfig(config, env = process.env) {
|
|
40
|
+
const provider = normalizeProvider(config?.provider);
|
|
41
|
+
const username = nonEmpty(config?.username);
|
|
42
|
+
const password = nonEmpty(config?.password) ?? nonEmpty(env.DSH_CALENDAR_PASSWORD);
|
|
43
|
+
if (username === undefined) {
|
|
44
|
+
throw new ConfigError('dsh-calendar 未配置 username:请在 profile 的 cordis.patch.yml 覆盖 calendar 行的 config,' +
|
|
45
|
+
'填上 CalDAV 账号(Google / iCloud 为账号邮箱)后重启。');
|
|
46
|
+
}
|
|
47
|
+
if (password === undefined) {
|
|
48
|
+
throw new ConfigError('dsh-calendar 未配置密码:请设置环境变量 DSH_CALENDAR_PASSWORD,' +
|
|
49
|
+
'或在 profile 的 cordis.patch.yml 覆盖 calendar 行的 password(Google / iCloud 请用应用专用密码)后重启。');
|
|
50
|
+
}
|
|
51
|
+
const caldavUrl = buildCaldavUrl(config ?? {}, provider);
|
|
52
|
+
return { provider, caldavUrl, username, password };
|
|
53
|
+
}
|
|
54
|
+
/** 依据 provider 预设或手填字段拼出日历集合 URL。 */
|
|
55
|
+
export function buildCaldavUrl(config, provider) {
|
|
56
|
+
const explicit = nonEmpty(config.caldavUrl);
|
|
57
|
+
if (explicit !== undefined)
|
|
58
|
+
return explicit;
|
|
59
|
+
if (provider === 'google') {
|
|
60
|
+
const calendarId = nonEmpty(config.calendarId);
|
|
61
|
+
if (calendarId === undefined) {
|
|
62
|
+
throw new ConfigError('dsh-calendar 的 provider=google 需要 calendarId(日历 ID,通常是你的邮箱地址):' +
|
|
63
|
+
'请在 profile 的 cordis.patch.yml 覆盖 calendar 行的 calendarId 并重启。');
|
|
64
|
+
}
|
|
65
|
+
return GOOGLE_CALDAV_PREFIX + encodeURIComponent(calendarId) + '/events';
|
|
66
|
+
}
|
|
67
|
+
if (provider === 'nextcloud') {
|
|
68
|
+
const host = nonEmpty(config.host);
|
|
69
|
+
const user = nonEmpty(config.user);
|
|
70
|
+
const calendar = nonEmpty(config.calendar);
|
|
71
|
+
if (host === undefined || user === undefined || calendar === undefined) {
|
|
72
|
+
throw new ConfigError('dsh-calendar 的 provider=nextcloud 需要 host、user、calendar 三个字段:' +
|
|
73
|
+
'请在 profile 的 cordis.patch.yml 覆盖 calendar 行并重启。');
|
|
74
|
+
}
|
|
75
|
+
return trimTrailingSlash(host) + NEXTCLOUD_PATH + encodeURIComponent(user) + '/' + encodeURIComponent(calendar) + '/';
|
|
76
|
+
}
|
|
77
|
+
if (provider === 'icloud') {
|
|
78
|
+
throw new ConfigError('dsh-calendar 的 provider=icloud 需要完整日历集合 URL:请在 icloud.com 的日历设置里找到具体日历的 CalDAV 地址,' +
|
|
79
|
+
'填入 profile 的 cordis.patch.yml 覆盖 calendar 行的 caldavUrl(形如 ' + ICLOUD_CALDAV_URL + '/<用户ID>/calendars/<日历ID>/)后重启。');
|
|
80
|
+
}
|
|
81
|
+
throw new ConfigError('dsh-calendar 未配置 caldavUrl:provider=custom 时需提供完整日历集合 URL,' +
|
|
82
|
+
'请在 profile 的 cordis.patch.yml 覆盖 calendar 行的 caldavUrl 并重启。');
|
|
83
|
+
}
|
package/lib/ical.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iCal 解析与生成:用 ical.js 处理 VEVENT 的字段提取与 round-trip 序列化。
|
|
3
|
+
* 输入输出统一 ISO 8601(含时区偏移):全天事件用 YYYY-MM-DD,定时事件转 UTC(Z)。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/ical
|
|
6
|
+
*/
|
|
7
|
+
/** 一个暴露给模型的日历事件。 */
|
|
8
|
+
export interface CalendarEvent {
|
|
9
|
+
/** 稳定标识(CalDAV href),calendar_update / calendar_delete 用它。 */
|
|
10
|
+
uid: string;
|
|
11
|
+
/** 与 uid 相同的完整对象 URL,便于调试。 */
|
|
12
|
+
href: string;
|
|
13
|
+
/** iCal 里的 UID 原始值(可能为空)。 */
|
|
14
|
+
icalUid?: string;
|
|
15
|
+
/** 服务器 ETag(内部用于并发安全更新)。 */
|
|
16
|
+
etag?: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
location?: string;
|
|
20
|
+
/** 开始时间,ISO 8601(定时事件为 UTC,全天为 YYYY-MM-DD)。 */
|
|
21
|
+
start: string;
|
|
22
|
+
/** 结束时间,同上;无 DTEND/DURATION 时等于 start。 */
|
|
23
|
+
end: string;
|
|
24
|
+
allDay: boolean;
|
|
25
|
+
/** 重复规则原样返回,不展开(见 README 已知限制)。 */
|
|
26
|
+
rrule?: string;
|
|
27
|
+
status?: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
created?: string;
|
|
30
|
+
lastModified?: string;
|
|
31
|
+
}
|
|
32
|
+
/** 新建 / 更新事件时需要的字段。 */
|
|
33
|
+
export interface EventFields {
|
|
34
|
+
summary: string;
|
|
35
|
+
start: string;
|
|
36
|
+
end: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
location?: string;
|
|
39
|
+
allDay?: boolean;
|
|
40
|
+
/** iCal UID;创建时缺省则自动生成,更新时用于保留原 UID。 */
|
|
41
|
+
icalUid?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析一段 iCal 文本中的首个 VEVENT 为 CalendarEvent;解析失败返回 null。
|
|
45
|
+
* @param data - iCal 文本(通常来自服务器的 calendar-data)。
|
|
46
|
+
* @param href - CalDAV 对象 href,作为稳定 uid。
|
|
47
|
+
* @param etag - 服务器 ETag。
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseEventFromICal(data: string, href: string, etag?: string): CalendarEvent | null;
|
|
50
|
+
/** 生成随机 iCal UID(带 host 后缀,形如 UUID)。 */
|
|
51
|
+
export declare function generateUid(): string;
|
|
52
|
+
/** 把字段生成一段完整 iCal 文本(单个 VEVENT)。 */
|
|
53
|
+
export declare function buildICalString(fields: EventFields): string;
|
package/lib/ical.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iCal 解析与生成:用 ical.js 处理 VEVENT 的字段提取与 round-trip 序列化。
|
|
3
|
+
* 输入输出统一 ISO 8601(含时区偏移):全天事件用 YYYY-MM-DD,定时事件转 UTC(Z)。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/ical
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import ICAL from 'ical.js';
|
|
9
|
+
function pad(value) {
|
|
10
|
+
return value < 10 ? '0' + value : String(value);
|
|
11
|
+
}
|
|
12
|
+
function isDateOnly(value) {
|
|
13
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
14
|
+
}
|
|
15
|
+
/** 把任意 hydrated 值安全转成字符串。 */
|
|
16
|
+
function asString(value) {
|
|
17
|
+
if (value === null || value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (typeof value === 'string')
|
|
20
|
+
return value;
|
|
21
|
+
if (typeof value === 'object' && 'toICALString' in value) {
|
|
22
|
+
const rendered = value.toICALString();
|
|
23
|
+
return typeof rendered === 'string' ? rendered : undefined;
|
|
24
|
+
}
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
/** ICAL.Time -> ISO 8601:全天 YYYY-MM-DD,定时转 UTC(Z,去毫秒)。 */
|
|
28
|
+
function icalTimeToIso(time) {
|
|
29
|
+
if (time.isDate) {
|
|
30
|
+
return pad(time.year) + '-' + pad(time.month) + '-' + pad(time.day);
|
|
31
|
+
}
|
|
32
|
+
return time.toJSDate().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
33
|
+
}
|
|
34
|
+
function timePropertyValue(vevent, name) {
|
|
35
|
+
const value = vevent.getFirstPropertyValue(name);
|
|
36
|
+
return value instanceof ICAL.Time ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 解析一段 iCal 文本中的首个 VEVENT 为 CalendarEvent;解析失败返回 null。
|
|
40
|
+
* @param data - iCal 文本(通常来自服务器的 calendar-data)。
|
|
41
|
+
* @param href - CalDAV 对象 href,作为稳定 uid。
|
|
42
|
+
* @param etag - 服务器 ETag。
|
|
43
|
+
*/
|
|
44
|
+
export function parseEventFromICal(data, href, etag) {
|
|
45
|
+
let vcal;
|
|
46
|
+
try {
|
|
47
|
+
vcal = new ICAL.Component(ICAL.parse(data));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const vevent = vcal.getFirstSubcomponent('vevent');
|
|
53
|
+
if (vevent === null)
|
|
54
|
+
return null;
|
|
55
|
+
const icalUid = asString(vevent.getFirstPropertyValue('uid'));
|
|
56
|
+
const summary = asString(vevent.getFirstPropertyValue('summary')) ?? '';
|
|
57
|
+
const description = asString(vevent.getFirstPropertyValue('description'));
|
|
58
|
+
const location = asString(vevent.getFirstPropertyValue('location'));
|
|
59
|
+
const status = asString(vevent.getFirstPropertyValue('status'));
|
|
60
|
+
const url = asString(vevent.getFirstPropertyValue('url'));
|
|
61
|
+
const created = timePropertyValue(vevent, 'created');
|
|
62
|
+
const lastModified = timePropertyValue(vevent, 'last-modified');
|
|
63
|
+
const dtstartProp = vevent.getFirstProperty('dtstart');
|
|
64
|
+
const dtstart = dtstartProp === null ? undefined : (dtstartProp.getFirstValue() instanceof ICAL.Time ? dtstartProp.getFirstValue() : undefined);
|
|
65
|
+
if (dtstart === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
const dtendProp = vevent.getFirstProperty('dtend');
|
|
68
|
+
const dtend = dtendProp === null ? undefined : (dtendProp.getFirstValue() instanceof ICAL.Time ? dtendProp.getFirstValue() : undefined);
|
|
69
|
+
const duration = vevent.getFirstPropertyValue('duration');
|
|
70
|
+
const start = icalTimeToIso(dtstart);
|
|
71
|
+
const allDay = dtstart.isDate === true;
|
|
72
|
+
let end = dtend === undefined ? undefined : icalTimeToIso(dtend);
|
|
73
|
+
if (end === undefined && duration instanceof ICAL.Duration) {
|
|
74
|
+
const endTime = dtstart.clone();
|
|
75
|
+
endTime.addDuration(duration);
|
|
76
|
+
end = icalTimeToIso(endTime);
|
|
77
|
+
}
|
|
78
|
+
if (end === undefined)
|
|
79
|
+
end = start;
|
|
80
|
+
const rruleValue = vevent.getFirstPropertyValue('rrule');
|
|
81
|
+
const rrule = rruleValue instanceof ICAL.Recur ? rruleValue.toString() : undefined;
|
|
82
|
+
const event = {
|
|
83
|
+
uid: href,
|
|
84
|
+
href,
|
|
85
|
+
summary,
|
|
86
|
+
start,
|
|
87
|
+
end,
|
|
88
|
+
allDay,
|
|
89
|
+
...(icalUid !== undefined ? { icalUid } : {}),
|
|
90
|
+
...(etag !== undefined ? { etag } : {}),
|
|
91
|
+
...(description !== undefined ? { description } : {}),
|
|
92
|
+
...(location !== undefined ? { location } : {}),
|
|
93
|
+
...(rrule !== undefined ? { rrule } : {}),
|
|
94
|
+
...(status !== undefined ? { status } : {}),
|
|
95
|
+
...(url !== undefined ? { url } : {}),
|
|
96
|
+
...(created !== undefined ? { created: icalTimeToIso(created) } : {}),
|
|
97
|
+
...(lastModified !== undefined ? { lastModified: icalTimeToIso(lastModified) } : {}),
|
|
98
|
+
};
|
|
99
|
+
return event;
|
|
100
|
+
}
|
|
101
|
+
/** 生成随机 iCal UID(带 host 后缀,形如 UUID)。 */
|
|
102
|
+
export function generateUid() {
|
|
103
|
+
return randomUUID();
|
|
104
|
+
}
|
|
105
|
+
/** 把字段生成一段完整 iCal 文本(单个 VEVENT)。 */
|
|
106
|
+
export function buildICalString(fields) {
|
|
107
|
+
const vcal = new ICAL.Component('vcalendar');
|
|
108
|
+
const vevent = new ICAL.Component('vevent');
|
|
109
|
+
vevent.addPropertyWithValue('uid', fields.icalUid ?? generateUid());
|
|
110
|
+
vevent.addPropertyWithValue('summary', fields.summary);
|
|
111
|
+
const start = parseTime(fields.start, fields.allDay === true || isDateOnly(fields.start));
|
|
112
|
+
const end = parseTime(fields.end, fields.allDay === true || isDateOnly(fields.end));
|
|
113
|
+
vevent.addPropertyWithValue('dtstart', start);
|
|
114
|
+
vevent.addPropertyWithValue('dtend', end);
|
|
115
|
+
if (fields.description !== undefined && fields.description !== '') {
|
|
116
|
+
vevent.addPropertyWithValue('description', fields.description);
|
|
117
|
+
}
|
|
118
|
+
if (fields.location !== undefined && fields.location !== '') {
|
|
119
|
+
vevent.addPropertyWithValue('location', fields.location);
|
|
120
|
+
}
|
|
121
|
+
vcal.addSubcomponent(vevent);
|
|
122
|
+
return vcal.toString();
|
|
123
|
+
}
|
|
124
|
+
/** 把 ISO 字符串解析成 ICAL.Time(全天 YYYY-MM-DD 或转 UTC 的定时时间)。 */
|
|
125
|
+
function parseTime(value, allDay) {
|
|
126
|
+
if (allDay || isDateOnly(value)) {
|
|
127
|
+
return ICAL.Time.fromDateString(value);
|
|
128
|
+
}
|
|
129
|
+
const date = new Date(value);
|
|
130
|
+
if (Number.isNaN(date.getTime())) {
|
|
131
|
+
throw new Error('时间格式不合法:' + value + '(应为 ISO 8601,含时区偏移,如 2025-01-01T09:00:00+08:00)');
|
|
132
|
+
}
|
|
133
|
+
// 用 UTC 瞬间构造,序列化为带 Z 的稳定形式,避免时区歧义。
|
|
134
|
+
return ICAL.Time.fromJSDate(date, true);
|
|
135
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-calendar —— CalDAV 日历工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
|
+
*
|
|
4
|
+
* 插件导出 apply(ctx, config):把五个面向模型的工具(calendar_list / calendar_create /
|
|
5
|
+
* calendar_update / calendar_delete / calendar_search)注册进宿主进程的工具注册表。
|
|
6
|
+
* 配置缺失时插件照常加载,工具在 execute 时才抛出带中文指引的错误。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-calendar
|
|
9
|
+
*/
|
|
10
|
+
import { type CalendarConfig } from './config.js';
|
|
11
|
+
import { type CalendarToolDefinition } from './tools.js';
|
|
12
|
+
/** 插件所需的最小 ctx 面(社区插件不依赖宿主内部类型)。 */
|
|
13
|
+
export interface CalendarPluginContext {
|
|
14
|
+
tools: {
|
|
15
|
+
register(definition: CalendarToolDefinition): () => void;
|
|
16
|
+
};
|
|
17
|
+
on?(event: string, listener: () => void): () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 插件入口:惰性解析配置并注册五个日历工具。
|
|
21
|
+
* @param ctx - 宿主上下文(至少含 tools.register)。
|
|
22
|
+
* @param config - 插件配置(可缺省)。
|
|
23
|
+
*/
|
|
24
|
+
export declare function apply(ctx: CalendarPluginContext, config?: CalendarConfig | null): void;
|
|
25
|
+
export default apply;
|
|
26
|
+
export * from './parameters.js';
|
|
27
|
+
export * from './config.js';
|
|
28
|
+
export * from './ical.js';
|
|
29
|
+
export * from './caldav.js';
|
|
30
|
+
export * from './tools.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-calendar —— CalDAV 日历工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
|
+
*
|
|
4
|
+
* 插件导出 apply(ctx, config):把五个面向模型的工具(calendar_list / calendar_create /
|
|
5
|
+
* calendar_update / calendar_delete / calendar_search)注册进宿主进程的工具注册表。
|
|
6
|
+
* 配置缺失时插件照常加载,工具在 execute 时才抛出带中文指引的错误。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-calendar
|
|
9
|
+
*/
|
|
10
|
+
import { resolveConfig } from './config.js';
|
|
11
|
+
import { buildCalendarTools } from './tools.js';
|
|
12
|
+
/**
|
|
13
|
+
* 插件入口:惰性解析配置并注册五个日历工具。
|
|
14
|
+
* @param ctx - 宿主上下文(至少含 tools.register)。
|
|
15
|
+
* @param config - 插件配置(可缺省)。
|
|
16
|
+
*/
|
|
17
|
+
export function apply(ctx, config) {
|
|
18
|
+
const cfg = config ?? {};
|
|
19
|
+
try {
|
|
20
|
+
resolveConfig(cfg);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.warn('dsh-calendar: ' + (error instanceof Error ? error.message : String(error)));
|
|
24
|
+
}
|
|
25
|
+
const disposers = [];
|
|
26
|
+
for (const definition of buildCalendarTools(cfg)) {
|
|
27
|
+
disposers.push(ctx.tools.register(definition));
|
|
28
|
+
}
|
|
29
|
+
if (typeof ctx.on === 'function') {
|
|
30
|
+
ctx.on('dispose', () => {
|
|
31
|
+
for (const dispose of disposers)
|
|
32
|
+
dispose();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default apply;
|
|
37
|
+
export * from './parameters.js';
|
|
38
|
+
export * from './config.js';
|
|
39
|
+
export * from './ical.js';
|
|
40
|
+
export * from './caldav.js';
|
|
41
|
+
export * from './tools.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具参数 DSL 编译:把作者友好的参数描述编译成注册所需的【编译好的 JSON Schema】。
|
|
3
|
+
*
|
|
4
|
+
* 这是社区插件必须遵守的坑:ctx.tools.register 的 parameters 字段必须是
|
|
5
|
+
* { type: 'object', properties: {...}, required?: [...] } 形式的 JSON Schema,
|
|
6
|
+
* 绝不能塞原始 DSL({ summary: { type: 'string' } } 这种),否则标准模式下 DeepSeek
|
|
7
|
+
* API 会以 "schema must be a JSON Schema of 'type: object'" 拒绝请求。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-calendar/parameters
|
|
10
|
+
*/
|
|
11
|
+
/** 单个参数属性支持的类型。 */
|
|
12
|
+
export type ParameterType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
|
13
|
+
/** 单个参数属性的 DSL 描述。 */
|
|
14
|
+
export interface ParameterSpec {
|
|
15
|
+
type: ParameterType;
|
|
16
|
+
/** 是否为必填参数;编译进顶层 required 数组。 */
|
|
17
|
+
required?: boolean;
|
|
18
|
+
/** 人类可读描述,投影进 JSON Schema 的 description 注解。 */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** 可选枚举约束(标量类型)。 */
|
|
21
|
+
enum?: readonly (string | number | boolean)[];
|
|
22
|
+
/** 数组元素的类型(type: 'array' 时生效)。 */
|
|
23
|
+
items?: Exclude<ParameterType, 'array' | 'object'>;
|
|
24
|
+
}
|
|
25
|
+
/** 参数 DSL 映射:键为参数名,值为属性描述。 */
|
|
26
|
+
export type ParameterDsl = Record<string, ParameterSpec>;
|
|
27
|
+
/** 编译后的 JSON Schema(object 根)。 */
|
|
28
|
+
export interface CompiledParameters {
|
|
29
|
+
type: 'object';
|
|
30
|
+
properties: Record<string, Record<string, unknown>>;
|
|
31
|
+
required?: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 把参数 DSL 编译成注册用的 JSON Schema。
|
|
35
|
+
* @param dsl - 参数描述映射。
|
|
36
|
+
* @returns 以 object 为根的编译后 schema。
|
|
37
|
+
*/
|
|
38
|
+
export declare function compileParameters(dsl: ParameterDsl): CompiledParameters;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具参数 DSL 编译:把作者友好的参数描述编译成注册所需的【编译好的 JSON Schema】。
|
|
3
|
+
*
|
|
4
|
+
* 这是社区插件必须遵守的坑:ctx.tools.register 的 parameters 字段必须是
|
|
5
|
+
* { type: 'object', properties: {...}, required?: [...] } 形式的 JSON Schema,
|
|
6
|
+
* 绝不能塞原始 DSL({ summary: { type: 'string' } } 这种),否则标准模式下 DeepSeek
|
|
7
|
+
* API 会以 "schema must be a JSON Schema of 'type: object'" 拒绝请求。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-calendar/parameters
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 把参数 DSL 编译成注册用的 JSON Schema。
|
|
13
|
+
* @param dsl - 参数描述映射。
|
|
14
|
+
* @returns 以 object 为根的编译后 schema。
|
|
15
|
+
*/
|
|
16
|
+
export function compileParameters(dsl) {
|
|
17
|
+
const properties = {};
|
|
18
|
+
const required = [];
|
|
19
|
+
for (const [name, spec] of Object.entries(dsl)) {
|
|
20
|
+
const property = { type: spec.type };
|
|
21
|
+
if (spec.enum !== undefined)
|
|
22
|
+
property.enum = [...spec.enum];
|
|
23
|
+
if (spec.description !== undefined)
|
|
24
|
+
property.description = spec.description;
|
|
25
|
+
if (spec.type === 'array' && spec.items !== undefined) {
|
|
26
|
+
property.items = { type: spec.items };
|
|
27
|
+
}
|
|
28
|
+
properties[name] = property;
|
|
29
|
+
if (spec.required === true)
|
|
30
|
+
required.push(name);
|
|
31
|
+
}
|
|
32
|
+
const schema = { type: 'object', properties };
|
|
33
|
+
if (required.length > 0)
|
|
34
|
+
schema.required = required;
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 五个面向模型的日历工具:list / create / update / delete / search。
|
|
3
|
+
* 直接调用 ctx.tools.register 注册【编译好的 JSON Schema】参数与 canonical 输出。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/tools
|
|
6
|
+
*/
|
|
7
|
+
import { type CalendarConfig } from './config.js';
|
|
8
|
+
/** 模型可见的内容块。 */
|
|
9
|
+
export interface ContentBlock {
|
|
10
|
+
type: 'text';
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
/** 注册给 ctx.tools.register 的原始工具定义(parameters 为编译好的 JSON Schema)。 */
|
|
14
|
+
export interface CalendarToolDefinition {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object';
|
|
19
|
+
properties: Record<string, unknown>;
|
|
20
|
+
required?: string[];
|
|
21
|
+
};
|
|
22
|
+
output: {
|
|
23
|
+
schema: Record<string, unknown>;
|
|
24
|
+
render(args: unknown, value: unknown): ContentBlock[];
|
|
25
|
+
};
|
|
26
|
+
execute(args: unknown, exec: unknown): Promise<unknown>;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** 构建五个工具定义;每个 execute 惰性解析配置,缺失时抛出中文指引。 */
|
|
30
|
+
export declare function buildCalendarTools(config: CalendarConfig | undefined): CalendarToolDefinition[];
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 五个面向模型的日历工具:list / create / update / delete / search。
|
|
3
|
+
* 直接调用 ctx.tools.register 注册【编译好的 JSON Schema】参数与 canonical 输出。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-calendar/tools
|
|
6
|
+
*/
|
|
7
|
+
import { CalendarService } from './caldav.js';
|
|
8
|
+
import { resolveConfig } from './config.js';
|
|
9
|
+
import { compileParameters } from './parameters.js';
|
|
10
|
+
const EVENT_SCHEMA = { type: 'object', additionalProperties: true };
|
|
11
|
+
const TIMEOUT_MS = 60000;
|
|
12
|
+
function asRecord(args) {
|
|
13
|
+
return typeof args === 'object' && args !== null ? args : {};
|
|
14
|
+
}
|
|
15
|
+
function optionalString(args, key) {
|
|
16
|
+
const value = args[key];
|
|
17
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
function requiredString(args, key, label) {
|
|
20
|
+
const value = optionalString(args, key);
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function optionalBoolean(args, key) {
|
|
27
|
+
const value = args[key];
|
|
28
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
function optionalInteger(args, key) {
|
|
31
|
+
const value = args[key];
|
|
32
|
+
return typeof value === 'number' && Number.isInteger(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function assertIsoTime(value, label) {
|
|
35
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
36
|
+
return;
|
|
37
|
+
if (Number.isNaN(new Date(value).getTime())) {
|
|
38
|
+
throw new Error(label + ' 不是合法的 ISO 8601 时间,例如 2025-01-01T09:00:00+08:00 或 2025-01-01T09:00:00Z。');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isoNoMillis(value) {
|
|
42
|
+
return value.replace(/\.\d{3}Z$/, 'Z');
|
|
43
|
+
}
|
|
44
|
+
function formatEvent(event) {
|
|
45
|
+
const range = event.allDay ? event.start + '(全天)' : event.start + ' ~ ' + event.end;
|
|
46
|
+
const extra = [event.location ? '地点:' + event.location : '', event.rrule ? '重复:' + event.rrule : '']
|
|
47
|
+
.filter(Boolean).join(';');
|
|
48
|
+
return '- ' + event.summary + '(' + range + ')' + (extra ? ',' + extra : '') + ',uid=' + event.uid;
|
|
49
|
+
}
|
|
50
|
+
function buildSearchFilter(query) {
|
|
51
|
+
const needle = query.toLowerCase();
|
|
52
|
+
return (event) => {
|
|
53
|
+
const haystack = [event.summary, event.description ?? '', event.location ?? '', event.icalUid ?? '']
|
|
54
|
+
.join(' ').toLowerCase();
|
|
55
|
+
return haystack.includes(needle);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** 构建五个工具定义;每个 execute 惰性解析配置,缺失时抛出中文指引。 */
|
|
59
|
+
export function buildCalendarTools(config) {
|
|
60
|
+
const service = () => new CalendarService(resolveConfig(config));
|
|
61
|
+
const list = {
|
|
62
|
+
name: 'calendar_list',
|
|
63
|
+
description: '列出某时间段内的日历事件(默认未来 7 天)。start/end 为 ISO 8601 时间(含时区偏移,如 2025-01-01T09:00:00+08:00),全天事件返回 YYYY-MM-DD。重复事件不展开,原始重复规则见每个事件的 rrule 字段。返回每个事件的稳定标识 uid,供 calendar_update / calendar_delete 使用。',
|
|
64
|
+
parameters: compileParameters({
|
|
65
|
+
start: { type: 'string', description: '起始时间(ISO 8601,含时区偏移)。缺省为当前时间(UTC)。' },
|
|
66
|
+
end: { type: 'string', description: '结束时间(ISO 8601,含时区偏移)。缺省为 start 后 7 天。' },
|
|
67
|
+
}),
|
|
68
|
+
output: {
|
|
69
|
+
schema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
additionalProperties: true,
|
|
72
|
+
properties: {
|
|
73
|
+
count: { type: 'integer' },
|
|
74
|
+
start: { type: 'string' },
|
|
75
|
+
end: { type: 'string' },
|
|
76
|
+
events: { type: 'array', items: EVENT_SCHEMA },
|
|
77
|
+
},
|
|
78
|
+
required: ['count', 'events'],
|
|
79
|
+
},
|
|
80
|
+
render: (_args, value) => {
|
|
81
|
+
const result = value;
|
|
82
|
+
const lines = ['时间段 ' + result.start + ' ~ ' + result.end + ' 内共 ' + result.count + ' 个事件:'];
|
|
83
|
+
for (const event of result.events)
|
|
84
|
+
lines.push(formatEvent(event));
|
|
85
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
async execute(args) {
|
|
89
|
+
const input = asRecord(args);
|
|
90
|
+
const now = new Date();
|
|
91
|
+
const defaultEnd = new Date(now.getTime() + 7 * 24 * 3600 * 1000);
|
|
92
|
+
const start = optionalString(input, 'start') ?? isoNoMillis(now.toISOString());
|
|
93
|
+
const end = optionalString(input, 'end') ?? isoNoMillis(defaultEnd.toISOString());
|
|
94
|
+
assertIsoTime(start, 'start');
|
|
95
|
+
assertIsoTime(end, 'end');
|
|
96
|
+
const events = await service().list(start, end);
|
|
97
|
+
return { count: events.length, start, end, events };
|
|
98
|
+
},
|
|
99
|
+
timeoutMs: TIMEOUT_MS,
|
|
100
|
+
};
|
|
101
|
+
const create = {
|
|
102
|
+
name: 'calendar_create',
|
|
103
|
+
description: '新建一个日历事件。summary 与 start、end(ISO 8601,含时区偏移)为必填;description、location、allDay 可选。全天事件 start/end 用 YYYY-MM-DD 或设置 allDay=true。成功返回新事件的稳定标识 uid。',
|
|
104
|
+
parameters: compileParameters({
|
|
105
|
+
summary: { type: 'string', required: true, description: '事件标题(必填)。' },
|
|
106
|
+
start: { type: 'string', required: true, description: '开始时间(ISO 8601,含时区偏移,必填)。' },
|
|
107
|
+
end: { type: 'string', required: true, description: '结束时间(ISO 8601,含时区偏移,必填)。' },
|
|
108
|
+
description: { type: 'string', description: '事件描述(可选)。' },
|
|
109
|
+
location: { type: 'string', description: '地点(可选)。' },
|
|
110
|
+
allDay: { type: 'boolean', description: '是否全天事件(可选,默认 false)。' },
|
|
111
|
+
}),
|
|
112
|
+
output: {
|
|
113
|
+
schema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
additionalProperties: true,
|
|
116
|
+
properties: { created: EVENT_SCHEMA },
|
|
117
|
+
required: ['created'],
|
|
118
|
+
},
|
|
119
|
+
render: (_args, value) => {
|
|
120
|
+
const event = value.created;
|
|
121
|
+
return [{ type: 'text', text: '已新建事件:' + formatEvent(event) }];
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async execute(args) {
|
|
125
|
+
const input = asRecord(args);
|
|
126
|
+
const summary = requiredString(input, 'summary', '事件标题');
|
|
127
|
+
const start = requiredString(input, 'start', '开始时间');
|
|
128
|
+
const end = requiredString(input, 'end', '结束时间');
|
|
129
|
+
assertIsoTime(start, 'start');
|
|
130
|
+
assertIsoTime(end, 'end');
|
|
131
|
+
const fields = {
|
|
132
|
+
summary,
|
|
133
|
+
start,
|
|
134
|
+
end,
|
|
135
|
+
...(optionalString(input, 'description') !== undefined ? { description: optionalString(input, 'description') } : {}),
|
|
136
|
+
...(optionalString(input, 'location') !== undefined ? { location: optionalString(input, 'location') } : {}),
|
|
137
|
+
...(optionalBoolean(input, 'allDay') !== undefined ? { allDay: optionalBoolean(input, 'allDay') } : {}),
|
|
138
|
+
};
|
|
139
|
+
const created = await service().create(fields);
|
|
140
|
+
return { created };
|
|
141
|
+
},
|
|
142
|
+
timeoutMs: TIMEOUT_MS,
|
|
143
|
+
};
|
|
144
|
+
const update = {
|
|
145
|
+
name: 'calendar_update',
|
|
146
|
+
description: '按 uid 修改日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。summary、start、end、description、location、allDay 均可选,未提供的字段保留原值。',
|
|
147
|
+
parameters: compileParameters({
|
|
148
|
+
uid: { type: 'string', required: true, description: '事件稳定标识(来自 calendar_list / calendar_search 的 uid,必填)。' },
|
|
149
|
+
summary: { type: 'string', description: '新标题(可选)。' },
|
|
150
|
+
start: { type: 'string', description: '新开始时间(ISO 8601,可选)。' },
|
|
151
|
+
end: { type: 'string', description: '新结束时间(ISO 8601,可选)。' },
|
|
152
|
+
description: { type: 'string', description: '新描述(可选)。' },
|
|
153
|
+
location: { type: 'string', description: '新地点(可选)。' },
|
|
154
|
+
allDay: { type: 'boolean', description: '是否全天事件(可选)。' },
|
|
155
|
+
}),
|
|
156
|
+
output: {
|
|
157
|
+
schema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
additionalProperties: true,
|
|
160
|
+
properties: { updated: EVENT_SCHEMA },
|
|
161
|
+
required: ['updated'],
|
|
162
|
+
},
|
|
163
|
+
render: (_args, value) => {
|
|
164
|
+
const event = value.updated;
|
|
165
|
+
return [{ type: 'text', text: '已更新事件:' + formatEvent(event) }];
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
async execute(args) {
|
|
169
|
+
const input = asRecord(args);
|
|
170
|
+
const uid = requiredString(input, 'uid', '事件 uid');
|
|
171
|
+
const changes = {};
|
|
172
|
+
const summary = optionalString(input, 'summary');
|
|
173
|
+
if (summary !== undefined)
|
|
174
|
+
changes.summary = summary;
|
|
175
|
+
const start = optionalString(input, 'start');
|
|
176
|
+
if (start !== undefined) {
|
|
177
|
+
assertIsoTime(start, 'start');
|
|
178
|
+
changes.start = start;
|
|
179
|
+
}
|
|
180
|
+
const end = optionalString(input, 'end');
|
|
181
|
+
if (end !== undefined) {
|
|
182
|
+
assertIsoTime(end, 'end');
|
|
183
|
+
changes.end = end;
|
|
184
|
+
}
|
|
185
|
+
const description = optionalString(input, 'description');
|
|
186
|
+
if (description !== undefined)
|
|
187
|
+
changes.description = description;
|
|
188
|
+
const location = optionalString(input, 'location');
|
|
189
|
+
if (location !== undefined)
|
|
190
|
+
changes.location = location;
|
|
191
|
+
const allDay = optionalBoolean(input, 'allDay');
|
|
192
|
+
if (allDay !== undefined)
|
|
193
|
+
changes.allDay = allDay;
|
|
194
|
+
const updated = await service().update(uid, changes);
|
|
195
|
+
return { updated };
|
|
196
|
+
},
|
|
197
|
+
timeoutMs: TIMEOUT_MS,
|
|
198
|
+
};
|
|
199
|
+
const remove = {
|
|
200
|
+
name: 'calendar_delete',
|
|
201
|
+
description: '按 uid 删除日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。删除不可撤销。',
|
|
202
|
+
parameters: compileParameters({
|
|
203
|
+
uid: { type: 'string', required: true, description: '事件稳定标识(来自 calendar_list / calendar_search 的 uid,必填)。' },
|
|
204
|
+
}),
|
|
205
|
+
output: {
|
|
206
|
+
schema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
additionalProperties: true,
|
|
209
|
+
properties: {
|
|
210
|
+
deleted: { type: 'boolean' },
|
|
211
|
+
uid: { type: 'string' },
|
|
212
|
+
href: { type: 'string' },
|
|
213
|
+
},
|
|
214
|
+
required: ['deleted', 'uid'],
|
|
215
|
+
},
|
|
216
|
+
render: (_args, value) => {
|
|
217
|
+
const result = value;
|
|
218
|
+
return [{ type: 'text', text: '已删除事件:uid=' + result.uid }];
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
async execute(args) {
|
|
222
|
+
const input = asRecord(args);
|
|
223
|
+
const uid = requiredString(input, 'uid', '事件 uid');
|
|
224
|
+
const result = await service().delete(uid);
|
|
225
|
+
return { deleted: true, uid: result.uid, href: result.href };
|
|
226
|
+
},
|
|
227
|
+
timeoutMs: TIMEOUT_MS,
|
|
228
|
+
};
|
|
229
|
+
const search = {
|
|
230
|
+
name: 'calendar_search',
|
|
231
|
+
description: '按关键词搜索日历事件(客户端过滤:匹配标题、描述、地点与 iCal UID,不区分大小写)。query 必填,limit 可选(默认 50)。返回每个事件的稳定标识 uid。',
|
|
232
|
+
parameters: compileParameters({
|
|
233
|
+
query: { type: 'string', required: true, description: '搜索关键词(必填)。' },
|
|
234
|
+
limit: { type: 'integer', description: '最多返回条数(可选,默认 50)。' },
|
|
235
|
+
}),
|
|
236
|
+
output: {
|
|
237
|
+
schema: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
additionalProperties: true,
|
|
240
|
+
properties: {
|
|
241
|
+
query: { type: 'string' },
|
|
242
|
+
count: { type: 'integer' },
|
|
243
|
+
events: { type: 'array', items: EVENT_SCHEMA },
|
|
244
|
+
},
|
|
245
|
+
required: ['query', 'count', 'events'],
|
|
246
|
+
},
|
|
247
|
+
render: (_args, value) => {
|
|
248
|
+
const result = value;
|
|
249
|
+
const lines = ['搜索「' + result.query + '」命中 ' + result.count + ' 个事件:'];
|
|
250
|
+
for (const event of result.events)
|
|
251
|
+
lines.push(formatEvent(event));
|
|
252
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
async execute(args) {
|
|
256
|
+
const input = asRecord(args);
|
|
257
|
+
const query = requiredString(input, 'query', '搜索关键词');
|
|
258
|
+
const limit = optionalInteger(input, 'limit') ?? 50;
|
|
259
|
+
const all = await service().all();
|
|
260
|
+
const matched = all.filter(buildSearchFilter(query)).slice(0, limit);
|
|
261
|
+
return { query, count: matched.length, events: matched };
|
|
262
|
+
},
|
|
263
|
+
timeoutMs: TIMEOUT_MS,
|
|
264
|
+
};
|
|
265
|
+
return [list, create, update, remove, search];
|
|
266
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-calendar",
|
|
3
|
+
"version": "0.1.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
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"prepare": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "pnpm run build && node --test \"test/*.test.mjs\"",
|
|
26
|
+
"prepublishOnly": "pnpm run build"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"dsh",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"plugin",
|
|
37
|
+
"calendar",
|
|
38
|
+
"caldav",
|
|
39
|
+
"ical"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ical.js": "^2.1.0",
|
|
47
|
+
"tsdav": "^2.3.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^24.0.0",
|
|
51
|
+
"typescript": "^5.6.0"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/STARDUSTLC666/dsh-calendar"
|
|
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
|
+
}
|