openclaw-server 0.1.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/package.json +29 -0
- package/packs/default/faq.yaml +8 -0
- package/packs/default/intents.yaml +19 -0
- package/packs/default/pack.yaml +12 -0
- package/packs/default/policies.yaml +1 -0
- package/packs/default/scenarios.yaml +1 -0
- package/packs/default/synonyms.yaml +1 -0
- package/packs/default/templates.yaml +16 -0
- package/packs/default/tools.yaml +1 -0
- package/readme.md +1219 -0
- package/src/auth.ts +24 -0
- package/src/better-sqlite3.d.ts +17 -0
- package/src/config.ts +63 -0
- package/src/core/matcher.ts +214 -0
- package/src/core/normalizer.test.ts +37 -0
- package/src/core/normalizer.ts +183 -0
- package/src/core/pack-loader.ts +97 -0
- package/src/core/reply-engine.test.ts +76 -0
- package/src/core/reply-engine.ts +256 -0
- package/src/core/request-adapter.ts +65 -0
- package/src/core/session-store.ts +48 -0
- package/src/core/stream-renderer.ts +237 -0
- package/src/core/tool-engine.ts +60 -0
- package/src/debug-log.ts +211 -0
- package/src/index.ts +23 -0
- package/src/openai.ts +79 -0
- package/src/response-api.ts +107 -0
- package/src/routes/admin.ts +32 -0
- package/src/routes/chat-completions.ts +173 -0
- package/src/routes/health.ts +7 -0
- package/src/routes/models.ts +21 -0
- package/src/routes/request-validation.ts +33 -0
- package/src/routes/responses.ts +182 -0
- package/src/routes/tasks.ts +138 -0
- package/src/runtime-stats.ts +80 -0
- package/src/server.test.ts +776 -0
- package/src/server.ts +108 -0
- package/src/tasks/chat-integration.ts +70 -0
- package/src/tasks/service.ts +320 -0
- package/src/tasks/store.test.ts +183 -0
- package/src/tasks/store.ts +602 -0
- package/src/tasks/time-parser.test.ts +94 -0
- package/src/tasks/time-parser.ts +610 -0
- package/src/tasks/timezone.ts +171 -0
- package/src/tasks/types.ts +128 -0
- package/src/types.ts +202 -0
- package/src/weather/chat-integration.ts +56 -0
- package/src/weather/location-catalog.ts +166 -0
- package/src/weather/open-meteo-provider.ts +221 -0
- package/src/weather/parser.test.ts +23 -0
- package/src/weather/parser.ts +102 -0
- package/src/weather/service.test.ts +54 -0
- package/src/weather/service.ts +188 -0
- package/src/weather/types.ts +56 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDaysInTimeZone,
|
|
3
|
+
getTimeZoneDateParts,
|
|
4
|
+
startOfDayInTimeZone,
|
|
5
|
+
zonedDateTimeToDate,
|
|
6
|
+
} from "./timezone.js";
|
|
7
|
+
import type { ParsedTaskAction, ParsedTaskDraft, ReminderPreference, TaskScope } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const weekdayMap: Record<string, number> = {
|
|
10
|
+
一: 1,
|
|
11
|
+
二: 2,
|
|
12
|
+
三: 3,
|
|
13
|
+
四: 4,
|
|
14
|
+
五: 5,
|
|
15
|
+
六: 6,
|
|
16
|
+
日: 0,
|
|
17
|
+
天: 0,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const chineseDigitMap: Record<string, number> = {
|
|
21
|
+
零: 0,
|
|
22
|
+
〇: 0,
|
|
23
|
+
一: 1,
|
|
24
|
+
二: 2,
|
|
25
|
+
两: 2,
|
|
26
|
+
三: 3,
|
|
27
|
+
四: 4,
|
|
28
|
+
五: 5,
|
|
29
|
+
六: 6,
|
|
30
|
+
七: 7,
|
|
31
|
+
八: 8,
|
|
32
|
+
九: 9,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const chineseUnitMap: Record<string, number> = {
|
|
36
|
+
十: 10,
|
|
37
|
+
百: 100,
|
|
38
|
+
千: 1000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const numberTokenSource = String.raw`(?:\d{1,4}|[零〇一二两三四五六七八九十百千]+)`;
|
|
42
|
+
const clockPattern = new RegExp(
|
|
43
|
+
String.raw`(?:(凌晨|早上|早晨|上午|中午|下午|晚上|今晚|傍晚)\s*)?(${numberTokenSource})(?:[::](${numberTokenSource})|点(半|一刻|三刻|(?:${numberTokenSource})分?)?)`,
|
|
44
|
+
"u",
|
|
45
|
+
);
|
|
46
|
+
const monthDayPattern = new RegExp(String.raw`(${numberTokenSource})月(${numberTokenSource})(?:日|号)`, "u");
|
|
47
|
+
const fullDatePattern = new RegExp(
|
|
48
|
+
String.raw`(\d{4})[-/年](${numberTokenSource})[-/月](${numberTokenSource})(?:日|号)?`,
|
|
49
|
+
"u",
|
|
50
|
+
);
|
|
51
|
+
const minuteAfterPattern = new RegExp(String.raw`(${numberTokenSource})\s*(分钟|分)后`, "u");
|
|
52
|
+
const hourAfterPattern = new RegExp(String.raw`(${numberTokenSource})\s*(小时|时)后`, "u");
|
|
53
|
+
const hourAndHalfAfterPattern = new RegExp(
|
|
54
|
+
String.raw`(${numberTokenSource})\s*(?:(?:个)?半小时|(?:个)?(?:小时|时)半)后`,
|
|
55
|
+
"u",
|
|
56
|
+
);
|
|
57
|
+
const reminderMinutePattern = new RegExp(
|
|
58
|
+
String.raw`(提前\s*(${numberTokenSource})\s*(分钟|分)提醒)`,
|
|
59
|
+
"u",
|
|
60
|
+
);
|
|
61
|
+
const reminderHourPattern = new RegExp(String.raw`(提前\s*(${numberTokenSource})\s*(小时|时)提醒)`, "u");
|
|
62
|
+
const reminderHourAndHalfPattern = new RegExp(
|
|
63
|
+
String.raw`(提前\s*(${numberTokenSource})\s*(?:(?:个)?半小时|(?:个)?(?:小时|时)半)提醒)`,
|
|
64
|
+
"u",
|
|
65
|
+
);
|
|
66
|
+
const snoozeMinutePattern = new RegExp(String.raw`^延后\s*(${numberTokenSource})\s*(分钟|分)$`, "u");
|
|
67
|
+
const snoozeHourPattern = new RegExp(String.raw`^延后\s*(${numberTokenSource})\s*(小时|时)$`, "u");
|
|
68
|
+
const snoozeHourAndHalfPattern = new RegExp(
|
|
69
|
+
String.raw`^延后\s*(${numberTokenSource})\s*(?:(?:个)?半小时|(?:个)?(?:小时|时)半)$`,
|
|
70
|
+
"u",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
function addMinutes(date: Date, minutes: number): Date {
|
|
74
|
+
return new Date(date.getTime() + minutes * 60_000);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stripText(text: string): string {
|
|
78
|
+
return text.replace(/\s+/g, " ").trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseNumberToken(token: string): number | undefined {
|
|
82
|
+
const normalized = token.normalize("NFKC").trim();
|
|
83
|
+
if (!normalized) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (/^\d+$/u.test(normalized)) {
|
|
87
|
+
return Number(normalized);
|
|
88
|
+
}
|
|
89
|
+
if (!/^[零〇一二两三四五六七八九十百千]+$/u.test(normalized)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!/[十百千]/u.test(normalized)) {
|
|
94
|
+
const digits = [...normalized].map((char) => chineseDigitMap[char]);
|
|
95
|
+
if (digits.some((value) => value === undefined)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return Number(digits.join(""));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let total = 0;
|
|
102
|
+
let current = 0;
|
|
103
|
+
for (const char of normalized) {
|
|
104
|
+
const digit = chineseDigitMap[char];
|
|
105
|
+
if (digit !== undefined) {
|
|
106
|
+
current = digit;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const unit = chineseUnitMap[char];
|
|
111
|
+
if (!unit) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
total += (current || 1) * unit;
|
|
115
|
+
current = 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return total + current;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseClockMinuteToken(token?: string): number | undefined {
|
|
122
|
+
if (!token) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (token === "半") {
|
|
126
|
+
return 30;
|
|
127
|
+
}
|
|
128
|
+
if (token === "一刻") {
|
|
129
|
+
return 15;
|
|
130
|
+
}
|
|
131
|
+
if (token === "三刻") {
|
|
132
|
+
return 45;
|
|
133
|
+
}
|
|
134
|
+
return parseNumberToken(token.replace(/分$/u, ""));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removeFragments(text: string, fragments: string[]): string {
|
|
138
|
+
let output = text;
|
|
139
|
+
for (const fragment of fragments) {
|
|
140
|
+
if (!fragment) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
output = output.replace(fragment, " ");
|
|
144
|
+
}
|
|
145
|
+
return stripText(output);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cleanupTitle(text: string): string {
|
|
149
|
+
return stripText(
|
|
150
|
+
text
|
|
151
|
+
.replace(/提醒我一下|提醒我|提醒一下|提醒/g, " ")
|
|
152
|
+
.replace(/需要提醒吗|准时提醒|到点提醒(?:就行|即可|就好|就可以)?|不用提醒(?:了)?|不需要提醒|别提醒(?:了)?/g, " ")
|
|
153
|
+
.replace(/[,。,!!??]/g, " "),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseClock(text: string): { hour: number; minute: number; matchedText: string } | undefined {
|
|
158
|
+
const match = clockPattern.exec(text);
|
|
159
|
+
if (!match) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const rawPeriod = match[1] ?? "";
|
|
164
|
+
const period = rawPeriod === "今晚" || rawPeriod === "傍晚" ? "晚上" : rawPeriod === "早晨" ? "早上" : rawPeriod;
|
|
165
|
+
let hour = parseNumberToken(match[2]);
|
|
166
|
+
const minute = match[3] ? parseNumberToken(match[3]) : parseClockMinuteToken(match[4]);
|
|
167
|
+
|
|
168
|
+
if (hour === undefined || minute === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (["下午", "晚上"].includes(period) && hour < 12) {
|
|
173
|
+
hour += 12;
|
|
174
|
+
}
|
|
175
|
+
if (period === "中午" && hour < 11) {
|
|
176
|
+
hour += 12;
|
|
177
|
+
}
|
|
178
|
+
if ((period === "凌晨" || period === "晚上") && hour === 12) {
|
|
179
|
+
hour = 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (hour > 23 || minute > 59) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
hour,
|
|
188
|
+
minute,
|
|
189
|
+
matchedText: match[0],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function shiftCalendarDate(
|
|
194
|
+
parts: Pick<ReturnType<typeof getTimeZoneDateParts>, "year" | "month" | "day">,
|
|
195
|
+
days: number,
|
|
196
|
+
) {
|
|
197
|
+
const shifted = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + days));
|
|
198
|
+
return {
|
|
199
|
+
year: shifted.getUTCFullYear(),
|
|
200
|
+
month: shifted.getUTCMonth() + 1,
|
|
201
|
+
day: shifted.getUTCDate(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function weekdayFromDateParts(parts: Pick<ReturnType<typeof getTimeZoneDateParts>, "year" | "month" | "day">): number {
|
|
206
|
+
return new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseDay(
|
|
210
|
+
text: string,
|
|
211
|
+
now: Date,
|
|
212
|
+
timeZone: string,
|
|
213
|
+
): { baseDate: Date; matchedText: string; rollForwardIfPast?: boolean } | undefined {
|
|
214
|
+
const todayStart = startOfDayInTimeZone(now, timeZone);
|
|
215
|
+
const todayParts = getTimeZoneDateParts(todayStart, timeZone);
|
|
216
|
+
const relativeMap: Array<{ pattern: RegExp; days: number }> = [
|
|
217
|
+
{ pattern: /大后天/u, days: 3 },
|
|
218
|
+
{ pattern: /后天/u, days: 2 },
|
|
219
|
+
{ pattern: /明天/u, days: 1 },
|
|
220
|
+
{ pattern: /今天/u, days: 0 },
|
|
221
|
+
];
|
|
222
|
+
for (const item of relativeMap) {
|
|
223
|
+
const match = item.pattern.exec(text);
|
|
224
|
+
if (match) {
|
|
225
|
+
return {
|
|
226
|
+
baseDate: addDaysInTimeZone(todayStart, item.days, timeZone),
|
|
227
|
+
matchedText: match[0],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const nextWeekMatch = /下(?:周|星期|礼拜)([一二三四五六日天])/u.exec(text);
|
|
233
|
+
if (nextWeekMatch) {
|
|
234
|
+
const target = weekdayMap[nextWeekMatch[1]];
|
|
235
|
+
const base = shiftCalendarDate(todayParts, 7);
|
|
236
|
+
const delta = (target - weekdayFromDateParts(base) + 7) % 7;
|
|
237
|
+
const candidate = shiftCalendarDate(base, delta);
|
|
238
|
+
return {
|
|
239
|
+
baseDate: zonedDateTimeToDate(candidate, timeZone),
|
|
240
|
+
matchedText: nextWeekMatch[0],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const weekdayMatch = /(?:本周|这周|本星期|这星期|本礼拜|这礼拜|周|星期|礼拜)([一二三四五六日天])/u.exec(text);
|
|
245
|
+
if (weekdayMatch) {
|
|
246
|
+
const target = weekdayMap[weekdayMatch[1]];
|
|
247
|
+
const delta = (target - weekdayFromDateParts(todayParts) + 7) % 7;
|
|
248
|
+
const candidate = shiftCalendarDate(todayParts, delta);
|
|
249
|
+
return {
|
|
250
|
+
baseDate: zonedDateTimeToDate(candidate, timeZone),
|
|
251
|
+
matchedText: weekdayMatch[0],
|
|
252
|
+
rollForwardIfPast: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const monthDayMatch = monthDayPattern.exec(text);
|
|
257
|
+
if (monthDayMatch) {
|
|
258
|
+
const month = parseNumberToken(monthDayMatch[1]);
|
|
259
|
+
const day = parseNumberToken(monthDayMatch[2]);
|
|
260
|
+
if (month === undefined || day === undefined) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
let year = todayParts.year;
|
|
264
|
+
if (month < todayParts.month || (month === todayParts.month && day < todayParts.day)) {
|
|
265
|
+
year += 1;
|
|
266
|
+
}
|
|
267
|
+
const candidate = zonedDateTimeToDate({ year, month, day }, timeZone);
|
|
268
|
+
const candidateParts = getTimeZoneDateParts(candidate, timeZone);
|
|
269
|
+
if (candidateParts.month !== month || candidateParts.day !== day) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
baseDate: candidate,
|
|
274
|
+
matchedText: monthDayMatch[0],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fullDateMatch = fullDatePattern.exec(text);
|
|
279
|
+
if (fullDateMatch) {
|
|
280
|
+
const year = Number(fullDateMatch[1]);
|
|
281
|
+
const month = parseNumberToken(fullDateMatch[2]);
|
|
282
|
+
const day = parseNumberToken(fullDateMatch[3]);
|
|
283
|
+
if (month === undefined || day === undefined) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const candidate = zonedDateTimeToDate({ year, month, day }, timeZone);
|
|
287
|
+
const candidateParts = getTimeZoneDateParts(candidate, timeZone);
|
|
288
|
+
if (candidateParts.year !== year || candidateParts.month !== month || candidateParts.day !== day) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
baseDate: candidate,
|
|
293
|
+
matchedText: fullDateMatch[0],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasDateHint(text: string): boolean {
|
|
301
|
+
return (
|
|
302
|
+
/(今天|明天|后天|大后天|下(?:周|星期|礼拜)[一二三四五六日天]|(?:本周|这周|本星期|这星期|本礼拜|这礼拜|周|星期|礼拜)[一二三四五六日天])/u.test(
|
|
303
|
+
text,
|
|
304
|
+
) ||
|
|
305
|
+
monthDayPattern.test(text) ||
|
|
306
|
+
fullDatePattern.test(text) ||
|
|
307
|
+
clockPattern.test(text) ||
|
|
308
|
+
/半(?:个)?小时后|一刻钟后|三刻钟后/u.test(text) ||
|
|
309
|
+
hourAndHalfAfterPattern.test(text) ||
|
|
310
|
+
minuteAfterPattern.test(text) ||
|
|
311
|
+
hourAfterPattern.test(text)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseAbsoluteDueAt(
|
|
316
|
+
text: string,
|
|
317
|
+
now: Date,
|
|
318
|
+
timeZone: string,
|
|
319
|
+
): { dueAt: Date; fragments: string[] } | undefined {
|
|
320
|
+
const clock = parseClock(text);
|
|
321
|
+
const day = parseDay(text, now, timeZone);
|
|
322
|
+
if (!clock && !day) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
if (!clock) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const baseDate = day?.baseDate ?? startOfDayInTimeZone(now, timeZone);
|
|
330
|
+
const baseParts = getTimeZoneDateParts(baseDate, timeZone);
|
|
331
|
+
let dueAt = zonedDateTimeToDate(
|
|
332
|
+
{
|
|
333
|
+
year: baseParts.year,
|
|
334
|
+
month: baseParts.month,
|
|
335
|
+
day: baseParts.day,
|
|
336
|
+
hour: clock.hour,
|
|
337
|
+
minute: clock.minute,
|
|
338
|
+
},
|
|
339
|
+
timeZone,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (!day && dueAt.getTime() <= now.getTime()) {
|
|
343
|
+
dueAt = addDaysInTimeZone(dueAt, 1, timeZone);
|
|
344
|
+
}
|
|
345
|
+
if (day?.rollForwardIfPast && dueAt.getTime() <= now.getTime()) {
|
|
346
|
+
dueAt = addDaysInTimeZone(dueAt, 7, timeZone);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
dueAt,
|
|
351
|
+
fragments: [day?.matchedText ?? "", clock.matchedText].filter(Boolean),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseRelativeDueAt(text: string, now: Date): { dueAt: Date; fragments: string[] } | undefined {
|
|
356
|
+
const halfHourMatch = /半(?:个)?小时后/u.exec(text);
|
|
357
|
+
if (halfHourMatch) {
|
|
358
|
+
return {
|
|
359
|
+
dueAt: addMinutes(now, 30),
|
|
360
|
+
fragments: [halfHourMatch[0]],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const quarterMatch = /(一刻钟|三刻钟)后/u.exec(text);
|
|
365
|
+
if (quarterMatch) {
|
|
366
|
+
return {
|
|
367
|
+
dueAt: addMinutes(now, quarterMatch[1] === "一刻钟" ? 15 : 45),
|
|
368
|
+
fragments: [quarterMatch[0]],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const hourAndHalfMatch = hourAndHalfAfterPattern.exec(text);
|
|
373
|
+
if (hourAndHalfMatch) {
|
|
374
|
+
const hours = parseNumberToken(hourAndHalfMatch[1]);
|
|
375
|
+
if (hours === undefined) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
dueAt: addMinutes(now, hours * 60 + 30),
|
|
380
|
+
fragments: [hourAndHalfMatch[0]],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const minuteMatch = minuteAfterPattern.exec(text);
|
|
385
|
+
if (minuteMatch) {
|
|
386
|
+
const minutes = parseNumberToken(minuteMatch[1]);
|
|
387
|
+
if (minutes === undefined) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
dueAt: addMinutes(now, minutes),
|
|
392
|
+
fragments: [minuteMatch[0]],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const hourMatch = hourAfterPattern.exec(text);
|
|
397
|
+
if (hourMatch) {
|
|
398
|
+
const hours = parseNumberToken(hourMatch[1]);
|
|
399
|
+
if (hours === undefined) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
dueAt: addMinutes(now, hours * 60),
|
|
404
|
+
fragments: [hourMatch[0]],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function parseReminderPreference(text: string): ReminderPreference | undefined {
|
|
412
|
+
const halfHourMatch = /(提前\s*半(?:个)?小时提醒)/u.exec(text);
|
|
413
|
+
if (halfHourMatch) {
|
|
414
|
+
return {
|
|
415
|
+
offsetMinutes: 30,
|
|
416
|
+
matchedText: halfHourMatch[1],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const quarterMatch = /(提前\s*(一刻钟|三刻钟)提醒)/u.exec(text);
|
|
421
|
+
if (quarterMatch) {
|
|
422
|
+
return {
|
|
423
|
+
offsetMinutes: quarterMatch[2] === "一刻钟" ? 15 : 45,
|
|
424
|
+
matchedText: quarterMatch[1],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const hourAndHalfMatch = reminderHourAndHalfPattern.exec(text);
|
|
429
|
+
if (hourAndHalfMatch) {
|
|
430
|
+
const hours = parseNumberToken(hourAndHalfMatch[2]);
|
|
431
|
+
if (hours === undefined) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
offsetMinutes: hours * 60 + 30,
|
|
436
|
+
matchedText: hourAndHalfMatch[1],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const minuteMatch = reminderMinutePattern.exec(text);
|
|
441
|
+
if (minuteMatch) {
|
|
442
|
+
const minutes = parseNumberToken(minuteMatch[2]);
|
|
443
|
+
if (minutes === undefined) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
offsetMinutes: minutes,
|
|
448
|
+
matchedText: minuteMatch[1],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const hourMatch = reminderHourPattern.exec(text);
|
|
453
|
+
if (hourMatch) {
|
|
454
|
+
const hours = parseNumberToken(hourMatch[2]);
|
|
455
|
+
if (hours === undefined) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
offsetMinutes: hours * 60,
|
|
460
|
+
matchedText: hourMatch[1],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const immediateMatch = /(到点提醒(?:就行|即可|就好|就可以)?|准时提醒)/u.exec(text);
|
|
465
|
+
if (immediateMatch) {
|
|
466
|
+
return {
|
|
467
|
+
offsetMinutes: 0,
|
|
468
|
+
matchedText: immediateMatch[1],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const noneMatch = /(不用提醒(?:了)?|不需要提醒|别提醒(?:了)?)/u.exec(text);
|
|
473
|
+
if (noneMatch) {
|
|
474
|
+
return {
|
|
475
|
+
offsetMinutes: null,
|
|
476
|
+
matchedText: noneMatch[1],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function parseTaskAction(text: string): ParsedTaskAction | undefined {
|
|
484
|
+
const normalized = stripText(text);
|
|
485
|
+
if (/^(完成|已完成)$/u.test(normalized)) {
|
|
486
|
+
return { kind: "done" };
|
|
487
|
+
}
|
|
488
|
+
if (/^(取消|取消任务)$/u.test(normalized)) {
|
|
489
|
+
return { kind: "cancel" };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const halfHourMatch = /^延后\s*半(?:个)?小时$/u.exec(normalized);
|
|
493
|
+
if (halfHourMatch) {
|
|
494
|
+
return { kind: "snooze", minutes: 30 };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const quarterMatch = /^延后\s*(一刻钟|三刻钟)$/u.exec(normalized);
|
|
498
|
+
if (quarterMatch) {
|
|
499
|
+
return { kind: "snooze", minutes: quarterMatch[1] === "一刻钟" ? 15 : 45 };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const hourAndHalfMatch = snoozeHourAndHalfPattern.exec(normalized);
|
|
503
|
+
if (hourAndHalfMatch) {
|
|
504
|
+
const hours = parseNumberToken(hourAndHalfMatch[1]);
|
|
505
|
+
if (hours === undefined) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
return { kind: "snooze", minutes: hours * 60 + 30 };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const minuteMatch = snoozeMinutePattern.exec(normalized);
|
|
512
|
+
if (minuteMatch) {
|
|
513
|
+
const minutes = parseNumberToken(minuteMatch[1]);
|
|
514
|
+
if (minutes === undefined) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
return { kind: "snooze", minutes };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const hourMatch = snoozeHourPattern.exec(normalized);
|
|
521
|
+
if (hourMatch) {
|
|
522
|
+
const hours = parseNumberToken(hourMatch[1]);
|
|
523
|
+
if (hours === undefined) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
return { kind: "snooze", minutes: hours * 60 };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function parseTaskQuery(text: string): TaskScope | undefined {
|
|
533
|
+
const normalized = stripText(text);
|
|
534
|
+
const compact = normalized.replace(/[,。!??,.!]/gu, "");
|
|
535
|
+
if (/(今天.*(任务|待办|安排|有什么)|今日.*(任务|待办|安排))/u.test(normalized)) {
|
|
536
|
+
return "today";
|
|
537
|
+
}
|
|
538
|
+
if (/(明天.*(任务|待办|安排|有什么))/u.test(normalized)) {
|
|
539
|
+
return "tomorrow";
|
|
540
|
+
}
|
|
541
|
+
if (/(本周.*(任务|待办|安排|计划)|这周.*(任务|待办|安排))/u.test(normalized)) {
|
|
542
|
+
return "week";
|
|
543
|
+
}
|
|
544
|
+
if (/(逾期.*(任务|待办)|过期.*(任务|待办))/u.test(normalized)) {
|
|
545
|
+
return "overdue";
|
|
546
|
+
}
|
|
547
|
+
if (/(全部.*任务|所有.*任务)/u.test(normalized)) {
|
|
548
|
+
return "all";
|
|
549
|
+
}
|
|
550
|
+
if (
|
|
551
|
+
/^(?:我(?:的)?(?:任务|待办|安排)|我(?:现在)?有哪(?:些)?(?:任务|待办|安排)|我(?:现在)?有什么(?:任务|待办|安排)|我(?:的)?(?:任务|待办|安排)有哪(?:些)?|(?:查看|看看|列出|显示)(?:一下)?我(?:的)?(?:任务|待办|安排)|(?:任务|待办|安排)列表|有哪些(?:任务|待办|安排)|有什么(?:任务|待办|安排))$/u.test(
|
|
552
|
+
compact,
|
|
553
|
+
)
|
|
554
|
+
) {
|
|
555
|
+
return "all";
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function isConfirmation(text: string): boolean {
|
|
561
|
+
return /^(确认|是|好的|好|可以|创建吧)$/u.test(stripText(text));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export { formatLocalDateTime } from "./timezone.js";
|
|
565
|
+
|
|
566
|
+
export function parseTaskDraft(text: string, now: Date, timeZone: string): ParsedTaskDraft | undefined {
|
|
567
|
+
const reminder = parseReminderPreference(text);
|
|
568
|
+
const withoutReminder = reminder ? text.replace(reminder.matchedText, " ") : text;
|
|
569
|
+
const relative = parseRelativeDueAt(withoutReminder, now);
|
|
570
|
+
const absolute = relative ? undefined : parseAbsoluteDueAt(withoutReminder, now, timeZone);
|
|
571
|
+
const parsed = relative ?? absolute;
|
|
572
|
+
|
|
573
|
+
if (!parsed) {
|
|
574
|
+
if (hasDateHint(withoutReminder)) {
|
|
575
|
+
const title = cleanupTitle(withoutReminder);
|
|
576
|
+
if (!title) {
|
|
577
|
+
return { kind: "missing_title" };
|
|
578
|
+
}
|
|
579
|
+
return { kind: "missing_time", title };
|
|
580
|
+
}
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const title = cleanupTitle(removeFragments(withoutReminder, parsed.fragments));
|
|
585
|
+
if (!title) {
|
|
586
|
+
return { kind: "missing_title" };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
kind: "ready",
|
|
591
|
+
title,
|
|
592
|
+
dueAt: parsed.dueAt,
|
|
593
|
+
reminderOffsetMinutes: reminder?.offsetMinutes,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function summarizeScope(scope: TaskScope): string {
|
|
598
|
+
switch (scope) {
|
|
599
|
+
case "today":
|
|
600
|
+
return "今天";
|
|
601
|
+
case "tomorrow":
|
|
602
|
+
return "明天";
|
|
603
|
+
case "week":
|
|
604
|
+
return "本周";
|
|
605
|
+
case "overdue":
|
|
606
|
+
return "逾期";
|
|
607
|
+
case "all":
|
|
608
|
+
return "全部";
|
|
609
|
+
}
|
|
610
|
+
}
|