@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,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory OAuth access-token cache with proactive refresh, single-flight,
|
|
3
|
+
* refresh-token rotation handling, and clear invalid_grant errors.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export type OAuthProviderKind = "google" | "microsoft";
|
|
9
|
+
|
|
10
|
+
export interface TokenSet {
|
|
11
|
+
readonly accessToken: string;
|
|
12
|
+
readonly refreshToken: string;
|
|
13
|
+
/** Epoch ms when the access token expires. */
|
|
14
|
+
readonly expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TokenManagerLogger {
|
|
18
|
+
warn(message: string): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Persistent storage for the refresh token (e.g. zhin plugin database state). */
|
|
22
|
+
export interface RefreshTokenStore {
|
|
23
|
+
load(): Promise<string | undefined>;
|
|
24
|
+
save(refreshToken: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AuthorizationLostListener = (
|
|
28
|
+
provider: OAuthProviderKind,
|
|
29
|
+
message: string
|
|
30
|
+
) => void | Promise<void>;
|
|
31
|
+
|
|
32
|
+
/** Chat command that (re-)authorizes a provider; shown in user-facing errors. */
|
|
33
|
+
export function authCommandFor(provider: OAuthProviderKind): string {
|
|
34
|
+
return `calendar auth ${provider}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TokenManagerOptions {
|
|
38
|
+
readonly provider: OAuthProviderKind;
|
|
39
|
+
readonly clientId: string;
|
|
40
|
+
/** Optional for Microsoft public/native clients. Required for Google. */
|
|
41
|
+
readonly clientSecret?: string;
|
|
42
|
+
readonly tenant?: string;
|
|
43
|
+
/** Fallback refresh token from config/env; the token store wins when it has a value. */
|
|
44
|
+
readonly initialRefreshToken?: string;
|
|
45
|
+
/** Refresh token persisted by the chat authorization flow. */
|
|
46
|
+
readonly tokenStore?: RefreshTokenStore;
|
|
47
|
+
/** Called once when the refresh token is rejected (invalid_grant). */
|
|
48
|
+
readonly onAuthorizationLost?: AuthorizationLostListener;
|
|
49
|
+
/**
|
|
50
|
+
* Optional path to persist rotated refresh tokens (JSON).
|
|
51
|
+
* When unset, rotation only logs a warning (update env manually).
|
|
52
|
+
*/
|
|
53
|
+
readonly tokenFile?: string;
|
|
54
|
+
readonly fetchImpl?: typeof fetch;
|
|
55
|
+
readonly now?: () => number;
|
|
56
|
+
readonly logger?: TokenManagerLogger;
|
|
57
|
+
/** Refresh this many ms before expiry. Default 5 minutes. */
|
|
58
|
+
readonly skewMs?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class OAuthTokenError extends Error {
|
|
62
|
+
readonly code: string;
|
|
63
|
+
constructor(code: string, message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "OAuthTokenError";
|
|
66
|
+
this.code = code;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface TokenFilePayload {
|
|
71
|
+
refresh_token?: string;
|
|
72
|
+
access_token?: string;
|
|
73
|
+
expires_at?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_SKEW_MS = 5 * 60 * 1000;
|
|
77
|
+
|
|
78
|
+
export class OAuthTokenManager {
|
|
79
|
+
readonly #provider: OAuthProviderKind;
|
|
80
|
+
readonly #clientId: string;
|
|
81
|
+
readonly #clientSecret: string;
|
|
82
|
+
readonly #tenant: string;
|
|
83
|
+
readonly #tokenFile: string;
|
|
84
|
+
readonly #fetch: typeof fetch;
|
|
85
|
+
readonly #now: () => number;
|
|
86
|
+
readonly #logger: TokenManagerLogger;
|
|
87
|
+
readonly #skewMs: number;
|
|
88
|
+
#refreshToken: string;
|
|
89
|
+
#accessToken = "";
|
|
90
|
+
#expiresAt = 0;
|
|
91
|
+
#inflight: Promise<string> | undefined;
|
|
92
|
+
readonly #tokenStore: RefreshTokenStore | undefined;
|
|
93
|
+
readonly #onAuthorizationLost: AuthorizationLostListener | undefined;
|
|
94
|
+
#authorizationLostMessage = "";
|
|
95
|
+
|
|
96
|
+
constructor(options: TokenManagerOptions) {
|
|
97
|
+
if (!options.clientId.trim()) {
|
|
98
|
+
throw new OAuthTokenError(
|
|
99
|
+
"config",
|
|
100
|
+
`${options.provider}: 缺少 client_id`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (options.provider === "google" && !options.clientSecret?.trim()) {
|
|
104
|
+
throw new OAuthTokenError("config", "google: 缺少 client_secret");
|
|
105
|
+
}
|
|
106
|
+
this.#provider = options.provider;
|
|
107
|
+
this.#clientId = options.clientId.trim();
|
|
108
|
+
this.#clientSecret = (options.clientSecret ?? "").trim();
|
|
109
|
+
this.#tenant = (options.tenant ?? "common").trim() || "common";
|
|
110
|
+
this.#tokenFile = (options.tokenFile ?? "").trim();
|
|
111
|
+
this.#fetch = options.fetchImpl ?? fetch;
|
|
112
|
+
this.#now = options.now ?? Date.now;
|
|
113
|
+
this.#logger = options.logger ?? {
|
|
114
|
+
warn: (message) => console.warn(message),
|
|
115
|
+
};
|
|
116
|
+
this.#skewMs = options.skewMs ?? DEFAULT_SKEW_MS;
|
|
117
|
+
this.#refreshToken = (options.initialRefreshToken ?? "").trim();
|
|
118
|
+
this.#tokenStore = options.tokenStore;
|
|
119
|
+
this.#onAuthorizationLost = options.onAuthorizationLost;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get provider(): OAuthProviderKind {
|
|
123
|
+
return this.#provider;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Whether a refresh token is available (stored or from config). */
|
|
127
|
+
get hasRefreshToken(): boolean {
|
|
128
|
+
return this.#refreshToken !== "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Whether the last refresh attempt was rejected with invalid_grant. */
|
|
132
|
+
get authorizationLost(): boolean {
|
|
133
|
+
return this.#authorizationLostMessage !== "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Loads persisted tokens: token_file overlay first, then the token store,
|
|
138
|
+
* which takes precedence over config/env because it is written by chat auth.
|
|
139
|
+
*/
|
|
140
|
+
async hydrate(): Promise<void> {
|
|
141
|
+
await this.hydrateFromFile();
|
|
142
|
+
if (!this.#tokenStore) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const storedRefreshToken = (await this.#tokenStore.load())?.trim() ?? "";
|
|
146
|
+
if (storedRefreshToken) {
|
|
147
|
+
this.#refreshToken = storedRefreshToken;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Replaces the refresh token after a successful chat authorization. */
|
|
152
|
+
setRefreshToken(refreshToken: string): void {
|
|
153
|
+
this.#refreshToken = refreshToken.trim();
|
|
154
|
+
this.#accessToken = "";
|
|
155
|
+
this.#expiresAt = 0;
|
|
156
|
+
this.#authorizationLostMessage = "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Forgets all tokens held in memory (used by `calendar logout`). */
|
|
160
|
+
clearTokens(): void {
|
|
161
|
+
this.setRefreshToken("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Current refresh token (may change after Microsoft rotation). */
|
|
165
|
+
get refreshToken(): string {
|
|
166
|
+
return this.#refreshToken;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Load optional token file overlay (rotated refresh / cached access). */
|
|
170
|
+
async hydrateFromFile(): Promise<void> {
|
|
171
|
+
if (!this.#tokenFile) return;
|
|
172
|
+
try {
|
|
173
|
+
const raw = await readFile(this.#tokenFile, "utf8");
|
|
174
|
+
const data = JSON.parse(raw) as TokenFilePayload;
|
|
175
|
+
if (typeof data.refresh_token === "string" && data.refresh_token.trim()) {
|
|
176
|
+
this.#refreshToken = data.refresh_token.trim();
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
typeof data.access_token === "string" &&
|
|
180
|
+
data.access_token.trim() &&
|
|
181
|
+
typeof data.expires_at === "number" &&
|
|
182
|
+
data.expires_at > this.#now() + this.#skewMs
|
|
183
|
+
) {
|
|
184
|
+
this.#accessToken = data.access_token.trim();
|
|
185
|
+
this.#expiresAt = data.expires_at;
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const err = error as NodeJS.ErrnoException;
|
|
189
|
+
if (err.code !== "ENOENT") {
|
|
190
|
+
this.#logger.warn(
|
|
191
|
+
`[calendar] 读取 token_file 失败:${this.#tokenFile}(${
|
|
192
|
+
err.message
|
|
193
|
+
})`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getAccessToken(): Promise<string> {
|
|
200
|
+
if (this.#accessToken && this.#now() < this.#expiresAt - this.#skewMs) {
|
|
201
|
+
return this.#accessToken;
|
|
202
|
+
}
|
|
203
|
+
return this.#refreshSingleFlight();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Bypass cache (e.g. after HTTP 401). Still single-flight. */
|
|
207
|
+
async forceRefresh(): Promise<string> {
|
|
208
|
+
this.#expiresAt = 0;
|
|
209
|
+
this.#accessToken = "";
|
|
210
|
+
return this.#refreshSingleFlight();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#refreshSingleFlight(): Promise<string> {
|
|
214
|
+
if (this.#inflight) return this.#inflight;
|
|
215
|
+
this.#inflight = this.#refresh().finally(() => {
|
|
216
|
+
this.#inflight = undefined;
|
|
217
|
+
});
|
|
218
|
+
return this.#inflight;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async #refresh(): Promise<string> {
|
|
222
|
+
if (this.#authorizationLostMessage) {
|
|
223
|
+
throw new OAuthTokenError(
|
|
224
|
+
"invalid_grant",
|
|
225
|
+
this.#authorizationLostMessage
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (!this.#refreshToken) {
|
|
229
|
+
throw new OAuthTokenError(
|
|
230
|
+
"not_authorized",
|
|
231
|
+
`${
|
|
232
|
+
this.#provider
|
|
233
|
+
}: 日历尚未授权。请 master 在聊天中发送「${authCommandFor(
|
|
234
|
+
this.#provider
|
|
235
|
+
)}」完成授权。`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const body = new URLSearchParams({
|
|
239
|
+
client_id: this.#clientId,
|
|
240
|
+
refresh_token: this.#refreshToken,
|
|
241
|
+
grant_type: "refresh_token",
|
|
242
|
+
});
|
|
243
|
+
if (this.#clientSecret) body.set("client_secret", this.#clientSecret);
|
|
244
|
+
if (this.#provider === "microsoft") {
|
|
245
|
+
body.set("scope", "offline_access Calendars.ReadWrite User.Read");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const url =
|
|
249
|
+
this.#provider === "google"
|
|
250
|
+
? "https://oauth2.googleapis.com/token"
|
|
251
|
+
: `https://login.microsoftonline.com/${this.#tenant}/oauth2/v2.0/token`;
|
|
252
|
+
|
|
253
|
+
const response = await this.#fetch(url, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
256
|
+
body,
|
|
257
|
+
});
|
|
258
|
+
const text = await response.text();
|
|
259
|
+
let data: Record<string, unknown> = {};
|
|
260
|
+
try {
|
|
261
|
+
data = text ? (JSON.parse(text) as Record<string, unknown>) : {};
|
|
262
|
+
} catch {
|
|
263
|
+
data = {};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
const errCode = String(data.error ?? "");
|
|
268
|
+
const errDesc = String(data.error_description ?? text.slice(0, 200));
|
|
269
|
+
if (errCode === "invalid_grant" || /invalid_grant/i.test(errDesc)) {
|
|
270
|
+
const message = `${
|
|
271
|
+
this.#provider
|
|
272
|
+
}: 日历授权已失效或被撤销(invalid_grant)。请 master 在聊天中发送「${authCommandFor(
|
|
273
|
+
this.#provider
|
|
274
|
+
)}」重新授权。详情:${errDesc}`;
|
|
275
|
+
this.#authorizationLostMessage = message;
|
|
276
|
+
await this.#notifyAuthorizationLost(message);
|
|
277
|
+
throw new OAuthTokenError("invalid_grant", message);
|
|
278
|
+
}
|
|
279
|
+
throw new OAuthTokenError(
|
|
280
|
+
errCode || "http_error",
|
|
281
|
+
`${this.#provider}: token 刷新失败 HTTP ${response.status}:${
|
|
282
|
+
errDesc || response.statusText
|
|
283
|
+
}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const accessToken = String(data.access_token ?? "");
|
|
288
|
+
if (!accessToken) {
|
|
289
|
+
throw new OAuthTokenError(
|
|
290
|
+
"response",
|
|
291
|
+
`${this.#provider}: 刷新响应缺少 access_token`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const expiresIn = Number(data.expires_in ?? 3600);
|
|
295
|
+
this.#accessToken = accessToken;
|
|
296
|
+
this.#expiresAt = this.#now() + Math.max(60, expiresIn) * 1000;
|
|
297
|
+
|
|
298
|
+
const rotated =
|
|
299
|
+
typeof data.refresh_token === "string" ? data.refresh_token.trim() : "";
|
|
300
|
+
if (rotated && rotated !== this.#refreshToken) {
|
|
301
|
+
this.#refreshToken = rotated;
|
|
302
|
+
if (this.#tokenStore) {
|
|
303
|
+
await this.#tokenStore.save(rotated);
|
|
304
|
+
this.#logger.warn(
|
|
305
|
+
`[calendar] ${
|
|
306
|
+
this.#provider
|
|
307
|
+
}: 提供方返回了新的 refresh_token,已写入插件数据库`
|
|
308
|
+
);
|
|
309
|
+
} else if (this.#tokenFile) {
|
|
310
|
+
await this.#persist();
|
|
311
|
+
this.#logger.warn(
|
|
312
|
+
`[calendar] ${
|
|
313
|
+
this.#provider
|
|
314
|
+
}: 提供方返回了新的 refresh_token,已写入 token_file:${
|
|
315
|
+
this.#tokenFile
|
|
316
|
+
}`
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
this.#logger.warn(
|
|
320
|
+
`[calendar] ${
|
|
321
|
+
this.#provider
|
|
322
|
+
}: 提供方返回了新的 refresh_token。请立刻更新环境变量中的 refresh_token(当前未配置 token_file,内存中的新值进程重启后会丢失):\n${rotated}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
} else if (this.#tokenFile) {
|
|
326
|
+
await this.#persist();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return this.#accessToken;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async #notifyAuthorizationLost(message: string): Promise<void> {
|
|
333
|
+
if (!this.#onAuthorizationLost) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await this.#onAuthorizationLost(this.#provider, message);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
340
|
+
this.#logger.warn(`[calendar] 授权失效通知发送失败:${reason}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async #persist(): Promise<void> {
|
|
345
|
+
if (!this.#tokenFile) return;
|
|
346
|
+
const payload: TokenFilePayload = {
|
|
347
|
+
refresh_token: this.#refreshToken,
|
|
348
|
+
access_token: this.#accessToken || undefined,
|
|
349
|
+
expires_at: this.#expiresAt || undefined,
|
|
350
|
+
};
|
|
351
|
+
await mkdir(path.dirname(this.#tokenFile), { recursive: true });
|
|
352
|
+
await writeFile(
|
|
353
|
+
this.#tokenFile,
|
|
354
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
355
|
+
"utf8"
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OAuthTokenError } from "./token-manager.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs a calendar tool body and turns OAuth authorization failures into a
|
|
5
|
+
* readable reply (e.g. "请 master 发送 calendar auth google") instead of a crash.
|
|
6
|
+
*/
|
|
7
|
+
export async function runCalendarTool(
|
|
8
|
+
operation: () => Promise<string>
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
try {
|
|
11
|
+
return await operation();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error instanceof OAuthTokenError) {
|
|
14
|
+
return error.message;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Calendar event as returned by every provider. */
|
|
2
|
+
export interface CalendarEvent {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly start: string;
|
|
6
|
+
readonly end: string;
|
|
7
|
+
readonly location?: string;
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
readonly all_day?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateEventInput {
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly start: Date;
|
|
15
|
+
readonly end: Date;
|
|
16
|
+
readonly location?: string;
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
readonly all_day?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface FreeSlot {
|
|
22
|
+
readonly start: string;
|
|
23
|
+
readonly end: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CalendarProvider {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
listEvents(
|
|
29
|
+
from: Date,
|
|
30
|
+
to: Date,
|
|
31
|
+
signal?: AbortSignal
|
|
32
|
+
): Promise<CalendarEvent[]>;
|
|
33
|
+
createEvent(
|
|
34
|
+
input: CreateEventInput,
|
|
35
|
+
signal?: AbortSignal
|
|
36
|
+
): Promise<CalendarEvent>;
|
|
37
|
+
}
|