@rei-standard/amsg-server 2.6.0-next.20 → 2.6.0-next.21
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/README.md +154 -7
- package/dist/adapters/d1.d.ts +28 -0
- package/dist/adapters/interface.d.ts +46 -5
- package/dist/adapters/neon.d.ts +7 -1
- package/dist/adapters/schema.d.ts +16 -0
- package/dist/{chunk-V6NZQ22S.cjs → chunk-7NMQFTDJ.cjs} +5 -1
- package/dist/{chunk-5FXVSC5O.mjs → chunk-GN44PST5.mjs} +5 -1
- package/dist/{chunk-HTGYGGUH.mjs → chunk-KOJQVOYS.mjs} +1125 -276
- package/dist/{chunk-WOXSQBAM.cjs → chunk-QHVID3V2.cjs} +1114 -265
- package/dist/cloudflare.cjs +7 -3
- package/dist/cloudflare.d.ts +1 -0
- package/dist/cloudflare.mjs +6 -2
- package/dist/index.cjs +40 -28
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.mjs +16 -4
- package/dist/lib/client-state-store.d.ts +19 -0
- package/dist/lib/errors.d.ts +129 -7
- package/dist/lib/message-processor.d.ts +18 -1
- package/dist/lib/outbox-store.d.ts +62 -0
- package/dist/lib/push-subscription-store.d.ts +23 -0
- package/dist/lib/request.d.ts +47 -0
- package/dist/lib/result-emitter.d.ts +54 -0
- package/dist/lib/run-tick.d.ts +9 -0
- package/dist/lib/task-projection.d.ts +8 -3
- package/dist/lib/validation.d.ts +35 -0
- package/dist/{neon-U6TQFKMJ.cjs → neon-KP2CPA57.cjs} +23 -15
- package/dist/{neon-Y4KEZO5C.mjs → neon-ZIMHALYI.mjs} +14 -6
- package/dist/{pg-GWJZBWAM.mjs → pg-QGC2XHUT.mjs} +13 -5
- package/dist/{pg-B324Q2R4.cjs → pg-RZPQGXR3.cjs} +22 -14
- package/dist/single-user.d.ts +1 -0
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
TASK_DELIVERY_COLUMNS,
|
|
3
|
+
TASK_DETAIL_COLUMNS,
|
|
2
4
|
UPDATABLE_COLUMNS
|
|
3
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GN44PST5.mjs";
|
|
4
6
|
|
|
5
7
|
// src/server/lib/recurrence.js
|
|
6
8
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -405,6 +407,26 @@ function validateLlmMessagesArray(messages) {
|
|
|
405
407
|
return `messages[${i}].content must be a non-empty string or a non-empty array`;
|
|
406
408
|
}
|
|
407
409
|
}
|
|
410
|
+
var D1_MAX_ROW_BYTES = 2e6;
|
|
411
|
+
var STORAGE_HEX_OVERHEAD_BYTES = 66;
|
|
412
|
+
var TASK_ROW_OTHER_COLUMNS_BYTES = 4096;
|
|
413
|
+
var TASK_LAST_ERROR_RESERVED_BYTES = 2048;
|
|
414
|
+
var MAX_TASK_PAYLOAD_BYTES = Math.floor(
|
|
415
|
+
(D1_MAX_ROW_BYTES - TASK_ROW_OTHER_COLUMNS_BYTES - STORAGE_HEX_OVERHEAD_BYTES) / 2
|
|
416
|
+
) - TASK_LAST_ERROR_RESERVED_BYTES;
|
|
417
|
+
var taskPayloadEncoder = new TextEncoder();
|
|
418
|
+
function validateTaskPayloadSize(serializedPayload) {
|
|
419
|
+
const bytes = taskPayloadByteLength(serializedPayload);
|
|
420
|
+
if (bytes <= MAX_TASK_PAYLOAD_BYTES) return null;
|
|
421
|
+
return {
|
|
422
|
+
code: "TASK_PAYLOAD_TOO_LARGE",
|
|
423
|
+
message: `\u4EFB\u52A1\u5185\u5BB9 ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7 ${MAX_TASK_PAYLOAD_BYTES} \u5B57\u8282\u4E0A\u9650`,
|
|
424
|
+
details: { bytes, maxBytes: MAX_TASK_PAYLOAD_BYTES }
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function taskPayloadByteLength(serializedPayload) {
|
|
428
|
+
return taskPayloadEncoder.encode(serializedPayload).length;
|
|
429
|
+
}
|
|
408
430
|
function validateScheduleMessagePayload(payload) {
|
|
409
431
|
if (!payload.contactName || typeof payload.contactName !== "string") {
|
|
410
432
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
|
|
@@ -456,6 +478,9 @@ function validateScheduleMessagePayload(payload) {
|
|
|
456
478
|
if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
|
|
457
479
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
|
|
458
480
|
}
|
|
481
|
+
if (payload.userMessage !== void 0 && payload.userMessage !== null && typeof payload.userMessage !== "string") {
|
|
482
|
+
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["userMessage (must be a string)"] } };
|
|
483
|
+
}
|
|
459
484
|
if (payload.messageType === "fixed") {
|
|
460
485
|
if (!payload.userMessage) {
|
|
461
486
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["userMessage (required for fixed type)"] } };
|
|
@@ -554,6 +579,89 @@ var REQUEST_ERRORS = {
|
|
|
554
579
|
INVALID_REQUEST_BODY: { code: "INVALID_REQUEST_BODY", message: "\u8BF7\u6C42\u4F53\u683C\u5F0F\u65E0\u6548" },
|
|
555
580
|
INVALID_ENCRYPTED_PAYLOAD: { code: "INVALID_ENCRYPTED_PAYLOAD", message: "\u52A0\u5BC6\u6570\u636E\u683C\u5F0F\u9519\u8BEF" }
|
|
556
581
|
};
|
|
582
|
+
var DEFAULT_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
|
|
583
|
+
var GZIP_MAGIC_0 = 31;
|
|
584
|
+
var GZIP_MAGIC_1 = 139;
|
|
585
|
+
function normalizeContentEncoding(raw) {
|
|
586
|
+
const value = String(raw || "").trim().toLowerCase();
|
|
587
|
+
if (!value) return "";
|
|
588
|
+
const tokens = value.split(",").map((t) => t.trim()).filter((t) => t && t !== "identity");
|
|
589
|
+
if (tokens.length === 0) return "";
|
|
590
|
+
if (tokens.length === 1) return tokens[0];
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
function looksGzipped(bytes) {
|
|
594
|
+
return bytes.length >= 2 && bytes[0] === GZIP_MAGIC_0 && bytes[1] === GZIP_MAGIC_1;
|
|
595
|
+
}
|
|
596
|
+
async function inflateGzipToText(bytes, maxBytes) {
|
|
597
|
+
const reader = new Response(bytes).body.pipeThrough(new DecompressionStream("gzip")).getReader();
|
|
598
|
+
const decoder = new TextDecoder("utf-8");
|
|
599
|
+
let text = "";
|
|
600
|
+
let total = 0;
|
|
601
|
+
for (; ; ) {
|
|
602
|
+
const { done, value } = await reader.read();
|
|
603
|
+
if (done) break;
|
|
604
|
+
total += value.byteLength;
|
|
605
|
+
if (total > maxBytes) {
|
|
606
|
+
await reader.cancel();
|
|
607
|
+
const error = new Error(`\u89E3\u538B\u540E\u7684\u8BF7\u6C42\u4F53\u8D85\u8FC7 ${maxBytes} \u5B57\u8282\u4E0A\u9650`);
|
|
608
|
+
error.code = "REQUEST_BODY_TOO_LARGE";
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
text += decoder.decode(value, { stream: true });
|
|
612
|
+
}
|
|
613
|
+
return text + decoder.decode();
|
|
614
|
+
}
|
|
615
|
+
async function readRequestBody(request, options = {}) {
|
|
616
|
+
const encoding = normalizeContentEncoding(
|
|
617
|
+
request.headers && typeof request.headers.get === "function" ? request.headers.get("content-encoding") : ""
|
|
618
|
+
);
|
|
619
|
+
if (!encoding) return { ok: true, body: await request.text() };
|
|
620
|
+
if (encoding !== "gzip" && encoding !== "x-gzip") {
|
|
621
|
+
return {
|
|
622
|
+
ok: false,
|
|
623
|
+
error: errorResponse(
|
|
624
|
+
415,
|
|
625
|
+
"UNSUPPORTED_CONTENT_ENCODING",
|
|
626
|
+
`\u8BF7\u6C42\u4F53\u7684 Content-Encoding\u300C${encoding}\u300D\u4E0D\u652F\u6301\uFF0C\u8FD9\u91CC\u53EA\u8BA4 gzip`,
|
|
627
|
+
{ contentEncoding: encoding, supported: ["gzip"] }
|
|
628
|
+
)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const bytes = new Uint8Array(await request.arrayBuffer());
|
|
632
|
+
if (!looksGzipped(bytes)) return { ok: true, body: new TextDecoder("utf-8").decode(bytes) };
|
|
633
|
+
if (typeof DecompressionStream !== "function") {
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
error: errorResponse(
|
|
637
|
+
415,
|
|
638
|
+
"UNSUPPORTED_CONTENT_ENCODING",
|
|
639
|
+
"\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u6CA1\u6709 DecompressionStream\uFF0C\u6536\u4E0D\u4E86 gzip \u8BF7\u6C42\u4F53",
|
|
640
|
+
{ contentEncoding: encoding }
|
|
641
|
+
)
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_REQUEST_BODY_BYTES;
|
|
645
|
+
try {
|
|
646
|
+
return { ok: true, body: await inflateGzipToText(bytes, maxBytes) };
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (error && error.code === "REQUEST_BODY_TOO_LARGE") {
|
|
649
|
+
return {
|
|
650
|
+
ok: false,
|
|
651
|
+
error: errorResponse(413, "REQUEST_BODY_TOO_LARGE", error.message, { maxBytes })
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
error: errorResponse(
|
|
657
|
+
400,
|
|
658
|
+
"INVALID_CONTENT_ENCODING",
|
|
659
|
+
"\u8BF7\u6C42\u4F53\u58F0\u660E\u662F gzip\uFF0C\u4F46\u89E3\u538B\u5931\u8D25",
|
|
660
|
+
{ contentEncoding: encoding }
|
|
661
|
+
)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
557
665
|
function parseBodyAsObject(body, options = {}) {
|
|
558
666
|
const invalidJson = options.invalidJson || REQUEST_ERRORS.INVALID_JSON;
|
|
559
667
|
const invalidType = options.invalidType || REQUEST_ERRORS.INVALID_REQUEST_BODY;
|
|
@@ -646,6 +754,184 @@ function createGetUserKeyHandler(ctx) {
|
|
|
646
754
|
return { GET };
|
|
647
755
|
}
|
|
648
756
|
|
|
757
|
+
// src/server/lib/webpush-webcrypto.js
|
|
758
|
+
import { sendWebPush as sharedSendWebPush } from "@rei-standard/amsg-shared";
|
|
759
|
+
import { buildVapidJwt, verifyVapidJwt, hmacSha256 as hmacSha2562 } from "@rei-standard/amsg-shared";
|
|
760
|
+
var WEB_PUSH_MAX_BODY_BYTES = 4096;
|
|
761
|
+
var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
|
|
762
|
+
var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
|
|
763
|
+
var PUSH_ENVELOPE_RESERVED_BYTES = 384;
|
|
764
|
+
var payloadEncoder = new TextEncoder();
|
|
765
|
+
function measurePushPayload(payload, options) {
|
|
766
|
+
const reserveEnvelope = !!(options && options.reserveEnvelope);
|
|
767
|
+
const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
|
|
768
|
+
const envelopeReservedBytes = reserveEnvelope ? PUSH_ENVELOPE_RESERVED_BYTES : 0;
|
|
769
|
+
const maxBytes = MAX_PUSH_PAYLOAD_BYTES - envelopeReservedBytes;
|
|
770
|
+
return {
|
|
771
|
+
bytes,
|
|
772
|
+
maxBytes,
|
|
773
|
+
remainingBytes: maxBytes - bytes,
|
|
774
|
+
withinLimit: bytes <= maxBytes,
|
|
775
|
+
envelopeReservedBytes
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function sendWebPush(args) {
|
|
779
|
+
const { payload } = args || {};
|
|
780
|
+
if (typeof payload === "string") {
|
|
781
|
+
const size = measurePushPayload(payload);
|
|
782
|
+
if (!size.withinLimit) {
|
|
783
|
+
const err5 = new Error(
|
|
784
|
+
`sendWebPush: payload is ${size.bytes} bytes, over the ${MAX_PUSH_PAYLOAD_BYTES}-byte limit (push services cap the encrypted body at ${WEB_PUSH_MAX_BODY_BYTES} bytes; aes128gcm adds ${WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES} bytes)`
|
|
785
|
+
);
|
|
786
|
+
err5.code = "PUSH_PAYLOAD_TOO_LARGE";
|
|
787
|
+
err5.bytes = size.bytes;
|
|
788
|
+
err5.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
|
|
789
|
+
throw err5;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return sharedSendWebPush(args);
|
|
793
|
+
}
|
|
794
|
+
var SCHEDULED_DEFAULT_TTL = 2419200;
|
|
795
|
+
function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}) {
|
|
796
|
+
return {
|
|
797
|
+
async sendNotification(subscription, payload) {
|
|
798
|
+
return sendWebPush({
|
|
799
|
+
subscription,
|
|
800
|
+
payload,
|
|
801
|
+
ttl,
|
|
802
|
+
vapid: {
|
|
803
|
+
email: vapid.email,
|
|
804
|
+
publicKey: vapid.publicKey,
|
|
805
|
+
privateKey: vapid.privateKey
|
|
806
|
+
},
|
|
807
|
+
fetch: globalThis.fetch
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/server/lib/errors.js
|
|
814
|
+
import { redactCredentials } from "@rei-standard/amsg-shared";
|
|
815
|
+
var NonRetryableError = class extends Error {
|
|
816
|
+
/**
|
|
817
|
+
* @param {string} message
|
|
818
|
+
* @param {{ code?: string, cause?: unknown }} [options]
|
|
819
|
+
*/
|
|
820
|
+
constructor(message, options = {}) {
|
|
821
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
822
|
+
this.name = "NonRetryableError";
|
|
823
|
+
this.permanent = true;
|
|
824
|
+
if (options.code) this.code = options.code;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
function isNonRetryableError(error) {
|
|
828
|
+
return !!error && typeof error === "object" && /** @type {any} */
|
|
829
|
+
error.permanent === true;
|
|
830
|
+
}
|
|
831
|
+
var DeploymentConfigError = class extends Error {
|
|
832
|
+
/**
|
|
833
|
+
* @param {string} message
|
|
834
|
+
* @param {{ code?: string, cause?: unknown }} [options]
|
|
835
|
+
*/
|
|
836
|
+
constructor(message, options = {}) {
|
|
837
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
838
|
+
this.name = "DeploymentConfigError";
|
|
839
|
+
if (options.code) this.code = options.code;
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
function markPermanent(error, code) {
|
|
843
|
+
if (!error || typeof error !== "object") return error;
|
|
844
|
+
const target = (
|
|
845
|
+
/** @type {any} */
|
|
846
|
+
error
|
|
847
|
+
);
|
|
848
|
+
try {
|
|
849
|
+
target.permanent = true;
|
|
850
|
+
if (code && typeof target.code !== "string") target.code = code;
|
|
851
|
+
} catch (_frozen) {
|
|
852
|
+
}
|
|
853
|
+
return error;
|
|
854
|
+
}
|
|
855
|
+
var TASK_CANCELLED_CODE = "TASK_CANCELLED";
|
|
856
|
+
function isTaskCancelledError(error) {
|
|
857
|
+
return !!error && typeof error === "object" && /** @type {any} */
|
|
858
|
+
error.code === TASK_CANCELLED_CODE;
|
|
859
|
+
}
|
|
860
|
+
var TERMINAL_PUSH_STATUSES = /* @__PURE__ */ new Set([404, 410]);
|
|
861
|
+
var PUSH_PAYLOAD_TOO_LARGE_STATUS = 413;
|
|
862
|
+
var PERMANENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
863
|
+
"PUSH_SUBSCRIPTION_MISSING",
|
|
864
|
+
"PUSH_SUBSCRIPTION_STORE_UNSUPPORTED",
|
|
865
|
+
"PUSH_PAYLOAD_TOO_LARGE"
|
|
866
|
+
]);
|
|
867
|
+
function isPermanentDeliveryFailure(failure) {
|
|
868
|
+
const { permanent, errorCode, pushStatus } = failure || {};
|
|
869
|
+
return permanent === true || typeof errorCode === "string" && PERMANENT_ERROR_CODES.has(errorCode) || Number.isInteger(pushStatus) && (TERMINAL_PUSH_STATUSES.has(
|
|
870
|
+
/** @type {number} */
|
|
871
|
+
pushStatus
|
|
872
|
+
) || pushStatus === PUSH_PAYLOAD_TOO_LARGE_STATUS);
|
|
873
|
+
}
|
|
874
|
+
var pushStatusCodes = /* @__PURE__ */ new WeakMap();
|
|
875
|
+
function tagPushStatusCode(error) {
|
|
876
|
+
if (error && typeof error === "object" && Number.isInteger(
|
|
877
|
+
/** @type {any} */
|
|
878
|
+
error.statusCode
|
|
879
|
+
)) {
|
|
880
|
+
pushStatusCodes.set(
|
|
881
|
+
/** @type {object} */
|
|
882
|
+
error,
|
|
883
|
+
/** @type {any} */
|
|
884
|
+
error.statusCode
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return error;
|
|
888
|
+
}
|
|
889
|
+
async function sendTaggedPush(webpush, subscription, payloadJson) {
|
|
890
|
+
try {
|
|
891
|
+
return await webpush.sendNotification(subscription, payloadJson);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
throw tagPushStatusCode(error);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function readPushStatusCode(error) {
|
|
897
|
+
if (!error || typeof error !== "object") return null;
|
|
898
|
+
const statusCode = pushStatusCodes.get(
|
|
899
|
+
/** @type {object} */
|
|
900
|
+
error
|
|
901
|
+
);
|
|
902
|
+
return Number.isInteger(statusCode) ? statusCode : null;
|
|
903
|
+
}
|
|
904
|
+
function buildErrorExtra(errorCode, pushStatus) {
|
|
905
|
+
const extra = {};
|
|
906
|
+
if (typeof errorCode === "string" && errorCode) extra.errorCode = errorCode.slice(0, 64);
|
|
907
|
+
if (Number.isInteger(pushStatus)) extra.pushStatus = /** @type {number} */
|
|
908
|
+
pushStatus;
|
|
909
|
+
return Object.keys(extra).length > 0 ? extra : void 0;
|
|
910
|
+
}
|
|
911
|
+
var ERROR_SUMMARY_MAX_CHARS = 500;
|
|
912
|
+
function sanitizeErrorSummary(reason) {
|
|
913
|
+
const flattened = String(reason ?? "").replace(/\s+/g, " ").trim();
|
|
914
|
+
const safe = redactCredentials(flattened);
|
|
915
|
+
return safe.length > ERROR_SUMMARY_MAX_CHARS ? `${safe.slice(0, ERROR_SUMMARY_MAX_CHARS - 3)}\u2026` : safe;
|
|
916
|
+
}
|
|
917
|
+
function summarizeErrorCause(error, stage) {
|
|
918
|
+
const raw = error && typeof error === "object" ? (
|
|
919
|
+
/** @type {any} */
|
|
920
|
+
error
|
|
921
|
+
) : {};
|
|
922
|
+
const name = typeof raw.name === "string" && raw.name ? raw.name : "Error";
|
|
923
|
+
const rawMessage = typeof raw.message === "string" && raw.message ? raw.message : String(error ?? "");
|
|
924
|
+
const cause = {
|
|
925
|
+
stage,
|
|
926
|
+
name: sanitizeErrorSummary(name).slice(0, 100),
|
|
927
|
+
message: sanitizeErrorSummary(rawMessage)
|
|
928
|
+
};
|
|
929
|
+
if (typeof raw.code === "string" && raw.code) {
|
|
930
|
+
cause.code = sanitizeErrorSummary(raw.code).slice(0, 100);
|
|
931
|
+
}
|
|
932
|
+
return cause;
|
|
933
|
+
}
|
|
934
|
+
|
|
649
935
|
// src/server/lib/agentic-fire.js
|
|
650
936
|
import {
|
|
651
937
|
assertValidDecision,
|
|
@@ -751,6 +1037,27 @@ var MAX_KEY_CHARS = 256;
|
|
|
751
1037
|
function stateValueBytes(value) {
|
|
752
1038
|
return utf82.encode(value).length;
|
|
753
1039
|
}
|
|
1040
|
+
var warnedInvalidTtl = /* @__PURE__ */ new Set();
|
|
1041
|
+
function planClientStateCleanup(ttl, now) {
|
|
1042
|
+
if (!ttl || typeof ttl !== "object" || Array.isArray(ttl)) return [];
|
|
1043
|
+
const targets = [];
|
|
1044
|
+
for (const [namespace, days] of Object.entries(ttl)) {
|
|
1045
|
+
if (!namespace) continue;
|
|
1046
|
+
if (typeof days !== "number" || !Number.isFinite(days) || days <= 0) {
|
|
1047
|
+
if (!warnedInvalidTtl.has(namespace)) {
|
|
1048
|
+
warnedInvalidTtl.add(namespace);
|
|
1049
|
+
console.warn(
|
|
1050
|
+
`[amsg-server] clientStateTtl['${namespace}'] \u4E0D\u662F\u6B63\u6570\uFF08\u6536\u5230 ${JSON.stringify(days)}\uFF09\uFF0C\u8FD9\u4E2A\u547D\u540D\u7A7A\u95F4\u4E0D\u505A\u6E05\u7406`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const updatedBefore = now - days * 24 * 60 * 60 * 1e3;
|
|
1056
|
+
targets.push({ namespace, updatedBefore });
|
|
1057
|
+
targets.push({ namespace: chunkNamespaceFor(namespace), updatedBefore });
|
|
1058
|
+
}
|
|
1059
|
+
return targets;
|
|
1060
|
+
}
|
|
754
1061
|
async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
755
1062
|
const physicalRows = [];
|
|
756
1063
|
const cleanups = [];
|
|
@@ -858,7 +1165,10 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
858
1165
|
throw new RangeError(`writeState: \u5355\u6B21\u6700\u591A ${MAX_STATE_ENTRIES_PER_BATCH} \u6761\uFF0C\u6536\u5230 ${entries.length} \u6761`);
|
|
859
1166
|
}
|
|
860
1167
|
if (!db || typeof db.upsertClientState !== "function") {
|
|
861
|
-
throw new
|
|
1168
|
+
throw new DeploymentConfigError(
|
|
1169
|
+
"AGENTIC_STATE_WRITE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state \u5199\u5165",
|
|
1170
|
+
{ code: "AGENTIC_STATE_WRITE_UNSUPPORTED" }
|
|
1171
|
+
);
|
|
862
1172
|
}
|
|
863
1173
|
const at = nowFn();
|
|
864
1174
|
const normalized = entries.map((entry, index) => {
|
|
@@ -899,51 +1209,88 @@ function createStateAccessors({ db, userId, userKey, maxStateValueBytes, now })
|
|
|
899
1209
|
return { readState, writeState };
|
|
900
1210
|
}
|
|
901
1211
|
|
|
902
|
-
// src/server/lib/
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
uuid: row.uuid ?? null,
|
|
909
|
-
contactName: payload.contactName,
|
|
910
|
-
// 行上的 message_type 是索引列,payload 里的是同一个值;列表接口一直读
|
|
911
|
-
// 行上那个,这里保持一致,行上没有时退回 payload。
|
|
912
|
-
messageType: row.message_type ?? payload.messageType,
|
|
913
|
-
messageSubtype: payload.messageSubtype,
|
|
914
|
-
nextSendAt: row.next_send_at ?? null,
|
|
915
|
-
recurrenceType: payload.recurrenceType,
|
|
916
|
-
tzId: payload.tzId ?? null,
|
|
917
|
-
status: row.status ?? null,
|
|
918
|
-
retryCount: row.retry_count ?? 0,
|
|
919
|
-
createdAt: row.created_at ?? null,
|
|
920
|
-
updatedAt: row.updated_at ?? null,
|
|
921
|
-
// 角色归属 / 客户端侧的任务身份,取自排程方写进 metadata 的字段,宿主
|
|
922
|
-
// 靠它按角色过滤(contactName 会跨角色重名)。缺席 → null。
|
|
923
|
-
charId: metadata.charId ?? null,
|
|
924
|
-
clientTaskId: metadata.amsgClientTaskId ?? null,
|
|
925
|
-
// 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
|
|
926
|
-
// 照旧被白名单挡在外面。
|
|
927
|
-
credRefs: payload.credRefs ?? null,
|
|
928
|
-
// 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
|
|
929
|
-
...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
|
|
930
|
-
// 上一次没发出去的原因(run-tick 记进 payload 的 lastError)。
|
|
931
|
-
// reason 'stale' 表示错过触发时刻太久被判定不再补发;其余是投递失败的
|
|
932
|
-
// 错误信息。payload 里没有时退回行上的 last_error 列(同形状的脱敏摘要,
|
|
933
|
-
// 等重试期间的失败也记在那里)。没有记录 → null。
|
|
934
|
-
// 记进去的字段整份带出来,不再挑一遍——`pushStatus`(410 = 订阅已注销,
|
|
935
|
-
// 404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在这一层的话,
|
|
936
|
-
// 客户端只能回去正则匹配 reason 那句人话。
|
|
937
|
-
lastError: payload.lastError ?? parseRowLastError(row.last_error)
|
|
938
|
-
};
|
|
1212
|
+
// src/server/lib/result-emitter.js
|
|
1213
|
+
import { buildResultPush } from "@rei-standard/amsg-shared";
|
|
1214
|
+
|
|
1215
|
+
// src/server/lib/outbox-store.js
|
|
1216
|
+
function supportsOutbox(db) {
|
|
1217
|
+
return !!db && typeof db.appendOutboxMessages === "function" && typeof db.markOutboxDelivered === "function";
|
|
939
1218
|
}
|
|
940
|
-
function
|
|
941
|
-
if (
|
|
1219
|
+
async function appendPushesToOutbox({ db, userId, userKey, pushes }) {
|
|
1220
|
+
if (!supportsOutbox(db) || !pushes || pushes.length === 0) return false;
|
|
942
1221
|
try {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1222
|
+
await db.appendOutboxMessages(userId, await toOutboxRows(pushes, userKey, Date.now()));
|
|
1223
|
+
return true;
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
console.warn("[amsg-server] outbox \u843D\u884C\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u6295\u9012\uFF09:", error && error.message);
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async function toOutboxRows(pushes, userKey, createdAt) {
|
|
1230
|
+
return Promise.all(pushes.map(async (push) => ({
|
|
1231
|
+
message_id: push.messageId,
|
|
1232
|
+
task_uuid: push.taskUuid ?? null,
|
|
1233
|
+
session_id: push.sessionId ?? null,
|
|
1234
|
+
message_index: push.messageIndex ?? null,
|
|
1235
|
+
total_messages: push.totalMessages ?? null,
|
|
1236
|
+
payload: await encryptForStorage(JSON.stringify(push), userKey),
|
|
1237
|
+
created_at: createdAt
|
|
1238
|
+
})));
|
|
1239
|
+
}
|
|
1240
|
+
async function markPushesDelivered({ db, userId, messageIds }) {
|
|
1241
|
+
if (!supportsOutbox(db) || !messageIds || messageIds.length === 0) return;
|
|
1242
|
+
try {
|
|
1243
|
+
await db.markOutboxDelivered(userId, messageIds, Date.now());
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
console.warn("[amsg-server] outbox \u6807\u8BB0 delivered \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function discardPushesFromOutbox({ db, userId, messageIds }) {
|
|
1249
|
+
if (!db || typeof db.discardOutboxMessages !== "function") return;
|
|
1250
|
+
if (!messageIds || messageIds.length === 0) return;
|
|
1251
|
+
try {
|
|
1252
|
+
await db.discardOutboxMessages(userId, messageIds);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
console.warn("[amsg-server] outbox \u64A4\u56DE\u672A\u53D1\u51FA\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function discardUndeliveredPushes({ db, userId, pushes, sentIds }) {
|
|
1258
|
+
const delivered = new Set(sentIds);
|
|
1259
|
+
await discardPushesFromOutbox({
|
|
1260
|
+
db,
|
|
1261
|
+
userId,
|
|
1262
|
+
messageIds: (pushes || []).map((push) => push.messageId).filter((messageId) => !delivered.has(messageId))
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
var OUTBOX_SCAN_PAGE_SIZE = 100;
|
|
1266
|
+
var OUTBOX_SCAN_MAX_ROWS = 5e3;
|
|
1267
|
+
async function discardUndeliveredPushesForTask({ db, userId, taskUuid }) {
|
|
1268
|
+
if (!db || typeof db.listUnackedOutbox !== "function" || typeof db.discardOutboxMessages !== "function") return;
|
|
1269
|
+
if (!taskUuid) return;
|
|
1270
|
+
const messageIds = [];
|
|
1271
|
+
try {
|
|
1272
|
+
let cursor = 0;
|
|
1273
|
+
let scanned = 0;
|
|
1274
|
+
while (scanned < OUTBOX_SCAN_MAX_ROWS) {
|
|
1275
|
+
const rows = await db.listUnackedOutbox(userId, cursor, OUTBOX_SCAN_PAGE_SIZE);
|
|
1276
|
+
if (!rows || rows.length === 0) break;
|
|
1277
|
+
scanned += rows.length;
|
|
1278
|
+
let nextCursor = cursor;
|
|
1279
|
+
for (const row of rows) {
|
|
1280
|
+
if (row.id > nextCursor) nextCursor = row.id;
|
|
1281
|
+
if (row.task_uuid !== taskUuid) continue;
|
|
1282
|
+
if (row.delivered_at != null) continue;
|
|
1283
|
+
messageIds.push(row.message_id);
|
|
1284
|
+
}
|
|
1285
|
+
if (nextCursor <= cursor) break;
|
|
1286
|
+
cursor = nextCursor;
|
|
1287
|
+
if (rows.length < OUTBOX_SCAN_PAGE_SIZE) break;
|
|
1288
|
+
}
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
console.warn("[amsg-server] outbox \u67E5\u672A\u6295\u9012\u7684\u884C\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
1291
|
+
return;
|
|
946
1292
|
}
|
|
1293
|
+
await discardPushesFromOutbox({ db, userId, messageIds });
|
|
947
1294
|
}
|
|
948
1295
|
|
|
949
1296
|
// src/server/lib/push-subscription-store.js
|
|
@@ -961,12 +1308,14 @@ async function savePushSubscription({ db, userId, userKey, subscription, updated
|
|
|
961
1308
|
await db.upsertPushSubscription(userId, encrypted, at);
|
|
962
1309
|
return { updatedAt: at };
|
|
963
1310
|
}
|
|
964
|
-
async function
|
|
965
|
-
const row = await db.getPushSubscription(userId);
|
|
1311
|
+
async function decodePushSubscriptionRow(row, userKey) {
|
|
966
1312
|
if (!row || typeof row.subscription !== "string" || !row.subscription) return null;
|
|
967
1313
|
const subscription = JSON.parse(await decryptFromStorage(row.subscription, userKey));
|
|
968
1314
|
return { subscription, updatedAt: row.updated_at ?? null };
|
|
969
1315
|
}
|
|
1316
|
+
async function loadPushSubscription({ db, userId, userKey }) {
|
|
1317
|
+
return decodePushSubscriptionRow(await db.getPushSubscription(userId), userKey);
|
|
1318
|
+
}
|
|
970
1319
|
function codedError2(code, message) {
|
|
971
1320
|
const error = new Error(`${code}: ${message}`);
|
|
972
1321
|
error.code = code;
|
|
@@ -986,36 +1335,143 @@ async function resolvePushSubscription({ db, userId, userKey, legacyFallback = n
|
|
|
986
1335
|
return stored.subscription;
|
|
987
1336
|
}
|
|
988
1337
|
|
|
989
|
-
// src/server/lib/
|
|
990
|
-
function
|
|
991
|
-
|
|
1338
|
+
// src/server/lib/result-emitter.js
|
|
1339
|
+
function createResultEmitter({
|
|
1340
|
+
db,
|
|
1341
|
+
task,
|
|
1342
|
+
userKey,
|
|
1343
|
+
decryptedPayload,
|
|
1344
|
+
messageIdBase,
|
|
1345
|
+
sessionId,
|
|
1346
|
+
occurrenceMs,
|
|
1347
|
+
webpush,
|
|
1348
|
+
now
|
|
1349
|
+
}) {
|
|
1350
|
+
const nowFn = typeof now === "function" ? now : Date.now;
|
|
1351
|
+
let emitted = 0;
|
|
1352
|
+
const emitResult = async (payload) => {
|
|
1353
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1354
|
+
throw new TypeError("emitResult(payload) \u9700\u8981\u4E00\u4E2A\u666E\u901A\u5BF9\u8C61");
|
|
1355
|
+
}
|
|
1356
|
+
if (!supportsOutbox(db)) {
|
|
1357
|
+
throw new DeploymentConfigError(
|
|
1358
|
+
"OUTBOX_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 message_outbox\uFF0CemitResult \u6CA1\u6709\u843D\u811A\u5904",
|
|
1359
|
+
{ code: "OUTBOX_UNSUPPORTED" }
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const seq = emitted++;
|
|
1363
|
+
const push = buildResultPush({
|
|
1364
|
+
messageType: decryptedPayload.messageType || "auto",
|
|
1365
|
+
source: "scheduled",
|
|
1366
|
+
messageId: `${messageIdBase}_result_${seq}`,
|
|
1367
|
+
sessionId,
|
|
1368
|
+
...payload,
|
|
1369
|
+
// 通知策略:宿主没表态就补一句「弹」。写进 payload 而不是只靠 SW 的默
|
|
1370
|
+
// 认行为,是为了让还没升级 SW 的客户端也弹得出来——旧版 SW 不认识
|
|
1371
|
+
// result 这个 kind,只认 notification.show。
|
|
1372
|
+
notification: withDefaultShow(payload.notification),
|
|
1373
|
+
// 任务身份由库覆盖写(与推送链路同一条规矩)。
|
|
1374
|
+
taskId: task.id ?? null,
|
|
1375
|
+
taskUuid: task.uuid ?? null,
|
|
1376
|
+
recurrenceType: decryptedPayload.recurrenceType || "none",
|
|
1377
|
+
occurrenceMs
|
|
1378
|
+
});
|
|
1379
|
+
await db.appendOutboxMessages(task.user_id, await toOutboxRows([push], userKey, nowFn()));
|
|
1380
|
+
let pushed;
|
|
1381
|
+
try {
|
|
1382
|
+
pushed = await sendResultPush({ db, task, userKey, decryptedPayload, webpush, push });
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
await discardUndeliveredPushes({ db, userId: task.user_id, pushes: [push], sentIds: [] });
|
|
1385
|
+
throw error;
|
|
1386
|
+
}
|
|
1387
|
+
if (pushed) {
|
|
1388
|
+
await markPushesDelivered({ db, userId: task.user_id, messageIds: [push.messageId] });
|
|
1389
|
+
}
|
|
1390
|
+
return { messageId: push.messageId, pushed };
|
|
1391
|
+
};
|
|
1392
|
+
return { emitResult };
|
|
992
1393
|
}
|
|
993
|
-
|
|
994
|
-
if (!
|
|
1394
|
+
function withDefaultShow(notification) {
|
|
1395
|
+
if (!notification || typeof notification !== "object" || Array.isArray(notification)) {
|
|
1396
|
+
return { show: "always" };
|
|
1397
|
+
}
|
|
1398
|
+
if (notification.show === void 0) return { ...notification, show: "always" };
|
|
1399
|
+
return notification;
|
|
1400
|
+
}
|
|
1401
|
+
async function sendResultPush({ db, task, userKey, decryptedPayload, webpush, push }) {
|
|
1402
|
+
if (!webpush || typeof webpush.sendNotification !== "function") return false;
|
|
995
1403
|
try {
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
payload: await encryptForStorage(JSON.stringify(push), userKey),
|
|
1004
|
-
created_at: now
|
|
1005
|
-
})));
|
|
1006
|
-
await db.appendOutboxMessages(userId, rows);
|
|
1404
|
+
const subscription = await resolvePushSubscription({
|
|
1405
|
+
db,
|
|
1406
|
+
userId: task.user_id,
|
|
1407
|
+
userKey,
|
|
1408
|
+
legacyFallback: (decryptedPayload && decryptedPayload.pushSubscription) ?? null
|
|
1409
|
+
});
|
|
1410
|
+
await sendTaggedPush(webpush, subscription, JSON.stringify(push));
|
|
1007
1411
|
return true;
|
|
1008
1412
|
} catch (error) {
|
|
1009
|
-
|
|
1413
|
+
if (isTaskCancelledError(error)) throw error;
|
|
1414
|
+
console.warn(
|
|
1415
|
+
`[amsg-server] \u7ED3\u679C ${push.messageId} \u7684\u63A8\u9001\u6CA1\u53D1\u51FA\u53BB\uFF08\u5DF2\u843D\u8FDB\u6536\u4EF6\u7BB1\uFF0C\u7B49\u5BA2\u6237\u7AEF\u8865\u6536\uFF09:`,
|
|
1416
|
+
error && error.message
|
|
1417
|
+
);
|
|
1010
1418
|
return false;
|
|
1011
1419
|
}
|
|
1012
1420
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1421
|
+
|
|
1422
|
+
// src/server/lib/task-projection.js
|
|
1423
|
+
function projectTask(row, decryptedPayload, options = {}) {
|
|
1424
|
+
const payload = decryptedPayload || {};
|
|
1425
|
+
const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
|
|
1426
|
+
return {
|
|
1427
|
+
id: row.id ?? null,
|
|
1428
|
+
uuid: row.uuid ?? null,
|
|
1429
|
+
contactName: payload.contactName,
|
|
1430
|
+
// 行上的 message_type 是索引列,payload 里的是同一个值;列表接口一直读
|
|
1431
|
+
// 行上那个,这里保持一致,行上没有时退回 payload。
|
|
1432
|
+
messageType: row.message_type ?? payload.messageType,
|
|
1433
|
+
messageSubtype: payload.messageSubtype,
|
|
1434
|
+
nextSendAt: row.next_send_at ?? null,
|
|
1435
|
+
recurrenceType: payload.recurrenceType,
|
|
1436
|
+
tzId: payload.tzId ?? null,
|
|
1437
|
+
status: row.status ?? null,
|
|
1438
|
+
retryCount: row.retry_count ?? 0,
|
|
1439
|
+
createdAt: row.created_at ?? null,
|
|
1440
|
+
updatedAt: row.updated_at ?? null,
|
|
1441
|
+
// 角色归属 / 客户端侧的任务身份,取自排程方写进 metadata 的字段,宿主
|
|
1442
|
+
// 靠它按角色过滤(contactName 会跨角色重名)。缺席 → null。
|
|
1443
|
+
charId: metadata.charId ?? null,
|
|
1444
|
+
clientTaskId: metadata.amsgClientTaskId ?? null,
|
|
1445
|
+
// 凭据引用只是名字(cred_id),本体在 llm_credentials 表里,凭据字段本身
|
|
1446
|
+
// 照旧被白名单挡在外面。
|
|
1447
|
+
credRefs: payload.credRefs ?? null,
|
|
1448
|
+
// 整份 metadata 只在单条查询里给(见上面的 includeMetadata)。
|
|
1449
|
+
...options.includeMetadata ? { metadata: payload.metadata ?? null } : {},
|
|
1450
|
+
// 上一次没发出去的原因。reason 'stale' 表示错过触发时刻太久被判定不再补
|
|
1451
|
+
// 发;其余是投递失败的错误信息。没有记录 → null。
|
|
1452
|
+
//
|
|
1453
|
+
// 行上的 last_error 列是权威的那一份:每次失败刷新、成功时清空。密文
|
|
1454
|
+
// payload 里那份是给没有这一列的适配器兜底的(run-tick 只在终审处置和过期
|
|
1455
|
+
// 快进时写它,成功时不会去重写整份密文)。所以只要行带着这一列——哪怕值是
|
|
1456
|
+
// NULL,那正说明「最近一次投递没失败」——就以它为准;反过来让 payload 优先
|
|
1457
|
+
// 的话,一次 410 会永远挂在这条任务上,用户重新登记订阅、之后天天正常送达
|
|
1458
|
+
// 也擦不掉。
|
|
1459
|
+
//
|
|
1460
|
+
// 记进去的字段整份带出来,不再挑一遍——`errorCode` 和 `pushStatus`(410 =
|
|
1461
|
+
// 订阅已注销,404 = 端点不存在)这类机读标注就是给客户端看的,白名单挡在
|
|
1462
|
+
// 这一层的话,客户端只能回去正则匹配 reason 那句人话。
|
|
1463
|
+
lastError: hasRowLastError(row) ? parseRowLastError(row.last_error) : payload.lastError ?? null
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function hasRowLastError(row) {
|
|
1467
|
+
return !!row && Object.prototype.hasOwnProperty.call(row, "last_error");
|
|
1468
|
+
}
|
|
1469
|
+
function parseRowLastError(raw) {
|
|
1470
|
+
if (typeof raw !== "string" || !raw) return null;
|
|
1015
1471
|
try {
|
|
1016
|
-
|
|
1017
|
-
} catch (
|
|
1018
|
-
|
|
1472
|
+
return JSON.parse(raw);
|
|
1473
|
+
} catch (_error) {
|
|
1474
|
+
return { reason: raw };
|
|
1019
1475
|
}
|
|
1020
1476
|
}
|
|
1021
1477
|
|
|
@@ -1074,8 +1530,11 @@ function normalizeBeforeFireResult(result) {
|
|
|
1074
1530
|
toolChoice: result.toolChoice
|
|
1075
1531
|
};
|
|
1076
1532
|
}
|
|
1077
|
-
throw
|
|
1078
|
-
|
|
1533
|
+
throw markPermanent(
|
|
1534
|
+
new TypeError(
|
|
1535
|
+
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
|
|
1536
|
+
),
|
|
1537
|
+
"AGENTIC_BAD_BEFORE_FIRE"
|
|
1079
1538
|
);
|
|
1080
1539
|
}
|
|
1081
1540
|
function firstPositiveInt(values, fallback) {
|
|
@@ -1093,7 +1552,10 @@ function firstPositiveNumber(values, fallback) {
|
|
|
1093
1552
|
async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
1094
1553
|
const hooks = ctx.hooks;
|
|
1095
1554
|
if (typeof hooks.onLLMOutput !== "function") {
|
|
1096
|
-
throw new
|
|
1555
|
+
throw new DeploymentConfigError(
|
|
1556
|
+
"AGENTIC_CONFIG_ERROR: hooks.onBeforeFire requires hooks.onLLMOutput to classify LLM rounds",
|
|
1557
|
+
{ code: "AGENTIC_CONFIG_ERROR" }
|
|
1558
|
+
);
|
|
1097
1559
|
}
|
|
1098
1560
|
const nowFn = typeof ctx._agenticNow === "function" ? ctx._agenticNow : Date.now;
|
|
1099
1561
|
const sleep = typeof ctx._agenticSleep === "function" ? ctx._agenticSleep : defaultSleep;
|
|
@@ -1104,6 +1566,20 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1104
1566
|
maxStateValueBytes: ctx.maxStateValueBytes,
|
|
1105
1567
|
now: nowFn
|
|
1106
1568
|
});
|
|
1569
|
+
const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
|
|
1570
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
|
|
1571
|
+
const occurrenceMs = occurrenceMsOf(task);
|
|
1572
|
+
const { emitResult } = createResultEmitter({
|
|
1573
|
+
db: ctx.db,
|
|
1574
|
+
task,
|
|
1575
|
+
userKey,
|
|
1576
|
+
decryptedPayload,
|
|
1577
|
+
messageIdBase,
|
|
1578
|
+
sessionId,
|
|
1579
|
+
occurrenceMs,
|
|
1580
|
+
webpush: ctx.webpush,
|
|
1581
|
+
now: nowFn
|
|
1582
|
+
});
|
|
1107
1583
|
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1108
1584
|
let scheduledTaskCount = 0;
|
|
1109
1585
|
async function readExistingTask(uuid) {
|
|
@@ -1178,15 +1654,6 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1178
1654
|
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
1179
1655
|
throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
1180
1656
|
}
|
|
1181
|
-
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
1182
|
-
throw new RangeError(
|
|
1183
|
-
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
1187
|
-
throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
|
|
1188
|
-
}
|
|
1189
|
-
scheduledTaskCount++;
|
|
1190
1657
|
const fullTaskData = {
|
|
1191
1658
|
contactName,
|
|
1192
1659
|
avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
|
|
@@ -1227,7 +1694,27 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1227
1694
|
llmExtraBody: decryptedPayload.llmExtraBody ?? null,
|
|
1228
1695
|
metadata
|
|
1229
1696
|
};
|
|
1230
|
-
const
|
|
1697
|
+
const serializedTaskData = JSON.stringify(fullTaskData);
|
|
1698
|
+
const sizeError = validateTaskPayloadSize(serializedTaskData);
|
|
1699
|
+
if (sizeError) {
|
|
1700
|
+
throw markPermanent(
|
|
1701
|
+
new RangeError(`${sizeError.code}: ${sizeError.message}`),
|
|
1702
|
+
sizeError.code
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
1706
|
+
throw new RangeError(
|
|
1707
|
+
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
1711
|
+
throw new DeploymentConfigError(
|
|
1712
|
+
"AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09",
|
|
1713
|
+
{ code: "AGENTIC_SCHEDULE_UNSUPPORTED" }
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
scheduledTaskCount++;
|
|
1717
|
+
const encryptedPayload = await encryptForStorage(serializedTaskData, userKey);
|
|
1231
1718
|
let created;
|
|
1232
1719
|
try {
|
|
1233
1720
|
created = await ctx.db.createTask({
|
|
@@ -1249,7 +1736,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1249
1736
|
throw error;
|
|
1250
1737
|
}
|
|
1251
1738
|
if (!created) {
|
|
1252
|
-
throw new
|
|
1739
|
+
throw new NonRetryableError(
|
|
1740
|
+
"AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C",
|
|
1741
|
+
{ code: "AGENTIC_SCHEDULE_FAILED" }
|
|
1742
|
+
);
|
|
1253
1743
|
}
|
|
1254
1744
|
return {
|
|
1255
1745
|
created: true,
|
|
@@ -1268,7 +1758,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1268
1758
|
);
|
|
1269
1759
|
}
|
|
1270
1760
|
if (!ctx.db || typeof ctx.db.deleteTaskByUuid !== "function") {
|
|
1271
|
-
throw new
|
|
1761
|
+
throw new DeploymentConfigError(
|
|
1762
|
+
"AGENTIC_CANCEL_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5220\u4EFB\u52A1\uFF08\u7F3A deleteTaskByUuid\uFF09",
|
|
1763
|
+
{ code: "AGENTIC_CANCEL_UNSUPPORTED" }
|
|
1764
|
+
);
|
|
1272
1765
|
}
|
|
1273
1766
|
const cancelled = await ctx.db.deleteTaskByUuid(uuid, task.user_id);
|
|
1274
1767
|
return { cancelled: !!cancelled };
|
|
@@ -1294,7 +1787,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1294
1787
|
);
|
|
1295
1788
|
}
|
|
1296
1789
|
if (!ctx.db || typeof ctx.db.getTaskByUuid !== "function" || typeof ctx.db.updateTaskByUuid !== "function") {
|
|
1297
|
-
throw new
|
|
1790
|
+
throw new DeploymentConfigError(
|
|
1791
|
+
"AGENTIC_RENEW_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u6539\u4EFB\u52A1\uFF08\u7F3A getTaskByUuid / updateTaskByUuid\uFF09",
|
|
1792
|
+
{ code: "AGENTIC_RENEW_UNSUPPORTED" }
|
|
1793
|
+
);
|
|
1298
1794
|
}
|
|
1299
1795
|
const row = await ctx.db.getTaskByUuid(uuid, task.user_id);
|
|
1300
1796
|
if (!row) return { renewed: false, reason: "not_found" };
|
|
@@ -1322,6 +1818,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1322
1818
|
userId: task.user_id,
|
|
1323
1819
|
readState,
|
|
1324
1820
|
writeState,
|
|
1821
|
+
emitResult,
|
|
1325
1822
|
scheduleTask,
|
|
1326
1823
|
cancelTask,
|
|
1327
1824
|
renewTask,
|
|
@@ -1343,13 +1840,17 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1343
1840
|
sleep,
|
|
1344
1841
|
readState,
|
|
1345
1842
|
writeState,
|
|
1843
|
+
emitResult,
|
|
1346
1844
|
scratch,
|
|
1347
1845
|
scheduleTask,
|
|
1348
1846
|
cancelTask,
|
|
1349
1847
|
renewTask,
|
|
1350
1848
|
resolveLlmCredential: resolveLlmCredential2,
|
|
1351
1849
|
fireCtx,
|
|
1352
|
-
progress
|
|
1850
|
+
progress,
|
|
1851
|
+
sessionId,
|
|
1852
|
+
messageIdBase,
|
|
1853
|
+
occurrenceMs
|
|
1353
1854
|
});
|
|
1354
1855
|
settledStatus = !outcome.handled ? "not-handled" : outcome.result.status === "skipped" ? "skipped" : "sent";
|
|
1355
1856
|
return outcome;
|
|
@@ -1375,7 +1876,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1375
1876
|
usage: progress.usage,
|
|
1376
1877
|
scratch,
|
|
1377
1878
|
readState,
|
|
1378
|
-
writeState
|
|
1879
|
+
writeState,
|
|
1880
|
+
emitResult
|
|
1379
1881
|
});
|
|
1380
1882
|
}
|
|
1381
1883
|
}
|
|
@@ -1389,13 +1891,17 @@ async function runFireChain({
|
|
|
1389
1891
|
sleep,
|
|
1390
1892
|
readState,
|
|
1391
1893
|
writeState,
|
|
1894
|
+
emitResult,
|
|
1392
1895
|
scratch,
|
|
1393
1896
|
scheduleTask,
|
|
1394
1897
|
cancelTask,
|
|
1395
1898
|
renewTask,
|
|
1396
1899
|
resolveLlmCredential: resolveLlmCredential2,
|
|
1397
1900
|
fireCtx,
|
|
1398
|
-
progress
|
|
1901
|
+
progress,
|
|
1902
|
+
sessionId,
|
|
1903
|
+
messageIdBase,
|
|
1904
|
+
occurrenceMs
|
|
1399
1905
|
}) {
|
|
1400
1906
|
const before = await hooks.onBeforeFire(fireCtx);
|
|
1401
1907
|
if (before == null) return { handled: false };
|
|
@@ -1413,8 +1919,6 @@ async function runFireChain({
|
|
|
1413
1919
|
DEFAULT_TOTAL_TIMEOUT_MS
|
|
1414
1920
|
);
|
|
1415
1921
|
const deadline = nowFn() + totalTimeoutMs;
|
|
1416
|
-
const sessionId = task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_${randomUUID()}`;
|
|
1417
|
-
const occurrenceMs = occurrenceMsOf(task);
|
|
1418
1922
|
let messages = normalized.messages.slice();
|
|
1419
1923
|
const chatCred = await resolveFireCredentials({
|
|
1420
1924
|
db: ctx.db,
|
|
@@ -1457,13 +1961,18 @@ async function runFireChain({
|
|
|
1457
1961
|
occurrenceMs,
|
|
1458
1962
|
readState,
|
|
1459
1963
|
writeState,
|
|
1964
|
+
emitResult,
|
|
1460
1965
|
scheduleTask,
|
|
1461
1966
|
cancelTask,
|
|
1462
1967
|
renewTask,
|
|
1463
1968
|
resolveLlmCredential: resolveLlmCredential2
|
|
1464
1969
|
});
|
|
1465
1970
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
1466
|
-
|
|
1971
|
+
try {
|
|
1972
|
+
assertValidDecision(decision, { inlineToolCalls: true });
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
throw markPermanent(error, "AGENTIC_BAD_DECISION");
|
|
1975
|
+
}
|
|
1467
1976
|
if (decision.decision === "continue") {
|
|
1468
1977
|
messages = decision.nextHistory.slice();
|
|
1469
1978
|
continue;
|
|
@@ -1478,6 +1987,7 @@ async function runFireChain({
|
|
|
1478
1987
|
decryptedPayload,
|
|
1479
1988
|
ctx,
|
|
1480
1989
|
sessionId,
|
|
1990
|
+
messageIdBase,
|
|
1481
1991
|
occurrenceMs,
|
|
1482
1992
|
task,
|
|
1483
1993
|
userKey,
|
|
@@ -1485,16 +1995,23 @@ async function runFireChain({
|
|
|
1485
1995
|
scratch,
|
|
1486
1996
|
readState,
|
|
1487
1997
|
writeState,
|
|
1998
|
+
emitResult,
|
|
1488
1999
|
progress
|
|
1489
2000
|
});
|
|
1490
2001
|
return { handled: true, result: { success: true, messagesSent, status: "finished", iterations: iteration + 1 } };
|
|
1491
2002
|
}
|
|
1492
2003
|
const toolCalls = extractToolCallsFromDecision(decision);
|
|
1493
2004
|
if (toolCalls.length === 0) {
|
|
1494
|
-
throw
|
|
2005
|
+
throw taggedRetryableError(
|
|
2006
|
+
"AGENTIC_EMPTY_TOOL_REQUEST: tool-request decision carried no toolCalls (neither decision.toolCalls nor pushPayloads[].toolCalls)",
|
|
2007
|
+
"AGENTIC_EMPTY_TOOL_REQUEST"
|
|
2008
|
+
);
|
|
1495
2009
|
}
|
|
1496
2010
|
if (typeof hooks.executeToolCalls !== "function") {
|
|
1497
|
-
throw new
|
|
2011
|
+
throw new DeploymentConfigError(
|
|
2012
|
+
"AGENTIC_CONFIG_ERROR: onLLMOutput returned tool-request but hooks.executeToolCalls is not configured",
|
|
2013
|
+
{ code: "AGENTIC_CONFIG_ERROR" }
|
|
2014
|
+
);
|
|
1498
2015
|
}
|
|
1499
2016
|
if (iteration === maxToolIterations - 1) {
|
|
1500
2017
|
break;
|
|
@@ -1518,7 +2035,15 @@ async function runFireChain({
|
|
|
1518
2035
|
const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
|
|
1519
2036
|
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
1520
2037
|
}
|
|
1521
|
-
throw
|
|
2038
|
+
throw taggedRetryableError(
|
|
2039
|
+
`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`,
|
|
2040
|
+
"AGENTIC_LOOP_EXCEEDED"
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
function taggedRetryableError(message, code) {
|
|
2044
|
+
const error = new Error(message);
|
|
2045
|
+
error.code = code;
|
|
2046
|
+
return error;
|
|
1522
2047
|
}
|
|
1523
2048
|
async function notifyAfterSend(ctx, info) {
|
|
1524
2049
|
if (typeof ctx.onAfterSend !== "function") return;
|
|
@@ -1547,6 +2072,7 @@ async function sendHookPushPayloads({
|
|
|
1547
2072
|
decryptedPayload,
|
|
1548
2073
|
ctx,
|
|
1549
2074
|
sessionId,
|
|
2075
|
+
messageIdBase,
|
|
1550
2076
|
occurrenceMs,
|
|
1551
2077
|
task,
|
|
1552
2078
|
userKey,
|
|
@@ -1554,13 +2080,15 @@ async function sendHookPushPayloads({
|
|
|
1554
2080
|
scratch,
|
|
1555
2081
|
readState,
|
|
1556
2082
|
writeState,
|
|
2083
|
+
emitResult,
|
|
1557
2084
|
progress
|
|
1558
2085
|
}) {
|
|
1559
2086
|
const total = pushPayloads.length;
|
|
1560
2087
|
let sentCount = 0;
|
|
1561
2088
|
progress.total = total;
|
|
1562
|
-
const afterSendBase = { task, total, scratch, readState, writeState, usage: progress.usage };
|
|
2089
|
+
const afterSendBase = { task, total, scratch, readState, writeState, emitResult, usage: progress.usage };
|
|
1563
2090
|
const sentIds = [];
|
|
2091
|
+
const finalized = [];
|
|
1564
2092
|
try {
|
|
1565
2093
|
if (!ctx.vapid || !ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
1566
2094
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
@@ -1571,8 +2099,6 @@ async function sendHookPushPayloads({
|
|
|
1571
2099
|
userKey,
|
|
1572
2100
|
legacyFallback: (decryptedPayload && decryptedPayload.pushSubscription) ?? null
|
|
1573
2101
|
});
|
|
1574
|
-
const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}`;
|
|
1575
|
-
const finalized = [];
|
|
1576
2102
|
for (let i = 0; i < total; i++) {
|
|
1577
2103
|
const push = { ...pushPayloads[i] };
|
|
1578
2104
|
if (typeof push.messageId !== "string" || !push.messageId) push.messageId = `${messageIdBase}_hook_${i}`;
|
|
@@ -1585,7 +2111,7 @@ async function sendHookPushPayloads({
|
|
|
1585
2111
|
}
|
|
1586
2112
|
await appendPushesToOutbox({ db: ctx.db, userId: task.user_id, userKey, pushes: finalized });
|
|
1587
2113
|
for (let i = 0; i < total; i++) {
|
|
1588
|
-
await ctx.webpush
|
|
2114
|
+
await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(finalized[i]));
|
|
1589
2115
|
sentCount++;
|
|
1590
2116
|
progress.sentCount = sentCount;
|
|
1591
2117
|
sentIds.push(finalized[i].messageId);
|
|
@@ -1593,6 +2119,14 @@ async function sendHookPushPayloads({
|
|
|
1593
2119
|
}
|
|
1594
2120
|
} catch (error) {
|
|
1595
2121
|
await markPushesDelivered({ db: ctx.db, userId: task.user_id, messageIds: sentIds });
|
|
2122
|
+
if (isTaskCancelledError(error)) {
|
|
2123
|
+
await discardUndeliveredPushes({
|
|
2124
|
+
db: ctx.db,
|
|
2125
|
+
userId: task.user_id,
|
|
2126
|
+
pushes: finalized,
|
|
2127
|
+
sentIds
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
1596
2130
|
await notifyAfterSend(ctx, { ...afterSendBase, sentCount, error });
|
|
1597
2131
|
throw error;
|
|
1598
2132
|
}
|
|
@@ -1601,58 +2135,22 @@ async function sendHookPushPayloads({
|
|
|
1601
2135
|
return total;
|
|
1602
2136
|
}
|
|
1603
2137
|
|
|
1604
|
-
// src/server/lib/errors.js
|
|
1605
|
-
var NonRetryableError = class extends Error {
|
|
1606
|
-
/**
|
|
1607
|
-
* @param {string} message
|
|
1608
|
-
* @param {{ code?: string, cause?: unknown }} [options]
|
|
1609
|
-
*/
|
|
1610
|
-
constructor(message, options = {}) {
|
|
1611
|
-
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
1612
|
-
this.name = "NonRetryableError";
|
|
1613
|
-
this.permanent = true;
|
|
1614
|
-
if (options.code) this.code = options.code;
|
|
1615
|
-
}
|
|
1616
|
-
};
|
|
1617
|
-
function isNonRetryableError(error) {
|
|
1618
|
-
return !!error && typeof error === "object" && /** @type {any} */
|
|
1619
|
-
error.permanent === true;
|
|
1620
|
-
}
|
|
1621
|
-
function sanitizeErrorSummary(reason) {
|
|
1622
|
-
let s = String(reason ?? "").replace(/\s+/g, " ").trim();
|
|
1623
|
-
s = s.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [redacted]");
|
|
1624
|
-
s = s.replace(/\b[A-Za-z]{2,6}-[A-Za-z0-9_-]{16,}/g, "[redacted]");
|
|
1625
|
-
s = s.replace(/[A-Za-z0-9+/_.-]{48,}/g, "[redacted]");
|
|
1626
|
-
if (s.length > 500) s = `${s.slice(0, 497)}\u2026`;
|
|
1627
|
-
return s;
|
|
1628
|
-
}
|
|
1629
|
-
function summarizeErrorCause(error, stage) {
|
|
1630
|
-
const raw = error && typeof error === "object" ? (
|
|
1631
|
-
/** @type {any} */
|
|
1632
|
-
error
|
|
1633
|
-
) : {};
|
|
1634
|
-
const name = typeof raw.name === "string" && raw.name ? raw.name : "Error";
|
|
1635
|
-
const rawMessage = typeof raw.message === "string" && raw.message ? raw.message : String(error ?? "");
|
|
1636
|
-
const cause = {
|
|
1637
|
-
stage,
|
|
1638
|
-
name: sanitizeErrorSummary(name).slice(0, 100),
|
|
1639
|
-
message: sanitizeErrorSummary(rawMessage)
|
|
1640
|
-
};
|
|
1641
|
-
if (typeof raw.code === "string" && raw.code) {
|
|
1642
|
-
cause.code = sanitizeErrorSummary(raw.code).slice(0, 100);
|
|
1643
|
-
}
|
|
1644
|
-
return cause;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
2138
|
// src/server/lib/message-processor.js
|
|
1648
2139
|
import {
|
|
1649
2140
|
buildContentPush,
|
|
2141
|
+
buildMultipartPushPayloads,
|
|
1650
2142
|
buildReasoningPush,
|
|
1651
2143
|
readReasoningContent,
|
|
1652
|
-
stripReasoningTags
|
|
2144
|
+
stripReasoningTags,
|
|
2145
|
+
DEFAULT_MULTIPART_CHUNK_BYTES,
|
|
2146
|
+
DEFAULT_MULTIPART_MAX_CHUNKS,
|
|
2147
|
+
DEFAULT_MULTIPART_MAX_TOTAL_BYTES,
|
|
2148
|
+
DEFAULT_MULTIPART_TTL_MS
|
|
1653
2149
|
} from "@rei-standard/amsg-shared";
|
|
1654
2150
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
1655
2151
|
var SLEEP_BETWEEN_MESSAGES_MS2 = 1500;
|
|
2152
|
+
var MULTIPART_WINDOW_USAGE = 0.5;
|
|
2153
|
+
var MIN_SLEEP_BETWEEN_CHUNKS_MS = 50;
|
|
1656
2154
|
function splitOnceByRegex(chunk, regex) {
|
|
1657
2155
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
1658
2156
|
if (i % 2 === 0 && part.trim()) {
|
|
@@ -1672,6 +2170,74 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
1672
2170
|
}
|
|
1673
2171
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
1674
2172
|
}
|
|
2173
|
+
async function deliverReasoningPush(ctx, pushSubscription, reasoningPush) {
|
|
2174
|
+
const multipart = resolveMultipartOptions(ctx);
|
|
2175
|
+
try {
|
|
2176
|
+
const serialized = JSON.stringify(reasoningPush);
|
|
2177
|
+
const { bytes, withinLimit } = measurePushPayload(serialized);
|
|
2178
|
+
if (withinLimit) {
|
|
2179
|
+
await sendTaggedPush(ctx.webpush, pushSubscription, serialized);
|
|
2180
|
+
return { shipped: true };
|
|
2181
|
+
}
|
|
2182
|
+
if (bytes > multipart.maxTotalBytes) {
|
|
2183
|
+
throw new Error(`\u601D\u8003\u8FC7\u7A0B ${bytes} \u5B57\u8282\uFF0C\u8D85\u8FC7\u5206\u7247\u4F20\u8F93\u7684 ${multipart.maxTotalBytes} \u5B57\u8282\u4E0A\u9650`);
|
|
2184
|
+
}
|
|
2185
|
+
const parts = buildMultipartPushPayloads(reasoningPush, {
|
|
2186
|
+
serializedPayload: serialized,
|
|
2187
|
+
maxChunkBytes: multipart.maxChunkBytes,
|
|
2188
|
+
ttlMs: multipart.ttlMs
|
|
2189
|
+
});
|
|
2190
|
+
if (parts.length > multipart.maxChunks) {
|
|
2191
|
+
throw new Error(`\u601D\u8003\u8FC7\u7A0B\u8981\u5207 ${parts.length} \u7247\uFF0C\u8D85\u8FC7\u5206\u7247\u4F20\u8F93\u7684 ${multipart.maxChunks} \u7247\u4E0A\u9650`);
|
|
2192
|
+
}
|
|
2193
|
+
const intervalMs = resolveChunkIntervalMs(parts.length, multipart.ttlMs);
|
|
2194
|
+
if (intervalMs === null) {
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`\u601D\u8003\u8FC7\u7A0B\u8981\u5207 ${parts.length} \u7247\uFF0C${multipart.ttlMs} \u6BEB\u79D2\u7684\u5206\u7247\u91CD\u7EC4\u7A97\u53E3\u5185\u53D1\u4E0D\u5B8C`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2200
|
+
await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(parts[i]));
|
|
2201
|
+
if (i < parts.length - 1) {
|
|
2202
|
+
await sleepFor(ctx, intervalMs);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return { shipped: true };
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
if (isTaskCancelledError(error)) throw error;
|
|
2208
|
+
const reason = sanitizeErrorSummary(error && error.message);
|
|
2209
|
+
console.warn("[amsg-server] \u601D\u8003\u8FC7\u7A0B\u672A\u9001\u8FBE\uFF08\u6B63\u6587\u7167\u5E38\u53D1\u9001\uFF09:", reason);
|
|
2210
|
+
return { shipped: false, error: reason };
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
function resolveChunkIntervalMs(chunkCount, windowMs) {
|
|
2214
|
+
if (chunkCount <= 1) return 0;
|
|
2215
|
+
const budgetMs = Math.floor(windowMs * MULTIPART_WINDOW_USAGE);
|
|
2216
|
+
const evenlySpaced = Math.floor(budgetMs / (chunkCount - 1));
|
|
2217
|
+
if (evenlySpaced < MIN_SLEEP_BETWEEN_CHUNKS_MS) return null;
|
|
2218
|
+
return Math.min(SLEEP_BETWEEN_MESSAGES_MS2, evenlySpaced);
|
|
2219
|
+
}
|
|
2220
|
+
function sleepFor(ctx, ms) {
|
|
2221
|
+
if (!(ms > 0)) return Promise.resolve();
|
|
2222
|
+
if (ctx && typeof ctx._pushSleep === "function") return ctx._pushSleep(ms);
|
|
2223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2224
|
+
}
|
|
2225
|
+
function resolveMultipartOptions(ctx) {
|
|
2226
|
+
const configured = ctx && ctx.multipart && typeof ctx.multipart === "object" ? ctx.multipart : {};
|
|
2227
|
+
return {
|
|
2228
|
+
maxChunkBytes: positiveIntegerOr(configured.maxChunkBytes, DEFAULT_MULTIPART_CHUNK_BYTES),
|
|
2229
|
+
maxChunks: positiveIntegerOr(configured.maxChunks, DEFAULT_MULTIPART_MAX_CHUNKS),
|
|
2230
|
+
maxTotalBytes: positiveIntegerOr(configured.maxTotalBytes, DEFAULT_MULTIPART_MAX_TOTAL_BYTES),
|
|
2231
|
+
ttlMs: positiveIntegerOr(configured.ttlMs, DEFAULT_MULTIPART_TTL_MS)
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function positiveIntegerOr(value, fallback) {
|
|
2235
|
+
return Number.isInteger(value) && /** @type {number} */
|
|
2236
|
+
value > 0 ? (
|
|
2237
|
+
/** @type {number} */
|
|
2238
|
+
value
|
|
2239
|
+
) : fallback;
|
|
2240
|
+
}
|
|
1675
2241
|
async function processSingleMessage(task, ctx, providedMasterKey, predecrypted = null) {
|
|
1676
2242
|
try {
|
|
1677
2243
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -1730,9 +2296,10 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
|
|
|
1730
2296
|
const metadata = decryptedPayload.metadata || {};
|
|
1731
2297
|
const messageIdBase = task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_${randomUUID()}_instant`;
|
|
1732
2298
|
const occurrenceMs = occurrenceMsOf(task);
|
|
1733
|
-
const
|
|
2299
|
+
const contentPushes = [];
|
|
2300
|
+
let reasoningPush = null;
|
|
1734
2301
|
if (reasoning) {
|
|
1735
|
-
|
|
2302
|
+
reasoningPush = buildReasoningPush({
|
|
1736
2303
|
messageType: decryptedPayload.messageType,
|
|
1737
2304
|
source,
|
|
1738
2305
|
messageId: `${messageIdBase}_reasoning`,
|
|
@@ -1746,7 +2313,6 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
|
|
|
1746
2313
|
metadata
|
|
1747
2314
|
});
|
|
1748
2315
|
stampTaskIdentity(reasoningPush, task, decryptedPayload, occurrenceMs);
|
|
1749
|
-
pushesToSend.push(reasoningPush);
|
|
1750
2316
|
}
|
|
1751
2317
|
for (let i = 0; i < messages.length; i++) {
|
|
1752
2318
|
const contentPush = buildContentPush({
|
|
@@ -1765,29 +2331,56 @@ async function processSingleMessage(task, ctx, providedMasterKey, predecrypted =
|
|
|
1765
2331
|
metadata
|
|
1766
2332
|
});
|
|
1767
2333
|
stampTaskIdentity(contentPush, task, decryptedPayload, occurrenceMs);
|
|
1768
|
-
|
|
2334
|
+
contentPushes.push(contentPush);
|
|
1769
2335
|
}
|
|
2336
|
+
const pushesToSend = reasoningPush ? [reasoningPush, ...contentPushes] : contentPushes;
|
|
1770
2337
|
await appendPushesToOutbox({ db: ctx.db, userId: task.user_id, userKey, pushes: pushesToSend });
|
|
1771
2338
|
const sentIds = [];
|
|
2339
|
+
let cancelledMidBurst = false;
|
|
2340
|
+
let reasoningError;
|
|
1772
2341
|
try {
|
|
1773
|
-
|
|
1774
|
-
await ctx
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
await
|
|
2342
|
+
if (reasoningPush) {
|
|
2343
|
+
const reasoning2 = await deliverReasoningPush(ctx, pushSubscription, reasoningPush);
|
|
2344
|
+
if (reasoning2.shipped) {
|
|
2345
|
+
sentIds.push(reasoningPush.messageId);
|
|
2346
|
+
await sleepFor(ctx, SLEEP_BETWEEN_MESSAGES_MS2);
|
|
2347
|
+
} else {
|
|
2348
|
+
reasoningError = reasoning2.error;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (let i = 0; i < contentPushes.length; i++) {
|
|
2352
|
+
await sendTaggedPush(ctx.webpush, pushSubscription, JSON.stringify(contentPushes[i]));
|
|
2353
|
+
sentIds.push(contentPushes[i].messageId);
|
|
2354
|
+
if (i < contentPushes.length - 1) {
|
|
2355
|
+
await sleepFor(ctx, SLEEP_BETWEEN_MESSAGES_MS2);
|
|
1778
2356
|
}
|
|
1779
2357
|
}
|
|
2358
|
+
} catch (error) {
|
|
2359
|
+
cancelledMidBurst = isTaskCancelledError(error);
|
|
2360
|
+
throw error;
|
|
1780
2361
|
} finally {
|
|
1781
2362
|
await markPushesDelivered({ db: ctx.db, userId: task.user_id, messageIds: sentIds });
|
|
2363
|
+
if (cancelledMidBurst) {
|
|
2364
|
+
await discardUndeliveredPushes({
|
|
2365
|
+
db: ctx.db,
|
|
2366
|
+
userId: task.user_id,
|
|
2367
|
+
pushes: pushesToSend,
|
|
2368
|
+
sentIds
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
1782
2371
|
}
|
|
1783
|
-
return {
|
|
2372
|
+
return {
|
|
2373
|
+
success: true,
|
|
2374
|
+
messagesSent: messages.length,
|
|
2375
|
+
...reasoningError ? { reasoningError } : {}
|
|
2376
|
+
};
|
|
1784
2377
|
} catch (error) {
|
|
1785
2378
|
return {
|
|
1786
2379
|
success: false,
|
|
1787
2380
|
messagesSent: 0,
|
|
1788
2381
|
error: error.message,
|
|
1789
2382
|
errorCode: error.code || null,
|
|
1790
|
-
pushStatusCode:
|
|
2383
|
+
pushStatusCode: readPushStatusCode(error),
|
|
1791
2384
|
permanent: isNonRetryableError(error)
|
|
1792
2385
|
};
|
|
1793
2386
|
}
|
|
@@ -1821,7 +2414,12 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1821
2414
|
}
|
|
1822
2415
|
const result = await processSingleMessage(task, ctx, masterKey);
|
|
1823
2416
|
if (!result.success) {
|
|
1824
|
-
|
|
2417
|
+
const permanent = isPermanentDeliveryFailure({
|
|
2418
|
+
permanent: result.permanent,
|
|
2419
|
+
errorCode: result.errorCode,
|
|
2420
|
+
pushStatus: result.pushStatusCode
|
|
2421
|
+
});
|
|
2422
|
+
if (!permanent && retryCount < maxRetries) {
|
|
1825
2423
|
retryCount++;
|
|
1826
2424
|
await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
|
|
1827
2425
|
continue;
|
|
@@ -1830,10 +2428,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1830
2428
|
await ctx.db.updateTaskById(task.id, {
|
|
1831
2429
|
status: "failed",
|
|
1832
2430
|
retry_count: retryCount,
|
|
2431
|
+
// 记录的形状跟定时任务那条路一致(同一个 buildErrorExtra):reason
|
|
2432
|
+
// 是给用户看的人话,errorCode / pushStatus 是给下游判定用的——410 =
|
|
2433
|
+
// 订阅已注销,客户端要据此引导用户重新登记,而不是回去正则匹配
|
|
2434
|
+
// reason 里那句话。
|
|
1833
2435
|
last_error: JSON.stringify({
|
|
1834
2436
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1835
2437
|
occurrence: task.next_send_at ?? null,
|
|
1836
|
-
reason: sanitizeErrorSummary(result.error)
|
|
2438
|
+
reason: sanitizeErrorSummary(result.error),
|
|
2439
|
+
...buildErrorExtra(result.errorCode, result.pushStatusCode) || {}
|
|
1837
2440
|
})
|
|
1838
2441
|
});
|
|
1839
2442
|
} catch (_updateError) {
|
|
@@ -1848,7 +2451,7 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1848
2451
|
code: "PROCESSING_ERROR",
|
|
1849
2452
|
message: result.error,
|
|
1850
2453
|
retriesAttempted: retryCount,
|
|
1851
|
-
...
|
|
2454
|
+
...permanent ? { permanent: true } : {}
|
|
1852
2455
|
}
|
|
1853
2456
|
};
|
|
1854
2457
|
}
|
|
@@ -1868,7 +2471,12 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1868
2471
|
}
|
|
1869
2472
|
};
|
|
1870
2473
|
}
|
|
1871
|
-
return {
|
|
2474
|
+
return {
|
|
2475
|
+
success: true,
|
|
2476
|
+
messagesSent: result.messagesSent,
|
|
2477
|
+
retriesUsed: retryCount,
|
|
2478
|
+
...result.reasoningError ? { reasoningError: result.reasoningError } : {}
|
|
2479
|
+
};
|
|
1872
2480
|
}
|
|
1873
2481
|
}
|
|
1874
2482
|
|
|
@@ -2004,7 +2612,12 @@ function createScheduleMessageHandler(ctx) {
|
|
|
2004
2612
|
llmExtraBody: payload.llmExtraBody ?? null,
|
|
2005
2613
|
metadata: payload.metadata || {}
|
|
2006
2614
|
};
|
|
2007
|
-
const
|
|
2615
|
+
const serializedTaskData = JSON.stringify(fullTaskData);
|
|
2616
|
+
const sizeError = validateTaskPayloadSize(serializedTaskData);
|
|
2617
|
+
if (sizeError) {
|
|
2618
|
+
return { status: 400, body: { success: false, error: sizeError } };
|
|
2619
|
+
}
|
|
2620
|
+
const encryptedPayload = await encryptForStorage(serializedTaskData, userKey);
|
|
2008
2621
|
if (payload.messageType === "instant") {
|
|
2009
2622
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
2010
2623
|
return {
|
|
@@ -2064,6 +2677,9 @@ function createScheduleMessageHandler(ctx) {
|
|
|
2064
2677
|
if (!dbResult) {
|
|
2065
2678
|
return { status: 500, body: { success: false, error: { code: "TASK_CREATE_FAILED", message: "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25" } } };
|
|
2066
2679
|
}
|
|
2680
|
+
if (superseded) {
|
|
2681
|
+
await discardUndeliveredPushesForTask({ db, userId, taskUuid: supersedesUuid });
|
|
2682
|
+
}
|
|
2067
2683
|
if (payload.messageType === "instant") {
|
|
2068
2684
|
try {
|
|
2069
2685
|
const sendResult = await processMessagesByUuid(taskUuid, {
|
|
@@ -2085,6 +2701,9 @@ function createScheduleMessageHandler(ctx) {
|
|
|
2085
2701
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2086
2702
|
status: "sent",
|
|
2087
2703
|
retriesUsed: sendResult.retriesUsed || 0,
|
|
2704
|
+
// 思考过程那条 push 没发出去的原因(有才带上)。正文照发,所以
|
|
2705
|
+
// 这次调用仍是成功的;调用方拿它决定要不要提示 / 重发。
|
|
2706
|
+
...sendResult.reasoningError ? { reasoningError: sendResult.reasoningError } : {},
|
|
2088
2707
|
...supersedesUuid ? { superseded } : {}
|
|
2089
2708
|
}
|
|
2090
2709
|
}
|
|
@@ -2120,10 +2739,50 @@ var CLAIM_LEASE_MARGIN_MS = 2 * 60 * 1e3;
|
|
|
2120
2739
|
var DEFAULT_LEASE_HEARTBEAT_MS = 30 * 1e3;
|
|
2121
2740
|
var DEFAULT_HEARTBEAT_LEASE_TTL_MS = 90 * 1e3;
|
|
2122
2741
|
var STALE_AFTER_MS = 60 * 60 * 1e3;
|
|
2123
|
-
var
|
|
2742
|
+
var adaptersWithoutLastErrorColumn = /* @__PURE__ */ new WeakSet();
|
|
2743
|
+
var lastErrorColumnSuspicions = /* @__PURE__ */ new WeakMap();
|
|
2744
|
+
var warnedAboutMissingLastErrorColumn = false;
|
|
2745
|
+
var adaptersWarnedAboutRetryAfter = /* @__PURE__ */ new WeakSet();
|
|
2746
|
+
function warnMissingRetryAfterColumn(db) {
|
|
2747
|
+
if (!db || adaptersWarnedAboutRetryAfter.has(db)) return;
|
|
2748
|
+
adaptersWarnedAboutRetryAfter.add(db);
|
|
2749
|
+
console.warn(
|
|
2750
|
+
"[amsg-server] \u9002\u914D\u5668\u8FD4\u56DE\u7684\u4EFB\u52A1\u884C\u91CC\u6CA1\u6709 retry_after\uFF0C\u9000\u907F\u5B88\u536B\u5931\u6548\uFF1A\u8FD8\u5728\u9000\u907F\u91CC\u7684\u4EFB\u52A1\u4F1A\u88AB\u7ACB\u523B\u91CD\u8DD1\u3002\u6295\u9012\u8DEF\u5F84\u7684\u884C\u8981\u5E26\u4E0A\u8FD9\u4E00\u5217\uFF0C\u89C1 adapters/interface.js \u7684 TASK_DELIVERY_COLUMNS\u3002"
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
function warnMissingLastErrorColumn(error) {
|
|
2754
|
+
if (warnedAboutMissingLastErrorColumn) return;
|
|
2755
|
+
warnedAboutMissingLastErrorColumn = true;
|
|
2756
|
+
console.warn(
|
|
2757
|
+
"[amsg-server] \u5E26 last_error \u7684\u5199\u6CA1\u6210\u529F\uFF0C\u5DF2\u9000\u56DE\u53EA\u5199\u72B6\u6001\u5B57\u6BB5\uFF08\u5931\u8D25\u539F\u56E0\u4ECD\u8BB0\u5728 payload \u7684 lastError \u91CC\uFF09\u3002\u5E93\u5347\u7EA7\u540E\u6CA1\u91CD\u8DD1\u8FC7 /init-tenant \u7684\u8BDD\uFF0C\u8865\u4E00\u6B21\u8868\u7ED3\u6784\u5C31\u80FD\u6062\u590D\uFF1B\u53EA\u662F\u5076\u53D1\u7684\u8BDD\u4E0D\u7528\u7BA1:",
|
|
2758
|
+
error && /** @type {{ message?: unknown }} */
|
|
2759
|
+
error.message
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2124
2762
|
function toPushStatus(value) {
|
|
2125
2763
|
return Number.isInteger(value) ? value : null;
|
|
2126
2764
|
}
|
|
2765
|
+
function rowVanished(writeResult) {
|
|
2766
|
+
return writeResult === null || writeResult === false;
|
|
2767
|
+
}
|
|
2768
|
+
function guardWebpushWithLease(webpush, lease) {
|
|
2769
|
+
if (!webpush || typeof webpush.sendNotification !== "function") return webpush;
|
|
2770
|
+
const guarded = Object.create(webpush);
|
|
2771
|
+
Object.defineProperty(guarded, "sendNotification", {
|
|
2772
|
+
value: async (...args) => {
|
|
2773
|
+
if (lease.lost) {
|
|
2774
|
+
const error = new Error("\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF0C\u63A8\u9001\u5DF2\u4E2D\u6B62");
|
|
2775
|
+
error.code = TASK_CANCELLED_CODE;
|
|
2776
|
+
throw error;
|
|
2777
|
+
}
|
|
2778
|
+
return webpush.sendNotification(...args);
|
|
2779
|
+
},
|
|
2780
|
+
writable: true,
|
|
2781
|
+
enumerable: true,
|
|
2782
|
+
configurable: true
|
|
2783
|
+
});
|
|
2784
|
+
return guarded;
|
|
2785
|
+
}
|
|
2127
2786
|
function resolveStaleAfterMs(ctx) {
|
|
2128
2787
|
return positiveNumber(ctx.staleAfterMs) || STALE_AFTER_MS;
|
|
2129
2788
|
}
|
|
@@ -2153,8 +2812,19 @@ async function runScheduledTick(ctx) {
|
|
|
2153
2812
|
console.warn("[amsg-server] cleanupOutbox \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
2154
2813
|
}
|
|
2155
2814
|
}
|
|
2815
|
+
await cleanupExpiredClientState(ctx);
|
|
2156
2816
|
return summary;
|
|
2157
2817
|
}
|
|
2818
|
+
async function cleanupExpiredClientState(ctx) {
|
|
2819
|
+
if (typeof ctx.db.cleanupClientState !== "function") return;
|
|
2820
|
+
const targets = planClientStateCleanup(ctx.clientStateTtl, Date.now());
|
|
2821
|
+
if (targets.length === 0) return;
|
|
2822
|
+
try {
|
|
2823
|
+
await ctx.db.cleanupClientState(targets);
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
console.warn("[amsg-server] cleanupClientState \u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09:", error && error.message);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2158
2828
|
async function runTask(ctx, uuid) {
|
|
2159
2829
|
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
2160
2830
|
throw new TypeError("runTask(ctx, uuid) requires a non-empty uuid string");
|
|
@@ -2171,6 +2841,9 @@ async function runTask(ctx, uuid) {
|
|
|
2171
2841
|
if (Number.isFinite(dueMs) && dueMs > Date.now()) {
|
|
2172
2842
|
return { ran: false, reason: "not_due", nextSendAt: task.next_send_at };
|
|
2173
2843
|
}
|
|
2844
|
+
if (!Object.prototype.hasOwnProperty.call(task, "retry_after")) {
|
|
2845
|
+
warnMissingRetryAfterColumn(ctx.db);
|
|
2846
|
+
}
|
|
2174
2847
|
const retryAfterMs = task.retry_after ? Date.parse(task.retry_after) : NaN;
|
|
2175
2848
|
if (Number.isFinite(retryAfterMs) && retryAfterMs > Date.now()) {
|
|
2176
2849
|
return { ran: false, reason: "retry_pending", retryAfter: task.retry_after };
|
|
@@ -2197,6 +2870,8 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2197
2870
|
deletedOnceOffTasks: 0,
|
|
2198
2871
|
updatedRecurringTasks: 0,
|
|
2199
2872
|
staleTasks: [],
|
|
2873
|
+
cancelledTasks: [],
|
|
2874
|
+
reasoningSkippedTasks: [],
|
|
2200
2875
|
failedTasks: []
|
|
2201
2876
|
};
|
|
2202
2877
|
const groupsTakenThisTick = /* @__PURE__ */ new Set();
|
|
@@ -2209,17 +2884,25 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2209
2884
|
return !!await db.claimTask(task.id, task.next_send_at, leaseUntil, serializeGroup);
|
|
2210
2885
|
}
|
|
2211
2886
|
function startLeaseHeartbeat(task) {
|
|
2212
|
-
|
|
2213
|
-
};
|
|
2887
|
+
const lease = { lost: false, released: false, stop: () => {
|
|
2888
|
+
} };
|
|
2889
|
+
if (!heartbeatEnabled) return lease;
|
|
2214
2890
|
let stopped = false;
|
|
2215
2891
|
let timer = null;
|
|
2216
2892
|
const beat = async () => {
|
|
2217
2893
|
if (stopped) return;
|
|
2894
|
+
let renewed;
|
|
2218
2895
|
try {
|
|
2219
|
-
await db.renewTaskLease(task.id, new Date(Date.now() + heartbeatLeaseTtlMs).toISOString());
|
|
2896
|
+
renewed = await db.renewTaskLease(task.id, new Date(Date.now() + heartbeatLeaseTtlMs).toISOString());
|
|
2220
2897
|
} catch (error) {
|
|
2221
2898
|
console.warn("[amsg-server] \u79DF\u7EA6\u7EED\u79DF\u5931\u8D25\uFF08\u4E0B\u4E2A\u5FC3\u8DF3\u518D\u8BD5\uFF09:", error && error.message);
|
|
2222
2899
|
}
|
|
2900
|
+
if (renewed === false) {
|
|
2901
|
+
if (stopped || lease.released) return;
|
|
2902
|
+
lease.lost = true;
|
|
2903
|
+
console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u7684\u79DF\u7EA6\u5DF2\u5931\u6548\uFF08\u884C\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF09\uFF0C\u5269\u4F59\u63A8\u9001\u5C06\u4E2D\u6B62`);
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2223
2906
|
if (!stopped) schedule();
|
|
2224
2907
|
};
|
|
2225
2908
|
const schedule = () => {
|
|
@@ -2227,22 +2910,42 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2227
2910
|
if (timer && typeof timer.unref === "function") timer.unref();
|
|
2228
2911
|
};
|
|
2229
2912
|
schedule();
|
|
2230
|
-
|
|
2913
|
+
lease.stop = () => {
|
|
2231
2914
|
stopped = true;
|
|
2232
2915
|
if (timer) clearTimeout(timer);
|
|
2233
2916
|
};
|
|
2917
|
+
return lease;
|
|
2918
|
+
}
|
|
2919
|
+
const activeLeases = /* @__PURE__ */ new Map();
|
|
2920
|
+
function markLeaseReleased(taskId) {
|
|
2921
|
+
const lease = activeLeases.get(taskId);
|
|
2922
|
+
if (lease) lease.released = true;
|
|
2234
2923
|
}
|
|
2235
|
-
const supportsLastError = supportsClaim;
|
|
2236
2924
|
async function updateTaskWithLastError(taskId, fields) {
|
|
2925
|
+
if (fields.lease_until === null) markLeaseReleased(taskId);
|
|
2926
|
+
if (!Object.prototype.hasOwnProperty.call(fields, "last_error")) {
|
|
2927
|
+
return db.updateTaskById(taskId, fields);
|
|
2928
|
+
}
|
|
2929
|
+
const { last_error: _lastError, ...stateFields } = fields;
|
|
2930
|
+
if (adaptersWithoutLastErrorColumn.has(db)) return db.updateTaskById(taskId, stateFields);
|
|
2931
|
+
let combinedError;
|
|
2237
2932
|
try {
|
|
2238
|
-
|
|
2933
|
+
const result2 = await db.updateTaskById(taskId, fields);
|
|
2934
|
+
lastErrorColumnSuspicions.delete(db);
|
|
2935
|
+
return result2;
|
|
2239
2936
|
} catch (error) {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2937
|
+
combinedError = error;
|
|
2938
|
+
}
|
|
2939
|
+
const result = await db.updateTaskById(taskId, stateFields);
|
|
2940
|
+
warnMissingLastErrorColumn(combinedError);
|
|
2941
|
+
const suspicions = (lastErrorColumnSuspicions.get(db) || 0) + 1;
|
|
2942
|
+
if (suspicions >= 2) {
|
|
2943
|
+
adaptersWithoutLastErrorColumn.add(db);
|
|
2944
|
+
lastErrorColumnSuspicions.delete(db);
|
|
2945
|
+
} else {
|
|
2946
|
+
lastErrorColumnSuspicions.set(db, suspicions);
|
|
2245
2947
|
}
|
|
2948
|
+
return result;
|
|
2246
2949
|
}
|
|
2247
2950
|
function lastErrorJson(task, reason, extra) {
|
|
2248
2951
|
return JSON.stringify({
|
|
@@ -2282,8 +2985,26 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2282
2985
|
groupsTakenThisTick.add(scopedKey);
|
|
2283
2986
|
return { taken: false, rawKey };
|
|
2284
2987
|
}
|
|
2988
|
+
function recordCancelled(task, status) {
|
|
2989
|
+
results.cancelledTasks.push({
|
|
2990
|
+
taskId: task.id,
|
|
2991
|
+
reason: "\u4EFB\u52A1\u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF",
|
|
2992
|
+
status
|
|
2993
|
+
});
|
|
2994
|
+
console.warn(`[amsg-server] \u4EFB\u52A1 ${task.id} \u5728\u6295\u9012\u671F\u95F4\u88AB\u53D6\u6D88\u6216\u9876\u66FF\uFF08${status}\uFF09`);
|
|
2995
|
+
}
|
|
2996
|
+
async function payloadStillFresh(task) {
|
|
2997
|
+
if (typeof db.getTaskByUuid !== "function" || !task.uuid) return true;
|
|
2998
|
+
try {
|
|
2999
|
+
const current = await db.getTaskByUuid(task.uuid, task.user_id);
|
|
3000
|
+
return Boolean(current) && current.encrypted_payload === task.encrypted_payload;
|
|
3001
|
+
} catch (_readError) {
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
2285
3005
|
async function encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, extra) {
|
|
2286
3006
|
if (!decryptedPayload || !userKey) return null;
|
|
3007
|
+
if (!await payloadStillFresh(task)) return null;
|
|
2287
3008
|
try {
|
|
2288
3009
|
return await encryptForStorage(JSON.stringify({
|
|
2289
3010
|
...decryptedPayload,
|
|
@@ -2298,6 +3019,16 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2298
3019
|
return null;
|
|
2299
3020
|
}
|
|
2300
3021
|
}
|
|
3022
|
+
async function encryptPayloadWithoutLastError(task, decryptedPayload, userKey) {
|
|
3023
|
+
if (!decryptedPayload || !userKey) return null;
|
|
3024
|
+
if (!await payloadStillFresh(task)) return null;
|
|
3025
|
+
try {
|
|
3026
|
+
const { lastError: _cleared, ...rest } = decryptedPayload;
|
|
3027
|
+
return await encryptForStorage(JSON.stringify(rest), userKey);
|
|
3028
|
+
} catch (_encryptError) {
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
2301
3032
|
async function notifyStaleSkip(task, info) {
|
|
2302
3033
|
if (typeof ctx.onStaleSkip !== "function") return;
|
|
2303
3034
|
try {
|
|
@@ -2311,8 +3042,8 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2311
3042
|
const errorCode = failure.errorCode ?? null;
|
|
2312
3043
|
const pushStatus = toPushStatus(failure.pushStatus);
|
|
2313
3044
|
const tzId = decryptedPayload ? decryptedPayload.tzId ?? null : null;
|
|
2314
|
-
const permanent = failure.permanent
|
|
2315
|
-
const errorExtra =
|
|
3045
|
+
const permanent = isPermanentDeliveryFailure({ permanent: failure.permanent, errorCode, pushStatus });
|
|
3046
|
+
const errorExtra = buildErrorExtra(errorCode, pushStatus);
|
|
2316
3047
|
try {
|
|
2317
3048
|
if (permanent || task.retry_count >= 3) {
|
|
2318
3049
|
const encrypted = await encryptPayloadWithLastError(task, decryptedPayload, userKey, reason, errorExtra);
|
|
@@ -2322,14 +3053,14 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2322
3053
|
next_send_at: nextSendAt,
|
|
2323
3054
|
retry_count: 0,
|
|
2324
3055
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2325
|
-
|
|
3056
|
+
last_error: lastErrorJson(task, reason, errorExtra)
|
|
2326
3057
|
});
|
|
2327
3058
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "occurrence_skipped", nextSendAt, ...permanent ? { permanent: true } : {} });
|
|
2328
3059
|
} else {
|
|
2329
3060
|
await updateAndRelease(task.id, {
|
|
2330
3061
|
status: "failed",
|
|
2331
3062
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2332
|
-
|
|
3063
|
+
last_error: lastErrorJson(task, reason, errorExtra)
|
|
2333
3064
|
});
|
|
2334
3065
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed", ...permanent ? { permanent: true } : {} });
|
|
2335
3066
|
}
|
|
@@ -2340,10 +3071,14 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2340
3071
|
retry_after: nextRetryTime.toISOString(),
|
|
2341
3072
|
lease_until: null,
|
|
2342
3073
|
retry_count: task.retry_count + 1,
|
|
2343
|
-
|
|
3074
|
+
last_error: lastErrorJson(task, reason, errorExtra)
|
|
2344
3075
|
});
|
|
2345
3076
|
} else {
|
|
2346
|
-
await
|
|
3077
|
+
await updateTaskWithLastError(task.id, {
|
|
3078
|
+
next_send_at: nextRetryTime.toISOString(),
|
|
3079
|
+
retry_count: task.retry_count + 1,
|
|
3080
|
+
last_error: lastErrorJson(task, reason, errorExtra)
|
|
3081
|
+
});
|
|
2347
3082
|
}
|
|
2348
3083
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
|
|
2349
3084
|
}
|
|
@@ -2401,14 +3136,16 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2401
3136
|
results.claimSkippedTasks++;
|
|
2402
3137
|
return;
|
|
2403
3138
|
}
|
|
2404
|
-
const
|
|
3139
|
+
const lease = startLeaseHeartbeat(task);
|
|
3140
|
+
activeLeases.set(task.id, lease);
|
|
2405
3141
|
try {
|
|
2406
|
-
await deliverClaimedTask(task, decrypted);
|
|
3142
|
+
await deliverClaimedTask(task, decrypted, lease);
|
|
2407
3143
|
} finally {
|
|
2408
|
-
|
|
3144
|
+
lease.stop();
|
|
3145
|
+
activeLeases.delete(task.id);
|
|
2409
3146
|
}
|
|
2410
3147
|
}
|
|
2411
|
-
async function deliverClaimedTask(task, decrypted) {
|
|
3148
|
+
async function deliverClaimedTask(task, decrypted, lease) {
|
|
2412
3149
|
const decryptedPayload = decrypted.ok ? decrypted.payload : null;
|
|
2413
3150
|
const userKey = decrypted.ok ? decrypted.userKey : null;
|
|
2414
3151
|
if (!decrypted.ok) {
|
|
@@ -2428,6 +3165,16 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2428
3165
|
userKey,
|
|
2429
3166
|
maxStateValueBytes: ctx.maxStateValueBytes
|
|
2430
3167
|
});
|
|
3168
|
+
const { emitResult } = createResultEmitter({
|
|
3169
|
+
db,
|
|
3170
|
+
task,
|
|
3171
|
+
userKey,
|
|
3172
|
+
decryptedPayload,
|
|
3173
|
+
messageIdBase: task.id != null ? `msg_task_${task.id}${occurrenceSuffix(task)}` : `msg_stale_${task.uuid || ""}`,
|
|
3174
|
+
sessionId: task.id != null ? `sess_task_${task.id}${occurrenceSuffix(task)}` : `sess_stale_${task.uuid || ""}`,
|
|
3175
|
+
occurrenceMs,
|
|
3176
|
+
webpush: ctx.webpush
|
|
3177
|
+
});
|
|
2431
3178
|
const recurring = isRecurringType(recurrenceType);
|
|
2432
3179
|
const plan = recurring ? planNextOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId) : null;
|
|
2433
3180
|
const nextSendAt = recurring ? new Date(plan.nextMs).toISOString() : null;
|
|
@@ -2443,7 +3190,7 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2443
3190
|
await updateAndRelease(task.id, {
|
|
2444
3191
|
...recurring ? { next_send_at: nextSendAt, retry_count: 0 } : { status: "failed" },
|
|
2445
3192
|
...encrypted ? { encrypted_payload: encrypted } : {},
|
|
2446
|
-
|
|
3193
|
+
last_error: lastErrorJson(task, "stale", recurring ? { skippedCount, nextSendAt } : void 0)
|
|
2447
3194
|
});
|
|
2448
3195
|
results.staleTasks.push({
|
|
2449
3196
|
taskId: task.id,
|
|
@@ -2462,7 +3209,8 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2462
3209
|
skippedOccurrences: recurring ? plan.skippedOccurrences : [occurrenceMs],
|
|
2463
3210
|
skippedTruncated: recurring ? plan.skippedTruncated : false,
|
|
2464
3211
|
nextSendAt,
|
|
2465
|
-
...stateAccessors
|
|
3212
|
+
...stateAccessors,
|
|
3213
|
+
emitResult
|
|
2466
3214
|
});
|
|
2467
3215
|
} catch (error) {
|
|
2468
3216
|
results.failedCount++;
|
|
@@ -2474,22 +3222,30 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2474
3222
|
try {
|
|
2475
3223
|
sendResult = await processSingleMessage(
|
|
2476
3224
|
task,
|
|
2477
|
-
{ ...ctx, db, masterKey },
|
|
3225
|
+
{ ...ctx, db, masterKey, webpush: guardWebpushWithLease(ctx.webpush, lease) },
|
|
2478
3226
|
masterKey,
|
|
2479
3227
|
{ userKey, payload: decryptedPayload }
|
|
2480
3228
|
);
|
|
2481
3229
|
} catch (error) {
|
|
3230
|
+
if (lease.lost) {
|
|
3231
|
+
recordCancelled(task, "cancelled_mid_delivery");
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
2482
3234
|
await handleDeliveryFailure(
|
|
2483
3235
|
task,
|
|
2484
3236
|
error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25",
|
|
2485
3237
|
recurrenceType,
|
|
2486
3238
|
decryptedPayload,
|
|
2487
3239
|
userKey,
|
|
2488
|
-
{ errorCode: error.code || null, permanent: error.permanent === true, pushStatus:
|
|
3240
|
+
{ errorCode: error.code || null, permanent: error.permanent === true, pushStatus: null }
|
|
2489
3241
|
);
|
|
2490
3242
|
return;
|
|
2491
3243
|
}
|
|
2492
3244
|
if (!sendResult.success) {
|
|
3245
|
+
if (lease.lost) {
|
|
3246
|
+
recordCancelled(task, "cancelled_mid_delivery");
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
2493
3249
|
await handleDeliveryFailure(
|
|
2494
3250
|
task,
|
|
2495
3251
|
sendResult.error || "\u6D88\u606F\u53D1\u9001\u5931\u8D25",
|
|
@@ -2500,17 +3256,33 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2500
3256
|
);
|
|
2501
3257
|
return;
|
|
2502
3258
|
}
|
|
3259
|
+
if (sendResult.reasoningError) {
|
|
3260
|
+
results.reasoningSkippedTasks.push({ taskId: task.id, reason: sendResult.reasoningError });
|
|
3261
|
+
console.warn(
|
|
3262
|
+
`[amsg-server] \u4EFB\u52A1 ${task.id} \u7684\u6B63\u6587\u5DF2\u9001\u8FBE\uFF0C\u601D\u8003\u8FC7\u7A0B\u6CA1\u53D1\u51FA\u53BB: ${sendResult.reasoningError}`
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
2503
3265
|
try {
|
|
2504
3266
|
if (recurrenceType === "none") {
|
|
2505
|
-
|
|
3267
|
+
markLeaseReleased(task.id);
|
|
3268
|
+
if (rowVanished(await db.deleteTaskById(task.id))) {
|
|
3269
|
+
recordCancelled(task, "cancelled_after_delivery");
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
2506
3272
|
results.deletedOnceOffTasks++;
|
|
2507
3273
|
} else {
|
|
2508
3274
|
const nextSendAt = nextFutureOccurrence(occurrenceMs, recurrenceType, Date.now(), tzId);
|
|
2509
|
-
await
|
|
3275
|
+
const clearedPayload = decryptedPayload && decryptedPayload.lastError ? await encryptPayloadWithoutLastError(task, decryptedPayload, userKey) : null;
|
|
3276
|
+
const updated = await updateAndRelease(task.id, {
|
|
2510
3277
|
next_send_at: nextSendAt,
|
|
2511
3278
|
retry_count: 0,
|
|
2512
|
-
|
|
3279
|
+
last_error: null,
|
|
3280
|
+
...clearedPayload ? { encrypted_payload: clearedPayload } : {}
|
|
2513
3281
|
});
|
|
3282
|
+
if (rowVanished(updated)) {
|
|
3283
|
+
recordCancelled(task, "cancelled_after_delivery");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
2514
3286
|
results.updatedRecurringTasks++;
|
|
2515
3287
|
}
|
|
2516
3288
|
results.successCount++;
|
|
@@ -2558,12 +3330,20 @@ async function deliverTasks(ctx, tasks) {
|
|
|
2558
3330
|
deletedOnceOffTasks: results.deletedOnceOffTasks,
|
|
2559
3331
|
updatedRecurringTasks: results.updatedRecurringTasks,
|
|
2560
3332
|
staleTasks: results.staleTasks,
|
|
3333
|
+
// 投递期间行被取消 / 顶替的任务。`cancelled_mid_delivery` = 推送在发出去
|
|
3334
|
+
// 之前被拦下;`cancelled_after_delivery` = 推送已经发完,收尾写库才发现
|
|
3335
|
+
// 行没了。两种都不计入 successCount / failedCount。
|
|
3336
|
+
cancelledTasks: results.cancelledTasks,
|
|
3337
|
+
// 正文送到了、只有思考过程没发出去的任务({ taskId, reason })。这些任务
|
|
3338
|
+
// 照常计入 successCount——列在这里只是让「这次没有思考过程」看得见。
|
|
3339
|
+
reasoningSkippedTasks: results.reasoningSkippedTasks,
|
|
2561
3340
|
failedTasks: results.failedTasks
|
|
2562
3341
|
}
|
|
2563
3342
|
};
|
|
2564
3343
|
}
|
|
2565
3344
|
|
|
2566
3345
|
// src/server/handlers/update-message.js
|
|
3346
|
+
var REQUEST_FIELD_BY_COLUMN = { next_send_at: "nextSendAt" };
|
|
2567
3347
|
function createUpdateMessageHandler(ctx) {
|
|
2568
3348
|
async function PUT(url, headers, body) {
|
|
2569
3349
|
const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
|
|
@@ -2610,6 +3390,15 @@ function createUpdateMessageHandler(ctx) {
|
|
|
2610
3390
|
if (Object.prototype.hasOwnProperty.call(updates, "contactName") && (typeof updates.contactName !== "string" || !updates.contactName.trim())) {
|
|
2611
3391
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", details: { invalidFields: ["contactName"] } } } };
|
|
2612
3392
|
}
|
|
3393
|
+
if (Object.prototype.hasOwnProperty.call(updates, "userMessage") && updates.userMessage !== null && typeof updates.userMessage !== "string") {
|
|
3394
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "userMessage \u5FC5\u987B\u662F\u5B57\u7B26\u4E32", details: { invalidFields: ["userMessage"] } } } };
|
|
3395
|
+
}
|
|
3396
|
+
if (Object.prototype.hasOwnProperty.call(updates, "messageSubtype") && updates.messageSubtype !== null && typeof updates.messageSubtype !== "string") {
|
|
3397
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "messageSubtype \u5FC5\u987B\u662F\u5B57\u7B26\u4E32", details: { invalidFields: ["messageSubtype"] } } } };
|
|
3398
|
+
}
|
|
3399
|
+
if (Object.prototype.hasOwnProperty.call(updates, "llmExtraBody") && updates.llmExtraBody !== null && !isPlainObject(updates.llmExtraBody)) {
|
|
3400
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "llmExtraBody \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61", details: { invalidFields: ["llmExtraBody"] } } } };
|
|
3401
|
+
}
|
|
2613
3402
|
if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
|
|
2614
3403
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
|
|
2615
3404
|
}
|
|
@@ -2685,8 +3474,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
2685
3474
|
promptUpdates.messages = updates.messages;
|
|
2686
3475
|
promptUpdates.completePrompt = null;
|
|
2687
3476
|
}
|
|
2688
|
-
const
|
|
2689
|
-
...existingData,
|
|
3477
|
+
const appliedPatch = {
|
|
2690
3478
|
...promptUpdates,
|
|
2691
3479
|
...Object.prototype.hasOwnProperty.call(updates, "contactName") && { contactName: updates.contactName },
|
|
2692
3480
|
...updates.userMessage && { userMessage: updates.userMessage },
|
|
@@ -2710,9 +3498,20 @@ function createUpdateMessageHandler(ctx) {
|
|
|
2710
3498
|
// splitPattern: hasOwnProperty so that explicit `null` (= revert to
|
|
2711
3499
|
// default) doesn't get swallowed by truthy-spread the way the optional
|
|
2712
3500
|
// string fields above are.
|
|
2713
|
-
...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
|
|
3501
|
+
...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null },
|
|
3502
|
+
// messageSubtype / llmExtraBody:显式传 null 表示改回默认(分别是投递时
|
|
3503
|
+
// 的 'chat' 和「不透传额外参数」),所以跟 tzId 那几个一样走
|
|
3504
|
+
// hasOwnProperty,不被 truthy spread 吞掉。
|
|
3505
|
+
...Object.prototype.hasOwnProperty.call(updates, "messageSubtype") && { messageSubtype: updates.messageSubtype ?? null },
|
|
3506
|
+
...Object.prototype.hasOwnProperty.call(updates, "llmExtraBody") && { llmExtraBody: updates.llmExtraBody ?? null }
|
|
2714
3507
|
};
|
|
2715
|
-
const
|
|
3508
|
+
const updatedData = { ...existingData, ...appliedPatch };
|
|
3509
|
+
const serializedUpdatedData = JSON.stringify(updatedData);
|
|
3510
|
+
const sizeError = validateTaskPayloadSize(serializedUpdatedData);
|
|
3511
|
+
if (sizeError && sizeError.details.bytes > taskPayloadByteLength(JSON.stringify(existingData))) {
|
|
3512
|
+
return { status: 400, body: { success: false, error: sizeError } };
|
|
3513
|
+
}
|
|
3514
|
+
const encryptedPayload = await encryptForStorage(serializedUpdatedData, userKey);
|
|
2716
3515
|
const extraFields = {
|
|
2717
3516
|
retry_count: 0,
|
|
2718
3517
|
...typeof db.claimTask === "function" ? { retry_after: null } : {},
|
|
@@ -2722,13 +3521,18 @@ function createUpdateMessageHandler(ctx) {
|
|
|
2722
3521
|
if (!result) {
|
|
2723
3522
|
return { status: 409, body: { success: false, error: { code: "UPDATE_CONFLICT", message: "\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25\uFF0C\u4EFB\u52A1\u53EF\u80FD\u5DF2\u88AB\u4FEE\u6539\u6216\u5220\u9664" } } };
|
|
2724
3523
|
}
|
|
3524
|
+
const applied = /* @__PURE__ */ new Set([
|
|
3525
|
+
...Object.keys(appliedPatch),
|
|
3526
|
+
...Object.keys(extraFields).map((column) => REQUEST_FIELD_BY_COLUMN[column])
|
|
3527
|
+
]);
|
|
3528
|
+
const updatedFields = Object.keys(updates).filter((name) => applied.has(name));
|
|
2725
3529
|
return {
|
|
2726
3530
|
status: 200,
|
|
2727
3531
|
body: {
|
|
2728
3532
|
success: true,
|
|
2729
3533
|
data: {
|
|
2730
3534
|
uuid: taskUuid,
|
|
2731
|
-
updatedFields
|
|
3535
|
+
updatedFields,
|
|
2732
3536
|
updatedAt: result.updated_at
|
|
2733
3537
|
}
|
|
2734
3538
|
}
|
|
@@ -2760,6 +3564,7 @@ function createCancelMessageHandler(ctx) {
|
|
|
2760
3564
|
body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } }
|
|
2761
3565
|
};
|
|
2762
3566
|
}
|
|
3567
|
+
await discardUndeliveredPushesForTask({ db, userId, taskUuid });
|
|
2763
3568
|
return {
|
|
2764
3569
|
status: 200,
|
|
2765
3570
|
body: {
|
|
@@ -2942,10 +3747,18 @@ function createPushSubscriptionHandler(ctx) {
|
|
|
2942
3747
|
const { userId } = gate;
|
|
2943
3748
|
if (!supportsPushSubscriptionStore(db)) return UNSUPPORTED;
|
|
2944
3749
|
const userKey = await deriveUserEncryptionKey(userId, masterKey);
|
|
3750
|
+
let row;
|
|
3751
|
+
try {
|
|
3752
|
+
row = await db.getPushSubscription(userId);
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
console.error("[amsg-server] push-subscription \u67E5\u8BE2\u5931\u8D25:", error && error.message);
|
|
3755
|
+
return err(503, "PUSH_SUBSCRIPTION_LOOKUP_FAILED", "\u63A8\u9001\u8BA2\u9605\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
3756
|
+
}
|
|
2945
3757
|
let stored = null;
|
|
2946
3758
|
try {
|
|
2947
|
-
stored = await
|
|
2948
|
-
} catch (
|
|
3759
|
+
stored = await decodePushSubscriptionRow(row, userKey);
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
console.warn("[amsg-server] push-subscription \u884C\u89E3\u5BC6\u5931\u8D25\uFF08\u6309\u672A\u767B\u8BB0\u5904\u7406\uFF09:", error && error.message);
|
|
2949
3762
|
stored = null;
|
|
2950
3763
|
}
|
|
2951
3764
|
return {
|
|
@@ -3516,7 +4329,7 @@ var D1Adapter = class {
|
|
|
3516
4329
|
}
|
|
3517
4330
|
async getTaskByUuid(uuid, userId) {
|
|
3518
4331
|
return this._db.prepare(
|
|
3519
|
-
`SELECT
|
|
4332
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
3520
4333
|
FROM scheduled_messages
|
|
3521
4334
|
WHERE uuid = ? AND user_id = ? AND status = 'pending'
|
|
3522
4335
|
LIMIT 1`
|
|
@@ -3524,7 +4337,7 @@ var D1Adapter = class {
|
|
|
3524
4337
|
}
|
|
3525
4338
|
async getTaskByUuidOnly(uuid) {
|
|
3526
4339
|
return this._db.prepare(
|
|
3527
|
-
`SELECT
|
|
4340
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
3528
4341
|
FROM scheduled_messages
|
|
3529
4342
|
WHERE uuid = ? AND status = 'pending'
|
|
3530
4343
|
LIMIT 1`
|
|
@@ -3597,7 +4410,7 @@ var D1Adapter = class {
|
|
|
3597
4410
|
async getPendingTasks(limit = 50) {
|
|
3598
4411
|
const now = this._now();
|
|
3599
4412
|
const res = await this._db.prepare(
|
|
3600
|
-
`SELECT
|
|
4413
|
+
`SELECT ${TASK_DELIVERY_COLUMNS}
|
|
3601
4414
|
FROM scheduled_messages
|
|
3602
4415
|
WHERE status = 'pending' AND next_send_at <= ?
|
|
3603
4416
|
AND (lease_until IS NULL OR lease_until <= ?)
|
|
@@ -3700,7 +4513,7 @@ var D1Adapter = class {
|
|
|
3700
4513
|
).bind(...params).first();
|
|
3701
4514
|
const total = Number(countRow.count) || 0;
|
|
3702
4515
|
const res = await this._db.prepare(
|
|
3703
|
-
`SELECT
|
|
4516
|
+
`SELECT ${TASK_DETAIL_COLUMNS}
|
|
3704
4517
|
FROM scheduled_messages
|
|
3705
4518
|
WHERE ${where}
|
|
3706
4519
|
ORDER BY next_send_at ASC
|
|
@@ -3816,6 +4629,34 @@ var D1Adapter = class {
|
|
|
3816
4629
|
).bind(userId, namespace).all();
|
|
3817
4630
|
return res.results || [];
|
|
3818
4631
|
}
|
|
4632
|
+
/**
|
|
4633
|
+
* 例行清理:把指定命名空间下太久没更新的条目删掉(run-tick 每跳顺手调,
|
|
4634
|
+
* 宿主配了 `clientStateTtl` 才会调)。
|
|
4635
|
+
*
|
|
4636
|
+
* 不限用户——「这个命名空间只留最近 N 天」是命名空间级的约定,单用户部署
|
|
4637
|
+
* 下也就是这一个用户的行。指令由 lib/client-state-store.js 的
|
|
4638
|
+
* `planClientStateCleanup` 算好(含大值切片所在的保留命名空间),这里只负
|
|
4639
|
+
* 责照着删。
|
|
4640
|
+
*
|
|
4641
|
+
* @param {Array<{ namespace: string, updatedBefore: number }>} targets
|
|
4642
|
+
* `updatedBefore` 是 epoch 毫秒,与 `updated_at` 列同一把尺子。
|
|
4643
|
+
* @returns {Promise<number>} 删掉的行数
|
|
4644
|
+
*/
|
|
4645
|
+
async cleanupClientState(targets = []) {
|
|
4646
|
+
if (!Array.isArray(targets) || targets.length === 0) return 0;
|
|
4647
|
+
const SQL = "DELETE FROM client_state WHERE namespace = ? AND updated_at < ?";
|
|
4648
|
+
const statements = targets.map(
|
|
4649
|
+
(target) => this._db.prepare(SQL).bind(target.namespace, target.updatedBefore)
|
|
4650
|
+
);
|
|
4651
|
+
let results;
|
|
4652
|
+
if (typeof this._db.batch === "function") {
|
|
4653
|
+
results = await this._db.batch(statements);
|
|
4654
|
+
} else {
|
|
4655
|
+
results = [];
|
|
4656
|
+
for (const statement of statements) results.push(await statement.run());
|
|
4657
|
+
}
|
|
4658
|
+
return results.reduce((sum, res) => sum + (res.meta.changes || 0), 0);
|
|
4659
|
+
}
|
|
3819
4660
|
/**
|
|
3820
4661
|
* Wipe every entry of this user.
|
|
3821
4662
|
*
|
|
@@ -4017,6 +4858,26 @@ var D1Adapter = class {
|
|
|
4017
4858
|
messageIds
|
|
4018
4859
|
);
|
|
4019
4860
|
}
|
|
4861
|
+
/**
|
|
4862
|
+
* 把这一批还没发出去的行删掉(任务投递到一半被取消 / 顶替时用)。
|
|
4863
|
+
*
|
|
4864
|
+
* 只删 delivered_at 仍为 NULL 的行:已经推给设备的那几条撤不回来,行留着让
|
|
4865
|
+
* 客户端照常 ack。已 ack 的行更不动。
|
|
4866
|
+
*
|
|
4867
|
+
* @param {string} userId
|
|
4868
|
+
* @param {string[]} messageIds
|
|
4869
|
+
* @returns {Promise<number>} 删掉的行数
|
|
4870
|
+
*/
|
|
4871
|
+
async discardOutboxMessages(userId, messageIds) {
|
|
4872
|
+
if (!messageIds || messageIds.length === 0) return 0;
|
|
4873
|
+
return this._runInClauseWrite(
|
|
4874
|
+
(placeholders) => `DELETE FROM message_outbox
|
|
4875
|
+
WHERE user_id = ? AND delivered_at IS NULL AND acked_at IS NULL
|
|
4876
|
+
AND message_id IN (${placeholders})`,
|
|
4877
|
+
[userId],
|
|
4878
|
+
messageIds
|
|
4879
|
+
);
|
|
4880
|
+
}
|
|
4020
4881
|
/**
|
|
4021
4882
|
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
4022
4883
|
*
|
|
@@ -4334,7 +5195,7 @@ function createClientStateHandler(ctx) {
|
|
|
4334
5195
|
}
|
|
4335
5196
|
|
|
4336
5197
|
// src/server/lib/version.js
|
|
4337
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
5198
|
+
var SERVER_VERSION = true ? "2.6.0-next.21" : "0.0.0-dev";
|
|
4338
5199
|
|
|
4339
5200
|
// src/server/handlers/capabilities.js
|
|
4340
5201
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -4414,7 +5275,13 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
4414
5275
|
// 用户级 LLM 凭据存储:PUT/GET/DELETE /llm-credentials,任务 payload 认
|
|
4415
5276
|
// credRefs(fire 时按引用现读、自排任务复制引用),hook ctx 带
|
|
4416
5277
|
// resolveLlmCredential。
|
|
4417
|
-
"llm-credentials"
|
|
5278
|
+
"llm-credentials",
|
|
5279
|
+
// 请求体带 Content-Encoding: gzip 时自动解压,所有带 body 的端点都认。
|
|
5280
|
+
"gzip-request-body",
|
|
5281
|
+
// client_state 按命名空间过期清理(config 的 clientStateTtl,cron 每跳顺手做)。
|
|
5282
|
+
"client-state-ttl",
|
|
5283
|
+
// hook ctx 带 emitResult(payload):往客户端补一条自定义结果(落收件箱 + 推送)。
|
|
5284
|
+
"emit-result"
|
|
4418
5285
|
]);
|
|
4419
5286
|
function createCapabilitiesHandler(ctx) {
|
|
4420
5287
|
async function GET(url, headers) {
|
|
@@ -4558,6 +5425,9 @@ function createSingleUserServer(config) {
|
|
|
4558
5425
|
privateKey: vapid.privateKey || ""
|
|
4559
5426
|
},
|
|
4560
5427
|
webpush: config.webpush || null,
|
|
5428
|
+
// 分片传输的限额(与 installReiSW 的 multipart 同一份)。instant 消息也走
|
|
5429
|
+
// 这个 ctx 发,所以它要跟着 handlers 一起进来。
|
|
5430
|
+
multipart: config.multipart || null,
|
|
4561
5431
|
tenantManager,
|
|
4562
5432
|
// Fire-time hooks (optional): the in-server instant path fires through
|
|
4563
5433
|
// processMessagesByUuid with this ctx, so instant-type tasks can take
|
|
@@ -4674,6 +5544,21 @@ function corsHeadersFor(cors, requestOrigin) {
|
|
|
4674
5544
|
function degradedCorsHeaders(requestOrigin) {
|
|
4675
5545
|
return corsHeadersFor({ origin: requestOrigin, maxAge: 0 }, requestOrigin);
|
|
4676
5546
|
}
|
|
5547
|
+
function isSameOriginCall(request, requestOrigin) {
|
|
5548
|
+
if (!requestOrigin) return true;
|
|
5549
|
+
try {
|
|
5550
|
+
return new URL(request.url).origin === requestOrigin;
|
|
5551
|
+
} catch (_error) {
|
|
5552
|
+
return false;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
function publicErrorCause(cause) {
|
|
5556
|
+
const { message: _serverSideOnly, ...safe } = cause;
|
|
5557
|
+
return (
|
|
5558
|
+
/** @type {import('../lib/errors.js').ErrorCause} */
|
|
5559
|
+
safe
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
4677
5562
|
function internalErrorResponse(cors, cause) {
|
|
4678
5563
|
return jsonResponse(500, {
|
|
4679
5564
|
success: false,
|
|
@@ -4743,7 +5628,14 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4743
5628
|
// 租约心跳间隔(默认 30s;0 = 关掉心跳,退回一次性长租约)。
|
|
4744
5629
|
leaseHeartbeatMs: cfg.leaseHeartbeatMs,
|
|
4745
5630
|
// 补发新鲜度阈值(默认 60 分钟;见 lib/run-tick.js 的 STALE_AFTER_MS)。
|
|
4746
|
-
staleAfterMs: cfg.staleAfterMs
|
|
5631
|
+
staleAfterMs: cfg.staleAfterMs,
|
|
5632
|
+
// client_state 的按命名空间过期清理 `{ 命名空间: 天数 }`。不配 = 一个都
|
|
5633
|
+
// 不清(见 lib/run-tick.js 的 cleanupExpiredClientState)。
|
|
5634
|
+
clientStateTtl: cfg.clientStateTtl,
|
|
5635
|
+
// 分片传输的限额 { maxChunkBytes, maxChunks, maxTotalBytes, ttlMs }:跟宿主
|
|
5636
|
+
// 传给 installReiSW 的那一份保持一致。装不下一条 push 的思考过程按它切片、
|
|
5637
|
+
// 按它排发送节奏(见 lib/message-processor.js)。
|
|
5638
|
+
multipart: cfg.multipart
|
|
4747
5639
|
};
|
|
4748
5640
|
}
|
|
4749
5641
|
function pushConfigured(cfg) {
|
|
@@ -4762,7 +5654,10 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4762
5654
|
await reportError({ stage: "config", error, cause, path: pathOf(request) });
|
|
4763
5655
|
const degraded = degradedCorsHeaders(requestOrigin);
|
|
4764
5656
|
if (method === "OPTIONS" && degraded) return new Response(null, { status: 204, headers: degraded });
|
|
4765
|
-
return internalErrorResponse(
|
|
5657
|
+
return internalErrorResponse(
|
|
5658
|
+
degraded,
|
|
5659
|
+
isSameOriginCall(request, requestOrigin) ? cause : publicErrorCause(cause)
|
|
5660
|
+
);
|
|
4766
5661
|
}
|
|
4767
5662
|
const cors = corsHeadersFor(cfg.cors, requestOrigin);
|
|
4768
5663
|
try {
|
|
@@ -4773,19 +5668,25 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4773
5668
|
const url = request.url;
|
|
4774
5669
|
const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
|
|
4775
5670
|
const headers = headersToObject(request.headers);
|
|
5671
|
+
let body = "";
|
|
5672
|
+
if (method === "POST" || method === "PUT" || method === "DELETE") {
|
|
5673
|
+
const read = await readRequestBody(request, { maxBytes: cfg.maxRequestBodyBytes });
|
|
5674
|
+
if (!read.ok) return jsonResponse(read.error.status, read.error.body, cors);
|
|
5675
|
+
body = read.body;
|
|
5676
|
+
}
|
|
4776
5677
|
let result;
|
|
4777
5678
|
if (method === "POST" && pathname.endsWith("/init-tenant")) {
|
|
4778
|
-
result = await server.handlers.init.POST(headers,
|
|
5679
|
+
result = await server.handlers.init.POST(headers, body);
|
|
4779
5680
|
} else if (method === "GET" && pathname.endsWith("/get-user-key")) {
|
|
4780
5681
|
result = await server.handlers.getUserKey.GET(url, headers);
|
|
4781
5682
|
} else if (method === "POST" && pathname.endsWith("/schedule-message")) {
|
|
4782
|
-
result = await server.handlers.scheduleMessage.POST(headers,
|
|
5683
|
+
result = await server.handlers.scheduleMessage.POST(headers, body);
|
|
4783
5684
|
} else if (method === "GET" && pathname.endsWith("/messages")) {
|
|
4784
5685
|
result = await server.handlers.messages.GET(url, headers);
|
|
4785
5686
|
} else if (method === "GET" && pathname.endsWith("/message")) {
|
|
4786
5687
|
result = await server.handlers.getMessage.GET(url, headers);
|
|
4787
5688
|
} else if (method === "PUT" && pathname.endsWith("/update-message")) {
|
|
4788
|
-
result = await server.handlers.updateMessage.PUT(url, headers,
|
|
5689
|
+
result = await server.handlers.updateMessage.PUT(url, headers, body);
|
|
4789
5690
|
} else if (method === "DELETE" && pathname.endsWith("/cancel-message")) {
|
|
4790
5691
|
result = await server.handlers.cancelMessage.DELETE(url, headers);
|
|
4791
5692
|
} else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
|
|
@@ -4793,7 +5694,7 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4793
5694
|
} else if (method === "GET" && pathname.endsWith("/capabilities")) {
|
|
4794
5695
|
result = await server.handlers.capabilities.GET(url, headers);
|
|
4795
5696
|
} else if (method === "PUT" && pathname.endsWith("/client-state")) {
|
|
4796
|
-
result = await server.handlers.clientState.PUT(headers,
|
|
5697
|
+
result = await server.handlers.clientState.PUT(headers, body);
|
|
4797
5698
|
} else if (method === "GET" && pathname.endsWith("/client-state")) {
|
|
4798
5699
|
result = await server.handlers.clientState.GET(url, headers);
|
|
4799
5700
|
} else if (method === "DELETE" && pathname.endsWith("/client-state")) {
|
|
@@ -4801,19 +5702,19 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4801
5702
|
} else if (method === "GET" && pathname.endsWith("/outbox")) {
|
|
4802
5703
|
result = await server.handlers.outbox.GET(url, headers);
|
|
4803
5704
|
} else if (method === "POST" && pathname.endsWith("/outbox/ack")) {
|
|
4804
|
-
result = await server.handlers.outbox.POST(headers,
|
|
5705
|
+
result = await server.handlers.outbox.POST(headers, body);
|
|
4805
5706
|
} else if (method === "PUT" && pathname.endsWith("/push-subscription")) {
|
|
4806
|
-
result = await server.handlers.pushSubscription.PUT(headers,
|
|
5707
|
+
result = await server.handlers.pushSubscription.PUT(headers, body);
|
|
4807
5708
|
} else if (method === "GET" && pathname.endsWith("/push-subscription")) {
|
|
4808
5709
|
result = await server.handlers.pushSubscription.GET(url, headers);
|
|
4809
5710
|
} else if (method === "DELETE" && pathname.endsWith("/push-subscription")) {
|
|
4810
5711
|
result = await server.handlers.pushSubscription.DELETE(url, headers);
|
|
4811
5712
|
} else if (method === "PUT" && pathname.endsWith("/llm-credentials")) {
|
|
4812
|
-
result = await server.handlers.llmCredentials.PUT(headers,
|
|
5713
|
+
result = await server.handlers.llmCredentials.PUT(headers, body);
|
|
4813
5714
|
} else if (method === "GET" && pathname.endsWith("/llm-credentials")) {
|
|
4814
5715
|
result = await server.handlers.llmCredentials.GET(url, headers);
|
|
4815
5716
|
} else if (method === "DELETE" && pathname.endsWith("/llm-credentials")) {
|
|
4816
|
-
result = await server.handlers.llmCredentials.DELETE(url, headers,
|
|
5717
|
+
result = await server.handlers.llmCredentials.DELETE(url, headers, body);
|
|
4817
5718
|
} else {
|
|
4818
5719
|
result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
|
|
4819
5720
|
}
|
|
@@ -4870,62 +5771,6 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
4870
5771
|
return { fetch, scheduled, runTask: runTask2, getSchemaVersion: getSchemaVersion2, ensureSchema: ensureSchema2 };
|
|
4871
5772
|
}
|
|
4872
5773
|
|
|
4873
|
-
// src/server/lib/webpush-webcrypto.js
|
|
4874
|
-
import { sendWebPush as sharedSendWebPush } from "@rei-standard/amsg-shared";
|
|
4875
|
-
import { buildVapidJwt, verifyVapidJwt, hmacSha256 as hmacSha2562 } from "@rei-standard/amsg-shared";
|
|
4876
|
-
var WEB_PUSH_MAX_BODY_BYTES = 4096;
|
|
4877
|
-
var WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = 16 + 4 + 1 + 65 + 1 + 16;
|
|
4878
|
-
var MAX_PUSH_PAYLOAD_BYTES = WEB_PUSH_MAX_BODY_BYTES - WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES;
|
|
4879
|
-
var PUSH_ENVELOPE_RESERVED_BYTES = 384;
|
|
4880
|
-
var payloadEncoder = new TextEncoder();
|
|
4881
|
-
function measurePushPayload(payload, options) {
|
|
4882
|
-
const reserveEnvelope = !!(options && options.reserveEnvelope);
|
|
4883
|
-
const bytes = payloadEncoder.encode(typeof payload === "string" ? payload : String(payload)).length;
|
|
4884
|
-
const envelopeReservedBytes = reserveEnvelope ? PUSH_ENVELOPE_RESERVED_BYTES : 0;
|
|
4885
|
-
const maxBytes = MAX_PUSH_PAYLOAD_BYTES - envelopeReservedBytes;
|
|
4886
|
-
return {
|
|
4887
|
-
bytes,
|
|
4888
|
-
maxBytes,
|
|
4889
|
-
remainingBytes: maxBytes - bytes,
|
|
4890
|
-
withinLimit: bytes <= maxBytes,
|
|
4891
|
-
envelopeReservedBytes
|
|
4892
|
-
};
|
|
4893
|
-
}
|
|
4894
|
-
async function sendWebPush(args) {
|
|
4895
|
-
const { payload } = args || {};
|
|
4896
|
-
if (typeof payload === "string") {
|
|
4897
|
-
const size = measurePushPayload(payload);
|
|
4898
|
-
if (!size.withinLimit) {
|
|
4899
|
-
const err5 = new Error(
|
|
4900
|
-
`sendWebPush: payload is ${size.bytes} bytes, over the ${MAX_PUSH_PAYLOAD_BYTES}-byte limit (push services cap the encrypted body at ${WEB_PUSH_MAX_BODY_BYTES} bytes; aes128gcm adds ${WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES} bytes)`
|
|
4901
|
-
);
|
|
4902
|
-
err5.code = "PUSH_PAYLOAD_TOO_LARGE";
|
|
4903
|
-
err5.bytes = size.bytes;
|
|
4904
|
-
err5.maxBytes = MAX_PUSH_PAYLOAD_BYTES;
|
|
4905
|
-
throw err5;
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
return sharedSendWebPush(args);
|
|
4909
|
-
}
|
|
4910
|
-
var SCHEDULED_DEFAULT_TTL = 2419200;
|
|
4911
|
-
function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}) {
|
|
4912
|
-
return {
|
|
4913
|
-
async sendNotification(subscription, payload) {
|
|
4914
|
-
return sendWebPush({
|
|
4915
|
-
subscription,
|
|
4916
|
-
payload,
|
|
4917
|
-
ttl,
|
|
4918
|
-
vapid: {
|
|
4919
|
-
email: vapid.email,
|
|
4920
|
-
publicKey: vapid.publicKey,
|
|
4921
|
-
privateKey: vapid.privateKey
|
|
4922
|
-
},
|
|
4923
|
-
fetch: globalThis.fetch
|
|
4924
|
-
});
|
|
4925
|
-
}
|
|
4926
|
-
};
|
|
4927
|
-
}
|
|
4928
|
-
|
|
4929
5774
|
export {
|
|
4930
5775
|
isValidTimeZoneId,
|
|
4931
5776
|
advanceOccurrence,
|
|
@@ -4957,18 +5802,28 @@ export {
|
|
|
4957
5802
|
isValidUUIDv4,
|
|
4958
5803
|
validateSplitPattern,
|
|
4959
5804
|
validateLlmMessagesArray,
|
|
5805
|
+
MAX_TASK_PAYLOAD_BYTES,
|
|
4960
5806
|
validateScheduleMessagePayload,
|
|
5807
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
5808
|
+
readRequestBody,
|
|
4961
5809
|
parseJsonBody,
|
|
4962
5810
|
getHeader,
|
|
4963
5811
|
createGetUserKeyHandler,
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
5812
|
+
WEB_PUSH_MAX_BODY_BYTES,
|
|
5813
|
+
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
5814
|
+
MAX_PUSH_PAYLOAD_BYTES,
|
|
5815
|
+
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
5816
|
+
measurePushPayload,
|
|
5817
|
+
createWebCryptoWebPush,
|
|
4968
5818
|
NonRetryableError,
|
|
4969
5819
|
isNonRetryableError,
|
|
5820
|
+
DeploymentConfigError,
|
|
4970
5821
|
sanitizeErrorSummary,
|
|
4971
5822
|
summarizeErrorCause,
|
|
5823
|
+
DEFAULT_MAX_TOOL_ITERATIONS,
|
|
5824
|
+
DEFAULT_TOTAL_TIMEOUT_MS,
|
|
5825
|
+
DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE,
|
|
5826
|
+
MIN_SCHEDULE_LEAD_MS,
|
|
4972
5827
|
createScheduleMessageHandler,
|
|
4973
5828
|
DEFAULT_CLAIM_LEASE_MS,
|
|
4974
5829
|
DEFAULT_LEASE_HEARTBEAT_MS,
|
|
@@ -4988,11 +5843,5 @@ export {
|
|
|
4988
5843
|
ensureSchema,
|
|
4989
5844
|
CORS_ALLOW_HEADERS,
|
|
4990
5845
|
CORS_ALLOW_METHODS,
|
|
4991
|
-
createSingleUserCloudflareWorker
|
|
4992
|
-
WEB_PUSH_MAX_BODY_BYTES,
|
|
4993
|
-
WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES,
|
|
4994
|
-
MAX_PUSH_PAYLOAD_BYTES,
|
|
4995
|
-
PUSH_ENVELOPE_RESERVED_BYTES,
|
|
4996
|
-
measurePushPayload,
|
|
4997
|
-
createWebCryptoWebPush
|
|
5846
|
+
createSingleUserCloudflareWorker
|
|
4998
5847
|
};
|