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