@rei-standard/amsg-server 2.5.2 → 2.6.0-next.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/dist/index.mjs CHANGED
@@ -1,3 +1,30 @@
1
+ import {
2
+ createCancelMessageHandler,
3
+ createD1Adapter,
4
+ createGetUserKeyHandler,
5
+ createMessagesHandler,
6
+ createScheduleMessageHandler,
7
+ createSingleUserCloudflareWorker,
8
+ createSingleUserServer,
9
+ createUpdateMessageHandler,
10
+ createWebCryptoWebPush,
11
+ decryptFromStorage,
12
+ decryptPayload,
13
+ deriveUserEncryptionKey,
14
+ encryptForStorage,
15
+ getHeader,
16
+ isValidISO8601,
17
+ isValidUUID,
18
+ isValidUUIDv4,
19
+ isValidUrl,
20
+ parseJsonBody,
21
+ runScheduledTick,
22
+ validateAvatarUrl,
23
+ validateLlmMessagesArray,
24
+ validateScheduleMessagePayload,
25
+ validateSplitPattern
26
+ } from "./chunk-PJAW56VN.mjs";
27
+
1
28
  // src/server/adapters/factory.js
2
29
  async function createAdapter(config) {
3
30
  if (!config || !config.driver) {
@@ -26,248 +53,6 @@ async function createAdapter(config) {
26
53
  }
27
54
  }
28
55
 
29
- // src/server/lib/validation.js
30
- function isValidISO8601(dateString) {
31
- const date = new Date(dateString);
32
- return date instanceof Date && !isNaN(date.getTime());
33
- }
34
- function isValidUrl(urlString) {
35
- try {
36
- new URL(urlString);
37
- return true;
38
- } catch {
39
- return false;
40
- }
41
- }
42
- function isValidUUID(uuid) {
43
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44
- return uuidRegex.test(uuid);
45
- }
46
- function isValidUUIDv4(uuid) {
47
- const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
48
- return uuidV4Regex.test(uuid);
49
- }
50
- var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
51
- var AVATAR_URL_MAX_LENGTH = 2048;
52
- function validateAvatarUrl(value) {
53
- if (value === void 0 || value === null) return null;
54
- if (typeof value !== "string") {
55
- return "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
56
- }
57
- if (/^data:/i.test(value)) {
58
- return "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
59
- }
60
- if (value.length > AVATAR_URL_MAX_LENGTH) {
61
- return `\u5934\u50CF URL \u957F\u5EA6 ${value.length} \u5B57\u7B26\u8D85\u8FC7 ${AVATAR_URL_MAX_LENGTH} \u4E0A\u9650\uFF0C\u8BF7\u6539\u4E3A\u66F4\u77ED\u7684\u56FE\u7247 URL`;
62
- }
63
- if (!isValidUrl(value)) {
64
- return "avatarUrl \u4E0D\u662F\u5408\u6CD5 URL";
65
- }
66
- return null;
67
- }
68
- var SPLIT_PATTERN_MAX_LENGTH = 200;
69
- var SPLIT_PATTERN_MAX_ITEMS = 10;
70
- function validateSplitPattern(value) {
71
- if (value === void 0 || value === null) return null;
72
- const isArray = Array.isArray(value);
73
- const items = isArray ? value : [value];
74
- if (isArray && items.length === 0) return null;
75
- if (items.length > SPLIT_PATTERN_MAX_ITEMS) {
76
- return `splitPattern \u6570\u7EC4\u6700\u591A ${SPLIT_PATTERN_MAX_ITEMS} \u9879`;
77
- }
78
- for (let i = 0; i < items.length; i++) {
79
- const s = items[i];
80
- const label = isArray ? `splitPattern[${i}]` : "splitPattern";
81
- if (typeof s !== "string") return `${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`;
82
- if (s.length > SPLIT_PATTERN_MAX_LENGTH) {
83
- return `${label} \u4E0D\u80FD\u8D85\u8FC7 ${SPLIT_PATTERN_MAX_LENGTH} \u5B57\u7B26`;
84
- }
85
- try {
86
- new RegExp(s);
87
- } catch (_) {
88
- return `${label} \u4E0D\u662F\u6709\u6548\u6B63\u5219\u8868\u8FBE\u5F0F`;
89
- }
90
- }
91
- return null;
92
- }
93
- function validateLlmMessagesArray(messages) {
94
- if (!Array.isArray(messages) || messages.length === 0) {
95
- return "messages must be a non-empty array";
96
- }
97
- for (let i = 0; i < messages.length; i++) {
98
- const m = messages[i];
99
- if (!m || typeof m !== "object" || Array.isArray(m)) {
100
- return `messages[${i}] must be an object`;
101
- }
102
- if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
103
- return `messages[${i}].role must be one of system / user / assistant / tool`;
104
- }
105
- if (typeof m.content === "string") {
106
- if (!m.content) return `messages[${i}].content must be a non-empty string`;
107
- } else if (Array.isArray(m.content)) {
108
- if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
109
- } else {
110
- return `messages[${i}].content must be a non-empty string or a non-empty array`;
111
- }
112
- }
113
- return null;
114
- }
115
- function validateScheduleMessagePayload(payload) {
116
- if (!payload.contactName || typeof payload.contactName !== "string") {
117
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
118
- }
119
- if (!payload.messageType || !["fixed", "prompted", "auto", "instant"].includes(payload.messageType)) {
120
- return { valid: false, errorCode: "INVALID_MESSAGE_TYPE", errorMessage: "\u6D88\u606F\u7C7B\u578B\u65E0\u6548", details: { providedType: payload.messageType, allowedTypes: ["fixed", "prompted", "auto", "instant"] } };
121
- }
122
- if (!payload.firstSendTime || !isValidISO8601(payload.firstSendTime)) {
123
- return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u683C\u5F0F\u65E0\u6548", details: { field: "firstSendTime" } };
124
- }
125
- if (payload.firstSendTime && new Date(payload.firstSendTime) <= /* @__PURE__ */ new Date()) {
126
- return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u5FC5\u987B\u5728\u672A\u6765", details: { field: "firstSendTime", reason: "must be in the future" } };
127
- }
128
- if (!payload.pushSubscription || typeof payload.pushSubscription !== "object") {
129
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["pushSubscription"] } };
130
- }
131
- if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
132
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
133
- }
134
- if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
135
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
136
- }
137
- if (payload.messageType === "fixed") {
138
- if (!payload.userMessage) {
139
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["userMessage (required for fixed type)"] } };
140
- }
141
- }
142
- const promptCheck = (() => {
143
- const hasCompletePrompt = payload.completePrompt !== void 0 && payload.completePrompt !== null && payload.completePrompt !== "";
144
- const hasMessages = payload.messages !== void 0 && payload.messages !== null;
145
- if (hasCompletePrompt && hasMessages) {
146
- return {
147
- error: { code: "INVALID_PARAMETERS", message: "exactly one of `completePrompt` or `messages` must be provided\uFF08\u4E24\u8005\u4E0D\u80FD\u540C\u65F6\u51FA\u73B0\uFF09", details: { invalidFields: ["completePrompt", "messages"] } },
148
- hasCompletePrompt: true,
149
- hasMessages: true
150
- };
151
- }
152
- if (hasMessages) {
153
- const err = validateLlmMessagesArray(payload.messages);
154
- if (err) {
155
- return {
156
- error: { code: "INVALID_PARAMETERS", message: err, details: { invalidFields: ["messages"] } },
157
- hasCompletePrompt: false,
158
- hasMessages: true
159
- };
160
- }
161
- }
162
- return { error: null, hasCompletePrompt, hasMessages };
163
- })();
164
- if (promptCheck.error) {
165
- return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
166
- }
167
- const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
168
- if (payload.messageType === "prompted" || payload.messageType === "auto") {
169
- const missingAiFields = [];
170
- if (!hasPrompt) missingAiFields.push("completePrompt or messages");
171
- if (!payload.apiUrl) missingAiFields.push("apiUrl");
172
- if (!payload.apiKey) missingAiFields.push("apiKey");
173
- if (!payload.primaryModel) missingAiFields.push("primaryModel");
174
- if (missingAiFields.length > 0) {
175
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: missingAiFields } };
176
- }
177
- }
178
- if (payload.messageType === "instant") {
179
- if (payload.recurrenceType && payload.recurrenceType !== "none") {
180
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u7684 recurrenceType \u5FC5\u987B\u4E3A none", details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
181
- }
182
- const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
183
- const hasUserMessage = payload.userMessage;
184
- if (!hasAiConfig && !hasUserMessage) {
185
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u5FC5\u987B\u63D0\u4F9B userMessage \u6216\u5B8C\u6574\u7684 AI \u914D\u7F6E", details: { missingFields: ["userMessage or ((completePrompt | messages) + apiUrl + apiKey + primaryModel)"] } };
186
- }
187
- }
188
- if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
189
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature (must be a finite number)"] } };
190
- }
191
- const avatarErr = validateAvatarUrl(payload.avatarUrl);
192
- if (avatarErr) {
193
- console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
194
- payload.avatarUrl = null;
195
- }
196
- if (payload.uuid && !isValidUUID(payload.uuid)) {
197
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
198
- }
199
- if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
200
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
201
- }
202
- const splitErr = validateSplitPattern(payload.splitPattern);
203
- if (splitErr) {
204
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: splitErr, details: { invalidFields: ["splitPattern"] } };
205
- }
206
- return { valid: true };
207
- }
208
-
209
- // src/server/lib/request.js
210
- var REQUEST_ERRORS = {
211
- INVALID_JSON: { code: "INVALID_JSON", message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548\u7684 JSON" },
212
- INVALID_REQUEST_BODY: { code: "INVALID_REQUEST_BODY", message: "\u8BF7\u6C42\u4F53\u683C\u5F0F\u65E0\u6548" },
213
- INVALID_ENCRYPTED_PAYLOAD: { code: "INVALID_ENCRYPTED_PAYLOAD", message: "\u52A0\u5BC6\u6570\u636E\u683C\u5F0F\u9519\u8BEF" }
214
- };
215
- function parseBodyAsObject(body, options = {}) {
216
- const invalidJson = options.invalidJson || REQUEST_ERRORS.INVALID_JSON;
217
- const invalidType = options.invalidType || REQUEST_ERRORS.INVALID_REQUEST_BODY;
218
- let parsed = body;
219
- if (typeof parsed === "string") {
220
- try {
221
- parsed = JSON.parse(parsed);
222
- } catch {
223
- return { ok: false, error: invalidJson };
224
- }
225
- }
226
- if (!isPlainObject(parsed)) {
227
- return { ok: false, error: invalidType };
228
- }
229
- return { ok: true, data: parsed };
230
- }
231
- function parseJsonBody(body) {
232
- return parseBodyAsObject(body, {
233
- invalidJson: REQUEST_ERRORS.INVALID_JSON,
234
- invalidType: REQUEST_ERRORS.INVALID_REQUEST_BODY
235
- });
236
- }
237
- function isPlainObject(value) {
238
- return typeof value === "object" && value !== null && !Array.isArray(value);
239
- }
240
- function isEncryptedEnvelope(payload) {
241
- if (!isPlainObject(payload)) return false;
242
- return typeof payload.iv === "string" && typeof payload.authTag === "string" && typeof payload.encryptedData === "string";
243
- }
244
- function parseEncryptedBody(body) {
245
- const parsedBody = parseBodyAsObject(body, {
246
- invalidJson: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD,
247
- invalidType: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD
248
- });
249
- if (!parsedBody.ok) {
250
- return parsedBody;
251
- }
252
- if (!isEncryptedEnvelope(parsedBody.data)) {
253
- return { ok: false, error: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD };
254
- }
255
- return parsedBody;
256
- }
257
- function getHeader(headers = {}, name) {
258
- if (!headers || typeof headers !== "object") {
259
- return "";
260
- }
261
- const lowerName = String(name || "").toLowerCase();
262
- if (!lowerName) return "";
263
- for (const [key, value] of Object.entries(headers)) {
264
- if (String(key).toLowerCase() === lowerName) {
265
- return String(value || "").trim();
266
- }
267
- }
268
- return "";
269
- }
270
-
271
56
  // src/server/handlers/init-tenant.js
272
57
  function createInitTenantHandler(ctx) {
273
58
  async function POST(headers, body) {
@@ -384,560 +169,6 @@ function createInitTenantHandler(ctx) {
384
169
  return { POST };
385
170
  }
386
171
 
387
- // src/server/lib/encryption.js
388
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
389
- function deriveUserEncryptionKey(userId, masterKey) {
390
- return createHash("sha256").update(masterKey + userId).digest("hex").slice(0, 64);
391
- }
392
- function decryptPayload(encryptedPayload, encryptionKey) {
393
- const { iv, authTag, encryptedData } = encryptedPayload;
394
- const decipher = createDecipheriv(
395
- "aes-256-gcm",
396
- Buffer.from(encryptionKey, "hex"),
397
- Buffer.from(iv, "base64")
398
- );
399
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
400
- const decrypted = Buffer.concat([
401
- decipher.update(Buffer.from(encryptedData, "base64")),
402
- decipher.final()
403
- ]);
404
- return JSON.parse(decrypted.toString("utf8"));
405
- }
406
- function encryptPayload(payload, encryptionKey) {
407
- const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
408
- const iv = randomBytes(12);
409
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
410
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
411
- const authTag = cipher.getAuthTag();
412
- return {
413
- iv: iv.toString("base64"),
414
- authTag: authTag.toString("base64"),
415
- encryptedData: encrypted.toString("base64")
416
- };
417
- }
418
- function encryptForStorage(text, encryptionKey) {
419
- const iv = randomBytes(16);
420
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
421
- const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
422
- const authTag = cipher.getAuthTag();
423
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
424
- }
425
- function decryptFromStorage(encryptedText, encryptionKey) {
426
- const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
427
- const decipher = createDecipheriv(
428
- "aes-256-gcm",
429
- Buffer.from(encryptionKey, "hex"),
430
- Buffer.from(ivHex, "hex")
431
- );
432
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
433
- return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
434
- }
435
-
436
- // src/server/handlers/get-user-key.js
437
- function createGetUserKeyHandler(ctx) {
438
- async function GET(url, headers) {
439
- const effectiveHeaders = headers || url || {};
440
- const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
441
- if (!tenantResult.ok) {
442
- return tenantResult.error;
443
- }
444
- const { masterKey } = tenantResult.context;
445
- const userId = getHeader(effectiveHeaders, "x-user-id");
446
- if (!userId) {
447
- return {
448
- status: 400,
449
- body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
450
- };
451
- }
452
- if (!isValidUUIDv4(userId)) {
453
- return {
454
- status: 400,
455
- body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
456
- };
457
- }
458
- return {
459
- status: 200,
460
- body: {
461
- success: true,
462
- data: {
463
- userKey: deriveUserEncryptionKey(userId, masterKey),
464
- version: 1
465
- }
466
- }
467
- };
468
- }
469
- return { GET };
470
- }
471
-
472
- // src/server/handlers/schedule-message.js
473
- import { randomUUID as randomUUID2 } from "crypto";
474
-
475
- // src/server/lib/db-errors.js
476
- function isUniqueViolation(error) {
477
- if (!error || typeof error !== "object") return false;
478
- const code = error.code;
479
- if (code === "23505") return true;
480
- const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
481
- return message.includes("duplicate key") || message.includes("unique constraint");
482
- }
483
-
484
- // src/server/lib/message-processor.js
485
- import { randomUUID } from "crypto";
486
- import {
487
- buildContentPush,
488
- buildReasoningPush
489
- } from "@rei-standard/amsg-shared";
490
- var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
491
- function splitOnceByRegex(chunk, regex) {
492
- const out = chunk.split(regex).reduce((acc, part, i, arr) => {
493
- if (i % 2 === 0 && part.trim()) {
494
- const punctuation = arr[i + 1] || "";
495
- acc.push(part.trim() + punctuation);
496
- }
497
- return acc;
498
- }, []).filter((s) => s.length > 0);
499
- return out.length > 0 ? out : [chunk];
500
- }
501
- function splitMessageIntoSentences(messageContent, splitPattern = null) {
502
- const sources = splitPattern == null ? null : Array.isArray(splitPattern) ? splitPattern : [splitPattern];
503
- const regexes = sources && sources.length > 0 ? sources.map((s) => new RegExp(s)) : [DEFAULT_SPLIT_REGEX];
504
- let chunks = [messageContent];
505
- for (const regex of regexes) {
506
- chunks = chunks.flatMap((c) => splitOnceByRegex(c, regex));
507
- }
508
- return chunks.length > 0 ? chunks : [messageContent];
509
- }
510
- function readReasoningContent(llmResponse) {
511
- if (!llmResponse || typeof llmResponse !== "object") return null;
512
- const choices = (
513
- /** @type {{ choices?: unknown }} */
514
- llmResponse.choices
515
- );
516
- if (!Array.isArray(choices) || choices.length === 0) return null;
517
- const message = (
518
- /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */
519
- choices[0]?.message
520
- );
521
- const raw = message?.reasoning_content;
522
- if (typeof raw === "string") {
523
- const trimmed = raw.trim();
524
- if (trimmed.length > 0) return trimmed;
525
- }
526
- const content = message?.content;
527
- if (typeof content === "string") {
528
- const match = content.match(REASONING_TAG_RE);
529
- if (match) {
530
- const trimmed = match[2].trim();
531
- if (trimmed.length > 0) return trimmed;
532
- }
533
- }
534
- return null;
535
- }
536
- var REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
537
- var REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
538
- function stripReasoningTags(content) {
539
- if (typeof content !== "string" || !content.includes("<")) return content;
540
- return content.replace(REASONING_TAG_RE_G, "").trim();
541
- }
542
- async function processSingleMessage(task, ctx, providedMasterKey) {
543
- try {
544
- const masterKey = providedMasterKey || ctx.masterKey;
545
- if (!masterKey) {
546
- return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
547
- }
548
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
549
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
550
- let messageContent;
551
- let llmResponse = null;
552
- if (decryptedPayload.messageType === "fixed") {
553
- messageContent = decryptedPayload.userMessage;
554
- } else if (decryptedPayload.messageType === "instant") {
555
- const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
556
- if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
557
- const aiResult = await _callAI(decryptedPayload);
558
- messageContent = aiResult.content;
559
- llmResponse = aiResult.response;
560
- } else if (decryptedPayload.userMessage) {
561
- messageContent = decryptedPayload.userMessage;
562
- } else {
563
- throw new Error("Invalid instant message: no content source available");
564
- }
565
- } else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
566
- const aiResult = await _callAI(decryptedPayload);
567
- messageContent = aiResult.content;
568
- llmResponse = aiResult.response;
569
- } else {
570
- throw new Error("Invalid message configuration: no content source available");
571
- }
572
- const reasoning = readReasoningContent(llmResponse);
573
- if (reasoning) {
574
- messageContent = stripReasoningTags(messageContent);
575
- }
576
- const messages = splitMessageIntoSentences(messageContent, decryptedPayload.splitPattern ?? null);
577
- if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
578
- throw new Error("VAPID configuration missing - push notifications cannot be sent");
579
- }
580
- const pushSubscription = decryptedPayload.pushSubscription;
581
- const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
582
- const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
583
- const messageSubtype = decryptedPayload.messageSubtype || "chat";
584
- const avatarUrl = decryptedPayload.avatarUrl || null;
585
- const metadata = decryptedPayload.metadata || {};
586
- const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
587
- if (reasoning) {
588
- const reasoningPush = buildReasoningPush({
589
- messageType: decryptedPayload.messageType,
590
- source,
591
- messageId: `${messageIdBase}_reasoning`,
592
- sessionId,
593
- reasoningContent: reasoning,
594
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
595
- title: `\u6765\u81EA ${decryptedPayload.contactName}`,
596
- contactName: decryptedPayload.contactName,
597
- avatarUrl,
598
- messageSubtype,
599
- metadata
600
- });
601
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
602
- await new Promise((resolve) => setTimeout(resolve, 1500));
603
- }
604
- for (let i = 0; i < messages.length; i++) {
605
- const contentPush = buildContentPush({
606
- messageType: decryptedPayload.messageType,
607
- source,
608
- messageId: `${messageIdBase}_${i}`,
609
- sessionId,
610
- message: messages[i],
611
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
612
- title: `\u6765\u81EA ${decryptedPayload.contactName}`,
613
- contactName: decryptedPayload.contactName,
614
- avatarUrl,
615
- messageSubtype,
616
- messageIndex: i + 1,
617
- totalMessages: messages.length,
618
- taskId: task.id || null,
619
- metadata
620
- });
621
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
622
- if (i < messages.length - 1) {
623
- await new Promise((resolve) => setTimeout(resolve, 1500));
624
- }
625
- }
626
- return { success: true, messagesSent: messages.length };
627
- } catch (error) {
628
- return { success: false, messagesSent: 0, error: error.message };
629
- }
630
- }
631
- async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, providedMasterKey) {
632
- let retryCount = 0;
633
- const masterKey = providedMasterKey || ctx.masterKey;
634
- if (!masterKey) {
635
- return {
636
- success: false,
637
- error: { code: "TENANT_MASTER_KEY_MISSING", message: "\u79DF\u6237\u4E3B\u5BC6\u94A5\u4E0D\u5B58\u5728\u6216\u914D\u7F6E\u5F02\u5E38" }
638
- };
639
- }
640
- while (retryCount <= maxRetries) {
641
- let task;
642
- try {
643
- task = userId ? await ctx.db.getTaskByUuid(uuid, userId) : await ctx.db.getTaskByUuidOnly(uuid);
644
- } catch (error) {
645
- if (retryCount < maxRetries) {
646
- retryCount++;
647
- await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
648
- continue;
649
- }
650
- return {
651
- success: false,
652
- error: { code: "INTERNAL_ERROR", message: error.message, retriesAttempted: retryCount }
653
- };
654
- }
655
- if (!task) {
656
- return { success: false, error: { code: "TASK_NOT_FOUND", message: "\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406" } };
657
- }
658
- const result = await processSingleMessage(task, ctx, masterKey);
659
- if (!result.success) {
660
- if (retryCount < maxRetries) {
661
- retryCount++;
662
- await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
663
- continue;
664
- }
665
- try {
666
- await ctx.db.updateTaskById(task.id, { status: "failed", retry_count: retryCount });
667
- } catch (_updateError) {
668
- }
669
- return {
670
- success: false,
671
- error: { code: "PROCESSING_ERROR", message: result.error, retriesAttempted: retryCount }
672
- };
673
- }
674
- try {
675
- await ctx.db.deleteTaskById(task.id);
676
- } catch (error) {
677
- try {
678
- await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
679
- } catch (_markSentError) {
680
- }
681
- return {
682
- success: false,
683
- error: {
684
- code: "POST_SEND_CLEANUP_FAILED",
685
- message: "\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u4F46\u4EFB\u52A1\u6E05\u7406\u5931\u8D25",
686
- details: { error: error.message }
687
- }
688
- };
689
- }
690
- return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
691
- }
692
- }
693
- async function _callAI(payload) {
694
- const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
695
- const requestBody = buildAiRequestBody(payload);
696
- const aiResponse = await fetch(normalizedApiUrl, {
697
- method: "POST",
698
- headers: {
699
- "Content-Type": "application/json",
700
- "Authorization": `Bearer ${payload.apiKey}`
701
- },
702
- body: JSON.stringify(requestBody),
703
- signal: AbortSignal.timeout(3e5)
704
- });
705
- if (!aiResponse.ok) {
706
- if (aiResponse.status === 405) {
707
- throw new Error(
708
- `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
709
- );
710
- }
711
- throw new Error(
712
- `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
713
- );
714
- }
715
- const aiData = await aiResponse.json();
716
- const content = aiData?.choices?.[0]?.message?.content;
717
- if (typeof content !== "string" || !content.trim()) {
718
- throw new Error("AI API error: response missing choices[0].message.content");
719
- }
720
- return { response: aiData, content: content.trim() };
721
- }
722
- function buildAiRequestBody(payload) {
723
- const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
724
- const requestBody = {
725
- model: payload.primaryModel,
726
- messages: llmMessages
727
- };
728
- if (payload.temperature !== void 0 && payload.temperature !== null) {
729
- requestBody.temperature = payload.temperature;
730
- } else if (!Array.isArray(payload.messages)) {
731
- requestBody.temperature = 0.8;
732
- }
733
- if (payload.maxTokens === void 0 || payload.maxTokens === null) {
734
- return requestBody;
735
- }
736
- if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
737
- throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
738
- }
739
- requestBody.max_tokens = payload.maxTokens;
740
- return requestBody;
741
- }
742
- function normalizeAiApiUrl(apiUrl) {
743
- if (typeof apiUrl !== "string" || !apiUrl.trim()) {
744
- throw new Error(
745
- "Invalid apiUrl: apiUrl is required. Please provide a chat endpoint URL (for example: https://api.openai.com or https://api.openai.com/v1/chat/completions)."
746
- );
747
- }
748
- const trimmedApiUrl = apiUrl.trim();
749
- let parsedUrl;
750
- try {
751
- parsedUrl = new URL(trimmedApiUrl);
752
- } catch {
753
- throw new Error(
754
- `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
755
- );
756
- }
757
- let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
758
- if (/\/chat\/completions$/.test(path)) {
759
- } else if (path === "/") {
760
- path = "/v1/chat/completions";
761
- } else if (/\/v\d+$/.test(path)) {
762
- path = `${path}/chat/completions`;
763
- }
764
- parsedUrl.pathname = path;
765
- return parsedUrl.toString();
766
- }
767
-
768
- // src/server/handlers/schedule-message.js
769
- function createScheduleMessageHandler(ctx) {
770
- async function POST(headers, body) {
771
- const tenantResult = await ctx.tenantManager.resolveTenant(headers);
772
- if (!tenantResult.ok) {
773
- return tenantResult.error;
774
- }
775
- const tenantCtx = tenantResult.context;
776
- const db = tenantCtx.db;
777
- const masterKey = tenantCtx.masterKey;
778
- const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
779
- const encryptionVersion = getHeader(headers, "x-encryption-version");
780
- const userId = getHeader(headers, "x-user-id");
781
- if (!isEncrypted) {
782
- return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
783
- }
784
- if (!userId) {
785
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
786
- }
787
- if (!isValidUUIDv4(userId)) {
788
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
789
- }
790
- if (encryptionVersion !== "1") {
791
- return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
792
- }
793
- const parsedBody = parseEncryptedBody(body);
794
- if (!parsedBody.ok) {
795
- return { status: 400, body: { success: false, error: parsedBody.error } };
796
- }
797
- const encryptedBody = parsedBody.data;
798
- let payload;
799
- try {
800
- const userKey2 = deriveUserEncryptionKey(userId, masterKey);
801
- payload = decryptPayload(encryptedBody, userKey2);
802
- } catch (error) {
803
- if (error instanceof SyntaxError) {
804
- return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
805
- }
806
- const message = typeof error.message === "string" ? error.message : "";
807
- if (message.includes("auth") || message.includes("Unsupported state")) {
808
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
809
- }
810
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
811
- }
812
- if (!isPlainObject(payload)) {
813
- return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61" } } };
814
- }
815
- const validationResult = validateScheduleMessagePayload(payload);
816
- if (!validationResult.valid) {
817
- return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
818
- }
819
- const taskUuid = payload.uuid || randomUUID2();
820
- const userKey = deriveUserEncryptionKey(userId, masterKey);
821
- const fullTaskData = {
822
- contactName: payload.contactName,
823
- avatarUrl: payload.avatarUrl || null,
824
- messageType: payload.messageType,
825
- messageSubtype: payload.messageSubtype || "chat",
826
- userMessage: payload.userMessage || null,
827
- firstSendTime: payload.firstSendTime,
828
- recurrenceType: payload.recurrenceType || "none",
829
- apiUrl: payload.apiUrl || null,
830
- apiKey: payload.apiKey || null,
831
- primaryModel: payload.primaryModel || null,
832
- // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
833
- // style array). Validation has already enforced exactly-one-of, so
834
- // exactly one of these will be non-null when an AI config is provided.
835
- completePrompt: payload.completePrompt || null,
836
- messages: Array.isArray(payload.messages) ? payload.messages : null,
837
- maxTokens: payload.maxTokens ?? null,
838
- temperature: payload.temperature ?? null,
839
- // 0.6.0+: optional caller-provided regex (string or string[]) used by
840
- // the message processor to chunk LLM output into individual pushes.
841
- // null → processor falls back to the default /([。!?!?]+)/ regex.
842
- splitPattern: payload.splitPattern ?? null,
843
- pushSubscription: payload.pushSubscription,
844
- metadata: payload.metadata || {}
845
- };
846
- const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
847
- if (payload.messageType === "instant") {
848
- if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
849
- return {
850
- status: 500,
851
- body: {
852
- success: false,
853
- error: {
854
- code: "VAPID_CONFIG_ERROR",
855
- message: "VAPID \u914D\u7F6E\u7F3A\u5931\uFF0C\u65E0\u6CD5\u53D1\u9001\u5373\u65F6\u6D88\u606F",
856
- details: {
857
- missingKeys: [
858
- !ctx.vapid.email && "VAPID_EMAIL",
859
- !ctx.vapid.publicKey && "NEXT_PUBLIC_VAPID_PUBLIC_KEY",
860
- !ctx.vapid.privateKey && "VAPID_PRIVATE_KEY"
861
- ].filter(Boolean)
862
- }
863
- }
864
- }
865
- };
866
- }
867
- }
868
- let dbResult;
869
- try {
870
- dbResult = await db.createTask({
871
- user_id: userId,
872
- uuid: taskUuid,
873
- encrypted_payload: encryptedPayload,
874
- next_send_at: payload.firstSendTime,
875
- message_type: payload.messageType
876
- });
877
- } catch (error) {
878
- if (isUniqueViolation(error)) {
879
- return {
880
- status: 409,
881
- body: {
882
- success: false,
883
- error: {
884
- code: "TASK_UUID_CONFLICT",
885
- message: "\u4EFB\u52A1 UUID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u65B0\u7684 uuid \u91CD\u65B0\u63D0\u4EA4"
886
- }
887
- }
888
- };
889
- }
890
- throw error;
891
- }
892
- if (!dbResult) {
893
- return { status: 500, body: { success: false, error: { code: "TASK_CREATE_FAILED", message: "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25" } } };
894
- }
895
- if (payload.messageType === "instant") {
896
- try {
897
- const sendResult = await processMessagesByUuid(taskUuid, {
898
- ...ctx,
899
- db,
900
- masterKey
901
- }, 2, userId, masterKey);
902
- if (!sendResult.success) {
903
- return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: sendResult.error } } };
904
- }
905
- return {
906
- status: 200,
907
- body: {
908
- success: true,
909
- data: {
910
- uuid: taskUuid,
911
- contactName: payload.contactName,
912
- messagesSent: sendResult.messagesSent,
913
- sentAt: (/* @__PURE__ */ new Date()).toISOString(),
914
- status: "sent",
915
- retriesUsed: sendResult.retriesUsed || 0
916
- }
917
- }
918
- };
919
- } catch (error) {
920
- return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: { error: error.message } } } };
921
- }
922
- }
923
- return {
924
- status: 201,
925
- body: {
926
- success: true,
927
- data: {
928
- id: dbResult.id,
929
- uuid: dbResult.uuid,
930
- contactName: payload.contactName,
931
- nextSendAt: dbResult.next_send_at,
932
- status: dbResult.status,
933
- createdAt: dbResult.created_at
934
- }
935
- }
936
- };
937
- }
938
- return { POST };
939
- }
940
-
941
172
  // src/server/handlers/send-notifications.js
942
173
  function createSendNotificationsHandler(ctx) {
943
174
  async function POST(urlOrHeaders, maybeHeaders) {
@@ -972,371 +203,14 @@ function createSendNotificationsHandler(ctx) {
972
203
  }
973
204
  };
974
205
  }
975
- const startTime = Date.now();
976
- const tasks = await db.getPendingTasks(50);
977
- const MAX_CONCURRENT = 8;
978
- const results = {
979
- totalTasks: tasks.length,
980
- successCount: 0,
981
- failedCount: 0,
982
- deletedOnceOffTasks: 0,
983
- updatedRecurringTasks: 0,
984
- failedTasks: []
985
- };
986
- async function handleDeliveryFailure(task, reason) {
987
- results.failedCount++;
988
- try {
989
- if (task.retry_count >= 3) {
990
- await db.updateTaskById(task.id, { status: "failed" });
991
- results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
992
- } else {
993
- const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
994
- await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
995
- results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
996
- }
997
- } catch (updateError) {
998
- results.failedTasks.push({
999
- taskId: task.id,
1000
- reason,
1001
- status: "retry_update_failed",
1002
- updateError: updateError.message
1003
- });
1004
- }
1005
- }
1006
- async function handlePostSendPersistenceFailure(task, reason) {
1007
- results.failedCount++;
1008
- let markedSent = false;
1009
- try {
1010
- await db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
1011
- markedSent = true;
1012
- } catch (_markSentError) {
1013
- markedSent = false;
1014
- }
1015
- results.failedTasks.push({
1016
- taskId: task.id,
1017
- reason,
1018
- status: markedSent ? "post_send_cleanup_failed_marked_sent" : "post_send_cleanup_failed",
1019
- messageDelivered: true
1020
- });
1021
- }
1022
- async function processTask(task) {
1023
- let sendResult;
1024
- try {
1025
- sendResult = await processSingleMessage(task, {
1026
- ...ctx,
1027
- db,
1028
- masterKey
1029
- }, masterKey);
1030
- } catch (error) {
1031
- await handleDeliveryFailure(task, error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
1032
- return;
1033
- }
1034
- if (!sendResult.success) {
1035
- await handleDeliveryFailure(task, sendResult.error || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
1036
- return;
1037
- }
1038
- try {
1039
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
1040
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
1041
- if (decryptedPayload.recurrenceType === "none") {
1042
- await db.deleteTaskById(task.id);
1043
- results.deletedOnceOffTasks++;
1044
- } else {
1045
- let nextSendAt;
1046
- const currentSendAt = new Date(task.next_send_at);
1047
- if (decryptedPayload.recurrenceType === "daily") {
1048
- nextSendAt = new Date(currentSendAt.getTime() + 24 * 60 * 60 * 1e3);
1049
- } else if (decryptedPayload.recurrenceType === "weekly") {
1050
- nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
1051
- }
1052
- await db.updateTaskById(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
1053
- results.updatedRecurringTasks++;
1054
- }
1055
- results.successCount++;
1056
- } catch (error) {
1057
- await handlePostSendPersistenceFailure(task, error.message || "\u53D1\u9001\u540E\u72B6\u6001\u66F4\u65B0\u5931\u8D25");
1058
- }
1059
- }
1060
- const taskQueue = [...tasks];
1061
- const processing = [];
1062
- while (taskQueue.length > 0 || processing.length > 0) {
1063
- while (processing.length < MAX_CONCURRENT && taskQueue.length > 0) {
1064
- const task = taskQueue.shift();
1065
- const promise = processTask(task);
1066
- processing.push(promise);
1067
- promise.finally(() => {
1068
- const index = processing.indexOf(promise);
1069
- if (index > -1) processing.splice(index, 1);
1070
- });
1071
- }
1072
- if (processing.length > 0) {
1073
- await Promise.race(processing);
1074
- }
1075
- }
1076
- await db.cleanupOldTasks(7);
1077
- const executionTime = Date.now() - startTime;
1078
- return {
1079
- status: 200,
1080
- body: {
1081
- success: true,
1082
- data: {
1083
- totalTasks: results.totalTasks,
1084
- successCount: results.successCount,
1085
- failedCount: results.failedCount,
1086
- processedAt: (/* @__PURE__ */ new Date()).toISOString(),
1087
- executionTime,
1088
- details: {
1089
- deletedOnceOffTasks: results.deletedOnceOffTasks,
1090
- updatedRecurringTasks: results.updatedRecurringTasks,
1091
- failedTasks: results.failedTasks
1092
- }
1093
- }
1094
- }
1095
- };
206
+ const data = await runScheduledTick({ ...ctx, db, masterKey });
207
+ return { status: 200, body: { success: true, data } };
1096
208
  }
1097
209
  return { POST };
1098
210
  }
1099
211
 
1100
- // src/server/handlers/update-message.js
1101
- function createUpdateMessageHandler(ctx) {
1102
- async function PUT(url, headers, body) {
1103
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1104
- if (!tenantResult.ok) {
1105
- return tenantResult.error;
1106
- }
1107
- const tenantCtx = tenantResult.context;
1108
- const db = tenantCtx.db;
1109
- const masterKey = tenantCtx.masterKey;
1110
- const u = new URL(url, "https://dummy");
1111
- const taskUuid = u.searchParams.get("id");
1112
- if (!taskUuid) {
1113
- return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
1114
- }
1115
- const userId = getHeader(headers, "x-user-id");
1116
- if (!userId) {
1117
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
1118
- }
1119
- if (!isValidUUIDv4(userId)) {
1120
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
1121
- }
1122
- const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
1123
- const encryptionVersion = getHeader(headers, "x-encryption-version");
1124
- if (!isEncrypted) {
1125
- return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
1126
- }
1127
- if (encryptionVersion !== "1") {
1128
- return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
1129
- }
1130
- const parsedBody = parseEncryptedBody(body);
1131
- if (!parsedBody.ok) {
1132
- return { status: 400, body: { success: false, error: parsedBody.error } };
1133
- }
1134
- const encryptedBody = parsedBody.data;
1135
- const userKey = deriveUserEncryptionKey(userId, masterKey);
1136
- let updates;
1137
- try {
1138
- updates = decryptPayload(encryptedBody, userKey);
1139
- } catch (_error) {
1140
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
1141
- }
1142
- if (!isPlainObject(updates)) {
1143
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF" } } };
1144
- }
1145
- if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
1146
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
1147
- }
1148
- if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
1149
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
1150
- }
1151
- if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
1152
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
1153
- }
1154
- if (updates.completePrompt && updates.messages !== void 0 && updates.messages !== null) {
1155
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "completePrompt \u4E0E messages \u4E0D\u80FD\u540C\u65F6\u66F4\u65B0\uFF08\u4E8C\u9009\u4E00\uFF09", details: { invalidFields: ["completePrompt", "messages"] } } } };
1156
- }
1157
- if (updates.messages !== void 0 && updates.messages !== null) {
1158
- const msgErr = validateLlmMessagesArray(updates.messages);
1159
- if (msgErr) {
1160
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: msgErr, details: { invalidFields: ["messages"] } } } };
1161
- }
1162
- }
1163
- if (Object.prototype.hasOwnProperty.call(updates, "temperature") && updates.temperature !== null && (typeof updates.temperature !== "number" || !Number.isFinite(updates.temperature))) {
1164
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature"] } } } };
1165
- }
1166
- if (Object.prototype.hasOwnProperty.call(updates, "splitPattern")) {
1167
- const splitErr = validateSplitPattern(updates.splitPattern);
1168
- if (splitErr) {
1169
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: splitErr, details: { invalidFields: ["splitPattern"] } } } };
1170
- }
1171
- }
1172
- if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
1173
- const avatarErr = validateAvatarUrl(updates.avatarUrl);
1174
- if (avatarErr) {
1175
- console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
1176
- delete updates.avatarUrl;
1177
- }
1178
- }
1179
- const existingTask = await db.getTaskByUuid(taskUuid, userId);
1180
- if (!existingTask) {
1181
- const taskStatus = await db.getTaskStatus(taskUuid, userId);
1182
- if (!taskStatus) {
1183
- return { status: 404, body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } } };
1184
- }
1185
- return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
1186
- }
1187
- const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1188
- const promptUpdates = {};
1189
- if (updates.completePrompt) {
1190
- promptUpdates.completePrompt = updates.completePrompt;
1191
- promptUpdates.messages = null;
1192
- } else if (updates.messages !== void 0 && updates.messages !== null) {
1193
- promptUpdates.messages = updates.messages;
1194
- promptUpdates.completePrompt = null;
1195
- }
1196
- const updatedData = {
1197
- ...existingData,
1198
- ...promptUpdates,
1199
- ...updates.userMessage && { userMessage: updates.userMessage },
1200
- ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
1201
- ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
1202
- ...updates.metadata && { metadata: updates.metadata },
1203
- ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null },
1204
- ...Object.prototype.hasOwnProperty.call(updates, "temperature") && { temperature: updates.temperature ?? null },
1205
- // splitPattern: hasOwnProperty so that explicit `null` (= revert to
1206
- // default) doesn't get swallowed by truthy-spread the way the optional
1207
- // string fields above are.
1208
- ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
1209
- };
1210
- const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
1211
- const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
1212
- const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
1213
- if (!result) {
1214
- return { status: 409, body: { success: false, error: { code: "UPDATE_CONFLICT", message: "\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25\uFF0C\u4EFB\u52A1\u53EF\u80FD\u5DF2\u88AB\u4FEE\u6539\u6216\u5220\u9664" } } };
1215
- }
1216
- return {
1217
- status: 200,
1218
- body: {
1219
- success: true,
1220
- data: {
1221
- uuid: taskUuid,
1222
- updatedFields: Object.keys(updates),
1223
- updatedAt: result.updated_at
1224
- }
1225
- }
1226
- };
1227
- }
1228
- return { PUT };
1229
- }
1230
-
1231
- // src/server/handlers/cancel-message.js
1232
- function createCancelMessageHandler(ctx) {
1233
- async function DELETE(url, headers) {
1234
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1235
- if (!tenantResult.ok) {
1236
- return tenantResult.error;
1237
- }
1238
- const db = tenantResult.context.db;
1239
- const u = new URL(url, "https://dummy");
1240
- const taskUuid = u.searchParams.get("id");
1241
- if (!taskUuid) {
1242
- return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
1243
- }
1244
- const userId = getHeader(headers, "x-user-id");
1245
- if (!userId) {
1246
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
1247
- }
1248
- if (!isValidUUIDv4(userId)) {
1249
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
1250
- }
1251
- const deleted = await db.deleteTaskByUuid(taskUuid, userId);
1252
- if (!deleted) {
1253
- return {
1254
- status: 404,
1255
- body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } }
1256
- };
1257
- }
1258
- return {
1259
- status: 200,
1260
- body: {
1261
- success: true,
1262
- data: { uuid: taskUuid, message: "\u4EFB\u52A1\u5DF2\u6210\u529F\u53D6\u6D88", deletedAt: (/* @__PURE__ */ new Date()).toISOString() }
1263
- }
1264
- };
1265
- }
1266
- return { DELETE };
1267
- }
1268
-
1269
- // src/server/handlers/messages.js
1270
- function createMessagesHandler(ctx) {
1271
- async function GET(url, headers) {
1272
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1273
- if (!tenantResult.ok) {
1274
- return tenantResult.error;
1275
- }
1276
- const tenantCtx = tenantResult.context;
1277
- const db = tenantCtx.db;
1278
- const masterKey = tenantCtx.masterKey;
1279
- const userId = getHeader(headers, "x-user-id");
1280
- if (!userId) {
1281
- return {
1282
- status: 400,
1283
- body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u5FC5\u987B\u63D0\u4F9B X-User-Id \u8BF7\u6C42\u5934" } }
1284
- };
1285
- }
1286
- if (!isValidUUIDv4(userId)) {
1287
- return {
1288
- status: 400,
1289
- body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
1290
- };
1291
- }
1292
- const u = new URL(url, "https://dummy");
1293
- const status = u.searchParams.get("status") || "all";
1294
- const limit = Math.min(parseInt(u.searchParams.get("limit") || "20", 10), 100);
1295
- const offset = parseInt(u.searchParams.get("offset") || "0", 10);
1296
- if (isNaN(limit) || limit < 1) {
1297
- return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "limit \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u6B63\u6574\u6570" } } };
1298
- }
1299
- if (isNaN(offset) || offset < 0) {
1300
- return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
1301
- }
1302
- const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
1303
- const userKey = deriveUserEncryptionKey(userId, masterKey);
1304
- const decryptedTasks = tasks.map((task) => {
1305
- const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
1306
- return {
1307
- id: task.id,
1308
- uuid: task.uuid,
1309
- contactName: decrypted.contactName,
1310
- messageType: task.message_type,
1311
- messageSubtype: decrypted.messageSubtype,
1312
- nextSendAt: task.next_send_at,
1313
- recurrenceType: decrypted.recurrenceType,
1314
- status: task.status,
1315
- retryCount: task.retry_count,
1316
- createdAt: task.created_at,
1317
- updatedAt: task.updated_at
1318
- };
1319
- });
1320
- const responsePayload = {
1321
- tasks: decryptedTasks,
1322
- pagination: { total, limit, offset, hasMore: offset + limit < total }
1323
- };
1324
- const encryptedResponse = encryptPayload(responsePayload, userKey);
1325
- return {
1326
- status: 200,
1327
- body: {
1328
- success: true,
1329
- encrypted: true,
1330
- version: 1,
1331
- data: encryptedResponse
1332
- }
1333
- };
1334
- }
1335
- return { GET };
1336
- }
1337
-
1338
212
  // src/server/tenant/blob-store.js
1339
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
213
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
1340
214
  var inMemoryNamespaces = /* @__PURE__ */ new Map();
1341
215
  function base64UrlEncode(input) {
1342
216
  return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
@@ -1351,11 +225,11 @@ function getKekBuffer(kek) {
1351
225
  if (!value) {
1352
226
  throw new Error("[rei-standard-amsg-server] tenant.kek is required");
1353
227
  }
1354
- return createHash2("sha256").update(value).digest();
228
+ return createHash("sha256").update(value).digest();
1355
229
  }
1356
230
  function encryptConfig(config, kekBuffer) {
1357
- const iv = randomBytes2(12);
1358
- const cipher = createCipheriv2("aes-256-gcm", kekBuffer, iv);
231
+ const iv = randomBytes(12);
232
+ const cipher = createCipheriv("aes-256-gcm", kekBuffer, iv);
1359
233
  const plaintext = Buffer.from(JSON.stringify(config), "utf8");
1360
234
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1361
235
  const authTag = cipher.getAuthTag();
@@ -1369,7 +243,7 @@ function decryptConfig(encrypted, kekBuffer) {
1369
243
  const iv = base64UrlDecode(parts[1]);
1370
244
  const authTag = base64UrlDecode(parts[2]);
1371
245
  const ciphertext = base64UrlDecode(parts[3]);
1372
- const decipher = createDecipheriv2("aes-256-gcm", kekBuffer, iv);
246
+ const decipher = createDecipheriv("aes-256-gcm", kekBuffer, iv);
1373
247
  decipher.setAuthTag(authTag);
1374
248
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
1375
249
  return JSON.parse(plaintext);
@@ -1431,7 +305,7 @@ function createTenantBlobStore(options) {
1431
305
  }
1432
306
 
1433
307
  // src/server/tenant/context.js
1434
- import { createHash as createHash3, randomBytes as randomBytes3, randomUUID as randomUUID3 } from "crypto";
308
+ import { createHash as createHash2, randomBytes as randomBytes2, randomUUID } from "crypto";
1435
309
 
1436
310
  // src/server/tenant/token.js
1437
311
  import { createHmac, timingSafeEqual } from "crypto";
@@ -1525,7 +399,7 @@ function extractBearerToken(headers = {}) {
1525
399
 
1526
400
  // src/server/tenant/context.js
1527
401
  function fingerprint(masterKey) {
1528
- return createHash3("sha256").update(masterKey).digest("hex").slice(0, 16);
402
+ return createHash2("sha256").update(masterKey).digest("hex").slice(0, 16);
1529
403
  }
1530
404
  function sanitizeDbConfig(driver, connectionString) {
1531
405
  const normalizedDriver = String(driver || "").trim();
@@ -1571,14 +445,14 @@ function createTenantContextManager(options) {
1571
445
  }
1572
446
  async function initializeTenant({ driver, connectionString, tenantId: providedTenantId }) {
1573
447
  const dbConfig = sanitizeDbConfig(driver, connectionString);
1574
- const tenantId = String(providedTenantId || randomUUID3()).trim();
448
+ const tenantId = String(providedTenantId || randomUUID()).trim();
1575
449
  const existing = await tenantStore.getTenantConfig(tenantId);
1576
450
  if (existing) {
1577
451
  throw new Error("TENANT_ALREADY_INITIALIZED");
1578
452
  }
1579
453
  const adapter = await getOrCreateAdapter(dbConfig);
1580
454
  await adapter.initSchema();
1581
- const masterKey = randomBytes3(32).toString("hex");
455
+ const masterKey = randomBytes2(32).toString("hex");
1582
456
  const now = (/* @__PURE__ */ new Date()).toISOString();
1583
457
  await tenantStore.setTenantConfig({
1584
458
  tenantId,
@@ -1654,11 +528,7 @@ function createTenantContextManager(options) {
1654
528
  }
1655
529
 
1656
530
  // src/server/index.js
1657
- function normalizeVapidSubject(email) {
1658
- const trimmedEmail = String(email || "").trim();
1659
- if (!trimmedEmail) return "";
1660
- return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
1661
- }
531
+ import { normalizeVapidSubject } from "@rei-standard/amsg-shared";
1662
532
  async function createReiServer(config) {
1663
533
  if (!config) throw new Error("[rei-standard-amsg-server] config is required");
1664
534
  if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
@@ -1716,8 +586,12 @@ async function createReiServer(config) {
1716
586
  }
1717
587
  export {
1718
588
  createAdapter,
589
+ createD1Adapter,
1719
590
  createReiServer,
591
+ createSingleUserCloudflareWorker,
592
+ createSingleUserServer,
1720
593
  createTenantToken,
594
+ createWebCryptoWebPush,
1721
595
  decryptFromStorage,
1722
596
  decryptPayload,
1723
597
  deriveUserEncryptionKey,
@@ -1726,6 +600,7 @@ export {
1726
600
  isValidUUID,
1727
601
  isValidUUIDv4,
1728
602
  isValidUrl,
603
+ runScheduledTick,
1729
604
  validateAvatarUrl,
1730
605
  validateLlmMessagesArray,
1731
606
  validateScheduleMessagePayload,