@rei-standard/amsg-server 2.5.2 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.cjs +16 -65
- package/dist/index.d.cts +17 -96
- package/dist/index.d.ts +17 -96
- package/dist/index.mjs +8 -57
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -62,9 +62,17 @@ const rei = await createReiServer({
|
|
|
62
62
|
|
|
63
63
|
当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
|
|
64
64
|
|
|
65
|
-
- `apiUrl`
|
|
66
|
-
- SDK
|
|
67
|
-
|
|
65
|
+
- `apiUrl` 是聊天端点 URL(例如:`https://api.openai.com/v1/chat/completions`),必须能 `new URL(...)` 解析。
|
|
66
|
+
- SDK 对 OpenAI 风格路径做**幂等**补全(去首尾空白、去尾部多余 `/` 后):
|
|
67
|
+
|
|
68
|
+
| 输入 | 输出 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `https://api.openai.com`(裸域名) | `https://api.openai.com/v1/chat/completions` |
|
|
71
|
+
| `https://api.openai.com/v1`(版本段结尾) | `https://api.openai.com/v1/chat/completions`(不重复加 `/v1`) |
|
|
72
|
+
| `https://api.openai.com/v1/chat/completions` | 原样返回 |
|
|
73
|
+
| `https://api.anthropic.com/v1/messages`(其他自定义路径) | 原样返回,不猜 |
|
|
74
|
+
|
|
75
|
+
- 规则幂等,传完整 URL 不会被改坏;代理路径很特殊时直接传完整 `…/chat/completions` 绕开补全。
|
|
68
76
|
- `maxTokens` 为可选字段:传了就映射为 `max_tokens`;不传则不指定(由上游模型默认策略决定)。
|
|
69
77
|
|
|
70
78
|
如果上游返回 `405 Method Not Allowed`,通常表示 `apiUrl` 指向了基础域名而非聊天端点,请优先检查配置值。
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ async function createAdapter(config) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// src/server/lib/validation.js
|
|
30
|
+
var _amsgshared = require('@rei-standard/amsg-shared');
|
|
30
31
|
function isValidISO8601(dateString) {
|
|
31
32
|
const date = new Date(dateString);
|
|
32
33
|
return date instanceof Date && !isNaN(date.getTime());
|
|
@@ -48,23 +49,6 @@ function isValidUUIDv4(uuid) {
|
|
|
48
49
|
return uuidV4Regex.test(uuid);
|
|
49
50
|
}
|
|
50
51
|
var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
51
|
-
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
52
|
-
function validateAvatarUrl(value) {
|
|
53
|
-
if (value === void 0 || value === null) return null;
|
|
54
|
-
if (typeof value !== "string") {
|
|
55
|
-
return "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
56
|
-
}
|
|
57
|
-
if (/^data:/i.test(value)) {
|
|
58
|
-
return "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
59
|
-
}
|
|
60
|
-
if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
61
|
-
return `\u5934\u50CF URL \u957F\u5EA6 ${value.length} \u5B57\u7B26\u8D85\u8FC7 ${AVATAR_URL_MAX_LENGTH} \u4E0A\u9650\uFF0C\u8BF7\u6539\u4E3A\u66F4\u77ED\u7684\u56FE\u7247 URL`;
|
|
62
|
-
}
|
|
63
|
-
if (!isValidUrl(value)) {
|
|
64
|
-
return "avatarUrl \u4E0D\u662F\u5408\u6CD5 URL";
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
52
|
var SPLIT_PATTERN_MAX_LENGTH = 200;
|
|
69
53
|
var SPLIT_PATTERN_MAX_ITEMS = 10;
|
|
70
54
|
function validateSplitPattern(value) {
|
|
@@ -188,7 +172,7 @@ function validateScheduleMessagePayload(payload) {
|
|
|
188
172
|
if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
|
|
189
173
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature (must be a finite number)"] } };
|
|
190
174
|
}
|
|
191
|
-
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
175
|
+
const avatarErr = _amsgshared.validateAvatarUrl.call(void 0, payload.avatarUrl);
|
|
192
176
|
if (avatarErr) {
|
|
193
177
|
console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
|
|
194
178
|
payload.avatarUrl = null;
|
|
@@ -486,8 +470,11 @@ function isUniqueViolation(error) {
|
|
|
486
470
|
|
|
487
471
|
|
|
488
472
|
|
|
489
|
-
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
490
476
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
477
|
+
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
491
478
|
function splitOnceByRegex(chunk, regex) {
|
|
492
479
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
493
480
|
if (i % 2 === 0 && part.trim()) {
|
|
@@ -507,38 +494,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
507
494
|
}
|
|
508
495
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
509
496
|
}
|
|
510
|
-
function readReasoningContent(llmResponse) {
|
|
511
|
-
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
512
|
-
const choices = (
|
|
513
|
-
/** @type {{ choices?: unknown }} */
|
|
514
|
-
llmResponse.choices
|
|
515
|
-
);
|
|
516
|
-
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
517
|
-
const message = (
|
|
518
|
-
/** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */
|
|
519
|
-
_optionalChain([choices, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.message])
|
|
520
|
-
);
|
|
521
|
-
const raw = _optionalChain([message, 'optionalAccess', _4 => _4.reasoning_content]);
|
|
522
|
-
if (typeof raw === "string") {
|
|
523
|
-
const trimmed = raw.trim();
|
|
524
|
-
if (trimmed.length > 0) return trimmed;
|
|
525
|
-
}
|
|
526
|
-
const content = _optionalChain([message, 'optionalAccess', _5 => _5.content]);
|
|
527
|
-
if (typeof content === "string") {
|
|
528
|
-
const match = content.match(REASONING_TAG_RE);
|
|
529
|
-
if (match) {
|
|
530
|
-
const trimmed = match[2].trim();
|
|
531
|
-
if (trimmed.length > 0) return trimmed;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
var REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
|
|
537
|
-
var REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
|
|
538
|
-
function stripReasoningTags(content) {
|
|
539
|
-
if (typeof content !== "string" || !content.includes("<")) return content;
|
|
540
|
-
return content.replace(REASONING_TAG_RE_G, "").trim();
|
|
541
|
-
}
|
|
542
497
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
543
498
|
try {
|
|
544
499
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -569,9 +524,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
569
524
|
} else {
|
|
570
525
|
throw new Error("Invalid message configuration: no content source available");
|
|
571
526
|
}
|
|
572
|
-
const reasoning = readReasoningContent(llmResponse);
|
|
527
|
+
const reasoning = _amsgshared.readReasoningContent.call(void 0, llmResponse);
|
|
573
528
|
if (reasoning) {
|
|
574
|
-
messageContent = stripReasoningTags(messageContent);
|
|
529
|
+
messageContent = _amsgshared.stripReasoningTags.call(void 0, messageContent);
|
|
575
530
|
}
|
|
576
531
|
const messages = splitMessageIntoSentences(messageContent, _nullishCoalesce(decryptedPayload.splitPattern, () => ( null)));
|
|
577
532
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
@@ -599,7 +554,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
599
554
|
metadata
|
|
600
555
|
});
|
|
601
556
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
602
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
557
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
603
558
|
}
|
|
604
559
|
for (let i = 0; i < messages.length; i++) {
|
|
605
560
|
const contentPush = _amsgshared.buildContentPush.call(void 0, {
|
|
@@ -620,7 +575,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
620
575
|
});
|
|
621
576
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
622
577
|
if (i < messages.length - 1) {
|
|
623
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
624
579
|
}
|
|
625
580
|
}
|
|
626
581
|
return { success: true, messagesSent: messages.length };
|
|
@@ -713,7 +668,7 @@ async function _callAI(payload) {
|
|
|
713
668
|
);
|
|
714
669
|
}
|
|
715
670
|
const aiData = await aiResponse.json();
|
|
716
|
-
const content = _optionalChain([aiData, 'optionalAccess',
|
|
671
|
+
const content = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.content]);
|
|
717
672
|
if (typeof content !== "string" || !content.trim()) {
|
|
718
673
|
throw new Error("AI API error: response missing choices[0].message.content");
|
|
719
674
|
}
|
|
@@ -1170,7 +1125,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1170
1125
|
}
|
|
1171
1126
|
}
|
|
1172
1127
|
if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
|
|
1173
|
-
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1128
|
+
const avatarErr = _amsgshared.validateAvatarUrl.call(void 0, updates.avatarUrl);
|
|
1174
1129
|
if (avatarErr) {
|
|
1175
1130
|
console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
|
|
1176
1131
|
delete updates.avatarUrl;
|
|
@@ -1397,7 +1352,7 @@ async function resolveBlobStore(namespace) {
|
|
|
1397
1352
|
}
|
|
1398
1353
|
try {
|
|
1399
1354
|
const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
|
|
1400
|
-
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access',
|
|
1355
|
+
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _6 => _6.default, 'optionalAccess', _7 => _7.getStore]);
|
|
1401
1356
|
if (typeof getStore === "function") {
|
|
1402
1357
|
return getStore({ name: namespace });
|
|
1403
1358
|
}
|
|
@@ -1654,11 +1609,7 @@ function createTenantContextManager(options) {
|
|
|
1654
1609
|
}
|
|
1655
1610
|
|
|
1656
1611
|
// src/server/index.js
|
|
1657
|
-
|
|
1658
|
-
const trimmedEmail = String(email || "").trim();
|
|
1659
|
-
if (!trimmedEmail) return "";
|
|
1660
|
-
return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
|
|
1661
|
-
}
|
|
1612
|
+
|
|
1662
1613
|
async function createReiServer(config) {
|
|
1663
1614
|
if (!config) throw new Error("[rei-standard-amsg-server] config is required");
|
|
1664
1615
|
if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
|
|
@@ -1674,7 +1625,7 @@ async function createReiServer(config) {
|
|
|
1674
1625
|
const vapid = config.vapid || {};
|
|
1675
1626
|
if (vapid.email && vapid.publicKey && vapid.privateKey) {
|
|
1676
1627
|
webpushModule.setVapidDetails(
|
|
1677
|
-
normalizeVapidSubject(vapid.email),
|
|
1628
|
+
_amsgshared.normalizeVapidSubject.call(void 0, vapid.email),
|
|
1678
1629
|
vapid.publicKey,
|
|
1679
1630
|
vapid.privateKey
|
|
1680
1631
|
);
|
|
@@ -1731,4 +1682,4 @@ async function createReiServer(config) {
|
|
|
1731
1682
|
|
|
1732
1683
|
|
|
1733
1684
|
|
|
1734
|
-
exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.createTenantToken = createTenantToken; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.isValidUrl = isValidUrl; exports.validateAvatarUrl = validateAvatarUrl; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.validateSplitPattern = validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
|
1685
|
+
exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.createTenantToken = createTenantToken; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.isValidUrl = isValidUrl; exports.validateAvatarUrl = _amsgshared.validateAvatarUrl; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.validateSplitPattern = validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { validateAvatarUrl, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, normalizeVapidSubject } from '@rei-standard/amsg-shared';
|
|
2
|
+
export { validateAvatarUrl } from '@rei-standard/amsg-shared';
|
|
1
3
|
import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
-
import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Adapter Factory
|
|
@@ -58,6 +59,7 @@ async function createAdapter(config) {
|
|
|
58
59
|
* ReiStandard SDK v2.0.1
|
|
59
60
|
*/
|
|
60
61
|
|
|
62
|
+
|
|
61
63
|
/**
|
|
62
64
|
* Validate ISO 8601 date string.
|
|
63
65
|
* @param {string} dateString
|
|
@@ -104,43 +106,12 @@ function isValidUUIDv4(uuid) {
|
|
|
104
106
|
|
|
105
107
|
const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
106
108
|
|
|
107
|
-
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Validate the optional `avatarUrl` field. Rejects `data:` URIs (typically
|
|
111
|
-
* base64-encoded inline images) and anything longer than 2048 chars, both
|
|
112
|
-
* of which are the dominant trigger for downstream 413 / Web Push 4 KB
|
|
113
|
-
* payload errors. Returns an error message string, or null when valid.
|
|
114
|
-
*
|
|
115
|
-
* Mirrors @rei-standard/amsg-instant's `validateAvatarUrl` (kept in lockstep
|
|
116
|
-
* on purpose — both packages forward `avatarUrl` to the same SW push payload).
|
|
117
|
-
*
|
|
118
|
-
* @param {unknown} value
|
|
119
|
-
* @returns {string | null}
|
|
120
|
-
*/
|
|
121
|
-
function validateAvatarUrl(value) {
|
|
122
|
-
if (value === undefined || value === null) return null;
|
|
123
|
-
if (typeof value !== 'string') {
|
|
124
|
-
return 'avatarUrl 必须是字符串';
|
|
125
|
-
}
|
|
126
|
-
if (/^data:/i.test(value)) {
|
|
127
|
-
return '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
128
|
-
}
|
|
129
|
-
if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
130
|
-
return `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
131
|
-
}
|
|
132
|
-
if (!isValidUrl(value)) {
|
|
133
|
-
return 'avatarUrl 不是合法 URL';
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
109
|
const SPLIT_PATTERN_MAX_LENGTH = 200;
|
|
139
110
|
const SPLIT_PATTERN_MAX_ITEMS = 10;
|
|
140
111
|
|
|
141
112
|
/**
|
|
142
|
-
* Validate the optional `splitPattern` field
|
|
143
|
-
*
|
|
113
|
+
* Validate the optional `splitPattern` field (amsg-server scheduled tasks
|
|
114
|
+
* only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
|
|
144
115
|
* Accepts `string`, `string[]`, or absent/null. Returns an error message
|
|
145
116
|
* string, or null when valid.
|
|
146
117
|
*
|
|
@@ -820,6 +791,11 @@ function isUniqueViolation(error) {
|
|
|
820
791
|
|
|
821
792
|
const DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
822
793
|
|
|
794
|
+
// Pacing between consecutive Web Push deliveries (reasoning → content, and
|
|
795
|
+
// between content sentences) so the client renders a natural typing cadence.
|
|
796
|
+
// Kept equal to amsg-instant's SLEEP_BETWEEN_MESSAGES_MS default.
|
|
797
|
+
const SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
798
|
+
|
|
823
799
|
/**
|
|
824
800
|
* Split a single chunk by one regex; on no-match return [chunk] so a later
|
|
825
801
|
* regex in a cascade can still take a swing at it.
|
|
@@ -839,11 +815,10 @@ function splitOnceByRegex(chunk, regex) {
|
|
|
839
815
|
}
|
|
840
816
|
|
|
841
817
|
/**
|
|
842
|
-
* Sentence splitter
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
* both copies; if you fix a bug here, fix it in the instant copy too.
|
|
818
|
+
* Sentence splitter for amsg-server's scheduled `splitPattern` feature
|
|
819
|
+
* (see standards §6.1). Server-only: amsg-instant 0.8.0 dropped its
|
|
820
|
+
* request-level `splitPattern`, so there is no instant counterpart to keep
|
|
821
|
+
* in lockstep.
|
|
847
822
|
*
|
|
848
823
|
* @param {string} messageContent
|
|
849
824
|
* @param {string | string[] | null} [splitPattern=null]
|
|
@@ -867,54 +842,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
867
842
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
868
843
|
}
|
|
869
844
|
|
|
870
|
-
/**
|
|
871
|
-
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
872
|
-
* string, or null when absent / empty. Mirrors
|
|
873
|
-
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
874
|
-
*
|
|
875
|
-
* @param {unknown} llmResponse
|
|
876
|
-
* @returns {string | null}
|
|
877
|
-
*/
|
|
878
|
-
function readReasoningContent(llmResponse) {
|
|
879
|
-
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
880
|
-
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
881
|
-
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
882
|
-
const message = /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */ (choices[0])?.message;
|
|
883
|
-
|
|
884
|
-
const raw = message?.reasoning_content;
|
|
885
|
-
if (typeof raw === 'string') {
|
|
886
|
-
const trimmed = raw.trim();
|
|
887
|
-
if (trimmed.length > 0) return trimmed;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const content = message?.content;
|
|
891
|
-
if (typeof content === 'string') {
|
|
892
|
-
const match = content.match(REASONING_TAG_RE);
|
|
893
|
-
if (match) {
|
|
894
|
-
const trimmed = match[2].trim();
|
|
895
|
-
if (trimmed.length > 0) return trimmed;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
|
|
903
|
-
const REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Mirrors `amsg-instant/src/message-processor.js#stripReasoningTags`.
|
|
907
|
-
* Removes any private chain-of-thought markup leaking through
|
|
908
|
-
* `message.content` so it does not also ship inside ContentPush.
|
|
909
|
-
*
|
|
910
|
-
* @param {string} content
|
|
911
|
-
* @returns {string}
|
|
912
|
-
*/
|
|
913
|
-
function stripReasoningTags(content) {
|
|
914
|
-
if (typeof content !== 'string' || !content.includes('<')) return content;
|
|
915
|
-
return content.replace(REASONING_TAG_RE_G, '').trim();
|
|
916
|
-
}
|
|
917
|
-
|
|
918
845
|
/**
|
|
919
846
|
* @typedef {Object} ProcessorContext
|
|
920
847
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -1028,7 +955,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
1028
955
|
metadata,
|
|
1029
956
|
});
|
|
1030
957
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
1031
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
958
|
+
await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
1032
959
|
}
|
|
1033
960
|
|
|
1034
961
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -1052,7 +979,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
1052
979
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
1053
980
|
|
|
1054
981
|
if (i < messages.length - 1) {
|
|
1055
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
982
|
+
await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
1056
983
|
}
|
|
1057
984
|
}
|
|
1058
985
|
|
|
@@ -2498,12 +2425,6 @@ function createTenantContextManager(options) {
|
|
|
2498
2425
|
*/
|
|
2499
2426
|
|
|
2500
2427
|
|
|
2501
|
-
function normalizeVapidSubject(email) {
|
|
2502
|
-
const trimmedEmail = String(email || '').trim();
|
|
2503
|
-
if (!trimmedEmail) return '';
|
|
2504
|
-
return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
2428
|
/**
|
|
2508
2429
|
* @typedef {Object} VapidConfig
|
|
2509
2430
|
* @property {string} [email] - VAPID contact email (e.g. mailto:…).
|
|
@@ -2615,4 +2536,4 @@ async function createReiServer(config) {
|
|
|
2615
2536
|
};
|
|
2616
2537
|
}
|
|
2617
2538
|
|
|
2618
|
-
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl,
|
|
2539
|
+
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { validateAvatarUrl, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, normalizeVapidSubject } from '@rei-standard/amsg-shared';
|
|
2
|
+
export { validateAvatarUrl } from '@rei-standard/amsg-shared';
|
|
1
3
|
import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
-
import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Adapter Factory
|
|
@@ -58,6 +59,7 @@ async function createAdapter(config) {
|
|
|
58
59
|
* ReiStandard SDK v2.0.1
|
|
59
60
|
*/
|
|
60
61
|
|
|
62
|
+
|
|
61
63
|
/**
|
|
62
64
|
* Validate ISO 8601 date string.
|
|
63
65
|
* @param {string} dateString
|
|
@@ -104,43 +106,12 @@ function isValidUUIDv4(uuid) {
|
|
|
104
106
|
|
|
105
107
|
const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
|
|
106
108
|
|
|
107
|
-
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Validate the optional `avatarUrl` field. Rejects `data:` URIs (typically
|
|
111
|
-
* base64-encoded inline images) and anything longer than 2048 chars, both
|
|
112
|
-
* of which are the dominant trigger for downstream 413 / Web Push 4 KB
|
|
113
|
-
* payload errors. Returns an error message string, or null when valid.
|
|
114
|
-
*
|
|
115
|
-
* Mirrors @rei-standard/amsg-instant's `validateAvatarUrl` (kept in lockstep
|
|
116
|
-
* on purpose — both packages forward `avatarUrl` to the same SW push payload).
|
|
117
|
-
*
|
|
118
|
-
* @param {unknown} value
|
|
119
|
-
* @returns {string | null}
|
|
120
|
-
*/
|
|
121
|
-
function validateAvatarUrl(value) {
|
|
122
|
-
if (value === undefined || value === null) return null;
|
|
123
|
-
if (typeof value !== 'string') {
|
|
124
|
-
return 'avatarUrl 必须是字符串';
|
|
125
|
-
}
|
|
126
|
-
if (/^data:/i.test(value)) {
|
|
127
|
-
return '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
128
|
-
}
|
|
129
|
-
if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
130
|
-
return `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
131
|
-
}
|
|
132
|
-
if (!isValidUrl(value)) {
|
|
133
|
-
return 'avatarUrl 不是合法 URL';
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
109
|
const SPLIT_PATTERN_MAX_LENGTH = 200;
|
|
139
110
|
const SPLIT_PATTERN_MAX_ITEMS = 10;
|
|
140
111
|
|
|
141
112
|
/**
|
|
142
|
-
* Validate the optional `splitPattern` field
|
|
143
|
-
*
|
|
113
|
+
* Validate the optional `splitPattern` field (amsg-server scheduled tasks
|
|
114
|
+
* only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
|
|
144
115
|
* Accepts `string`, `string[]`, or absent/null. Returns an error message
|
|
145
116
|
* string, or null when valid.
|
|
146
117
|
*
|
|
@@ -820,6 +791,11 @@ function isUniqueViolation(error) {
|
|
|
820
791
|
|
|
821
792
|
const DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
822
793
|
|
|
794
|
+
// Pacing between consecutive Web Push deliveries (reasoning → content, and
|
|
795
|
+
// between content sentences) so the client renders a natural typing cadence.
|
|
796
|
+
// Kept equal to amsg-instant's SLEEP_BETWEEN_MESSAGES_MS default.
|
|
797
|
+
const SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
798
|
+
|
|
823
799
|
/**
|
|
824
800
|
* Split a single chunk by one regex; on no-match return [chunk] so a later
|
|
825
801
|
* regex in a cascade can still take a swing at it.
|
|
@@ -839,11 +815,10 @@ function splitOnceByRegex(chunk, regex) {
|
|
|
839
815
|
}
|
|
840
816
|
|
|
841
817
|
/**
|
|
842
|
-
* Sentence splitter
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
* both copies; if you fix a bug here, fix it in the instant copy too.
|
|
818
|
+
* Sentence splitter for amsg-server's scheduled `splitPattern` feature
|
|
819
|
+
* (see standards §6.1). Server-only: amsg-instant 0.8.0 dropped its
|
|
820
|
+
* request-level `splitPattern`, so there is no instant counterpart to keep
|
|
821
|
+
* in lockstep.
|
|
847
822
|
*
|
|
848
823
|
* @param {string} messageContent
|
|
849
824
|
* @param {string | string[] | null} [splitPattern=null]
|
|
@@ -867,54 +842,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
867
842
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
868
843
|
}
|
|
869
844
|
|
|
870
|
-
/**
|
|
871
|
-
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
872
|
-
* string, or null when absent / empty. Mirrors
|
|
873
|
-
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
874
|
-
*
|
|
875
|
-
* @param {unknown} llmResponse
|
|
876
|
-
* @returns {string | null}
|
|
877
|
-
*/
|
|
878
|
-
function readReasoningContent(llmResponse) {
|
|
879
|
-
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
880
|
-
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
881
|
-
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
882
|
-
const message = /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */ (choices[0])?.message;
|
|
883
|
-
|
|
884
|
-
const raw = message?.reasoning_content;
|
|
885
|
-
if (typeof raw === 'string') {
|
|
886
|
-
const trimmed = raw.trim();
|
|
887
|
-
if (trimmed.length > 0) return trimmed;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const content = message?.content;
|
|
891
|
-
if (typeof content === 'string') {
|
|
892
|
-
const match = content.match(REASONING_TAG_RE);
|
|
893
|
-
if (match) {
|
|
894
|
-
const trimmed = match[2].trim();
|
|
895
|
-
if (trimmed.length > 0) return trimmed;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
|
|
903
|
-
const REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Mirrors `amsg-instant/src/message-processor.js#stripReasoningTags`.
|
|
907
|
-
* Removes any private chain-of-thought markup leaking through
|
|
908
|
-
* `message.content` so it does not also ship inside ContentPush.
|
|
909
|
-
*
|
|
910
|
-
* @param {string} content
|
|
911
|
-
* @returns {string}
|
|
912
|
-
*/
|
|
913
|
-
function stripReasoningTags(content) {
|
|
914
|
-
if (typeof content !== 'string' || !content.includes('<')) return content;
|
|
915
|
-
return content.replace(REASONING_TAG_RE_G, '').trim();
|
|
916
|
-
}
|
|
917
|
-
|
|
918
845
|
/**
|
|
919
846
|
* @typedef {Object} ProcessorContext
|
|
920
847
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -1028,7 +955,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
1028
955
|
metadata,
|
|
1029
956
|
});
|
|
1030
957
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
1031
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
958
|
+
await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
1032
959
|
}
|
|
1033
960
|
|
|
1034
961
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -1052,7 +979,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
1052
979
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
1053
980
|
|
|
1054
981
|
if (i < messages.length - 1) {
|
|
1055
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
982
|
+
await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
1056
983
|
}
|
|
1057
984
|
}
|
|
1058
985
|
|
|
@@ -2498,12 +2425,6 @@ function createTenantContextManager(options) {
|
|
|
2498
2425
|
*/
|
|
2499
2426
|
|
|
2500
2427
|
|
|
2501
|
-
function normalizeVapidSubject(email) {
|
|
2502
|
-
const trimmedEmail = String(email || '').trim();
|
|
2503
|
-
if (!trimmedEmail) return '';
|
|
2504
|
-
return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
2428
|
/**
|
|
2508
2429
|
* @typedef {Object} VapidConfig
|
|
2509
2430
|
* @property {string} [email] - VAPID contact email (e.g. mailto:…).
|
|
@@ -2615,4 +2536,4 @@ async function createReiServer(config) {
|
|
|
2615
2536
|
};
|
|
2616
2537
|
}
|
|
2617
2538
|
|
|
2618
|
-
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl,
|
|
2539
|
+
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
|
package/dist/index.mjs
CHANGED
|
@@ -27,6 +27,7 @@ async function createAdapter(config) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// src/server/lib/validation.js
|
|
30
|
+
import { validateAvatarUrl } from "@rei-standard/amsg-shared";
|
|
30
31
|
function isValidISO8601(dateString) {
|
|
31
32
|
const date = new Date(dateString);
|
|
32
33
|
return date instanceof Date && !isNaN(date.getTime());
|
|
@@ -48,23 +49,6 @@ function isValidUUIDv4(uuid) {
|
|
|
48
49
|
return uuidV4Regex.test(uuid);
|
|
49
50
|
}
|
|
50
51
|
var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
|
|
51
|
-
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
52
|
-
function validateAvatarUrl(value) {
|
|
53
|
-
if (value === void 0 || value === null) return null;
|
|
54
|
-
if (typeof value !== "string") {
|
|
55
|
-
return "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
56
|
-
}
|
|
57
|
-
if (/^data:/i.test(value)) {
|
|
58
|
-
return "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
59
|
-
}
|
|
60
|
-
if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
61
|
-
return `\u5934\u50CF URL \u957F\u5EA6 ${value.length} \u5B57\u7B26\u8D85\u8FC7 ${AVATAR_URL_MAX_LENGTH} \u4E0A\u9650\uFF0C\u8BF7\u6539\u4E3A\u66F4\u77ED\u7684\u56FE\u7247 URL`;
|
|
62
|
-
}
|
|
63
|
-
if (!isValidUrl(value)) {
|
|
64
|
-
return "avatarUrl \u4E0D\u662F\u5408\u6CD5 URL";
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
52
|
var SPLIT_PATTERN_MAX_LENGTH = 200;
|
|
69
53
|
var SPLIT_PATTERN_MAX_ITEMS = 10;
|
|
70
54
|
function validateSplitPattern(value) {
|
|
@@ -485,9 +469,12 @@ function isUniqueViolation(error) {
|
|
|
485
469
|
import { randomUUID } from "crypto";
|
|
486
470
|
import {
|
|
487
471
|
buildContentPush,
|
|
488
|
-
buildReasoningPush
|
|
472
|
+
buildReasoningPush,
|
|
473
|
+
readReasoningContent,
|
|
474
|
+
stripReasoningTags
|
|
489
475
|
} from "@rei-standard/amsg-shared";
|
|
490
476
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
477
|
+
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
491
478
|
function splitOnceByRegex(chunk, regex) {
|
|
492
479
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
493
480
|
if (i % 2 === 0 && part.trim()) {
|
|
@@ -507,38 +494,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
507
494
|
}
|
|
508
495
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
509
496
|
}
|
|
510
|
-
function readReasoningContent(llmResponse) {
|
|
511
|
-
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
512
|
-
const choices = (
|
|
513
|
-
/** @type {{ choices?: unknown }} */
|
|
514
|
-
llmResponse.choices
|
|
515
|
-
);
|
|
516
|
-
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
517
|
-
const message = (
|
|
518
|
-
/** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */
|
|
519
|
-
choices[0]?.message
|
|
520
|
-
);
|
|
521
|
-
const raw = message?.reasoning_content;
|
|
522
|
-
if (typeof raw === "string") {
|
|
523
|
-
const trimmed = raw.trim();
|
|
524
|
-
if (trimmed.length > 0) return trimmed;
|
|
525
|
-
}
|
|
526
|
-
const content = message?.content;
|
|
527
|
-
if (typeof content === "string") {
|
|
528
|
-
const match = content.match(REASONING_TAG_RE);
|
|
529
|
-
if (match) {
|
|
530
|
-
const trimmed = match[2].trim();
|
|
531
|
-
if (trimmed.length > 0) return trimmed;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
var REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
|
|
537
|
-
var REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
|
|
538
|
-
function stripReasoningTags(content) {
|
|
539
|
-
if (typeof content !== "string" || !content.includes("<")) return content;
|
|
540
|
-
return content.replace(REASONING_TAG_RE_G, "").trim();
|
|
541
|
-
}
|
|
542
497
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
543
498
|
try {
|
|
544
499
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -599,7 +554,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
599
554
|
metadata
|
|
600
555
|
});
|
|
601
556
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
602
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
557
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
603
558
|
}
|
|
604
559
|
for (let i = 0; i < messages.length; i++) {
|
|
605
560
|
const contentPush = buildContentPush({
|
|
@@ -620,7 +575,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
620
575
|
});
|
|
621
576
|
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
622
577
|
if (i < messages.length - 1) {
|
|
623
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
|
|
624
579
|
}
|
|
625
580
|
}
|
|
626
581
|
return { success: true, messagesSent: messages.length };
|
|
@@ -1654,11 +1609,7 @@ function createTenantContextManager(options) {
|
|
|
1654
1609
|
}
|
|
1655
1610
|
|
|
1656
1611
|
// src/server/index.js
|
|
1657
|
-
|
|
1658
|
-
const trimmedEmail = String(email || "").trim();
|
|
1659
|
-
if (!trimmedEmail) return "";
|
|
1660
|
-
return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
|
|
1661
|
-
}
|
|
1612
|
+
import { normalizeVapidSubject } from "@rei-standard/amsg-shared";
|
|
1662
1613
|
async function createReiServer(config) {
|
|
1663
1614
|
if (!config) throw new Error("[rei-standard-amsg-server] config is required");
|
|
1664
1615
|
if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=20"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@rei-standard/amsg-shared": "0.
|
|
36
|
+
"@rei-standard/amsg-shared": "^0.3.0",
|
|
37
37
|
"web-push": "^3.6.7",
|
|
38
38
|
"@netlify/blobs": "^8.1.0"
|
|
39
39
|
},
|