@zhin.js/plugin-calendar 0.0.0-stage → 0.1.2
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/CHANGELOG.md +14 -0
- package/README.md +189 -2
- package/commands/calendar/auth/index.js +26 -0
- package/commands/calendar/auth/index.ts +29 -0
- package/commands/calendar/logout/index.js +18 -0
- package/commands/calendar/logout/index.ts +18 -0
- package/commands/calendar/status/index.js +10 -0
- package/commands/calendar/status/index.ts +10 -0
- package/lib/caldav.d.ts +18 -0
- package/lib/caldav.d.ts.map +1 -0
- package/lib/caldav.js +232 -0
- package/lib/caldav.js.map +1 -0
- package/lib/chat-auth.d.ts +49 -0
- package/lib/chat-auth.d.ts.map +1 -0
- package/lib/chat-auth.js +247 -0
- package/lib/chat-auth.js.map +1 -0
- package/lib/cli/auth.d.ts +3 -0
- package/lib/cli/auth.d.ts.map +1 -0
- package/lib/cli/auth.js +182 -0
- package/lib/cli/auth.js.map +1 -0
- package/lib/config.d.ts +87 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +95 -0
- package/lib/config.js.map +1 -0
- package/lib/credential-store.d.ts +30 -0
- package/lib/credential-store.d.ts.map +1 -0
- package/lib/credential-store.js +42 -0
- package/lib/credential-store.js.map +1 -0
- package/lib/free-time.d.ts +13 -0
- package/lib/free-time.d.ts.map +1 -0
- package/lib/free-time.js +96 -0
- package/lib/free-time.js.map +1 -0
- package/lib/http-callback.d.ts +13 -0
- package/lib/http-callback.d.ts.map +1 -0
- package/lib/http-callback.js +26 -0
- package/lib/http-callback.js.map +1 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +122 -0
- package/lib/index.js.map +1 -0
- package/lib/oauth-auth.d.ts +37 -0
- package/lib/oauth-auth.d.ts.map +1 -0
- package/lib/oauth-auth.js +91 -0
- package/lib/oauth-auth.js.map +1 -0
- package/lib/oauth-session.d.ts +27 -0
- package/lib/oauth-session.d.ts.map +1 -0
- package/lib/oauth-session.js +43 -0
- package/lib/oauth-session.js.map +1 -0
- package/lib/oauth.d.ts +28 -0
- package/lib/oauth.d.ts.map +1 -0
- package/lib/oauth.js +200 -0
- package/lib/oauth.js.map +1 -0
- package/lib/runtime.d.ts +11 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +3 -0
- package/lib/runtime.js.map +1 -0
- package/lib/secrets.d.ts +3 -0
- package/lib/secrets.d.ts.map +1 -0
- package/lib/secrets.js +6 -0
- package/lib/secrets.js.map +1 -0
- package/lib/time.d.ts +37 -0
- package/lib/time.d.ts.map +1 -0
- package/lib/time.js +118 -0
- package/lib/time.js.map +1 -0
- package/lib/token-manager.d.ts +71 -0
- package/lib/token-manager.d.ts.map +1 -0
- package/lib/token-manager.js +239 -0
- package/lib/token-manager.js.map +1 -0
- package/lib/tool-errors.d.ts +6 -0
- package/lib/tool-errors.d.ts.map +1 -0
- package/lib/tool-errors.js +17 -0
- package/lib/tool-errors.js.map +1 -0
- package/lib/types.d.ts +28 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +104 -3
- package/plugin.js +71 -0
- package/skills/calendar-scheduling/SKILL.md +38 -0
- package/skills/calendar-scheduling/tools/create_event/index.js +38 -0
- package/skills/calendar-scheduling/tools/create_event/index.ts +44 -0
- package/skills/calendar-scheduling/tools/find_free_time/index.js +46 -0
- package/skills/calendar-scheduling/tools/find_free_time/index.ts +50 -0
- package/skills/calendar-scheduling/tools/list_events/index.js +32 -0
- package/skills/calendar-scheduling/tools/list_events/index.ts +42 -0
- package/src/caldav.ts +263 -0
- package/src/chat-auth.ts +352 -0
- package/src/cli/auth.ts +235 -0
- package/src/config.ts +108 -0
- package/src/credential-store.ts +79 -0
- package/src/free-time.ts +128 -0
- package/src/http-callback.ts +55 -0
- package/src/index.ts +193 -0
- package/src/oauth-auth.ts +145 -0
- package/src/oauth-session.ts +65 -0
- package/src/oauth.ts +298 -0
- package/src/runtime.ts +16 -0
- package/src/secrets.ts +5 -0
- package/src/time.ts +167 -0
- package/src/token-manager.ts +358 -0
- package/src/tool-errors.ts +18 -0
- package/src/types.ts +37 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineAgentTool } from "@zhin.js/tool";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { findFreeSlots } from "../../../../lib/free-time.js";
|
|
5
|
+
import { calendarRuntimeToken } from "../../../../lib/runtime.js";
|
|
6
|
+
import { runCalendarTool } from "../../../../lib/tool-errors.js";
|
|
7
|
+
import { parseDateTimeInZone } from "../../../../lib/time.js";
|
|
8
|
+
export default defineAgentTool({
|
|
9
|
+
description: "Find free slots of a given duration inside work hours between two datetimes.",
|
|
10
|
+
requiresApproval: "never",
|
|
11
|
+
tags: ["calendar"],
|
|
12
|
+
keywords: ["空闲", "free time", "有空"],
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
from: z.string(),
|
|
15
|
+
to: z.string(),
|
|
16
|
+
duration_minutes: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.min(5)
|
|
20
|
+
.max(24 * 60),
|
|
21
|
+
}),
|
|
22
|
+
async execute({ from, to, duration_minutes }, context) {
|
|
23
|
+
return runCalendarTool(async () => {
|
|
24
|
+
const runtime = context.use(calendarRuntimeToken);
|
|
25
|
+
if (!runtime.provider)
|
|
26
|
+
return "日历未启用:请先配置 provider 与凭据";
|
|
27
|
+
const timezone = runtime.config.timezone;
|
|
28
|
+
const fromDate = parseDateTimeInZone(from, timezone);
|
|
29
|
+
const toDate = parseDateTimeInZone(to, timezone);
|
|
30
|
+
const events = await runtime.provider.listEvents(fromDate, toDate, context.signal);
|
|
31
|
+
const slots = findFreeSlots({
|
|
32
|
+
from: fromDate,
|
|
33
|
+
to: toDate,
|
|
34
|
+
durationMinutes: duration_minutes,
|
|
35
|
+
workDayStart: runtime.config.work_day_start,
|
|
36
|
+
workDayEnd: runtime.config.work_day_end,
|
|
37
|
+
timezone,
|
|
38
|
+
events,
|
|
39
|
+
});
|
|
40
|
+
if (slots.length === 0) {
|
|
41
|
+
return `在工作时段(${runtime.config.work_day_start}-${runtime.config.work_day_end} ${timezone})没有足够的空闲`;
|
|
42
|
+
}
|
|
43
|
+
return slots.map((slot) => `• ${slot.start} → ${slot.end}`).join("\n");
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineAgentTool } from "@zhin.js/tool";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { findFreeSlots } from "../../../../src/free-time.js";
|
|
4
|
+
import { calendarRuntimeToken } from "../../../../src/runtime.js";
|
|
5
|
+
import { runCalendarTool } from "../../../../src/tool-errors.js";
|
|
6
|
+
import { parseDateTimeInZone } from "../../../../src/time.js";
|
|
7
|
+
|
|
8
|
+
export default defineAgentTool({
|
|
9
|
+
description:
|
|
10
|
+
"Find free slots of a given duration inside work hours between two datetimes.",
|
|
11
|
+
requiresApproval: "never",
|
|
12
|
+
tags: ["calendar"],
|
|
13
|
+
keywords: ["空闲", "free time", "有空"],
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
from: z.string(),
|
|
16
|
+
to: z.string(),
|
|
17
|
+
duration_minutes: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(5)
|
|
21
|
+
.max(24 * 60),
|
|
22
|
+
}),
|
|
23
|
+
async execute({ from, to, duration_minutes }, context) {
|
|
24
|
+
return runCalendarTool(async () => {
|
|
25
|
+
const runtime = context.use(calendarRuntimeToken);
|
|
26
|
+
if (!runtime.provider) return "日历未启用:请先配置 provider 与凭据";
|
|
27
|
+
const timezone = runtime.config.timezone;
|
|
28
|
+
const fromDate = parseDateTimeInZone(from, timezone);
|
|
29
|
+
const toDate = parseDateTimeInZone(to, timezone);
|
|
30
|
+
const events = await runtime.provider.listEvents(
|
|
31
|
+
fromDate,
|
|
32
|
+
toDate,
|
|
33
|
+
context.signal
|
|
34
|
+
);
|
|
35
|
+
const slots = findFreeSlots({
|
|
36
|
+
from: fromDate,
|
|
37
|
+
to: toDate,
|
|
38
|
+
durationMinutes: duration_minutes,
|
|
39
|
+
workDayStart: runtime.config.work_day_start,
|
|
40
|
+
workDayEnd: runtime.config.work_day_end,
|
|
41
|
+
timezone,
|
|
42
|
+
events,
|
|
43
|
+
});
|
|
44
|
+
if (slots.length === 0) {
|
|
45
|
+
return `在工作时段(${runtime.config.work_day_start}-${runtime.config.work_day_end} ${timezone})没有足够的空闲`;
|
|
46
|
+
}
|
|
47
|
+
return slots.map((slot) => `• ${slot.start} → ${slot.end}`).join("\n");
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineAgentTool } from "@zhin.js/tool";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { calendarRuntimeToken } from "../../../../lib/runtime.js";
|
|
5
|
+
import { runCalendarTool } from "../../../../lib/tool-errors.js";
|
|
6
|
+
import { parseDateTimeInZone } from "../../../../lib/time.js";
|
|
7
|
+
export default defineAgentTool({
|
|
8
|
+
description: "List calendar events between two datetimes in the configured timezone.",
|
|
9
|
+
requiresApproval: "never",
|
|
10
|
+
tags: ["calendar"],
|
|
11
|
+
keywords: ["日历", "日程", "events", "list events"],
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
from: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("Start YYYY-MM-DDTHH:mm (configured timezone if no offset)"),
|
|
16
|
+
to: z.string().describe("End YYYY-MM-DDTHH:mm"),
|
|
17
|
+
}),
|
|
18
|
+
async execute({ from, to }, context) {
|
|
19
|
+
return runCalendarTool(async () => {
|
|
20
|
+
const runtime = context.use(calendarRuntimeToken);
|
|
21
|
+
if (!runtime.provider)
|
|
22
|
+
return "日历未启用:请在 plugins.calendar 配置 provider(caldav / google / microsoft)与凭据";
|
|
23
|
+
const timezone = runtime.config.timezone;
|
|
24
|
+
const events = await runtime.provider.listEvents(parseDateTimeInZone(from, timezone), parseDateTimeInZone(to, timezone), context.signal);
|
|
25
|
+
if (events.length === 0)
|
|
26
|
+
return `(${runtime.provider.name})${from} ~ ${to} 没有事件`;
|
|
27
|
+
return events
|
|
28
|
+
.map((event) => `• ${event.start} → ${event.end} ${event.title}${event.location ? ` @ ${event.location}` : ""} [${event.id}]`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineAgentTool } from "@zhin.js/tool";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { calendarRuntimeToken } from "../../../../src/runtime.js";
|
|
4
|
+
import { runCalendarTool } from "../../../../src/tool-errors.js";
|
|
5
|
+
import { parseDateTimeInZone } from "../../../../src/time.js";
|
|
6
|
+
|
|
7
|
+
export default defineAgentTool({
|
|
8
|
+
description:
|
|
9
|
+
"List calendar events between two datetimes in the configured timezone.",
|
|
10
|
+
requiresApproval: "never",
|
|
11
|
+
tags: ["calendar"],
|
|
12
|
+
keywords: ["日历", "日程", "events", "list events"],
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
from: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("Start YYYY-MM-DDTHH:mm (configured timezone if no offset)"),
|
|
17
|
+
to: z.string().describe("End YYYY-MM-DDTHH:mm"),
|
|
18
|
+
}),
|
|
19
|
+
async execute({ from, to }, context) {
|
|
20
|
+
return runCalendarTool(async () => {
|
|
21
|
+
const runtime = context.use(calendarRuntimeToken);
|
|
22
|
+
if (!runtime.provider)
|
|
23
|
+
return "日历未启用:请在 plugins.calendar 配置 provider(caldav / google / microsoft)与凭据";
|
|
24
|
+
const timezone = runtime.config.timezone;
|
|
25
|
+
const events = await runtime.provider.listEvents(
|
|
26
|
+
parseDateTimeInZone(from, timezone),
|
|
27
|
+
parseDateTimeInZone(to, timezone),
|
|
28
|
+
context.signal
|
|
29
|
+
);
|
|
30
|
+
if (events.length === 0)
|
|
31
|
+
return `(${runtime.provider.name})${from} ~ ${to} 没有事件`;
|
|
32
|
+
return events
|
|
33
|
+
.map(
|
|
34
|
+
(event) =>
|
|
35
|
+
`• ${event.start} → ${event.end} ${event.title}${
|
|
36
|
+
event.location ? ` @ ${event.location}` : ""
|
|
37
|
+
} [${event.id}]`
|
|
38
|
+
)
|
|
39
|
+
.join("\n");
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
package/src/caldav.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal CalDAV client (RFC 4791) for listing and creating VEVENTs.
|
|
3
|
+
* Works with iCloud, Fastmail and Nextcloud calendars.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import type {
|
|
7
|
+
CalendarEvent,
|
|
8
|
+
CalendarProvider,
|
|
9
|
+
CreateEventInput,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import { formatInZone } from "./time.js";
|
|
12
|
+
|
|
13
|
+
export interface CalDavOptions {
|
|
14
|
+
readonly calendarUrl: string;
|
|
15
|
+
readonly username: string;
|
|
16
|
+
readonly password: string;
|
|
17
|
+
readonly timezone: string;
|
|
18
|
+
readonly fetchImpl?: typeof fetch;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function basicAuth(username: string, password: string): string {
|
|
22
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeXml(value: string): string {
|
|
26
|
+
return value
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toUtcStamp(date: Date): string {
|
|
34
|
+
return date
|
|
35
|
+
.toISOString()
|
|
36
|
+
.replace(/[-:]/g, "")
|
|
37
|
+
.replace(/\.\d{3}/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseIcsDate(value: string): Date {
|
|
41
|
+
const match = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z)?)?/.exec(
|
|
42
|
+
value.trim()
|
|
43
|
+
);
|
|
44
|
+
if (!match) return new Date(NaN);
|
|
45
|
+
if (!match[4]) {
|
|
46
|
+
return new Date(
|
|
47
|
+
Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const utc = match[7] === "Z";
|
|
51
|
+
const parts = [
|
|
52
|
+
Number(match[1]),
|
|
53
|
+
Number(match[2]) - 1,
|
|
54
|
+
Number(match[3]),
|
|
55
|
+
Number(match[4]),
|
|
56
|
+
Number(match[5]),
|
|
57
|
+
Number(match[6] ?? 0),
|
|
58
|
+
] as const;
|
|
59
|
+
return utc ? new Date(Date.UTC(...parts)) : new Date(...parts);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractField(block: string, name: string): string | undefined {
|
|
63
|
+
const pattern = new RegExp(`^${name}(?:;[^:]*)?:(.*)$`, "im");
|
|
64
|
+
const match = pattern.exec(block);
|
|
65
|
+
return match
|
|
66
|
+
? match[1]!.trim().replace(/\\n/g, "\n").replace(/\\,/g, ",")
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Returns the text content of every `<href>` element (any namespace prefix). */
|
|
71
|
+
export function extractHrefs(xml: string): string[] {
|
|
72
|
+
const hrefs: string[] = [];
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
while (cursor < xml.length) {
|
|
75
|
+
const tagStart = xml.indexOf("<", cursor);
|
|
76
|
+
if (tagStart === -1) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const tagEnd = xml.indexOf(">", tagStart);
|
|
80
|
+
if (tagEnd === -1) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const tagName = readTagName(xml.slice(tagStart + 1, tagEnd));
|
|
84
|
+
cursor = tagEnd + 1;
|
|
85
|
+
if (localName(tagName) !== "href") {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const closingStart = xml.indexOf("</", cursor);
|
|
89
|
+
if (closingStart === -1) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const value = xml.slice(cursor, closingStart).trim();
|
|
93
|
+
if (value) {
|
|
94
|
+
hrefs.push(value);
|
|
95
|
+
}
|
|
96
|
+
cursor = closingStart;
|
|
97
|
+
}
|
|
98
|
+
return hrefs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readTagName(tagContent: string): string {
|
|
102
|
+
if (tagContent.startsWith("/") || tagContent.startsWith("?")) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
let end = 0;
|
|
106
|
+
while (end < tagContent.length && !/[\s/]/.test(tagContent[end]!)) {
|
|
107
|
+
end += 1;
|
|
108
|
+
}
|
|
109
|
+
return tagContent.slice(0, end);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function localName(tagName: string): string {
|
|
113
|
+
const colonIndex = tagName.lastIndexOf(":");
|
|
114
|
+
return tagName.slice(colonIndex + 1).toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Returns the body between each BEGIN:VEVENT / END:VEVENT pair. */
|
|
118
|
+
export function extractVeventBlocks(text: string): string[] {
|
|
119
|
+
const beginMarker = "BEGIN:VEVENT";
|
|
120
|
+
const endMarker = "END:VEVENT";
|
|
121
|
+
const blocks: string[] = [];
|
|
122
|
+
let cursor = 0;
|
|
123
|
+
while (cursor < text.length) {
|
|
124
|
+
const begin = text.indexOf(beginMarker, cursor);
|
|
125
|
+
if (begin === -1) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const bodyStart = begin + beginMarker.length;
|
|
129
|
+
const end = text.indexOf(endMarker, bodyStart);
|
|
130
|
+
if (end === -1) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
blocks.push(text.slice(bodyStart, end));
|
|
134
|
+
cursor = end + endMarker.length;
|
|
135
|
+
}
|
|
136
|
+
return blocks;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Parses VEVENT components from a CalDAV multistatus response. */
|
|
140
|
+
export function parseCalDavEvents(
|
|
141
|
+
xml: string,
|
|
142
|
+
timezone: string
|
|
143
|
+
): CalendarEvent[] {
|
|
144
|
+
const events: CalendarEvent[] = [];
|
|
145
|
+
const hrefs = extractHrefs(xml);
|
|
146
|
+
const blocks = extractVeventBlocks(xml);
|
|
147
|
+
blocks.forEach((block, index) => {
|
|
148
|
+
const uid = extractField(block, "UID") ?? hrefs[index] ?? `event-${index}`;
|
|
149
|
+
const summary = extractField(block, "SUMMARY") ?? "(无标题)";
|
|
150
|
+
const startRaw = extractField(block, "DTSTART");
|
|
151
|
+
const endRaw = extractField(block, "DTEND") ?? startRaw;
|
|
152
|
+
if (!startRaw || !endRaw) return;
|
|
153
|
+
const start = parseIcsDate(startRaw);
|
|
154
|
+
const end = parseIcsDate(endRaw);
|
|
155
|
+
const allDay = !/T/.test(startRaw);
|
|
156
|
+
events.push({
|
|
157
|
+
id: uid,
|
|
158
|
+
title: summary,
|
|
159
|
+
start: formatInZone(start, timezone),
|
|
160
|
+
end: formatInZone(end, timezone),
|
|
161
|
+
location: extractField(block, "LOCATION"),
|
|
162
|
+
description: extractField(block, "DESCRIPTION"),
|
|
163
|
+
all_day: allDay,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return events;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildVevent(input: CreateEventInput, uid: string): string {
|
|
170
|
+
const lines = [
|
|
171
|
+
"BEGIN:VCALENDAR",
|
|
172
|
+
"VERSION:2.0",
|
|
173
|
+
"PRODID:-//zhin.js-assistant-toolkit//EN",
|
|
174
|
+
"BEGIN:VEVENT",
|
|
175
|
+
`UID:${uid}`,
|
|
176
|
+
`DTSTAMP:${toUtcStamp(new Date())}`,
|
|
177
|
+
input.all_day
|
|
178
|
+
? `DTSTART;VALUE=DATE:${input.start
|
|
179
|
+
.toISOString()
|
|
180
|
+
.slice(0, 10)
|
|
181
|
+
.replace(/-/g, "")}`
|
|
182
|
+
: `DTSTART:${toUtcStamp(input.start)}`,
|
|
183
|
+
input.all_day
|
|
184
|
+
? `DTEND;VALUE=DATE:${input.end
|
|
185
|
+
.toISOString()
|
|
186
|
+
.slice(0, 10)
|
|
187
|
+
.replace(/-/g, "")}`
|
|
188
|
+
: `DTEND:${toUtcStamp(input.end)}`,
|
|
189
|
+
`SUMMARY:${input.title.replace(/\n/g, " ")}`,
|
|
190
|
+
];
|
|
191
|
+
if (input.location)
|
|
192
|
+
lines.push(`LOCATION:${input.location.replace(/\n/g, " ")}`);
|
|
193
|
+
if (input.description)
|
|
194
|
+
lines.push(`DESCRIPTION:${input.description.replace(/\n/g, "\\n")}`);
|
|
195
|
+
lines.push("END:VEVENT", "END:VCALENDAR");
|
|
196
|
+
return lines.join("\r\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createCalDavProvider(options: CalDavOptions): CalendarProvider {
|
|
200
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
201
|
+
const auth = basicAuth(options.username, options.password);
|
|
202
|
+
const base = options.calendarUrl.endsWith("/")
|
|
203
|
+
? options.calendarUrl
|
|
204
|
+
: `${options.calendarUrl}/`;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
name: "caldav",
|
|
208
|
+
async listEvents(from, to, signal) {
|
|
209
|
+
const body = `<?xml version="1.0" encoding="utf-8" ?>
|
|
210
|
+
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
|
|
211
|
+
<D:prop><D:getetag/><C:calendar-data/></D:prop>
|
|
212
|
+
<C:filter>
|
|
213
|
+
<C:comp-filter name="VCALENDAR">
|
|
214
|
+
<C:comp-filter name="VEVENT">
|
|
215
|
+
<C:time-range start="${toUtcStamp(from)}" end="${toUtcStamp(to)}"/>
|
|
216
|
+
</C:comp-filter>
|
|
217
|
+
</C:comp-filter>
|
|
218
|
+
</C:filter>
|
|
219
|
+
</C:calendar-query>`;
|
|
220
|
+
const response = await fetchImpl(base, {
|
|
221
|
+
method: "REPORT",
|
|
222
|
+
headers: {
|
|
223
|
+
authorization: auth,
|
|
224
|
+
depth: "1",
|
|
225
|
+
"content-type": "application/xml; charset=utf-8",
|
|
226
|
+
},
|
|
227
|
+
body,
|
|
228
|
+
signal,
|
|
229
|
+
});
|
|
230
|
+
if (!response.ok)
|
|
231
|
+
throw new Error(`CalDAV REPORT 失败:HTTP ${response.status}`);
|
|
232
|
+
return parseCalDavEvents(await response.text(), options.timezone);
|
|
233
|
+
},
|
|
234
|
+
async createEvent(input, signal) {
|
|
235
|
+
const uid = `${randomUUID()}@zhin`;
|
|
236
|
+
const url = new URL(`${uid}.ics`, base).toString();
|
|
237
|
+
const response = await fetchImpl(url, {
|
|
238
|
+
method: "PUT",
|
|
239
|
+
headers: {
|
|
240
|
+
authorization: auth,
|
|
241
|
+
"content-type": "text/calendar; charset=utf-8",
|
|
242
|
+
"if-none-match": "*",
|
|
243
|
+
},
|
|
244
|
+
body: buildVevent(input, uid),
|
|
245
|
+
signal,
|
|
246
|
+
});
|
|
247
|
+
if (response.status !== 201 && response.status !== 204) {
|
|
248
|
+
throw new Error(`CalDAV PUT 失败:HTTP ${response.status}`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
id: uid,
|
|
252
|
+
title: input.title,
|
|
253
|
+
start: formatInZone(input.start, options.timezone),
|
|
254
|
+
end: formatInZone(input.end, options.timezone),
|
|
255
|
+
location: input.location,
|
|
256
|
+
description: input.description,
|
|
257
|
+
all_day: input.all_day,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { escapeXml };
|