@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 +15 -0
- package/dist/index.cjs +94 -9
- package/dist/index.d.cts +142 -9
- package/dist/index.d.ts +142 -9
- package/dist/index.mjs +93 -8
- package/package.json +1 -1
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 (!
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
...
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
...
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
...
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
...
|
|
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
|
};
|