@mcpcn/mcp-notification 1.1.3 → 1.1.6

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.
Files changed (2) hide show
  1. package/dist/index.js +140 -12
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -95,21 +95,45 @@ class ReminderServer {
95
95
  tools: [
96
96
  {
97
97
  name: 'set_reminder',
98
- description: '设置通知提醒。支持一次性、按间隔循环、每日循环。',
98
+ description: '设置通知提醒,支持一次性、按间隔循环、每日循环;一次性提醒需提供triggerAt或delaySec,或提供timeOfDay用于推算,优先提供delaySec。',
99
99
  inputSchema: {
100
100
  type: 'object',
101
101
  properties: {
102
- content: { type: 'string' },
103
- repeat: { type: 'string', enum: ['none', 'interval', 'daily'] },
104
- delaySec: { type: 'number' },
105
- triggerAt: { type: 'string', description: 'RFC3339 时间,例如 2025-11-15T20:00:00+08:00' },
106
- intervalSec: { type: 'number' },
107
- timeOfDay: { type: 'string', description: '例如 18:00 或 18:00:00' },
108
- tzOffsetMin: { type: 'number', description: '时区偏移分钟,例如北京为 480' },
109
- chatSessionId: { type: 'string' },
102
+ content: { type: 'string', minLength: 1, description: '提醒内容,例如 “开会”。' },
103
+ repeat: { type: 'string', enum: ['none', 'interval', 'daily'], default: 'none', description: '提醒类型:none 一次性、interval 按间隔、daily 每日。' },
104
+ delaySec: { type: 'number', minimum: 1, description: '一次性提醒相对延迟秒数,例如 300 表示5分钟后触发。与triggerAt二选一。' },
105
+ triggerAt: { type: 'string', description: '一次性提醒绝对时间,RFC3339 格式,例如 2025-11-21T08:00:00+08:00' },
106
+ intervalSec: { type: 'number', minimum: 1, description: '按间隔循环的间隔秒数,例如 3600 表示每小时提醒一次。仅repeat=interval时必需。' },
107
+ timeOfDay: { type: 'string', pattern: '^\\d{1,2}:\\d{2}(:\\d{2})?$', description: '一天中的时间,格式 HH:mm 或 HH:mm:ss,例如 08:00 或 08:00:00。repeat=daily时必需;repeat=none且未提供triggerAt/delaySec时用于推算。' },
108
+ tzOffsetMin: { type: 'number', description: '时区偏移分钟,例如北京为 480;不提供时默认使用本机时区。' },
109
+ chatSessionId: { type: 'string', minLength: 1, description: '设备会话标识,由宿主环境传入。' },
110
110
  },
111
111
  required: ['content', 'repeat'],
112
112
  additionalProperties: false,
113
+ oneOf: [
114
+ {
115
+ properties: { repeat: { const: 'none' } },
116
+ anyOf: [
117
+ { required: ['triggerAt'] },
118
+ { required: ['delaySec'] },
119
+ { required: ['timeOfDay'] },
120
+ ],
121
+ },
122
+ {
123
+ properties: { repeat: { const: 'interval' } },
124
+ required: ['intervalSec'],
125
+ },
126
+ {
127
+ properties: { repeat: { const: 'daily' } },
128
+ required: ['timeOfDay'],
129
+ },
130
+ ],
131
+ examples: [
132
+ { content: '开会', repeat: 'none', triggerAt: '2025-11-21T08:00:00+08:00' },
133
+ { content: '喝水', repeat: 'none', delaySec: 300 },
134
+ { content: '站立休息', repeat: 'interval', intervalSec: 1800 },
135
+ { content: '打卡', repeat: 'daily', timeOfDay: '09:00', tzOffsetMin: 480 },
136
+ ],
113
137
  },
114
138
  },
115
139
  {
@@ -117,7 +141,7 @@ class ReminderServer {
117
141
  description: '获取设备的提醒列表(仅未触发的 scheduled)。',
118
142
  inputSchema: {
119
143
  type: 'object',
120
- properties: { chatSessionId: { type: 'string' } },
144
+ properties: { chatSessionId: { type: 'string', minLength: 1, description: '设备会话标识。' } },
121
145
  required: [],
122
146
  additionalProperties: false,
123
147
  },
@@ -127,7 +151,7 @@ class ReminderServer {
127
151
  description: '取消指定提醒。',
128
152
  inputSchema: {
129
153
  type: 'object',
130
- properties: { id: { type: 'string' }, chatSessionId: { type: 'string' } },
154
+ properties: { id: { type: 'string', minLength: 1, description: '提醒ID。' }, chatSessionId: { type: 'string', minLength: 1, description: '设备会话标识。' } },
131
155
  required: ['id'],
132
156
  additionalProperties: false,
133
157
  },
@@ -169,6 +193,73 @@ class ReminderServer {
169
193
  timeOfDay: args.timeOfDay,
170
194
  tzOffsetMin: args.tzOffsetMin,
171
195
  };
196
+ const needSingleTrigger = params.repeat === 'none';
197
+ const hasTrigger = typeof params.triggerAt === 'string' && params.triggerAt.trim().length > 0;
198
+ const hasDelay = typeof params.delaySec === 'number' && Number.isFinite(params.delaySec);
199
+ const sendOffsetMin = typeof params.tzOffsetMin === 'number' ? params.tzOffsetMin : -new Date().getTimezoneOffset();
200
+ params.tzOffsetMin = sendOffsetMin;
201
+ if (needSingleTrigger && !hasTrigger && !hasDelay) {
202
+ if (typeof params.timeOfDay === 'string' && params.timeOfDay.trim()) {
203
+ const m = params.timeOfDay.trim().match(/^\s*(\d{1,2}):(\d{2})(?::(\d{2}))?\s*$/);
204
+ if (!m) {
205
+ return { content: [{ type: 'text', text: '设置失败:timeOfDay格式应为HH:mm或HH:mm:ss' }], isError: true };
206
+ }
207
+ const hh = parseInt(m[1], 10);
208
+ const mi = parseInt(m[2], 10);
209
+ const ss = m[3] ? parseInt(m[3], 10) : 0;
210
+ if (hh < 0 || hh > 23 || mi < 0 || mi > 59 || ss < 0 || ss > 59) {
211
+ return { content: [{ type: 'text', text: '设置失败:timeOfDay格式应为HH:mm或HH:mm:ss' }], isError: true };
212
+ }
213
+ const offsetMin = typeof params.tzOffsetMin === 'number' ? params.tzOffsetMin : -new Date().getTimezoneOffset();
214
+ const nowUtc = Date.now();
215
+ const zoneNowMs = nowUtc + offsetMin * 60000;
216
+ const zoneNow = new Date(zoneNowMs);
217
+ let y = zoneNow.getUTCFullYear();
218
+ let mth = zoneNow.getUTCMonth();
219
+ let d = zoneNow.getUTCDate();
220
+ let targetUtcMs = Date.UTC(y, mth, d, hh, mi, ss) - offsetMin * 60000;
221
+ if (targetUtcMs <= nowUtc) {
222
+ const tomorrowZone = new Date(zoneNowMs + 86400000);
223
+ y = tomorrowZone.getUTCFullYear();
224
+ mth = tomorrowZone.getUTCMonth();
225
+ d = tomorrowZone.getUTCDate();
226
+ targetUtcMs = Date.UTC(y, mth, d, hh, mi, ss) - offsetMin * 60000;
227
+ }
228
+ const targetZone = new Date(targetUtcMs + offsetMin * 60000);
229
+ const pad2 = (n) => String(n).padStart(2, '0');
230
+ const yStr = String(targetZone.getUTCFullYear());
231
+ const mStr = pad2(targetZone.getUTCMonth() + 1);
232
+ const dStr = pad2(targetZone.getUTCDate());
233
+ const sign = offsetMin >= 0 ? '+' : '-';
234
+ const abs = Math.abs(offsetMin);
235
+ const oh = pad2(Math.floor(abs / 60));
236
+ const om = pad2(abs % 60);
237
+ params.triggerAt = `${yStr}-${mStr}-${dStr}T${pad2(hh)}:${pad2(mi)}:${pad2(ss)}${sign}${oh}:${om}`;
238
+ console.error(`自动计算triggerAt: ${params.triggerAt}`);
239
+ }
240
+ else {
241
+ return { content: [{ type: 'text', text: '设置失败:必须提供triggerAt或delaySec,或提供timeOfDay用于推算' }], isError: true };
242
+ }
243
+ }
244
+ if (needSingleTrigger && hasDelay && !hasTrigger) {
245
+ const offsetMin = typeof params.tzOffsetMin === 'number' ? params.tzOffsetMin : -new Date().getTimezoneOffset();
246
+ const pad2 = (n) => String(n).padStart(2, '0');
247
+ const nowUtc = Date.now();
248
+ const targetUtcMs = nowUtc + params.delaySec * 1000;
249
+ const targetZone = new Date(targetUtcMs + offsetMin * 60000);
250
+ const yStr = String(targetZone.getUTCFullYear());
251
+ const mStr = pad2(targetZone.getUTCMonth() + 1);
252
+ const dStr = pad2(targetZone.getUTCDate());
253
+ const hhStr = pad2(targetZone.getUTCMinutes() >= 0 ? targetZone.getUTCHours() : targetZone.getUTCHours());
254
+ const miStr = pad2(targetZone.getUTCMinutes());
255
+ const ssStr = pad2(targetZone.getUTCSeconds());
256
+ const sign = offsetMin >= 0 ? '+' : '-';
257
+ const abs = Math.abs(offsetMin);
258
+ const oh = pad2(Math.floor(abs / 60));
259
+ const om = pad2(abs % 60);
260
+ params.triggerAt = `${yStr}-${mStr}-${dStr}T${hhStr}:${miStr}:${ssStr}${sign}${oh}:${om}`;
261
+ console.error(`delaySec计算triggerAt: ${params.triggerAt}`);
262
+ }
172
263
  const resp = await postJson('/reminder/set', params, chatSessionId);
173
264
  if (resp.code !== 0) {
174
265
  return { content: [{ type: 'text', text: `设置失败:${resp.msg}` }], isError: true };
@@ -212,8 +303,45 @@ class ReminderServer {
212
303
  if (resp.code !== 0) {
213
304
  return { content: [{ type: 'text', text: `获取失败:${resp.msg}` }], isError: true };
214
305
  }
306
+ const offsetMin = typeof args?.tzOffsetMin === 'number' ? args.tzOffsetMin : -new Date().getTimezoneOffset();
307
+ const formatOffset = (min) => {
308
+ const sign = min >= 0 ? '+' : '-';
309
+ const abs = Math.abs(min);
310
+ const hh = String(Math.floor(abs / 60)).padStart(2, '0');
311
+ const mm = String(abs % 60).padStart(2, '0');
312
+ return `${sign}${hh}:${mm}`;
313
+ };
314
+ const toLocal = (iso) => {
315
+ if (!iso)
316
+ return iso;
317
+ const d = new Date(iso);
318
+ if (isNaN(d.getTime()))
319
+ return iso;
320
+ const y = d.getFullYear();
321
+ const m = String(d.getMonth() + 1).padStart(2, '0');
322
+ const day = String(d.getDate()).padStart(2, '0');
323
+ const hh = String(d.getHours()).padStart(2, '0');
324
+ const mi = String(d.getMinutes()).padStart(2, '0');
325
+ const ss = String(d.getSeconds()).padStart(2, '0');
326
+ return `${y}-${m}-${day}T${hh}:${mi}:${ss}${formatOffset(offsetMin)}`;
327
+ };
328
+ const raw = resp.data?.list ?? [];
329
+ const mapped = Array.isArray(raw)
330
+ ? raw.map((it) => {
331
+ if (it && typeof it === 'object') {
332
+ const anyIt = it;
333
+ if (anyIt.triggerAt)
334
+ anyIt.triggerAtLocal = toLocal(anyIt.triggerAt);
335
+ if (anyIt.nextAt)
336
+ anyIt.nextAtLocal = toLocal(anyIt.nextAt);
337
+ if (anyIt.scheduledAt)
338
+ anyIt.scheduledAtLocal = toLocal(anyIt.scheduledAt);
339
+ }
340
+ return it;
341
+ })
342
+ : raw;
215
343
  return {
216
- content: [{ type: 'text', text: JSON.stringify(resp.data?.list ?? [], null, 2) }],
344
+ content: [{ type: 'text', text: JSON.stringify(mapped, null, 2) }],
217
345
  isError: false,
218
346
  };
219
347
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpcn/mcp-notification",
3
- "version": "1.1.3",
3
+ "version": "1.1.6",
4
4
  "description": "系统通知MCP服务器",
5
5
  "packageManager": "yarn@1.22.22",
6
6
  "main": "dist/index.js",
@@ -36,6 +36,6 @@
36
36
  "typescript": "^5.7.2"
37
37
  },
38
38
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.10.0"
39
+ "@modelcontextprotocol/sdk": "^1.0.4"
40
40
  }
41
41
  }