dsh-calendar 0.10.4 → 0.11.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,15 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.11.1(2026-10-20)
8
+
9
+ - 修复 calendar_update 传入空字符串时无法清空说明、地点或重复规则的问题;省略字段仍保留原值。
10
+ - 首次使用引导跟随宿主中文 / English,按当前连接、视图和写入行为说明操作,不再承诺所有改删都有额外确认。
11
+
12
+ ## 0.11.0(2026-10-17)
13
+
14
+ - 设置面板顶部新增「首次使用」分步引导:一次只讲一步,支持上一步/下一步/跳过,看完记在本机不再出现。
15
+ - 引导内容与该插件的实际用法一致;可在开发者工具里清掉 `dsh-first-run:dsh-calendar:v1` 重新查看。
7
16
  ## 0.10.4 (2026-10-17)
8
17
 
9
18
  - 日程与连接表单采用每个实例独立的字段标识,点击标签可聚焦输入框,并提供正确的可访问名称。
package/docs/USAGE.md CHANGED
@@ -187,7 +187,7 @@ Nextcloud / 自定义 Basic 服务:检查账号、密码或服务要求的应
187
187
  - `calendar_health`:离线检查服务商、日历集合地址与 Basic/OAuth 凭据完整性,不回显密钥、不发起网络连接。
188
188
  - `calendar_list`:列出某时间段事件(start/end,ISO 8601,缺省未来 7 天)。默认展开重复事件(`expand` 默认 true,`maxOccurrences` 默认 30、clamp 1-200):每个实例独立成行,带 `isOccurrence: true` 与 `seriesStart`;非重复事件保持 `isOccurrence: false`。`expand=false` 时重复事件按原始单条返回并带 `rrule`。结果按开始时间稳定排序
189
189
  - `calendar_create`:新建事件(summary/start/end 必填,description/location/allDay/rrule 可选)。严格校验真实日历日期与 `end >= start`
190
- - `calendar_update`:按 uid 改事件(summary/start/end/description/location/allDay/rrule 可选,未提供保留原值;ATTENDEE、ORGANIZER、VALARM 等原始属性一并保留,重复规则不再丢失)
190
+ - `calendar_update`:按 uid 改事件(summary/start/end/description/location/allDay/rrule 可选,未提供保留原值;description、location、rrule 显式传空字符串可清空说明、地点或停止重复;ATTENDEE、ORGANIZER、VALARM 等原始属性一并保留)
191
191
  - `calendar_delete`:按 uid 删事件
192
192
  - `calendar_search`:按关键词搜事件(只查询 start~end 窗口内的事件,缺省为当前时间前后各 1 年;客户端过滤标题/描述/地点/UID,不区分大小写;`limit` 默认 50、clamp 1-200,结果按开始时间排序)
193
193
 
package/lib/client.js CHANGED
@@ -10,6 +10,84 @@ var module = { exports: {} }; var exports = module.exports;
10
10
  "use strict";
11
11
 
12
12
  const React = require("react");
13
+ var __FR_KEY = "dsh-first-run:dsh-calendar:v1";
14
+ var __FR_TITLE = "日历 · 首次使用(4 步)";
15
+ var __FR_STEPS = [{title:"先测试日历连接",body:"选择 Google、iCloud、Nextcloud 或自建 CalDAV,填写对应登录信息,先测试再保存。"},{title:"查看实际日程",body:"在月、周或议程视图查看日程;重复事件可展开成每次发生的实例。"},{title:"把时间说清楚",body:"新建时说明标题、开始和结束时间;跨时区请带上时区。",tryIt:"下周三下午三点创建一小时的产品评审,地点会议室 A,使用北京时间"},{title:"保存前核对",body:"更改会写入已连接的日历。保存前核对日期和重复规则;删除后无法在插件里撤销。"}];
16
+ var __FR_TITLE_EN = "Calendar · First steps";
17
+ var __FR_STEPS_EN = [{title:"Test the calendar connection",body:"Choose Google, iCloud, Nextcloud or a custom CalDAV service. Fill in its sign-in details, then test before saving."},{title:"View your events",body:"Use the month, week or agenda view. Recurring events can be expanded into individual occurrences."},{title:"Specify the time",body:"Include a title, start and end time. Add a time zone when working across regions.",tryIt:"Create a one-hour product review next Wednesday at 3 pm Beijing time in Meeting Room A."},{title:"Review before saving",body:"Changes are written to the connected calendar. Check dates and recurrence rules before saving; deleted events cannot be undone in this plugin."}];
18
+
19
+ // ---- first-run guide (shared snippet; keep in sync across plugins) ----
20
+ var __FR_STYLE_ID = 'dsh-first-run-css';
21
+ var __FR_CSS = [
22
+ '.fr-card{border:1px solid var(--dsw-border,rgba(0,0,0,.10));border-radius:12px;padding:14px 16px;margin:0 0 14px;background:linear-gradient(180deg,rgba(59,130,246,.06),rgba(59,130,246,.02))}',
23
+ '.fr-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}',
24
+ '.fr-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-accent,#3b82f6)}',
25
+ '.fr-title{font-weight:650;font-size:13.5px}',
26
+ '.fr-count{margin-left:auto;font-size:11px;color:var(--dsw-text-weak,#6b7280);font-variant-numeric:tabular-nums}',
27
+ '.fr-step{font-weight:600;font-size:13px;margin:2px 0 4px}',
28
+ '.fr-body{color:var(--dsw-text-weak,#4b5563);font-size:12.5px;line-height:1.65}',
29
+ '.fr-try{margin-top:8px;padding:7px 9px;border-radius:8px;background:rgba(59,130,246,.09);font-size:12px;display:flex;gap:8px}',
30
+ '.fr-try code{font-family:ui-monospace,monospace;white-space:pre-wrap;word-break:break-word}',
31
+ '.fr-bar{display:flex;gap:8px;align-items:center;margin-top:12px}',
32
+ '.fr-progress{display:flex;gap:4px;margin-right:auto}',
33
+ '.fr-seg{width:16px;height:3px;border-radius:2px;background:var(--dsw-border,rgba(0,0,0,.12))}',
34
+ '.fr-seg.on{background:var(--dsw-accent,#3b82f6)}',
35
+ '.fr-btn{border:1px solid var(--dsw-border,rgba(0,0,0,.14));background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer}',
36
+ '.fr-btn:hover{background:rgba(0,0,0,.04)}',
37
+ '.fr-btn.primary{background:var(--dsw-accent,#3b82f6);border-color:transparent;color:#fff}',
38
+ '.fr-btn.ghost{border-color:transparent;color:var(--dsw-text-weak,#6b7280)}',
39
+ ].join('\n');
40
+ function __frEnsureCss() {
41
+ if (typeof document === 'undefined' || document.getElementById(__FR_STYLE_ID)) return;
42
+ var el = document.createElement('style');
43
+ el.id = __FR_STYLE_ID;
44
+ el.textContent = __FR_CSS;
45
+ document.head.appendChild(el);
46
+ }
47
+ function __frRead() { try { return localStorage.getItem(__FR_KEY) === 'done'; } catch (e) { return false; } }
48
+ function __frWrite() { try { localStorage.setItem(__FR_KEY, 'done'); } catch (e) {} }
49
+ function __FirstRunCard() {
50
+ useUILanguage();
51
+ var english = UI_LANG === 'en';
52
+ try { if (activeLocaleService?.getSnapshot) english = localeToLang(activeLocaleService.getSnapshot().active) === 'en'; } catch {}
53
+ var steps = english ? __FR_STEPS_EN : __FR_STEPS;
54
+ var text = (zh, en) => english ? en : zh;
55
+ var doneState = React.useState(__frRead);
56
+ var done = doneState[0], setDone = doneState[1];
57
+ var idxState = React.useState(0);
58
+ var index = idxState[0], setIndex = idxState[1];
59
+ React.useEffect(function () { __frEnsureCss(); }, []);
60
+ if (done) return null;
61
+ var step = steps[index];
62
+ var last = index >= steps.length - 1;
63
+ var close = function () { __frWrite(); setDone(true); };
64
+ var kids = [
65
+ React.createElement('div', { className: 'fr-head', key: 'h' },
66
+ React.createElement('span', { className: 'fr-dot' }),
67
+ React.createElement('span', { className: 'fr-title' }, english ? __FR_TITLE_EN : __FR_TITLE),
68
+ React.createElement('span', { className: 'fr-count' }, (index + 1) + ' / ' + steps.length)),
69
+ React.createElement('div', { className: 'fr-step', key: 't' }, step.title),
70
+ React.createElement('div', { className: 'fr-body', key: 'b' }, step.body),
71
+ ];
72
+ if (step.tryIt) kids.push(React.createElement('div', { className: 'fr-try', key: 'y' },
73
+ React.createElement('span', null, text('可以直接说', 'Try saying')),
74
+ React.createElement('code', null, text('「', '“') + step.tryIt + text('」', '”'))));
75
+ var buttons = [React.createElement('div', { className: 'fr-progress', key: 'p' }, steps.map(function (_, i) {
76
+ return React.createElement('span', { key: i, className: 'fr-seg' + (i <= index ? ' on' : '') });
77
+ }))];
78
+ buttons.push(React.createElement('button', { className: 'fr-btn ghost', type: 'button', onClick: close, key: 's' }, text('跳过', 'Skip')));
79
+ if (index > 0) buttons.push(React.createElement('button', { className: 'fr-btn', type: 'button', key: 'prev', onClick: function () { setIndex(index - 1); } }, text('上一步', 'Back')));
80
+ buttons.push(React.createElement('button', { className: 'fr-btn primary', type: 'button', key: 'next', onClick: function () { if (last) close(); else setIndex(index + 1); } }, last ? text('开始使用', 'Start') : text('下一步', 'Next')));
81
+ kids.push(React.createElement('div', { className: 'fr-bar', key: 'bar' }, buttons));
82
+ return React.createElement('div', { className: 'fr-card' }, kids);
83
+ }
84
+ function __withFirstRun(Component) {
85
+ return function FirstRunWrapped(props) {
86
+ return React.createElement(React.Fragment, null, React.createElement(__FirstRunCard, null), React.createElement(Component, props));
87
+ };
88
+ }
89
+ // ---- end first-run guide ----
90
+
13
91
  const { useState, useEffect, useMemo, useRef, useCallback, useId } = React;
14
92
  const h = React.createElement;
15
93
 
@@ -2195,7 +2273,7 @@ function apply(ctx) {
2195
2273
  return UI[lang]["nav.title"];
2196
2274
  },
2197
2275
  inject: () => ({})
2198
- }, CalendarSection));
2276
+ }, __withFirstRun(CalendarSection)));
2199
2277
  } catch (e) { /* 没有 slots 服务:仍是合法的插件,只是没有设置页 */ }
2200
2278
 
2201
2279
  try {
package/lib/tools.js CHANGED
@@ -24,6 +24,11 @@ export function optionalString(args, key) {
24
24
  const value = args[key];
25
25
  return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
26
26
  }
27
+ /** An omitted patch preserves the field; an explicit empty string clears it. */
28
+ function optionalPatchString(args, key) {
29
+ const value = args[key];
30
+ return typeof value === 'string' ? value.trim() : undefined;
31
+ }
27
32
  function requiredString(args, key, label) {
28
33
  const value = optionalString(args, key);
29
34
  if (value === undefined) {
@@ -198,16 +203,16 @@ export function buildCalendarTools(config, env = process.env) {
198
203
  };
199
204
  const update = {
200
205
  name: 'calendar_update',
201
- description: '按 uid 修改日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。summary、start、end、description、location、allDay、rrule 均可选,未提供的字段保留原值。',
206
+ description: '按 uid 修改日历事件。uid 必填(来自 calendar_list / calendar_search 返回的 uid)。summary、start、end、description、location、allDay、rrule 均可选,未提供的字段保留原值。description、location、rrule 显式传空字符串可分别清空说明、地点或停止重复。',
202
207
  parameters: compileParameters({
203
208
  uid: { type: 'string', required: true, description: '事件稳定标识(来自 calendar_list / calendar_search 的 uid,必填)。' },
204
209
  summary: { type: 'string', description: '新标题(可选)。' },
205
210
  start: { type: 'string', description: '新开始时间(ISO 8601,可选)。' },
206
211
  end: { type: 'string', description: '新结束时间(ISO 8601,可选)。' },
207
- description: { type: 'string', description: '新描述(可选)。' },
208
- location: { type: 'string', description: '新地点(可选)。' },
212
+ description: { type: 'string', description: '新描述(可选);空字符串清空说明,省略则保留。' },
213
+ location: { type: 'string', description: '新地点(可选);空字符串清空地点,省略则保留。' },
209
214
  allDay: { type: 'boolean', description: '是否全天事件(可选)。' },
210
- rrule: { type: 'string', description: '新的重复规则(可选)。' },
215
+ rrule: { type: 'string', description: '新的重复规则(可选);空字符串停止重复,省略则保留。' },
211
216
  }),
212
217
  output: {
213
218
  schema: {
@@ -238,16 +243,16 @@ export function buildCalendarTools(config, env = process.env) {
238
243
  assertIsoTime(end, 'end');
239
244
  changes.end = end;
240
245
  }
241
- const description = optionalString(input, 'description');
246
+ const description = optionalPatchString(input, 'description');
242
247
  if (description !== undefined)
243
248
  changes.description = description;
244
- const location = optionalString(input, 'location');
249
+ const location = optionalPatchString(input, 'location');
245
250
  if (location !== undefined)
246
251
  changes.location = location;
247
252
  const allDay = optionalBoolean(input, 'allDay');
248
253
  if (allDay !== undefined)
249
254
  changes.allDay = allDay;
250
- const rrule = optionalString(input, 'rrule');
255
+ const rrule = optionalPatchString(input, 'rrule');
251
256
  if (rrule !== undefined)
252
257
  changes.rrule = rrule;
253
258
  const updated = await service().update(uid, changes, executionSignal(exec));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calendar",
3
- "version": "0.10.4",
3
+ "version": "0.11.1",
4
4
  "description": "连接 CalDAV 日历,在 DSH 中查看、创建和调整日程。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",