@rei-standard/amsg-server 2.0.0-pre1 → 2.1.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 +13 -2
- package/dist/index.cjs +72 -17
- package/dist/index.d.cts +139 -27
- package/dist/index.d.ts +139 -27
- package/dist/index.mjs +68 -13
- package/dist/{neon-DyBfYFkz.d.ts → neon-CJl66EGy.d.cts} +2 -2
- package/dist/{neon-Ch9Abtfp.d.cts → neon-DYvGnCzx.d.ts} +2 -2
- package/dist/{pg-DJWS_HZ3.d.ts → pg-07S-u_H4.d.cts} +2 -2
- package/dist/{pg-BpYu3O81.d.cts → pg-Du-pN_UT.d.ts} +2 -2
- package/dist/{schema-D-ldTuIC.d.cts → schema-C8OnYk6j.d.cts} +1 -1
- package/dist/{schema-D-ldTuIC.d.ts → schema-C8OnYk6j.d.ts} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @rei-standard/amsg-server
|
|
2
2
|
|
|
3
|
-
`@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK(v2.0.
|
|
3
|
+
`@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK(v2.0.1),提供 Blob 租户配置、租户 token 鉴权和标准路由处理器。
|
|
4
4
|
|
|
5
|
-
## v2.0.
|
|
5
|
+
## v2.0.1 变更摘要
|
|
6
6
|
|
|
7
7
|
- 初始化流程合并为 `POST /api/v1/init-tenant`
|
|
8
8
|
- 移除旧端点:`init-database`、`init-master-key`
|
|
@@ -50,6 +50,17 @@ const rei = await createReiServer({
|
|
|
50
50
|
// GET /api/v1/messages -> rei.handlers.messages.GET
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## AI 接口 `apiUrl` 约束
|
|
54
|
+
|
|
55
|
+
当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
|
|
56
|
+
|
|
57
|
+
- `apiUrl` 必须是完整聊天端点(例如:`https://api.openai.com/v1/chat/completions`)。
|
|
58
|
+
- SDK 会自动做最小规范化:去首尾空白、去路径尾部多余 `/`。
|
|
59
|
+
- SDK **不会**自动补全 `/v1`、`/chat/completions` 等路径。
|
|
60
|
+
- `maxTokens` 为可选字段:传了就映射为 `max_tokens`;不传则不指定(由上游模型默认策略决定)。
|
|
61
|
+
|
|
62
|
+
如果上游返回 `405 Method Not Allowed`,通常表示 `apiUrl` 指向了基础域名而非聊天端点,请优先检查配置值。
|
|
63
|
+
|
|
53
64
|
## 一体化初始化流程
|
|
54
65
|
|
|
55
66
|
1. 管理员配置环境变量(VAPID + tenant secrets)
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/adapters/factory.js
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/adapters/factory.js
|
|
2
2
|
async function createAdapter(config) {
|
|
3
3
|
if (!config || !config.driver) {
|
|
4
4
|
throw new Error(
|
|
@@ -66,6 +66,9 @@ function validateScheduleMessagePayload(payload) {
|
|
|
66
66
|
if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
|
|
67
67
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
|
|
68
68
|
}
|
|
69
|
+
if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
|
|
70
|
+
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
|
|
71
|
+
}
|
|
69
72
|
if (payload.messageType === "fixed") {
|
|
70
73
|
if (!payload.userMessage) {
|
|
71
74
|
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)"] } };
|
|
@@ -97,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
|
|
|
97
100
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
98
101
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
|
|
99
102
|
}
|
|
100
|
-
if (payload.messageSubtype &&
|
|
103
|
+
if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
|
|
101
104
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
|
|
102
105
|
}
|
|
103
106
|
return { valid: true };
|
|
@@ -505,25 +508,66 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
async function _callAI(payload) {
|
|
508
|
-
const
|
|
511
|
+
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
512
|
+
const requestBody = buildAiRequestBody(payload);
|
|
513
|
+
const aiResponse = await fetch(normalizedApiUrl, {
|
|
509
514
|
method: "POST",
|
|
510
515
|
headers: {
|
|
511
516
|
"Content-Type": "application/json",
|
|
512
517
|
"Authorization": `Bearer ${payload.apiKey}`
|
|
513
518
|
},
|
|
514
|
-
body: JSON.stringify(
|
|
515
|
-
model: payload.primaryModel,
|
|
516
|
-
messages: [{ role: "user", content: payload.completePrompt }],
|
|
517
|
-
max_tokens: 500,
|
|
518
|
-
temperature: 0.8
|
|
519
|
-
}),
|
|
519
|
+
body: JSON.stringify(requestBody),
|
|
520
520
|
signal: AbortSignal.timeout(3e5)
|
|
521
521
|
});
|
|
522
522
|
if (!aiResponse.ok) {
|
|
523
|
-
|
|
523
|
+
if (aiResponse.status === 405) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(
|
|
529
|
+
`AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
|
|
530
|
+
);
|
|
524
531
|
}
|
|
525
532
|
const aiData = await aiResponse.json();
|
|
526
|
-
|
|
533
|
+
const content = _optionalChain([aiData, 'optionalAccess', _ => _.choices, 'optionalAccess', _2 => _2[0], 'optionalAccess', _3 => _3.message, 'optionalAccess', _4 => _4.content]);
|
|
534
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
535
|
+
throw new Error("AI API error: response missing choices[0].message.content");
|
|
536
|
+
}
|
|
537
|
+
return content.trim();
|
|
538
|
+
}
|
|
539
|
+
function buildAiRequestBody(payload) {
|
|
540
|
+
const requestBody = {
|
|
541
|
+
model: payload.primaryModel,
|
|
542
|
+
messages: [{ role: "user", content: payload.completePrompt }],
|
|
543
|
+
temperature: 0.8
|
|
544
|
+
};
|
|
545
|
+
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
546
|
+
return requestBody;
|
|
547
|
+
}
|
|
548
|
+
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
549
|
+
throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
|
|
550
|
+
}
|
|
551
|
+
requestBody.max_tokens = payload.maxTokens;
|
|
552
|
+
return requestBody;
|
|
553
|
+
}
|
|
554
|
+
function normalizeAiApiUrl(apiUrl) {
|
|
555
|
+
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
"Invalid apiUrl: apiUrl is required. Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions)."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const trimmedApiUrl = apiUrl.trim();
|
|
561
|
+
let parsedUrl;
|
|
562
|
+
try {
|
|
563
|
+
parsedUrl = new URL(trimmedApiUrl);
|
|
564
|
+
} catch (e3) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL that points to a full chat endpoint.`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
570
|
+
return parsedUrl.toString();
|
|
527
571
|
}
|
|
528
572
|
|
|
529
573
|
// src/server/handlers/schedule-message.js
|
|
@@ -591,6 +635,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
591
635
|
apiKey: payload.apiKey || null,
|
|
592
636
|
primaryModel: payload.primaryModel || null,
|
|
593
637
|
completePrompt: payload.completePrompt || null,
|
|
638
|
+
maxTokens: _nullishCoalesce(payload.maxTokens, () => ( null)),
|
|
594
639
|
pushSubscription: payload.pushSubscription,
|
|
595
640
|
metadata: payload.metadata || {}
|
|
596
641
|
};
|
|
@@ -899,6 +944,9 @@ function createUpdateMessageHandler(ctx) {
|
|
|
899
944
|
if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
|
|
900
945
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
|
|
901
946
|
}
|
|
947
|
+
if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
|
|
948
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
|
|
949
|
+
}
|
|
902
950
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
903
951
|
if (!existingTask) {
|
|
904
952
|
const taskStatus = await db.getTaskStatus(taskUuid, userId);
|
|
@@ -914,7 +962,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
914
962
|
...updates.userMessage && { userMessage: updates.userMessage },
|
|
915
963
|
...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
|
|
916
964
|
...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
|
|
917
|
-
...updates.metadata && { metadata: updates.metadata }
|
|
965
|
+
...updates.metadata && { metadata: updates.metadata },
|
|
966
|
+
...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: _nullishCoalesce(updates.maxTokens, () => ( null)) }
|
|
918
967
|
};
|
|
919
968
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
920
969
|
const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
|
|
@@ -1106,7 +1155,7 @@ async function resolveBlobStore(namespace) {
|
|
|
1106
1155
|
}
|
|
1107
1156
|
try {
|
|
1108
1157
|
const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
|
|
1109
|
-
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access',
|
|
1158
|
+
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _5 => _5.default, 'optionalAccess', _6 => _6.getStore]);
|
|
1110
1159
|
if (typeof getStore === "function") {
|
|
1111
1160
|
return getStore({ name: namespace });
|
|
1112
1161
|
}
|
|
@@ -1186,15 +1235,21 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1186
1235
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
1187
1236
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1188
1237
|
const expectedSignature = sign(signingInput, secret);
|
|
1189
|
-
|
|
1190
|
-
|
|
1238
|
+
let receivedBuffer;
|
|
1239
|
+
let expectedBuffer;
|
|
1240
|
+
try {
|
|
1241
|
+
receivedBuffer = base64UrlDecode2(receivedSignature);
|
|
1242
|
+
expectedBuffer = base64UrlDecode2(expectedSignature);
|
|
1243
|
+
} catch (e4) {
|
|
1244
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1245
|
+
}
|
|
1191
1246
|
if (receivedBuffer.length !== expectedBuffer.length || !_crypto.timingSafeEqual.call(void 0, receivedBuffer, expectedBuffer)) {
|
|
1192
1247
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1193
1248
|
}
|
|
1194
1249
|
let payload;
|
|
1195
1250
|
try {
|
|
1196
1251
|
payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
|
|
1197
|
-
} catch (
|
|
1252
|
+
} catch (e5) {
|
|
1198
1253
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1199
1254
|
}
|
|
1200
1255
|
if (!payload || typeof payload !== "object" || payload.v !== 1) {
|
|
@@ -1251,7 +1306,7 @@ function parseTokenFromUrl(url) {
|
|
|
1251
1306
|
try {
|
|
1252
1307
|
const parsed = new URL(url, "https://dummy.local");
|
|
1253
1308
|
return String(parsed.searchParams.get("token") || "").trim();
|
|
1254
|
-
} catch (
|
|
1309
|
+
} catch (e6) {
|
|
1255
1310
|
return "";
|
|
1256
1311
|
}
|
|
1257
1312
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID,
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter Factory
|
|
5
|
-
* ReiStandard SDK v2.0.
|
|
5
|
+
* ReiStandard SDK v2.0.1
|
|
6
6
|
*
|
|
7
7
|
* Creates a database adapter instance based on the supplied configuration.
|
|
8
8
|
*
|
|
@@ -35,12 +35,12 @@ async function createAdapter(config) {
|
|
|
35
35
|
|
|
36
36
|
switch (config.driver) {
|
|
37
37
|
case 'neon': {
|
|
38
|
-
const { NeonAdapter } = await import('./neon-
|
|
38
|
+
const { NeonAdapter } = await import('./neon-CJl66EGy.d.cts');
|
|
39
39
|
return new NeonAdapter(config.connectionString);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
case 'pg': {
|
|
43
|
-
const { PgAdapter } = await import('./pg-
|
|
43
|
+
const { PgAdapter } = await import('./pg-07S-u_H4.d.cts');
|
|
44
44
|
return new PgAdapter(config.connectionString);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -54,7 +54,7 @@ async function createAdapter(config) {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Validation utility library (SDK version)
|
|
57
|
-
* ReiStandard SDK v2.0.
|
|
57
|
+
* ReiStandard SDK v2.0.1
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -132,6 +132,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
132
132
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['recurrenceType'] } };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
if (
|
|
136
|
+
payload.maxTokens !== undefined &&
|
|
137
|
+
payload.maxTokens !== null &&
|
|
138
|
+
(!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)
|
|
139
|
+
) {
|
|
140
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['maxTokens'] } };
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
if (payload.messageType === 'fixed') {
|
|
136
144
|
if (!payload.userMessage) {
|
|
137
145
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { missingFields: ['userMessage (required for fixed type)'] } };
|
|
@@ -166,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
166
174
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
167
175
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
// messageSubtype is a free-form string tag forwarded to SW-side push payload
|
|
178
|
+
// for classification. Only the type is enforced; the taxonomy is the
|
|
179
|
+
// consumer's call (previously the enum was chat/forum/moment).
|
|
180
|
+
if (
|
|
181
|
+
payload.messageSubtype !== undefined &&
|
|
182
|
+
payload.messageSubtype !== null &&
|
|
183
|
+
typeof payload.messageSubtype !== 'string'
|
|
184
|
+
) {
|
|
170
185
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
|
|
171
186
|
}
|
|
172
187
|
|
|
@@ -320,7 +335,7 @@ function getHeader(headers = {}, name) {
|
|
|
320
335
|
|
|
321
336
|
/**
|
|
322
337
|
* Handler: init-tenant
|
|
323
|
-
* ReiStandard SDK v2.0.
|
|
338
|
+
* ReiStandard SDK v2.0.1
|
|
324
339
|
*
|
|
325
340
|
* @param {Object} ctx - Server context.
|
|
326
341
|
* @returns {{ POST: function }}
|
|
@@ -456,7 +471,7 @@ function createInitTenantHandler(ctx) {
|
|
|
456
471
|
|
|
457
472
|
/**
|
|
458
473
|
* Encryption utility library (SDK version)
|
|
459
|
-
* ReiStandard SDK v2.0.
|
|
474
|
+
* ReiStandard SDK v2.0.1
|
|
460
475
|
*
|
|
461
476
|
* Wraps AES-256-GCM operations for request/response and storage encryption.
|
|
462
477
|
*/
|
|
@@ -558,7 +573,7 @@ function decryptFromStorage(encryptedText, encryptionKey) {
|
|
|
558
573
|
|
|
559
574
|
/**
|
|
560
575
|
* Handler: get-user-key
|
|
561
|
-
* ReiStandard SDK v2.0.
|
|
576
|
+
* ReiStandard SDK v2.0.1
|
|
562
577
|
*
|
|
563
578
|
* @param {Object} ctx - Server context.
|
|
564
579
|
* @returns {{ GET: function }}
|
|
@@ -627,7 +642,7 @@ function isUniqueViolation(error) {
|
|
|
627
642
|
|
|
628
643
|
/**
|
|
629
644
|
* Message Processor (SDK version)
|
|
630
|
-
* ReiStandard SDK v2.0.
|
|
645
|
+
* ReiStandard SDK v2.0.1
|
|
631
646
|
*
|
|
632
647
|
* Handles single message content generation and Web Push delivery.
|
|
633
648
|
* Receives its dependencies (encryption helpers, webpush, VAPID config)
|
|
@@ -824,32 +839,110 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
824
839
|
* @private
|
|
825
840
|
*/
|
|
826
841
|
async function _callAI(payload) {
|
|
827
|
-
const
|
|
842
|
+
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
843
|
+
const requestBody = buildAiRequestBody(payload);
|
|
844
|
+
|
|
845
|
+
const aiResponse = await fetch(normalizedApiUrl, {
|
|
828
846
|
method: 'POST',
|
|
829
847
|
headers: {
|
|
830
848
|
'Content-Type': 'application/json',
|
|
831
849
|
'Authorization': `Bearer ${payload.apiKey}`
|
|
832
850
|
},
|
|
833
|
-
body: JSON.stringify(
|
|
834
|
-
model: payload.primaryModel,
|
|
835
|
-
messages: [{ role: 'user', content: payload.completePrompt }],
|
|
836
|
-
max_tokens: 500,
|
|
837
|
-
temperature: 0.8
|
|
838
|
-
}),
|
|
851
|
+
body: JSON.stringify(requestBody),
|
|
839
852
|
signal: AbortSignal.timeout(300000)
|
|
840
853
|
});
|
|
841
854
|
|
|
842
855
|
if (!aiResponse.ok) {
|
|
843
|
-
|
|
856
|
+
if (aiResponse.status === 405) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`AI API error: 405 Method Not Allowed. ` +
|
|
859
|
+
`apiUrl must point to a full chat endpoint (for example: /chat/completions). ` +
|
|
860
|
+
`Received: ${normalizedApiUrl}`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
throw new Error(
|
|
865
|
+
`AI API error: ${aiResponse.status} ${aiResponse.statusText || 'Unknown Error'}. ` +
|
|
866
|
+
`Request URL: ${normalizedApiUrl}`
|
|
867
|
+
);
|
|
844
868
|
}
|
|
845
869
|
|
|
846
870
|
const aiData = await aiResponse.json();
|
|
847
|
-
|
|
871
|
+
const content = aiData?.choices?.[0]?.message?.content;
|
|
872
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
873
|
+
throw new Error('AI API error: response missing choices[0].message.content');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return content.trim();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Build OpenAI-compatible request body.
|
|
881
|
+
*
|
|
882
|
+
* `max_tokens` is optional:
|
|
883
|
+
* - include it only when payload.maxTokens is provided
|
|
884
|
+
* - omit it when payload.maxTokens is undefined / null
|
|
885
|
+
*
|
|
886
|
+
* @param {Object} payload
|
|
887
|
+
* @returns {Object}
|
|
888
|
+
*/
|
|
889
|
+
function buildAiRequestBody(payload) {
|
|
890
|
+
const requestBody = {
|
|
891
|
+
model: payload.primaryModel,
|
|
892
|
+
messages: [{ role: 'user', content: payload.completePrompt }],
|
|
893
|
+
temperature: 0.8
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
if (payload.maxTokens === undefined || payload.maxTokens === null) {
|
|
897
|
+
return requestBody;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
901
|
+
throw new Error('Invalid maxTokens: maxTokens must be a positive integer when provided.');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
requestBody.max_tokens = payload.maxTokens;
|
|
905
|
+
return requestBody;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Normalize AI API URL with minimal, non-breaking rules:
|
|
910
|
+
* - trim whitespace
|
|
911
|
+
* - remove redundant trailing slashes from pathname
|
|
912
|
+
* - keep query string untouched
|
|
913
|
+
*
|
|
914
|
+
* Note: This function does NOT auto-append /v1 or /chat/completions.
|
|
915
|
+
*
|
|
916
|
+
* @param {string} apiUrl
|
|
917
|
+
* @returns {string}
|
|
918
|
+
*/
|
|
919
|
+
function normalizeAiApiUrl(apiUrl) {
|
|
920
|
+
if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
'Invalid apiUrl: apiUrl is required. ' +
|
|
923
|
+
'Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions).'
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const trimmedApiUrl = apiUrl.trim();
|
|
928
|
+
let parsedUrl;
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
parsedUrl = new URL(trimmedApiUrl);
|
|
932
|
+
} catch {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Invalid apiUrl: "${apiUrl}". ` +
|
|
935
|
+
'Please provide a valid absolute URL that points to a full chat endpoint.'
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/';
|
|
940
|
+
return parsedUrl.toString();
|
|
848
941
|
}
|
|
849
942
|
|
|
850
943
|
/**
|
|
851
944
|
* Handler: schedule-message
|
|
852
|
-
* ReiStandard SDK v2.0.
|
|
945
|
+
* ReiStandard SDK v2.0.1
|
|
853
946
|
*
|
|
854
947
|
* @param {Object} ctx - Server context.
|
|
855
948
|
* @returns {{ POST: function }}
|
|
@@ -933,6 +1026,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
933
1026
|
apiKey: payload.apiKey || null,
|
|
934
1027
|
primaryModel: payload.primaryModel || null,
|
|
935
1028
|
completePrompt: payload.completePrompt || null,
|
|
1029
|
+
maxTokens: payload.maxTokens ?? null,
|
|
936
1030
|
pushSubscription: payload.pushSubscription,
|
|
937
1031
|
metadata: payload.metadata || {}
|
|
938
1032
|
};
|
|
@@ -1046,7 +1140,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1046
1140
|
|
|
1047
1141
|
/**
|
|
1048
1142
|
* Handler: send-notifications
|
|
1049
|
-
* ReiStandard SDK v2.0.
|
|
1143
|
+
* ReiStandard SDK v2.0.1
|
|
1050
1144
|
*
|
|
1051
1145
|
* @param {Object} ctx - Server context.
|
|
1052
1146
|
* @returns {{ POST: function }}
|
|
@@ -1236,7 +1330,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
1236
1330
|
|
|
1237
1331
|
/**
|
|
1238
1332
|
* Handler: update-message
|
|
1239
|
-
* ReiStandard SDK v2.0.
|
|
1333
|
+
* ReiStandard SDK v2.0.1
|
|
1240
1334
|
*
|
|
1241
1335
|
* @param {Object} ctx - Server context.
|
|
1242
1336
|
* @returns {{ PUT: function }}
|
|
@@ -1306,6 +1400,14 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1306
1400
|
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['recurrenceType'] } } } };
|
|
1307
1401
|
}
|
|
1308
1402
|
|
|
1403
|
+
if (
|
|
1404
|
+
Object.prototype.hasOwnProperty.call(updates, 'maxTokens') &&
|
|
1405
|
+
updates.maxTokens !== null &&
|
|
1406
|
+
(!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)
|
|
1407
|
+
) {
|
|
1408
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1309
1411
|
// Fetch existing task
|
|
1310
1412
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
1311
1413
|
|
|
@@ -1325,7 +1427,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1325
1427
|
...(updates.userMessage && { userMessage: updates.userMessage }),
|
|
1326
1428
|
...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
|
|
1327
1429
|
...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
|
|
1328
|
-
...(updates.metadata && { metadata: updates.metadata })
|
|
1430
|
+
...(updates.metadata && { metadata: updates.metadata }),
|
|
1431
|
+
...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
|
|
1329
1432
|
};
|
|
1330
1433
|
|
|
1331
1434
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
@@ -1355,7 +1458,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1355
1458
|
|
|
1356
1459
|
/**
|
|
1357
1460
|
* Handler: cancel-message
|
|
1358
|
-
* ReiStandard SDK v2.0.
|
|
1461
|
+
* ReiStandard SDK v2.0.1
|
|
1359
1462
|
*
|
|
1360
1463
|
* @param {Object} ctx - Server context.
|
|
1361
1464
|
* @returns {{ DELETE: function }}
|
|
@@ -1408,7 +1511,7 @@ function createCancelMessageHandler(ctx) {
|
|
|
1408
1511
|
|
|
1409
1512
|
/**
|
|
1410
1513
|
* Handler: messages
|
|
1411
|
-
* ReiStandard SDK v2.0.
|
|
1514
|
+
* ReiStandard SDK v2.0.1
|
|
1412
1515
|
*
|
|
1413
1516
|
* @param {Object} ctx - Server context.
|
|
1414
1517
|
* @returns {{ GET: function }}
|
|
@@ -1696,8 +1799,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1696
1799
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1697
1800
|
const expectedSignature = sign(signingInput, secret);
|
|
1698
1801
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1802
|
+
// Compare raw HMAC bytes (decoded from base64url) rather than the
|
|
1803
|
+
// encoded character bytes — semantically clearer and avoids the
|
|
1804
|
+
// implicit-UTF-8 default of Buffer.from(string).
|
|
1805
|
+
let receivedBuffer;
|
|
1806
|
+
let expectedBuffer;
|
|
1807
|
+
try {
|
|
1808
|
+
receivedBuffer = base64UrlDecode(receivedSignature);
|
|
1809
|
+
expectedBuffer = base64UrlDecode(expectedSignature);
|
|
1810
|
+
} catch {
|
|
1811
|
+
throw new Error('INVALID_TENANT_AUTH');
|
|
1812
|
+
}
|
|
1701
1813
|
if (
|
|
1702
1814
|
receivedBuffer.length !== expectedBuffer.length ||
|
|
1703
1815
|
!timingSafeEqual(receivedBuffer, expectedBuffer)
|
|
@@ -1911,7 +2023,7 @@ function createTenantContextManager(options) {
|
|
|
1911
2023
|
|
|
1912
2024
|
/**
|
|
1913
2025
|
* ReiStandard Server SDK Entry Point
|
|
1914
|
-
* v2.0.
|
|
2026
|
+
* v2.0.1
|
|
1915
2027
|
*
|
|
1916
2028
|
* Usage:
|
|
1917
2029
|
* import { createReiServer } from '@rei-standard/amsg-server';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID,
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Adapter Factory
|
|
5
|
-
* ReiStandard SDK v2.0.
|
|
5
|
+
* ReiStandard SDK v2.0.1
|
|
6
6
|
*
|
|
7
7
|
* Creates a database adapter instance based on the supplied configuration.
|
|
8
8
|
*
|
|
@@ -35,12 +35,12 @@ async function createAdapter(config) {
|
|
|
35
35
|
|
|
36
36
|
switch (config.driver) {
|
|
37
37
|
case 'neon': {
|
|
38
|
-
const { NeonAdapter } = await import('./neon-
|
|
38
|
+
const { NeonAdapter } = await import('./neon-DYvGnCzx.d.ts');
|
|
39
39
|
return new NeonAdapter(config.connectionString);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
case 'pg': {
|
|
43
|
-
const { PgAdapter } = await import('./pg-
|
|
43
|
+
const { PgAdapter } = await import('./pg-Du-pN_UT.d.ts');
|
|
44
44
|
return new PgAdapter(config.connectionString);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -54,7 +54,7 @@ async function createAdapter(config) {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Validation utility library (SDK version)
|
|
57
|
-
* ReiStandard SDK v2.0.
|
|
57
|
+
* ReiStandard SDK v2.0.1
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -132,6 +132,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
132
132
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['recurrenceType'] } };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
if (
|
|
136
|
+
payload.maxTokens !== undefined &&
|
|
137
|
+
payload.maxTokens !== null &&
|
|
138
|
+
(!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)
|
|
139
|
+
) {
|
|
140
|
+
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['maxTokens'] } };
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
if (payload.messageType === 'fixed') {
|
|
136
144
|
if (!payload.userMessage) {
|
|
137
145
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { missingFields: ['userMessage (required for fixed type)'] } };
|
|
@@ -166,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
166
174
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
167
175
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
// messageSubtype is a free-form string tag forwarded to SW-side push payload
|
|
178
|
+
// for classification. Only the type is enforced; the taxonomy is the
|
|
179
|
+
// consumer's call (previously the enum was chat/forum/moment).
|
|
180
|
+
if (
|
|
181
|
+
payload.messageSubtype !== undefined &&
|
|
182
|
+
payload.messageSubtype !== null &&
|
|
183
|
+
typeof payload.messageSubtype !== 'string'
|
|
184
|
+
) {
|
|
170
185
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
|
|
171
186
|
}
|
|
172
187
|
|
|
@@ -320,7 +335,7 @@ function getHeader(headers = {}, name) {
|
|
|
320
335
|
|
|
321
336
|
/**
|
|
322
337
|
* Handler: init-tenant
|
|
323
|
-
* ReiStandard SDK v2.0.
|
|
338
|
+
* ReiStandard SDK v2.0.1
|
|
324
339
|
*
|
|
325
340
|
* @param {Object} ctx - Server context.
|
|
326
341
|
* @returns {{ POST: function }}
|
|
@@ -456,7 +471,7 @@ function createInitTenantHandler(ctx) {
|
|
|
456
471
|
|
|
457
472
|
/**
|
|
458
473
|
* Encryption utility library (SDK version)
|
|
459
|
-
* ReiStandard SDK v2.0.
|
|
474
|
+
* ReiStandard SDK v2.0.1
|
|
460
475
|
*
|
|
461
476
|
* Wraps AES-256-GCM operations for request/response and storage encryption.
|
|
462
477
|
*/
|
|
@@ -558,7 +573,7 @@ function decryptFromStorage(encryptedText, encryptionKey) {
|
|
|
558
573
|
|
|
559
574
|
/**
|
|
560
575
|
* Handler: get-user-key
|
|
561
|
-
* ReiStandard SDK v2.0.
|
|
576
|
+
* ReiStandard SDK v2.0.1
|
|
562
577
|
*
|
|
563
578
|
* @param {Object} ctx - Server context.
|
|
564
579
|
* @returns {{ GET: function }}
|
|
@@ -627,7 +642,7 @@ function isUniqueViolation(error) {
|
|
|
627
642
|
|
|
628
643
|
/**
|
|
629
644
|
* Message Processor (SDK version)
|
|
630
|
-
* ReiStandard SDK v2.0.
|
|
645
|
+
* ReiStandard SDK v2.0.1
|
|
631
646
|
*
|
|
632
647
|
* Handles single message content generation and Web Push delivery.
|
|
633
648
|
* Receives its dependencies (encryption helpers, webpush, VAPID config)
|
|
@@ -824,32 +839,110 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
824
839
|
* @private
|
|
825
840
|
*/
|
|
826
841
|
async function _callAI(payload) {
|
|
827
|
-
const
|
|
842
|
+
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
843
|
+
const requestBody = buildAiRequestBody(payload);
|
|
844
|
+
|
|
845
|
+
const aiResponse = await fetch(normalizedApiUrl, {
|
|
828
846
|
method: 'POST',
|
|
829
847
|
headers: {
|
|
830
848
|
'Content-Type': 'application/json',
|
|
831
849
|
'Authorization': `Bearer ${payload.apiKey}`
|
|
832
850
|
},
|
|
833
|
-
body: JSON.stringify(
|
|
834
|
-
model: payload.primaryModel,
|
|
835
|
-
messages: [{ role: 'user', content: payload.completePrompt }],
|
|
836
|
-
max_tokens: 500,
|
|
837
|
-
temperature: 0.8
|
|
838
|
-
}),
|
|
851
|
+
body: JSON.stringify(requestBody),
|
|
839
852
|
signal: AbortSignal.timeout(300000)
|
|
840
853
|
});
|
|
841
854
|
|
|
842
855
|
if (!aiResponse.ok) {
|
|
843
|
-
|
|
856
|
+
if (aiResponse.status === 405) {
|
|
857
|
+
throw new Error(
|
|
858
|
+
`AI API error: 405 Method Not Allowed. ` +
|
|
859
|
+
`apiUrl must point to a full chat endpoint (for example: /chat/completions). ` +
|
|
860
|
+
`Received: ${normalizedApiUrl}`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
throw new Error(
|
|
865
|
+
`AI API error: ${aiResponse.status} ${aiResponse.statusText || 'Unknown Error'}. ` +
|
|
866
|
+
`Request URL: ${normalizedApiUrl}`
|
|
867
|
+
);
|
|
844
868
|
}
|
|
845
869
|
|
|
846
870
|
const aiData = await aiResponse.json();
|
|
847
|
-
|
|
871
|
+
const content = aiData?.choices?.[0]?.message?.content;
|
|
872
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
873
|
+
throw new Error('AI API error: response missing choices[0].message.content');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return content.trim();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Build OpenAI-compatible request body.
|
|
881
|
+
*
|
|
882
|
+
* `max_tokens` is optional:
|
|
883
|
+
* - include it only when payload.maxTokens is provided
|
|
884
|
+
* - omit it when payload.maxTokens is undefined / null
|
|
885
|
+
*
|
|
886
|
+
* @param {Object} payload
|
|
887
|
+
* @returns {Object}
|
|
888
|
+
*/
|
|
889
|
+
function buildAiRequestBody(payload) {
|
|
890
|
+
const requestBody = {
|
|
891
|
+
model: payload.primaryModel,
|
|
892
|
+
messages: [{ role: 'user', content: payload.completePrompt }],
|
|
893
|
+
temperature: 0.8
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
if (payload.maxTokens === undefined || payload.maxTokens === null) {
|
|
897
|
+
return requestBody;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
901
|
+
throw new Error('Invalid maxTokens: maxTokens must be a positive integer when provided.');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
requestBody.max_tokens = payload.maxTokens;
|
|
905
|
+
return requestBody;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Normalize AI API URL with minimal, non-breaking rules:
|
|
910
|
+
* - trim whitespace
|
|
911
|
+
* - remove redundant trailing slashes from pathname
|
|
912
|
+
* - keep query string untouched
|
|
913
|
+
*
|
|
914
|
+
* Note: This function does NOT auto-append /v1 or /chat/completions.
|
|
915
|
+
*
|
|
916
|
+
* @param {string} apiUrl
|
|
917
|
+
* @returns {string}
|
|
918
|
+
*/
|
|
919
|
+
function normalizeAiApiUrl(apiUrl) {
|
|
920
|
+
if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
'Invalid apiUrl: apiUrl is required. ' +
|
|
923
|
+
'Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions).'
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const trimmedApiUrl = apiUrl.trim();
|
|
928
|
+
let parsedUrl;
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
parsedUrl = new URL(trimmedApiUrl);
|
|
932
|
+
} catch {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Invalid apiUrl: "${apiUrl}". ` +
|
|
935
|
+
'Please provide a valid absolute URL that points to a full chat endpoint.'
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/';
|
|
940
|
+
return parsedUrl.toString();
|
|
848
941
|
}
|
|
849
942
|
|
|
850
943
|
/**
|
|
851
944
|
* Handler: schedule-message
|
|
852
|
-
* ReiStandard SDK v2.0.
|
|
945
|
+
* ReiStandard SDK v2.0.1
|
|
853
946
|
*
|
|
854
947
|
* @param {Object} ctx - Server context.
|
|
855
948
|
* @returns {{ POST: function }}
|
|
@@ -933,6 +1026,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
933
1026
|
apiKey: payload.apiKey || null,
|
|
934
1027
|
primaryModel: payload.primaryModel || null,
|
|
935
1028
|
completePrompt: payload.completePrompt || null,
|
|
1029
|
+
maxTokens: payload.maxTokens ?? null,
|
|
936
1030
|
pushSubscription: payload.pushSubscription,
|
|
937
1031
|
metadata: payload.metadata || {}
|
|
938
1032
|
};
|
|
@@ -1046,7 +1140,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1046
1140
|
|
|
1047
1141
|
/**
|
|
1048
1142
|
* Handler: send-notifications
|
|
1049
|
-
* ReiStandard SDK v2.0.
|
|
1143
|
+
* ReiStandard SDK v2.0.1
|
|
1050
1144
|
*
|
|
1051
1145
|
* @param {Object} ctx - Server context.
|
|
1052
1146
|
* @returns {{ POST: function }}
|
|
@@ -1236,7 +1330,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
1236
1330
|
|
|
1237
1331
|
/**
|
|
1238
1332
|
* Handler: update-message
|
|
1239
|
-
* ReiStandard SDK v2.0.
|
|
1333
|
+
* ReiStandard SDK v2.0.1
|
|
1240
1334
|
*
|
|
1241
1335
|
* @param {Object} ctx - Server context.
|
|
1242
1336
|
* @returns {{ PUT: function }}
|
|
@@ -1306,6 +1400,14 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1306
1400
|
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['recurrenceType'] } } } };
|
|
1307
1401
|
}
|
|
1308
1402
|
|
|
1403
|
+
if (
|
|
1404
|
+
Object.prototype.hasOwnProperty.call(updates, 'maxTokens') &&
|
|
1405
|
+
updates.maxTokens !== null &&
|
|
1406
|
+
(!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)
|
|
1407
|
+
) {
|
|
1408
|
+
return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1309
1411
|
// Fetch existing task
|
|
1310
1412
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
1311
1413
|
|
|
@@ -1325,7 +1427,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1325
1427
|
...(updates.userMessage && { userMessage: updates.userMessage }),
|
|
1326
1428
|
...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
|
|
1327
1429
|
...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
|
|
1328
|
-
...(updates.metadata && { metadata: updates.metadata })
|
|
1430
|
+
...(updates.metadata && { metadata: updates.metadata }),
|
|
1431
|
+
...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
|
|
1329
1432
|
};
|
|
1330
1433
|
|
|
1331
1434
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
@@ -1355,7 +1458,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1355
1458
|
|
|
1356
1459
|
/**
|
|
1357
1460
|
* Handler: cancel-message
|
|
1358
|
-
* ReiStandard SDK v2.0.
|
|
1461
|
+
* ReiStandard SDK v2.0.1
|
|
1359
1462
|
*
|
|
1360
1463
|
* @param {Object} ctx - Server context.
|
|
1361
1464
|
* @returns {{ DELETE: function }}
|
|
@@ -1408,7 +1511,7 @@ function createCancelMessageHandler(ctx) {
|
|
|
1408
1511
|
|
|
1409
1512
|
/**
|
|
1410
1513
|
* Handler: messages
|
|
1411
|
-
* ReiStandard SDK v2.0.
|
|
1514
|
+
* ReiStandard SDK v2.0.1
|
|
1412
1515
|
*
|
|
1413
1516
|
* @param {Object} ctx - Server context.
|
|
1414
1517
|
* @returns {{ GET: function }}
|
|
@@ -1696,8 +1799,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1696
1799
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1697
1800
|
const expectedSignature = sign(signingInput, secret);
|
|
1698
1801
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1802
|
+
// Compare raw HMAC bytes (decoded from base64url) rather than the
|
|
1803
|
+
// encoded character bytes — semantically clearer and avoids the
|
|
1804
|
+
// implicit-UTF-8 default of Buffer.from(string).
|
|
1805
|
+
let receivedBuffer;
|
|
1806
|
+
let expectedBuffer;
|
|
1807
|
+
try {
|
|
1808
|
+
receivedBuffer = base64UrlDecode(receivedSignature);
|
|
1809
|
+
expectedBuffer = base64UrlDecode(expectedSignature);
|
|
1810
|
+
} catch {
|
|
1811
|
+
throw new Error('INVALID_TENANT_AUTH');
|
|
1812
|
+
}
|
|
1701
1813
|
if (
|
|
1702
1814
|
receivedBuffer.length !== expectedBuffer.length ||
|
|
1703
1815
|
!timingSafeEqual(receivedBuffer, expectedBuffer)
|
|
@@ -1911,7 +2023,7 @@ function createTenantContextManager(options) {
|
|
|
1911
2023
|
|
|
1912
2024
|
/**
|
|
1913
2025
|
* ReiStandard Server SDK Entry Point
|
|
1914
|
-
* v2.0.
|
|
2026
|
+
* v2.0.1
|
|
1915
2027
|
*
|
|
1916
2028
|
* Usage:
|
|
1917
2029
|
* import { createReiServer } from '@rei-standard/amsg-server';
|
package/dist/index.mjs
CHANGED
|
@@ -66,6 +66,9 @@ function validateScheduleMessagePayload(payload) {
|
|
|
66
66
|
if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
|
|
67
67
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
|
|
68
68
|
}
|
|
69
|
+
if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
|
|
70
|
+
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
|
|
71
|
+
}
|
|
69
72
|
if (payload.messageType === "fixed") {
|
|
70
73
|
if (!payload.userMessage) {
|
|
71
74
|
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)"] } };
|
|
@@ -97,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
|
|
|
97
100
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
98
101
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
|
|
99
102
|
}
|
|
100
|
-
if (payload.messageSubtype &&
|
|
103
|
+
if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
|
|
101
104
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
|
|
102
105
|
}
|
|
103
106
|
return { valid: true };
|
|
@@ -505,25 +508,66 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
async function _callAI(payload) {
|
|
508
|
-
const
|
|
511
|
+
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
512
|
+
const requestBody = buildAiRequestBody(payload);
|
|
513
|
+
const aiResponse = await fetch(normalizedApiUrl, {
|
|
509
514
|
method: "POST",
|
|
510
515
|
headers: {
|
|
511
516
|
"Content-Type": "application/json",
|
|
512
517
|
"Authorization": `Bearer ${payload.apiKey}`
|
|
513
518
|
},
|
|
514
|
-
body: JSON.stringify(
|
|
515
|
-
model: payload.primaryModel,
|
|
516
|
-
messages: [{ role: "user", content: payload.completePrompt }],
|
|
517
|
-
max_tokens: 500,
|
|
518
|
-
temperature: 0.8
|
|
519
|
-
}),
|
|
519
|
+
body: JSON.stringify(requestBody),
|
|
520
520
|
signal: AbortSignal.timeout(3e5)
|
|
521
521
|
});
|
|
522
522
|
if (!aiResponse.ok) {
|
|
523
|
-
|
|
523
|
+
if (aiResponse.status === 405) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(
|
|
529
|
+
`AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
|
|
530
|
+
);
|
|
524
531
|
}
|
|
525
532
|
const aiData = await aiResponse.json();
|
|
526
|
-
|
|
533
|
+
const content = aiData?.choices?.[0]?.message?.content;
|
|
534
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
535
|
+
throw new Error("AI API error: response missing choices[0].message.content");
|
|
536
|
+
}
|
|
537
|
+
return content.trim();
|
|
538
|
+
}
|
|
539
|
+
function buildAiRequestBody(payload) {
|
|
540
|
+
const requestBody = {
|
|
541
|
+
model: payload.primaryModel,
|
|
542
|
+
messages: [{ role: "user", content: payload.completePrompt }],
|
|
543
|
+
temperature: 0.8
|
|
544
|
+
};
|
|
545
|
+
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
546
|
+
return requestBody;
|
|
547
|
+
}
|
|
548
|
+
if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
|
|
549
|
+
throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
|
|
550
|
+
}
|
|
551
|
+
requestBody.max_tokens = payload.maxTokens;
|
|
552
|
+
return requestBody;
|
|
553
|
+
}
|
|
554
|
+
function normalizeAiApiUrl(apiUrl) {
|
|
555
|
+
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
"Invalid apiUrl: apiUrl is required. Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions)."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const trimmedApiUrl = apiUrl.trim();
|
|
561
|
+
let parsedUrl;
|
|
562
|
+
try {
|
|
563
|
+
parsedUrl = new URL(trimmedApiUrl);
|
|
564
|
+
} catch {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL that points to a full chat endpoint.`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
570
|
+
return parsedUrl.toString();
|
|
527
571
|
}
|
|
528
572
|
|
|
529
573
|
// src/server/handlers/schedule-message.js
|
|
@@ -591,6 +635,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
591
635
|
apiKey: payload.apiKey || null,
|
|
592
636
|
primaryModel: payload.primaryModel || null,
|
|
593
637
|
completePrompt: payload.completePrompt || null,
|
|
638
|
+
maxTokens: payload.maxTokens ?? null,
|
|
594
639
|
pushSubscription: payload.pushSubscription,
|
|
595
640
|
metadata: payload.metadata || {}
|
|
596
641
|
};
|
|
@@ -899,6 +944,9 @@ function createUpdateMessageHandler(ctx) {
|
|
|
899
944
|
if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
|
|
900
945
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
|
|
901
946
|
}
|
|
947
|
+
if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
|
|
948
|
+
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
|
|
949
|
+
}
|
|
902
950
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
903
951
|
if (!existingTask) {
|
|
904
952
|
const taskStatus = await db.getTaskStatus(taskUuid, userId);
|
|
@@ -914,7 +962,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
914
962
|
...updates.userMessage && { userMessage: updates.userMessage },
|
|
915
963
|
...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
|
|
916
964
|
...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
|
|
917
|
-
...updates.metadata && { metadata: updates.metadata }
|
|
965
|
+
...updates.metadata && { metadata: updates.metadata },
|
|
966
|
+
...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null }
|
|
918
967
|
};
|
|
919
968
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
920
969
|
const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
|
|
@@ -1186,8 +1235,14 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1186
1235
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
1187
1236
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1188
1237
|
const expectedSignature = sign(signingInput, secret);
|
|
1189
|
-
|
|
1190
|
-
|
|
1238
|
+
let receivedBuffer;
|
|
1239
|
+
let expectedBuffer;
|
|
1240
|
+
try {
|
|
1241
|
+
receivedBuffer = base64UrlDecode2(receivedSignature);
|
|
1242
|
+
expectedBuffer = base64UrlDecode2(expectedSignature);
|
|
1243
|
+
} catch {
|
|
1244
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1245
|
+
}
|
|
1191
1246
|
if (receivedBuffer.length !== expectedBuffer.length || !timingSafeEqual(receivedBuffer, expectedBuffer)) {
|
|
1192
1247
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1193
1248
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { neon } from '@neondatabase/serverless';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Neon Serverless Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.
|
|
6
|
+
* ReiStandard SDK v2.0.1
|
|
7
7
|
*
|
|
8
8
|
* @implements {import('./interface.js').DbAdapter}
|
|
9
9
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { neon } from '@neondatabase/serverless';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Neon Serverless Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.
|
|
6
|
+
* ReiStandard SDK v2.0.1
|
|
7
7
|
*
|
|
8
8
|
* @implements {import('./interface.js').DbAdapter}
|
|
9
9
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL (pg) Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.
|
|
6
|
+
* ReiStandard SDK v2.0.1
|
|
7
7
|
*
|
|
8
8
|
* Uses the standard 'pg' npm package.
|
|
9
9
|
*
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL (pg) Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.
|
|
6
|
+
* ReiStandard SDK v2.0.1
|
|
7
7
|
*
|
|
8
8
|
* Uses the standard 'pg' npm package.
|
|
9
9
|
*
|