@rei-standard/amsg-server 2.1.1 → 2.2.0

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
@@ -65,6 +65,21 @@ const rei = await createReiServer({
65
65
 
66
66
  如果上游返回 `405 Method Not Allowed`,通常表示 `apiUrl` 指向了基础域名而非聊天端点,请优先检查配置值。
67
67
 
68
+ ## 提示词字段(`completePrompt` vs `messages`,2.2.0+)
69
+
70
+ AI 配置消息的提示词可以用两种形态之一,**互斥二选一**:
71
+
72
+ - `completePrompt: string` —— 简单场景:内部包成单条 `{role:'user', content}` 后发给 LLM。
73
+ - `messages: Array<{ role: 'system'|'user'|'assistant'|'tool', content: string | unknown[] }>` —— 多轮 / 带 system role:**原样**转发给 LLM,不做任何 role 注入或重排。和上游主聊天路径调用 LLM 的 body 字节级一致。
74
+
75
+ 两个字段同时给 → `400 INVALID_PARAMETERS`;都不给且没 `userMessage`(仅 instant 类型允许 fallback)也是 400。`messages` 数组必须非空,role 必须是上面四种之一。
76
+
77
+ 可选 `temperature?: number` 透传给 LLM:`completePrompt` 路径未传时默认 0.8(保持旧行为);`messages` 路径未传时**不发**,让上游主路径自己决定。
78
+
79
+ ## 导出(新增)
80
+
81
+ - `validateLlmMessagesArray(messages)` — 同步预校验 messages 数组,返回 `string | null`(错误信息 / 通过)。和 `@rei-standard/amsg-instant` 的校验规则字节级一致。
82
+
68
83
  ## 一体化初始化流程
69
84
 
70
85
  1. 管理员配置环境变量(VAPID + tenant secrets)
package/dist/index.cjs CHANGED
@@ -47,6 +47,29 @@ 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
+ function validateLlmMessagesArray(messages) {
52
+ if (!Array.isArray(messages) || messages.length === 0) {
53
+ return "messages must be a non-empty array";
54
+ }
55
+ for (let i = 0; i < messages.length; i++) {
56
+ const m = messages[i];
57
+ if (!m || typeof m !== "object" || Array.isArray(m)) {
58
+ return `messages[${i}] must be an object`;
59
+ }
60
+ if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
61
+ return `messages[${i}].role must be one of system / user / assistant / tool`;
62
+ }
63
+ if (typeof m.content === "string") {
64
+ if (!m.content) return `messages[${i}].content must be a non-empty string`;
65
+ } else if (Array.isArray(m.content)) {
66
+ if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
67
+ } else {
68
+ return `messages[${i}].content must be a non-empty string or a non-empty array`;
69
+ }
70
+ }
71
+ return null;
72
+ }
50
73
  function validateScheduleMessagePayload(payload) {
51
74
  if (!payload.contactName || typeof payload.contactName !== "string") {
52
75
  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 +97,35 @@ function validateScheduleMessagePayload(payload) {
74
97
  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
98
  }
76
99
  }
100
+ const promptCheck = (() => {
101
+ const hasCompletePrompt = payload.completePrompt !== void 0 && payload.completePrompt !== null && payload.completePrompt !== "";
102
+ const hasMessages = payload.messages !== void 0 && payload.messages !== null;
103
+ if (hasCompletePrompt && hasMessages) {
104
+ return {
105
+ 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"] } },
106
+ hasCompletePrompt: true,
107
+ hasMessages: true
108
+ };
109
+ }
110
+ if (hasMessages) {
111
+ const err = validateLlmMessagesArray(payload.messages);
112
+ if (err) {
113
+ return {
114
+ error: { code: "INVALID_PARAMETERS", message: err, details: { invalidFields: ["messages"] } },
115
+ hasCompletePrompt: false,
116
+ hasMessages: true
117
+ };
118
+ }
119
+ }
120
+ return { error: null, hasCompletePrompt, hasMessages };
121
+ })();
122
+ if (promptCheck.error) {
123
+ return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
124
+ }
125
+ const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
77
126
  if (payload.messageType === "prompted" || payload.messageType === "auto") {
78
127
  const missingAiFields = [];
79
- if (!payload.completePrompt) missingAiFields.push("completePrompt");
128
+ if (!hasPrompt) missingAiFields.push("completePrompt or messages");
80
129
  if (!payload.apiUrl) missingAiFields.push("apiUrl");
81
130
  if (!payload.apiKey) missingAiFields.push("apiKey");
82
131
  if (!payload.primaryModel) missingAiFields.push("primaryModel");
@@ -88,12 +137,15 @@ function validateScheduleMessagePayload(payload) {
88
137
  if (payload.recurrenceType && payload.recurrenceType !== "none") {
89
138
  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
139
  }
91
- const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
140
+ const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
92
141
  const hasUserMessage = payload.userMessage;
93
142
  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)"] } };
143
+ 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
144
  }
96
145
  }
146
+ if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
147
+ 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)"] } };
148
+ }
97
149
  if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
98
150
  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)"] } };
99
151
  }
@@ -395,7 +447,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
395
447
  if (decryptedPayload.messageType === "fixed") {
396
448
  messageContent = decryptedPayload.userMessage;
397
449
  } else if (decryptedPayload.messageType === "instant") {
398
- if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
450
+ const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
451
+ if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
399
452
  messageContent = await _callAI(decryptedPayload);
400
453
  } else if (decryptedPayload.userMessage) {
401
454
  messageContent = decryptedPayload.userMessage;
@@ -537,11 +590,16 @@ async function _callAI(payload) {
537
590
  return content.trim();
538
591
  }
539
592
  function buildAiRequestBody(payload) {
593
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
540
594
  const requestBody = {
541
595
  model: payload.primaryModel,
542
- messages: [{ role: "user", content: payload.completePrompt }],
543
- temperature: 0.8
596
+ messages: llmMessages
544
597
  };
598
+ if (payload.temperature !== void 0 && payload.temperature !== null) {
599
+ requestBody.temperature = payload.temperature;
600
+ } else if (!Array.isArray(payload.messages)) {
601
+ requestBody.temperature = 0.8;
602
+ }
545
603
  if (payload.maxTokens === void 0 || payload.maxTokens === null) {
546
604
  return requestBody;
547
605
  }
@@ -641,8 +699,13 @@ function createScheduleMessageHandler(ctx) {
641
699
  apiUrl: payload.apiUrl || null,
642
700
  apiKey: payload.apiKey || null,
643
701
  primaryModel: payload.primaryModel || null,
702
+ // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
703
+ // style array). Validation has already enforced exactly-one-of, so
704
+ // exactly one of these will be non-null when an AI config is provided.
644
705
  completePrompt: payload.completePrompt || null,
706
+ messages: Array.isArray(payload.messages) ? payload.messages : null,
645
707
  maxTokens: _nullishCoalesce(payload.maxTokens, () => ( null)),
708
+ temperature: _nullishCoalesce(payload.temperature, () => ( null)),
646
709
  pushSubscription: payload.pushSubscription,
647
710
  metadata: payload.metadata || {}
648
711
  };
@@ -954,6 +1017,18 @@ function createUpdateMessageHandler(ctx) {
954
1017
  if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
955
1018
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
956
1019
  }
1020
+ if (updates.completePrompt && updates.messages !== void 0 && updates.messages !== null) {
1021
+ 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"] } } } };
1022
+ }
1023
+ if (updates.messages !== void 0 && updates.messages !== null) {
1024
+ const msgErr = validateLlmMessagesArray(updates.messages);
1025
+ if (msgErr) {
1026
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: msgErr, details: { invalidFields: ["messages"] } } } };
1027
+ }
1028
+ }
1029
+ if (Object.prototype.hasOwnProperty.call(updates, "temperature") && updates.temperature !== null && (typeof updates.temperature !== "number" || !Number.isFinite(updates.temperature))) {
1030
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature"] } } } };
1031
+ }
957
1032
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
958
1033
  if (!existingTask) {
959
1034
  const taskStatus = await db.getTaskStatus(taskUuid, userId);
@@ -963,14 +1038,23 @@ function createUpdateMessageHandler(ctx) {
963
1038
  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
1039
  }
965
1040
  const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1041
+ const promptUpdates = {};
1042
+ if (updates.completePrompt) {
1043
+ promptUpdates.completePrompt = updates.completePrompt;
1044
+ promptUpdates.messages = null;
1045
+ } else if (updates.messages !== void 0 && updates.messages !== null) {
1046
+ promptUpdates.messages = updates.messages;
1047
+ promptUpdates.completePrompt = null;
1048
+ }
966
1049
  const updatedData = {
967
1050
  ...existingData,
968
- ...updates.completePrompt && { completePrompt: updates.completePrompt },
1051
+ ...promptUpdates,
969
1052
  ...updates.userMessage && { userMessage: updates.userMessage },
970
1053
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
971
1054
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
972
1055
  ...updates.metadata && { metadata: updates.metadata },
973
- ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: _nullishCoalesce(updates.maxTokens, () => ( null)) }
1056
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: _nullishCoalesce(updates.maxTokens, () => ( null)) },
1057
+ ...Object.prototype.hasOwnProperty.call(updates, "temperature") && { temperature: _nullishCoalesce(updates.temperature, () => ( null)) }
974
1058
  };
975
1059
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
976
1060
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
@@ -1493,4 +1577,5 @@ async function createReiServer(config) {
1493
1577
 
1494
1578
 
1495
1579
 
1496
- 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.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.verifyTenantToken = verifyTenantToken;
1580
+
1581
+ 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.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.verifyTenantToken = verifyTenantToken;
package/dist/index.d.cts CHANGED
@@ -101,6 +101,40 @@ function isValidUUIDv4(uuid) {
101
101
  return uuidV4Regex.test(uuid);
102
102
  }
103
103
 
104
+ const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
105
+
106
+ /**
107
+ * Validate an OpenAI-style messages array. Same shape contract as
108
+ * `@rei-standard/amsg-instant` (kept in lockstep on purpose — both packages
109
+ * end up forwarding this to the same LLM body).
110
+ *
111
+ * @param {unknown} messages
112
+ * @returns {string | null} Error message, or null if valid.
113
+ */
114
+ function validateLlmMessagesArray(messages) {
115
+ if (!Array.isArray(messages) || messages.length === 0) {
116
+ return 'messages must be a non-empty array';
117
+ }
118
+ for (let i = 0; i < messages.length; i++) {
119
+ const m = messages[i];
120
+ if (!m || typeof m !== 'object' || Array.isArray(m)) {
121
+ return `messages[${i}] must be an object`;
122
+ }
123
+ if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
124
+ return `messages[${i}].role must be one of system / user / assistant / tool`;
125
+ }
126
+ if (typeof m.content === 'string') {
127
+ if (!m.content) return `messages[${i}].content must be a non-empty string`;
128
+ } else if (Array.isArray(m.content)) {
129
+ if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
130
+ // Element schema is intentionally not enforced — passed through to LLM as-is.
131
+ } else {
132
+ return `messages[${i}].content must be a non-empty string or a non-empty array`;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
104
138
  /**
105
139
  * Validate the schedule-message request payload.
106
140
  *
@@ -146,9 +180,40 @@ function validateScheduleMessagePayload(payload) {
146
180
  }
147
181
  }
148
182
 
183
+ // ─── Prompt schema (shared by prompted / auto / instant AI configs) ──
184
+ //
185
+ // Callers provide *exactly one of* `completePrompt` (string) or `messages`
186
+ // (OpenAI-style array). Same contract as @rei-standard/amsg-instant; the
187
+ // server's LLM path forwards either verbatim.
188
+ const promptCheck = (() => {
189
+ const hasCompletePrompt = payload.completePrompt !== undefined && payload.completePrompt !== null && payload.completePrompt !== '';
190
+ const hasMessages = payload.messages !== undefined && payload.messages !== null;
191
+ if (hasCompletePrompt && hasMessages) {
192
+ return {
193
+ error: { code: 'INVALID_PARAMETERS', message: 'exactly one of `completePrompt` or `messages` must be provided(两者不能同时出现)', details: { invalidFields: ['completePrompt', 'messages'] } },
194
+ hasCompletePrompt: true, hasMessages: true,
195
+ };
196
+ }
197
+ if (hasMessages) {
198
+ const err = validateLlmMessagesArray(payload.messages);
199
+ if (err) {
200
+ return {
201
+ error: { code: 'INVALID_PARAMETERS', message: err, details: { invalidFields: ['messages'] } },
202
+ hasCompletePrompt: false, hasMessages: true,
203
+ };
204
+ }
205
+ }
206
+ return { error: null, hasCompletePrompt, hasMessages };
207
+ })();
208
+
209
+ if (promptCheck.error) {
210
+ return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
211
+ }
212
+ const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
213
+
149
214
  if (payload.messageType === 'prompted' || payload.messageType === 'auto') {
150
215
  const missingAiFields = [];
151
- if (!payload.completePrompt) missingAiFields.push('completePrompt');
216
+ if (!hasPrompt) missingAiFields.push('completePrompt or messages');
152
217
  if (!payload.apiUrl) missingAiFields.push('apiUrl');
153
218
  if (!payload.apiKey) missingAiFields.push('apiKey');
154
219
  if (!payload.primaryModel) missingAiFields.push('primaryModel');
@@ -161,13 +226,21 @@ function validateScheduleMessagePayload(payload) {
161
226
  if (payload.recurrenceType && payload.recurrenceType !== 'none') {
162
227
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型的 recurrenceType 必须为 none', details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
163
228
  }
164
- const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
229
+ const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
165
230
  const hasUserMessage = payload.userMessage;
166
231
  if (!hasAiConfig && !hasUserMessage) {
167
- return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or (completePrompt + apiUrl + apiKey + primaryModel)'] } };
232
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or ((completePrompt | messages) + apiUrl + apiKey + primaryModel)'] } };
168
233
  }
169
234
  }
170
235
 
236
+ if (
237
+ payload.temperature !== undefined &&
238
+ payload.temperature !== null &&
239
+ (typeof payload.temperature !== 'number' || !Number.isFinite(payload.temperature))
240
+ ) {
241
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['temperature (must be a finite number)'] } };
242
+ }
243
+
171
244
  if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
172
245
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['avatarUrl (invalid URL format)'] } };
173
246
  }
@@ -681,7 +754,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
681
754
  messageContent = decryptedPayload.userMessage;
682
755
 
683
756
  } else if (decryptedPayload.messageType === 'instant') {
684
- if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
757
+ const hasPrompt = !!decryptedPayload.completePrompt || (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
758
+ if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
685
759
  messageContent = await _callAI(decryptedPayload);
686
760
  } else if (decryptedPayload.userMessage) {
687
761
  messageContent = decryptedPayload.userMessage;
@@ -887,12 +961,29 @@ async function _callAI(payload) {
887
961
  * @returns {Object}
888
962
  */
889
963
  function buildAiRequestBody(payload) {
964
+ // messages mode (added in v2.2.0): forward the caller's OpenAI-style array
965
+ // verbatim — same contract as @rei-standard/amsg-instant 0.5.0+. No auto
966
+ // role injection, no concatenation back to a single user message. Lets
967
+ // the upstream app preserve system / multi-turn context byte-for-byte
968
+ // across the schedule-message path.
969
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0
970
+ ? payload.messages
971
+ : [{ role: 'user', content: payload.completePrompt }];
972
+
890
973
  const requestBody = {
891
974
  model: payload.primaryModel,
892
- messages: [{ role: 'user', content: payload.completePrompt }],
893
- temperature: 0.8
975
+ messages: llmMessages,
894
976
  };
895
977
 
978
+ // Match the instant package's behavior: only inject default temperature
979
+ // for the legacy completePrompt path; messages mode forwards whatever the
980
+ // upstream app set (or nothing) so behavior matches their main chat path.
981
+ if (payload.temperature !== undefined && payload.temperature !== null) {
982
+ requestBody.temperature = payload.temperature;
983
+ } else if (!Array.isArray(payload.messages)) {
984
+ requestBody.temperature = 0.8;
985
+ }
986
+
896
987
  if (payload.maxTokens === undefined || payload.maxTokens === null) {
897
988
  return requestBody;
898
989
  }
@@ -1052,8 +1143,13 @@ function createScheduleMessageHandler(ctx) {
1052
1143
  apiUrl: payload.apiUrl || null,
1053
1144
  apiKey: payload.apiKey || null,
1054
1145
  primaryModel: payload.primaryModel || null,
1146
+ // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
1147
+ // style array). Validation has already enforced exactly-one-of, so
1148
+ // exactly one of these will be non-null when an AI config is provided.
1055
1149
  completePrompt: payload.completePrompt || null,
1150
+ messages: Array.isArray(payload.messages) ? payload.messages : null,
1056
1151
  maxTokens: payload.maxTokens ?? null,
1152
+ temperature: payload.temperature ?? null,
1057
1153
  pushSubscription: payload.pushSubscription,
1058
1154
  metadata: payload.metadata || {}
1059
1155
  };
@@ -1450,6 +1546,30 @@ function createUpdateMessageHandler(ctx) {
1450
1546
  return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
1451
1547
  }
1452
1548
 
1549
+ // Reject updates that try to set both completePrompt and messages at
1550
+ // once. We don't enforce one-of-required here (callers may patch other
1551
+ // fields), but the two prompt sources are mutually exclusive and must
1552
+ // stay that way in storage too.
1553
+ if (
1554
+ updates.completePrompt &&
1555
+ updates.messages !== undefined && updates.messages !== null
1556
+ ) {
1557
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: 'completePrompt 与 messages 不能同时更新(二选一)', details: { invalidFields: ['completePrompt', 'messages'] } } } };
1558
+ }
1559
+ if (updates.messages !== undefined && updates.messages !== null) {
1560
+ const msgErr = validateLlmMessagesArray(updates.messages);
1561
+ if (msgErr) {
1562
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: msgErr, details: { invalidFields: ['messages'] } } } };
1563
+ }
1564
+ }
1565
+ if (
1566
+ Object.prototype.hasOwnProperty.call(updates, 'temperature') &&
1567
+ updates.temperature !== null &&
1568
+ (typeof updates.temperature !== 'number' || !Number.isFinite(updates.temperature))
1569
+ ) {
1570
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['temperature'] } } } };
1571
+ }
1572
+
1453
1573
  // Fetch existing task
1454
1574
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
1455
1575
 
@@ -1463,14 +1583,27 @@ function createUpdateMessageHandler(ctx) {
1463
1583
 
1464
1584
  const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1465
1585
 
1586
+ // When the caller switches prompt source (completePrompt ↔ messages),
1587
+ // null out the other so storage stays one-of (matches schedule-message
1588
+ // shape and prevents buildAiRequestBody from accidentally seeing both).
1589
+ const promptUpdates = {};
1590
+ if (updates.completePrompt) {
1591
+ promptUpdates.completePrompt = updates.completePrompt;
1592
+ promptUpdates.messages = null;
1593
+ } else if (updates.messages !== undefined && updates.messages !== null) {
1594
+ promptUpdates.messages = updates.messages;
1595
+ promptUpdates.completePrompt = null;
1596
+ }
1597
+
1466
1598
  const updatedData = {
1467
1599
  ...existingData,
1468
- ...(updates.completePrompt && { completePrompt: updates.completePrompt }),
1600
+ ...promptUpdates,
1469
1601
  ...(updates.userMessage && { userMessage: updates.userMessage }),
1470
1602
  ...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
1471
1603
  ...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
1472
1604
  ...(updates.metadata && { metadata: updates.metadata }),
1473
- ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
1605
+ ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null }),
1606
+ ...(Object.prototype.hasOwnProperty.call(updates, 'temperature') && { temperature: updates.temperature ?? null })
1474
1607
  };
1475
1608
 
1476
1609
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
@@ -2204,4 +2337,4 @@ async function createReiServer(config) {
2204
2337
  };
2205
2338
  }
2206
2339
 
2207
- export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateScheduleMessagePayload, verifyTenantToken };
2340
+ export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, verifyTenantToken };
package/dist/index.d.ts CHANGED
@@ -101,6 +101,40 @@ function isValidUUIDv4(uuid) {
101
101
  return uuidV4Regex.test(uuid);
102
102
  }
103
103
 
104
+ const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
105
+
106
+ /**
107
+ * Validate an OpenAI-style messages array. Same shape contract as
108
+ * `@rei-standard/amsg-instant` (kept in lockstep on purpose — both packages
109
+ * end up forwarding this to the same LLM body).
110
+ *
111
+ * @param {unknown} messages
112
+ * @returns {string | null} Error message, or null if valid.
113
+ */
114
+ function validateLlmMessagesArray(messages) {
115
+ if (!Array.isArray(messages) || messages.length === 0) {
116
+ return 'messages must be a non-empty array';
117
+ }
118
+ for (let i = 0; i < messages.length; i++) {
119
+ const m = messages[i];
120
+ if (!m || typeof m !== 'object' || Array.isArray(m)) {
121
+ return `messages[${i}] must be an object`;
122
+ }
123
+ if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
124
+ return `messages[${i}].role must be one of system / user / assistant / tool`;
125
+ }
126
+ if (typeof m.content === 'string') {
127
+ if (!m.content) return `messages[${i}].content must be a non-empty string`;
128
+ } else if (Array.isArray(m.content)) {
129
+ if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
130
+ // Element schema is intentionally not enforced — passed through to LLM as-is.
131
+ } else {
132
+ return `messages[${i}].content must be a non-empty string or a non-empty array`;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
104
138
  /**
105
139
  * Validate the schedule-message request payload.
106
140
  *
@@ -146,9 +180,40 @@ function validateScheduleMessagePayload(payload) {
146
180
  }
147
181
  }
148
182
 
183
+ // ─── Prompt schema (shared by prompted / auto / instant AI configs) ──
184
+ //
185
+ // Callers provide *exactly one of* `completePrompt` (string) or `messages`
186
+ // (OpenAI-style array). Same contract as @rei-standard/amsg-instant; the
187
+ // server's LLM path forwards either verbatim.
188
+ const promptCheck = (() => {
189
+ const hasCompletePrompt = payload.completePrompt !== undefined && payload.completePrompt !== null && payload.completePrompt !== '';
190
+ const hasMessages = payload.messages !== undefined && payload.messages !== null;
191
+ if (hasCompletePrompt && hasMessages) {
192
+ return {
193
+ error: { code: 'INVALID_PARAMETERS', message: 'exactly one of `completePrompt` or `messages` must be provided(两者不能同时出现)', details: { invalidFields: ['completePrompt', 'messages'] } },
194
+ hasCompletePrompt: true, hasMessages: true,
195
+ };
196
+ }
197
+ if (hasMessages) {
198
+ const err = validateLlmMessagesArray(payload.messages);
199
+ if (err) {
200
+ return {
201
+ error: { code: 'INVALID_PARAMETERS', message: err, details: { invalidFields: ['messages'] } },
202
+ hasCompletePrompt: false, hasMessages: true,
203
+ };
204
+ }
205
+ }
206
+ return { error: null, hasCompletePrompt, hasMessages };
207
+ })();
208
+
209
+ if (promptCheck.error) {
210
+ return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
211
+ }
212
+ const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
213
+
149
214
  if (payload.messageType === 'prompted' || payload.messageType === 'auto') {
150
215
  const missingAiFields = [];
151
- if (!payload.completePrompt) missingAiFields.push('completePrompt');
216
+ if (!hasPrompt) missingAiFields.push('completePrompt or messages');
152
217
  if (!payload.apiUrl) missingAiFields.push('apiUrl');
153
218
  if (!payload.apiKey) missingAiFields.push('apiKey');
154
219
  if (!payload.primaryModel) missingAiFields.push('primaryModel');
@@ -161,13 +226,21 @@ function validateScheduleMessagePayload(payload) {
161
226
  if (payload.recurrenceType && payload.recurrenceType !== 'none') {
162
227
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型的 recurrenceType 必须为 none', details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
163
228
  }
164
- const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
229
+ const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
165
230
  const hasUserMessage = payload.userMessage;
166
231
  if (!hasAiConfig && !hasUserMessage) {
167
- return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or (completePrompt + apiUrl + apiKey + primaryModel)'] } };
232
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or ((completePrompt | messages) + apiUrl + apiKey + primaryModel)'] } };
168
233
  }
169
234
  }
170
235
 
236
+ if (
237
+ payload.temperature !== undefined &&
238
+ payload.temperature !== null &&
239
+ (typeof payload.temperature !== 'number' || !Number.isFinite(payload.temperature))
240
+ ) {
241
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['temperature (must be a finite number)'] } };
242
+ }
243
+
171
244
  if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
172
245
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['avatarUrl (invalid URL format)'] } };
173
246
  }
@@ -681,7 +754,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
681
754
  messageContent = decryptedPayload.userMessage;
682
755
 
683
756
  } else if (decryptedPayload.messageType === 'instant') {
684
- if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
757
+ const hasPrompt = !!decryptedPayload.completePrompt || (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
758
+ if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
685
759
  messageContent = await _callAI(decryptedPayload);
686
760
  } else if (decryptedPayload.userMessage) {
687
761
  messageContent = decryptedPayload.userMessage;
@@ -887,12 +961,29 @@ async function _callAI(payload) {
887
961
  * @returns {Object}
888
962
  */
889
963
  function buildAiRequestBody(payload) {
964
+ // messages mode (added in v2.2.0): forward the caller's OpenAI-style array
965
+ // verbatim — same contract as @rei-standard/amsg-instant 0.5.0+. No auto
966
+ // role injection, no concatenation back to a single user message. Lets
967
+ // the upstream app preserve system / multi-turn context byte-for-byte
968
+ // across the schedule-message path.
969
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0
970
+ ? payload.messages
971
+ : [{ role: 'user', content: payload.completePrompt }];
972
+
890
973
  const requestBody = {
891
974
  model: payload.primaryModel,
892
- messages: [{ role: 'user', content: payload.completePrompt }],
893
- temperature: 0.8
975
+ messages: llmMessages,
894
976
  };
895
977
 
978
+ // Match the instant package's behavior: only inject default temperature
979
+ // for the legacy completePrompt path; messages mode forwards whatever the
980
+ // upstream app set (or nothing) so behavior matches their main chat path.
981
+ if (payload.temperature !== undefined && payload.temperature !== null) {
982
+ requestBody.temperature = payload.temperature;
983
+ } else if (!Array.isArray(payload.messages)) {
984
+ requestBody.temperature = 0.8;
985
+ }
986
+
896
987
  if (payload.maxTokens === undefined || payload.maxTokens === null) {
897
988
  return requestBody;
898
989
  }
@@ -1052,8 +1143,13 @@ function createScheduleMessageHandler(ctx) {
1052
1143
  apiUrl: payload.apiUrl || null,
1053
1144
  apiKey: payload.apiKey || null,
1054
1145
  primaryModel: payload.primaryModel || null,
1146
+ // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
1147
+ // style array). Validation has already enforced exactly-one-of, so
1148
+ // exactly one of these will be non-null when an AI config is provided.
1055
1149
  completePrompt: payload.completePrompt || null,
1150
+ messages: Array.isArray(payload.messages) ? payload.messages : null,
1056
1151
  maxTokens: payload.maxTokens ?? null,
1152
+ temperature: payload.temperature ?? null,
1057
1153
  pushSubscription: payload.pushSubscription,
1058
1154
  metadata: payload.metadata || {}
1059
1155
  };
@@ -1450,6 +1546,30 @@ function createUpdateMessageHandler(ctx) {
1450
1546
  return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
1451
1547
  }
1452
1548
 
1549
+ // Reject updates that try to set both completePrompt and messages at
1550
+ // once. We don't enforce one-of-required here (callers may patch other
1551
+ // fields), but the two prompt sources are mutually exclusive and must
1552
+ // stay that way in storage too.
1553
+ if (
1554
+ updates.completePrompt &&
1555
+ updates.messages !== undefined && updates.messages !== null
1556
+ ) {
1557
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: 'completePrompt 与 messages 不能同时更新(二选一)', details: { invalidFields: ['completePrompt', 'messages'] } } } };
1558
+ }
1559
+ if (updates.messages !== undefined && updates.messages !== null) {
1560
+ const msgErr = validateLlmMessagesArray(updates.messages);
1561
+ if (msgErr) {
1562
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: msgErr, details: { invalidFields: ['messages'] } } } };
1563
+ }
1564
+ }
1565
+ if (
1566
+ Object.prototype.hasOwnProperty.call(updates, 'temperature') &&
1567
+ updates.temperature !== null &&
1568
+ (typeof updates.temperature !== 'number' || !Number.isFinite(updates.temperature))
1569
+ ) {
1570
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['temperature'] } } } };
1571
+ }
1572
+
1453
1573
  // Fetch existing task
1454
1574
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
1455
1575
 
@@ -1463,14 +1583,27 @@ function createUpdateMessageHandler(ctx) {
1463
1583
 
1464
1584
  const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1465
1585
 
1586
+ // When the caller switches prompt source (completePrompt ↔ messages),
1587
+ // null out the other so storage stays one-of (matches schedule-message
1588
+ // shape and prevents buildAiRequestBody from accidentally seeing both).
1589
+ const promptUpdates = {};
1590
+ if (updates.completePrompt) {
1591
+ promptUpdates.completePrompt = updates.completePrompt;
1592
+ promptUpdates.messages = null;
1593
+ } else if (updates.messages !== undefined && updates.messages !== null) {
1594
+ promptUpdates.messages = updates.messages;
1595
+ promptUpdates.completePrompt = null;
1596
+ }
1597
+
1466
1598
  const updatedData = {
1467
1599
  ...existingData,
1468
- ...(updates.completePrompt && { completePrompt: updates.completePrompt }),
1600
+ ...promptUpdates,
1469
1601
  ...(updates.userMessage && { userMessage: updates.userMessage }),
1470
1602
  ...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
1471
1603
  ...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
1472
1604
  ...(updates.metadata && { metadata: updates.metadata }),
1473
- ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
1605
+ ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null }),
1606
+ ...(Object.prototype.hasOwnProperty.call(updates, 'temperature') && { temperature: updates.temperature ?? null })
1474
1607
  };
1475
1608
 
1476
1609
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
@@ -2204,4 +2337,4 @@ async function createReiServer(config) {
2204
2337
  };
2205
2338
  }
2206
2339
 
2207
- export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateScheduleMessagePayload, verifyTenantToken };
2340
+ export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, verifyTenantToken };
package/dist/index.mjs CHANGED
@@ -47,6 +47,29 @@ 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
+ function validateLlmMessagesArray(messages) {
52
+ if (!Array.isArray(messages) || messages.length === 0) {
53
+ return "messages must be a non-empty array";
54
+ }
55
+ for (let i = 0; i < messages.length; i++) {
56
+ const m = messages[i];
57
+ if (!m || typeof m !== "object" || Array.isArray(m)) {
58
+ return `messages[${i}] must be an object`;
59
+ }
60
+ if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
61
+ return `messages[${i}].role must be one of system / user / assistant / tool`;
62
+ }
63
+ if (typeof m.content === "string") {
64
+ if (!m.content) return `messages[${i}].content must be a non-empty string`;
65
+ } else if (Array.isArray(m.content)) {
66
+ if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
67
+ } else {
68
+ return `messages[${i}].content must be a non-empty string or a non-empty array`;
69
+ }
70
+ }
71
+ return null;
72
+ }
50
73
  function validateScheduleMessagePayload(payload) {
51
74
  if (!payload.contactName || typeof payload.contactName !== "string") {
52
75
  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 +97,35 @@ function validateScheduleMessagePayload(payload) {
74
97
  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
98
  }
76
99
  }
100
+ const promptCheck = (() => {
101
+ const hasCompletePrompt = payload.completePrompt !== void 0 && payload.completePrompt !== null && payload.completePrompt !== "";
102
+ const hasMessages = payload.messages !== void 0 && payload.messages !== null;
103
+ if (hasCompletePrompt && hasMessages) {
104
+ return {
105
+ 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"] } },
106
+ hasCompletePrompt: true,
107
+ hasMessages: true
108
+ };
109
+ }
110
+ if (hasMessages) {
111
+ const err = validateLlmMessagesArray(payload.messages);
112
+ if (err) {
113
+ return {
114
+ error: { code: "INVALID_PARAMETERS", message: err, details: { invalidFields: ["messages"] } },
115
+ hasCompletePrompt: false,
116
+ hasMessages: true
117
+ };
118
+ }
119
+ }
120
+ return { error: null, hasCompletePrompt, hasMessages };
121
+ })();
122
+ if (promptCheck.error) {
123
+ return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
124
+ }
125
+ const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
77
126
  if (payload.messageType === "prompted" || payload.messageType === "auto") {
78
127
  const missingAiFields = [];
79
- if (!payload.completePrompt) missingAiFields.push("completePrompt");
128
+ if (!hasPrompt) missingAiFields.push("completePrompt or messages");
80
129
  if (!payload.apiUrl) missingAiFields.push("apiUrl");
81
130
  if (!payload.apiKey) missingAiFields.push("apiKey");
82
131
  if (!payload.primaryModel) missingAiFields.push("primaryModel");
@@ -88,12 +137,15 @@ function validateScheduleMessagePayload(payload) {
88
137
  if (payload.recurrenceType && payload.recurrenceType !== "none") {
89
138
  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
139
  }
91
- const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
140
+ const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
92
141
  const hasUserMessage = payload.userMessage;
93
142
  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)"] } };
143
+ 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
144
  }
96
145
  }
146
+ if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
147
+ 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)"] } };
148
+ }
97
149
  if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
98
150
  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)"] } };
99
151
  }
@@ -395,7 +447,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
395
447
  if (decryptedPayload.messageType === "fixed") {
396
448
  messageContent = decryptedPayload.userMessage;
397
449
  } else if (decryptedPayload.messageType === "instant") {
398
- if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
450
+ const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
451
+ if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
399
452
  messageContent = await _callAI(decryptedPayload);
400
453
  } else if (decryptedPayload.userMessage) {
401
454
  messageContent = decryptedPayload.userMessage;
@@ -537,11 +590,16 @@ async function _callAI(payload) {
537
590
  return content.trim();
538
591
  }
539
592
  function buildAiRequestBody(payload) {
593
+ const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
540
594
  const requestBody = {
541
595
  model: payload.primaryModel,
542
- messages: [{ role: "user", content: payload.completePrompt }],
543
- temperature: 0.8
596
+ messages: llmMessages
544
597
  };
598
+ if (payload.temperature !== void 0 && payload.temperature !== null) {
599
+ requestBody.temperature = payload.temperature;
600
+ } else if (!Array.isArray(payload.messages)) {
601
+ requestBody.temperature = 0.8;
602
+ }
545
603
  if (payload.maxTokens === void 0 || payload.maxTokens === null) {
546
604
  return requestBody;
547
605
  }
@@ -641,8 +699,13 @@ function createScheduleMessageHandler(ctx) {
641
699
  apiUrl: payload.apiUrl || null,
642
700
  apiKey: payload.apiKey || null,
643
701
  primaryModel: payload.primaryModel || null,
702
+ // Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
703
+ // style array). Validation has already enforced exactly-one-of, so
704
+ // exactly one of these will be non-null when an AI config is provided.
644
705
  completePrompt: payload.completePrompt || null,
706
+ messages: Array.isArray(payload.messages) ? payload.messages : null,
645
707
  maxTokens: payload.maxTokens ?? null,
708
+ temperature: payload.temperature ?? null,
646
709
  pushSubscription: payload.pushSubscription,
647
710
  metadata: payload.metadata || {}
648
711
  };
@@ -954,6 +1017,18 @@ function createUpdateMessageHandler(ctx) {
954
1017
  if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
955
1018
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
956
1019
  }
1020
+ if (updates.completePrompt && updates.messages !== void 0 && updates.messages !== null) {
1021
+ 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"] } } } };
1022
+ }
1023
+ if (updates.messages !== void 0 && updates.messages !== null) {
1024
+ const msgErr = validateLlmMessagesArray(updates.messages);
1025
+ if (msgErr) {
1026
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: msgErr, details: { invalidFields: ["messages"] } } } };
1027
+ }
1028
+ }
1029
+ if (Object.prototype.hasOwnProperty.call(updates, "temperature") && updates.temperature !== null && (typeof updates.temperature !== "number" || !Number.isFinite(updates.temperature))) {
1030
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature"] } } } };
1031
+ }
957
1032
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
958
1033
  if (!existingTask) {
959
1034
  const taskStatus = await db.getTaskStatus(taskUuid, userId);
@@ -963,14 +1038,23 @@ function createUpdateMessageHandler(ctx) {
963
1038
  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
1039
  }
965
1040
  const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1041
+ const promptUpdates = {};
1042
+ if (updates.completePrompt) {
1043
+ promptUpdates.completePrompt = updates.completePrompt;
1044
+ promptUpdates.messages = null;
1045
+ } else if (updates.messages !== void 0 && updates.messages !== null) {
1046
+ promptUpdates.messages = updates.messages;
1047
+ promptUpdates.completePrompt = null;
1048
+ }
966
1049
  const updatedData = {
967
1050
  ...existingData,
968
- ...updates.completePrompt && { completePrompt: updates.completePrompt },
1051
+ ...promptUpdates,
969
1052
  ...updates.userMessage && { userMessage: updates.userMessage },
970
1053
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
971
1054
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
972
1055
  ...updates.metadata && { metadata: updates.metadata },
973
- ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null }
1056
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null },
1057
+ ...Object.prototype.hasOwnProperty.call(updates, "temperature") && { temperature: updates.temperature ?? null }
974
1058
  };
975
1059
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
976
1060
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
@@ -1491,6 +1575,7 @@ export {
1491
1575
  isValidUUID,
1492
1576
  isValidUUIDv4,
1493
1577
  isValidUrl,
1578
+ validateLlmMessagesArray,
1494
1579
  validateScheduleMessagePayload,
1495
1580
  verifyTenantToken
1496
1581
  };
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.2.0",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters",
5
5
  "repository": {
6
6
  "type": "git",