dsh-calendar 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.10.1 (2026-10-03)
8
+
9
+ - 修复语言切换时菜单和悬浮按钮不更新;日期、星期、重复规则、ICS 告警与连接引导支持英文。
10
+ - 验收范围见 [中英文界面检查](docs/validation-language-2026-10-03.md)。
11
+
7
12
  ## 0.10.0 (2026-10-03)
8
13
 
9
14
  - 新增 ICS 导入:选择文件或粘贴内容,预览后自行勾选、分批确认;保留重复规则、排除日期、改期例外和时区定义。
@@ -1,5 +1,7 @@
1
1
  # dsh-calendar 验证记录
2
2
 
3
+ [0.10.1 中英文界面验收](validation-language-2026-10-03.md)。
4
+
3
5
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
6
 
5
7
  ## 当前版本
@@ -0,0 +1,16 @@
1
+ # 0.10.1:中英文界面验收
2
+
3
+ 2026-10-03,Windows / Node 24.16.0。采用 E 盘独立 DSH_HOME、合成资料与无密钥 profile;用户资料、账号、密钥、桌面窗口与 VPN 保持原状。
4
+
5
+ - 自动测试:192 项全部通过。
6
+ - 可见 Web 操作:英文 ICS 粘贴预览,选择 Asia/Shanghai 后事件可选;浮动时间、DTSTAMP 与缺少连接凭据的引导为英文,事件标题保留中文。示例日历中的日期、星期和重复详情为英文;中文切换时导航与独立悬浮按钮及时更新。
7
+ - 宿主:官方 DSH 0.2.1-alpha.1 Web;官方 RC2 与 alpha SDK 同载 18 组件,110 工具、35 插件技能、19 输出样例注册与 schema 检查通过。无账号的健康结果不代表真实连接。
8
+ - 边界:未导入真实 CalDAV 账号;无账号时的连接引导通过不能等同于服务连接通过。
9
+
10
+ 中英文共用同一份资料;界面翻译不会改写标题、正文、路径、引用或项目文件。系统文件选择控件和文件对话框跟随 Windows / 浏览器语言。未知技术诊断与原始引擎日志保留原文;英文界面提供操作恢复引导。
11
+
12
+ 此记录不代表原生桌面窗口内的新功能已逐项操作通过。发布包完整性、GitHub CI 与公开 npm 全新安装在发布时另行核对。
13
+
14
+ ## English scope
15
+
16
+ The Chinese and English UI share the same data. UI translation preserves original content, paths and project files. Visible interactions were performed in an isolated official alpha Web profile; both RC2 and alpha SDK registration/schema checks passed. Native file controls use the OS/browser language, and original diagnostic logs are retained. Browser results do not certify native desktop interaction or real-service delivery.
package/lib/client.js CHANGED
@@ -333,6 +333,62 @@ const UI = {
333
333
 
334
334
  var UI_LANG = "zh";
335
335
  var relocalize = null;
336
+ var activeLocaleService = null;
337
+
338
+ // Backend guidance is localized separately from event titles and source content.
339
+ const BACKEND_EN = {
340
+ "日期格式不合法": "Invalid date format.",
341
+ "日程包含不存在的日期或时间": "This event contains an invalid date or time.",
342
+ "请先选择或粘贴 ICS 文件内容": "Choose an ICS file or paste its content first.",
343
+ "ICS 文件超过 256 KiB,请分批导出": "The ICS file exceeds 256 KiB. Export smaller batches.",
344
+ "ICS 结构不合法或嵌套过深": "The ICS structure is invalid or nested too deeply.",
345
+ "ICS 文件不完整": "The ICS file is incomplete.",
346
+ "无法读取 ICS,请从日历应用重新导出": "Could not read the ICS file. Export it again from your calendar app.",
347
+ "暂不支持 PERIOD 类型的 RDATE,请改用日期列表后导入": "PERIOD RDATE values are unsupported. Export a list of dates instead.",
348
+ "全天日期不能含 TZID": "All-day dates must not contain TZID.",
349
+ "文件含未标注时区的时间,请选择解释时区后重新预览": "This file contains times without a timezone. Choose an interpretation timezone and preview again.",
350
+ "UTC 时间不能同时标注 TZID": "UTC timestamps must not also contain TZID.",
351
+ "原文件缺少 DTSTAMP,导入时补齐": "DTSTAMP is missing and will be added during import.",
352
+ "作为私人日程复制,不导入参与者或发送邀请": "Imported as a private copy. Attendees are omitted and no invitations are sent.",
353
+ "仅保留屏幕提醒,已移除其他提醒动作": "Only display reminders are retained; other alarm actions were removed.",
354
+ "开始与结束必须同时为全天日期或具体时间": "Start and end must both be all-day dates or both be timestamps.",
355
+ "日程或重复例外的结束时间早于开始时间": "The event or recurring exception ends before it starts.",
356
+ "日程无法解析": "Could not parse this event.",
357
+ "结束时间早于开始时间": "End time precedes start time.",
358
+ "重复规则 UNTIL 需要 UTC 时间,请重新导出": "The recurrence UNTIL must use UTC. Export the event again.",
359
+ "日程格式不合法": "Invalid event format.",
360
+ "日历连接已改变,请重新预览": "The calendar connection changed. Preview again.",
361
+ "请先核对预览,再确认导入": "Review the preview before confirming import.",
362
+ "预览已过期或插件已重启,请重新预览": "The preview expired or the plugin restarted. Preview again.",
363
+ "每次请选择 1–20 个日程系列,其余可分批导入": "Select 1–20 event series per batch.",
364
+ "选择包含不可导入的日程,请重新预览": "Some selected events cannot be imported. Preview again.",
365
+ "另一批日程正在导入,请稍后重试": "Another batch is importing. Try again after it finishes.",
366
+ "不支持此导入操作": "Unsupported import operation.",
367
+ "请求必须是对象": "The request must be an object."
368
+ };
369
+ function backendMessage(error) {
370
+ const message = typeof error === 'string' ? error : error?.message || String(error || '');
371
+ if (UI_LANG !== 'en') return message;
372
+ if (error?.status === 401) return 'Your DSH session expired. Reopen DSH and try again.';
373
+ if (BACKEND_EN[message]) return BACKEND_EN[message];
374
+ if (!/[\u3400-\u9fff]/.test(message)) return message;
375
+ const timezone = message.match(/^(文件重复定义时区|时区定义不完整|时区规则的 DTSTART 必须是本地时间|时区偏移不合法|缺少时区定义):(.+)$/);
376
+ if (timezone) return ({ '文件重复定义时区': 'Duplicate timezone definition', '时区定义不完整': 'Incomplete timezone definition', '时区规则的 DTSTART 必须是本地时间': 'Timezone DTSTART must use local time', '时区偏移不合法': 'Invalid timezone offset', '缺少时区定义': 'Missing timezone definition' })[timezone[1]] + ': ' + timezone[2];
377
+ const floating = message.match(/^原文件未标注时区,按 (.+) 解释$/);
378
+ if (floating) return 'No timezone in the source; interpreted as ' + floating[1] + '.';
379
+ if (/OAuth|Google|凭据|未配置|尚未配置|没有配置|连接设置/.test(message)) return 'Calendar credentials are incomplete. Open Connection settings and configure the calendar account; the DSH model API key cannot be used here.';
380
+ if (/时区/.test(message)) return 'Invalid or missing timezone. Choose an interpretation timezone or export a file with VTIMEZONE.';
381
+ if (/VCALENDAR|VERSION|VEVENT/.test(message)) return 'Use a valid iCalendar 2.0 file containing VEVENT entries.';
382
+ if (/UID|重复/.test(message)) return 'Check event UIDs and recurrence definitions, then export the file again.';
383
+ if (/冲突|修改|改变/.test(message)) return 'The calendar or event changed. Reload it and review your input before retrying.';
384
+ return 'The calendar operation failed. Check the connection and source file, then try again.';
385
+ }
386
+
387
+ const languageListeners = new Set();
388
+ function useUILanguage() {
389
+ const [, render] = useState(UI_LANG);
390
+ useEffect(() => { const update = () => render(UI_LANG); languageListeners.add(update); update(); return () => languageListeners.delete(update); }, []);
391
+ }
336
392
 
337
393
  /** 宿主 locale 服务:get('locale') 是正规读法,ctx.locale 兜旧宿主,都没有就是没有。 */
338
394
  function localeServiceOf(ctx) {
@@ -354,6 +410,7 @@ function localeToLang(active) {
354
410
  /** 订阅语言切换;服务缺失或形状不同都只是留在 zh,绝不把面板拖垮。 */
355
411
  function subscribeLocale(ctx) {
356
412
  const service = localeServiceOf(ctx);
413
+ activeLocaleService = service;
357
414
  if (service === null) return () => {};
358
415
  const apply = () => {
359
416
  try {
@@ -399,6 +456,7 @@ async function api(action, payload) {
399
456
  const error = new Error(message);
400
457
  error.status = res.status;
401
458
  error.code = body && body.error ? body.error.code : undefined;
459
+ error.message = backendMessage(error);
402
460
  throw error;
403
461
  }
404
462
  return body.value;
@@ -425,11 +483,13 @@ function daysBetween(a, b) { return Math.round((startOfDay(b) - startOfDay(a)) /
425
483
 
426
484
  function formatDay(date, opts) {
427
485
  const withYear = opts && opts.withYear;
486
+ if (UI_LANG === "en") return date.toLocaleDateString("en-GB", { year: "numeric", month: "short", day: "numeric" });
428
487
  return date.getFullYear() + " 年 " + (date.getMonth() + 1) + " 月 " + date.getDate() + " 日" + (withYear ? "" : "");
429
488
  }
430
- function formatMonthTitle(date) { return date.getFullYear() + " 年 " + (date.getMonth() + 1) + " 月"; }
489
+ function formatMonthTitle(date) { return UI_LANG === "en" ? date.toLocaleDateString("en-GB", { year: "numeric", month: "long" }) : date.getFullYear() + " 年 " + (date.getMonth() + 1) + " 月"; }
431
490
  function formatWeekTitle(date) {
432
491
  const from = startOfWeek(date), to = addDays(from, 6);
492
+ if (UI_LANG === "en") return formatDay(from) + " – " + formatDay(to);
433
493
  return formatMonthTitle(from) + " " + from.getDate() + "–" + (to.getMonth() === from.getMonth() ? "" : (to.getMonth() + 1) + " 月 ") + to.getDate() + " 日";
434
494
  }
435
495
  function relativeDay(date) {
@@ -437,7 +497,7 @@ function relativeDay(date) {
437
497
  if (diff === 0) return t("day.today");
438
498
  if (diff === 1) return t("day.tomorrow");
439
499
  if (diff === -1) return t("day.yesterday");
440
- return "周" + WEEKDAYS_ZH[isoWeekday(date) - 1];
500
+ return UI_LANG === "en" ? date.toLocaleDateString("en-GB", { weekday: "short" }) : "周" + WEEKDAYS_ZH[isoWeekday(date) - 1];
441
501
  }
442
502
  function formatWhen(event) {
443
503
  if (event.allDay) return t("format.allday") + " · " + formatDay(startDate(event));
@@ -448,7 +508,7 @@ function formatWhen(event) {
448
508
  function formatDuration(event) {
449
509
  if (event.allDay) {
450
510
  const days = Math.max(1, daysBetween(startDate(event), endDate(event)));
451
- return days === 1 ? t("format.allday") : days + " 天";
511
+ return days === 1 ? t("format.allday") : days + (UI_LANG === "en" ? " days" : " 天");
452
512
  }
453
513
  const minutes = Math.max(0, Math.round((endDate(event) - startDate(event)) / 60000));
454
514
  const hours = Math.floor(minutes / 60), rest = minutes % 60;
@@ -467,6 +527,18 @@ function humanRepeat(rrule) {
467
527
  if (i > 0) parts[piece.slice(0, i).toUpperCase()] = piece.slice(i + 1);
468
528
  });
469
529
  const freq = parts.FREQ;
530
+ if (UI_LANG === "en") {
531
+ const units = { DAILY: ["Daily", "days"], WEEKLY: ["Weekly", "weeks"], MONTHLY: ["Monthly", "months"], YEARLY: ["Yearly", "years"] };
532
+ if (!units[freq]) return rrule;
533
+ const interval = Number(parts.INTERVAL || "1");
534
+ const every = interval > 1 ? "Every " + interval + " " + units[freq][1] : units[freq][0];
535
+ if (parts.COUNT) return every + " (" + parts.COUNT + " occurrences)";
536
+ if (parts.UNTIL) {
537
+ const raw = String(parts.UNTIL), date = raw.length >= 8 ? raw.slice(0, 4) + "-" + raw.slice(4, 6) + "-" + raw.slice(6, 8) : raw;
538
+ return every + ", until " + date;
539
+ }
540
+ return every;
541
+ }
470
542
  const base = freq === "DAILY" ? "每天" : freq === "WEEKLY" ? "每周" : freq === "MONTHLY" ? "每月" : freq === "YEARLY" ? "每年" : "";
471
543
  if (base === "") return rrule;
472
544
  const interval = Number(parts.INTERVAL || "1");
@@ -754,7 +826,7 @@ function MonthView(props) {
754
826
  ]));
755
827
  }
756
828
  return h("div", null, [
757
- h("div", { className: "dshc-grid", style: { marginBottom: "4px" } }, WEEKDAYS_ZH.map((d) => h("div", { className: "dshc-dow", key: d }, "周" + d))),
829
+ h("div", { className: "dshc-grid", style: { marginBottom: "4px" } }, WEEKDAYS_ZH.map((d, i) => h("div", { className: "dshc-dow", key: d }, UI_LANG === "en" ? ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][i] : "周" + d))),
758
830
  h("div", { className: "dshc-grid" }, cells)
759
831
  ]);
760
832
  }
@@ -1438,7 +1510,7 @@ function ConnectionForm(props) {
1438
1510
  const sample = (value.sample || []).join("、");
1439
1511
  setResult({ ok: true, text: t("conn.ok", { n: value.count }) + (sample === "" ? "" : t("conn.okSample", { sample })) });
1440
1512
  } catch (error) {
1441
- setResult({ ok: false, text: error && error.message ? error.message : String(error) });
1513
+ setResult({ ok: false, text: backendMessage(error) });
1442
1514
  } finally {
1443
1515
  setBusy("");
1444
1516
  }
@@ -1449,7 +1521,7 @@ function ConnectionForm(props) {
1449
1521
  try {
1450
1522
  onSaved(await api("saveConnection", Object.assign({}, payload(), { test: true })));
1451
1523
  } catch (error) {
1452
- setResult({ ok: false, text: error && error.message ? error.message : String(error) });
1524
+ setResult({ ok: false, text: backendMessage(error) });
1453
1525
  } finally {
1454
1526
  setBusy("");
1455
1527
  }
@@ -1614,7 +1686,7 @@ Object.assign(UI.en, {
1614
1686
  async function importApi(action, payload, signal) {
1615
1687
  const res = await fetch(IMPORT_ROUTE, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "X-DSH-Calendar-Import": "1" }, body: JSON.stringify(Object.assign({ action }, payload)), signal });
1616
1688
  let body; try { body = await res.json(); } catch (e) { body = null; }
1617
- if (!res.ok || !body || body.ok !== true) throw new Error(body && body.message || "HTTP " + res.status);
1689
+ if (!res.ok || !body || body.ok !== true) throw Object.assign(new Error(backendMessage({ message: body && body.message || "HTTP " + res.status, status: res.status })), { status: res.status });
1618
1690
  return body.value;
1619
1691
  }
1620
1692
  function importDate(row) {
@@ -1642,7 +1714,7 @@ function IcsImportPanel(props) {
1642
1714
  const file = event.target.files && event.target.files[0]; if (!file) return;
1643
1715
  const sequence = ++fileSequence.current; reset(); setContent("");
1644
1716
  if (file.size > 256 * 1024) { setError(t("import.errorSize")); return; }
1645
- try { const value = await file.text(); if (alive.current && sequence === fileSequence.current) setContent(value); } catch (e) { if (alive.current && sequence === fileSequence.current) setError(String(e.message || e)); }
1717
+ try { const value = await file.text(); if (alive.current && sequence === fileSequence.current) setContent(value); } catch (e) { if (alive.current && sequence === fileSequence.current) setError(backendMessage(e)); }
1646
1718
  };
1647
1719
  const eligible = preview ? preview.rows.filter(row => row.status === "ready" && !["imported", "duplicate"].includes(results[row.id] && results[row.id].status)) : [];
1648
1720
  const run = async committing => {
@@ -1655,7 +1727,7 @@ function IcsImportPanel(props) {
1655
1727
  const next = Object.assign({}, results); value.results.forEach(row => { next[row.id] = row; }); setResults(next);
1656
1728
  setSelected(value.results.filter(row => row.status === "failed").map(row => row.id)); props.onImported();
1657
1729
  } else { setPreview(value); setResults({}); setSelected([]); }
1658
- } catch (e) { if (alive.current) setError(abort.signal.aborted ? t("import.stopped") : String(e.message || e)); }
1730
+ } catch (e) { if (alive.current) setError(abort.signal.aborted ? t("import.stopped") : backendMessage(e)); }
1659
1731
  finally { clearTimeout(timer); controller.current = null; if (alive.current) setBusy(false); }
1660
1732
  };
1661
1733
  const count = Object.values(results);
@@ -1668,7 +1740,7 @@ function IcsImportPanel(props) {
1668
1740
  h("div", { key: "actions", className: "dshc-import-actions" }, [h(Btn, { key: "preview", disabled: busy || !content.trim(), onClick: () => run(false) }, busy ? t("import.busy") : t("import.preview")), h(Btn, { key: "close", disabled: busy, onClick: props.onClose }, t("import.close")), busy ? h(Btn, { key: "stop", onClick: () => controller.current && controller.current.abort() }, t("import.stop")) : null]),
1669
1741
  error ? h("div", { key: "error", className: "dshc-alert error", role: "alert" }, error) : null,
1670
1742
  preview ? h("div", { key: "preview-data" }, [
1671
- preview.connectionReady === false ? h("div", { key: "connection", className: "dshc-alert warn" }, [preview.connectionMessage, h(Btn, { onClick: props.onConnection }, t("import.connection"))]) : null,
1743
+ preview.connectionReady === false ? h("div", { key: "connection", className: "dshc-alert warn" }, [backendMessage(preview.connectionMessage), h(Btn, { onClick: props.onConnection }, t("import.connection"))]) : null,
1672
1744
  h("p", { key: "scope", className: "dshc-sub" }, t("import.scope")),
1673
1745
  preview.ignored ? h("p", { key: "ignored" }, t("import.ignored", { n: preview.ignored })) : null,
1674
1746
  h("div", { key: "rows", className: "dshc-import-rows" }, preview.rows.map(row => {
@@ -1678,8 +1750,8 @@ function IcsImportPanel(props) {
1678
1750
  h("span", { key: "status", className: "dshc-tag" }, t("import." + status)), h("p", { key: "date" }, importDate(row)),
1679
1751
  row.recurring ? h("p", { key: "repeat" }, t("import.recurring", { n: row.exceptions })) : null,
1680
1752
  status !== "duplicate" && status !== "imported" && row.conflicts ? h("p", { key: "conflict", className: "dshc-import-warning" }, t("import.conflicts", { n: row.conflicts })) : null,
1681
- ...(status === "duplicate" || status === "imported" ? [] : row.warnings.concat(row.errors)).map((message, i) => h("p", { key: "message-" + i, className: "dshc-import-warning" }, message)),
1682
- result && result.message ? h("p", { key: "failure", role: "alert" }, result.message) : null
1753
+ ...(status === "duplicate" || status === "imported" ? [] : row.warnings.concat(row.errors)).map((message, i) => h("p", { key: "message-" + i, className: "dshc-import-warning" }, backendMessage(message))),
1754
+ result && result.message ? h("p", { key: "failure", role: "alert" }, backendMessage(result.message)) : null
1683
1755
  ]);
1684
1756
  })),
1685
1757
  count.length ? h("p", { key: "results", role: "status", "aria-live": "polite" }, t("import.results", { ok: count.filter(r => r.status === "imported").length, duplicate: count.filter(r => r.status === "duplicate").length, failed: count.filter(r => r.status === "failed").length })) : null,
@@ -1691,6 +1763,7 @@ function IcsImportPanel(props) {
1691
1763
  }
1692
1764
 
1693
1765
  function CalendarPanel(props) {
1766
+ useUILanguage();
1694
1767
  const compact = props.compact === true;
1695
1768
  const [view, setView] = useState(loadStoredView);
1696
1769
  const [anchor, setAnchor] = useState(() => new Date());
@@ -1910,7 +1983,7 @@ function CalendarPanel(props) {
1910
1983
  reload();
1911
1984
  notify(form.uid === undefined ? "已创建" : "已保存");
1912
1985
  } catch (error) {
1913
- setFormError(error && error.message ? error.message : String(error));
1986
+ setFormError(backendMessage(error));
1914
1987
  } finally {
1915
1988
  setBusy(false);
1916
1989
  }
@@ -1926,7 +1999,7 @@ function CalendarPanel(props) {
1926
1999
  reload();
1927
2000
  notify("已删除");
1928
2001
  } catch (error) {
1929
- setBanner(error && error.message ? error.message : String(error));
2002
+ setBanner(backendMessage(error));
1930
2003
  } finally {
1931
2004
  setBusy(false);
1932
2005
  }
@@ -1989,7 +2062,7 @@ function CalendarPanel(props) {
1989
2062
  demo === true ? h("div", { className: "dshc-alert demo" }, t("demo.banner")) : null,
1990
2063
  data.error !== null && data.error !== undefined && demo === false && configured !== false
1991
2064
  ? h("div", { className: "dshc-alert error" }, [
1992
- h("span", null, (data.error.message || t("error.load"))),
2065
+ h("span", null, backendMessage(data.error) || t("error.load")),
1993
2066
  h("span", { className: "dshc-spacer" }),
1994
2067
  h(Btn, { onClick: reload }, t("action.retry"))
1995
2068
  ])
@@ -2044,6 +2117,7 @@ function addMonthShift(date, delta) {
2044
2117
 
2045
2118
  /** 设置页里的那一节。 */
2046
2119
  function CalendarSection() {
2120
+ useUILanguage();
2047
2121
  return h("div", null, [
2048
2122
  h("h2", null, t("section.title")),
2049
2123
  h("p", { className: "dshc-sub" }, t("section.intro")),
@@ -2053,6 +2127,7 @@ function CalendarSection() {
2053
2127
 
2054
2128
  /** 对话页右下角的小按钮 + 悬浮面板(自己挂 root,不占用宿主槽位)。 */
2055
2129
  function Launcher() {
2130
+ useUILanguage();
2056
2131
  const [open, setOpen] = useState(false);
2057
2132
  const [layerOpen, setLayerOpen] = useState(false);
2058
2133
  useEffect(() => {
@@ -2109,7 +2184,11 @@ function apply(ctx) {
2109
2184
  name: "settings.section",
2110
2185
  id: "dsh-calendar",
2111
2186
  order: 50,
2112
- label: () => t("nav.title"),
2187
+ label: () => {
2188
+ let lang = UI_LANG;
2189
+ try { if (activeLocaleService?.getSnapshot) lang = localeToLang(activeLocaleService.getSnapshot().active); } catch {}
2190
+ return UI[lang]["nav.title"];
2191
+ },
2113
2192
  inject: () => ({})
2114
2193
  }, CalendarSection));
2115
2194
  } catch (e) { /* 没有 slots 服务:仍是合法的插件,只是没有设置页 */ }
@@ -2142,7 +2221,7 @@ function mountLauncher() {
2142
2221
  };
2143
2222
  }
2144
2223
 
2145
- relocalize = () => { /* 面板是 React 树,语言切换由 subscribeLocale 触发的重渲染覆盖 */ };
2224
+ relocalize = () => { for (const update of languageListeners) update(); };
2146
2225
 
2147
2226
  exports.apply = apply;
2148
2227
  exports.inject = inject;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calendar",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "连接 CalDAV 日历,在 DSH 中查看、创建和调整日程。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",