dsh-calendar 0.11.1 → 0.11.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 CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.11.2(2026-10-20)
8
+
9
+ - 修复 #3:更新时间时保留原 TZID 对应的本地钟点和夏令时规则,不再写出 TZID 与 UTC `Z` 混用的属性。
10
+ - 含 EXDATE、RDATE 或单次覆盖的系列,拒绝整体改开始时间、全天类型或重复规则,防止取消场次复活、改期场次重复。
11
+ - 重复系列更新 / 删除必须显式传 `scope:"series"`;面板编辑先读取系列主记录,并提示影响全部场次,删除确认包含视图外场次。
12
+
7
13
  ## 0.11.1(2026-10-20)
8
14
 
9
15
  - 修复 calendar_update 传入空字符串时无法清空说明、地点或重复规则的问题;省略字段仍保留原值。
package/docs/USAGE.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## 本次改进
6
6
 
7
+ 重复系列的 `calendar_update` / `calendar_delete` 需先向用户说明影响,确认操作全部场次后传 `scope:"series"`。无法只改或删除某一次。面板编辑读取系列原始日期,删除确认覆盖整个系列。有取消、额外或单独改期场次的系列不能整体改开始时间、全天类型或重复规则,请在原日历客户端处理;描述、地点、标题仍可修改。带 TZID 的时间保留其本地钟点与夏令时语义。
8
+
7
9
  日期使用 YYYY-MM-DD;带时间使用带 Z 或 UTC 偏移的 ISO 格式。错误日期会在创建或更新远端日程前报错,原日程保留。
8
10
 
9
11
  ## 安装
package/lib/caldav.d.ts CHANGED
@@ -45,10 +45,12 @@ export declare class CalendarService {
45
45
  }>;
46
46
  /** Insert one already reviewed UID resource; tsdav uses If-None-Match:* and never overwrites existing objects. */
47
47
  importRaw(uid: string, data: string, signal?: AbortSignal): Promise<void>;
48
+ /** Read the resource master rather than a projected occurrence. */
49
+ get(uid: string, signal?: AbortSignal): Promise<CalendarEvent>;
48
50
  /** 按 uid 更新事件;未提供的字段保留原值。 */
49
- update(uid: string, changes: Partial<EventFields>, signal?: AbortSignal): Promise<CalendarEvent>;
51
+ update(uid: string, changes: Partial<EventFields>, signal?: AbortSignal, scope?: 'series'): Promise<CalendarEvent>;
50
52
  /** 按 uid 删除事件。 */
51
- delete(uid: string, signal?: AbortSignal): Promise<{
53
+ delete(uid: string, signal?: AbortSignal, scope?: 'series'): Promise<{
52
54
  uid: string;
53
55
  href: string;
54
56
  }>;
package/lib/caldav.js CHANGED
@@ -9,7 +9,7 @@ import { createHash } from 'node:crypto';
9
9
  import ICAL from 'ical.js';
10
10
  import { createProxyFetch } from './proxy-fetch.js';
11
11
  import { createOAuthFetch, OAuthError } from './oauth.js';
12
- import { buildICalString, expandEventFromICal, ExpansionLimitError, generateUid, parseEventFromICal, updateICalString, } from './ical.js';
12
+ import { buildICalString, expandEventFromICal, ExpansionLimitError, generateUid, parseEventFromICal, updateICalString, isRecurringResource, } from './ical.js';
13
13
  /** CalDAV 操作错误:带中文指引。 */
14
14
  export class CalDAVError extends Error {
15
15
  status;
@@ -279,8 +279,22 @@ export class CalendarService {
279
279
  throw translateError(error, '导入日程', this.config);
280
280
  }
281
281
  }
282
+ /** Read the resource master rather than a projected occurrence. */
283
+ async get(uid, signal) {
284
+ try {
285
+ const object = await this.findObject(uid, signal);
286
+ const event = object && parseEventFromICal(String(object.data ?? ''), object.url, object.etag);
287
+ if (!event)
288
+ throw new CalDAVError('找不到或无法解析日程,请刷新日历后重试。');
289
+ return event;
290
+ }
291
+ catch (error) {
292
+ signal?.throwIfAborted();
293
+ throw translateError(error, '读取系列事件', this.config);
294
+ }
295
+ }
282
296
  /** 按 uid 更新事件;未提供的字段保留原值。 */
283
- async update(uid, changes, signal) {
297
+ async update(uid, changes, signal, scope) {
284
298
  signal?.throwIfAborted();
285
299
  let object;
286
300
  try {
@@ -295,6 +309,10 @@ export class CalendarService {
295
309
  '该事件可能已被删除或 uid 已过期。');
296
310
  }
297
311
  const existing = parseEventFromICal(String(object.data ?? ''), object.url, object.etag);
312
+ if (!existing)
313
+ throw new CalDAVError('无法解析原日程,更新已取消;请在原日历客户端检查。');
314
+ if (isRecurringResource(String(object.data ?? '')) && scope !== 'series')
315
+ throw new CalDAVError('这是整个重复系列。请先向用户说明影响,确认后显式传 scope="series";当前不支持只改某一次。');
298
316
  const start = changes.start ?? existing?.start;
299
317
  const end = changes.end ?? existing?.end;
300
318
  if (start === undefined || end === undefined) {
@@ -340,7 +358,7 @@ export class CalendarService {
340
358
  return event;
341
359
  }
342
360
  /** 按 uid 删除事件。 */
343
- async delete(uid, signal) {
361
+ async delete(uid, signal, scope) {
344
362
  signal?.throwIfAborted();
345
363
  let object;
346
364
  try {
@@ -354,6 +372,8 @@ export class CalendarService {
354
372
  throw new CalDAVError('找不到 uid 对应的事件:请用 calendar_list 或 calendar_search 重新获取最新 uid,' +
355
373
  '该事件可能已被删除或 uid 已过期。');
356
374
  }
375
+ if (isRecurringResource(String(object.data ?? '')) && scope !== 'series')
376
+ throw new CalDAVError('这是整个重复系列。请先向用户说明影响,确认后显式传 scope="series";当前不支持只删某一次。');
357
377
  try {
358
378
  const client = await this.client();
359
379
  signal?.throwIfAborted();
package/lib/client.js CHANGED
@@ -240,6 +240,8 @@ const UI = {
240
240
  "conflict.title": "这个时段已有 {n} 个日程,会重叠:", "conflict.hint": "仍然保存?",
241
241
  "error.title": "操作失败", "error.load": "读取日历失败",
242
242
  "confirm.delete": "删除「{name}」?这个操作会同步到你的日历服务器。",
243
+ "confirm.deleteSeries": "将删除「{name}」整个重复系列的所有场次,包含当前视图外的场次。无法撤销。",
244
+ "form.series": "正在编辑整个重复系列,所有场次都会受影响。含取消或单独改期场次时,请到原日历客户端调整时间和规则。",
243
245
  "form.newTitle": "新建日程", "form.editTitle": "编辑日程",
244
246
  "form.required": "标题和时间都要填。",
245
247
  "day.today": "今天", "day.tomorrow": "明天", "day.yesterday": "昨天",
@@ -336,6 +338,8 @@ const UI = {
336
338
  "conflict.title": "This slot already has {n} event(s); they overlap:", "conflict.hint": "Save anyway?",
337
339
  "error.title": "Something failed", "error.load": "Could not load the calendar",
338
340
  "confirm.delete": "Delete「{name}」? This is pushed to your calendar server.",
341
+ "confirm.deleteSeries": "Delete every occurrence of {name}, including dates outside this view? This cannot be undone.",
342
+ "form.series": "Editing the entire recurring series. All occurrences are affected. If it has cancelled or rescheduled occurrences, change times and rules in your original calendar app.",
339
343
  "form.newTitle": "New event", "form.editTitle": "Edit event",
340
344
  "form.required": "A title and a time range are required.",
341
345
  "day.today": "Today", "day.tomorrow": "Tomorrow", "day.yesterday": "Yesterday",
@@ -417,6 +421,11 @@ var activeLocaleService = null;
417
421
 
418
422
  // Backend guidance is localized separately from event titles and source content.
419
423
  const BACKEND_EN = {
424
+ '系列含已取消、额外或单独改期的场次,不能整体改开始时间、全天类型或重复规则;请在原日历客户端编辑,现有日程未改动。': 'This series has cancelled, extra or rescheduled occurrences. Change its start time, all-day type or recurrence rule in the original calendar app. No event was changed.',
425
+ '这是整个重复系列。请先向用户说明影响,确认后显式传 scope="series";当前不支持只改某一次。': 'This action changes the entire recurring series. Explain its impact, then use scope="series" after confirmation. Editing just one occurrence is unsupported.',
426
+ '这是整个重复系列。请先向用户说明影响,确认后显式传 scope="series";当前不支持只删某一次。': 'This action deletes the entire recurring series. Explain its impact, then use scope="series" after confirmation. Deleting just one occurrence is unsupported.',
427
+ '找不到或无法解析日程,请刷新日历后重试。': 'The event is missing or could not be parsed. Refresh the calendar and try again.',
428
+ '无法解析原日程,更新已取消;请在原日历客户端检查。': 'The original event could not be parsed. No update was made. Check it in the original calendar app.',
420
429
  "日期格式不合法": "Invalid date format.",
421
430
  "日程包含不存在的日期或时间": "This event contains an invalid date or time.",
422
431
  "请先选择或粘贴 ICS 文件内容": "Choose an ICS file or paste its content first.",
@@ -1310,6 +1319,7 @@ function draftOfEvent(event) {
1310
1319
  const e = allDay ? addDays(rawEnd, -1) : rawEnd;
1311
1320
  return {
1312
1321
  uid: event.uid,
1322
+ isSeries: isRecurring(event),
1313
1323
  summary: event.summary || "",
1314
1324
  date: dayKey(s),
1315
1325
  startTime: pad2(s.getHours()) + ":" + pad2(s.getMinutes()),
@@ -1364,7 +1374,7 @@ function rruleForRepeat(repeat) {
1364
1374
 
1365
1375
  /** 草稿 → 后端字段(全天走 DATE,定时走本地时间换算出的 UTC 秒级 ISO)。 */
1366
1376
  function fieldsOfDraft(draft) {
1367
- const fields = { summary: draft.summary.trim(), location: draft.location.trim(), description: draft.description.trim() };
1377
+ const fields = { summary: draft.summary.trim(), location: draft.location.trim(), description: draft.description.trim(), ...(draft.isSeries ? { scope: "series" } : {}) };
1368
1378
  if (draft.allDay === true) {
1369
1379
  fields.start = draft.date;
1370
1380
  fields.end = dayKey(addDays(new Date(draft.date + "T00:00:00"), 1));
@@ -1400,6 +1410,7 @@ function EventForm(props) {
1400
1410
  const canSave = form.summary.trim() !== "" && (form.allDay === true || form.startTime <= form.endTime);
1401
1411
 
1402
1412
  const body = h("div", null, [
1413
+ draft.isSeries ? h("div", { className: "dshc-alert" }, t("form.series")) : null,
1403
1414
  error ? h("div", { className: "dshc-alert error" }, error) : null,
1404
1415
  h("div", { className: "dshc-field" }, [
1405
1416
  h("label", { htmlFor: formId + "-summary" }, t("field.summary")),
@@ -1491,7 +1502,7 @@ function EventDrawer(props) {
1491
1502
  : h(Btn, { key: "delete", variant: "danger", onClick: () => setConfirmDelete(true), disabled: busy === true || readOnly === true }, t("action.delete")),
1492
1503
  h(Btn, { key: "edit", variant: "outline", onClick: () => onEdit(event), disabled: busy === true || readOnly === true }, t("action.edit"))
1493
1504
  ]),
1494
- confirmDelete ? h("div", { className: "dshc-sub", style: { textAlign: "right", marginTop: "6px" } }, t("confirm.delete", { name: event.summary })) : null
1505
+ confirmDelete ? h("div", { className: "dshc-sub", style: { textAlign: "right", marginTop: "6px" } }, t(isRecurring(event) ? "confirm.deleteSeries" : "confirm.delete", { name: event.summary })) : null
1495
1506
  ])
1496
1507
  ]);
1497
1508
  }
@@ -2076,7 +2087,7 @@ function CalendarPanel(props) {
2076
2087
  if (demo === true) { notify(t("demo.banner")); return; }
2077
2088
  setBusy(true);
2078
2089
  try {
2079
- await api("delete", { uid: event.uid });
2090
+ await api("delete", { uid: event.uid, ...(isRecurring(event) ? { scope: "series" } : {}) });
2080
2091
  setSelected(null);
2081
2092
  setConfirmDelete(false);
2082
2093
  reload();
@@ -2167,7 +2178,13 @@ function CalendarPanel(props) {
2167
2178
  confirmDelete,
2168
2179
  setConfirmDelete,
2169
2180
  onClose: () => { setSelected(null); setConfirmDelete(false); },
2170
- onEdit: (event) => { setSelected(null); setDraft(draftOfEvent(event)); },
2181
+ onEdit: async (event) => {
2182
+ if (!isRecurring(event) || demo) { setSelected(null); setDraft(draftOfEvent(event)); return; }
2183
+ setBusy(true);
2184
+ try { const value = await api("event", { uid: event.uid }); setSelected(null); setDraft({ ...draftOfEvent(value.event), isSeries: true }); }
2185
+ catch (error) { setBanner(backendMessage(error)); }
2186
+ finally { setBusy(false); }
2187
+ },
2171
2188
  onDelete: remove
2172
2189
  })
2173
2190
  : null,
package/lib/ical.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * @module dsh-calendar/ical
6
6
  */
7
+ export declare function isRecurringResource(data: string): boolean;
7
8
  /** 一个暴露给模型的日历事件(或重复系列的一个展开实例)。 */
8
9
  export interface CalendarEvent {
9
10
  /** 稳定标识(CalDAV href),calendar_update / calendar_delete 用它。 */
package/lib/ical.js CHANGED
@@ -6,7 +6,30 @@
6
6
  */
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import ICAL from 'ical.js';
9
+ import { tzlib_get_ical_block, tzlib_get_timezones } from 'timezones-ical-library';
9
10
  import { assertIsoTime } from './time.js';
11
+ const supportedZones = new Set(tzlib_get_timezones());
12
+ /** Install missing IANA definitions in this resource before hydrating any dates. */
13
+ function prepareTimezones(calendar) {
14
+ const defined = new Set(calendar.getAllSubcomponents('vtimezone').map(zone => String(zone.getFirstPropertyValue('tzid'))));
15
+ for (const event of calendar.getAllSubcomponents('vevent'))
16
+ for (const property of event.getAllProperties()) {
17
+ const tzid = property.getParameter('tzid');
18
+ if (typeof tzid !== 'string' || tzid === 'UTC' || defined.has(tzid))
19
+ continue;
20
+ if (!supportedZones.has(tzid))
21
+ throw new Error('无法识别日程时区 ' + tzid + ',请从原日历重新导出完整的 VTIMEZONE');
22
+ const block = tzlib_get_ical_block(tzid)[0];
23
+ if (!block)
24
+ throw new Error('缺少日程时区定义:' + tzid);
25
+ calendar.addSubcomponent(new ICAL.Component(ICAL.parse(block)));
26
+ defined.add(tzid);
27
+ }
28
+ }
29
+ export function isRecurringResource(data) {
30
+ const calendar = new ICAL.Component(ICAL.parse(data));
31
+ return calendar.getAllSubcomponents('vevent').some(event => ['rrule', 'rdate', 'exdate', 'recurrence-id'].some(name => event.hasProperty(name)));
32
+ }
10
33
  /** 展开重复事件超出迭代预算:显式报错,避免把「没走到窗口」静默当成「窗口内没有实例」。 */
11
34
  export class ExpansionLimitError extends Error {
12
35
  constructor(message) {
@@ -69,6 +92,7 @@ export function parseEventFromICal(data, href, etag) {
69
92
  catch {
70
93
  return null;
71
94
  }
95
+ prepareTimezones(vcal);
72
96
  const vevent = masterVevent(vcal);
73
97
  if (vevent === null)
74
98
  return null;
@@ -218,6 +242,7 @@ export function expandEventFromICal(data, href, etag, rangeStart, rangeEnd, maxO
218
242
  catch {
219
243
  return [];
220
244
  }
245
+ prepareTimezones(vcal);
221
246
  const master = masterVevent(vcal);
222
247
  if (master === null)
223
248
  return [];
@@ -360,6 +385,22 @@ function applyEventTimes(vevent, changes) {
360
385
  const existingStart = timePropertyValue(vevent, 'dtstart');
361
386
  const existingEnd = timePropertyValue(vevent, 'dtend');
362
387
  const allDay = changes.allDay ?? (existingStart?.isDate === true);
388
+ const originalZone = vevent.getFirstProperty('dtstart')?.getParameter('tzid');
389
+ const writeTime = (name, time) => {
390
+ const tzid = vevent.getFirstProperty(name)?.getParameter('tzid') ?? originalZone;
391
+ if (!time.isDate && typeof tzid === 'string') {
392
+ const zone = vevent.parent?.getTimeZoneByID(tzid);
393
+ if (!zone)
394
+ throw new Error('无法保留日程时区:' + tzid);
395
+ time = time.convertToZone(zone);
396
+ }
397
+ vevent.updatePropertyWithValue(name, time);
398
+ const property = vevent.getFirstProperty(name);
399
+ if (!time.isDate && typeof tzid === 'string')
400
+ property.setParameter('tzid', tzid);
401
+ else
402
+ property.removeParameter('tzid');
403
+ };
363
404
  let start;
364
405
  if (changes.start !== undefined) {
365
406
  start = parseTime(changes.start, allDay || isDateOnly(changes.start));
@@ -368,7 +409,7 @@ function applyEventTimes(vevent, changes) {
368
409
  start = convertTimeForAllDay(existingStart, allDay);
369
410
  }
370
411
  if (start !== undefined)
371
- vevent.updatePropertyWithValue('dtstart', start);
412
+ writeTime('dtstart', start);
372
413
  let end;
373
414
  if (changes.end !== undefined) {
374
415
  end = parseTime(changes.end, allDay || isDateOnly(changes.end));
@@ -377,7 +418,7 @@ function applyEventTimes(vevent, changes) {
377
418
  end = convertTimeForAllDay(existingEnd, allDay);
378
419
  }
379
420
  if (end !== undefined) {
380
- vevent.updatePropertyWithValue('dtend', end);
421
+ writeTime('dtend', end);
381
422
  // DTEND 与 DURATION 互斥;显式给结束时间时移除原 DURATION,避免双份定义。
382
423
  vevent.removeProperty('duration');
383
424
  }
@@ -399,9 +440,19 @@ export function updateICalString(data, changes) {
399
440
  catch {
400
441
  return null;
401
442
  }
443
+ prepareTimezones(vcal);
402
444
  const vevent = masterVevent(vcal);
403
445
  if (vevent === null)
404
446
  return null;
447
+ const originalStart = timePropertyValue(vevent, 'dtstart');
448
+ const timeMoves = changes.start !== undefined && originalStart !== undefined &&
449
+ parseTime(changes.start, changes.allDay ?? originalStart.isDate).toUnixTime() !== originalStart.toUnixTime();
450
+ const typeChanges = changes.allDay !== undefined && changes.allDay !== originalStart?.isDate;
451
+ const ruleChanges = changes.rrule !== undefined && changes.rrule.trim() !== String(vevent.getFirstPropertyValue('rrule') ?? '');
452
+ const hasExceptions = vevent.hasProperty('exdate') || vevent.hasProperty('rdate') || vcal.getAllSubcomponents('vevent').some(event => event.hasProperty('recurrence-id'));
453
+ if (hasExceptions && (timeMoves || typeChanges || ruleChanges)) {
454
+ throw new Error('系列含已取消、额外或单独改期的场次,不能整体改开始时间、全天类型或重复规则;请在原日历客户端编辑,现有日程未改动。');
455
+ }
405
456
  if (!vcal.hasProperty('version'))
406
457
  vcal.addPropertyWithValue('version', '2.0');
407
458
  if (!vcal.hasProperty('prodid'))
package/lib/tools.js CHANGED
@@ -203,9 +203,10 @@ export function buildCalendarTools(config, env = process.env) {
203
203
  };
204
204
  const update = {
205
205
  name: 'calendar_update',
206
- description: '按 uid 修改日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。summary、start、end、description、location、allDay、rrule 均可选,未提供的字段保留原值。description、location、rrule 显式传空字符串可分别清空说明、地点或停止重复。',
206
+ description: '按 uid 修改日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。summary、start、end、description、location、allDay、rrule 均可选,未提供的字段保留原值。description、location、rrule 显式传空字符串可分别清空说明、地点或停止重复。重复日程的 uid 指整个系列;先向用户说明,确认后传 scope="series",不能只改某一次。有例外场次的系列不允许整体改开始时间、全天类型或重复规则。',
207
207
  parameters: compileParameters({
208
208
  uid: { type: 'string', required: true, description: '事件稳定标识(来自 calendar_list / calendar_search 的 uid,必填)。' },
209
+ scope: { type: 'string', enum: ['series'], description: '重复日程必须先向用户说明影响;确认操作整个系列后传 series。不能只改或删除某一次。' },
209
210
  summary: { type: 'string', description: '新标题(可选)。' },
210
211
  start: { type: 'string', description: '新开始时间(ISO 8601,可选)。' },
211
212
  end: { type: 'string', description: '新结束时间(ISO 8601,可选)。' },
@@ -255,16 +256,17 @@ export function buildCalendarTools(config, env = process.env) {
255
256
  const rrule = optionalPatchString(input, 'rrule');
256
257
  if (rrule !== undefined)
257
258
  changes.rrule = rrule;
258
- const updated = await service().update(uid, changes, executionSignal(exec));
259
+ const updated = await service().update(uid, changes, executionSignal(exec), input.scope === 'series' ? 'series' : undefined);
259
260
  return { updated };
260
261
  },
261
262
  timeoutMs: TIMEOUT_MS,
262
263
  };
263
264
  const remove = {
264
265
  name: 'calendar_delete',
265
- description: '按 uid 删除日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。删除不可撤销。',
266
+ description: '按 uid 删除日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。删除不可撤销。重复日程的 uid 指向整个系列,不能只删某一次;必须先向用户说明将删除所有场次,确认后传 scope="series",否则拒绝。',
266
267
  parameters: compileParameters({
267
268
  uid: { type: 'string', required: true, description: '事件稳定标识(来自 calendar_list / calendar_search 的 uid,必填)。' },
269
+ scope: { type: 'string', enum: ['series'], description: '重复日程必须先向用户说明影响;确认操作整个系列后传 series。不能只改或删除某一次。' },
268
270
  }),
269
271
  output: {
270
272
  schema: {
@@ -285,7 +287,7 @@ export function buildCalendarTools(config, env = process.env) {
285
287
  async execute(args, exec) {
286
288
  const input = asRecord(args);
287
289
  const uid = requiredString(input, 'uid', '事件 uid');
288
- const result = await service().delete(uid, executionSignal(exec));
290
+ const result = await service().delete(uid, executionSignal(exec), input.scope === 'series' ? 'series' : undefined);
289
291
  return { deleted: true, uid: result.uid, href: result.href };
290
292
  },
291
293
  timeoutMs: TIMEOUT_MS,
package/lib/web.d.ts CHANGED
@@ -27,10 +27,11 @@ export declare function postVerdict(headers: Record<string, unknown>): {
27
27
  } | undefined;
28
28
  /** 面板需要的服务面;测试里可以塞假的进来。 */
29
29
  export interface CalendarServiceLike {
30
+ get?(uid: string, signal?: AbortSignal): Promise<CalendarEvent>;
30
31
  list(start: string, end: string, options?: unknown, signal?: AbortSignal): Promise<CalendarEvent[]>;
31
32
  create(fields: unknown, signal?: AbortSignal): Promise<CalendarEvent>;
32
- update(uid: string, changes: unknown, signal?: AbortSignal): Promise<CalendarEvent>;
33
- delete(uid: string, signal?: AbortSignal): Promise<{
33
+ update(uid: string, changes: unknown, signal?: AbortSignal, scope?: 'series'): Promise<CalendarEvent>;
34
+ delete(uid: string, signal?: AbortSignal, scope?: 'series'): Promise<{
34
35
  uid: string;
35
36
  href: string;
36
37
  }>;
package/lib/web.js CHANGED
@@ -388,13 +388,19 @@ export class CalendarSettingsBackend {
388
388
  const fields = eventFields(body, true);
389
389
  return { event: await this.service().create(fields) };
390
390
  }
391
+ if (action === 'event') {
392
+ const service = this.service();
393
+ if (!service.get)
394
+ throw new Error('当前日历连接不支持读取系列原始事件');
395
+ return { event: await service.get(requireString(body, 'uid', '缺少事件 uid')) };
396
+ }
391
397
  if (action === 'update') {
392
398
  const uid = requireString(body, 'uid', '缺少要修改的事件 uid');
393
- return { event: await this.service().update(uid, eventFields(body, false)) };
399
+ return { event: await this.service().update(uid, eventFields(body, false), undefined, body.scope === 'series' ? 'series' : undefined) };
394
400
  }
395
401
  if (action === 'delete') {
396
402
  const uid = requireString(body, 'uid', '缺少要删除的事件 uid');
397
- return await this.service().delete(uid);
403
+ return await this.service().delete(uid, undefined, body.scope === 'series' ? 'series' : undefined);
398
404
  }
399
405
  throw new Error('未知动作:' + (action === '' ? '(空)' : action));
400
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calendar",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "连接 CalDAV 日历,在 DSH 中查看、创建和调整日程。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",