@rei-standard/amsg-server 2.0.1 → 2.1.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 +4 -0
- package/dist/index.cjs +21 -8
- package/dist/index.d.cts +70 -12
- package/dist/index.d.ts +70 -12
- package/dist/index.mjs +19 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,10 @@ const rei = await createReiServer({
|
|
|
50
50
|
// GET /api/v1/messages -> rei.handlers.messages.GET
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## 关于 `messageType: 'instant'`
|
|
54
|
+
|
|
55
|
+
> **Note**:新代码的 instant 消息请用 [@rei-standard/amsg-instant](../instant/README.md),跳过本端点的"建任务 → 处理 → 删任务" DB 来回。本端点的 `instant` 分支为兼容保留,行为不变、不会有运行时警告。
|
|
56
|
+
|
|
53
57
|
## AI 接口 `apiUrl` 约束
|
|
54
58
|
|
|
55
59
|
当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
|
package/dist/index.cjs
CHANGED
|
@@ -100,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
|
|
|
100
100
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
101
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)"] } };
|
|
102
102
|
}
|
|
103
|
-
if (payload.messageSubtype &&
|
|
103
|
+
if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
|
|
104
104
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
|
|
105
105
|
}
|
|
106
106
|
return { valid: true };
|
|
@@ -554,7 +554,7 @@ function buildAiRequestBody(payload) {
|
|
|
554
554
|
function normalizeAiApiUrl(apiUrl) {
|
|
555
555
|
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
556
556
|
throw new Error(
|
|
557
|
-
"Invalid apiUrl: apiUrl is required. Please provide a
|
|
557
|
+
"Invalid apiUrl: apiUrl is required. Please provide a chat endpoint URL (for example: https://api.openai.com or https://api.openai.com/v1/chat/completions)."
|
|
558
558
|
);
|
|
559
559
|
}
|
|
560
560
|
const trimmedApiUrl = apiUrl.trim();
|
|
@@ -563,10 +563,17 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
563
563
|
parsedUrl = new URL(trimmedApiUrl);
|
|
564
564
|
} catch (e3) {
|
|
565
565
|
throw new Error(
|
|
566
|
-
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL
|
|
566
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
567
567
|
);
|
|
568
568
|
}
|
|
569
|
-
|
|
569
|
+
let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
570
|
+
if (/\/chat\/completions$/.test(path)) {
|
|
571
|
+
} else if (path === "/") {
|
|
572
|
+
path = "/v1/chat/completions";
|
|
573
|
+
} else if (/\/v\d+$/.test(path)) {
|
|
574
|
+
path = `${path}/chat/completions`;
|
|
575
|
+
}
|
|
576
|
+
parsedUrl.pathname = path;
|
|
570
577
|
return parsedUrl.toString();
|
|
571
578
|
}
|
|
572
579
|
|
|
@@ -1235,15 +1242,21 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1235
1242
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
1236
1243
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1237
1244
|
const expectedSignature = sign(signingInput, secret);
|
|
1238
|
-
|
|
1239
|
-
|
|
1245
|
+
let receivedBuffer;
|
|
1246
|
+
let expectedBuffer;
|
|
1247
|
+
try {
|
|
1248
|
+
receivedBuffer = base64UrlDecode2(receivedSignature);
|
|
1249
|
+
expectedBuffer = base64UrlDecode2(expectedSignature);
|
|
1250
|
+
} catch (e4) {
|
|
1251
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1252
|
+
}
|
|
1240
1253
|
if (receivedBuffer.length !== expectedBuffer.length || !_crypto.timingSafeEqual.call(void 0, receivedBuffer, expectedBuffer)) {
|
|
1241
1254
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1242
1255
|
}
|
|
1243
1256
|
let payload;
|
|
1244
1257
|
try {
|
|
1245
1258
|
payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
|
|
1246
|
-
} catch (
|
|
1259
|
+
} catch (e5) {
|
|
1247
1260
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1248
1261
|
}
|
|
1249
1262
|
if (!payload || typeof payload !== "object" || payload.v !== 1) {
|
|
@@ -1300,7 +1313,7 @@ function parseTokenFromUrl(url) {
|
|
|
1300
1313
|
try {
|
|
1301
1314
|
const parsed = new URL(url, "https://dummy.local");
|
|
1302
1315
|
return String(parsed.searchParams.get("token") || "").trim();
|
|
1303
|
-
} catch (
|
|
1316
|
+
} catch (e6) {
|
|
1304
1317
|
return "";
|
|
1305
1318
|
}
|
|
1306
1319
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -174,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
174
174
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
175
175
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
176
176
|
}
|
|
177
|
-
|
|
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
|
+
) {
|
|
178
185
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -899,12 +906,27 @@ function buildAiRequestBody(payload) {
|
|
|
899
906
|
}
|
|
900
907
|
|
|
901
908
|
/**
|
|
902
|
-
* Normalize AI API URL
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* -
|
|
909
|
+
* Normalize AI API URL for OpenAI-compatible chat endpoints.
|
|
910
|
+
*
|
|
911
|
+
* **Keep in sync** with `@rei-standard/amsg-instant`'s
|
|
912
|
+
* `src/message-processor.js` `normalizeAiApiUrl` — same rules, same
|
|
913
|
+
* tests. The two packages share this logic but each carry their own copy
|
|
914
|
+
* to avoid an architectural dependency (server should not depend on the
|
|
915
|
+
* stateless worker package).
|
|
906
916
|
*
|
|
907
|
-
*
|
|
917
|
+
* Rules (idempotent — running it twice equals running it once):
|
|
918
|
+
* - Already ends with `/chat/completions` → leave as-is.
|
|
919
|
+
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
920
|
+
* - Path ends with a version segment like `/v1`,
|
|
921
|
+
* `/v2`, … (with or without trailing slash) → append only
|
|
922
|
+
* `/chat/completions`; never doubles `/v1` for
|
|
923
|
+
* callers who already include it.
|
|
924
|
+
* - Anything else (custom path that doesn't match the
|
|
925
|
+
* OpenAI shape, e.g. `/v1/messages` for
|
|
926
|
+
* Anthropic-style proxies) → leave as-is. The caller
|
|
927
|
+
* knows their own routing.
|
|
928
|
+
*
|
|
929
|
+
* Query string is preserved verbatim.
|
|
908
930
|
*
|
|
909
931
|
* @param {string} apiUrl
|
|
910
932
|
* @returns {string}
|
|
@@ -913,7 +935,8 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
913
935
|
if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
|
|
914
936
|
throw new Error(
|
|
915
937
|
'Invalid apiUrl: apiUrl is required. ' +
|
|
916
|
-
'Please provide a
|
|
938
|
+
'Please provide a chat endpoint URL ' +
|
|
939
|
+
'(for example: https://api.openai.com or https://api.openai.com/v1/chat/completions).'
|
|
917
940
|
);
|
|
918
941
|
}
|
|
919
942
|
|
|
@@ -924,12 +947,23 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
924
947
|
parsedUrl = new URL(trimmedApiUrl);
|
|
925
948
|
} catch {
|
|
926
949
|
throw new Error(
|
|
927
|
-
`Invalid apiUrl: "${apiUrl}".
|
|
928
|
-
'Please provide a valid absolute URL that points to a full chat endpoint.'
|
|
950
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
929
951
|
);
|
|
930
952
|
}
|
|
931
953
|
|
|
932
|
-
|
|
954
|
+
let path = parsedUrl.pathname.replace(/\/+$/, '') || '/';
|
|
955
|
+
|
|
956
|
+
if (/\/chat\/completions$/.test(path)) ; else if (path === '/') {
|
|
957
|
+
// Bare host → assume OpenAI shape.
|
|
958
|
+
path = '/v1/chat/completions';
|
|
959
|
+
} else if (/\/v\d+$/.test(path)) {
|
|
960
|
+
// Path ends in `/v1`, `/v2`, … — caller already versioned the URL.
|
|
961
|
+
// Append only `/chat/completions`; never re-add `/v1`.
|
|
962
|
+
path = `${path}/chat/completions`;
|
|
963
|
+
}
|
|
964
|
+
// Any other custom path is left untouched on purpose.
|
|
965
|
+
|
|
966
|
+
parsedUrl.pathname = path;
|
|
933
967
|
return parsedUrl.toString();
|
|
934
968
|
}
|
|
935
969
|
|
|
@@ -1026,6 +1060,14 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1026
1060
|
|
|
1027
1061
|
const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1028
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
|
|
1065
|
+
* This branch is kept for backward compatibility and the existing behavior
|
|
1066
|
+
* (create task → process → delete) is unchanged. The dedicated amsg-instant
|
|
1067
|
+
* package is stateless (no DB roundtrip), deployable to Cloudflare Workers,
|
|
1068
|
+
* and locks the encryption + push-payload contract behind a single version.
|
|
1069
|
+
* See packages/rei-standard-amsg/instant/README.md.
|
|
1070
|
+
*/
|
|
1029
1071
|
// Instant type: check VAPID before creating the task to avoid orphaned rows
|
|
1030
1072
|
if (payload.messageType === 'instant') {
|
|
1031
1073
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
@@ -1079,6 +1121,13 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1079
1121
|
return { status: 500, body: { success: false, error: { code: 'TASK_CREATE_FAILED', message: '创建任务失败' } } };
|
|
1080
1122
|
}
|
|
1081
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
|
|
1126
|
+
* The "create-task → process-by-uuid → delete-task" sequence below is
|
|
1127
|
+
* preserved verbatim so existing clients keep working. New integrations
|
|
1128
|
+
* should call the dedicated amsg-instant endpoint instead — it skips this
|
|
1129
|
+
* DB round-trip entirely. See packages/rei-standard-amsg/instant/README.md.
|
|
1130
|
+
*/
|
|
1082
1131
|
// Instant type: send immediately
|
|
1083
1132
|
if (payload.messageType === 'instant') {
|
|
1084
1133
|
try {
|
|
@@ -1792,8 +1841,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1792
1841
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1793
1842
|
const expectedSignature = sign(signingInput, secret);
|
|
1794
1843
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1844
|
+
// Compare raw HMAC bytes (decoded from base64url) rather than the
|
|
1845
|
+
// encoded character bytes — semantically clearer and avoids the
|
|
1846
|
+
// implicit-UTF-8 default of Buffer.from(string).
|
|
1847
|
+
let receivedBuffer;
|
|
1848
|
+
let expectedBuffer;
|
|
1849
|
+
try {
|
|
1850
|
+
receivedBuffer = base64UrlDecode(receivedSignature);
|
|
1851
|
+
expectedBuffer = base64UrlDecode(expectedSignature);
|
|
1852
|
+
} catch {
|
|
1853
|
+
throw new Error('INVALID_TENANT_AUTH');
|
|
1854
|
+
}
|
|
1797
1855
|
if (
|
|
1798
1856
|
receivedBuffer.length !== expectedBuffer.length ||
|
|
1799
1857
|
!timingSafeEqual(receivedBuffer, expectedBuffer)
|
package/dist/index.d.ts
CHANGED
|
@@ -174,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
|
|
|
174
174
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
175
175
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
176
176
|
}
|
|
177
|
-
|
|
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
|
+
) {
|
|
178
185
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -899,12 +906,27 @@ function buildAiRequestBody(payload) {
|
|
|
899
906
|
}
|
|
900
907
|
|
|
901
908
|
/**
|
|
902
|
-
* Normalize AI API URL
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* -
|
|
909
|
+
* Normalize AI API URL for OpenAI-compatible chat endpoints.
|
|
910
|
+
*
|
|
911
|
+
* **Keep in sync** with `@rei-standard/amsg-instant`'s
|
|
912
|
+
* `src/message-processor.js` `normalizeAiApiUrl` — same rules, same
|
|
913
|
+
* tests. The two packages share this logic but each carry their own copy
|
|
914
|
+
* to avoid an architectural dependency (server should not depend on the
|
|
915
|
+
* stateless worker package).
|
|
906
916
|
*
|
|
907
|
-
*
|
|
917
|
+
* Rules (idempotent — running it twice equals running it once):
|
|
918
|
+
* - Already ends with `/chat/completions` → leave as-is.
|
|
919
|
+
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
920
|
+
* - Path ends with a version segment like `/v1`,
|
|
921
|
+
* `/v2`, … (with or without trailing slash) → append only
|
|
922
|
+
* `/chat/completions`; never doubles `/v1` for
|
|
923
|
+
* callers who already include it.
|
|
924
|
+
* - Anything else (custom path that doesn't match the
|
|
925
|
+
* OpenAI shape, e.g. `/v1/messages` for
|
|
926
|
+
* Anthropic-style proxies) → leave as-is. The caller
|
|
927
|
+
* knows their own routing.
|
|
928
|
+
*
|
|
929
|
+
* Query string is preserved verbatim.
|
|
908
930
|
*
|
|
909
931
|
* @param {string} apiUrl
|
|
910
932
|
* @returns {string}
|
|
@@ -913,7 +935,8 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
913
935
|
if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
|
|
914
936
|
throw new Error(
|
|
915
937
|
'Invalid apiUrl: apiUrl is required. ' +
|
|
916
|
-
'Please provide a
|
|
938
|
+
'Please provide a chat endpoint URL ' +
|
|
939
|
+
'(for example: https://api.openai.com or https://api.openai.com/v1/chat/completions).'
|
|
917
940
|
);
|
|
918
941
|
}
|
|
919
942
|
|
|
@@ -924,12 +947,23 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
924
947
|
parsedUrl = new URL(trimmedApiUrl);
|
|
925
948
|
} catch {
|
|
926
949
|
throw new Error(
|
|
927
|
-
`Invalid apiUrl: "${apiUrl}".
|
|
928
|
-
'Please provide a valid absolute URL that points to a full chat endpoint.'
|
|
950
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
929
951
|
);
|
|
930
952
|
}
|
|
931
953
|
|
|
932
|
-
|
|
954
|
+
let path = parsedUrl.pathname.replace(/\/+$/, '') || '/';
|
|
955
|
+
|
|
956
|
+
if (/\/chat\/completions$/.test(path)) ; else if (path === '/') {
|
|
957
|
+
// Bare host → assume OpenAI shape.
|
|
958
|
+
path = '/v1/chat/completions';
|
|
959
|
+
} else if (/\/v\d+$/.test(path)) {
|
|
960
|
+
// Path ends in `/v1`, `/v2`, … — caller already versioned the URL.
|
|
961
|
+
// Append only `/chat/completions`; never re-add `/v1`.
|
|
962
|
+
path = `${path}/chat/completions`;
|
|
963
|
+
}
|
|
964
|
+
// Any other custom path is left untouched on purpose.
|
|
965
|
+
|
|
966
|
+
parsedUrl.pathname = path;
|
|
933
967
|
return parsedUrl.toString();
|
|
934
968
|
}
|
|
935
969
|
|
|
@@ -1026,6 +1060,14 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1026
1060
|
|
|
1027
1061
|
const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1028
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
|
|
1065
|
+
* This branch is kept for backward compatibility and the existing behavior
|
|
1066
|
+
* (create task → process → delete) is unchanged. The dedicated amsg-instant
|
|
1067
|
+
* package is stateless (no DB roundtrip), deployable to Cloudflare Workers,
|
|
1068
|
+
* and locks the encryption + push-payload contract behind a single version.
|
|
1069
|
+
* See packages/rei-standard-amsg/instant/README.md.
|
|
1070
|
+
*/
|
|
1029
1071
|
// Instant type: check VAPID before creating the task to avoid orphaned rows
|
|
1030
1072
|
if (payload.messageType === 'instant') {
|
|
1031
1073
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
@@ -1079,6 +1121,13 @@ function createScheduleMessageHandler(ctx) {
|
|
|
1079
1121
|
return { status: 500, body: { success: false, error: { code: 'TASK_CREATE_FAILED', message: '创建任务失败' } } };
|
|
1080
1122
|
}
|
|
1081
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
|
|
1126
|
+
* The "create-task → process-by-uuid → delete-task" sequence below is
|
|
1127
|
+
* preserved verbatim so existing clients keep working. New integrations
|
|
1128
|
+
* should call the dedicated amsg-instant endpoint instead — it skips this
|
|
1129
|
+
* DB round-trip entirely. See packages/rei-standard-amsg/instant/README.md.
|
|
1130
|
+
*/
|
|
1082
1131
|
// Instant type: send immediately
|
|
1083
1132
|
if (payload.messageType === 'instant') {
|
|
1084
1133
|
try {
|
|
@@ -1792,8 +1841,17 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1792
1841
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1793
1842
|
const expectedSignature = sign(signingInput, secret);
|
|
1794
1843
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1844
|
+
// Compare raw HMAC bytes (decoded from base64url) rather than the
|
|
1845
|
+
// encoded character bytes — semantically clearer and avoids the
|
|
1846
|
+
// implicit-UTF-8 default of Buffer.from(string).
|
|
1847
|
+
let receivedBuffer;
|
|
1848
|
+
let expectedBuffer;
|
|
1849
|
+
try {
|
|
1850
|
+
receivedBuffer = base64UrlDecode(receivedSignature);
|
|
1851
|
+
expectedBuffer = base64UrlDecode(expectedSignature);
|
|
1852
|
+
} catch {
|
|
1853
|
+
throw new Error('INVALID_TENANT_AUTH');
|
|
1854
|
+
}
|
|
1797
1855
|
if (
|
|
1798
1856
|
receivedBuffer.length !== expectedBuffer.length ||
|
|
1799
1857
|
!timingSafeEqual(receivedBuffer, expectedBuffer)
|
package/dist/index.mjs
CHANGED
|
@@ -100,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
|
|
|
100
100
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
101
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)"] } };
|
|
102
102
|
}
|
|
103
|
-
if (payload.messageSubtype &&
|
|
103
|
+
if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
|
|
104
104
|
return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
|
|
105
105
|
}
|
|
106
106
|
return { valid: true };
|
|
@@ -554,7 +554,7 @@ function buildAiRequestBody(payload) {
|
|
|
554
554
|
function normalizeAiApiUrl(apiUrl) {
|
|
555
555
|
if (typeof apiUrl !== "string" || !apiUrl.trim()) {
|
|
556
556
|
throw new Error(
|
|
557
|
-
"Invalid apiUrl: apiUrl is required. Please provide a
|
|
557
|
+
"Invalid apiUrl: apiUrl is required. Please provide a chat endpoint URL (for example: https://api.openai.com or https://api.openai.com/v1/chat/completions)."
|
|
558
558
|
);
|
|
559
559
|
}
|
|
560
560
|
const trimmedApiUrl = apiUrl.trim();
|
|
@@ -563,10 +563,17 @@ function normalizeAiApiUrl(apiUrl) {
|
|
|
563
563
|
parsedUrl = new URL(trimmedApiUrl);
|
|
564
564
|
} catch {
|
|
565
565
|
throw new Error(
|
|
566
|
-
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL
|
|
566
|
+
`Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL.`
|
|
567
567
|
);
|
|
568
568
|
}
|
|
569
|
-
|
|
569
|
+
let path = parsedUrl.pathname.replace(/\/+$/, "") || "/";
|
|
570
|
+
if (/\/chat\/completions$/.test(path)) {
|
|
571
|
+
} else if (path === "/") {
|
|
572
|
+
path = "/v1/chat/completions";
|
|
573
|
+
} else if (/\/v\d+$/.test(path)) {
|
|
574
|
+
path = `${path}/chat/completions`;
|
|
575
|
+
}
|
|
576
|
+
parsedUrl.pathname = path;
|
|
570
577
|
return parsedUrl.toString();
|
|
571
578
|
}
|
|
572
579
|
|
|
@@ -1235,8 +1242,14 @@ function verifyTenantToken(token, secret, options = {}) {
|
|
|
1235
1242
|
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
1236
1243
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1237
1244
|
const expectedSignature = sign(signingInput, secret);
|
|
1238
|
-
|
|
1239
|
-
|
|
1245
|
+
let receivedBuffer;
|
|
1246
|
+
let expectedBuffer;
|
|
1247
|
+
try {
|
|
1248
|
+
receivedBuffer = base64UrlDecode2(receivedSignature);
|
|
1249
|
+
expectedBuffer = base64UrlDecode2(expectedSignature);
|
|
1250
|
+
} catch {
|
|
1251
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1252
|
+
}
|
|
1240
1253
|
if (receivedBuffer.length !== expectedBuffer.length || !timingSafeEqual(receivedBuffer, expectedBuffer)) {
|
|
1241
1254
|
throw new Error("INVALID_TENANT_AUTH");
|
|
1242
1255
|
}
|