@rei-standard/amsg-server 2.6.0-next.2 → 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-GN5QTMCL.cjs → chunk-L7WMM33Q.cjs} +512 -89
- package/dist/{chunk-PVNLB6TF.mjs → chunk-PPPWETND.mjs} +512 -89
- package/dist/{cloudflare-B-E51VjP.d.cts → cloudflare-DlLTdCp0.d.cts} +787 -160
- package/dist/{cloudflare-B-E51VjP.d.ts → cloudflare-DlLTdCp0.d.ts} +787 -160
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- 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
|
};
|
|
@@ -399,8 +399,270 @@ import {
|
|
|
399
399
|
readReasoningContent,
|
|
400
400
|
stripReasoningTags
|
|
401
401
|
} from "@rei-standard/amsg-shared";
|
|
402
|
-
|
|
402
|
+
|
|
403
|
+
// src/server/lib/llm.js
|
|
404
|
+
async function callLlm(payload, options = {}) {
|
|
405
|
+
const requireContent = options.requireContent !== false;
|
|
406
|
+
const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 3e5;
|
|
407
|
+
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
408
|
+
const requestBody = buildAiRequestBody(payload);
|
|
409
|
+
const aiResponse = await fetch(normalizedApiUrl, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: {
|
|
412
|
+
"Content-Type": "application/json",
|
|
413
|
+
"Authorization": `Bearer ${payload.apiKey}`
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify(requestBody),
|
|
416
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
417
|
+
});
|
|
418
|
+
if (!aiResponse.ok) {
|
|
419
|
+
if (aiResponse.status === 405) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
throw new Error(
|
|
425
|
+
`AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const aiData = await aiResponse.json();
|
|
429
|
+
const rawContent = aiData?.choices?.[0]?.message?.content;
|
|
430
|
+
if (requireContent && (typeof rawContent !== "string" || !rawContent.trim())) {
|
|
431
|
+
throw new Error("AI API error: response missing choices[0].message.content");
|
|
432
|
+
}
|
|
433
|
+
return { response: aiData, content: typeof rawContent === "string" ? rawContent.trim() : "" };
|
|
434
|
+
}
|
|
435
|
+
function buildAiRequestBody(payload) {
|
|
436
|
+
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
437
|
+
const requestBody = {
|
|
438
|
+
model: payload.primaryModel,
|
|
439
|
+
messages: llmMessages
|
|
440
|
+
};
|
|
441
|
+
if (payload.temperature !== void 0 && payload.temperature !== null) {
|
|
442
|
+
requestBody.temperature = payload.temperature;
|
|
443
|
+
} else if (!Array.isArray(payload.messages)) {
|
|
444
|
+
requestBody.temperature = 0.8;
|
|
445
|
+
}
|
|
446
|
+
if (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 {
|
|
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
|
+
import {
|
|
483
|
+
assertValidDecision,
|
|
484
|
+
buildSessionContext,
|
|
485
|
+
extractAssistantMessage,
|
|
486
|
+
extractToolCallsFromDecision
|
|
487
|
+
} from "@rei-standard/amsg-shared";
|
|
488
|
+
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
489
|
+
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
403
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: task.id ?? null,
|
|
509
|
+
uuid: task.uuid ?? null,
|
|
510
|
+
nextSendAt: task.next_send_at ?? null,
|
|
511
|
+
retryCount: 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 = extractAssistantMessage(llmResponse);
|
|
591
|
+
messages = [...messages, assistantMessage];
|
|
592
|
+
const sessionCtx = buildSessionContext({
|
|
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
|
+
assertValidDecision(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 = extractToolCallsFromDecision(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: ${error?.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;
|
|
404
666
|
function splitOnceByRegex(chunk, regex) {
|
|
405
667
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
406
668
|
if (i % 2 === 0 && part.trim()) {
|
|
@@ -428,6 +690,10 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
428
690
|
}
|
|
429
691
|
const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
|
|
430
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
|
+
}
|
|
431
697
|
let messageContent;
|
|
432
698
|
let llmResponse = null;
|
|
433
699
|
if (decryptedPayload.messageType === "fixed") {
|
|
@@ -435,7 +701,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
435
701
|
} else if (decryptedPayload.messageType === "instant") {
|
|
436
702
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
437
703
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
438
|
-
const aiResult = await
|
|
704
|
+
const aiResult = await callLlm(decryptedPayload);
|
|
439
705
|
messageContent = aiResult.content;
|
|
440
706
|
llmResponse = aiResult.response;
|
|
441
707
|
} else if (decryptedPayload.userMessage) {
|
|
@@ -444,7 +710,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
444
710
|
throw new Error("Invalid instant message: no content source available");
|
|
445
711
|
}
|
|
446
712
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
447
|
-
const aiResult = await
|
|
713
|
+
const aiResult = await callLlm(decryptedPayload);
|
|
448
714
|
messageContent = aiResult.content;
|
|
449
715
|
llmResponse = aiResult.response;
|
|
450
716
|
} else {
|
|
@@ -480,7 +746,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
480
746
|
metadata
|
|
481
747
|
});
|
|
482
748
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
483
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
749
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
|
|
484
750
|
}
|
|
485
751
|
for (let i = 0; i < messages.length; i++) {
|
|
486
752
|
const contentPush = buildContentPush({
|
|
@@ -501,7 +767,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
501
767
|
});
|
|
502
768
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
503
769
|
if (i < messages.length - 1) {
|
|
504
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
770
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS2));
|
|
505
771
|
}
|
|
506
772
|
}
|
|
507
773
|
return { success: true, messagesSent: messages.length };
|
|
@@ -571,80 +837,6 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
571
837
|
return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
|
|
572
838
|
}
|
|
573
839
|
}
|
|
574
|
-
async function _callAI(payload) {
|
|
575
|
-
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
576
|
-
const requestBody = buildAiRequestBody(payload);
|
|
577
|
-
const aiResponse = await fetch(normalizedApiUrl, {
|
|
578
|
-
method: "POST",
|
|
579
|
-
headers: {
|
|
580
|
-
"Content-Type": "application/json",
|
|
581
|
-
"Authorization": `Bearer ${payload.apiKey}`
|
|
582
|
-
},
|
|
583
|
-
body: JSON.stringify(requestBody),
|
|
584
|
-
signal: AbortSignal.timeout(3e5)
|
|
585
|
-
});
|
|
586
|
-
if (!aiResponse.ok) {
|
|
587
|
-
if (aiResponse.status === 405) {
|
|
588
|
-
throw new Error(
|
|
589
|
-
`AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
throw new Error(
|
|
593
|
-
`AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
const aiData = await aiResponse.json();
|
|
597
|
-
const content = aiData?.choices?.[0]?.message?.content;
|
|
598
|
-
if (typeof content !== "string" || !content.trim()) {
|
|
599
|
-
throw new Error("AI API error: response missing choices[0].message.content");
|
|
600
|
-
}
|
|
601
|
-
return { response: aiData, content: content.trim() };
|
|
602
|
-
}
|
|
603
|
-
function buildAiRequestBody(payload) {
|
|
604
|
-
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
605
|
-
const requestBody = {
|
|
606
|
-
model: payload.primaryModel,
|
|
607
|
-
messages: llmMessages
|
|
608
|
-
};
|
|
609
|
-
if (payload.temperature !== void 0 && payload.temperature !== null) {
|
|
610
|
-
requestBody.temperature = payload.temperature;
|
|
611
|
-
} else if (!Array.isArray(payload.messages)) {
|
|
612
|
-
requestBody.temperature = 0.8;
|
|
613
|
-
}
|
|
614
|
-
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
615
|
-
return requestBody;
|
|
616
|
-
}
|
|
617
|
-
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
618
|
-
throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
|
|
619
|
-
}
|
|
620
|
-
requestBody.max_tokens = payload.maxTokens;
|
|
621
|
-
return requestBody;
|
|
622
|
-
}
|
|
623
|
-
function normalizeAiApiUrl(apiUrl) {
|
|
624
|
-
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
625
|
-
throw new Error(
|
|
626
|
-
"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)."
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
const trimmedApiUrl = apiUrl.trim();
|
|
630
|
-
let parsedUrl;
|
|
631
|
-
try {
|
|
632
|
-
parsedUrl = new URL(trimmedApiUrl);
|
|
633
|
-
} catch {
|
|
634
|
-
throw new Error(
|
|
635
|
-
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
639
|
-
if (/\/chat\/completions$/.test(path)) {
|
|
640
|
-
} else if (path === "/") {
|
|
641
|
-
path = "/v1/chat/completions";
|
|
642
|
-
} else if (/\/v\d+$/.test(path)) {
|
|
643
|
-
path = `${path}/chat/completions`;
|
|
644
|
-
}
|
|
645
|
-
parsedUrl.pathname = path;
|
|
646
|
-
return parsedUrl.toString();
|
|
647
|
-
}
|
|
648
840
|
|
|
649
841
|
// src/server/handlers/schedule-message.js
|
|
650
842
|
function createScheduleMessageHandler(ctx) {
|
|
@@ -1224,6 +1416,16 @@ var SQLITE_INDEXES = [
|
|
|
1224
1416
|
critical: true
|
|
1225
1417
|
}
|
|
1226
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
|
+
`;
|
|
1227
1429
|
|
|
1228
1430
|
// src/server/adapters/d1.js
|
|
1229
1431
|
var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
@@ -1256,6 +1458,7 @@ var D1Adapter = class {
|
|
|
1256
1458
|
}
|
|
1257
1459
|
async initSchema() {
|
|
1258
1460
|
await this._db.prepare(SQLITE_TABLE_SQL).run();
|
|
1461
|
+
await this._db.prepare(CLIENT_STATE_TABLE_SQL).run();
|
|
1259
1462
|
const indexResults = [];
|
|
1260
1463
|
for (const index of SQLITE_INDEXES) {
|
|
1261
1464
|
try {
|
|
@@ -1282,6 +1485,7 @@ var D1Adapter = class {
|
|
|
1282
1485
|
}
|
|
1283
1486
|
async dropSchema() {
|
|
1284
1487
|
await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
|
|
1488
|
+
await this._db.prepare("DROP TABLE IF EXISTS client_state").run();
|
|
1285
1489
|
}
|
|
1286
1490
|
async createTask(params) {
|
|
1287
1491
|
const now = this._now();
|
|
@@ -1410,6 +1614,77 @@ var D1Adapter = class {
|
|
|
1410
1614
|
).bind(uuid, userId).first();
|
|
1411
1615
|
return row ? row.status : null;
|
|
1412
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
|
+
}
|
|
1413
1688
|
};
|
|
1414
1689
|
function createD1Adapter(db) {
|
|
1415
1690
|
if (!db || typeof db.prepare !== "function") {
|
|
@@ -1523,6 +1798,131 @@ function createVapidPublicKeyHandler(ctx) {
|
|
|
1523
1798
|
return { GET };
|
|
1524
1799
|
}
|
|
1525
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
|
+
|
|
1526
1926
|
// src/server/single-user.js
|
|
1527
1927
|
function createSingleUserServer(config) {
|
|
1528
1928
|
if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
|
|
@@ -1540,7 +1940,13 @@ function createSingleUserServer(config) {
|
|
|
1540
1940
|
privateKey: vapid.privateKey || ""
|
|
1541
1941
|
},
|
|
1542
1942
|
webpush: config.webpush || null,
|
|
1543
|
-
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
|
|
1544
1950
|
};
|
|
1545
1951
|
return {
|
|
1546
1952
|
ctx,
|
|
@@ -1551,7 +1957,8 @@ function createSingleUserServer(config) {
|
|
|
1551
1957
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
1552
1958
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
1553
1959
|
messages: createMessagesHandler(ctx),
|
|
1554
|
-
vapidPublicKey: createVapidPublicKeyHandler(ctx)
|
|
1960
|
+
vapidPublicKey: createVapidPublicKeyHandler(ctx),
|
|
1961
|
+
clientState: createClientStateHandler(ctx)
|
|
1555
1962
|
}
|
|
1556
1963
|
};
|
|
1557
1964
|
}
|
|
@@ -1605,12 +2012,12 @@ async function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl
|
|
|
1605
2012
|
});
|
|
1606
2013
|
if (!res.ok) {
|
|
1607
2014
|
const text = await safeReadText(res);
|
|
1608
|
-
const
|
|
2015
|
+
const err2 = new Error(
|
|
1609
2016
|
`Web Push delivery failed: ${res.status} ${res.statusText || ""}${text ? ` \u2014 ${text}` : ""}`
|
|
1610
2017
|
);
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
throw
|
|
2018
|
+
err2.code = "PUSH_SEND_FAILED";
|
|
2019
|
+
err2.statusCode = res.status;
|
|
2020
|
+
throw err2;
|
|
1614
2021
|
}
|
|
1615
2022
|
return {
|
|
1616
2023
|
statusCode: res.status,
|
|
@@ -1817,6 +2224,12 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
1817
2224
|
result = await server.handlers.cancelMessage.DELETE(url, headers);
|
|
1818
2225
|
} else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
|
|
1819
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);
|
|
1820
2233
|
} else {
|
|
1821
2234
|
result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
|
|
1822
2235
|
}
|
|
@@ -1834,7 +2247,17 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
1834
2247
|
return;
|
|
1835
2248
|
}
|
|
1836
2249
|
try {
|
|
1837
|
-
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
|
+
});
|
|
1838
2261
|
} catch (error) {
|
|
1839
2262
|
console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
|
|
1840
2263
|
}
|