@rei-standard/amsg-server 2.6.0-next.1 → 2.6.0-next.3
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-S5DV7AUH.cjs → chunk-L7WMM33Q.cjs} +642 -175
- package/dist/{chunk-6WA3XDPP.mjs → chunk-PPPWETND.mjs} +639 -172
- package/dist/{cloudflare-5Ryuxz10.d.cts → cloudflare-DlLTdCp0.d.cts} +1051 -351
- package/dist/{cloudflare-5Ryuxz10.d.ts → cloudflare-DlLTdCp0.d.ts} +1051 -351
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -2
- package/dist/cloudflare.d.ts +1 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -106,10 +106,10 @@ function validateScheduleMessagePayload(payload) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
if (hasMessages) {
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
109
|
+
const err2 = validateLlmMessagesArray(payload.messages);
|
|
110
|
+
if (err2) {
|
|
111
111
|
return {
|
|
112
|
-
error: { code: "INVALID_PARAMETERS", message:
|
|
112
|
+
error: { code: "INVALID_PARAMETERS", message: err2, details: { invalidFields: ["messages"] } },
|
|
113
113
|
hasCompletePrompt: false,
|
|
114
114
|
hasMessages: true
|
|
115
115
|
};
|
|
@@ -162,53 +162,127 @@ function validateScheduleMessagePayload(payload) {
|
|
|
162
162
|
return { valid: true };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// src/server/lib/webcrypto-utils.js
|
|
166
|
+
|
|
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(_amsgshared.toUint8.call(void 0, buf));
|
|
174
|
+
}
|
|
175
|
+
function bytesToBase64(buf) {
|
|
176
|
+
const bytes = _amsgshared.toUint8.call(void 0, 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 = _amsgshared.toUint8.call(void 0, 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
|
+
|
|
165
213
|
// src/server/lib/encryption.js
|
|
166
|
-
var
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
var base64ToBytes = _amsgshared.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
|
+
);
|
|
169
224
|
}
|
|
170
|
-
function
|
|
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
|
+
_amsgshared.concatBytes.call(void 0, 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) {
|
|
171
253
|
const { iv, authTag, encryptedData } = encryptedPayload;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
254
|
+
const plain = await aesGcmOpen(
|
|
255
|
+
encryptionKey,
|
|
256
|
+
base64ToBytes(iv),
|
|
257
|
+
base64ToBytes(encryptedData),
|
|
258
|
+
base64ToBytes(authTag)
|
|
176
259
|
);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
decipher.final()
|
|
181
|
-
]);
|
|
182
|
-
return JSON.parse(decrypted.toString("utf8"));
|
|
183
|
-
}
|
|
184
|
-
function encryptPayload(payload, encryptionKey) {
|
|
260
|
+
return JSON.parse(utf8Decode(plain));
|
|
261
|
+
}
|
|
262
|
+
async function encryptPayload(payload, encryptionKey) {
|
|
185
263
|
const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
186
|
-
const iv =
|
|
187
|
-
const
|
|
188
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
189
|
-
const authTag = cipher.getAuthTag();
|
|
264
|
+
const iv = randomBytes(12);
|
|
265
|
+
const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(plaintext));
|
|
190
266
|
return {
|
|
191
|
-
iv: iv
|
|
192
|
-
authTag: authTag
|
|
193
|
-
encryptedData:
|
|
267
|
+
iv: bytesToBase64(iv),
|
|
268
|
+
authTag: bytesToBase64(authTag),
|
|
269
|
+
encryptedData: bytesToBase64(ciphertext)
|
|
194
270
|
};
|
|
195
271
|
}
|
|
196
|
-
function encryptForStorage(text, encryptionKey) {
|
|
197
|
-
const iv =
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const authTag = cipher.getAuthTag();
|
|
201
|
-
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
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)}`;
|
|
202
276
|
}
|
|
203
|
-
function decryptFromStorage(encryptedText, encryptionKey) {
|
|
277
|
+
async function decryptFromStorage(encryptedText, encryptionKey) {
|
|
204
278
|
const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
279
|
+
const plain = await aesGcmOpen(
|
|
280
|
+
encryptionKey,
|
|
281
|
+
hexToBytes(ivHex),
|
|
282
|
+
hexToBytes(encryptedDataHex),
|
|
283
|
+
hexToBytes(authTagHex)
|
|
209
284
|
);
|
|
210
|
-
|
|
211
|
-
return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
|
|
285
|
+
return utf8Decode(plain);
|
|
212
286
|
}
|
|
213
287
|
|
|
214
288
|
// src/server/lib/request.js
|
|
@@ -300,7 +374,7 @@ function createGetUserKeyHandler(ctx) {
|
|
|
300
374
|
body: {
|
|
301
375
|
success: true,
|
|
302
376
|
data: {
|
|
303
|
-
userKey: deriveUserEncryptionKey(userId, masterKey),
|
|
377
|
+
userKey: await deriveUserEncryptionKey(userId, masterKey),
|
|
304
378
|
version: 1
|
|
305
379
|
}
|
|
306
380
|
}
|
|
@@ -309,9 +383,6 @@ function createGetUserKeyHandler(ctx) {
|
|
|
309
383
|
return { GET };
|
|
310
384
|
}
|
|
311
385
|
|
|
312
|
-
// src/server/handlers/schedule-message.js
|
|
313
|
-
|
|
314
|
-
|
|
315
386
|
// src/server/lib/db-errors.js
|
|
316
387
|
function isUniqueViolation(error) {
|
|
317
388
|
if (!error || typeof error !== "object") return false;
|
|
@@ -329,8 +400,269 @@ function isUniqueViolation(error) {
|
|
|
329
400
|
|
|
330
401
|
|
|
331
402
|
|
|
332
|
-
|
|
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 = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.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 (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
447
|
+
return requestBody;
|
|
448
|
+
}
|
|
449
|
+
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
450
|
+
throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
|
|
451
|
+
}
|
|
452
|
+
requestBody.max_tokens = payload.maxTokens;
|
|
453
|
+
return requestBody;
|
|
454
|
+
}
|
|
455
|
+
function normalizeAiApiUrl(apiUrl) {
|
|
456
|
+
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
"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)."
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const trimmedApiUrl = apiUrl.trim();
|
|
462
|
+
let parsedUrl;
|
|
463
|
+
try {
|
|
464
|
+
parsedUrl = new URL(trimmedApiUrl);
|
|
465
|
+
} catch (e3) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
471
|
+
if (/\/chat\/completions$/.test(path)) {
|
|
472
|
+
} else if (path === "/") {
|
|
473
|
+
path = "/v1/chat/completions";
|
|
474
|
+
} else if (/\/v\d+$/.test(path)) {
|
|
475
|
+
path = `${path}/chat/completions`;
|
|
476
|
+
}
|
|
477
|
+
parsedUrl.pathname = path;
|
|
478
|
+
return parsedUrl.toString();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/server/lib/agentic-fire.js
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
489
|
+
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
333
490
|
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
491
|
+
var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
|
|
492
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
493
|
+
function taskNeedsLlm(decryptedPayload) {
|
|
494
|
+
const type = decryptedPayload.messageType;
|
|
495
|
+
if (type === "prompted" || type === "auto") return true;
|
|
496
|
+
if (type === "instant") {
|
|
497
|
+
return !!(decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel);
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
function buildHookTask(task, decryptedPayload) {
|
|
502
|
+
const safe = {};
|
|
503
|
+
for (const [key, value] of Object.entries(decryptedPayload)) {
|
|
504
|
+
if (!CREDENTIAL_PAYLOAD_KEYS.has(key)) safe[key] = value;
|
|
505
|
+
}
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
...safe,
|
|
508
|
+
id: _nullishCoalesce(task.id, () => ( null)),
|
|
509
|
+
uuid: _nullishCoalesce(task.uuid, () => ( null)),
|
|
510
|
+
nextSendAt: _nullishCoalesce(task.next_send_at, () => ( null)),
|
|
511
|
+
retryCount: _nullishCoalesce(task.retry_count, () => ( 0))
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function normalizeBeforeFireResult(result) {
|
|
515
|
+
if (Array.isArray(result)) {
|
|
516
|
+
return { messages: result };
|
|
517
|
+
}
|
|
518
|
+
if (result && typeof result === "object" && Array.isArray(result.messages)) {
|
|
519
|
+
return {
|
|
520
|
+
messages: result.messages,
|
|
521
|
+
maxToolIterations: result.maxToolIterations,
|
|
522
|
+
totalTimeoutMs: result.totalTimeoutMs
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
throw new TypeError(
|
|
526
|
+
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | null"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
function firstPositiveInt(values, fallback) {
|
|
530
|
+
for (const v of values) {
|
|
531
|
+
if (Number.isInteger(v) && v > 0) return v;
|
|
532
|
+
}
|
|
533
|
+
return fallback;
|
|
534
|
+
}
|
|
535
|
+
function firstPositiveNumber(values, fallback) {
|
|
536
|
+
for (const v of values) {
|
|
537
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
538
|
+
}
|
|
539
|
+
return fallback;
|
|
540
|
+
}
|
|
541
|
+
async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
542
|
+
const hooks = ctx.hooks;
|
|
543
|
+
if (typeof hooks.onLLMOutput !== "function") {
|
|
544
|
+
throw new Error("AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds");
|
|
545
|
+
}
|
|
546
|
+
const nowFn = typeof ctx._agenticNow === "function" ? ctx._agenticNow : Date.now;
|
|
547
|
+
const sleep = typeof ctx._agenticSleep === "function" ? ctx._agenticSleep : defaultSleep;
|
|
548
|
+
const readState = async (namespace) => {
|
|
549
|
+
if (typeof namespace !== "string" || !namespace.trim()) {
|
|
550
|
+
throw new TypeError("readState(namespace) requires a non-empty string");
|
|
551
|
+
}
|
|
552
|
+
if (!ctx.db || typeof ctx.db.getClientState !== "function") return [];
|
|
553
|
+
const rows = await ctx.db.getClientState(task.user_id, namespace);
|
|
554
|
+
return Promise.all(rows.map(async (row) => ({
|
|
555
|
+
namespace: row.namespace,
|
|
556
|
+
key: row.key,
|
|
557
|
+
value: await decryptFromStorage(row.value, userKey),
|
|
558
|
+
updatedAt: row.updated_at
|
|
559
|
+
})));
|
|
560
|
+
};
|
|
561
|
+
const fireCtx = Object.freeze({
|
|
562
|
+
task: buildHookTask(task, decryptedPayload),
|
|
563
|
+
userId: task.user_id,
|
|
564
|
+
readState,
|
|
565
|
+
now: new Date(nowFn())
|
|
566
|
+
});
|
|
567
|
+
const before = await hooks.onBeforeFire(fireCtx);
|
|
568
|
+
if (before == null) return { handled: false };
|
|
569
|
+
const normalized = normalizeBeforeFireResult(before);
|
|
570
|
+
const maxToolIterations = firstPositiveInt(
|
|
571
|
+
[normalized.maxToolIterations, ctx.maxToolIterations],
|
|
572
|
+
DEFAULT_MAX_TOOL_ITERATIONS
|
|
573
|
+
);
|
|
574
|
+
const totalTimeoutMs = firstPositiveNumber(
|
|
575
|
+
[normalized.totalTimeoutMs, ctx.totalTimeoutMs],
|
|
576
|
+
DEFAULT_TOTAL_TIMEOUT_MS
|
|
577
|
+
);
|
|
578
|
+
const deadline = nowFn() + totalTimeoutMs;
|
|
579
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
|
|
580
|
+
let messages = normalized.messages.slice();
|
|
581
|
+
for (let iteration = 0; iteration < maxToolIterations; iteration++) {
|
|
582
|
+
if (nowFn() >= deadline) {
|
|
583
|
+
throw new Error(`AGENTIC_TOTAL_TIMEOUT: fire chain exceeded ${totalTimeoutMs}ms after ${iteration} LLM round(s)`);
|
|
584
|
+
}
|
|
585
|
+
const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
|
|
586
|
+
const { response: llmResponse } = await callLlm(
|
|
587
|
+
{ ...decryptedPayload, messages },
|
|
588
|
+
{ requireContent: false, timeoutMs: roundTimeoutMs }
|
|
589
|
+
);
|
|
590
|
+
const assistantMessage = _amsgshared.extractAssistantMessage.call(void 0, llmResponse);
|
|
591
|
+
messages = [...messages, assistantMessage];
|
|
592
|
+
const sessionCtx = _amsgshared.buildSessionContext.call(void 0, {
|
|
593
|
+
sessionId,
|
|
594
|
+
messages,
|
|
595
|
+
llmResponse,
|
|
596
|
+
iteration,
|
|
597
|
+
contactName: decryptedPayload.contactName,
|
|
598
|
+
avatarUrl: decryptedPayload.avatarUrl || void 0,
|
|
599
|
+
charId: decryptedPayload.charId,
|
|
600
|
+
metadata: decryptedPayload.metadata
|
|
601
|
+
});
|
|
602
|
+
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
603
|
+
_amsgshared.assertValidDecision.call(void 0, decision, { inlineToolCalls: true });
|
|
604
|
+
if (decision.decision === "continue") {
|
|
605
|
+
messages = decision.nextHistory.slice();
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (decision.decision === "skip-push") {
|
|
609
|
+
return { handled: true, result: { success: true, messagesSent: 0, status: "skipped", iterations: iteration + 1 } };
|
|
610
|
+
}
|
|
611
|
+
if (decision.decision === "finish") {
|
|
612
|
+
const messagesSent = await sendHookPushPayloads(decision.pushPayloads, decryptedPayload, ctx, sessionId, task, sleep);
|
|
613
|
+
return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
|
|
614
|
+
}
|
|
615
|
+
const toolCalls = _amsgshared.extractToolCallsFromDecision.call(void 0, decision);
|
|
616
|
+
if (toolCalls.length === 0) {
|
|
617
|
+
throw new Error("AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)");
|
|
618
|
+
}
|
|
619
|
+
if (typeof hooks.executeToolCalls !== "function") {
|
|
620
|
+
throw new Error("AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured");
|
|
621
|
+
}
|
|
622
|
+
if (iteration === maxToolIterations - 1) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
let toolResults;
|
|
626
|
+
try {
|
|
627
|
+
toolResults = await hooks.executeToolCalls(toolCalls, sessionCtx);
|
|
628
|
+
if (!Array.isArray(toolResults)) {
|
|
629
|
+
throw new TypeError('executeToolCalls must resolve to an array of { tool_call_id, role: "tool", content }');
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
toolResults = toolCalls.map((toolCall) => ({
|
|
633
|
+
tool_call_id: toolCall && typeof toolCall === "object" && typeof toolCall.id === "string" ? toolCall.id : "",
|
|
634
|
+
role: "tool",
|
|
635
|
+
content: `Tool execution failed: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _6 => _6.message]), () => ( String(error)))}`
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
const assistantWithTools = Array.isArray(assistantMessage.tool_calls) && assistantMessage.tool_calls.length > 0 ? assistantMessage : { ...assistantMessage, tool_calls: toolCalls };
|
|
639
|
+
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
640
|
+
}
|
|
641
|
+
throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
|
|
642
|
+
}
|
|
643
|
+
async function sendHookPushPayloads(pushPayloads, decryptedPayload, ctx, sessionId, task, sleep) {
|
|
644
|
+
if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
645
|
+
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
646
|
+
}
|
|
647
|
+
const pushSubscription = decryptedPayload.pushSubscription;
|
|
648
|
+
const total = pushPayloads.length;
|
|
649
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}`;
|
|
650
|
+
for (let i = 0; i < total; i++) {
|
|
651
|
+
const push = { ...pushPayloads[i] };
|
|
652
|
+
if (typeof push.messageId !== "string" || !push.messageId) push.messageId = `${messageIdBase}_hook_${i}`;
|
|
653
|
+
if (typeof push.sessionId !== "string" || !push.sessionId) push.sessionId = sessionId;
|
|
654
|
+
if (typeof push.timestamp !== "string" || !push.timestamp) push.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
655
|
+
push.messageIndex = i + 1;
|
|
656
|
+
push.totalMessages = total;
|
|
657
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(push));
|
|
658
|
+
if (i < total - 1) await sleep(SLEEP_BETWEEN_MESSAGES_MS);
|
|
659
|
+
}
|
|
660
|
+
return total;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/server/lib/message-processor.js
|
|
664
|
+
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
665
|
+
var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
|
|
334
666
|
function splitOnceByRegex(chunk, regex) {
|
|
335
667
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
336
668
|
if (i % 2 === 0 && part.trim()) {
|
|
@@ -356,8 +688,12 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
356
688
|
if (!masterKey) {
|
|
357
689
|
return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
|
|
358
690
|
}
|
|
359
|
-
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
360
|
-
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
691
|
+
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
692
|
+
const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
693
|
+
if (ctx.hooks && typeof ctx.hooks.onBeforeFire === "function" && taskNeedsLlm(decryptedPayload)) {
|
|
694
|
+
const agentic = await runAgenticFire({ task, decryptedPayload, userKey, ctx });
|
|
695
|
+
if (agentic.handled) return agentic.result;
|
|
696
|
+
}
|
|
361
697
|
let messageContent;
|
|
362
698
|
let llmResponse = null;
|
|
363
699
|
if (decryptedPayload.messageType === "fixed") {
|
|
@@ -365,7 +701,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
365
701
|
} else if (decryptedPayload.messageType === "instant") {
|
|
366
702
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
367
703
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
368
|
-
const aiResult = await
|
|
704
|
+
const aiResult = await callLlm(decryptedPayload);
|
|
369
705
|
messageContent = aiResult.content;
|
|
370
706
|
llmResponse = aiResult.response;
|
|
371
707
|
} else if (decryptedPayload.userMessage) {
|
|
@@ -374,7 +710,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
374
710
|
throw new Error("Invalid instant message: no content source available");
|
|
375
711
|
}
|
|
376
712
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
377
|
-
const aiResult = await
|
|
713
|
+
const aiResult = await callLlm(decryptedPayload);
|
|
378
714
|
messageContent = aiResult.content;
|
|
379
715
|
llmResponse = aiResult.response;
|
|
380
716
|
} else {
|
|
@@ -389,12 +725,12 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
389
725
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
390
726
|
}
|
|
391
727
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
392
|
-
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${
|
|
728
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
|
|
393
729
|
const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
|
|
394
730
|
const messageSubtype = decryptedPayload.messageSubtype || "chat";
|
|
395
731
|
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
396
732
|
const metadata = decryptedPayload.metadata || {};
|
|
397
|
-
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${
|
|
733
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
|
|
398
734
|
if (reasoning) {
|
|
399
735
|
const reasoningPush = _amsgshared.buildReasoningPush.call(void 0, {
|
|
400
736
|
messageType: decryptedPayload.messageType,
|
|
@@ -410,7 +746,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
410
746
|
metadata
|
|
411
747
|
});
|
|
412
748
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
413
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
|
|
414
750
|
}
|
|
415
751
|
for (let i = 0; i < messages.length; i++) {
|
|
416
752
|
const contentPush = _amsgshared.buildContentPush.call(void 0, {
|
|
@@ -431,7 +767,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
431
767
|
});
|
|
432
768
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
433
769
|
if (i < messages.length - 1) {
|
|
434
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
770
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
|
|
435
771
|
}
|
|
436
772
|
}
|
|
437
773
|
return { success: true, messagesSent: messages.length };
|
|
@@ -501,80 +837,6 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
501
837
|
return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
|
|
502
838
|
}
|
|
503
839
|
}
|
|
504
|
-
async function _callAI(payload) {
|
|
505
|
-
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
506
|
-
const requestBody = buildAiRequestBody(payload);
|
|
507
|
-
const aiResponse = await fetch(normalizedApiUrl, {
|
|
508
|
-
method: "POST",
|
|
509
|
-
headers: {
|
|
510
|
-
"Content-Type": "application/json",
|
|
511
|
-
"Authorization": `Bearer ${payload.apiKey}`
|
|
512
|
-
},
|
|
513
|
-
body: JSON.stringify(requestBody),
|
|
514
|
-
signal: AbortSignal.timeout(3e5)
|
|
515
|
-
});
|
|
516
|
-
if (!aiResponse.ok) {
|
|
517
|
-
if (aiResponse.status === 405) {
|
|
518
|
-
throw new Error(
|
|
519
|
-
`AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
throw new Error(
|
|
523
|
-
`AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
const aiData = await aiResponse.json();
|
|
527
|
-
const content = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.content]);
|
|
528
|
-
if (typeof content !== "string" || !content.trim()) {
|
|
529
|
-
throw new Error("AI API error: response missing choices[0].message.content");
|
|
530
|
-
}
|
|
531
|
-
return { response: aiData, content: content.trim() };
|
|
532
|
-
}
|
|
533
|
-
function buildAiRequestBody(payload) {
|
|
534
|
-
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
535
|
-
const requestBody = {
|
|
536
|
-
model: payload.primaryModel,
|
|
537
|
-
messages: llmMessages
|
|
538
|
-
};
|
|
539
|
-
if (payload.temperature !== void 0 && payload.temperature !== null) {
|
|
540
|
-
requestBody.temperature = payload.temperature;
|
|
541
|
-
} else if (!Array.isArray(payload.messages)) {
|
|
542
|
-
requestBody.temperature = 0.8;
|
|
543
|
-
}
|
|
544
|
-
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
545
|
-
return requestBody;
|
|
546
|
-
}
|
|
547
|
-
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
548
|
-
throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
|
|
549
|
-
}
|
|
550
|
-
requestBody.max_tokens = payload.maxTokens;
|
|
551
|
-
return requestBody;
|
|
552
|
-
}
|
|
553
|
-
function normalizeAiApiUrl(apiUrl) {
|
|
554
|
-
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
555
|
-
throw new Error(
|
|
556
|
-
"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)."
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
const trimmedApiUrl = apiUrl.trim();
|
|
560
|
-
let parsedUrl;
|
|
561
|
-
try {
|
|
562
|
-
parsedUrl = new URL(trimmedApiUrl);
|
|
563
|
-
} catch (e3) {
|
|
564
|
-
throw new Error(
|
|
565
|
-
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
569
|
-
if (/\/chat\/completions$/.test(path)) {
|
|
570
|
-
} else if (path === "/") {
|
|
571
|
-
path = "/v1/chat/completions";
|
|
572
|
-
} else if (/\/v\d+$/.test(path)) {
|
|
573
|
-
path = `${path}/chat/completions`;
|
|
574
|
-
}
|
|
575
|
-
parsedUrl.pathname = path;
|
|
576
|
-
return parsedUrl.toString();
|
|
577
|
-
}
|
|
578
840
|
|
|
579
841
|
// src/server/handlers/schedule-message.js
|
|
580
842
|
function createScheduleMessageHandler(ctx) {
|
|
@@ -606,10 +868,10 @@ function createScheduleMessageHandler(ctx) {
|
|
|
606
868
|
return { status: 400, body: { success: false, error: parsedBody.error } };
|
|
607
869
|
}
|
|
608
870
|
const encryptedBody = parsedBody.data;
|
|
871
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
609
872
|
let payload;
|
|
610
873
|
try {
|
|
611
|
-
|
|
612
|
-
payload = decryptPayload(encryptedBody, userKey2);
|
|
874
|
+
payload = await decryptPayload(encryptedBody, userKey);
|
|
613
875
|
} catch (error) {
|
|
614
876
|
if (error instanceof SyntaxError) {
|
|
615
877
|
return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
|
|
@@ -627,8 +889,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
627
889
|
if (!validationResult.valid) {
|
|
628
890
|
return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
|
|
629
891
|
}
|
|
630
|
-
const taskUuid = payload.uuid ||
|
|
631
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
892
|
+
const taskUuid = payload.uuid || randomUUID();
|
|
632
893
|
const fullTaskData = {
|
|
633
894
|
contactName: payload.contactName,
|
|
634
895
|
avatarUrl: payload.avatarUrl || null,
|
|
@@ -654,7 +915,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
654
915
|
pushSubscription: payload.pushSubscription,
|
|
655
916
|
metadata: payload.metadata || {}
|
|
656
917
|
};
|
|
657
|
-
const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
918
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
658
919
|
if (payload.messageType === "instant") {
|
|
659
920
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
660
921
|
return {
|
|
@@ -808,8 +1069,8 @@ async function runScheduledTick(ctx) {
|
|
|
808
1069
|
return;
|
|
809
1070
|
}
|
|
810
1071
|
try {
|
|
811
|
-
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
812
|
-
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
1072
|
+
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
1073
|
+
const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
813
1074
|
if (decryptedPayload.recurrenceType === "none") {
|
|
814
1075
|
await db.deleteTaskById(task.id);
|
|
815
1076
|
results.deletedOnceOffTasks++;
|
|
@@ -896,10 +1157,10 @@ function createUpdateMessageHandler(ctx) {
|
|
|
896
1157
|
return { status: 400, body: { success: false, error: parsedBody.error } };
|
|
897
1158
|
}
|
|
898
1159
|
const encryptedBody = parsedBody.data;
|
|
899
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
1160
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
900
1161
|
let updates;
|
|
901
1162
|
try {
|
|
902
|
-
updates = decryptPayload(encryptedBody, userKey);
|
|
1163
|
+
updates = await decryptPayload(encryptedBody, userKey);
|
|
903
1164
|
} catch (_error) {
|
|
904
1165
|
return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
|
|
905
1166
|
}
|
|
@@ -948,7 +1209,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
948
1209
|
}
|
|
949
1210
|
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" } } };
|
|
950
1211
|
}
|
|
951
|
-
const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
1212
|
+
const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
952
1213
|
const promptUpdates = {};
|
|
953
1214
|
if (updates.completePrompt) {
|
|
954
1215
|
promptUpdates.completePrompt = updates.completePrompt;
|
|
@@ -971,7 +1232,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
971
1232
|
// string fields above are.
|
|
972
1233
|
...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: _nullishCoalesce(updates.splitPattern, () => ( null)) }
|
|
973
1234
|
};
|
|
974
|
-
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
1235
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
975
1236
|
const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
|
|
976
1237
|
const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
|
|
977
1238
|
if (!result) {
|
|
@@ -1064,9 +1325,9 @@ function createMessagesHandler(ctx) {
|
|
|
1064
1325
|
return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
|
|
1065
1326
|
}
|
|
1066
1327
|
const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
|
|
1067
|
-
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
1068
|
-
const decryptedTasks = tasks.map((task) => {
|
|
1069
|
-
const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
1328
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1329
|
+
const decryptedTasks = await Promise.all(tasks.map(async (task) => {
|
|
1330
|
+
const decrypted = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
|
|
1070
1331
|
return {
|
|
1071
1332
|
id: task.id,
|
|
1072
1333
|
uuid: task.uuid,
|
|
@@ -1080,12 +1341,12 @@ function createMessagesHandler(ctx) {
|
|
|
1080
1341
|
createdAt: task.created_at,
|
|
1081
1342
|
updatedAt: task.updated_at
|
|
1082
1343
|
};
|
|
1083
|
-
});
|
|
1344
|
+
}));
|
|
1084
1345
|
const responsePayload = {
|
|
1085
1346
|
tasks: decryptedTasks,
|
|
1086
1347
|
pagination: { total, limit, offset, hasMore: offset + limit < total }
|
|
1087
1348
|
};
|
|
1088
|
-
const encryptedResponse = encryptPayload(responsePayload, userKey);
|
|
1349
|
+
const encryptedResponse = await encryptPayload(responsePayload, userKey);
|
|
1089
1350
|
return {
|
|
1090
1351
|
status: 200,
|
|
1091
1352
|
body: {
|
|
@@ -1155,6 +1416,16 @@ var SQLITE_INDEXES = [
|
|
|
1155
1416
|
critical: true
|
|
1156
1417
|
}
|
|
1157
1418
|
];
|
|
1419
|
+
var CLIENT_STATE_TABLE_SQL = `
|
|
1420
|
+
CREATE TABLE IF NOT EXISTS client_state (
|
|
1421
|
+
user_id TEXT NOT NULL,
|
|
1422
|
+
namespace TEXT NOT NULL,
|
|
1423
|
+
key TEXT NOT NULL,
|
|
1424
|
+
value TEXT NOT NULL,
|
|
1425
|
+
updated_at INTEGER NOT NULL,
|
|
1426
|
+
PRIMARY KEY (user_id, namespace, key)
|
|
1427
|
+
)
|
|
1428
|
+
`;
|
|
1158
1429
|
|
|
1159
1430
|
// src/server/adapters/d1.js
|
|
1160
1431
|
var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
@@ -1187,6 +1458,7 @@ var D1Adapter = class {
|
|
|
1187
1458
|
}
|
|
1188
1459
|
async initSchema() {
|
|
1189
1460
|
await this._db.prepare(SQLITE_TABLE_SQL).run();
|
|
1461
|
+
await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
|
|
1190
1462
|
const indexResults = [];
|
|
1191
1463
|
for (const index of SQLITE_INDEXES) {
|
|
1192
1464
|
try {
|
|
@@ -1213,6 +1485,7 @@ var D1Adapter = class {
|
|
|
1213
1485
|
}
|
|
1214
1486
|
async dropSchema() {
|
|
1215
1487
|
await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
|
|
1488
|
+
await this._db.prepare("DROP TABLE IF EXISTS client_state").run();
|
|
1216
1489
|
}
|
|
1217
1490
|
async createTask(params) {
|
|
1218
1491
|
const now = this._now();
|
|
@@ -1341,6 +1614,77 @@ var D1Adapter = class {
|
|
|
1341
1614
|
).bind(uuid, userId).first();
|
|
1342
1615
|
return row ? row.status : null;
|
|
1343
1616
|
}
|
|
1617
|
+
// ── client_state (single-user cloud state mirror) ──────────────────────
|
|
1618
|
+
/**
|
|
1619
|
+
* Batch upsert. Last-write-wins per (namespace, key): an entry older
|
|
1620
|
+
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
1621
|
+
* newer overwrites. Values arrive pre-encrypted (the handler encrypts).
|
|
1622
|
+
*
|
|
1623
|
+
* Uses D1's batch() — one network round trip for the whole set (implicit
|
|
1624
|
+
* transaction). The client calls this endpoint inside its few-seconds
|
|
1625
|
+
* background window, so N sequential round trips could eat the whole
|
|
1626
|
+
* window. Bindings without batch() (e.g. the sqlite test shim, custom
|
|
1627
|
+
* adapters) fall back to a sequential loop.
|
|
1628
|
+
*
|
|
1629
|
+
* @param {string} userId
|
|
1630
|
+
* @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
|
|
1631
|
+
* @returns {Promise<{ upserted: number, skipped: number }>}
|
|
1632
|
+
*/
|
|
1633
|
+
async upsertClientState(userId, entries) {
|
|
1634
|
+
const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
|
|
1635
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1636
|
+
ON CONFLICT (user_id, namespace, key) DO UPDATE SET
|
|
1637
|
+
value = excluded.value,
|
|
1638
|
+
updated_at = excluded.updated_at
|
|
1639
|
+
WHERE excluded.updated_at >= client_state.updated_at`;
|
|
1640
|
+
let results;
|
|
1641
|
+
if (typeof this._db.batch === "function") {
|
|
1642
|
+
const statements = entries.map(
|
|
1643
|
+
(entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt)
|
|
1644
|
+
);
|
|
1645
|
+
results = await this._db.batch(statements);
|
|
1646
|
+
} else {
|
|
1647
|
+
results = [];
|
|
1648
|
+
for (const entry of entries) {
|
|
1649
|
+
results.push(
|
|
1650
|
+
await this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt).run()
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
let upserted = 0;
|
|
1655
|
+
let skipped = 0;
|
|
1656
|
+
for (const res of results) {
|
|
1657
|
+
if (res.meta.changes > 0) upserted++;
|
|
1658
|
+
else skipped++;
|
|
1659
|
+
}
|
|
1660
|
+
return { upserted, skipped };
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* All entries of one namespace (values still encrypted).
|
|
1664
|
+
*
|
|
1665
|
+
* @param {string} userId
|
|
1666
|
+
* @param {string} namespace
|
|
1667
|
+
* @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
|
|
1668
|
+
*/
|
|
1669
|
+
async getClientState(userId, namespace) {
|
|
1670
|
+
const res = await this._db.prepare(
|
|
1671
|
+
`SELECT namespace, key, value, updated_at
|
|
1672
|
+
FROM client_state
|
|
1673
|
+
WHERE user_id = ? AND namespace = ?
|
|
1674
|
+
ORDER BY key ASC`
|
|
1675
|
+
).bind(userId, namespace).all();
|
|
1676
|
+
return res.results || [];
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Wipe every entry of this user.
|
|
1680
|
+
*
|
|
1681
|
+
* @param {string} userId
|
|
1682
|
+
* @returns {Promise<number>} rows deleted
|
|
1683
|
+
*/
|
|
1684
|
+
async clearClientState(userId) {
|
|
1685
|
+
const res = await this._db.prepare("DELETE FROM client_state WHERE user_id = ?").bind(userId).run();
|
|
1686
|
+
return res.meta.changes || 0;
|
|
1687
|
+
}
|
|
1344
1688
|
};
|
|
1345
1689
|
function createD1Adapter(db) {
|
|
1346
1690
|
if (!db || typeof db.prepare !== "function") {
|
|
@@ -1454,6 +1798,131 @@ function createVapidPublicKeyHandler(ctx) {
|
|
|
1454
1798
|
return { GET };
|
|
1455
1799
|
}
|
|
1456
1800
|
|
|
1801
|
+
// src/server/handlers/client-state.js
|
|
1802
|
+
var MAX_STATE_VALUE_BYTES = 200 * 1024;
|
|
1803
|
+
var MAX_STATE_ENTRIES_PER_REQUEST = 200;
|
|
1804
|
+
var MAX_NAMESPACE_CHARS = 128;
|
|
1805
|
+
var MAX_KEY_CHARS = 256;
|
|
1806
|
+
var utf82 = new TextEncoder();
|
|
1807
|
+
function err(status, code, message, details) {
|
|
1808
|
+
const error = details === void 0 ? { code, message } : { code, message, details };
|
|
1809
|
+
return { status, body: { success: false, error } };
|
|
1810
|
+
}
|
|
1811
|
+
function requireUserId(headers) {
|
|
1812
|
+
const userId = getHeader(headers, "x-user-id");
|
|
1813
|
+
if (!userId) return { error: err(400, "USER_ID_REQUIRED", "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26") };
|
|
1814
|
+
if (!isValidUUIDv4(userId)) return { error: err(400, "INVALID_USER_ID_FORMAT", "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F") };
|
|
1815
|
+
return { userId };
|
|
1816
|
+
}
|
|
1817
|
+
function validateEntry(entry, index) {
|
|
1818
|
+
if (!isPlainObject(entry)) return err(400, "INVALID_STATE_ENTRY", `entries[${index}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
1819
|
+
if (typeof entry.namespace !== "string" || !entry.namespace.trim() || entry.namespace.length > MAX_NAMESPACE_CHARS) {
|
|
1820
|
+
return err(400, "INVALID_STATE_NAMESPACE", `entries[${index}].namespace \u5FC5\u987B\u662F 1-${MAX_NAMESPACE_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
|
|
1821
|
+
}
|
|
1822
|
+
if (typeof entry.key !== "string" || !entry.key.trim() || entry.key.length > MAX_KEY_CHARS) {
|
|
1823
|
+
return err(400, "INVALID_STATE_KEY", `entries[${index}].key \u5FC5\u987B\u662F 1-${MAX_KEY_CHARS} \u5B57\u7B26\u7684\u5B57\u7B26\u4E32`);
|
|
1824
|
+
}
|
|
1825
|
+
if (typeof entry.value !== "string") {
|
|
1826
|
+
return err(400, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
|
|
1827
|
+
}
|
|
1828
|
+
const bytes = utf82.encode(entry.value).length;
|
|
1829
|
+
if (bytes > MAX_STATE_VALUE_BYTES) {
|
|
1830
|
+
return err(413, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u4E0A\u9650`, { index, key: entry.key, bytes, maxBytes: MAX_STATE_VALUE_BYTES });
|
|
1831
|
+
}
|
|
1832
|
+
if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
|
|
1833
|
+
return err(400, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
|
|
1834
|
+
}
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
function createClientStateHandler(ctx) {
|
|
1838
|
+
async function PUT(headers, body) {
|
|
1839
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers);
|
|
1840
|
+
if (!tenantResult.ok) return tenantResult.error;
|
|
1841
|
+
const { db, masterKey } = tenantResult.context;
|
|
1842
|
+
if (getHeader(headers, "x-payload-encrypted") !== "true") {
|
|
1843
|
+
return err(400, "ENCRYPTION_REQUIRED", "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6");
|
|
1844
|
+
}
|
|
1845
|
+
const gate = requireUserId(headers);
|
|
1846
|
+
if (gate.error) return gate.error;
|
|
1847
|
+
const { userId } = gate;
|
|
1848
|
+
if (getHeader(headers, "x-encryption-version") !== "1") {
|
|
1849
|
+
return err(400, "UNSUPPORTED_ENCRYPTION_VERSION", "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301");
|
|
1850
|
+
}
|
|
1851
|
+
const parsedBody = parseEncryptedBody(body);
|
|
1852
|
+
if (!parsedBody.ok) return { status: 400, body: { success: false, error: parsedBody.error } };
|
|
1853
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1854
|
+
let payload;
|
|
1855
|
+
try {
|
|
1856
|
+
payload = await decryptPayload(parsedBody.data, userKey);
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
if (error instanceof SyntaxError) {
|
|
1859
|
+
return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON");
|
|
1860
|
+
}
|
|
1861
|
+
return err(400, "DECRYPTION_FAILED", "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25");
|
|
1862
|
+
}
|
|
1863
|
+
if (!isPlainObject(payload)) return err(400, "INVALID_PAYLOAD_FORMAT", "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61");
|
|
1864
|
+
const entries = payload.entries;
|
|
1865
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
1866
|
+
return err(400, "INVALID_STATE_ENTRIES", "entries \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4");
|
|
1867
|
+
}
|
|
1868
|
+
if (entries.length > MAX_STATE_ENTRIES_PER_REQUEST) {
|
|
1869
|
+
return err(400, "TOO_MANY_STATE_ENTRIES", `\u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_REQUEST} \u6761`, { count: entries.length });
|
|
1870
|
+
}
|
|
1871
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1872
|
+
const invalid = validateEntry(entries[i], i);
|
|
1873
|
+
if (invalid) return invalid;
|
|
1874
|
+
}
|
|
1875
|
+
if (typeof db.upsertClientState !== "function") {
|
|
1876
|
+
return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
1877
|
+
}
|
|
1878
|
+
const encryptedEntries = await Promise.all(entries.map(async (entry) => ({
|
|
1879
|
+
namespace: entry.namespace,
|
|
1880
|
+
key: entry.key,
|
|
1881
|
+
value: await encryptForStorage(entry.value, userKey),
|
|
1882
|
+
updatedAt: entry.updatedAt
|
|
1883
|
+
})));
|
|
1884
|
+
const { upserted, skipped } = await db.upsertClientState(userId, encryptedEntries);
|
|
1885
|
+
return { status: 200, body: { success: true, data: { upserted, skipped } } };
|
|
1886
|
+
}
|
|
1887
|
+
async function GET(url, headers) {
|
|
1888
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers);
|
|
1889
|
+
if (!tenantResult.ok) return tenantResult.error;
|
|
1890
|
+
const { db, masterKey } = tenantResult.context;
|
|
1891
|
+
const gate = requireUserId(headers);
|
|
1892
|
+
if (gate.error) return gate.error;
|
|
1893
|
+
const { userId } = gate;
|
|
1894
|
+
const namespace = new URL(url, "https://dummy").searchParams.get("namespace") || "";
|
|
1895
|
+
if (!namespace.trim()) return err(400, "NAMESPACE_REQUIRED", "\u5FC5\u987B\u63D0\u4F9B namespace \u67E5\u8BE2\u53C2\u6570");
|
|
1896
|
+
if (typeof db.getClientState !== "function") {
|
|
1897
|
+
return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
1898
|
+
}
|
|
1899
|
+
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
1900
|
+
const rows = await db.getClientState(userId, namespace);
|
|
1901
|
+
const decrypted = await Promise.all(rows.map(async (row) => ({
|
|
1902
|
+
namespace: row.namespace,
|
|
1903
|
+
key: row.key,
|
|
1904
|
+
value: await decryptFromStorage(row.value, userKey),
|
|
1905
|
+
updatedAt: row.updated_at
|
|
1906
|
+
})));
|
|
1907
|
+
const encryptedResponse = await encryptPayload({ namespace, entries: decrypted }, userKey);
|
|
1908
|
+
return { status: 200, body: { success: true, encrypted: true, version: 1, data: encryptedResponse } };
|
|
1909
|
+
}
|
|
1910
|
+
async function DELETE(url, headers) {
|
|
1911
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers);
|
|
1912
|
+
if (!tenantResult.ok) return tenantResult.error;
|
|
1913
|
+
const { db } = tenantResult.context;
|
|
1914
|
+
const gate = requireUserId(headers);
|
|
1915
|
+
if (gate.error) return gate.error;
|
|
1916
|
+
const { userId } = gate;
|
|
1917
|
+
if (typeof db.clearClientState !== "function") {
|
|
1918
|
+
return err(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
|
|
1919
|
+
}
|
|
1920
|
+
const deleted = await db.clearClientState(userId);
|
|
1921
|
+
return { status: 200, body: { success: true, data: { deleted } } };
|
|
1922
|
+
}
|
|
1923
|
+
return { PUT, GET, DELETE };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1457
1926
|
// src/server/single-user.js
|
|
1458
1927
|
function createSingleUserServer(config) {
|
|
1459
1928
|
if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
|
|
@@ -1471,7 +1940,13 @@ function createSingleUserServer(config) {
|
|
|
1471
1940
|
privateKey: vapid.privateKey || ""
|
|
1472
1941
|
},
|
|
1473
1942
|
webpush: config.webpush || null,
|
|
1474
|
-
tenantManager
|
|
1943
|
+
tenantManager,
|
|
1944
|
+
// Fire-time hooks (optional): the in-server instant path fires through
|
|
1945
|
+
// processMessagesByUuid with this ctx, so instant-type tasks can take
|
|
1946
|
+
// the agentic path too.
|
|
1947
|
+
hooks: config.hooks || null,
|
|
1948
|
+
maxToolIterations: config.maxToolIterations,
|
|
1949
|
+
totalTimeoutMs: config.totalTimeoutMs
|
|
1475
1950
|
};
|
|
1476
1951
|
return {
|
|
1477
1952
|
ctx,
|
|
@@ -1482,36 +1957,12 @@ function createSingleUserServer(config) {
|
|
|
1482
1957
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
1483
1958
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
1484
1959
|
messages: createMessagesHandler(ctx),
|
|
1485
|
-
vapidPublicKey: createVapidPublicKeyHandler(ctx)
|
|
1960
|
+
vapidPublicKey: createVapidPublicKeyHandler(ctx),
|
|
1961
|
+
clientState: createClientStateHandler(ctx)
|
|
1486
1962
|
}
|
|
1487
1963
|
};
|
|
1488
1964
|
}
|
|
1489
1965
|
|
|
1490
|
-
// src/server/lib/webcrypto-utils.js
|
|
1491
|
-
|
|
1492
|
-
var TEXT_ENCODER = new TextEncoder();
|
|
1493
|
-
var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
|
|
1494
|
-
function utf8(str) {
|
|
1495
|
-
return TEXT_ENCODER.encode(String(str));
|
|
1496
|
-
}
|
|
1497
|
-
function bytesToBase64Url(buf) {
|
|
1498
|
-
const bytes = _amsgshared.toUint8.call(void 0, buf);
|
|
1499
|
-
let bin = "";
|
|
1500
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1501
|
-
bin += String.fromCharCode(bytes[i]);
|
|
1502
|
-
}
|
|
1503
|
-
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
1504
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1505
|
-
}
|
|
1506
|
-
function jsonToBase64Url(value) {
|
|
1507
|
-
return bytesToBase64Url(utf8(JSON.stringify(value)));
|
|
1508
|
-
}
|
|
1509
|
-
function randomBytes2(n) {
|
|
1510
|
-
const out = new Uint8Array(n);
|
|
1511
|
-
globalThis.crypto.getRandomValues(out);
|
|
1512
|
-
return out;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
1966
|
// src/server/lib/webpush-webcrypto.js
|
|
1516
1967
|
|
|
1517
1968
|
var KEY_INFO_PREFIX = utf8("WebPush: info\0");
|
|
@@ -1561,12 +2012,12 @@ async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl
|
|
|
1561
2012
|
});
|
|
1562
2013
|
if (!res.ok) {
|
|
1563
2014
|
const text = await safeReadText(res);
|
|
1564
|
-
const
|
|
2015
|
+
const err2 = new Error(
|
|
1565
2016
|
`Web Push delivery failed: ${res.status} ${res.statusText || ""}${text ? ` \u2014 ${text}` : ""}`
|
|
1566
2017
|
);
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
throw
|
|
2018
|
+
err2.code = "PUSH_SEND_FAILED";
|
|
2019
|
+
err2.statusCode = res.status;
|
|
2020
|
+
throw err2;
|
|
1570
2021
|
}
|
|
1571
2022
|
return {
|
|
1572
2023
|
statusCode: res.status,
|
|
@@ -1599,7 +2050,7 @@ async function encryptPushPayload({ plaintext, uaPublicKey, authSecret }) {
|
|
|
1599
2050
|
);
|
|
1600
2051
|
const keyInfo = _amsgshared.concatBytes.call(void 0, KEY_INFO_PREFIX, uaPublicKey, asPublicRaw);
|
|
1601
2052
|
const ikm = await hkdfSha256(authSecret, ecdhSecret, keyInfo, 32);
|
|
1602
|
-
const salt =
|
|
2053
|
+
const salt = randomBytes(16);
|
|
1603
2054
|
const cekBytes = await hkdfSha256(salt, ikm, CEK_INFO, 16);
|
|
1604
2055
|
const cek = await globalThis.crypto.subtle.importKey(
|
|
1605
2056
|
"raw",
|
|
@@ -1773,6 +2224,12 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
1773
2224
|
result = await server.handlers.cancelMessage.DELETE(url, headers);
|
|
1774
2225
|
} else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
|
|
1775
2226
|
result = await server.handlers.vapidPublicKey.GET(url, headers);
|
|
2227
|
+
} else if (method === "PUT" && pathname.endsWith("/client-state")) {
|
|
2228
|
+
result = await server.handlers.clientState.PUT(headers, await request.text());
|
|
2229
|
+
} else if (method === "GET" && pathname.endsWith("/client-state")) {
|
|
2230
|
+
result = await server.handlers.clientState.GET(url, headers);
|
|
2231
|
+
} else if (method === "DELETE" && pathname.endsWith("/client-state")) {
|
|
2232
|
+
result = await server.handlers.clientState.DELETE(url, headers);
|
|
1776
2233
|
} else {
|
|
1777
2234
|
result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
|
|
1778
2235
|
}
|
|
@@ -1790,7 +2247,17 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
1790
2247
|
return;
|
|
1791
2248
|
}
|
|
1792
2249
|
try {
|
|
1793
|
-
await runScheduledTick({
|
|
2250
|
+
await runScheduledTick({
|
|
2251
|
+
db: cfg.db,
|
|
2252
|
+
masterKey: cfg.masterKey,
|
|
2253
|
+
vapid,
|
|
2254
|
+
webpush: cfg.webpush,
|
|
2255
|
+
// Fire-time hooks (optional; see lib/agentic-fire.js). runScheduledTick
|
|
2256
|
+
// spreads its ctx into processSingleMessage, so these ride along.
|
|
2257
|
+
hooks: cfg.hooks || null,
|
|
2258
|
+
maxToolIterations: cfg.maxToolIterations,
|
|
2259
|
+
totalTimeoutMs: cfg.totalTimeoutMs
|
|
2260
|
+
});
|
|
1794
2261
|
} catch (error) {
|
|
1795
2262
|
console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
|
|
1796
2263
|
}
|