@rei-standard/amsg-server 2.1.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -47,6 +47,71 @@ function isValidUUIDv4(uuid) {
47
47
  const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
48
48
  return uuidV4Regex.test(uuid);
49
49
  }
50
+ 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
+ var SPLIT_PATTERN_MAX_LENGTH = 200;
69
+ var SPLIT_PATTERN_MAX_ITEMS = 10;
70
+ function validateSplitPattern(value) {
71
+ if (value === void 0 || value === null) return null;
72
+ const isArray = Array.isArray(value);
73
+ const items = isArray ? value : [value];
74
+ if (isArray && items.length === 0) return null;
75
+ if (items.length > SPLIT_PATTERN_MAX_ITEMS) {
76
+ return `splitPattern \u6570\u7EC4\u6700\u591A ${SPLIT_PATTERN_MAX_ITEMS} \u9879`;
77
+ }
78
+ for (let i = 0; i < items.length; i++) {
79
+ const s = items[i];
80
+ const label = isArray ? `splitPattern[${i}]` : "splitPattern";
81
+ if (typeof s !== "string") return `${label} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`;
82
+ if (s.length > SPLIT_PATTERN_MAX_LENGTH) {
83
+ return `${label} \u4E0D\u80FD\u8D85\u8FC7 ${SPLIT_PATTERN_MAX_LENGTH} \u5B57\u7B26`;
84
+ }
85
+ try {
86
+ new RegExp(s);
87
+ } catch (_) {
88
+ return `${label} \u4E0D\u662F\u6709\u6548\u6B63\u5219\u8868\u8FBE\u5F0F`;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function validateLlmMessagesArray(messages) {
94
+ if (!Array.isArray(messages) || messages.length === 0) {
95
+ return "messages must be a non-empty array";
96
+ }
97
+ for (let i = 0; i < messages.length; i++) {
98
+ const m = messages[i];
99
+ if (!m || typeof m !== "object" || Array.isArray(m)) {
100
+ return `messages[${i}] must be an object`;
101
+ }
102
+ if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
103
+ return `messages[${i}].role must be one of system / user / assistant / tool`;
104
+ }
105
+ if (typeof m.content === "string") {
106
+ if (!m.content) return `messages[${i}].content must be a non-empty string`;
107
+ } else if (Array.isArray(m.content)) {
108
+ if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
109
+ } else {
110
+ return `messages[${i}].content must be a non-empty string or a non-empty array`;
111
+ }
112
+ }
113
+ return null;
114
+ }
50
115
  function validateScheduleMessagePayload(payload) {
51
116
  if (!payload.contactName || typeof payload.contactName !== "string") {
52
117
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
@@ -74,9 +139,35 @@ function validateScheduleMessagePayload(payload) {
74
139
  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)"] } };
75
140
  }
76
141
  }
142
+ const promptCheck = (() => {
143
+ const hasCompletePrompt = payload.completePrompt !== void 0 && payload.completePrompt !== null && payload.completePrompt !== "";
144
+ const hasMessages = payload.messages !== void 0 && payload.messages !== null;
145
+ if (hasCompletePrompt && hasMessages) {
146
+ return {
147
+ error: { code: "INVALID_PARAMETERS", message: "exactly one of `completePrompt` or `messages` must be provided\uFF08\u4E24\u8005\u4E0D\u80FD\u540C\u65F6\u51FA\u73B0\uFF09", details: { invalidFields: ["completePrompt", "messages"] } },
148
+ hasCompletePrompt: true,
149
+ hasMessages: true
150
+ };
151
+ }
152
+ if (hasMessages) {
153
+ const err = validateLlmMessagesArray(payload.messages);
154
+ if (err) {
155
+ return {
156
+ error: { code: "INVALID_PARAMETERS", message: err, details: { invalidFields: ["messages"] } },
157
+ hasCompletePrompt: false,
158
+ hasMessages: true
159
+ };
160
+ }
161
+ }
162
+ return { error: null, hasCompletePrompt, hasMessages };
163
+ })();
164
+ if (promptCheck.error) {
165
+ return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
166
+ }
167
+ const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
77
168
  if (payload.messageType === "prompted" || payload.messageType === "auto") {
78
169
  const missingAiFields = [];
79
- if (!payload.completePrompt) missingAiFields.push("completePrompt");
170
+ if (!hasPrompt) missingAiFields.push("completePrompt or messages");
80
171
  if (!payload.apiUrl) missingAiFields.push("apiUrl");
81
172
  if (!payload.apiKey) missingAiFields.push("apiKey");
82
173
  if (!payload.primaryModel) missingAiFields.push("primaryModel");
@@ -88,14 +179,18 @@ function validateScheduleMessagePayload(payload) {
88
179
  if (payload.recurrenceType && payload.recurrenceType !== "none") {
89
180
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u7684 recurrenceType \u5FC5\u987B\u4E3A none", details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
90
181
  }
91
- const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
182
+ const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
92
183
  const hasUserMessage = payload.userMessage;
93
184
  if (!hasAiConfig && !hasUserMessage) {
94
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u5FC5\u987B\u63D0\u4F9B userMessage \u6216\u5B8C\u6574\u7684 AI \u914D\u7F6E", details: { missingFields: ["userMessage or (completePrompt + apiUrl + apiKey + primaryModel)"] } };
185
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u5FC5\u987B\u63D0\u4F9B userMessage \u6216\u5B8C\u6574\u7684 AI \u914D\u7F6E", details: { missingFields: ["userMessage or ((completePrompt | messages) + apiUrl + apiKey + primaryModel)"] } };
95
186
  }
96
187
  }
97
- if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
98
- return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["avatarUrl (invalid URL format)"] } };
188
+ if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
189
+ 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
+ }
191
+ const avatarErr = validateAvatarUrl(payload.avatarUrl);
192
+ if (avatarErr) {
193
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: avatarErr, details: { invalidFields: ["avatarUrl"] } };
99
194
  }
100
195
  if (payload.uuid && !isValidUUID(payload.uuid)) {
101
196
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
@@ -103,6 +198,10 @@ function validateScheduleMessagePayload(payload) {
103
198
  if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
104
199
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
105
200
  }
201
+ const splitErr = validateSplitPattern(payload.splitPattern);
202
+ if (splitErr) {
203
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: splitErr, details: { invalidFields: ["splitPattern"] } };
204
+ }
106
205
  return { valid: true };
107
206
  }
108
207
 
@@ -383,6 +482,26 @@ function isUniqueViolation(error) {
383
482
 
384
483
  // src/server/lib/message-processor.js
385
484
  import { randomUUID } from "crypto";
485
+ var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
486
+ function splitOnceByRegex(chunk, regex) {
487
+ const out = chunk.split(regex).reduce((acc, part, i, arr) => {
488
+ if (i % 2 === 0 && part.trim()) {
489
+ const punctuation = arr[i + 1] || "";
490
+ acc.push(part.trim() + punctuation);
491
+ }
492
+ return acc;
493
+ }, []).filter((s) => s.length > 0);
494
+ return out.length > 0 ? out : [chunk];
495
+ }
496
+ function splitMessageIntoSentences(messageContent, splitPattern = null) {
497
+ const sources = splitPattern == null ? null : Array.isArray(splitPattern) ? splitPattern : [splitPattern];
498
+ const regexes = sources && sources.length > 0 ? sources.map((s) => new RegExp(s)) : [DEFAULT_SPLIT_REGEX];
499
+ let chunks = [messageContent];
500
+ for (const regex of regexes) {
501
+ chunks = chunks.flatMap((c) => splitOnceByRegex(c, regex));
502
+ }
503
+ return chunks.length > 0 ? chunks : [messageContent];
504
+ }
386
505
  async function processSingleMessage(task, ctx, providedMasterKey) {
387
506
  try {
388
507
  const masterKey = providedMasterKey || ctx.masterKey;
@@ -395,7 +514,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
395
514
  if (decryptedPayload.messageType === "fixed") {
396
515
  messageContent = decryptedPayload.userMessage;
397
516
  } else if (decryptedPayload.messageType === "instant") {
398
- if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
517
+ const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
518
+ if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
399
519
  messageContent = await _callAI(decryptedPayload);
400
520
  } else if (decryptedPayload.userMessage) {
401
521
  messageContent = decryptedPayload.userMessage;
@@ -407,14 +527,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
407
527
  } else {
408
528
  throw new Error("Invalid message configuration: no content source available");
409
529
  }
410
- const sentences = messageContent.split(/([。!?!?]+)/).reduce((acc, part, i, arr) => {
411
- if (i % 2 === 0 && part.trim()) {
412
- const punctuation = arr[i + 1] || "";
413
- acc.push(part.trim() + punctuation);
414
- }
415
- return acc;
416
- }, []).filter((s) => s.length > 0);
417
- const messages = sentences.length > 0 ? sentences : [messageContent];
530
+ const messages = splitMessageIntoSentences(messageContent, decryptedPayload.splitPattern ?? null);
418
531
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
419
532
  throw new Error("VAPID configuration missing - push notifications cannot be sent");
420
533
  }
@@ -537,11 +650,16 @@ async function _callAI(payload) {
537
650
  return content.trim();
538
651
  }
539
652
  function buildAiRequestBody(payload) {
653
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
540
654
  const requestBody = {
541
655
  model: payload.primaryModel,
542
- messages: [{ role: "user", content: payload.completePrompt }],
543
- temperature: 0.8
656
+ messages: llmMessages
544
657
  };
658
+ if (payload.temperature !== void 0 && payload.temperature !== null) {
659
+ requestBody.temperature = payload.temperature;
660
+ } else if (!Array.isArray(payload.messages)) {
661
+ requestBody.temperature = 0.8;
662
+ }
545
663
  if (payload.maxTokens === void 0 || payload.maxTokens === null) {
546
664
  return requestBody;
547
665
  }
@@ -641,8 +759,17 @@ function createScheduleMessageHandler(ctx) {
641
759
  apiUrl: payload.apiUrl || null,
642
760
  apiKey: payload.apiKey || null,
643
761
  primaryModel: payload.primaryModel || null,
762
+ // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
763
+ // style array). Validation has already enforced exactly-one-of, so
764
+ // exactly one of these will be non-null when an AI config is provided.
644
765
  completePrompt: payload.completePrompt || null,
766
+ messages: Array.isArray(payload.messages) ? payload.messages : null,
645
767
  maxTokens: payload.maxTokens ?? null,
768
+ temperature: payload.temperature ?? null,
769
+ // 0.6.0+: optional caller-provided regex (string or string[]) used by
770
+ // the message processor to chunk LLM output into individual pushes.
771
+ // null → processor falls back to the default /([。!?!?]+)/ regex.
772
+ splitPattern: payload.splitPattern ?? null,
646
773
  pushSubscription: payload.pushSubscription,
647
774
  metadata: payload.metadata || {}
648
775
  };
@@ -954,6 +1081,30 @@ function createUpdateMessageHandler(ctx) {
954
1081
  if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
955
1082
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
956
1083
  }
1084
+ if (updates.completePrompt && updates.messages !== void 0 && updates.messages !== null) {
1085
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "completePrompt \u4E0E messages \u4E0D\u80FD\u540C\u65F6\u66F4\u65B0\uFF08\u4E8C\u9009\u4E00\uFF09", details: { invalidFields: ["completePrompt", "messages"] } } } };
1086
+ }
1087
+ if (updates.messages !== void 0 && updates.messages !== null) {
1088
+ const msgErr = validateLlmMessagesArray(updates.messages);
1089
+ if (msgErr) {
1090
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: msgErr, details: { invalidFields: ["messages"] } } } };
1091
+ }
1092
+ }
1093
+ if (Object.prototype.hasOwnProperty.call(updates, "temperature") && updates.temperature !== null && (typeof updates.temperature !== "number" || !Number.isFinite(updates.temperature))) {
1094
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature"] } } } };
1095
+ }
1096
+ if (Object.prototype.hasOwnProperty.call(updates, "splitPattern")) {
1097
+ const splitErr = validateSplitPattern(updates.splitPattern);
1098
+ if (splitErr) {
1099
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: splitErr, details: { invalidFields: ["splitPattern"] } } } };
1100
+ }
1101
+ }
1102
+ if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
1103
+ const avatarErr = validateAvatarUrl(updates.avatarUrl);
1104
+ if (avatarErr) {
1105
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: avatarErr, details: { invalidFields: ["avatarUrl"] } } } };
1106
+ }
1107
+ }
957
1108
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
958
1109
  if (!existingTask) {
959
1110
  const taskStatus = await db.getTaskStatus(taskUuid, userId);
@@ -963,14 +1114,27 @@ function createUpdateMessageHandler(ctx) {
963
1114
  return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
964
1115
  }
965
1116
  const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1117
+ const promptUpdates = {};
1118
+ if (updates.completePrompt) {
1119
+ promptUpdates.completePrompt = updates.completePrompt;
1120
+ promptUpdates.messages = null;
1121
+ } else if (updates.messages !== void 0 && updates.messages !== null) {
1122
+ promptUpdates.messages = updates.messages;
1123
+ promptUpdates.completePrompt = null;
1124
+ }
966
1125
  const updatedData = {
967
1126
  ...existingData,
968
- ...updates.completePrompt && { completePrompt: updates.completePrompt },
1127
+ ...promptUpdates,
969
1128
  ...updates.userMessage && { userMessage: updates.userMessage },
970
1129
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
971
1130
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
972
1131
  ...updates.metadata && { metadata: updates.metadata },
973
- ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null }
1132
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null },
1133
+ ...Object.prototype.hasOwnProperty.call(updates, "temperature") && { temperature: updates.temperature ?? null },
1134
+ // splitPattern: hasOwnProperty so that explicit `null` (= revert to
1135
+ // default) doesn't get swallowed by truthy-spread the way the optional
1136
+ // string fields above are.
1137
+ ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
974
1138
  };
975
1139
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
976
1140
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
@@ -1491,6 +1655,9 @@ export {
1491
1655
  isValidUUID,
1492
1656
  isValidUUIDv4,
1493
1657
  isValidUrl,
1658
+ validateAvatarUrl,
1659
+ validateLlmMessagesArray,
1494
1660
  validateScheduleMessagePayload,
1661
+ validateSplitPattern,
1495
1662
  verifyTenantToken
1496
1663
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-server",
3
- "version": "2.1.1",
3
+ "version": "2.3.1",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters",
5
5
  "repository": {
6
6
  "type": "git",