dsh-calendar 0.2.1 → 0.3.0

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/README.md CHANGED
@@ -19,10 +19,35 @@ dsh plugin --profile web add dsh-calendar
19
19
  - `provider`:google | icloud | nextcloud | custom
20
20
  - `caldavUrl`:完整日历集合 URL(custom / icloud 必填;google / nextcloud 也可手填覆盖预设)
21
21
  - `username`:CalDAV 账号(Google / iCloud 为账号邮箱)
22
- - `password`:密码;Google / iCloud 请用应用专用密码。推荐用环境变量 `DSH_CALENDAR_PASSWORD`,避免明文入库。
22
+ - `password`:密码;Google / iCloud 请用应用专用密码。推荐用环境变量 `DSH_CALENDAR_PASSWORD`,避免明文入库。
23
+ - `proxyUrl`:本机代理地址(如 http://127.0.0.1:7890)。中国大陆访问 Google / iCloud 必填,详见上文「特殊代理配置」专节;国内可直连的 CalDAV 服务无需填写。
23
24
  - `calendarId`:google 专用,日历 ID(通常是你的邮箱)
24
25
  - `host` / `user` / `calendar`:nextcloud 专用
25
26
 
27
+ ## 中国用户:特殊代理配置(Google / iCloud)
28
+
29
+ Google 与 iCloud 的 CalDAV 端点在中国大陆**不可直连**,需要配合你常用的梯子/特殊代理使用。插件内置 `proxyUrl` 配置:把 CalDAV 请求路由到**你本机代理客户端的端口**,不影响其他插件,也无需改任何系统设置。
30
+
31
+ ```yaml
32
+ - id: calendar
33
+ config:
34
+ provider: google
35
+ username: you@gmail.com
36
+ calendarId: you@gmail.com
37
+ password: 你的应用专用密码
38
+ proxyUrl: http://127.0.0.1:7890 # 改成你代理客户端的本地端口
39
+ ```
40
+
41
+ ### 常见代理客户端本地端口
42
+
43
+ | 客户端 | 本地端口 |
44
+ |---|---|
45
+ | Clash / Clash Verge(HTTP) | 7890 / 7897 |
46
+ | v2rayN(HTTP / SOCKS) | 10808 / 10809 |
47
+ | Shadowsocks | 1080 |
48
+
49
+ 在客户端界面确认你的实际端口,填进 `proxyUrl` 即可。国内可直连的 CalDAV 服务(如自建 Nextcloud)则无需填写。
50
+
26
51
  ### Google 示例
27
52
 
28
53
  ```yaml
@@ -101,7 +126,10 @@ iCloud:登录 appleid.apple.com → 登录与安全 → App 专用密码,生
101
126
 
102
127
  输入输出统一 ISO 8601。定时事件输出为 UTC(如 `2025-01-15T01:00:00Z`),全天事件输出 `YYYY-MM-DD`。输入可带时区偏移(如 `2025-01-15T09:00:00+08:00`),插件内部转 UTC 存储。
103
128
 
104
- ## 已知限制
129
+ ## 已知限制
130
+
131
+ - **国内网络**:Google 与 iCloud 的 CalDAV 端点在中国大陆不可直连;可用 `proxyUrl` 走本机梯子/特殊代理,或改用国内可直连的 CalDAV 端点(自建 Nextcloud/Radicale)。
132
+
105
133
 
106
134
  - 重复事件展开:calendar_list 默认用 ICAL.RecurExpansion 展开 RRULE(`expand=true`),受 `maxOccurrences` 封顶;calendar_search 仍返回原始系列(不展开)。
107
135
  - 不支持单次实例的改/删:calendar_update / calendar_delete 针对整个重复系列(按 uid 操作),无法只修改或删除某一次发生(不支持 RECURRENCE-ID 实例级操作)。
package/lib/caldav.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module dsh-calendar/caldav
6
6
  */
7
7
  import { createDAVClient } from 'tsdav';
8
+ import { createProxyFetch } from './proxy-fetch.js';
8
9
  import { buildICalString, expandEventFromICal, generateUid, parseEventFromICal, } from './ical.js';
9
10
  /** CalDAV 操作错误:带中文指引。 */
10
11
  export class CalDAVError extends Error {
@@ -47,6 +48,7 @@ export class CalendarService {
47
48
  serverUrl: this.config.caldavUrl,
48
49
  credentials: { username: this.config.username, password: this.config.password },
49
50
  authMethod: 'Basic',
51
+ ...(this.config.proxyUrl !== '' ? { fetch: createProxyFetch(this.config.proxyUrl) } : {}),
50
52
  });
51
53
  return this.clientPromise;
52
54
  }
package/lib/config.d.ts CHANGED
@@ -24,6 +24,12 @@ export interface CalendarConfig {
24
24
  user?: string;
25
25
  /** nextcloud:日历名。 */
26
26
  calendar?: string;
27
+ /**
28
+ * HTTP 代理地址,如 http://127.0.0.1:7890。
29
+ * 中国用户访问 Google / iCloud 需经代理:填你本地代理客户端的端口即可,
30
+ * 插件会把所有 CalDAV 请求路由到该代理,不影响其他插件。
31
+ */
32
+ proxyUrl?: string;
27
33
  }
28
34
  /** 解析完成、可直接建客户端的配置。 */
29
35
  export interface ResolvedConfig {
@@ -31,6 +37,7 @@ export interface ResolvedConfig {
31
37
  caldavUrl: string;
32
38
  username: string;
33
39
  password: string;
40
+ proxyUrl: string;
34
41
  }
35
42
  /** 预设端点常量。 */
36
43
  export declare const GOOGLE_CALDAV_PREFIX = "https://apidata.googleusercontent.com/caldav/v2/";
package/lib/config.js CHANGED
@@ -49,7 +49,7 @@ export function resolveConfig(config, env = process.env) {
49
49
  '或在 profile 的 cordis.patch.yml 覆盖 calendar 行的 password(Google / iCloud 请用应用专用密码)后重启。');
50
50
  }
51
51
  const caldavUrl = buildCaldavUrl(config ?? {}, provider);
52
- return { provider, caldavUrl, username, password };
52
+ return { provider, caldavUrl, username, password, proxyUrl: nonEmpty(config?.proxyUrl) ?? '' };
53
53
  }
54
54
  /** 依据 provider 预设或手填字段拼出日历集合 URL。 */
55
55
  export function buildCaldavUrl(config, provider) {
package/lib/ical.d.ts CHANGED
@@ -41,6 +41,8 @@ export interface EventFields {
41
41
  description?: string;
42
42
  location?: string;
43
43
  allDay?: boolean;
44
+ /** 重复规则(RFC 5545 RRULE 语法),如 FREQ=WEEKLY;COUNT=4。 */
45
+ rrule?: string;
44
46
  /** iCal UID;创建时缺省则自动生成,更新时用于保留原 UID。 */
45
47
  icalUid?: string;
46
48
  }
package/lib/ical.js CHANGED
@@ -216,6 +216,18 @@ export function buildICalString(fields) {
216
216
  if (fields.location !== undefined && fields.location !== '') {
217
217
  vevent.addPropertyWithValue('location', fields.location);
218
218
  }
219
+ if (fields.rrule !== undefined && fields.rrule !== '') {
220
+ const rule = fields.rrule.trim();
221
+ if (!/^FREQ=/i.test(rule)) {
222
+ throw new Error('rrule 格式无效:' + fields.rrule + '(应为 RFC 5545 RRULE,如 FREQ=WEEKLY;COUNT=4)');
223
+ }
224
+ try {
225
+ vevent.addPropertyWithValue('rrule', ICAL.Recur.fromString(rule));
226
+ }
227
+ catch (error) {
228
+ throw new Error('rrule 格式无效:' + fields.rrule);
229
+ }
230
+ }
219
231
  vcal.addSubcomponent(vevent);
220
232
  return vcal.toString();
221
233
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 构造一个把所有请求路由到 proxyUrl 的 fetch(供 tsdav 的 fetch 注入使用)。
3
+ * @param proxyUrl - 如 http://127.0.0.1:7890
4
+ */
5
+ export declare function createProxyFetch(proxyUrl: string): typeof globalThis.fetch;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 插件级 HTTP 代理:给 CalDAV 请求一个走指定代理的 fetch,不影响同进程其他插件。
3
+ */
4
+ import { ProxyAgent, fetch as undiciFetch } from 'undici';
5
+ /**
6
+ * 构造一个把所有请求路由到 proxyUrl 的 fetch(供 tsdav 的 fetch 注入使用)。
7
+ * @param proxyUrl - 如 http://127.0.0.1:7890
8
+ */
9
+ export function createProxyFetch(proxyUrl) {
10
+ const agent = new ProxyAgent(proxyUrl);
11
+ // undici 自带类型与全局 fetch 类型不完全一致,桥接处用 any 避免类型摩擦;
12
+ // 对外契约仍是 typeof globalThis.fetch。
13
+ return ((input, init) => undiciFetch(input, { ...init, dispatcher: agent }));
14
+ }
package/lib/tools.js CHANGED
@@ -115,7 +115,7 @@ export function buildCalendarTools(config) {
115
115
  };
116
116
  const create = {
117
117
  name: 'calendar_create',
118
- description: '新建一个日历事件。summary 与 start、end(ISO 8601,含时区偏移)为必填;description、location、allDay 可选。全天事件 start/end 用 YYYY-MM-DD 或设置 allDay=true。成功返回新事件的稳定标识 uid。',
118
+ description: '新建一个日历事件。summary 与 start、end(ISO 8601,含时区偏移)为必填;description、location、allDay、rrule 可选。全天事件 start/end 用 YYYY-MM-DD 或设置 allDay=true。成功返回新事件的稳定标识 uid。',
119
119
  parameters: compileParameters({
120
120
  summary: { type: 'string', required: true, description: '事件标题(必填)。' },
121
121
  start: { type: 'string', required: true, description: '开始时间(ISO 8601,含时区偏移,必填)。' },
@@ -123,6 +123,7 @@ export function buildCalendarTools(config) {
123
123
  description: { type: 'string', description: '事件描述(可选)。' },
124
124
  location: { type: 'string', description: '地点(可选)。' },
125
125
  allDay: { type: 'boolean', description: '是否全天事件(可选,默认 false)。' },
126
+ rrule: { type: 'string', description: '重复规则(可选,RFC 5545 RRULE 语法),如 FREQ=WEEKLY;COUNT=4;calendar_list 会按该规则展开实例。' },
126
127
  }),
127
128
  output: {
128
129
  schema: {
@@ -150,6 +151,7 @@ export function buildCalendarTools(config) {
150
151
  ...(optionalString(input, 'description') !== undefined ? { description: optionalString(input, 'description') } : {}),
151
152
  ...(optionalString(input, 'location') !== undefined ? { location: optionalString(input, 'location') } : {}),
152
153
  ...(optionalBoolean(input, 'allDay') !== undefined ? { allDay: optionalBoolean(input, 'allDay') } : {}),
154
+ ...(optionalString(input, 'rrule') !== undefined ? { rrule: optionalString(input, 'rrule') } : {}),
153
155
  };
154
156
  const created = await service().create(fields);
155
157
  return { created };
@@ -167,6 +169,7 @@ export function buildCalendarTools(config) {
167
169
  description: { type: 'string', description: '新描述(可选)。' },
168
170
  location: { type: 'string', description: '新地点(可选)。' },
169
171
  allDay: { type: 'boolean', description: '是否全天事件(可选)。' },
172
+ rrule: { type: 'string', description: '新的重复规则(可选)。' },
170
173
  }),
171
174
  output: {
172
175
  schema: {
@@ -206,6 +209,9 @@ export function buildCalendarTools(config) {
206
209
  const allDay = optionalBoolean(input, 'allDay');
207
210
  if (allDay !== undefined)
208
211
  changes.allDay = allDay;
212
+ const rrule = optionalString(input, 'rrule');
213
+ if (rrule !== undefined)
214
+ changes.rrule = rrule;
209
215
  const updated = await service().update(uid, changes);
210
216
  return { updated };
211
217
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-calendar",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "DSH 日历工具插件:通过 CalDAV 读写日历事件(Google / iCloud / Nextcloud / 自定义)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "ical.js": "^2.1.0",
47
- "tsdav": "^2.3.1"
47
+ "tsdav": "^2.3.1",
48
+ "undici": "^8.10.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/node": "^24.0.0",