@rei-standard/amsg-server 2.6.0-next.9 → 3.0.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +822 -41
  3. package/dist/adapters/d1.d.ts +693 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +384 -0
  6. package/dist/adapters/neon.d.ts +105 -0
  7. package/dist/adapters/pg-shared.d.ts +194 -0
  8. package/dist/adapters/pg.d.ts +101 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +80 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
  13. package/dist/chunk-RL7V7VSS.mjs +8784 -0
  14. package/dist/chunk-TKDISKCF.cjs +163 -0
  15. package/dist/chunk-XKUWMYVF.mjs +163 -0
  16. package/dist/chunk-YBDQJVFQ.cjs +8784 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +39 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +11 -2
  20. package/dist/cloudflare.mjs +36 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state-namespaces.d.ts +5 -0
  24. package/dist/handlers/client-state.d.ts +7 -0
  25. package/dist/handlers/cloud-data.d.ts +4 -0
  26. package/dist/handlers/get-message.d.ts +3 -0
  27. package/dist/handlers/get-user-key.d.ts +3 -0
  28. package/dist/handlers/init-tenant.d.ts +23 -0
  29. package/dist/handlers/llm-credentials.d.ts +5 -0
  30. package/dist/handlers/messages.d.ts +3 -0
  31. package/dist/handlers/outbox.d.ts +9 -0
  32. package/dist/handlers/push-subscription.d.ts +5 -0
  33. package/dist/handlers/schedule-message.d.ts +3 -0
  34. package/dist/handlers/send-notifications.d.ts +3 -0
  35. package/dist/handlers/single-user-init.d.ts +13 -0
  36. package/dist/handlers/update-message.d.ts +3 -0
  37. package/dist/handlers/vapid-public-key.d.ts +19 -0
  38. package/dist/index.cjs +133 -44
  39. package/dist/index.d.cts +145 -766
  40. package/dist/index.d.ts +145 -766
  41. package/dist/index.mjs +125 -36
  42. package/dist/lib/agentic-fire.d.ts +177 -0
  43. package/dist/lib/client-state-store.d.ts +88 -0
  44. package/dist/lib/cloud-data-cleanup.d.ts +36 -0
  45. package/dist/lib/cloud-data-guard.d.ts +14 -0
  46. package/dist/lib/cloud-data-inventory.d.ts +15 -0
  47. package/dist/lib/constant-time.d.ts +14 -0
  48. package/dist/lib/db-errors.d.ts +10 -0
  49. package/dist/lib/encryption.d.ts +48 -0
  50. package/dist/lib/errors.d.ts +273 -0
  51. package/dist/lib/llm-credentials-store.d.ts +147 -0
  52. package/dist/lib/llm.d.ts +12 -0
  53. package/dist/lib/message-processor.d.ts +82 -0
  54. package/dist/lib/outbox-store.d.ts +162 -0
  55. package/dist/lib/push-policy.d.ts +11 -0
  56. package/dist/lib/push-subscription-store.d.ts +94 -0
  57. package/dist/lib/recurrence.d.ts +51 -0
  58. package/dist/lib/request.d.ts +182 -0
  59. package/dist/lib/result-emitter.d.ts +60 -0
  60. package/dist/lib/retry-policy.d.ts +11 -0
  61. package/dist/lib/run-tick.d.ts +121 -0
  62. package/dist/lib/schema-version.d.ts +50 -0
  63. package/dist/lib/state-accessors.d.ts +70 -0
  64. package/dist/lib/state-chunks.d.ts +65 -0
  65. package/dist/lib/task-projection.d.ts +92 -0
  66. package/dist/lib/validation.d.ts +97 -0
  67. package/dist/lib/version.d.ts +7 -0
  68. package/dist/lib/webcrypto-utils.d.ts +1 -0
  69. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  70. package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
  71. package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
  72. package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
  73. package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
  74. package/dist/single-user.d.ts +106 -0
  75. package/dist/tenant/blob-store.d.ts +30 -0
  76. package/dist/tenant/context.d.ts +47 -0
  77. package/dist/tenant/single-user-context.d.ts +29 -0
  78. package/dist/tenant/token.d.ts +26 -0
  79. package/package.json +3 -3
  80. package/dist/chunk-3QRVP4JE.cjs +0 -2834
  81. package/dist/chunk-DCKCAGFV.cjs +0 -81
  82. package/dist/chunk-S6O3MXQO.mjs +0 -2834
  83. package/dist/cloudflare-kUY6z-k5.d.cts +0 -4460
  84. package/dist/cloudflare-kUY6z-k5.d.ts +0 -4460
  85. package/dist/cloudflare.d.cts +0 -2
  86. package/dist/neon-D8Z2Nn97.d.cts +0 -295
  87. package/dist/neon-PqNFJIz2.d.ts +0 -295
  88. package/dist/pg-C48FwCWW.d.cts +0 -289
  89. package/dist/pg-C5gtWUfU.d.ts +0 -289
  90. package/dist/schema-BHuyH_6Z.d.cts +0 -88
  91. package/dist/schema-BHuyH_6Z.d.ts +0 -88
@@ -1,2834 +0,0 @@
1
- // src/server/lib/validation.js
2
- import { validateAvatarUrl } from "@rei-standard/amsg-shared";
3
- function isValidISO8601(dateString) {
4
- const date = new Date(dateString);
5
- return date instanceof Date && !isNaN(date.getTime());
6
- }
7
- function isValidUrl(urlString) {
8
- try {
9
- new URL(urlString);
10
- return true;
11
- } catch {
12
- return false;
13
- }
14
- }
15
- function isValidUUID(uuid) {
16
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
- return uuidRegex.test(uuid);
18
- }
19
- function isValidUUIDv4(uuid) {
20
- 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;
21
- return uuidV4Regex.test(uuid);
22
- }
23
- var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
24
- var SPLIT_PATTERN_MAX_LENGTH = 200;
25
- var SPLIT_PATTERN_MAX_ITEMS = 10;
26
- function validateSplitPattern(value) {
27
- if (value === void 0 || value === null) return null;
28
- const isArray = Array.isArray(value);
29
- const items = isArray ? value : [value];
30
- if (isArray && items.length === 0) return null;
31
- if (items.length > SPLIT_PATTERN_MAX_ITEMS) {
32
- return `splitPattern \u6570\u7EC4\u6700\u591A ${SPLIT_PATTERN_MAX_ITEMS} \u9879`;
33
- }
34
- for (let i = 0; i < items.length; i++) {
35
- const s = items[i];
36
- const label = isArray ? `splitPattern[${i}]` : "splitPattern";
37
- if (typeof s !== "string") return `${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`;
38
- if (s.length > SPLIT_PATTERN_MAX_LENGTH) {
39
- return `${label} \u4E0D\u80FD\u8D85\u8FC7 ${SPLIT_PATTERN_MAX_LENGTH} \u5B57\u7B26`;
40
- }
41
- try {
42
- new RegExp(s);
43
- } catch (_) {
44
- return `${label} \u4E0D\u662F\u6709\u6548\u6B63\u5219\u8868\u8FBE\u5F0F`;
45
- }
46
- }
47
- return null;
48
- }
49
- function validateLlmMessagesArray(messages) {
50
- if (!Array.isArray(messages) || messages.length === 0) {
51
- return "messages must be a non-empty array";
52
- }
53
- for (let i = 0; i < messages.length; i++) {
54
- const m = messages[i];
55
- if (!m || typeof m !== "object" || Array.isArray(m)) {
56
- return `messages[${i}] must be an object`;
57
- }
58
- if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
59
- return `messages[${i}].role must be one of system / user / assistant / tool`;
60
- }
61
- if (typeof m.content === "string") {
62
- if (!m.content) return `messages[${i}].content must be a non-empty string`;
63
- } else if (Array.isArray(m.content)) {
64
- if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
65
- } else {
66
- return `messages[${i}].content must be a non-empty string or a non-empty array`;
67
- }
68
- }
69
- return null;
70
- }
71
- function validateScheduleMessagePayload(payload) {
72
- if (!payload.contactName || typeof payload.contactName !== "string") {
73
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
74
- }
75
- if (!payload.messageType || !["fixed", "prompted", "auto", "instant"].includes(payload.messageType)) {
76
- return { valid: false, errorCode: "INVALID_MESSAGE_TYPE", errorMessage: "\u6D88\u606F\u7C7B\u578B\u65E0\u6548", details: { providedType: payload.messageType, allowedTypes: ["fixed", "prompted", "auto", "instant"] } };
77
- }
78
- if (!payload.firstSendTime || !isValidISO8601(payload.firstSendTime)) {
79
- return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u683C\u5F0F\u65E0\u6548", details: { field: "firstSendTime" } };
80
- }
81
- if (payload.firstSendTime && new Date(payload.firstSendTime) <= /* @__PURE__ */ new Date()) {
82
- return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u5FC5\u987B\u5728\u672A\u6765", details: { field: "firstSendTime", reason: "must be in the future" } };
83
- }
84
- if (!payload.pushSubscription || typeof payload.pushSubscription !== "object") {
85
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["pushSubscription"] } };
86
- }
87
- if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
88
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
89
- }
90
- if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
91
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
92
- }
93
- if (payload.messageType === "fixed") {
94
- if (!payload.userMessage) {
95
- 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)"] } };
96
- }
97
- }
98
- const promptCheck = (() => {
99
- const hasCompletePrompt = payload.completePrompt !== void 0 && payload.completePrompt !== null && payload.completePrompt !== "";
100
- const hasMessages = payload.messages !== void 0 && payload.messages !== null;
101
- if (hasCompletePrompt && hasMessages) {
102
- return {
103
- 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"] } },
104
- hasCompletePrompt: true,
105
- hasMessages: true
106
- };
107
- }
108
- if (hasMessages) {
109
- const err2 = validateLlmMessagesArray(payload.messages);
110
- if (err2) {
111
- return {
112
- error: { code: "INVALID_PARAMETERS", message: err2, details: { invalidFields: ["messages"] } },
113
- hasCompletePrompt: false,
114
- hasMessages: true
115
- };
116
- }
117
- }
118
- return { error: null, hasCompletePrompt, hasMessages };
119
- })();
120
- if (promptCheck.error) {
121
- return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
122
- }
123
- const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
124
- if (payload.messageType === "prompted" || payload.messageType === "auto") {
125
- const missingAiFields = [];
126
- if (!hasPrompt) missingAiFields.push("completePrompt or messages");
127
- if (!payload.apiUrl) missingAiFields.push("apiUrl");
128
- if (!payload.apiKey) missingAiFields.push("apiKey");
129
- if (!payload.primaryModel) missingAiFields.push("primaryModel");
130
- if (missingAiFields.length > 0) {
131
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: missingAiFields } };
132
- }
133
- }
134
- if (payload.messageType === "instant") {
135
- if (payload.recurrenceType && payload.recurrenceType !== "none") {
136
- 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)'] } };
137
- }
138
- const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
139
- const hasUserMessage = payload.userMessage;
140
- if (!hasAiConfig && !hasUserMessage) {
141
- 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)"] } };
142
- }
143
- }
144
- if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
145
- 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)"] } };
146
- }
147
- const avatarErr = validateAvatarUrl(payload.avatarUrl);
148
- if (avatarErr) {
149
- console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
150
- payload.avatarUrl = null;
151
- }
152
- if (payload.uuid && !isValidUUID(payload.uuid)) {
153
- 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)"] } };
154
- }
155
- if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
156
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
157
- }
158
- const splitErr = validateSplitPattern(payload.splitPattern);
159
- if (splitErr) {
160
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: splitErr, details: { invalidFields: ["splitPattern"] } };
161
- }
162
- return { valid: true };
163
- }
164
-
165
- // src/server/lib/webcrypto-utils.js
166
- import { toUint8, concatBytes, base64UrlToBytes } from "@rei-standard/amsg-shared";
167
- var TEXT_ENCODER = new TextEncoder();
168
- var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
169
- function utf8(str) {
170
- return TEXT_ENCODER.encode(String(str));
171
- }
172
- function utf8Decode(buf) {
173
- return TEXT_DECODER.decode(toUint8(buf));
174
- }
175
- function bytesToBase64(buf) {
176
- const bytes = toUint8(buf);
177
- let bin = "";
178
- for (let i = 0; i < bytes.length; i++) {
179
- bin += String.fromCharCode(bytes[i]);
180
- }
181
- return typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
182
- }
183
- function bytesToBase64Url(buf) {
184
- return bytesToBase64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
185
- }
186
- function bytesToHex(buf) {
187
- const bytes = toUint8(buf);
188
- let out = "";
189
- for (let i = 0; i < bytes.length; i++) {
190
- out += bytes[i].toString(16).padStart(2, "0");
191
- }
192
- return out;
193
- }
194
- function hexToBytes(hex) {
195
- const out = new Uint8Array(hex.length / 2);
196
- for (let i = 0; i < out.length; i++) {
197
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
198
- }
199
- return out;
200
- }
201
- function jsonToBase64Url(value) {
202
- return bytesToBase64Url(utf8(JSON.stringify(value)));
203
- }
204
- function randomUUID() {
205
- return globalThis.crypto.randomUUID();
206
- }
207
- function randomBytes(n) {
208
- const out = new Uint8Array(n);
209
- globalThis.crypto.getRandomValues(out);
210
- return out;
211
- }
212
-
213
- // src/server/lib/encryption.js
214
- var base64ToBytes = base64UrlToBytes;
215
- var TAG_LENGTH_BYTES = 16;
216
- function importAesKey(hexKey, usage) {
217
- return globalThis.crypto.subtle.importKey(
218
- "raw",
219
- hexToBytes(hexKey),
220
- { name: "AES-GCM" },
221
- false,
222
- [usage]
223
- );
224
- }
225
- async function aesGcmSeal(hexKey, iv, plaintextBytes) {
226
- const key = await importAesKey(hexKey, "encrypt");
227
- const sealed = new Uint8Array(await globalThis.crypto.subtle.encrypt(
228
- { name: "AES-GCM", iv, tagLength: TAG_LENGTH_BYTES * 8 },
229
- key,
230
- plaintextBytes
231
- ));
232
- return {
233
- ciphertext: sealed.slice(0, sealed.length - TAG_LENGTH_BYTES),
234
- authTag: sealed.slice(sealed.length - TAG_LENGTH_BYTES)
235
- };
236
- }
237
- async function aesGcmOpen(hexKey, iv, ciphertext, authTag) {
238
- const key = await importAesKey(hexKey, "decrypt");
239
- const tagBits = authTag.length * 8;
240
- const tagLength = tagBits >= 96 && tagBits <= 128 ? tagBits : 128;
241
- const plain = await globalThis.crypto.subtle.decrypt(
242
- { name: "AES-GCM", iv, tagLength },
243
- key,
244
- concatBytes(ciphertext, authTag)
245
- );
246
- return new Uint8Array(plain);
247
- }
248
- async function deriveUserEncryptionKey(userId, masterKey) {
249
- const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8(masterKey + userId));
250
- return bytesToHex(new Uint8Array(digest)).slice(0, 64);
251
- }
252
- async function decryptPayload(encryptedPayload, encryptionKey) {
253
- const { iv, authTag, encryptedData } = encryptedPayload;
254
- const plain = await aesGcmOpen(
255
- encryptionKey,
256
- base64ToBytes(iv),
257
- base64ToBytes(encryptedData),
258
- base64ToBytes(authTag)
259
- );
260
- return JSON.parse(utf8Decode(plain));
261
- }
262
- async function encryptPayload(payload, encryptionKey) {
263
- const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
264
- const iv = randomBytes(12);
265
- const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(plaintext));
266
- return {
267
- iv: bytesToBase64(iv),
268
- authTag: bytesToBase64(authTag),
269
- encryptedData: bytesToBase64(ciphertext)
270
- };
271
- }
272
- async function encryptForStorage(text, encryptionKey) {
273
- const iv = randomBytes(16);
274
- const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(text));
275
- return `${bytesToHex(iv)}:${bytesToHex(authTag)}:${bytesToHex(ciphertext)}`;
276
- }
277
- async function decryptFromStorage(encryptedText, encryptionKey) {
278
- const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
279
- const plain = await aesGcmOpen(
280
- encryptionKey,
281
- hexToBytes(ivHex),
282
- hexToBytes(encryptedDataHex),
283
- hexToBytes(authTagHex)
284
- );
285
- return utf8Decode(plain);
286
- }
287
-
288
- // src/server/lib/request.js
289
- var REQUEST_ERRORS = {
290
- INVALID_JSON: { code: "INVALID_JSON", message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548\u7684 JSON" },
291
- INVALID_REQUEST_BODY: { code: "INVALID_REQUEST_BODY", message: "\u8BF7\u6C42\u4F53\u683C\u5F0F\u65E0\u6548" },
292
- INVALID_ENCRYPTED_PAYLOAD: { code: "INVALID_ENCRYPTED_PAYLOAD", message: "\u52A0\u5BC6\u6570\u636E\u683C\u5F0F\u9519\u8BEF" }
293
- };
294
- function parseBodyAsObject(body, options = {}) {
295
- const invalidJson = options.invalidJson || REQUEST_ERRORS.INVALID_JSON;
296
- const invalidType = options.invalidType || REQUEST_ERRORS.INVALID_REQUEST_BODY;
297
- let parsed = body;
298
- if (typeof parsed === "string") {
299
- try {
300
- parsed = JSON.parse(parsed);
301
- } catch {
302
- return { ok: false, error: invalidJson };
303
- }
304
- }
305
- if (!isPlainObject(parsed)) {
306
- return { ok: false, error: invalidType };
307
- }
308
- return { ok: true, data: parsed };
309
- }
310
- function parseJsonBody(body) {
311
- return parseBodyAsObject(body, {
312
- invalidJson: REQUEST_ERRORS.INVALID_JSON,
313
- invalidType: REQUEST_ERRORS.INVALID_REQUEST_BODY
314
- });
315
- }
316
- function isPlainObject(value) {
317
- return typeof value === "object" && value !== null && !Array.isArray(value);
318
- }
319
- function isEncryptedEnvelope(payload) {
320
- if (!isPlainObject(payload)) return false;
321
- return typeof payload.iv === "string" && typeof payload.authTag === "string" && typeof payload.encryptedData === "string";
322
- }
323
- function parseEncryptedBody(body) {
324
- const parsedBody = parseBodyAsObject(body, {
325
- invalidJson: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD,
326
- invalidType: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD
327
- });
328
- if (!parsedBody.ok) {
329
- return parsedBody;
330
- }
331
- if (!isEncryptedEnvelope(parsedBody.data)) {
332
- return { ok: false, error: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD };
333
- }
334
- return parsedBody;
335
- }
336
- function getHeader(headers = {}, name) {
337
- if (!headers || typeof headers !== "object") {
338
- return "";
339
- }
340
- const lowerName = String(name || "").toLowerCase();
341
- if (!lowerName) return "";
342
- for (const [key, value] of Object.entries(headers)) {
343
- if (String(key).toLowerCase() === lowerName) {
344
- return String(value || "").trim();
345
- }
346
- }
347
- return "";
348
- }
349
-
350
- // src/server/handlers/get-user-key.js
351
- function createGetUserKeyHandler(ctx) {
352
- async function GET(url, headers) {
353
- const effectiveHeaders = headers || url || {};
354
- const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
355
- if (!tenantResult.ok) {
356
- return tenantResult.error;
357
- }
358
- const { masterKey } = tenantResult.context;
359
- const userId = getHeader(effectiveHeaders, "x-user-id");
360
- if (!userId) {
361
- return {
362
- status: 400,
363
- body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
364
- };
365
- }
366
- if (!isValidUUIDv4(userId)) {
367
- return {
368
- status: 400,
369
- body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
370
- };
371
- }
372
- return {
373
- status: 200,
374
- body: {
375
- success: true,
376
- data: {
377
- userKey: await deriveUserEncryptionKey(userId, masterKey),
378
- version: 1
379
- }
380
- }
381
- };
382
- }
383
- return { GET };
384
- }
385
-
386
- // src/server/lib/db-errors.js
387
- function isUniqueViolation(error) {
388
- if (!error || typeof error !== "object") return false;
389
- const code = error.code;
390
- if (code === "23505") return true;
391
- const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
392
- return message.includes("duplicate key") || message.includes("unique constraint");
393
- }
394
-
395
- // src/server/lib/message-processor.js
396
- import {
397
- buildContentPush,
398
- buildReasoningPush,
399
- readReasoningContent,
400
- stripReasoningTags
401
- } from "@rei-standard/amsg-shared";
402
-
403
- // src/server/lib/llm.js
404
- async function callLlm(payload, options = {}) {
405
- const requireContent = options.requireContent !== false;
406
- const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 3e5;
407
- const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
408
- const requestBody = buildAiRequestBody(payload);
409
- const aiResponse = await fetch(normalizedApiUrl, {
410
- method: "POST",
411
- headers: {
412
- "Content-Type": "application/json",
413
- "Authorization": `Bearer ${payload.apiKey}`
414
- },
415
- body: JSON.stringify(requestBody),
416
- signal: AbortSignal.timeout(timeoutMs)
417
- });
418
- if (!aiResponse.ok) {
419
- if (aiResponse.status === 405) {
420
- throw new Error(
421
- `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
422
- );
423
- }
424
- throw new Error(
425
- `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
426
- );
427
- }
428
- const aiData = await aiResponse.json();
429
- const rawContent = aiData?.choices?.[0]?.message?.content;
430
- if (requireContent && (typeof rawContent !== "string" || !rawContent.trim())) {
431
- throw new Error("AI API error: response missing choices[0].message.content");
432
- }
433
- return { response: aiData, content: typeof rawContent === "string" ? rawContent.trim() : "" };
434
- }
435
- function buildAiRequestBody(payload) {
436
- const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
437
- const requestBody = {
438
- model: payload.primaryModel,
439
- messages: llmMessages
440
- };
441
- if (payload.temperature !== void 0 && payload.temperature !== null) {
442
- requestBody.temperature = payload.temperature;
443
- } else if (!Array.isArray(payload.messages)) {
444
- requestBody.temperature = 0.8;
445
- }
446
- if (Array.isArray(payload.tools) && payload.tools.length > 0) {
447
- requestBody.tools = payload.tools;
448
- if (payload.toolChoice !== void 0 && payload.toolChoice !== null) {
449
- requestBody.tool_choice = payload.toolChoice;
450
- }
451
- }
452
- if (payload.maxTokens === void 0 || payload.maxTokens === null) {
453
- return requestBody;
454
- }
455
- if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
456
- throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
457
- }
458
- requestBody.max_tokens = payload.maxTokens;
459
- return requestBody;
460
- }
461
- function normalizeAiApiUrl(apiUrl) {
462
- if (typeof apiUrl !== "string" || !apiUrl.trim()) {
463
- throw new Error(
464
- "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)."
465
- );
466
- }
467
- const trimmedApiUrl = apiUrl.trim();
468
- let parsedUrl;
469
- try {
470
- parsedUrl = new URL(trimmedApiUrl);
471
- } catch {
472
- throw new Error(
473
- `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
474
- );
475
- }
476
- let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
477
- if (/\/chat\/completions$/.test(path)) {
478
- } else if (path === "/") {
479
- path = "/v1/chat/completions";
480
- } else if (/\/v\d+$/.test(path)) {
481
- path = `${path}/chat/completions`;
482
- }
483
- parsedUrl.pathname = path;
484
- return parsedUrl.toString();
485
- }
486
-
487
- // src/server/lib/agentic-fire.js
488
- import {
489
- assertValidDecision,
490
- buildSessionContext,
491
- extractAssistantMessage,
492
- extractToolCallsFromDecision
493
- } from "@rei-standard/amsg-shared";
494
-
495
- // src/server/lib/state-chunks.js
496
- import { chunkReasoningByUtf8Bytes } from "@rei-standard/amsg-shared";
497
- var STATE_CHUNK_SLICE_BYTES = 200 * 1024;
498
- var DEFAULT_MAX_STATE_VALUE_BYTES = 5 * 1024 * 1024;
499
- var INTERNAL_STATE_CHAR_RE = /[\u0000-\u001f]/;
500
- var SEP = "";
501
- var CHUNK_NS_PREFIX = `${SEP}amsg-chunks${SEP}`;
502
- var ROOT_MARKER_PREFIX = `${SEP}amsg-chunked${SEP}v1${SEP}`;
503
- function chunkNamespaceFor(namespace) {
504
- return CHUNK_NS_PREFIX + namespace;
505
- }
506
- function chunkKeyFor(key, index) {
507
- return `${key}${SEP}${index}`;
508
- }
509
- function chunkKeyPrefixFor(key) {
510
- return `${key}${SEP}`;
511
- }
512
- function buildChunkedRootValue(chunkCount) {
513
- return `${ROOT_MARKER_PREFIX}${chunkCount}`;
514
- }
515
- function parseChunkedRootCount(value) {
516
- if (typeof value !== "string" || !value.startsWith(ROOT_MARKER_PREFIX)) return null;
517
- const raw = value.slice(ROOT_MARKER_PREFIX.length);
518
- if (!/^[1-9][0-9]*$/.test(raw)) return null;
519
- return Number(raw);
520
- }
521
- function splitStateValue(value) {
522
- return chunkReasoningByUtf8Bytes(value, STATE_CHUNK_SLICE_BYTES);
523
- }
524
- async function resolveClientStateEntries(rows, fetchChunkRows, decryptValue) {
525
- let chunkMap = null;
526
- const loadChunks = async () => {
527
- if (chunkMap === null) {
528
- const chunkRows = await fetchChunkRows();
529
- chunkMap = new Map(chunkRows.map((row) => [row.key, row]));
530
- }
531
- return chunkMap;
532
- };
533
- const entries = [];
534
- for (const row of rows) {
535
- const count = parseChunkedRootCount(row.value);
536
- if (count === null) {
537
- entries.push({
538
- namespace: row.namespace,
539
- key: row.key,
540
- value: await decryptValue(row.value),
541
- updatedAt: row.updated_at
542
- });
543
- continue;
544
- }
545
- const map = await loadChunks();
546
- const chunkRows = [];
547
- let intact = true;
548
- for (let i = 0; i < count; i++) {
549
- const chunk = map.get(chunkKeyFor(row.key, i));
550
- if (!chunk || chunk.updated_at !== row.updated_at) {
551
- intact = false;
552
- break;
553
- }
554
- chunkRows.push(chunk);
555
- }
556
- if (!intact) continue;
557
- const parts = await Promise.all(chunkRows.map((chunk) => decryptValue(chunk.value)));
558
- entries.push({
559
- namespace: row.namespace,
560
- key: row.key,
561
- value: parts.join(""),
562
- updatedAt: row.updated_at
563
- });
564
- }
565
- return entries;
566
- }
567
-
568
- // src/server/lib/client-state-store.js
569
- var utf82 = new TextEncoder();
570
- var MAX_STATE_ENTRIES_PER_BATCH = 200;
571
- var MAX_NAMESPACE_CHARS = 128;
572
- var MAX_KEY_CHARS = 256;
573
- function stateValueBytes(value) {
574
- return utf82.encode(value).length;
575
- }
576
- async function writeClientStateEntries({ db, userId, userKey, entries }) {
577
- const physicalRows = [];
578
- const cleanups = [];
579
- const rootRowIndexes = [];
580
- let deleted = 0;
581
- for (const entry of entries) {
582
- cleanups.push({
583
- namespace: chunkNamespaceFor(entry.namespace),
584
- keyPrefix: chunkKeyPrefixFor(entry.key),
585
- updatedAt: entry.updatedAt
586
- });
587
- if (entry.value === null) {
588
- cleanups.push({
589
- namespace: entry.namespace,
590
- key: entry.key,
591
- updatedAt: entry.updatedAt
592
- });
593
- deleted++;
594
- continue;
595
- }
596
- rootRowIndexes.push(physicalRows.length);
597
- if (stateValueBytes(entry.value) <= STATE_CHUNK_SLICE_BYTES) {
598
- physicalRows.push({
599
- namespace: entry.namespace,
600
- key: entry.key,
601
- value: await encryptForStorage(entry.value, userKey),
602
- updatedAt: entry.updatedAt
603
- });
604
- } else {
605
- const slices = splitStateValue(entry.value);
606
- physicalRows.push({
607
- namespace: entry.namespace,
608
- key: entry.key,
609
- value: buildChunkedRootValue(slices.length),
610
- updatedAt: entry.updatedAt
611
- });
612
- const encryptedSlices = await Promise.all(slices.map((slice) => encryptForStorage(slice, userKey)));
613
- for (let c = 0; c < encryptedSlices.length; c++) {
614
- physicalRows.push({
615
- namespace: chunkNamespaceFor(entry.namespace),
616
- key: chunkKeyFor(entry.key, c),
617
- value: encryptedSlices[c],
618
- updatedAt: entry.updatedAt
619
- });
620
- }
621
- }
622
- }
623
- if (physicalRows.length === 0 && cleanups.length === 0) {
624
- return { upserted: 0, skipped: 0, deleted: 0 };
625
- }
626
- const result = await db.upsertClientState(userId, physicalRows, cleanups);
627
- let upserted = 0;
628
- let skipped = 0;
629
- if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
630
- for (const rootIndex of rootRowIndexes) {
631
- if (result.outcomes[rootIndex]) upserted++;
632
- else skipped++;
633
- }
634
- } else {
635
- upserted = result.upserted;
636
- skipped = result.skipped;
637
- }
638
- return { upserted, skipped, deleted };
639
- }
640
-
641
- // src/server/lib/agentic-fire.js
642
- var DEFAULT_MAX_TOOL_ITERATIONS = 5;
643
- var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
644
- var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
645
- var MIN_SCHEDULE_LEAD_MS = 6e4;
646
- var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
647
- var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
648
- var SLEEP_BETWEEN_MESSAGES_MS = 1500;
649
- var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
650
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
651
- function taskNeedsLlm(decryptedPayload) {
652
- const type = decryptedPayload.messageType;
653
- if (type === "prompted" || type === "auto") return true;
654
- if (type === "instant") {
655
- return !!(decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel);
656
- }
657
- return false;
658
- }
659
- function buildHookTask(task, decryptedPayload) {
660
- const safe = {};
661
- for (const [key, value] of Object.entries(decryptedPayload)) {
662
- if (!CREDENTIAL_PAYLOAD_KEYS.has(key)) safe[key] = value;
663
- }
664
- return Object.freeze({
665
- ...safe,
666
- id: task.id ?? null,
667
- uuid: task.uuid ?? null,
668
- nextSendAt: task.next_send_at ?? null,
669
- retryCount: task.retry_count ?? 0
670
- });
671
- }
672
- function normalizeBeforeFireResult(result) {
673
- if (Array.isArray(result)) {
674
- return { messages: result };
675
- }
676
- if (result && typeof result === "object" && Array.isArray(result.messages)) {
677
- return {
678
- messages: result.messages,
679
- maxToolIterations: result.maxToolIterations,
680
- totalTimeoutMs: result.totalTimeoutMs,
681
- // 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
682
- // 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
683
- // buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
684
- tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
685
- toolChoice: result.toolChoice
686
- };
687
- }
688
- throw new TypeError(
689
- "AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
690
- );
691
- }
692
- function firstPositiveInt(values, fallback) {
693
- for (const v of values) {
694
- if (Number.isInteger(v) && v > 0) return v;
695
- }
696
- return fallback;
697
- }
698
- function firstPositiveNumber(values, fallback) {
699
- for (const v of values) {
700
- if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
701
- }
702
- return fallback;
703
- }
704
- async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
705
- const hooks = ctx.hooks;
706
- if (typeof hooks.onLLMOutput !== "function") {
707
- throw new Error("AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds");
708
- }
709
- const nowFn = typeof ctx._agenticNow === "function" ? ctx._agenticNow : Date.now;
710
- const sleep = typeof ctx._agenticSleep === "function" ? ctx._agenticSleep : defaultSleep;
711
- const readState = async (namespace) => {
712
- if (typeof namespace !== "string" || !namespace.trim()) {
713
- throw new TypeError("readState(namespace) requires a non-empty string");
714
- }
715
- if (!ctx.db || typeof ctx.db.getClientState !== "function") return [];
716
- const rows = await ctx.db.getClientState(task.user_id, namespace);
717
- return resolveClientStateEntries(
718
- rows,
719
- () => ctx.db.getClientState(task.user_id, chunkNamespaceFor(namespace)),
720
- (value) => decryptFromStorage(value, userKey)
721
- );
722
- };
723
- const maxStateValueBytes = Number.isInteger(ctx.maxStateValueBytes) && ctx.maxStateValueBytes > 0 ? ctx.maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
724
- const writeState = async (namespace, entries) => {
725
- if (typeof namespace !== "string" || !namespace.trim()) {
726
- throw new TypeError("writeState(namespace, entries) requires a non-empty string namespace");
727
- }
728
- if (namespace.length > MAX_NAMESPACE_CHARS || INTERNAL_STATE_CHAR_RE.test(namespace)) {
729
- throw new TypeError(
730
- `writeState: namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u4E14\u4E0D\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`
731
- );
732
- }
733
- if (!Array.isArray(entries)) {
734
- throw new TypeError("writeState(namespace, entries) requires an array of { key, value }");
735
- }
736
- if (entries.length === 0) return { upserted: 0, skipped: 0, deleted: 0 };
737
- if (entries.length > MAX_STATE_ENTRIES_PER_BATCH) {
738
- throw new RangeError(`writeState: \u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_BATCH} \u6761\uFF0C\u6536\u5230 ${entries.length} \u6761`);
739
- }
740
- if (!ctx.db || typeof ctx.db.upsertClientState !== "function") {
741
- throw new Error("AGENTIC_STATE_WRITE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state \u5199\u5165");
742
- }
743
- const now = nowFn();
744
- const normalized2 = entries.map((entry, index) => {
745
- if (!entry || typeof entry !== "object") {
746
- throw new TypeError(`writeState: entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
747
- }
748
- if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
749
- throw new TypeError(`writeState: entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
750
- }
751
- if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
752
- throw new TypeError(`writeState: entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
753
- }
754
- if (entry.value !== null && typeof entry.value !== "string") {
755
- throw new TypeError(`writeState: entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09\uFF0C\u6216 null \u8868\u793A\u5220\u9664`);
756
- }
757
- if (typeof entry.value === "string") {
758
- const bytes = stateValueBytes(entry.value);
759
- if (bytes > maxStateValueBytes) {
760
- throw new RangeError(`writeState: entries[${index}].value \u4E3A ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5355\u6761\u4E0A\u9650 ${maxStateValueBytes} \u5B57\u8282`);
761
- }
762
- }
763
- if (entry.updatedAt !== void 0 && (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0)) {
764
- throw new TypeError(`writeState: entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
765
- }
766
- return {
767
- namespace,
768
- key: entry.key,
769
- value: entry.value,
770
- updatedAt: entry.updatedAt ?? now
771
- };
772
- });
773
- return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
774
- };
775
- const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
776
- let scheduledTaskCount = 0;
777
- const scheduleTask = async (options) => {
778
- if (!options || typeof options !== "object" || Array.isArray(options)) {
779
- throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
780
- }
781
- if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
782
- throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
783
- }
784
- const firstSendAt = new Date(options.firstSendTime);
785
- if (Number.isNaN(firstSendAt.getTime())) {
786
- throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
787
- }
788
- const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
789
- if (firstSendAt.getTime() < earliest) {
790
- throw new RangeError(
791
- `scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
792
- );
793
- }
794
- const nextSendAt = firstSendAt.toISOString();
795
- const inheritedType = options.messageType == null;
796
- const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
797
- if (messageType === "instant") {
798
- throw new TypeError(
799
- "scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
800
- );
801
- }
802
- if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
803
- throw new TypeError(
804
- `scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
805
- );
806
- }
807
- const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
808
- if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
809
- throw new TypeError(
810
- `scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
811
- );
812
- }
813
- const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
814
- if (typeof contactName !== "string" || !contactName.trim()) {
815
- throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
816
- }
817
- const userMessage = options.userMessage === void 0 ? decryptedPayload.userMessage ?? null : options.userMessage;
818
- if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
819
- throw new TypeError(
820
- "scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
821
- );
822
- }
823
- const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
824
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
825
- throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
826
- }
827
- const uuid = options.uuid == null ? randomUUID() : options.uuid;
828
- if (typeof uuid !== "string" || !uuid.trim()) {
829
- throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
830
- }
831
- if (scheduledTaskCount >= maxScheduledTasksPerFire) {
832
- throw new RangeError(
833
- `scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
834
- );
835
- }
836
- if (!ctx.db || typeof ctx.db.createTask !== "function") {
837
- throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
838
- }
839
- scheduledTaskCount++;
840
- const fullTaskData = {
841
- contactName,
842
- avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
843
- messageType,
844
- messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
845
- userMessage: userMessage || null,
846
- firstSendTime: nextSendAt,
847
- recurrenceType,
848
- apiUrl: decryptedPayload.apiUrl || null,
849
- apiKey: decryptedPayload.apiKey || null,
850
- primaryModel: decryptedPayload.primaryModel || null,
851
- // 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
852
- // 任务走回老链路时顶替宿主的意图。
853
- completePrompt: null,
854
- messages: null,
855
- maxTokens: decryptedPayload.maxTokens ?? null,
856
- temperature: decryptedPayload.temperature ?? null,
857
- splitPattern: decryptedPayload.splitPattern ?? null,
858
- pushSubscription: decryptedPayload.pushSubscription,
859
- metadata
860
- };
861
- const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
862
- let created;
863
- try {
864
- created = await ctx.db.createTask({
865
- user_id: task.user_id,
866
- uuid,
867
- encrypted_payload: encryptedPayload,
868
- next_send_at: nextSendAt,
869
- message_type: messageType
870
- });
871
- } catch (error) {
872
- if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
873
- throw error;
874
- }
875
- if (!created) {
876
- throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
877
- }
878
- return {
879
- created: true,
880
- id: created.id ?? null,
881
- uuid: created.uuid ?? uuid,
882
- nextSendAt: created.next_send_at ?? nextSendAt
883
- };
884
- };
885
- const scratch = {};
886
- const fireCtx = Object.freeze({
887
- task: buildHookTask(task, decryptedPayload),
888
- userId: task.user_id,
889
- readState,
890
- writeState,
891
- scheduleTask,
892
- now: new Date(nowFn()),
893
- scratch
894
- });
895
- const before = await hooks.onBeforeFire(fireCtx);
896
- if (before == null) return { handled: false };
897
- if (typeof before === "object" && before.skip === true) {
898
- return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: 0 } };
899
- }
900
- const normalized = normalizeBeforeFireResult(before);
901
- const maxToolIterations = firstPositiveInt(
902
- [normalized.maxToolIterations, ctx.maxToolIterations],
903
- DEFAULT_MAX_TOOL_ITERATIONS
904
- );
905
- const totalTimeoutMs = firstPositiveNumber(
906
- [normalized.totalTimeoutMs, ctx.totalTimeoutMs],
907
- DEFAULT_TOTAL_TIMEOUT_MS
908
- );
909
- const deadline = nowFn() + totalTimeoutMs;
910
- const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
911
- let messages = normalized.messages.slice();
912
- for (let iteration = 0; iteration < maxToolIterations; iteration++) {
913
- if (nowFn() >= deadline) {
914
- throw new Error(`AGENTIC_TOTAL_TIMEOUT: fire chain exceeded ${totalTimeoutMs}ms after ${iteration} LLM round(s)`);
915
- }
916
- const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
917
- const { response: llmResponse } = await callLlm(
918
- {
919
- ...decryptedPayload,
920
- messages,
921
- ...normalized.tools ? { tools: normalized.tools, toolChoice: normalized.toolChoice } : {}
922
- },
923
- { requireContent: false, timeoutMs: roundTimeoutMs }
924
- );
925
- const assistantMessage = extractAssistantMessage(llmResponse);
926
- messages = [...messages, assistantMessage];
927
- const sessionCtx = Object.freeze({
928
- ...buildSessionContext({
929
- sessionId,
930
- messages,
931
- llmResponse,
932
- iteration,
933
- contactName: decryptedPayload.contactName,
934
- avatarUrl: decryptedPayload.avatarUrl || void 0,
935
- charId: decryptedPayload.charId,
936
- metadata: decryptedPayload.metadata,
937
- scratch
938
- }),
939
- readState,
940
- writeState,
941
- scheduleTask
942
- });
943
- const decision = await hooks.onLLMOutput(sessionCtx);
944
- assertValidDecision(decision, { inlineToolCalls: true });
945
- if (decision.decision === "continue") {
946
- messages = decision.nextHistory.slice();
947
- continue;
948
- }
949
- if (decision.decision === "skip-push") {
950
- return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: iteration + 1 } };
951
- }
952
- if (decision.decision === "finish") {
953
- const messagesSent = await sendHookPushPayloads(decision.pushPayloads, decryptedPayload, ctx, sessionId, task, sleep);
954
- return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
955
- }
956
- const toolCalls = extractToolCallsFromDecision(decision);
957
- if (toolCalls.length === 0) {
958
- throw new Error("AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)");
959
- }
960
- if (typeof hooks.executeToolCalls !== "function") {
961
- throw new Error("AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured");
962
- }
963
- if (iteration === maxToolIterations - 1) {
964
- break;
965
- }
966
- let toolResults;
967
- try {
968
- toolResults = await hooks.executeToolCalls(toolCalls, sessionCtx);
969
- if (!Array.isArray(toolResults)) {
970
- throw new TypeError('executeToolCalls must resolve to an array of { tool_call_id, role: "tool", content }');
971
- }
972
- } catch (error) {
973
- toolResults = toolCalls.map((toolCall) => ({
974
- tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
975
- role: "tool",
976
- content: `Tool execution failed: ${error?.message ?? String(error)}`
977
- }));
978
- }
979
- const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
980
- const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
981
- const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
982
- const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
983
- messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
984
- }
985
- throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
986
- }
987
- async function sendHookPushPayloads(pushPayloads, decryptedPayload, ctx, sessionId, task, sleep) {
988
- if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
989
- throw new Error("VAPID configuration missing - push notifications cannot be sent");
990
- }
991
- const pushSubscription = decryptedPayload.pushSubscription;
992
- const total = pushPayloads.length;
993
- const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}`;
994
- for (let i = 0; i < total; i++) {
995
- const push = { ...pushPayloads[i] };
996
- if (typeof push.messageId !== "string" || !push.messageId) push.messageId = `${messageIdBase}_hook_${i}`;
997
- if (typeof push.sessionId !== "string" || !push.sessionId) push.sessionId = sessionId;
998
- if (typeof push.timestamp !== "string" || !push.timestamp) push.timestamp = (/* @__PURE__ */ new Date()).toISOString();
999
- push.messageIndex = i + 1;
1000
- push.totalMessages = total;
1001
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(push));
1002
- if (i < total - 1) await sleep(SLEEP_BETWEEN_MESSAGES_MS);
1003
- }
1004
- return total;
1005
- }
1006
-
1007
- // src/server/lib/message-processor.js
1008
- var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
1009
- var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
1010
- function splitOnceByRegex(chunk, regex) {
1011
- const out = chunk.split(regex).reduce((acc, part, i, arr) => {
1012
- if (i % 2 === 0 && part.trim()) {
1013
- const punctuation = arr[i + 1] || "";
1014
- acc.push(part.trim() + punctuation);
1015
- }
1016
- return acc;
1017
- }, []).filter((s) => s.length > 0);
1018
- return out.length > 0 ? out : [chunk];
1019
- }
1020
- function splitMessageIntoSentences(messageContent, splitPattern = null) {
1021
- const sources = splitPattern == null ? null : Array.isArray(splitPattern) ? splitPattern : [splitPattern];
1022
- const regexes = sources && sources.length > 0 ? sources.map((s) => new RegExp(s)) : [DEFAULT_SPLIT_REGEX];
1023
- let chunks = [messageContent];
1024
- for (const regex of regexes) {
1025
- chunks = chunks.flatMap((c) => splitOnceByRegex(c, regex));
1026
- }
1027
- return chunks.length > 0 ? chunks : [messageContent];
1028
- }
1029
- async function processSingleMessage(task, ctx, providedMasterKey) {
1030
- try {
1031
- const masterKey = providedMasterKey || ctx.masterKey;
1032
- if (!masterKey) {
1033
- return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
1034
- }
1035
- const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
1036
- const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
1037
- if (ctx.hooks && typeof ctx.hooks.onBeforeFire === "function" && taskNeedsLlm(decryptedPayload)) {
1038
- const agentic = await runAgenticFire({ task, decryptedPayload, userKey, ctx });
1039
- if (agentic.handled) return agentic.result;
1040
- }
1041
- let messageContent;
1042
- let llmResponse = null;
1043
- if (decryptedPayload.messageType === "fixed") {
1044
- messageContent = decryptedPayload.userMessage;
1045
- } else if (decryptedPayload.messageType === "instant") {
1046
- const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
1047
- if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
1048
- const aiResult = await callLlm(decryptedPayload);
1049
- messageContent = aiResult.content;
1050
- llmResponse = aiResult.response;
1051
- } else if (decryptedPayload.userMessage) {
1052
- messageContent = decryptedPayload.userMessage;
1053
- } else {
1054
- throw new Error("Invalid instant message: no content source available");
1055
- }
1056
- } else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
1057
- const aiResult = await callLlm(decryptedPayload);
1058
- messageContent = aiResult.content;
1059
- llmResponse = aiResult.response;
1060
- } else {
1061
- throw new Error("Invalid message configuration: no content source available");
1062
- }
1063
- const reasoning = readReasoningContent(llmResponse);
1064
- if (reasoning) {
1065
- messageContent = stripReasoningTags(messageContent);
1066
- }
1067
- const messages = splitMessageIntoSentences(messageContent, decryptedPayload.splitPattern ?? null);
1068
- if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
1069
- throw new Error("VAPID configuration missing - push notifications cannot be sent");
1070
- }
1071
- const pushSubscription = decryptedPayload.pushSubscription;
1072
- const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
1073
- const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
1074
- const messageSubtype = decryptedPayload.messageSubtype || "chat";
1075
- const avatarUrl = decryptedPayload.avatarUrl || null;
1076
- const metadata = decryptedPayload.metadata || {};
1077
- const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
1078
- if (reasoning) {
1079
- const reasoningPush = buildReasoningPush({
1080
- messageType: decryptedPayload.messageType,
1081
- source,
1082
- messageId: `${messageIdBase}_reasoning`,
1083
- sessionId,
1084
- reasoningContent: reasoning,
1085
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1086
- title: `\u6765\u81EA ${decryptedPayload.contactName}`,
1087
- contactName: decryptedPayload.contactName,
1088
- avatarUrl,
1089
- messageSubtype,
1090
- metadata
1091
- });
1092
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
1093
- await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
1094
- }
1095
- for (let i = 0; i < messages.length; i++) {
1096
- const contentPush = buildContentPush({
1097
- messageType: decryptedPayload.messageType,
1098
- source,
1099
- messageId: `${messageIdBase}_${i}`,
1100
- sessionId,
1101
- message: messages[i],
1102
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1103
- title: `\u6765\u81EA ${decryptedPayload.contactName}`,
1104
- contactName: decryptedPayload.contactName,
1105
- avatarUrl,
1106
- messageSubtype,
1107
- messageIndex: i + 1,
1108
- totalMessages: messages.length,
1109
- taskId: task.id || null,
1110
- metadata
1111
- });
1112
- await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
1113
- if (i < messages.length - 1) {
1114
- await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
1115
- }
1116
- }
1117
- return { success: true, messagesSent: messages.length };
1118
- } catch (error) {
1119
- return { success: false, messagesSent: 0, error: error.message };
1120
- }
1121
- }
1122
- async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, providedMasterKey) {
1123
- let retryCount = 0;
1124
- const masterKey = providedMasterKey || ctx.masterKey;
1125
- if (!masterKey) {
1126
- return {
1127
- success: false,
1128
- error: { code: "TENANT_MASTER_KEY_MISSING", message: "\u79DF\u6237\u4E3B\u5BC6\u94A5\u4E0D\u5B58\u5728\u6216\u914D\u7F6E\u5F02\u5E38" }
1129
- };
1130
- }
1131
- while (retryCount <= maxRetries) {
1132
- let task;
1133
- try {
1134
- task = userId ? await ctx.db.getTaskByUuid(uuid, userId) : await ctx.db.getTaskByUuidOnly(uuid);
1135
- } catch (error) {
1136
- if (retryCount < maxRetries) {
1137
- retryCount++;
1138
- await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
1139
- continue;
1140
- }
1141
- return {
1142
- success: false,
1143
- error: { code: "INTERNAL_ERROR", message: error.message, retriesAttempted: retryCount }
1144
- };
1145
- }
1146
- if (!task) {
1147
- return { success: false, error: { code: "TASK_NOT_FOUND", message: "\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406" } };
1148
- }
1149
- const result = await processSingleMessage(task, ctx, masterKey);
1150
- if (!result.success) {
1151
- if (retryCount < maxRetries) {
1152
- retryCount++;
1153
- await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
1154
- continue;
1155
- }
1156
- try {
1157
- await ctx.db.updateTaskById(task.id, { status: "failed", retry_count: retryCount });
1158
- } catch (_updateError) {
1159
- }
1160
- return {
1161
- success: false,
1162
- error: { code: "PROCESSING_ERROR", message: result.error, retriesAttempted: retryCount }
1163
- };
1164
- }
1165
- try {
1166
- await ctx.db.deleteTaskById(task.id);
1167
- } catch (error) {
1168
- try {
1169
- await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
1170
- } catch (_markSentError) {
1171
- }
1172
- return {
1173
- success: false,
1174
- error: {
1175
- code: "POST_SEND_CLEANUP_FAILED",
1176
- message: "\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u4F46\u4EFB\u52A1\u6E05\u7406\u5931\u8D25",
1177
- details: { error: error.message }
1178
- }
1179
- };
1180
- }
1181
- return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
1182
- }
1183
- }
1184
-
1185
- // src/server/handlers/schedule-message.js
1186
- function createScheduleMessageHandler(ctx) {
1187
- async function POST(headers, body) {
1188
- const tenantResult = await ctx.tenantManager.resolveTenant(headers);
1189
- if (!tenantResult.ok) {
1190
- return tenantResult.error;
1191
- }
1192
- const tenantCtx = tenantResult.context;
1193
- const db = tenantCtx.db;
1194
- const masterKey = tenantCtx.masterKey;
1195
- const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
1196
- const encryptionVersion = getHeader(headers, "x-encryption-version");
1197
- const userId = getHeader(headers, "x-user-id");
1198
- if (!isEncrypted) {
1199
- return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
1200
- }
1201
- if (!userId) {
1202
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
1203
- }
1204
- if (!isValidUUIDv4(userId)) {
1205
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
1206
- }
1207
- if (encryptionVersion !== "1") {
1208
- return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
1209
- }
1210
- const parsedBody = parseEncryptedBody(body);
1211
- if (!parsedBody.ok) {
1212
- return { status: 400, body: { success: false, error: parsedBody.error } };
1213
- }
1214
- const encryptedBody = parsedBody.data;
1215
- const userKey = await deriveUserEncryptionKey(userId, masterKey);
1216
- let payload;
1217
- try {
1218
- payload = await decryptPayload(encryptedBody, userKey);
1219
- } catch (error) {
1220
- if (error instanceof SyntaxError) {
1221
- return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
1222
- }
1223
- const message = typeof error.message === "string" ? error.message : "";
1224
- if (message.includes("auth") || message.includes("Unsupported state")) {
1225
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
1226
- }
1227
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
1228
- }
1229
- if (!isPlainObject(payload)) {
1230
- return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61" } } };
1231
- }
1232
- const validationResult = validateScheduleMessagePayload(payload);
1233
- if (!validationResult.valid) {
1234
- return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
1235
- }
1236
- const taskUuid = payload.uuid || randomUUID();
1237
- const fullTaskData = {
1238
- contactName: payload.contactName,
1239
- avatarUrl: payload.avatarUrl || null,
1240
- messageType: payload.messageType,
1241
- messageSubtype: payload.messageSubtype || "chat",
1242
- userMessage: payload.userMessage || null,
1243
- firstSendTime: payload.firstSendTime,
1244
- recurrenceType: payload.recurrenceType || "none",
1245
- apiUrl: payload.apiUrl || null,
1246
- apiKey: payload.apiKey || null,
1247
- primaryModel: payload.primaryModel || null,
1248
- // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
1249
- // style array). Validation has already enforced exactly-one-of, so
1250
- // exactly one of these will be non-null when an AI config is provided.
1251
- completePrompt: payload.completePrompt || null,
1252
- messages: Array.isArray(payload.messages) ? payload.messages : null,
1253
- maxTokens: payload.maxTokens ?? null,
1254
- temperature: payload.temperature ?? null,
1255
- // 0.6.0+: optional caller-provided regex (string or string[]) used by
1256
- // the message processor to chunk LLM output into individual pushes.
1257
- // null → processor falls back to the default /([。!?!?]+)/ regex.
1258
- splitPattern: payload.splitPattern ?? null,
1259
- pushSubscription: payload.pushSubscription,
1260
- metadata: payload.metadata || {}
1261
- };
1262
- const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
1263
- if (payload.messageType === "instant") {
1264
- if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
1265
- return {
1266
- status: 500,
1267
- body: {
1268
- success: false,
1269
- error: {
1270
- code: "VAPID_CONFIG_ERROR",
1271
- message: "VAPID \u914D\u7F6E\u7F3A\u5931\uFF0C\u65E0\u6CD5\u53D1\u9001\u5373\u65F6\u6D88\u606F",
1272
- details: {
1273
- missingKeys: [
1274
- !ctx.vapid.email && "VAPID_EMAIL",
1275
- !ctx.vapid.publicKey && "NEXT_PUBLIC_VAPID_PUBLIC_KEY",
1276
- !ctx.vapid.privateKey && "VAPID_PRIVATE_KEY"
1277
- ].filter(Boolean)
1278
- }
1279
- }
1280
- }
1281
- };
1282
- }
1283
- }
1284
- let dbResult;
1285
- try {
1286
- dbResult = await db.createTask({
1287
- user_id: userId,
1288
- uuid: taskUuid,
1289
- encrypted_payload: encryptedPayload,
1290
- next_send_at: payload.firstSendTime,
1291
- message_type: payload.messageType
1292
- });
1293
- } catch (error) {
1294
- if (isUniqueViolation(error)) {
1295
- return {
1296
- status: 409,
1297
- body: {
1298
- success: false,
1299
- error: {
1300
- code: "TASK_UUID_CONFLICT",
1301
- message: "\u4EFB\u52A1 UUID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u65B0\u7684 uuid \u91CD\u65B0\u63D0\u4EA4"
1302
- }
1303
- }
1304
- };
1305
- }
1306
- throw error;
1307
- }
1308
- if (!dbResult) {
1309
- return { status: 500, body: { success: false, error: { code: "TASK_CREATE_FAILED", message: "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25" } } };
1310
- }
1311
- if (payload.messageType === "instant") {
1312
- try {
1313
- const sendResult = await processMessagesByUuid(taskUuid, {
1314
- ...ctx,
1315
- db,
1316
- masterKey
1317
- }, 2, userId, masterKey);
1318
- if (!sendResult.success) {
1319
- return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: sendResult.error } } };
1320
- }
1321
- return {
1322
- status: 200,
1323
- body: {
1324
- success: true,
1325
- data: {
1326
- uuid: taskUuid,
1327
- contactName: payload.contactName,
1328
- messagesSent: sendResult.messagesSent,
1329
- sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1330
- status: "sent",
1331
- retriesUsed: sendResult.retriesUsed || 0
1332
- }
1333
- }
1334
- };
1335
- } catch (error) {
1336
- return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: { error: error.message } } } };
1337
- }
1338
- }
1339
- return {
1340
- status: 201,
1341
- body: {
1342
- success: true,
1343
- data: {
1344
- id: dbResult.id,
1345
- uuid: dbResult.uuid,
1346
- contactName: payload.contactName,
1347
- nextSendAt: dbResult.next_send_at,
1348
- status: dbResult.status,
1349
- createdAt: dbResult.created_at
1350
- }
1351
- }
1352
- };
1353
- }
1354
- return { POST };
1355
- }
1356
-
1357
- // src/server/lib/run-tick.js
1358
- var DEFAULT_CLAIM_LEASE_MS = 10 * 60 * 1e3;
1359
- var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
1360
- function positiveNumber(value) {
1361
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
1362
- }
1363
- function resolveClaimLeaseMs(ctx) {
1364
- return positiveNumber(ctx.claimLeaseMs) || Math.max(DEFAULT_CLAIM_LEASE_MS, positiveNumber(ctx.totalTimeoutMs) + CLAIM_LEASE_MARGIN_MS);
1365
- }
1366
- async function runScheduledTick(ctx) {
1367
- const db = ctx.db;
1368
- const masterKey = ctx.masterKey;
1369
- const claimLeaseMs = resolveClaimLeaseMs(ctx);
1370
- const startTime = Date.now();
1371
- const tasks = await db.getPendingTasks(50);
1372
- const MAX_CONCURRENT = 8;
1373
- const results = {
1374
- totalTasks: tasks.length,
1375
- successCount: 0,
1376
- failedCount: 0,
1377
- claimSkippedTasks: 0,
1378
- deletedOnceOffTasks: 0,
1379
- updatedRecurringTasks: 0,
1380
- failedTasks: []
1381
- };
1382
- const supportsClaim = typeof db.claimTask === "function";
1383
- async function claimForThisTick(task) {
1384
- if (!supportsClaim) return true;
1385
- const leaseUntil = new Date(Date.now() + claimLeaseMs).toISOString();
1386
- return !!await db.claimTask(task.id, task.next_send_at, leaseUntil);
1387
- }
1388
- async function updateAndRelease(taskId, fields) {
1389
- return db.updateTaskById(taskId, supportsClaim ? { ...fields, lease_until: null } : fields);
1390
- }
1391
- async function handleDeliveryFailure(task, reason) {
1392
- results.failedCount++;
1393
- try {
1394
- if (task.retry_count >= 3) {
1395
- await updateAndRelease(task.id, { status: "failed" });
1396
- results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
1397
- } else {
1398
- const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
1399
- await updateAndRelease(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
1400
- results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
1401
- }
1402
- } catch (updateError) {
1403
- results.failedTasks.push({ taskId: task.id, reason, status: "retry_update_failed", updateError: updateError.message });
1404
- }
1405
- }
1406
- async function handlePostSendPersistenceFailure(task, reason) {
1407
- results.failedCount++;
1408
- let markedSent = false;
1409
- try {
1410
- await updateAndRelease(task.id, { status: "sent", retry_count: 0 });
1411
- markedSent = true;
1412
- } catch (_markSentError) {
1413
- markedSent = false;
1414
- }
1415
- results.failedTasks.push({
1416
- taskId: task.id,
1417
- reason,
1418
- status: markedSent ? "post_send_cleanup_failed_marked_sent" : "post_send_cleanup_failed",
1419
- messageDelivered: true
1420
- });
1421
- }
1422
- async function processTask(task) {
1423
- let claimed;
1424
- try {
1425
- claimed = await claimForThisTick(task);
1426
- } catch (error) {
1427
- results.failedCount++;
1428
- results.failedTasks.push({ taskId: task.id, reason: error.message || "\u4EFB\u52A1\u5360\u4F4D\u5931\u8D25", status: "claim_failed" });
1429
- return;
1430
- }
1431
- if (!claimed) {
1432
- results.claimSkippedTasks++;
1433
- return;
1434
- }
1435
- let sendResult;
1436
- try {
1437
- sendResult = await processSingleMessage(task, { ...ctx, db, masterKey }, masterKey);
1438
- } catch (error) {
1439
- await handleDeliveryFailure(task, error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
1440
- return;
1441
- }
1442
- if (!sendResult.success) {
1443
- await handleDeliveryFailure(task, sendResult.error || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
1444
- return;
1445
- }
1446
- try {
1447
- const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
1448
- const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
1449
- if (decryptedPayload.recurrenceType === "none") {
1450
- await db.deleteTaskById(task.id);
1451
- results.deletedOnceOffTasks++;
1452
- } else {
1453
- let nextSendAt;
1454
- const currentSendAt = new Date(task.next_send_at);
1455
- if (decryptedPayload.recurrenceType === "daily") {
1456
- nextSendAt = new Date(currentSendAt.getTime() + 24 * 60 * 60 * 1e3);
1457
- } else if (decryptedPayload.recurrenceType === "weekly") {
1458
- nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
1459
- }
1460
- await updateAndRelease(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
1461
- results.updatedRecurringTasks++;
1462
- }
1463
- results.successCount++;
1464
- } catch (error) {
1465
- await handlePostSendPersistenceFailure(task, error.message || "\u53D1\u9001\u540E\u72B6\u6001\u66F4\u65B0\u5931\u8D25");
1466
- }
1467
- }
1468
- const taskQueue = [...tasks];
1469
- const processing = [];
1470
- while (taskQueue.length > 0 || processing.length > 0) {
1471
- while (processing.length < MAX_CONCURRENT && taskQueue.length > 0) {
1472
- const task = taskQueue.shift();
1473
- const promise = processTask(task);
1474
- processing.push(promise);
1475
- promise.finally(() => {
1476
- const index = processing.indexOf(promise);
1477
- if (index > -1) processing.splice(index, 1);
1478
- });
1479
- }
1480
- if (processing.length > 0) {
1481
- await Promise.race(processing);
1482
- }
1483
- }
1484
- await db.cleanupOldTasks(7);
1485
- const executionTime = Date.now() - startTime;
1486
- return {
1487
- totalTasks: results.totalTasks,
1488
- successCount: results.successCount,
1489
- failedCount: results.failedCount,
1490
- processedAt: (/* @__PURE__ */ new Date()).toISOString(),
1491
- executionTime,
1492
- details: {
1493
- claimSkippedTasks: results.claimSkippedTasks,
1494
- deletedOnceOffTasks: results.deletedOnceOffTasks,
1495
- updatedRecurringTasks: results.updatedRecurringTasks,
1496
- failedTasks: results.failedTasks
1497
- }
1498
- };
1499
- }
1500
-
1501
- // src/server/handlers/update-message.js
1502
- function createUpdateMessageHandler(ctx) {
1503
- async function PUT(url, headers, body) {
1504
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1505
- if (!tenantResult.ok) {
1506
- return tenantResult.error;
1507
- }
1508
- const tenantCtx = tenantResult.context;
1509
- const db = tenantCtx.db;
1510
- const masterKey = tenantCtx.masterKey;
1511
- const u = new URL(url, "https://dummy");
1512
- const taskUuid = u.searchParams.get("id");
1513
- if (!taskUuid) {
1514
- return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
1515
- }
1516
- const userId = getHeader(headers, "x-user-id");
1517
- if (!userId) {
1518
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
1519
- }
1520
- if (!isValidUUIDv4(userId)) {
1521
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
1522
- }
1523
- const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
1524
- const encryptionVersion = getHeader(headers, "x-encryption-version");
1525
- if (!isEncrypted) {
1526
- return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
1527
- }
1528
- if (encryptionVersion !== "1") {
1529
- return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
1530
- }
1531
- const parsedBody = parseEncryptedBody(body);
1532
- if (!parsedBody.ok) {
1533
- return { status: 400, body: { success: false, error: parsedBody.error } };
1534
- }
1535
- const encryptedBody = parsedBody.data;
1536
- const userKey = await deriveUserEncryptionKey(userId, masterKey);
1537
- let updates;
1538
- try {
1539
- updates = await decryptPayload(encryptedBody, userKey);
1540
- } catch (_error) {
1541
- return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
1542
- }
1543
- if (!isPlainObject(updates)) {
1544
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF" } } };
1545
- }
1546
- if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
1547
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
1548
- }
1549
- if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
1550
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
1551
- }
1552
- if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
1553
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
1554
- }
1555
- if (updates.completePrompt && updates.messages !== void 0 && updates.messages !== null) {
1556
- 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"] } } } };
1557
- }
1558
- if (updates.messages !== void 0 && updates.messages !== null) {
1559
- const msgErr = validateLlmMessagesArray(updates.messages);
1560
- if (msgErr) {
1561
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: msgErr, details: { invalidFields: ["messages"] } } } };
1562
- }
1563
- }
1564
- if (Object.prototype.hasOwnProperty.call(updates, "temperature") && updates.temperature !== null && (typeof updates.temperature !== "number" || !Number.isFinite(updates.temperature))) {
1565
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature"] } } } };
1566
- }
1567
- if (Object.prototype.hasOwnProperty.call(updates, "splitPattern")) {
1568
- const splitErr = validateSplitPattern(updates.splitPattern);
1569
- if (splitErr) {
1570
- return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: splitErr, details: { invalidFields: ["splitPattern"] } } } };
1571
- }
1572
- }
1573
- if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
1574
- const avatarErr = validateAvatarUrl(updates.avatarUrl);
1575
- if (avatarErr) {
1576
- console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
1577
- delete updates.avatarUrl;
1578
- }
1579
- }
1580
- const existingTask = await db.getTaskByUuid(taskUuid, userId);
1581
- if (!existingTask) {
1582
- const taskStatus = await db.getTaskStatus(taskUuid, userId);
1583
- if (!taskStatus) {
1584
- 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" } } };
1585
- }
1586
- 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" } } };
1587
- }
1588
- const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
1589
- const promptUpdates = {};
1590
- if (updates.completePrompt) {
1591
- promptUpdates.completePrompt = updates.completePrompt;
1592
- promptUpdates.messages = null;
1593
- } else if (updates.messages !== void 0 && updates.messages !== null) {
1594
- promptUpdates.messages = updates.messages;
1595
- promptUpdates.completePrompt = null;
1596
- }
1597
- const updatedData = {
1598
- ...existingData,
1599
- ...promptUpdates,
1600
- ...updates.userMessage && { userMessage: updates.userMessage },
1601
- ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
1602
- ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
1603
- ...updates.metadata && { metadata: updates.metadata },
1604
- ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null },
1605
- ...Object.prototype.hasOwnProperty.call(updates, "temperature") && { temperature: updates.temperature ?? null },
1606
- // splitPattern: hasOwnProperty so that explicit `null` (= revert to
1607
- // default) doesn't get swallowed by truthy-spread the way the optional
1608
- // string fields above are.
1609
- ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
1610
- };
1611
- const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
1612
- const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
1613
- const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
1614
- if (!result) {
1615
- 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" } } };
1616
- }
1617
- return {
1618
- status: 200,
1619
- body: {
1620
- success: true,
1621
- data: {
1622
- uuid: taskUuid,
1623
- updatedFields: Object.keys(updates),
1624
- updatedAt: result.updated_at
1625
- }
1626
- }
1627
- };
1628
- }
1629
- return { PUT };
1630
- }
1631
-
1632
- // src/server/handlers/cancel-message.js
1633
- function createCancelMessageHandler(ctx) {
1634
- async function DELETE(url, headers) {
1635
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1636
- if (!tenantResult.ok) {
1637
- return tenantResult.error;
1638
- }
1639
- const db = tenantResult.context.db;
1640
- const u = new URL(url, "https://dummy");
1641
- const taskUuid = u.searchParams.get("id");
1642
- if (!taskUuid) {
1643
- return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
1644
- }
1645
- const userId = getHeader(headers, "x-user-id");
1646
- if (!userId) {
1647
- return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
1648
- }
1649
- if (!isValidUUIDv4(userId)) {
1650
- return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
1651
- }
1652
- const deleted = await db.deleteTaskByUuid(taskUuid, userId);
1653
- if (!deleted) {
1654
- return {
1655
- status: 404,
1656
- body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } }
1657
- };
1658
- }
1659
- return {
1660
- status: 200,
1661
- body: {
1662
- success: true,
1663
- data: { uuid: taskUuid, message: "\u4EFB\u52A1\u5DF2\u6210\u529F\u53D6\u6D88", deletedAt: (/* @__PURE__ */ new Date()).toISOString() }
1664
- }
1665
- };
1666
- }
1667
- return { DELETE };
1668
- }
1669
-
1670
- // src/server/handlers/messages.js
1671
- function createMessagesHandler(ctx) {
1672
- async function GET(url, headers) {
1673
- const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1674
- if (!tenantResult.ok) {
1675
- return tenantResult.error;
1676
- }
1677
- const tenantCtx = tenantResult.context;
1678
- const db = tenantCtx.db;
1679
- const masterKey = tenantCtx.masterKey;
1680
- const userId = getHeader(headers, "x-user-id");
1681
- if (!userId) {
1682
- return {
1683
- status: 400,
1684
- body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u5FC5\u987B\u63D0\u4F9B X-User-Id \u8BF7\u6C42\u5934" } }
1685
- };
1686
- }
1687
- if (!isValidUUIDv4(userId)) {
1688
- return {
1689
- status: 400,
1690
- body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
1691
- };
1692
- }
1693
- const u = new URL(url, "https://dummy");
1694
- const status = u.searchParams.get("status") || "all";
1695
- const limit = Math.min(parseInt(u.searchParams.get("limit") || "20", 10), 100);
1696
- const offset = parseInt(u.searchParams.get("offset") || "0", 10);
1697
- if (isNaN(limit) || limit < 1) {
1698
- return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "limit \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u6B63\u6574\u6570" } } };
1699
- }
1700
- if (isNaN(offset) || offset < 0) {
1701
- return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
1702
- }
1703
- const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
1704
- const userKey = await deriveUserEncryptionKey(userId, masterKey);
1705
- const decryptedTasks = await Promise.all(tasks.map(async (task) => {
1706
- const decrypted = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
1707
- return {
1708
- id: task.id,
1709
- uuid: task.uuid,
1710
- contactName: decrypted.contactName,
1711
- messageType: task.message_type,
1712
- messageSubtype: decrypted.messageSubtype,
1713
- nextSendAt: task.next_send_at,
1714
- recurrenceType: decrypted.recurrenceType,
1715
- status: task.status,
1716
- retryCount: task.retry_count,
1717
- createdAt: task.created_at,
1718
- updatedAt: task.updated_at,
1719
- // Character ownership / client-side task identity, pulled from the
1720
- // scheduling host's metadata so it can filter tasks by character
1721
- // (contactName can collide across characters). Only these two
1722
- // metadata fields are surfaced — the rest of metadata may hold
1723
- // host-private data and stays server-side. Absent → null.
1724
- charId: decrypted.metadata?.charId ?? null,
1725
- clientTaskId: decrypted.metadata?.amsgClientTaskId ?? null
1726
- };
1727
- }));
1728
- const responsePayload = {
1729
- tasks: decryptedTasks,
1730
- pagination: { total, limit, offset, hasMore: offset + limit < total }
1731
- };
1732
- const encryptedResponse = await encryptPayload(responsePayload, userKey);
1733
- return {
1734
- status: 200,
1735
- body: {
1736
- success: true,
1737
- encrypted: true,
1738
- version: 1,
1739
- data: encryptedResponse
1740
- }
1741
- };
1742
- }
1743
- return { GET };
1744
- }
1745
-
1746
- // src/server/adapters/schema.sqlite.js
1747
- var SQLITE_TABLE_SQL = `
1748
- CREATE TABLE IF NOT EXISTS scheduled_messages (
1749
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1750
- user_id TEXT NOT NULL,
1751
- uuid TEXT,
1752
- encrypted_payload TEXT NOT NULL,
1753
- message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
1754
- next_send_at TEXT NOT NULL,
1755
- lease_until TEXT,
1756
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
1757
- retry_count INTEGER NOT NULL DEFAULT 0,
1758
- created_at TEXT NOT NULL,
1759
- updated_at TEXT NOT NULL
1760
- )
1761
- `;
1762
- var SQLITE_MIGRATIONS = [
1763
- {
1764
- name: "add_lease_until",
1765
- sql: "ALTER TABLE scheduled_messages ADD COLUMN lease_until TEXT",
1766
- description: "Task claim lease (2.6.0)"
1767
- }
1768
- ];
1769
- var SQLITE_INDEXES = [
1770
- {
1771
- name: "idx_pending_tasks_optimized",
1772
- sql: `CREATE INDEX IF NOT EXISTS idx_pending_tasks_optimized
1773
- ON scheduled_messages (status, next_send_at, id, retry_count)
1774
- WHERE status = 'pending'`,
1775
- description: "Main query index (Cron Job finds pending tasks)",
1776
- critical: false
1777
- },
1778
- {
1779
- name: "idx_cleanup_completed",
1780
- sql: `CREATE INDEX IF NOT EXISTS idx_cleanup_completed
1781
- ON scheduled_messages (status, updated_at)
1782
- WHERE status IN ('sent', 'failed')`,
1783
- description: "Cleanup query index",
1784
- critical: false
1785
- },
1786
- {
1787
- name: "idx_failed_retry",
1788
- sql: `CREATE INDEX IF NOT EXISTS idx_failed_retry
1789
- ON scheduled_messages (status, retry_count, next_send_at)
1790
- WHERE status = 'failed' AND retry_count < 3`,
1791
- description: "Failed retry index",
1792
- critical: false
1793
- },
1794
- {
1795
- name: "idx_user_id",
1796
- sql: `CREATE INDEX IF NOT EXISTS idx_user_id
1797
- ON scheduled_messages (user_id)`,
1798
- description: "User task query index",
1799
- critical: false
1800
- },
1801
- {
1802
- name: "uidx_uuid",
1803
- sql: `CREATE UNIQUE INDEX IF NOT EXISTS uidx_uuid
1804
- ON scheduled_messages (uuid)
1805
- WHERE uuid IS NOT NULL`,
1806
- description: "UUID uniqueness guard",
1807
- critical: true
1808
- }
1809
- ];
1810
- var CLIENT_STATE_TABLE_SQL = `
1811
- CREATE TABLE IF NOT EXISTS client_state (
1812
- user_id TEXT NOT NULL,
1813
- namespace TEXT NOT NULL,
1814
- key TEXT NOT NULL,
1815
- value TEXT NOT NULL,
1816
- updated_at INTEGER NOT NULL,
1817
- PRIMARY KEY (user_id, namespace, key)
1818
- )
1819
- `;
1820
-
1821
- // src/server/adapters/d1.js
1822
- var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
1823
- "user_id",
1824
- "uuid",
1825
- "encrypted_payload",
1826
- "message_type",
1827
- "next_send_at",
1828
- "lease_until",
1829
- "status",
1830
- "retry_count",
1831
- "created_at",
1832
- "updated_at"
1833
- ]);
1834
- function escapeLikePrefix(prefix) {
1835
- return prefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
1836
- }
1837
- var D1Adapter = class {
1838
- /** @param {{ prepare: (sql: string) => any }} db - Cloudflare D1 binding */
1839
- constructor(db) {
1840
- this._db = db;
1841
- }
1842
- /** @private */
1843
- _now() {
1844
- return (/* @__PURE__ */ new Date()).toISOString();
1845
- }
1846
- /** @private */
1847
- _iso(value) {
1848
- const d = new Date(value);
1849
- if (Number.isNaN(d.getTime())) {
1850
- throw new Error(`[amsg-server D1] invalid timestamp: ${value}`);
1851
- }
1852
- return d.toISOString();
1853
- }
1854
- async initSchema() {
1855
- await this._db.prepare(SQLITE_TABLE_SQL).run();
1856
- await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
1857
- for (const migration of SQLITE_MIGRATIONS) {
1858
- try {
1859
- await this._db.prepare(migration.sql).run();
1860
- } catch (error) {
1861
- if (!/duplicate column name/i.test(error.message || "")) throw error;
1862
- }
1863
- }
1864
- const indexResults = [];
1865
- for (const index of SQLITE_INDEXES) {
1866
- try {
1867
- await this._db.prepare(index.sql).run();
1868
- indexResults.push({ name: index.name, status: "success", description: index.description, critical: !!index.critical });
1869
- } catch (error) {
1870
- indexResults.push({ name: index.name, status: "failed", description: index.description, critical: !!index.critical, error: error.message });
1871
- }
1872
- }
1873
- const criticalFailures = indexResults.filter((i) => i.critical && i.status === "failed");
1874
- if (criticalFailures.length > 0) {
1875
- const names = criticalFailures.map((i) => i.name).join(", ");
1876
- throw new Error(
1877
- `Critical index creation failed (${names}). Please remove duplicate UUID rows and run initSchema again.`
1878
- );
1879
- }
1880
- return {
1881
- columnsCreated: 11,
1882
- indexesCreated: indexResults.filter((r) => r.status === "success").length,
1883
- indexesFailed: indexResults.filter((r) => r.status === "failed").length,
1884
- columns: [],
1885
- indexes: indexResults
1886
- };
1887
- }
1888
- async dropSchema() {
1889
- await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
1890
- await this._db.prepare("DROP TABLE IF EXISTS client_state").run();
1891
- }
1892
- async createTask(params) {
1893
- const now = this._now();
1894
- const nextSendAt = this._iso(params.next_send_at);
1895
- const res = await this._db.prepare(
1896
- `INSERT INTO scheduled_messages
1897
- (user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
1898
- VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)`
1899
- ).bind(params.user_id, params.uuid, params.encrypted_payload, nextSendAt, params.message_type, now, now).run();
1900
- const id = res.meta.last_row_id;
1901
- return this._db.prepare(
1902
- `SELECT id, uuid, next_send_at, status, created_at FROM scheduled_messages WHERE id = ?`
1903
- ).bind(id).first();
1904
- }
1905
- async getTaskByUuid(uuid, userId) {
1906
- return this._db.prepare(
1907
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
1908
- FROM scheduled_messages
1909
- WHERE uuid = ? AND user_id = ? AND status = 'pending'
1910
- LIMIT 1`
1911
- ).bind(uuid, userId).first();
1912
- }
1913
- async getTaskByUuidOnly(uuid) {
1914
- return this._db.prepare(
1915
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
1916
- FROM scheduled_messages
1917
- WHERE uuid = ? AND status = 'pending'
1918
- LIMIT 1`
1919
- ).bind(uuid).first();
1920
- }
1921
- async updateTaskById(taskId, updates) {
1922
- const sets = [];
1923
- const values = [];
1924
- for (const [key, value] of Object.entries(updates)) {
1925
- if (!UPDATABLE_COLUMNS.has(key)) {
1926
- throw new Error(`[amsg-server D1] rejected unknown update column: ${key}`);
1927
- }
1928
- sets.push(`${key} = ?`);
1929
- values.push(key === "next_send_at" ? this._iso(value) : value);
1930
- }
1931
- if (!Object.prototype.hasOwnProperty.call(updates, "updated_at")) {
1932
- sets.push("updated_at = ?");
1933
- values.push(this._now());
1934
- }
1935
- values.push(taskId);
1936
- await this._db.prepare(
1937
- `UPDATE scheduled_messages SET ${sets.join(", ")} WHERE id = ?`
1938
- ).bind(...values).run();
1939
- return this._db.prepare("SELECT * FROM scheduled_messages WHERE id = ?").bind(taskId).first();
1940
- }
1941
- async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
1942
- const now = this._now();
1943
- const sets = ["encrypted_payload = ?", "updated_at = ?"];
1944
- const values = [encryptedPayload, now];
1945
- if (extraFields) {
1946
- for (const [key, value] of Object.entries(extraFields)) {
1947
- if (!UPDATABLE_COLUMNS.has(key)) {
1948
- throw new Error(`[amsg-server D1] rejected unknown update column: ${key}`);
1949
- }
1950
- sets.push(`${key} = ?`);
1951
- values.push(key === "next_send_at" ? this._iso(value) : value);
1952
- }
1953
- }
1954
- values.push(uuid, userId);
1955
- const res = await this._db.prepare(
1956
- `UPDATE scheduled_messages SET ${sets.join(", ")}
1957
- WHERE uuid = ? AND user_id = ? AND status = 'pending'`
1958
- ).bind(...values).run();
1959
- if (!res.meta.changes) return null;
1960
- return { uuid, updated_at: now };
1961
- }
1962
- async deleteTaskById(taskId) {
1963
- const res = await this._db.prepare("DELETE FROM scheduled_messages WHERE id = ?").bind(taskId).run();
1964
- return res.meta.changes > 0;
1965
- }
1966
- async deleteTaskByUuid(uuid, userId) {
1967
- const res = await this._db.prepare(
1968
- "DELETE FROM scheduled_messages WHERE uuid = ? AND user_id = ?"
1969
- ).bind(uuid, userId).run();
1970
- return res.meta.changes > 0;
1971
- }
1972
- async getPendingTasks(limit = 50) {
1973
- const now = this._now();
1974
- const res = await this._db.prepare(
1975
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
1976
- FROM scheduled_messages
1977
- WHERE status = 'pending' AND next_send_at <= ?
1978
- AND (lease_until IS NULL OR lease_until <= ?)
1979
- ORDER BY next_send_at ASC
1980
- LIMIT ?`
1981
- ).bind(now, now, limit).all();
1982
- return res.results || [];
1983
- }
1984
- /**
1985
- * 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
1986
- * 本次投递期间别的 tick 领不走它。
1987
- *
1988
- * 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
1989
- * 任务列表要读它、循环任务推进下一次也要拿它当基准。
1990
- *
1991
- * 两个 tick 抢同一行时只有一个能改到行,另一个拿到 changes = 0,据此跳过。
1992
- * WHERE 里的两个条件各管一件事:
1993
- * - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
1994
- * 也不会把行焊死,租约到期后自然可以被接手。
1995
- * - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
1996
- * 这一跳就不该再按旧时刻发。
1997
- *
1998
- * 不加一个 'sending' 状态来表达「正在跑」:建表语句里 status 有
1999
- * CHECK (status IN ('pending','sent','failed')),加值要重建表。
2000
- *
2001
- * expectedNextSendAt 按读到的原样比对,不做时区归一化——老部署里可能还留
2002
- * 着非归一化写法的行(如 +08:00 结尾),归一化后反而对不上,那条任务会永
2003
- * 远领不到。
2004
- *
2005
- * @param {number} taskId
2006
- * @param {string} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
2007
- * @param {string|Date} leaseUntil - 租期末尾
2008
- * @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
2009
- */
2010
- async claimTask(taskId, expectedNextSendAt, leaseUntil) {
2011
- const expected = typeof expectedNextSendAt === "string" ? expectedNextSendAt : this._iso(expectedNextSendAt);
2012
- const res = await this._db.prepare(
2013
- `UPDATE scheduled_messages
2014
- SET lease_until = ?, updated_at = ?
2015
- WHERE id = ? AND status = 'pending' AND next_send_at = ?
2016
- AND (lease_until IS NULL OR lease_until <= ?)`
2017
- ).bind(this._iso(leaseUntil), this._now(), taskId, expected, this._now()).run();
2018
- return (res.meta.changes || 0) > 0;
2019
- }
2020
- async listTasks(userId, opts = {}) {
2021
- const { status = "all", limit = 20, offset = 0 } = opts;
2022
- const conditions = ["user_id = ?"];
2023
- const params = [userId];
2024
- if (status !== "all") {
2025
- conditions.push("status = ?");
2026
- params.push(status);
2027
- }
2028
- const where = conditions.join(" AND ");
2029
- const countRow = await this._db.prepare(
2030
- `SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`
2031
- ).bind(...params).first();
2032
- const total = Number(countRow.count) || 0;
2033
- const res = await this._db.prepare(
2034
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
2035
- FROM scheduled_messages
2036
- WHERE ${where}
2037
- ORDER BY next_send_at ASC
2038
- LIMIT ? OFFSET ?`
2039
- ).bind(...params, limit, offset).all();
2040
- return { tasks: res.results || [], total };
2041
- }
2042
- async cleanupOldTasks(days = 7) {
2043
- const safeDays = Math.max(1, Math.floor(Number(days)));
2044
- const cutoff = new Date(Date.now() - safeDays * 24 * 60 * 60 * 1e3).toISOString();
2045
- const res = await this._db.prepare(
2046
- `DELETE FROM scheduled_messages
2047
- WHERE status IN ('sent', 'failed') AND updated_at < ?`
2048
- ).bind(cutoff).run();
2049
- return res.meta.changes || 0;
2050
- }
2051
- async getTaskStatus(uuid, userId) {
2052
- const row = await this._db.prepare(
2053
- "SELECT status FROM scheduled_messages WHERE uuid = ? AND user_id = ? LIMIT 1"
2054
- ).bind(uuid, userId).first();
2055
- return row ? row.status : null;
2056
- }
2057
- // ── client_state (single-user cloud state mirror) ──────────────────────
2058
- /**
2059
- * Batch upsert. Last-write-wins per (namespace, key): an entry older
2060
- * than the stored row (updatedAt strictly lower) is skipped; equal or
2061
- * newer overwrites. Values arrive pre-encrypted (the handler encrypts).
2062
- *
2063
- * `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
2064
- * 条件保证陈旧批次删不动更新写入的行。两种形态——
2065
- * - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
2066
- * 大值旧写入留下的切片行(见 lib/state-chunks.js);
2067
- * - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
2068
- * 连带删掉同前缀的兄弟 key,删单条必须走精确匹配)。
2069
- *
2070
- * 删掉一整条状态就是这两种各来一条(切片行走前缀、根行走精确 key),
2071
- * `entries` 传空数组即可——见 lib/client-state-store.js。
2072
- *
2073
- * Uses D1's batch() — one network round trip for the whole set (implicit
2074
- * transaction). The client calls this endpoint inside its few-seconds
2075
- * background window, so N sequential round trips could eat the whole
2076
- * window. Bindings without batch() (e.g. the sqlite test shim, custom
2077
- * adapters) fall back to a sequential loop.
2078
- *
2079
- * @param {string} userId
2080
- * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
2081
- * @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
2082
- * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
2083
- * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
2084
- */
2085
- async upsertClientState(userId, entries, cleanups = []) {
2086
- const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
2087
- VALUES (?, ?, ?, ?, ?)
2088
- ON CONFLICT (user_id, namespace, key) DO UPDATE SET
2089
- value = excluded.value,
2090
- updated_at = excluded.updated_at
2091
- WHERE excluded.updated_at >= client_state.updated_at`;
2092
- const CLEANUP_PREFIX_SQL = `DELETE FROM client_state
2093
- WHERE user_id = ? AND namespace = ? AND key LIKE ? ESCAPE '\\' AND updated_at <= ?`;
2094
- const CLEANUP_KEY_SQL = `DELETE FROM client_state
2095
- WHERE user_id = ? AND namespace = ? AND key = ? AND updated_at <= ?`;
2096
- const buildStatements = () => [
2097
- ...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, `${escapeLikePrefix(c.keyPrefix)}%`, c.updatedAt)),
2098
- ...entries.map(
2099
- (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
2100
- )
2101
- ];
2102
- let results;
2103
- if (typeof this._db.batch === "function") {
2104
- results = await this._db.batch(buildStatements());
2105
- } else {
2106
- results = [];
2107
- for (const stmt of buildStatements()) {
2108
- results.push(await stmt.run());
2109
- }
2110
- }
2111
- const outcomes = results.slice(cleanups.length).map((res) => res.meta.changes > 0);
2112
- let upserted = 0;
2113
- let skipped = 0;
2114
- for (const wrote of outcomes) {
2115
- if (wrote) upserted++;
2116
- else skipped++;
2117
- }
2118
- return { upserted, skipped, outcomes };
2119
- }
2120
- /**
2121
- * All entries of one namespace (values still encrypted).
2122
- *
2123
- * @param {string} userId
2124
- * @param {string} namespace
2125
- * @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
2126
- */
2127
- async getClientState(userId, namespace) {
2128
- const res = await this._db.prepare(
2129
- `SELECT namespace, key, value, updated_at
2130
- FROM client_state
2131
- WHERE user_id = ? AND namespace = ?
2132
- ORDER BY key ASC`
2133
- ).bind(userId, namespace).all();
2134
- return res.results || [];
2135
- }
2136
- /**
2137
- * Wipe every entry of this user.
2138
- *
2139
- * @param {string} userId
2140
- * @returns {Promise<number>} rows deleted
2141
- */
2142
- async clearClientState(userId) {
2143
- const res = await this._db.prepare("DELETE FROM client_state WHERE user_id = ?").bind(userId).run();
2144
- return res.meta.changes || 0;
2145
- }
2146
- };
2147
- function createD1Adapter(db) {
2148
- if (!db || typeof db.prepare !== "function") {
2149
- throw new Error("[amsg-server] createD1Adapter requires a D1 database binding (env.DB)");
2150
- }
2151
- return new D1Adapter(db);
2152
- }
2153
-
2154
- // src/server/lib/constant-time.js
2155
- async function constantTimeEqual(a, b) {
2156
- const enc = new TextEncoder();
2157
- const keyBytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
2158
- const key = await globalThis.crypto.subtle.importKey(
2159
- "raw",
2160
- keyBytes,
2161
- { name: "HMAC", hash: "SHA-256" },
2162
- false,
2163
- ["sign"]
2164
- );
2165
- const da = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(String(a))));
2166
- const db = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(String(b))));
2167
- let diff = 0;
2168
- for (let i = 0; i < da.length; i++) {
2169
- diff |= da[i] ^ db[i];
2170
- }
2171
- return diff === 0;
2172
- }
2173
-
2174
- // src/server/tenant/single-user-context.js
2175
- function createSingleUserContextManager({ db, masterKey, serverToken } = {}) {
2176
- if (!db) throw new Error("[amsg-server single-user] db (adapter) is required");
2177
- if (!masterKey) throw new Error("[amsg-server single-user] masterKey is required");
2178
- const token = String(serverToken || "").trim();
2179
- async function isAuthorized(headers) {
2180
- if (!token) return true;
2181
- const provided = getHeader(headers, "x-client-token");
2182
- if (!provided) return false;
2183
- return constantTimeEqual(provided, token);
2184
- }
2185
- async function resolveTenant(headers) {
2186
- if (!await isAuthorized(headers)) {
2187
- return {
2188
- ok: false,
2189
- error: {
2190
- status: 401,
2191
- body: { success: false, error: { code: "INVALID_CLIENT_TOKEN", message: "\u5171\u4EAB\u5BC6\u94A5\u65E0\u6548\u6216\u7F3A\u5931" } }
2192
- }
2193
- };
2194
- }
2195
- return {
2196
- ok: true,
2197
- context: { tenantId: "single", tokenType: "tenant", db, masterKey }
2198
- };
2199
- }
2200
- async function initializeTenant() {
2201
- const schema = await db.initSchema();
2202
- return { tenantId: "single", schema };
2203
- }
2204
- return { resolveTenant, initializeTenant };
2205
- }
2206
-
2207
- // src/server/handlers/single-user-init.js
2208
- function createSingleUserInitHandler(ctx) {
2209
- async function POST(headers) {
2210
- const auth = await ctx.tenantManager.resolveTenant(headers || {});
2211
- if (!auth.ok) {
2212
- return auth.error;
2213
- }
2214
- try {
2215
- const result = await ctx.tenantManager.initializeTenant();
2216
- return {
2217
- status: 200,
2218
- body: { success: true, data: { tenantId: result.tenantId, schema: result.schema } }
2219
- };
2220
- } catch (error) {
2221
- return {
2222
- status: 500,
2223
- body: { success: false, error: { code: "INIT_FAILED", message: error.message } }
2224
- };
2225
- }
2226
- }
2227
- return { POST };
2228
- }
2229
-
2230
- // src/server/handlers/vapid-public-key.js
2231
- function createVapidPublicKeyHandler(ctx) {
2232
- async function GET(url, headers) {
2233
- const effectiveHeaders = headers || url || {};
2234
- const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
2235
- if (!tenantResult.ok) {
2236
- return tenantResult.error;
2237
- }
2238
- const publicKey = ctx.vapid && ctx.vapid.publicKey;
2239
- if (!publicKey) {
2240
- return {
2241
- status: 503,
2242
- body: {
2243
- success: false,
2244
- error: {
2245
- code: "VAPID_NOT_CONFIGURED",
2246
- message: "VAPID \u516C\u94A5\u672A\u914D\u7F6E\uFF1A\u8BF7\u4E3A\u672C Worker \u8BBE\u7F6E VAPID_PUBLIC_KEY"
2247
- }
2248
- }
2249
- };
2250
- }
2251
- return {
2252
- status: 200,
2253
- body: { success: true, publicKey }
2254
- };
2255
- }
2256
- return { GET };
2257
- }
2258
-
2259
- // src/server/handlers/client-state.js
2260
- var MAX_STATE_ENTRIES_PER_REQUEST = MAX_STATE_ENTRIES_PER_BATCH;
2261
- function err(status, code, message, details) {
2262
- const error = details === void 0 ? { code, message } : { code, message, details };
2263
- return { status, body: { success: false, error } };
2264
- }
2265
- function requireUserId(headers) {
2266
- const userId = getHeader(headers, "x-user-id");
2267
- if (!userId) return { error: err(400, "USER_ID_REQUIRED", "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26") };
2268
- if (!isValidUUIDv4(userId)) return { error: err(400, "INVALID_USER_ID_FORMAT", "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F") };
2269
- return { userId };
2270
- }
2271
- function rejectEntry(entry, index, code, message, extra) {
2272
- const rejection = { index, code, message, ...extra || {} };
2273
- if (entry && typeof entry === "object") {
2274
- if (typeof entry.namespace === "string") rejection.namespace = entry.namespace;
2275
- if (typeof entry.key === "string") rejection.key = entry.key;
2276
- }
2277
- return rejection;
2278
- }
2279
- function validateEntry(entry, index, maxValueBytes) {
2280
- if (!isPlainObject(entry)) {
2281
- return rejectEntry(entry, index, "INVALID_STATE_ENTRY", `entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
2282
- }
2283
- if (typeof entry.namespace !== "string" || !entry.namespace.trim() || entry.namespace.length > MAX_NAMESPACE_CHARS) {
2284
- return rejectEntry(entry, index, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
2285
- }
2286
- if (INTERNAL_STATE_CHAR_RE.test(entry.namespace)) {
2287
- return rejectEntry(entry, index, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
2288
- }
2289
- if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
2290
- return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
2291
- }
2292
- if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
2293
- return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
2294
- }
2295
- if (typeof entry.value !== "string") {
2296
- return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
2297
- }
2298
- const bytes = stateValueBytes(entry.value);
2299
- if (bytes > maxValueBytes) {
2300
- return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
2301
- }
2302
- if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
2303
- return rejectEntry(entry, index, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
2304
- }
2305
- return null;
2306
- }
2307
- function createClientStateHandler(ctx) {
2308
- async function PUT(headers, body) {
2309
- const tenantResult = await ctx.tenantManager.resolveTenant(headers);
2310
- if (!tenantResult.ok) return tenantResult.error;
2311
- const { db, masterKey } = tenantResult.context;
2312
- if (getHeader(headers, "x-payload-encrypted") !== "true") {
2313
- return err(400, "ENCRYPTION_REQUIRED", "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6");
2314
- }
2315
- const gate = requireUserId(headers);
2316
- if (gate.error) return gate.error;
2317
- const { userId } = gate;
2318
- if (getHeader(headers, "x-encryption-version") !== "1") {
2319
- return err(400, "UNSUPPORTED_ENCRYPTION_VERSION", "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301");
2320
- }
2321
- const parsedBody = parseEncryptedBody(body);
2322
- if (!parsedBody.ok) return { status: 400, body: { success: false, error: parsedBody.error } };
2323
- const userKey = await deriveUserEncryptionKey(userId, masterKey);
2324
- let payload;
2325
- try {
2326
- payload = await decryptPayload(parsedBody.data, userKey);
2327
- } catch (error) {
2328
- if (error instanceof SyntaxError) {
2329
- return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON");
2330
- }
2331
- return err(400, "DECRYPTION_FAILED", "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25");
2332
- }
2333
- if (!isPlainObject(payload)) return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
2334
- const entries = payload.entries;
2335
- if (!Array.isArray(entries) || entries.length === 0) {
2336
- return err(400, "INVALID_STATE_ENTRIES", "entries \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4");
2337
- }
2338
- if (entries.length > MAX_STATE_ENTRIES_PER_REQUEST) {
2339
- return err(400, "TOO_MANY_STATE_ENTRIES", `\u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_REQUEST} \u6761`, { count: entries.length });
2340
- }
2341
- const maxValueBytes = Number.isInteger(ctx.maxStateValueBytes) && ctx.maxStateValueBytes > 0 ? ctx.maxStateValueBytes : DEFAULT_MAX_STATE_VALUE_BYTES;
2342
- const accepted = [];
2343
- const rejected = [];
2344
- for (let i = 0; i < entries.length; i++) {
2345
- const rejection = validateEntry(entries[i], i, maxValueBytes);
2346
- if (rejection) rejected.push(rejection);
2347
- else accepted.push(entries[i]);
2348
- }
2349
- if (typeof db.upsertClientState !== "function") {
2350
- return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
2351
- }
2352
- const { upserted, skipped } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
2353
- const data = { upserted, skipped };
2354
- if (rejected.length > 0) data.rejected = rejected;
2355
- return { status: 200, body: { success: true, data } };
2356
- }
2357
- async function GET(url, headers) {
2358
- const tenantResult = await ctx.tenantManager.resolveTenant(headers);
2359
- if (!tenantResult.ok) return tenantResult.error;
2360
- const { db, masterKey } = tenantResult.context;
2361
- const gate = requireUserId(headers);
2362
- if (gate.error) return gate.error;
2363
- const { userId } = gate;
2364
- const namespace = new URL(url, "https://dummy").searchParams.get("namespace") || "";
2365
- if (!namespace.trim()) return err(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
2366
- if (INTERNAL_STATE_CHAR_RE.test(namespace)) {
2367
- return err(400, "INVALID_STATE_NAMESPACE", "namespace \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09");
2368
- }
2369
- if (typeof db.getClientState !== "function") {
2370
- return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
2371
- }
2372
- const userKey = await deriveUserEncryptionKey(userId, masterKey);
2373
- const rows = await db.getClientState(userId, namespace);
2374
- const decrypted = await resolveClientStateEntries(
2375
- rows,
2376
- () => db.getClientState(userId, chunkNamespaceFor(namespace)),
2377
- (value) => decryptFromStorage(value, userKey)
2378
- );
2379
- const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
2380
- return { status: 200, body: { success: true, encrypted: true, version: 1, data: encryptedResponse } };
2381
- }
2382
- async function DELETE(url, headers) {
2383
- const tenantResult = await ctx.tenantManager.resolveTenant(headers);
2384
- if (!tenantResult.ok) return tenantResult.error;
2385
- const { db } = tenantResult.context;
2386
- const gate = requireUserId(headers);
2387
- if (gate.error) return gate.error;
2388
- const { userId } = gate;
2389
- if (typeof db.clearClientState !== "function") {
2390
- return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
2391
- }
2392
- const deleted = await db.clearClientState(userId);
2393
- return { status: 200, body: { success: true, data: { deleted } } };
2394
- }
2395
- return { PUT, GET, DELETE };
2396
- }
2397
-
2398
- // src/server/lib/version.js
2399
- var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
2400
-
2401
- // src/server/handlers/capabilities.js
2402
- var SERVER_FEATURES = Object.freeze([
2403
- "client-state",
2404
- "client-state-chunking",
2405
- "client-state-partial-failure",
2406
- "agentic-hooks",
2407
- "agentic-scratch",
2408
- "agentic-write-state",
2409
- "agentic-fire-tools",
2410
- "agentic-schedule-task",
2411
- "vapid-public-key"
2412
- ]);
2413
- function createCapabilitiesHandler(ctx) {
2414
- async function GET(url, headers) {
2415
- const effectiveHeaders = headers || url || {};
2416
- const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
2417
- if (!tenantResult.ok) {
2418
- return tenantResult.error;
2419
- }
2420
- return {
2421
- status: 200,
2422
- body: { success: true, serverVersion: SERVER_VERSION, features: [...SERVER_FEATURES] }
2423
- };
2424
- }
2425
- return { GET };
2426
- }
2427
-
2428
- // src/server/single-user.js
2429
- function createSingleUserServer(config) {
2430
- if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
2431
- if (!config.masterKey) throw new Error("[amsg-server single-user] config.masterKey is required");
2432
- const vapid = config.vapid || {};
2433
- const tenantManager = createSingleUserContextManager({
2434
- db: config.db,
2435
- masterKey: config.masterKey,
2436
- serverToken: config.serverToken
2437
- });
2438
- const ctx = {
2439
- vapid: {
2440
- email: vapid.email || "",
2441
- publicKey: vapid.publicKey || "",
2442
- privateKey: vapid.privateKey || ""
2443
- },
2444
- webpush: config.webpush || null,
2445
- tenantManager,
2446
- // Fire-time hooks (optional): the in-server instant path fires through
2447
- // processMessagesByUuid with this ctx, so instant-type tasks can take
2448
- // the agentic path too.
2449
- hooks: config.hooks || null,
2450
- maxToolIterations: config.maxToolIterations,
2451
- totalTimeoutMs: config.totalTimeoutMs,
2452
- maxStateValueBytes: config.maxStateValueBytes,
2453
- // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2454
- maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
2455
- };
2456
- return {
2457
- ctx,
2458
- handlers: {
2459
- init: createSingleUserInitHandler(ctx),
2460
- getUserKey: createGetUserKeyHandler(ctx),
2461
- scheduleMessage: createScheduleMessageHandler(ctx),
2462
- updateMessage: createUpdateMessageHandler(ctx),
2463
- cancelMessage: createCancelMessageHandler(ctx),
2464
- messages: createMessagesHandler(ctx),
2465
- vapidPublicKey: createVapidPublicKeyHandler(ctx),
2466
- clientState: createClientStateHandler(ctx),
2467
- capabilities: createCapabilitiesHandler(ctx)
2468
- }
2469
- };
2470
- }
2471
-
2472
- // src/server/lib/webpush-webcrypto.js
2473
- import { normalizeVapidSubject } from "@rei-standard/amsg-shared";
2474
- var KEY_INFO_PREFIX = utf8("WebPush: info\0");
2475
- var CEK_INFO = utf8("Content-Encoding: aes128gcm\0");
2476
- var NONCE_INFO = utf8("Content-Encoding: nonce\0");
2477
- var VAPID_DEFAULT_TTL = 60;
2478
- var VAPID_TOKEN_LIFETIME = 12 * 3600;
2479
- var RECORD_SIZE = 4096;
2480
- var WEB_PUSH_MAX_BODY_BYTES = 4096;
2481
- var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
2482
- var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
2483
- var payloadEncoder = new TextEncoder();
2484
- function measurePushPayload(payload) {
2485
- const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
2486
- return {
2487
- bytes,
2488
- maxBytes: MAX_PUSH_PAYLOAD_BYTES,
2489
- remainingBytes: MAX_PUSH_PAYLOAD_BYTES - bytes,
2490
- withinLimit: bytes <= MAX_PUSH_PAYLOAD_BYTES
2491
- };
2492
- }
2493
- async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl }) {
2494
- if (!subscription || typeof subscription.endpoint !== "string") {
2495
- throw new Error("sendWebPush: invalid subscription");
2496
- }
2497
- if (typeof payload !== "string") {
2498
- throw new Error("sendWebPush: payload must be a string");
2499
- }
2500
- if (!vapid || !vapid.email || !vapid.publicKey || !vapid.privateKey) {
2501
- throw new Error("VAPID_CONFIG_MISSING");
2502
- }
2503
- const subscriptionKeys = subscription.keys || {};
2504
- if (typeof subscriptionKeys.p256dh !== "string" || typeof subscriptionKeys.auth !== "string") {
2505
- throw new Error("sendWebPush: subscription.keys.p256dh and .auth are required");
2506
- }
2507
- const size = measurePushPayload(payload);
2508
- if (!size.withinLimit) {
2509
- const err2 = new Error(
2510
- `sendWebPush: payload is ${size.bytes} bytes, over the ${MAX_PUSH_PAYLOAD_BYTES}-byte limit (push services cap the encrypted body at ${WEB_PUSH_MAX_BODY_BYTES} bytes; aes128gcm adds ${WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES} bytes)`
2511
- );
2512
- err2.code = "PUSH_PAYLOAD_TOO_LARGE";
2513
- err2.bytes = size.bytes;
2514
- err2.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
2515
- throw err2;
2516
- }
2517
- const encryptedBody = await encryptPushPayload({
2518
- plaintext: utf8(payload),
2519
- uaPublicKey: base64UrlToBytes(subscriptionKeys.p256dh),
2520
- authSecret: base64UrlToBytes(subscriptionKeys.auth)
2521
- });
2522
- const jwt = await buildVapidJwt({
2523
- audience: originOf(subscription.endpoint),
2524
- subject: normalizeVapidSubject(vapid.email),
2525
- publicKey: vapid.publicKey,
2526
- privateKey: vapid.privateKey
2527
- });
2528
- const fetchFn = fetchImpl || globalThis.fetch;
2529
- if (typeof fetchFn !== "function") {
2530
- throw new Error("sendWebPush: no fetch implementation available");
2531
- }
2532
- const res = await fetchFn(subscription.endpoint, {
2533
- method: "POST",
2534
- headers: {
2535
- "Content-Type": "application/octet-stream",
2536
- "Content-Encoding": "aes128gcm",
2537
- "TTL": String(Number.isFinite(ttl) ? ttl : VAPID_DEFAULT_TTL),
2538
- "Authorization": `vapid t=${jwt}, k=${vapid.publicKey}`
2539
- },
2540
- body: encryptedBody
2541
- });
2542
- if (!res.ok) {
2543
- const text = await safeReadText(res);
2544
- const err2 = new Error(
2545
- `Web Push delivery failed: ${res.status} ${res.statusText || ""}${text ? ` \u2014 ${text}` : ""}`
2546
- );
2547
- err2.code = "PUSH_SEND_FAILED";
2548
- err2.statusCode = res.status;
2549
- throw err2;
2550
- }
2551
- return {
2552
- statusCode: res.status,
2553
- body: await safeReadText(res),
2554
- headers: res.headers
2555
- };
2556
- }
2557
- async function encryptPushPayload({ plaintext, uaPublicKey, authSecret }) {
2558
- const asKeyPair = await globalThis.crypto.subtle.generateKey(
2559
- { name: "ECDH", namedCurve: "P-256" },
2560
- true,
2561
- ["deriveBits"]
2562
- );
2563
- const asPublicRaw = new Uint8Array(
2564
- await globalThis.crypto.subtle.exportKey("raw", asKeyPair.publicKey)
2565
- );
2566
- const uaPublicCryptoKey = await globalThis.crypto.subtle.importKey(
2567
- "raw",
2568
- uaPublicKey,
2569
- { name: "ECDH", namedCurve: "P-256" },
2570
- false,
2571
- []
2572
- );
2573
- const ecdhSecret = new Uint8Array(
2574
- await globalThis.crypto.subtle.deriveBits(
2575
- { name: "ECDH", public: uaPublicCryptoKey },
2576
- asKeyPair.privateKey,
2577
- 256
2578
- )
2579
- );
2580
- const keyInfo = concatBytes(KEY_INFO_PREFIX, uaPublicKey, asPublicRaw);
2581
- const ikm = await hkdfSha256(authSecret, ecdhSecret, keyInfo, 32);
2582
- const salt = randomBytes(16);
2583
- const cekBytes = await hkdfSha256(salt, ikm, CEK_INFO, 16);
2584
- const cek = await globalThis.crypto.subtle.importKey(
2585
- "raw",
2586
- cekBytes,
2587
- { name: "AES-GCM" },
2588
- false,
2589
- ["encrypt"]
2590
- );
2591
- const nonce = await hkdfSha256(salt, ikm, NONCE_INFO, 12);
2592
- const padded = concatBytes(plaintext, new Uint8Array([2]));
2593
- const ciphertext = new Uint8Array(
2594
- await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, cek, padded)
2595
- );
2596
- const header = new Uint8Array(16 + 4 + 1 + asPublicRaw.byteLength);
2597
- header.set(salt, 0);
2598
- writeUint32BE(header, 16, RECORD_SIZE);
2599
- header[20] = asPublicRaw.byteLength;
2600
- header.set(asPublicRaw, 21);
2601
- return concatBytes(header, ciphertext);
2602
- }
2603
- async function hkdfSha256(salt, ikm, info, length) {
2604
- const baseKey = await globalThis.crypto.subtle.importKey(
2605
- "raw",
2606
- toUint8(ikm),
2607
- { name: "HKDF" },
2608
- false,
2609
- ["deriveBits"]
2610
- );
2611
- const bits = await globalThis.crypto.subtle.deriveBits(
2612
- {
2613
- name: "HKDF",
2614
- hash: "SHA-256",
2615
- salt: toUint8(salt),
2616
- info: toUint8(info)
2617
- },
2618
- baseKey,
2619
- length * 8
2620
- );
2621
- return new Uint8Array(bits);
2622
- }
2623
- async function buildVapidJwt({ audience, subject, publicKey, privateKey }) {
2624
- const header = jsonToBase64Url({ typ: "JWT", alg: "ES256" });
2625
- const payload = jsonToBase64Url({
2626
- aud: audience,
2627
- exp: Math.floor(Date.now() / 1e3) + VAPID_TOKEN_LIFETIME,
2628
- sub: subject
2629
- });
2630
- const signingInput = utf8(`${header}.${payload}`);
2631
- const pubBytes = base64UrlToBytes(publicKey);
2632
- const privBytes = base64UrlToBytes(privateKey);
2633
- if (pubBytes.length !== 65 || pubBytes[0] !== 4) {
2634
- throw new Error("VAPID publicKey must be a 65-byte uncompressed P-256 point (base64url).");
2635
- }
2636
- if (privBytes.length !== 32) {
2637
- throw new Error("VAPID privateKey must be a 32-byte scalar (base64url).");
2638
- }
2639
- const jwk = {
2640
- kty: "EC",
2641
- crv: "P-256",
2642
- d: bytesToBase64Url(privBytes),
2643
- x: bytesToBase64Url(pubBytes.subarray(1, 33)),
2644
- y: bytesToBase64Url(pubBytes.subarray(33, 65)),
2645
- ext: true
2646
- };
2647
- const key = await globalThis.crypto.subtle.importKey(
2648
- "jwk",
2649
- jwk,
2650
- { name: "ECDSA", namedCurve: "P-256" },
2651
- false,
2652
- ["sign"]
2653
- );
2654
- const sig = await globalThis.crypto.subtle.sign(
2655
- { name: "ECDSA", hash: "SHA-256" },
2656
- key,
2657
- signingInput
2658
- );
2659
- return `${header}.${payload}.${bytesToBase64Url(sig)}`;
2660
- }
2661
- function originOf(endpoint) {
2662
- return new URL(endpoint).origin;
2663
- }
2664
- function writeUint32BE(buf, offset, value) {
2665
- buf[offset] = value >>> 24 & 255;
2666
- buf[offset + 1] = value >>> 16 & 255;
2667
- buf[offset + 2] = value >>> 8 & 255;
2668
- buf[offset + 3] = value & 255;
2669
- }
2670
- async function safeReadText(res) {
2671
- try {
2672
- return await res.text();
2673
- } catch {
2674
- return "";
2675
- }
2676
- }
2677
- var SCHEDULED_DEFAULT_TTL = 2419200;
2678
- function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}) {
2679
- return {
2680
- async sendNotification(subscription, payload) {
2681
- return sendWebPush({
2682
- subscription,
2683
- payload,
2684
- ttl,
2685
- vapid: {
2686
- email: vapid.email,
2687
- publicKey: vapid.publicKey,
2688
- privateKey: vapid.privateKey
2689
- },
2690
- fetch: globalThis.fetch
2691
- });
2692
- }
2693
- };
2694
- }
2695
-
2696
- // src/server/cloudflare/single-user-worker.js
2697
- function headersToObject(h) {
2698
- const out = {};
2699
- for (const [k, v] of h) out[k] = v;
2700
- return out;
2701
- }
2702
- function jsonResponse(status, body, extraHeaders) {
2703
- return new Response(JSON.stringify(body), {
2704
- status,
2705
- headers: { "Content-Type": "application/json; charset=utf-8", ...extraHeaders || {} }
2706
- });
2707
- }
2708
- var CORS_ALLOW_HEADERS = "Content-Type, X-User-Id, X-Payload-Encrypted, X-Encryption-Version, X-Response-Encrypted, X-Client-Token";
2709
- var CORS_ALLOW_METHODS = "GET, POST, PUT, DELETE, OPTIONS";
2710
- function corsHeadersFor(cors, requestOrigin) {
2711
- if (!cors || cors.origin == null) return null;
2712
- const allowOrigin = typeof cors.origin === "function" ? cors.origin(requestOrigin) || null : cors.origin;
2713
- if (!allowOrigin) return null;
2714
- const headers = {
2715
- "Access-Control-Allow-Origin": allowOrigin,
2716
- "Access-Control-Allow-Methods": CORS_ALLOW_METHODS,
2717
- "Access-Control-Allow-Headers": cors.allowHeaders || CORS_ALLOW_HEADERS,
2718
- "Access-Control-Max-Age": String(cors.maxAge ?? 86400)
2719
- };
2720
- if (allowOrigin !== "*") headers["Vary"] = "Origin";
2721
- return headers;
2722
- }
2723
- function createSingleUserCloudflareWorker(buildConfig) {
2724
- async function resolveConfig(env) {
2725
- const cfg = await buildConfig(env);
2726
- if (!cfg.db) cfg.db = createD1Adapter(env.DB);
2727
- return cfg;
2728
- }
2729
- async function fetch2(request, env) {
2730
- try {
2731
- const cfg = await resolveConfig(env);
2732
- const cors = corsHeadersFor(cfg.cors, request.headers.get("origin") || "");
2733
- const method = request.method.toUpperCase();
2734
- if (method === "OPTIONS") {
2735
- return cors ? new Response(null, { status: 204, headers: cors }) : jsonResponse(404, { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } });
2736
- }
2737
- const server = createSingleUserServer(cfg);
2738
- const url = request.url;
2739
- const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
2740
- const headers = headersToObject(request.headers);
2741
- let result;
2742
- if (method === "POST" && pathname.endsWith("/init-tenant")) {
2743
- result = await server.handlers.init.POST(headers, await request.text());
2744
- } else if (method === "GET" && pathname.endsWith("/get-user-key")) {
2745
- result = await server.handlers.getUserKey.GET(url, headers);
2746
- } else if (method === "POST" && pathname.endsWith("/schedule-message")) {
2747
- result = await server.handlers.scheduleMessage.POST(headers, await request.text());
2748
- } else if (method === "GET" && pathname.endsWith("/messages")) {
2749
- result = await server.handlers.messages.GET(url, headers);
2750
- } else if (method === "PUT" && pathname.endsWith("/update-message")) {
2751
- result = await server.handlers.updateMessage.PUT(url, headers, await request.text());
2752
- } else if (method === "DELETE" && pathname.endsWith("/cancel-message")) {
2753
- result = await server.handlers.cancelMessage.DELETE(url, headers);
2754
- } else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
2755
- result = await server.handlers.vapidPublicKey.GET(url, headers);
2756
- } else if (method === "GET" && pathname.endsWith("/capabilities")) {
2757
- result = await server.handlers.capabilities.GET(url, headers);
2758
- } else if (method === "PUT" && pathname.endsWith("/client-state")) {
2759
- result = await server.handlers.clientState.PUT(headers, await request.text());
2760
- } else if (method === "GET" && pathname.endsWith("/client-state")) {
2761
- result = await server.handlers.clientState.GET(url, headers);
2762
- } else if (method === "DELETE" && pathname.endsWith("/client-state")) {
2763
- result = await server.handlers.clientState.DELETE(url, headers);
2764
- } else {
2765
- result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
2766
- }
2767
- return jsonResponse(result.status, result.body, cors);
2768
- } catch (error) {
2769
- console.error("[amsg single-user] fetch() unhandled error:", error && error.message);
2770
- return jsonResponse(500, { success: false, error: { code: "INTERNAL_ERROR", message: "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF" } });
2771
- }
2772
- }
2773
- async function scheduled(event, env) {
2774
- const cfg = await resolveConfig(env);
2775
- const vapid = cfg.vapid || {};
2776
- if (!cfg.webpush || !vapid.email || !vapid.publicKey || !vapid.privateKey) {
2777
- console.error("[amsg single-user] scheduled(): VAPID/webpush not configured; skipping tick");
2778
- return;
2779
- }
2780
- try {
2781
- await runScheduledTick({
2782
- db: cfg.db,
2783
- masterKey: cfg.masterKey,
2784
- vapid,
2785
- webpush: cfg.webpush,
2786
- // Fire-time hooks (optional; see lib/agentic-fire.js). runScheduledTick
2787
- // spreads its ctx into processSingleMessage, so these ride along.
2788
- hooks: cfg.hooks || null,
2789
- maxToolIterations: cfg.maxToolIterations,
2790
- totalTimeoutMs: cfg.totalTimeoutMs,
2791
- // hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
2792
- maxStateValueBytes: cfg.maxStateValueBytes,
2793
- // hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
2794
- maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
2795
- // 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
2796
- claimLeaseMs: cfg.claimLeaseMs
2797
- });
2798
- } catch (error) {
2799
- console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
2800
- }
2801
- }
2802
- return { fetch: fetch2, scheduled };
2803
- }
2804
-
2805
- export {
2806
- validateAvatarUrl,
2807
- isValidISO8601,
2808
- isValidUrl,
2809
- isValidUUID,
2810
- isValidUUIDv4,
2811
- validateSplitPattern,
2812
- validateLlmMessagesArray,
2813
- validateScheduleMessagePayload,
2814
- parseJsonBody,
2815
- getHeader,
2816
- deriveUserEncryptionKey,
2817
- decryptPayload,
2818
- encryptForStorage,
2819
- decryptFromStorage,
2820
- createGetUserKeyHandler,
2821
- createScheduleMessageHandler,
2822
- runScheduledTick,
2823
- createUpdateMessageHandler,
2824
- createCancelMessageHandler,
2825
- createMessagesHandler,
2826
- createD1Adapter,
2827
- createSingleUserServer,
2828
- WEB_PUSH_MAX_BODY_BYTES,
2829
- WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
2830
- MAX_PUSH_PAYLOAD_BYTES,
2831
- measurePushPayload,
2832
- createWebCryptoWebPush,
2833
- createSingleUserCloudflareWorker
2834
- };