@rei-standard/amsg-server 2.1.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.cjs +188 -21
- package/dist/index.d.cts +297 -24
- package/dist/index.d.ts +297 -24
- package/dist/index.mjs +185 -18
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -101,6 +101,112 @@ 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
|
+
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate the optional `avatarUrl` field. Rejects `data:` URIs (typically
|
|
110
|
+
* base64-encoded inline images) and anything longer than 2048 chars, both
|
|
111
|
+
* of which are the dominant trigger for downstream 413 / Web Push 4 KB
|
|
112
|
+
* payload errors. Returns an error message string, or null when valid.
|
|
113
|
+
*
|
|
114
|
+
* Mirrors @rei-standard/amsg-instant's `validateAvatarUrl` (kept in lockstep
|
|
115
|
+
* on purpose — both packages forward `avatarUrl` to the same SW push payload).
|
|
116
|
+
*
|
|
117
|
+
* @param {unknown} value
|
|
118
|
+
* @returns {string | null}
|
|
119
|
+
*/
|
|
120
|
+
function validateAvatarUrl(value) {
|
|
121
|
+
if (value === undefined || value === null) return null;
|
|
122
|
+
if (typeof value !== 'string') {
|
|
123
|
+
return 'avatarUrl 必须是字符串';
|
|
124
|
+
}
|
|
125
|
+
if (/^data:/i.test(value)) {
|
|
126
|
+
return '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
127
|
+
}
|
|
128
|
+
if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
129
|
+
return `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
130
|
+
}
|
|
131
|
+
if (!isValidUrl(value)) {
|
|
132
|
+
return 'avatarUrl 不是合法 URL';
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const SPLIT_PATTERN_MAX_LENGTH = 200;
|
|
138
|
+
const SPLIT_PATTERN_MAX_ITEMS = 10;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate the optional `splitPattern` field. Mirrors
|
|
142
|
+
* @rei-standard/amsg-instant's `validateSplitPattern` (kept in lockstep).
|
|
143
|
+
* Accepts `string`, `string[]`, or absent/null. Returns an error message
|
|
144
|
+
* string, or null when valid.
|
|
145
|
+
*
|
|
146
|
+
* Limits (per-item length ≤ 200, array ≤ 10 items, must compile via
|
|
147
|
+
* `new RegExp(item)`) are an **input-size guard**, NOT a ReDoS defense —
|
|
148
|
+
* a 6-character pattern like `(a+)+$` is enough to trigger catastrophic
|
|
149
|
+
* backtracking. The real backstop is Worker / runtime CPU limits + the
|
|
150
|
+
* fact that splitPattern is stored under the user's own encrypted task
|
|
151
|
+
* and matched against output from the user's own LLM API key, so the
|
|
152
|
+
* blast radius is self-inflicted only (no cross-tenant attack surface).
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} value
|
|
155
|
+
* @returns {string | null}
|
|
156
|
+
*/
|
|
157
|
+
function validateSplitPattern(value) {
|
|
158
|
+
if (value === undefined || value === null) return null;
|
|
159
|
+
const isArray = Array.isArray(value);
|
|
160
|
+
const items = isArray ? value : [value];
|
|
161
|
+
if (isArray && items.length === 0) return null; // empty array = use default
|
|
162
|
+
if (items.length > SPLIT_PATTERN_MAX_ITEMS) {
|
|
163
|
+
return `splitPattern 数组最多 ${SPLIT_PATTERN_MAX_ITEMS} 项`;
|
|
164
|
+
}
|
|
165
|
+
for (let i = 0; i < items.length; i++) {
|
|
166
|
+
const s = items[i];
|
|
167
|
+
const label = isArray ? `splitPattern[${i}]` : 'splitPattern';
|
|
168
|
+
if (typeof s !== 'string') return `${label} 必须是字符串`;
|
|
169
|
+
if (s.length > SPLIT_PATTERN_MAX_LENGTH) {
|
|
170
|
+
return `${label} 不能超过 ${SPLIT_PATTERN_MAX_LENGTH} 字符`;
|
|
171
|
+
}
|
|
172
|
+
try { new RegExp(s); }
|
|
173
|
+
catch (_) { return `${label} 不是有效正则表达式`; }
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate an OpenAI-style messages array. Same shape contract as
|
|
180
|
+
* `@rei-standard/amsg-instant` (kept in lockstep on purpose — both packages
|
|
181
|
+
* end up forwarding this to the same LLM body).
|
|
182
|
+
*
|
|
183
|
+
* @param {unknown} messages
|
|
184
|
+
* @returns {string | null} Error message, or null if valid.
|
|
185
|
+
*/
|
|
186
|
+
function validateLlmMessagesArray(messages) {
|
|
187
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
188
|
+
return 'messages must be a non-empty array';
|
|
189
|
+
}
|
|
190
|
+
for (let i = 0; i < messages.length; i++) {
|
|
191
|
+
const m = messages[i];
|
|
192
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) {
|
|
193
|
+
return `messages[${i}] must be an object`;
|
|
194
|
+
}
|
|
195
|
+
if (!VALID_LLM_MESSAGE_ROLES.has(m.role)) {
|
|
196
|
+
return `messages[${i}].role must be one of system / user / assistant / tool`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof m.content === 'string') {
|
|
199
|
+
if (!m.content) return `messages[${i}].content must be a non-empty string`;
|
|
200
|
+
} else if (Array.isArray(m.content)) {
|
|
201
|
+
if (m.content.length === 0) return `messages[${i}].content array must be non-empty`;
|
|
202
|
+
// Element schema is intentionally not enforced — passed through to LLM as-is.
|
|
203
|
+
} else {
|
|
204
|
+
return `messages[${i}].content must be a non-empty string or a non-empty array`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
104
210
|
/**
|
|
105
211
|
* Validate the schedule-message request payload.
|
|
106
212
|
*
|
|
@@ -146,9 +252,40 @@ function validateScheduleMessagePayload(payload) {
|
|
|
146
252
|
}
|
|
147
253
|
}
|
|
148
254
|
|
|
255
|
+
// ─── Prompt schema (shared by prompted / auto / instant AI configs) ──
|
|
256
|
+
//
|
|
257
|
+
// Callers provide *exactly one of* `completePrompt` (string) or `messages`
|
|
258
|
+
// (OpenAI-style array). Same contract as @rei-standard/amsg-instant; the
|
|
259
|
+
// server's LLM path forwards either verbatim.
|
|
260
|
+
const promptCheck = (() => {
|
|
261
|
+
const hasCompletePrompt = payload.completePrompt !== undefined && payload.completePrompt !== null && payload.completePrompt !== '';
|
|
262
|
+
const hasMessages = payload.messages !== undefined && payload.messages !== null;
|
|
263
|
+
if (hasCompletePrompt && hasMessages) {
|
|
264
|
+
return {
|
|
265
|
+
error: { code: 'INVALID_PARAMETERS', message: 'exactly one of `completePrompt` or `messages` must be provided(两者不能同时出现)', details: { invalidFields: ['completePrompt', 'messages'] } },
|
|
266
|
+
hasCompletePrompt: true, hasMessages: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (hasMessages) {
|
|
270
|
+
const err = validateLlmMessagesArray(payload.messages);
|
|
271
|
+
if (err) {
|
|
272
|
+
return {
|
|
273
|
+
error: { code: 'INVALID_PARAMETERS', message: err, details: { invalidFields: ['messages'] } },
|
|
274
|
+
hasCompletePrompt: false, hasMessages: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { error: null, hasCompletePrompt, hasMessages };
|
|
279
|
+
})();
|
|
280
|
+
|
|
281
|
+
if (promptCheck.error) {
|
|
282
|
+
return { valid: false, errorCode: promptCheck.error.code, errorMessage: promptCheck.error.message, details: promptCheck.error.details };
|
|
283
|
+
}
|
|
284
|
+
const hasPrompt = promptCheck.hasCompletePrompt || promptCheck.hasMessages;
|
|
285
|
+
|
|
149
286
|
if (payload.messageType === 'prompted' || payload.messageType === 'auto') {
|
|
150
287
|
const missingAiFields = [];
|
|
151
|
-
if (!
|
|
288
|
+
if (!hasPrompt) missingAiFields.push('completePrompt or messages');
|
|
152
289
|
if (!payload.apiUrl) missingAiFields.push('apiUrl');
|
|
153
290
|
if (!payload.apiKey) missingAiFields.push('apiKey');
|
|
154
291
|
if (!payload.primaryModel) missingAiFields.push('primaryModel');
|
|
@@ -161,15 +298,24 @@ function validateScheduleMessagePayload(payload) {
|
|
|
161
298
|
if (payload.recurrenceType && payload.recurrenceType !== 'none') {
|
|
162
299
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型的 recurrenceType 必须为 none', details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
|
|
163
300
|
}
|
|
164
|
-
const hasAiConfig =
|
|
301
|
+
const hasAiConfig = hasPrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
|
|
165
302
|
const hasUserMessage = payload.userMessage;
|
|
166
303
|
if (!hasAiConfig && !hasUserMessage) {
|
|
167
|
-
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or (completePrompt + apiUrl + apiKey + primaryModel)'] } };
|
|
304
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: 'instant 类型必须提供 userMessage 或完整的 AI 配置', details: { missingFields: ['userMessage or ((completePrompt | messages) + apiUrl + apiKey + primaryModel)'] } };
|
|
168
305
|
}
|
|
169
306
|
}
|
|
170
307
|
|
|
171
|
-
if (
|
|
172
|
-
|
|
308
|
+
if (
|
|
309
|
+
payload.temperature !== undefined &&
|
|
310
|
+
payload.temperature !== null &&
|
|
311
|
+
(typeof payload.temperature !== 'number' || !Number.isFinite(payload.temperature))
|
|
312
|
+
) {
|
|
313
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['temperature (must be a finite number)'] } };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
317
|
+
if (avatarErr) {
|
|
318
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: avatarErr, details: { invalidFields: ['avatarUrl'] } };
|
|
173
319
|
}
|
|
174
320
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
175
321
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
@@ -185,6 +331,11 @@ function validateScheduleMessagePayload(payload) {
|
|
|
185
331
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
|
|
186
332
|
}
|
|
187
333
|
|
|
334
|
+
const splitErr = validateSplitPattern(payload.splitPattern);
|
|
335
|
+
if (splitErr) {
|
|
336
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: splitErr, details: { invalidFields: ['splitPattern'] } };
|
|
337
|
+
}
|
|
338
|
+
|
|
188
339
|
return { valid: true };
|
|
189
340
|
}
|
|
190
341
|
|
|
@@ -650,6 +801,55 @@ function isUniqueViolation(error) {
|
|
|
650
801
|
*/
|
|
651
802
|
|
|
652
803
|
|
|
804
|
+
const DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Split a single chunk by one regex; on no-match return [chunk] so a later
|
|
808
|
+
* regex in a cascade can still take a swing at it.
|
|
809
|
+
*/
|
|
810
|
+
function splitOnceByRegex(chunk, regex) {
|
|
811
|
+
const out = chunk
|
|
812
|
+
.split(regex)
|
|
813
|
+
.reduce((acc, part, i, arr) => {
|
|
814
|
+
if (i % 2 === 0 && part.trim()) {
|
|
815
|
+
const punctuation = arr[i + 1] || '';
|
|
816
|
+
acc.push(part.trim() + punctuation);
|
|
817
|
+
}
|
|
818
|
+
return acc;
|
|
819
|
+
}, [])
|
|
820
|
+
.filter(s => s.length > 0);
|
|
821
|
+
return out.length > 0 ? out : [chunk];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Sentence splitter — kept byte-for-byte equivalent to
|
|
826
|
+
* `@rei-standard/amsg-instant`'s `splitMessageIntoSentences`. Server carries
|
|
827
|
+
* its own copy to avoid an architectural dependency on the instant package.
|
|
828
|
+
* Name matches the instant export on purpose so cross-package grep finds
|
|
829
|
+
* both copies; if you fix a bug here, fix it in the instant copy too.
|
|
830
|
+
*
|
|
831
|
+
* @param {string} messageContent
|
|
832
|
+
* @param {string | string[] | null} [splitPattern=null]
|
|
833
|
+
* @returns {string[]}
|
|
834
|
+
*/
|
|
835
|
+
function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
836
|
+
const sources =
|
|
837
|
+
splitPattern == null ? null :
|
|
838
|
+
Array.isArray(splitPattern) ? splitPattern :
|
|
839
|
+
[splitPattern];
|
|
840
|
+
|
|
841
|
+
const regexes = (sources && sources.length > 0)
|
|
842
|
+
? sources.map(s => new RegExp(s))
|
|
843
|
+
: [DEFAULT_SPLIT_REGEX];
|
|
844
|
+
|
|
845
|
+
let chunks = [messageContent];
|
|
846
|
+
for (const regex of regexes) {
|
|
847
|
+
chunks = chunks.flatMap(c => splitOnceByRegex(c, regex));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return chunks.length > 0 ? chunks : [messageContent];
|
|
851
|
+
}
|
|
852
|
+
|
|
653
853
|
/**
|
|
654
854
|
* @typedef {Object} ProcessorContext
|
|
655
855
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -681,7 +881,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
681
881
|
messageContent = decryptedPayload.userMessage;
|
|
682
882
|
|
|
683
883
|
} else if (decryptedPayload.messageType === 'instant') {
|
|
684
|
-
|
|
884
|
+
const hasPrompt = !!decryptedPayload.completePrompt || (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
|
|
885
|
+
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
685
886
|
messageContent = await _callAI(decryptedPayload);
|
|
686
887
|
} else if (decryptedPayload.userMessage) {
|
|
687
888
|
messageContent = decryptedPayload.userMessage;
|
|
@@ -695,19 +896,12 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
695
896
|
throw new Error('Invalid message configuration: no content source available');
|
|
696
897
|
}
|
|
697
898
|
|
|
698
|
-
// Sentence splitting
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
acc.push(part.trim() + punctuation);
|
|
705
|
-
}
|
|
706
|
-
return acc;
|
|
707
|
-
}, [])
|
|
708
|
-
.filter(s => s.length > 0);
|
|
709
|
-
|
|
710
|
-
const messages = sentences.length > 0 ? sentences : [messageContent];
|
|
899
|
+
// Sentence splitting (mirrors @rei-standard/amsg-instant
|
|
900
|
+
// splitMessageIntoSentences — keep in lockstep; do not drift). Caller may
|
|
901
|
+
// override the default regex via decryptedPayload.splitPattern (string
|
|
902
|
+
// for a single regex, string[] for a cascade). Validation already enforces
|
|
903
|
+
// length cap + RegExp compilability upstream.
|
|
904
|
+
const messages = splitMessageIntoSentences(messageContent, decryptedPayload.splitPattern ?? null);
|
|
711
905
|
|
|
712
906
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
713
907
|
throw new Error('VAPID configuration missing - push notifications cannot be sent');
|
|
@@ -887,12 +1081,29 @@ async function _callAI(payload) {
|
|
|
887
1081
|
* @returns {Object}
|
|
888
1082
|
*/
|
|
889
1083
|
function buildAiRequestBody(payload) {
|
|
1084
|
+
// messages mode (added in v2.2.0): forward the caller's OpenAI-style array
|
|
1085
|
+
// verbatim — same contract as @rei-standard/amsg-instant 0.5.0+. No auto
|
|
1086
|
+
// role injection, no concatenation back to a single user message. Lets
|
|
1087
|
+
// the upstream app preserve system / multi-turn context byte-for-byte
|
|
1088
|
+
// across the schedule-message path.
|
|
1089
|
+
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0
|
|
1090
|
+
? payload.messages
|
|
1091
|
+
: [{ role: 'user', content: payload.completePrompt }];
|
|
1092
|
+
|
|
890
1093
|
const requestBody = {
|
|
891
1094
|
model: payload.primaryModel,
|
|
892
|
-
messages:
|
|
893
|
-
temperature: 0.8
|
|
1095
|
+
messages: llmMessages,
|
|
894
1096
|
};
|
|
895
1097
|
|
|
1098
|
+
// Match the instant package's behavior: only inject default temperature
|
|
1099
|
+
// for the legacy completePrompt path; messages mode forwards whatever the
|
|
1100
|
+
// upstream app set (or nothing) so behavior matches their main chat path.
|
|
1101
|
+
if (payload.temperature !== undefined && payload.temperature !== null) {
|
|
1102
|
+
requestBody.temperature = payload.temperature;
|
|
1103
|
+
} else if (!Array.isArray(payload.messages)) {
|
|
1104
|
+
requestBody.temperature = 0.8;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
896
1107
|
if (payload.maxTokens === undefined || payload.maxTokens === null) {
|
|
897
1108
|
return requestBody;
|
|
898
1109
|
}
|
|
@@ -1052,8 +1263,17 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1052
1263
|
apiUrl: payload.apiUrl || null,
|
|
1053
1264
|
apiKey: payload.apiKey || null,
|
|
1054
1265
|
primaryModel: payload.primaryModel || null,
|
|
1266
|
+
// Prompt is one-of: legacy completePrompt (string) OR messages (OpenAI-
|
|
1267
|
+
// style array). Validation has already enforced exactly-one-of, so
|
|
1268
|
+
// exactly one of these will be non-null when an AI config is provided.
|
|
1055
1269
|
completePrompt: payload.completePrompt || null,
|
|
1270
|
+
messages: Array.isArray(payload.messages) ? payload.messages : null,
|
|
1056
1271
|
maxTokens: payload.maxTokens ?? null,
|
|
1272
|
+
temperature: payload.temperature ?? null,
|
|
1273
|
+
// 0.6.0+: optional caller-provided regex (string or string[]) used by
|
|
1274
|
+
// the message processor to chunk LLM output into individual pushes.
|
|
1275
|
+
// null → processor falls back to the default /([。!?!?]+)/ regex.
|
|
1276
|
+
splitPattern: payload.splitPattern ?? null,
|
|
1057
1277
|
pushSubscription: payload.pushSubscription,
|
|
1058
1278
|
metadata: payload.metadata || {}
|
|
1059
1279
|
};
|
|
@@ -1450,6 +1670,42 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1450
1670
|
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
|
|
1451
1671
|
}
|
|
1452
1672
|
|
|
1673
|
+
// Reject updates that try to set both completePrompt and messages at
|
|
1674
|
+
// once. We don't enforce one-of-required here (callers may patch other
|
|
1675
|
+
// fields), but the two prompt sources are mutually exclusive and must
|
|
1676
|
+
// stay that way in storage too.
|
|
1677
|
+
if (
|
|
1678
|
+
updates.completePrompt &&
|
|
1679
|
+
updates.messages !== undefined && updates.messages !== null
|
|
1680
|
+
) {
|
|
1681
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: 'completePrompt 与 messages 不能同时更新(二选一)', details: { invalidFields: ['completePrompt', 'messages'] } } } };
|
|
1682
|
+
}
|
|
1683
|
+
if (updates.messages !== undefined && updates.messages !== null) {
|
|
1684
|
+
const msgErr = validateLlmMessagesArray(updates.messages);
|
|
1685
|
+
if (msgErr) {
|
|
1686
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: msgErr, details: { invalidFields: ['messages'] } } } };
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (
|
|
1690
|
+
Object.prototype.hasOwnProperty.call(updates, 'temperature') &&
|
|
1691
|
+
updates.temperature !== null &&
|
|
1692
|
+
(typeof updates.temperature !== 'number' || !Number.isFinite(updates.temperature))
|
|
1693
|
+
) {
|
|
1694
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['temperature'] } } } };
|
|
1695
|
+
}
|
|
1696
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'splitPattern')) {
|
|
1697
|
+
const splitErr = validateSplitPattern(updates.splitPattern);
|
|
1698
|
+
if (splitErr) {
|
|
1699
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: splitErr, details: { invalidFields: ['splitPattern'] } } } };
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'avatarUrl')) {
|
|
1703
|
+
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1704
|
+
if (avatarErr) {
|
|
1705
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: avatarErr, details: { invalidFields: ['avatarUrl'] } } } };
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1453
1709
|
// Fetch existing task
|
|
1454
1710
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
1455
1711
|
|
|
@@ -1463,14 +1719,31 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1463
1719
|
|
|
1464
1720
|
const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
|
|
1465
1721
|
|
|
1722
|
+
// When the caller switches prompt source (completePrompt ↔ messages),
|
|
1723
|
+
// null out the other so storage stays one-of (matches schedule-message
|
|
1724
|
+
// shape and prevents buildAiRequestBody from accidentally seeing both).
|
|
1725
|
+
const promptUpdates = {};
|
|
1726
|
+
if (updates.completePrompt) {
|
|
1727
|
+
promptUpdates.completePrompt = updates.completePrompt;
|
|
1728
|
+
promptUpdates.messages = null;
|
|
1729
|
+
} else if (updates.messages !== undefined && updates.messages !== null) {
|
|
1730
|
+
promptUpdates.messages = updates.messages;
|
|
1731
|
+
promptUpdates.completePrompt = null;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1466
1734
|
const updatedData = {
|
|
1467
1735
|
...existingData,
|
|
1468
|
-
...
|
|
1736
|
+
...promptUpdates,
|
|
1469
1737
|
...(updates.userMessage && { userMessage: updates.userMessage }),
|
|
1470
1738
|
...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
|
|
1471
1739
|
...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
|
|
1472
1740
|
...(updates.metadata && { metadata: updates.metadata }),
|
|
1473
|
-
...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
|
|
1741
|
+
...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null }),
|
|
1742
|
+
...(Object.prototype.hasOwnProperty.call(updates, 'temperature') && { temperature: updates.temperature ?? null }),
|
|
1743
|
+
// splitPattern: hasOwnProperty so that explicit `null` (= revert to
|
|
1744
|
+
// default) doesn't get swallowed by truthy-spread the way the optional
|
|
1745
|
+
// string fields above are.
|
|
1746
|
+
...(Object.prototype.hasOwnProperty.call(updates, 'splitPattern') && { splitPattern: updates.splitPattern ?? null })
|
|
1474
1747
|
};
|
|
1475
1748
|
|
|
1476
1749
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
@@ -2204,4 +2477,4 @@ async function createReiServer(config) {
|
|
|
2204
2477
|
};
|
|
2205
2478
|
}
|
|
2206
2479
|
|
|
2207
|
-
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateScheduleMessagePayload, verifyTenantToken };
|
|
2480
|
+
export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateAvatarUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
|