openclaw-server 0.1.0 → 0.2.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 +319 -0
- package/package.json +1 -1
- package/packs/default/templates.yaml +6 -6
- package/src/bookmark-digest/chat-integration.ts +49 -0
- package/src/bookmark-digest/service.test.ts +92 -0
- package/src/bookmark-digest/service.ts +573 -0
- package/src/bookmark-digest/store.ts +349 -0
- package/src/bookmark-digest/types.ts +62 -0
- package/src/bookmark-search/chat-integration.ts +56 -0
- package/src/bookmark-search/parser.test.ts +67 -0
- package/src/bookmark-search/parser.ts +235 -0
- package/src/bookmark-search/service.test.ts +330 -0
- package/src/bookmark-search/service.ts +660 -0
- package/src/bookmark-search/shuqianlan-provider.ts +334 -0
- package/src/bookmark-search/types.ts +78 -0
- package/src/config.ts +30 -2
- package/src/debug-log.ts +22 -18
- package/src/request-user.test.ts +29 -0
- package/src/request-user.ts +49 -0
- package/src/routes/admin.ts +53 -0
- package/src/routes/chat-completions.ts +42 -46
- package/src/routes/responses.ts +44 -47
- package/src/server.test.ts +336 -440
- package/src/server.ts +26 -18
- package/readme.md +0 -1219
- package/src/routes/tasks.ts +0 -138
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import type {
|
|
6
|
+
BookmarkDigestNotification,
|
|
7
|
+
BookmarkDigestNotificationPayload,
|
|
8
|
+
BookmarkDigestSubscription,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
type SubscriptionRow = {
|
|
12
|
+
user_id: string;
|
|
13
|
+
timezone: string;
|
|
14
|
+
schedule_hour: number;
|
|
15
|
+
schedule_minute: number;
|
|
16
|
+
status: BookmarkDigestSubscription["status"];
|
|
17
|
+
last_scheduled_slot: string | null;
|
|
18
|
+
last_digest_signatures_json: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type NotificationRow = {
|
|
24
|
+
id: string;
|
|
25
|
+
user_id: string;
|
|
26
|
+
slot_at: string;
|
|
27
|
+
message: string;
|
|
28
|
+
payload_json: string;
|
|
29
|
+
status: BookmarkDigestNotification["status"];
|
|
30
|
+
delivered_at: string | null;
|
|
31
|
+
last_attempt_at: string | null;
|
|
32
|
+
last_error: string | null;
|
|
33
|
+
created_at: string;
|
|
34
|
+
updated_at: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function toIsoString(date: Date): string {
|
|
38
|
+
return date.toISOString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseStringArray(value: string): string[] {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(value);
|
|
44
|
+
if (!Array.isArray(parsed)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parsePayload(value: string): BookmarkDigestNotificationPayload {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(value);
|
|
56
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
57
|
+
const payload = parsed as Partial<BookmarkDigestNotificationPayload>;
|
|
58
|
+
return {
|
|
59
|
+
type: "bookmark_digest",
|
|
60
|
+
userId: typeof payload.userId === "string" ? payload.userId : "",
|
|
61
|
+
slotAt: typeof payload.slotAt === "string" ? payload.slotAt : "",
|
|
62
|
+
hasNewArticles: Boolean(payload.hasNewArticles),
|
|
63
|
+
articles: Array.isArray(payload.articles) ? payload.articles : [],
|
|
64
|
+
browseUrl: typeof payload.browseUrl === "string" ? payload.browseUrl : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Fall back to an empty payload when the row is malformed.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
type: "bookmark_digest",
|
|
73
|
+
userId: "",
|
|
74
|
+
slotAt: "",
|
|
75
|
+
hasNewArticles: false,
|
|
76
|
+
articles: [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mapSubscription(row: SubscriptionRow): BookmarkDigestSubscription {
|
|
81
|
+
return {
|
|
82
|
+
userId: row.user_id,
|
|
83
|
+
timeZone: row.timezone,
|
|
84
|
+
scheduleHour: row.schedule_hour,
|
|
85
|
+
scheduleMinute: row.schedule_minute,
|
|
86
|
+
status: row.status,
|
|
87
|
+
lastScheduledSlot: row.last_scheduled_slot,
|
|
88
|
+
lastDigestSignatures: parseStringArray(row.last_digest_signatures_json),
|
|
89
|
+
createdAt: row.created_at,
|
|
90
|
+
updatedAt: row.updated_at,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mapNotification(row: NotificationRow): BookmarkDigestNotification {
|
|
95
|
+
return {
|
|
96
|
+
id: row.id,
|
|
97
|
+
userId: row.user_id,
|
|
98
|
+
slotAt: row.slot_at,
|
|
99
|
+
message: row.message,
|
|
100
|
+
payload: parsePayload(row.payload_json),
|
|
101
|
+
status: row.status,
|
|
102
|
+
deliveredAt: row.delivered_at,
|
|
103
|
+
lastAttemptAt: row.last_attempt_at,
|
|
104
|
+
lastError: row.last_error,
|
|
105
|
+
createdAt: row.created_at,
|
|
106
|
+
updatedAt: row.updated_at,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class BookmarkDigestStore {
|
|
111
|
+
private readonly db: Database;
|
|
112
|
+
|
|
113
|
+
constructor(dbPath: string) {
|
|
114
|
+
if (dbPath !== ":memory:") {
|
|
115
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
this.db = new Database(dbPath);
|
|
118
|
+
this.db.pragma("journal_mode = WAL");
|
|
119
|
+
this.db.pragma("foreign_keys = ON");
|
|
120
|
+
this.db.pragma("synchronous = NORMAL");
|
|
121
|
+
this.db.exec(`
|
|
122
|
+
CREATE TABLE IF NOT EXISTS bookmark_digest_subscriptions (
|
|
123
|
+
user_id TEXT PRIMARY KEY,
|
|
124
|
+
timezone TEXT NOT NULL,
|
|
125
|
+
schedule_hour INTEGER NOT NULL,
|
|
126
|
+
schedule_minute INTEGER NOT NULL,
|
|
127
|
+
status TEXT NOT NULL,
|
|
128
|
+
last_scheduled_slot TEXT,
|
|
129
|
+
last_digest_signatures_json TEXT NOT NULL,
|
|
130
|
+
created_at TEXT NOT NULL,
|
|
131
|
+
updated_at TEXT NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS bookmark_digest_notifications (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
user_id TEXT NOT NULL,
|
|
137
|
+
slot_at TEXT NOT NULL,
|
|
138
|
+
message TEXT NOT NULL,
|
|
139
|
+
payload_json TEXT NOT NULL,
|
|
140
|
+
status TEXT NOT NULL,
|
|
141
|
+
delivered_at TEXT,
|
|
142
|
+
last_attempt_at TEXT,
|
|
143
|
+
last_error TEXT,
|
|
144
|
+
created_at TEXT NOT NULL,
|
|
145
|
+
updated_at TEXT NOT NULL,
|
|
146
|
+
UNIQUE(user_id, slot_at)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_bookmark_digest_subscriptions_status
|
|
150
|
+
ON bookmark_digest_subscriptions(status);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_bookmark_digest_notifications_status_slot
|
|
152
|
+
ON bookmark_digest_notifications(status, slot_at);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_bookmark_digest_notifications_user_status
|
|
154
|
+
ON bookmark_digest_notifications(user_id, status);
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close(): void {
|
|
159
|
+
this.db.close();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSubscription(userId: string): BookmarkDigestSubscription | undefined {
|
|
163
|
+
const row = this.db
|
|
164
|
+
.prepare(`SELECT * FROM bookmark_digest_subscriptions WHERE user_id = ?`)
|
|
165
|
+
.get(userId) as SubscriptionRow | undefined;
|
|
166
|
+
return row ? mapSubscription(row) : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
listActiveSubscriptions(): BookmarkDigestSubscription[] {
|
|
170
|
+
const rows = this.db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT * FROM bookmark_digest_subscriptions
|
|
173
|
+
WHERE status = 'active'
|
|
174
|
+
ORDER BY created_at ASC`,
|
|
175
|
+
)
|
|
176
|
+
.all() as SubscriptionRow[];
|
|
177
|
+
return rows.map(mapSubscription);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
upsertSubscription(params: {
|
|
181
|
+
userId: string;
|
|
182
|
+
timeZone: string;
|
|
183
|
+
scheduleHour: number;
|
|
184
|
+
scheduleMinute: number;
|
|
185
|
+
now: Date;
|
|
186
|
+
}): BookmarkDigestSubscription {
|
|
187
|
+
const nowIso = toIsoString(params.now);
|
|
188
|
+
this.db
|
|
189
|
+
.prepare(
|
|
190
|
+
`INSERT INTO bookmark_digest_subscriptions (
|
|
191
|
+
user_id,
|
|
192
|
+
timezone,
|
|
193
|
+
schedule_hour,
|
|
194
|
+
schedule_minute,
|
|
195
|
+
status,
|
|
196
|
+
last_scheduled_slot,
|
|
197
|
+
last_digest_signatures_json,
|
|
198
|
+
created_at,
|
|
199
|
+
updated_at
|
|
200
|
+
) VALUES (?, ?, ?, ?, 'active', NULL, '[]', ?, ?)
|
|
201
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
202
|
+
timezone = excluded.timezone,
|
|
203
|
+
schedule_hour = excluded.schedule_hour,
|
|
204
|
+
schedule_minute = excluded.schedule_minute,
|
|
205
|
+
status = 'active',
|
|
206
|
+
updated_at = excluded.updated_at`,
|
|
207
|
+
)
|
|
208
|
+
.run(
|
|
209
|
+
params.userId,
|
|
210
|
+
params.timeZone,
|
|
211
|
+
params.scheduleHour,
|
|
212
|
+
params.scheduleMinute,
|
|
213
|
+
nowIso,
|
|
214
|
+
nowIso,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return this.getSubscription(params.userId)!;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cancelSubscription(userId: string, now: Date): BookmarkDigestSubscription | undefined {
|
|
221
|
+
this.db
|
|
222
|
+
.prepare(
|
|
223
|
+
`UPDATE bookmark_digest_subscriptions
|
|
224
|
+
SET status = 'cancelled', updated_at = ?
|
|
225
|
+
WHERE user_id = ? AND status = 'active'`,
|
|
226
|
+
)
|
|
227
|
+
.run(toIsoString(now), userId);
|
|
228
|
+
|
|
229
|
+
return this.getSubscription(userId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
updateDigestState(params: {
|
|
233
|
+
userId: string;
|
|
234
|
+
lastScheduledSlot: string;
|
|
235
|
+
lastDigestSignatures: string[];
|
|
236
|
+
now: Date;
|
|
237
|
+
}): void {
|
|
238
|
+
this.db
|
|
239
|
+
.prepare(
|
|
240
|
+
`UPDATE bookmark_digest_subscriptions
|
|
241
|
+
SET last_scheduled_slot = ?,
|
|
242
|
+
last_digest_signatures_json = ?,
|
|
243
|
+
updated_at = ?
|
|
244
|
+
WHERE user_id = ?`,
|
|
245
|
+
)
|
|
246
|
+
.run(
|
|
247
|
+
params.lastScheduledSlot,
|
|
248
|
+
JSON.stringify(params.lastDigestSignatures),
|
|
249
|
+
toIsoString(params.now),
|
|
250
|
+
params.userId,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
createNotification(params: {
|
|
255
|
+
userId: string;
|
|
256
|
+
slotAt: string;
|
|
257
|
+
message: string;
|
|
258
|
+
payload: BookmarkDigestNotificationPayload;
|
|
259
|
+
now: Date;
|
|
260
|
+
}): BookmarkDigestNotification | undefined {
|
|
261
|
+
const id = randomUUID();
|
|
262
|
+
const nowIso = toIsoString(params.now);
|
|
263
|
+
const result = this.db
|
|
264
|
+
.prepare(
|
|
265
|
+
`INSERT OR IGNORE INTO bookmark_digest_notifications (
|
|
266
|
+
id,
|
|
267
|
+
user_id,
|
|
268
|
+
slot_at,
|
|
269
|
+
message,
|
|
270
|
+
payload_json,
|
|
271
|
+
status,
|
|
272
|
+
created_at,
|
|
273
|
+
updated_at
|
|
274
|
+
) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`,
|
|
275
|
+
)
|
|
276
|
+
.run(
|
|
277
|
+
id,
|
|
278
|
+
params.userId,
|
|
279
|
+
params.slotAt,
|
|
280
|
+
params.message,
|
|
281
|
+
JSON.stringify(params.payload),
|
|
282
|
+
nowIso,
|
|
283
|
+
nowIso,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (result.changes === 0) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const row = this.db
|
|
291
|
+
.prepare(`SELECT * FROM bookmark_digest_notifications WHERE id = ?`)
|
|
292
|
+
.get(id) as NotificationRow | undefined;
|
|
293
|
+
return row ? mapNotification(row) : undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
listPendingNotifications(params: {
|
|
297
|
+
userId?: string;
|
|
298
|
+
limit?: number;
|
|
299
|
+
} = {}): BookmarkDigestNotification[] {
|
|
300
|
+
let query =
|
|
301
|
+
`SELECT * FROM bookmark_digest_notifications
|
|
302
|
+
WHERE status = 'pending'`;
|
|
303
|
+
const args: unknown[] = [];
|
|
304
|
+
|
|
305
|
+
if (params.userId) {
|
|
306
|
+
query += ` AND user_id = ?`;
|
|
307
|
+
args.push(params.userId);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
query += ` ORDER BY slot_at ASC, created_at ASC LIMIT ?`;
|
|
311
|
+
args.push(params.limit ?? 20);
|
|
312
|
+
const rows = this.db.prepare(query).all(...args) as NotificationRow[];
|
|
313
|
+
return rows.map(mapNotification);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
markDelivered(ids: string[], now: Date): number {
|
|
317
|
+
if (ids.length === 0) {
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const nowIso = toIsoString(now);
|
|
322
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
323
|
+
const result = this.db
|
|
324
|
+
.prepare(
|
|
325
|
+
`UPDATE bookmark_digest_notifications
|
|
326
|
+
SET status = 'delivered',
|
|
327
|
+
delivered_at = ?,
|
|
328
|
+
last_attempt_at = ?,
|
|
329
|
+
last_error = NULL,
|
|
330
|
+
updated_at = ?
|
|
331
|
+
WHERE id IN (${placeholders}) AND status = 'pending'`,
|
|
332
|
+
)
|
|
333
|
+
.run(nowIso, nowIso, nowIso, ...ids);
|
|
334
|
+
return result.changes;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
recordDeliveryAttempt(id: string, now: Date, error?: string): void {
|
|
338
|
+
const nowIso = toIsoString(now);
|
|
339
|
+
this.db
|
|
340
|
+
.prepare(
|
|
341
|
+
`UPDATE bookmark_digest_notifications
|
|
342
|
+
SET last_attempt_at = ?,
|
|
343
|
+
last_error = ?,
|
|
344
|
+
updated_at = ?
|
|
345
|
+
WHERE id = ?`,
|
|
346
|
+
)
|
|
347
|
+
.run(nowIso, error ?? null, nowIso, id);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { BookmarkSearchItem } from "../bookmark-search/types.js";
|
|
3
|
+
|
|
4
|
+
export type BookmarkDigestSubscriptionStatus = "active" | "cancelled";
|
|
5
|
+
export type BookmarkDigestNotificationStatus = "pending" | "delivered";
|
|
6
|
+
|
|
7
|
+
export type BookmarkDigestSubscription = {
|
|
8
|
+
userId: string;
|
|
9
|
+
timeZone: string;
|
|
10
|
+
scheduleHour: number;
|
|
11
|
+
scheduleMinute: number;
|
|
12
|
+
status: BookmarkDigestSubscriptionStatus;
|
|
13
|
+
lastScheduledSlot: string | null;
|
|
14
|
+
lastDigestSignatures: string[];
|
|
15
|
+
createdAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type BookmarkDigestNotificationPayload = {
|
|
20
|
+
type: "bookmark_digest";
|
|
21
|
+
userId: string;
|
|
22
|
+
slotAt: string;
|
|
23
|
+
hasNewArticles: boolean;
|
|
24
|
+
articles: BookmarkSearchItem[];
|
|
25
|
+
browseUrl?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type BookmarkDigestNotification = {
|
|
29
|
+
id: string;
|
|
30
|
+
userId: string;
|
|
31
|
+
slotAt: string;
|
|
32
|
+
message: string;
|
|
33
|
+
payload: BookmarkDigestNotificationPayload;
|
|
34
|
+
status: BookmarkDigestNotificationStatus;
|
|
35
|
+
deliveredAt: string | null;
|
|
36
|
+
lastAttemptAt: string | null;
|
|
37
|
+
lastError: string | null;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BookmarkDigestMessageInspection = {
|
|
43
|
+
shouldHandle: boolean;
|
|
44
|
+
reason: "subscribe" | "cancel" | "status" | "missing_time" | "no_match";
|
|
45
|
+
scheduleHour?: number;
|
|
46
|
+
scheduleMinute?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type BookmarkDigestChatResult = {
|
|
50
|
+
reply: string;
|
|
51
|
+
intent: "subscribed" | "cancelled" | "status" | "clarify" | "error";
|
|
52
|
+
subscription?: BookmarkDigestSubscription;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const BookmarkDigestPendingQuerySchema = z.object({
|
|
56
|
+
user: z.string().trim().min(1).optional(),
|
|
57
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const BookmarkDigestMarkDeliveredSchema = z.object({
|
|
61
|
+
ids: z.array(z.string().trim().min(1)).min(1).max(100),
|
|
62
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { EngineResult, NormalizedTurn } from "../types.js";
|
|
3
|
+
import type { BookmarkSearchMessageInspection } from "./types.js";
|
|
4
|
+
import type { BookmarkSearchService } from "./service.js";
|
|
5
|
+
|
|
6
|
+
function firstUserText(turn: NormalizedTurn): string {
|
|
7
|
+
return turn.history.find((message) => message.role === "user")?.text ?? turn.userText;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveBookmarkSearchUserId(turn: NormalizedTurn, explicitUser?: string): string {
|
|
11
|
+
if (explicitUser?.trim()) {
|
|
12
|
+
return explicitUser.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const seed = `${turn.model}\n${firstUserText(turn)}`;
|
|
16
|
+
const digest = createHash("sha1").update(seed).digest("hex").slice(0, 16);
|
|
17
|
+
return `search:${digest}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function inspectBookmarkSearchMessage(params: {
|
|
21
|
+
bookmarkSearchService: BookmarkSearchService;
|
|
22
|
+
turn: NormalizedTurn;
|
|
23
|
+
explicitUser?: string;
|
|
24
|
+
}): { userId: string; inspection: BookmarkSearchMessageInspection } {
|
|
25
|
+
const userId = resolveBookmarkSearchUserId(params.turn, params.explicitUser);
|
|
26
|
+
const inspection = params.bookmarkSearchService.inspectMessage({
|
|
27
|
+
userId,
|
|
28
|
+
text: params.turn.userText,
|
|
29
|
+
});
|
|
30
|
+
return { userId, inspection };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function respondToBookmarkSearchMessage(params: {
|
|
34
|
+
bookmarkSearchService: BookmarkSearchService;
|
|
35
|
+
turn: NormalizedTurn;
|
|
36
|
+
explicitUser?: string;
|
|
37
|
+
}): Promise<EngineResult | undefined> {
|
|
38
|
+
const { userId, inspection } = inspectBookmarkSearchMessage(params);
|
|
39
|
+
if (!inspection.shouldHandle) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await params.bookmarkSearchService.processMessage({
|
|
44
|
+
userId,
|
|
45
|
+
text: params.turn.userText,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
model: params.turn.model,
|
|
50
|
+
sessionId: params.turn.sessionId,
|
|
51
|
+
text: result.reply,
|
|
52
|
+
finishReason: "stop",
|
|
53
|
+
matchedIntentId: `bookmark-search.${result.intent}`,
|
|
54
|
+
templateId: `bookmark-search.${result.intent}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractBookmarkSearchQuery, inspectBookmarkSearchQuery } from "./parser.js";
|
|
3
|
+
|
|
4
|
+
describe("bookmark search parser", () => {
|
|
5
|
+
it("strips surrounding quotes from direct search queries", () => {
|
|
6
|
+
expect(extractBookmarkSearchQuery('搜索 "python 教程"')).toBe("python 教程");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("treats a bare keyword as a bookmark search query", () => {
|
|
10
|
+
expect(inspectBookmarkSearchQuery("百度")).toEqual({
|
|
11
|
+
shouldHandle: true,
|
|
12
|
+
reason: "query",
|
|
13
|
+
action: "search",
|
|
14
|
+
query: "百度",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("does not treat greetings as implicit bookmark searches", () => {
|
|
19
|
+
expect(inspectBookmarkSearchQuery("你好")).toEqual({
|
|
20
|
+
shouldHandle: false,
|
|
21
|
+
reason: "no_match",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("asks for a query when the user only mentions searching bookmarks", () => {
|
|
26
|
+
expect(inspectBookmarkSearchQuery("帮我在书签篮里搜一下")).toEqual({
|
|
27
|
+
shouldHandle: true,
|
|
28
|
+
reason: "query",
|
|
29
|
+
action: "search",
|
|
30
|
+
missing: "query",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("detects latest-article requests", () => {
|
|
35
|
+
expect(inspectBookmarkSearchQuery("最新文章")).toEqual({
|
|
36
|
+
shouldHandle: true,
|
|
37
|
+
reason: "query",
|
|
38
|
+
action: "latest",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("detects category-list requests", () => {
|
|
43
|
+
expect(inspectBookmarkSearchQuery("分类列表")).toEqual({
|
|
44
|
+
shouldHandle: true,
|
|
45
|
+
reason: "query",
|
|
46
|
+
action: "categories",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("detects category article requests", () => {
|
|
51
|
+
expect(inspectBookmarkSearchQuery("开发工具分类")).toEqual({
|
|
52
|
+
shouldHandle: true,
|
|
53
|
+
reason: "query",
|
|
54
|
+
action: "category_articles",
|
|
55
|
+
category: "开发工具",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("detects category link requests", () => {
|
|
60
|
+
expect(inspectBookmarkSearchQuery("开发工具常用链接")).toEqual({
|
|
61
|
+
shouldHandle: true,
|
|
62
|
+
reason: "query",
|
|
63
|
+
action: "category_links",
|
|
64
|
+
category: "开发工具",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|