@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 CHANGED
@@ -62,9 +62,17 @@ const rei = await createReiServer({
62
62
 
63
63
  当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
64
64
 
65
- - `apiUrl` 必须是完整聊天端点(例如:`https://api.openai.com/v1/chat/completions`)。
66
- - SDK 会自动做最小规范化:去首尾空白、去路径尾部多余 `/`。
67
- - SDK **不会**自动补全 `/v1`、`/chat/completions` 等路径。
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
- var _amsgshared = require('@rei-standard/amsg-shared');
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, 1500));
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, 1500));
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', _6 => _6.choices, 'optionalAccess', _7 => _7[0], 'optionalAccess', _8 => _8.message, 'optionalAccess', _9 => _9.content]);
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', _10 => _10.default, 'optionalAccess', _11 => _11.getStore]);
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
- function normalizeVapidSubject(email) {
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. Mirrors
143
- * @rei-standard/amsg-instant's `validateSplitPattern` (kept in lockstep).
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 — kept byte-for-byte equivalent to
843
- * `@rei-standard/amsg-instant`'s `splitMessageIntoSentences`. Server carries
844
- * its own copy to avoid an architectural dependency on the instant package.
845
- * Name matches the instant export on purpose so cross-package grep finds
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, 1500));
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, 1500));
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, validateAvatarUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
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. Mirrors
143
- * @rei-standard/amsg-instant's `validateSplitPattern` (kept in lockstep).
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 — kept byte-for-byte equivalent to
843
- * `@rei-standard/amsg-instant`'s `splitMessageIntoSentences`. Server carries
844
- * its own copy to avoid an architectural dependency on the instant package.
845
- * Name matches the instant export on purpose so cross-package grep finds
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, 1500));
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, 1500));
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, validateAvatarUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
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, 1500));
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, 1500));
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
- function normalizeVapidSubject(email) {
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.2",
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.2.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
  },