@rei-standard/amsg-server 2.3.2 → 2.4.0-next.2
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 +1 -1
- package/dist/index.cjs +70 -18
- package/dist/index.d.cts +119 -35
- package/dist/index.d.ts +119 -35
- package/dist/index.mjs +68 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 业务端点统一使用 `Authorization: Bearer <tenantToken>`
|
|
10
10
|
- `send-notifications` 支持 `cronToken`(Header 或 query token)
|
|
11
11
|
|
|
12
|
-
2.2+ 的字段增量(`messages` 数组、`splitPattern`、`avatarUrl`
|
|
12
|
+
2.2+ 的字段增量(`messages` 数组、`splitPattern`、`avatarUrl` 软清空策略)在规范的 [§6.1](https://github.com/Tosd0/ReiStandard/blob/main/standards/active-messaging-api.md#61-ai-消息字段约束) / [§6.2](https://github.com/Tosd0/ReiStandard/blob/main/standards/active-messaging-api.md#62-avatarurl-软清空策略);行为已对齐 `amsg-instant`,向后兼容。
|
|
13
13
|
|
|
14
14
|
## 安装
|
|
15
15
|
|
package/dist/index.cjs
CHANGED
|
@@ -190,7 +190,8 @@ function validateScheduleMessagePayload(payload) {
|
|
|
190
190
|
}
|
|
191
191
|
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
192
192
|
if (avatarErr) {
|
|
193
|
-
|
|
193
|
+
console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
|
|
194
|
+
payload.avatarUrl = null;
|
|
194
195
|
}
|
|
195
196
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
196
197
|
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)"] } };
|
|
@@ -482,6 +483,10 @@ function isUniqueViolation(error) {
|
|
|
482
483
|
|
|
483
484
|
// src/server/lib/message-processor.js
|
|
484
485
|
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
var _amsgshared = require('@rei-standard/amsg-shared');
|
|
485
490
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
486
491
|
function splitOnceByRegex(chunk, regex) {
|
|
487
492
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
@@ -502,6 +507,22 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
502
507
|
}
|
|
503
508
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
504
509
|
}
|
|
510
|
+
function readReasoningContent(llmResponse) {
|
|
511
|
+
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
512
|
+
const choices = (
|
|
513
|
+
/** @type {{ choices?: unknown }} */
|
|
514
|
+
llmResponse.choices
|
|
515
|
+
);
|
|
516
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
517
|
+
const message = (
|
|
518
|
+
/** @type {{ message?: { reasoning_content?: unknown } }} */
|
|
519
|
+
_optionalChain([choices, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.message])
|
|
520
|
+
);
|
|
521
|
+
const raw = _optionalChain([message, 'optionalAccess', _4 => _4.reasoning_content]);
|
|
522
|
+
if (typeof raw !== "string") return null;
|
|
523
|
+
const trimmed = raw.trim();
|
|
524
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
525
|
+
}
|
|
505
526
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
506
527
|
try {
|
|
507
528
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -511,19 +532,24 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
511
532
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
512
533
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
513
534
|
let messageContent;
|
|
535
|
+
let llmResponse = null;
|
|
514
536
|
if (decryptedPayload.messageType === "fixed") {
|
|
515
537
|
messageContent = decryptedPayload.userMessage;
|
|
516
538
|
} else if (decryptedPayload.messageType === "instant") {
|
|
517
539
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
518
540
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
519
|
-
|
|
541
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
542
|
+
messageContent = aiResult.content;
|
|
543
|
+
llmResponse = aiResult.response;
|
|
520
544
|
} else if (decryptedPayload.userMessage) {
|
|
521
545
|
messageContent = decryptedPayload.userMessage;
|
|
522
546
|
} else {
|
|
523
547
|
throw new Error("Invalid instant message: no content source available");
|
|
524
548
|
}
|
|
525
549
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
526
|
-
|
|
550
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
551
|
+
messageContent = aiResult.content;
|
|
552
|
+
llmResponse = aiResult.response;
|
|
527
553
|
} else {
|
|
528
554
|
throw new Error("Invalid message configuration: no content source available");
|
|
529
555
|
}
|
|
@@ -532,23 +558,48 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
532
558
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
533
559
|
}
|
|
534
560
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
535
|
-
|
|
536
|
-
|
|
561
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${_crypto.randomUUID.call(void 0, )}`;
|
|
562
|
+
const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
|
|
563
|
+
const messageSubtype = decryptedPayload.messageSubtype || "chat";
|
|
564
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
565
|
+
const metadata = decryptedPayload.metadata || {};
|
|
566
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${_crypto.randomUUID.call(void 0, )}_instant`;
|
|
567
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
568
|
+
if (reasoning) {
|
|
569
|
+
const reasoningPush = _amsgshared.buildReasoningPush.call(void 0, {
|
|
570
|
+
messageType: decryptedPayload.messageType,
|
|
571
|
+
source,
|
|
572
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
573
|
+
sessionId,
|
|
574
|
+
reasoningContent: reasoning,
|
|
575
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
537
576
|
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
577
|
+
contactName: decryptedPayload.contactName,
|
|
578
|
+
avatarUrl,
|
|
579
|
+
messageSubtype,
|
|
580
|
+
metadata
|
|
581
|
+
});
|
|
582
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
584
|
+
}
|
|
585
|
+
for (let i = 0; i < messages.length; i++) {
|
|
586
|
+
const contentPush = _amsgshared.buildContentPush.call(void 0, {
|
|
587
|
+
messageType: decryptedPayload.messageType,
|
|
588
|
+
source,
|
|
589
|
+
messageId: `${messageIdBase}_${i}`,
|
|
590
|
+
sessionId,
|
|
538
591
|
message: messages[i],
|
|
592
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
593
|
+
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
539
594
|
contactName: decryptedPayload.contactName,
|
|
540
|
-
|
|
595
|
+
avatarUrl,
|
|
596
|
+
messageSubtype,
|
|
541
597
|
messageIndex: i + 1,
|
|
542
598
|
totalMessages: messages.length,
|
|
543
|
-
messageType: decryptedPayload.messageType,
|
|
544
|
-
messageSubtype: decryptedPayload.messageSubtype || "chat",
|
|
545
599
|
taskId: task.id || null,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
metadata: decryptedPayload.metadata || {}
|
|
550
|
-
};
|
|
551
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(notificationPayload));
|
|
600
|
+
metadata
|
|
601
|
+
});
|
|
602
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
552
603
|
if (i < messages.length - 1) {
|
|
553
604
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
554
605
|
}
|
|
@@ -643,11 +694,11 @@ async function _callAI(payload) {
|
|
|
643
694
|
);
|
|
644
695
|
}
|
|
645
696
|
const aiData = await aiResponse.json();
|
|
646
|
-
const content = _optionalChain([aiData, 'optionalAccess',
|
|
697
|
+
const content = _optionalChain([aiData, 'optionalAccess', _5 => _5.choices, 'optionalAccess', _6 => _6[0], 'optionalAccess', _7 => _7.message, 'optionalAccess', _8 => _8.content]);
|
|
647
698
|
if (typeof content !== "string" || !content.trim()) {
|
|
648
699
|
throw new Error("AI API error: response missing choices[0].message.content");
|
|
649
700
|
}
|
|
650
|
-
return content.trim();
|
|
701
|
+
return { response: aiData, content: content.trim() };
|
|
651
702
|
}
|
|
652
703
|
function buildAiRequestBody(payload) {
|
|
653
704
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
@@ -1102,7 +1153,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1102
1153
|
if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
|
|
1103
1154
|
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1104
1155
|
if (avatarErr) {
|
|
1105
|
-
|
|
1156
|
+
console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
|
|
1157
|
+
delete updates.avatarUrl;
|
|
1106
1158
|
}
|
|
1107
1159
|
}
|
|
1108
1160
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
@@ -1326,7 +1378,7 @@ async function resolveBlobStore(namespace) {
|
|
|
1326
1378
|
}
|
|
1327
1379
|
try {
|
|
1328
1380
|
const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
|
|
1329
|
-
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access',
|
|
1381
|
+
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _9 => _9.default, 'optionalAccess', _10 => _10.getStore]);
|
|
1330
1382
|
if (typeof getStore === "function") {
|
|
1331
1383
|
return getStore({ name: namespace });
|
|
1332
1384
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
+
import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Adapter Factory
|
|
@@ -315,7 +316,11 @@ function validateScheduleMessagePayload(payload) {
|
|
|
315
316
|
|
|
316
317
|
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
317
318
|
if (avatarErr) {
|
|
318
|
-
|
|
319
|
+
// Soft-strip: a bad avatarUrl (data: URI / oversized / malformed) used to
|
|
320
|
+
// 400 the whole schedule. Avatar is cosmetic — drop the field, log, and
|
|
321
|
+
// let the rest of the task ship. See standards §6.2.
|
|
322
|
+
console.warn('[amsg-server] avatarUrl 不合法,已置空:', avatarErr);
|
|
323
|
+
payload.avatarUrl = null;
|
|
319
324
|
}
|
|
320
325
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
321
326
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
@@ -793,11 +798,23 @@ function isUniqueViolation(error) {
|
|
|
793
798
|
|
|
794
799
|
/**
|
|
795
800
|
* Message Processor (SDK version)
|
|
796
|
-
* ReiStandard
|
|
801
|
+
* ReiStandard amsg-server v2.4.0
|
|
802
|
+
*
|
|
803
|
+
* Handles single message content generation and Web Push delivery for
|
|
804
|
+
* scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
|
|
805
|
+
* via-server instant path (`messageType: 'instant'`).
|
|
797
806
|
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
807
|
+
* Push wire shape comes from `@rei-standard/amsg-shared`'s
|
|
808
|
+
* discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
|
|
809
|
+
* routes on `messageKind`. Server-driven pushes always carry
|
|
810
|
+
* `source: 'instant'` (for the legacy in-server instant) or
|
|
811
|
+
* `source: 'scheduled'` (for everything else).
|
|
812
|
+
*
|
|
813
|
+
* v2.4.0: when the LLM response carries non-empty
|
|
814
|
+
* `choices[0].message.reasoning_content`, the processor now emits a
|
|
815
|
+
* standalone `ReasoningPush` **before** the `ContentPush` burst.
|
|
816
|
+
* `messagesSent` in the return value continues to reflect the sentence
|
|
817
|
+
* count only (reasoning is an auxiliary push, not a sentence).
|
|
801
818
|
*/
|
|
802
819
|
|
|
803
820
|
|
|
@@ -850,6 +867,25 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
850
867
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
851
868
|
}
|
|
852
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
872
|
+
* string, or null when absent / empty. Mirrors
|
|
873
|
+
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
874
|
+
*
|
|
875
|
+
* @param {unknown} llmResponse
|
|
876
|
+
* @returns {string | null}
|
|
877
|
+
*/
|
|
878
|
+
function readReasoningContent(llmResponse) {
|
|
879
|
+
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
880
|
+
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
881
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
882
|
+
const message = /** @type {{ message?: { reasoning_content?: unknown } }} */ (choices[0])?.message;
|
|
883
|
+
const raw = message?.reasoning_content;
|
|
884
|
+
if (typeof raw !== 'string') return null;
|
|
885
|
+
const trimmed = raw.trim();
|
|
886
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
887
|
+
}
|
|
888
|
+
|
|
853
889
|
/**
|
|
854
890
|
* @typedef {Object} ProcessorContext
|
|
855
891
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -876,14 +912,19 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
876
912
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
877
913
|
|
|
878
914
|
let messageContent;
|
|
915
|
+
/** @type {unknown} */
|
|
916
|
+
let llmResponse = null;
|
|
879
917
|
|
|
880
918
|
if (decryptedPayload.messageType === 'fixed') {
|
|
881
919
|
messageContent = decryptedPayload.userMessage;
|
|
882
920
|
|
|
883
921
|
} else if (decryptedPayload.messageType === 'instant') {
|
|
884
|
-
const hasPrompt = !!decryptedPayload.completePrompt
|
|
922
|
+
const hasPrompt = !!decryptedPayload.completePrompt
|
|
923
|
+
|| (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
|
|
885
924
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
886
|
-
|
|
925
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
926
|
+
messageContent = aiResult.content;
|
|
927
|
+
llmResponse = aiResult.response;
|
|
887
928
|
} else if (decryptedPayload.userMessage) {
|
|
888
929
|
messageContent = decryptedPayload.userMessage;
|
|
889
930
|
} else {
|
|
@@ -891,7 +932,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
891
932
|
}
|
|
892
933
|
|
|
893
934
|
} else if (decryptedPayload.messageType === 'prompted' || decryptedPayload.messageType === 'auto') {
|
|
894
|
-
|
|
935
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
936
|
+
messageContent = aiResult.content;
|
|
937
|
+
llmResponse = aiResult.response;
|
|
895
938
|
} else {
|
|
896
939
|
throw new Error('Invalid message configuration: no content source available');
|
|
897
940
|
}
|
|
@@ -908,25 +951,68 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
908
951
|
}
|
|
909
952
|
|
|
910
953
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
954
|
+
// sessionId is shared across the optional ReasoningPush and every
|
|
955
|
+
// ContentPush from this LLM round. Pin it to the task id when
|
|
956
|
+
// available (scheduled tasks) so retries reuse the same id;
|
|
957
|
+
// otherwise mint a UUID.
|
|
958
|
+
const sessionId = task.id != null
|
|
959
|
+
? `sess_task_${task.id}`
|
|
960
|
+
: `sess_${randomUUID()}`;
|
|
961
|
+
const source = decryptedPayload.messageType === 'instant' ? 'instant' : 'scheduled';
|
|
962
|
+
const messageSubtype = decryptedPayload.messageSubtype || 'chat';
|
|
963
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
964
|
+
const metadata = decryptedPayload.metadata || {};
|
|
965
|
+
|
|
966
|
+
// `messageId` format — deterministic when we have a task.id so a
|
|
967
|
+
// retry produces the same id for the same (task, sentence) pair
|
|
968
|
+
// (downstream dedupers can key on it). Falls back to a UUID for
|
|
969
|
+
// the legacy in-server instant path that has no row id.
|
|
970
|
+
const messageIdBase = task.id != null
|
|
971
|
+
? `msg_task_${task.id}`
|
|
972
|
+
: `msg_${randomUUID()}_instant`;
|
|
973
|
+
|
|
974
|
+
// ReasoningPush — auto-emitted before the content burst when the
|
|
975
|
+
// LLM response carried non-empty reasoning_content. `fixed` and
|
|
976
|
+
// explicit-userMessage paths produce no LLM response, so this
|
|
977
|
+
// block is naturally skipped for them (llmResponse stays null).
|
|
978
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
979
|
+
if (reasoning) {
|
|
980
|
+
const reasoningPush = buildReasoningPush({
|
|
981
|
+
messageType: decryptedPayload.messageType,
|
|
982
|
+
source,
|
|
983
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
984
|
+
sessionId,
|
|
985
|
+
reasoningContent: reasoning,
|
|
986
|
+
timestamp: new Date().toISOString(),
|
|
987
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
988
|
+
contactName: decryptedPayload.contactName,
|
|
989
|
+
avatarUrl,
|
|
990
|
+
messageSubtype,
|
|
991
|
+
metadata,
|
|
992
|
+
});
|
|
993
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
994
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
995
|
+
}
|
|
911
996
|
|
|
912
997
|
for (let i = 0; i < messages.length; i++) {
|
|
913
|
-
const
|
|
914
|
-
|
|
998
|
+
const contentPush = buildContentPush({
|
|
999
|
+
messageType: decryptedPayload.messageType,
|
|
1000
|
+
source,
|
|
1001
|
+
messageId: `${messageIdBase}_${i}`,
|
|
1002
|
+
sessionId,
|
|
915
1003
|
message: messages[i],
|
|
1004
|
+
timestamp: new Date().toISOString(),
|
|
1005
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
916
1006
|
contactName: decryptedPayload.contactName,
|
|
917
|
-
|
|
1007
|
+
avatarUrl,
|
|
1008
|
+
messageSubtype,
|
|
918
1009
|
messageIndex: i + 1,
|
|
919
1010
|
totalMessages: messages.length,
|
|
920
|
-
messageType: decryptedPayload.messageType,
|
|
921
|
-
messageSubtype: decryptedPayload.messageSubtype || 'chat',
|
|
922
1011
|
taskId: task.id || null,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
avatarUrl: decryptedPayload.avatarUrl || null,
|
|
926
|
-
metadata: decryptedPayload.metadata || {}
|
|
927
|
-
};
|
|
1012
|
+
metadata,
|
|
1013
|
+
});
|
|
928
1014
|
|
|
929
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(
|
|
1015
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
930
1016
|
|
|
931
1017
|
if (i < messages.length - 1) {
|
|
932
1018
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
@@ -1030,7 +1116,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1030
1116
|
|
|
1031
1117
|
/**
|
|
1032
1118
|
* Call an OpenAI-compatible API.
|
|
1119
|
+
*
|
|
1120
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
1121
|
+
* `content` string. Callers that only need the text can ignore
|
|
1122
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
1123
|
+
* read from `response.choices[0].message`.
|
|
1124
|
+
*
|
|
1033
1125
|
* @private
|
|
1126
|
+
* @param {Object} payload
|
|
1127
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
1034
1128
|
*/
|
|
1035
1129
|
async function _callAI(payload) {
|
|
1036
1130
|
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
@@ -1067,7 +1161,7 @@ async function _callAI(payload) {
|
|
|
1067
1161
|
throw new Error('AI API error: response missing choices[0].message.content');
|
|
1068
1162
|
}
|
|
1069
1163
|
|
|
1070
|
-
return content.trim();
|
|
1164
|
+
return { response: aiData, content: content.trim() };
|
|
1071
1165
|
}
|
|
1072
1166
|
|
|
1073
1167
|
/**
|
|
@@ -1125,20 +1219,6 @@ function buildAiRequestBody(payload) {
|
|
|
1125
1219
|
* to avoid an architectural dependency (server should not depend on the
|
|
1126
1220
|
* stateless worker package).
|
|
1127
1221
|
*
|
|
1128
|
-
* Rules (idempotent — running it twice equals running it once):
|
|
1129
|
-
* - Already ends with `/chat/completions` → leave as-is.
|
|
1130
|
-
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
1131
|
-
* - Path ends with a version segment like `/v1`,
|
|
1132
|
-
* `/v2`, … (with or without trailing slash) → append only
|
|
1133
|
-
* `/chat/completions`; never doubles `/v1` for
|
|
1134
|
-
* callers who already include it.
|
|
1135
|
-
* - Anything else (custom path that doesn't match the
|
|
1136
|
-
* OpenAI shape, e.g. `/v1/messages` for
|
|
1137
|
-
* Anthropic-style proxies) → leave as-is. The caller
|
|
1138
|
-
* knows their own routing.
|
|
1139
|
-
*
|
|
1140
|
-
* Query string is preserved verbatim.
|
|
1141
|
-
*
|
|
1142
1222
|
* @param {string} apiUrl
|
|
1143
1223
|
* @returns {string}
|
|
1144
1224
|
*/
|
|
@@ -1702,7 +1782,11 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1702
1782
|
if (Object.prototype.hasOwnProperty.call(updates, 'avatarUrl')) {
|
|
1703
1783
|
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1704
1784
|
if (avatarErr) {
|
|
1705
|
-
|
|
1785
|
+
// Soft-strip: drop the bad avatarUrl from the patch (keeps the
|
|
1786
|
+
// existing stored avatar untouched) and continue applying the rest
|
|
1787
|
+
// of the update. See standards §6.2.
|
|
1788
|
+
console.warn('[amsg-server] update-message avatarUrl 不合法,已忽略:', avatarErr);
|
|
1789
|
+
delete updates.avatarUrl;
|
|
1706
1790
|
}
|
|
1707
1791
|
}
|
|
1708
1792
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
+
import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Adapter Factory
|
|
@@ -315,7 +316,11 @@ function validateScheduleMessagePayload(payload) {
|
|
|
315
316
|
|
|
316
317
|
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
317
318
|
if (avatarErr) {
|
|
318
|
-
|
|
319
|
+
// Soft-strip: a bad avatarUrl (data: URI / oversized / malformed) used to
|
|
320
|
+
// 400 the whole schedule. Avatar is cosmetic — drop the field, log, and
|
|
321
|
+
// let the rest of the task ship. See standards §6.2.
|
|
322
|
+
console.warn('[amsg-server] avatarUrl 不合法,已置空:', avatarErr);
|
|
323
|
+
payload.avatarUrl = null;
|
|
319
324
|
}
|
|
320
325
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
321
326
|
return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
|
|
@@ -793,11 +798,23 @@ function isUniqueViolation(error) {
|
|
|
793
798
|
|
|
794
799
|
/**
|
|
795
800
|
* Message Processor (SDK version)
|
|
796
|
-
* ReiStandard
|
|
801
|
+
* ReiStandard amsg-server v2.4.0
|
|
802
|
+
*
|
|
803
|
+
* Handles single message content generation and Web Push delivery for
|
|
804
|
+
* scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
|
|
805
|
+
* via-server instant path (`messageType: 'instant'`).
|
|
797
806
|
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
807
|
+
* Push wire shape comes from `@rei-standard/amsg-shared`'s
|
|
808
|
+
* discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
|
|
809
|
+
* routes on `messageKind`. Server-driven pushes always carry
|
|
810
|
+
* `source: 'instant'` (for the legacy in-server instant) or
|
|
811
|
+
* `source: 'scheduled'` (for everything else).
|
|
812
|
+
*
|
|
813
|
+
* v2.4.0: when the LLM response carries non-empty
|
|
814
|
+
* `choices[0].message.reasoning_content`, the processor now emits a
|
|
815
|
+
* standalone `ReasoningPush` **before** the `ContentPush` burst.
|
|
816
|
+
* `messagesSent` in the return value continues to reflect the sentence
|
|
817
|
+
* count only (reasoning is an auxiliary push, not a sentence).
|
|
801
818
|
*/
|
|
802
819
|
|
|
803
820
|
|
|
@@ -850,6 +867,25 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
850
867
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
851
868
|
}
|
|
852
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
872
|
+
* string, or null when absent / empty. Mirrors
|
|
873
|
+
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
874
|
+
*
|
|
875
|
+
* @param {unknown} llmResponse
|
|
876
|
+
* @returns {string | null}
|
|
877
|
+
*/
|
|
878
|
+
function readReasoningContent(llmResponse) {
|
|
879
|
+
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
880
|
+
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
881
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
882
|
+
const message = /** @type {{ message?: { reasoning_content?: unknown } }} */ (choices[0])?.message;
|
|
883
|
+
const raw = message?.reasoning_content;
|
|
884
|
+
if (typeof raw !== 'string') return null;
|
|
885
|
+
const trimmed = raw.trim();
|
|
886
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
887
|
+
}
|
|
888
|
+
|
|
853
889
|
/**
|
|
854
890
|
* @typedef {Object} ProcessorContext
|
|
855
891
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -876,14 +912,19 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
876
912
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
877
913
|
|
|
878
914
|
let messageContent;
|
|
915
|
+
/** @type {unknown} */
|
|
916
|
+
let llmResponse = null;
|
|
879
917
|
|
|
880
918
|
if (decryptedPayload.messageType === 'fixed') {
|
|
881
919
|
messageContent = decryptedPayload.userMessage;
|
|
882
920
|
|
|
883
921
|
} else if (decryptedPayload.messageType === 'instant') {
|
|
884
|
-
const hasPrompt = !!decryptedPayload.completePrompt
|
|
922
|
+
const hasPrompt = !!decryptedPayload.completePrompt
|
|
923
|
+
|| (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
|
|
885
924
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
886
|
-
|
|
925
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
926
|
+
messageContent = aiResult.content;
|
|
927
|
+
llmResponse = aiResult.response;
|
|
887
928
|
} else if (decryptedPayload.userMessage) {
|
|
888
929
|
messageContent = decryptedPayload.userMessage;
|
|
889
930
|
} else {
|
|
@@ -891,7 +932,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
891
932
|
}
|
|
892
933
|
|
|
893
934
|
} else if (decryptedPayload.messageType === 'prompted' || decryptedPayload.messageType === 'auto') {
|
|
894
|
-
|
|
935
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
936
|
+
messageContent = aiResult.content;
|
|
937
|
+
llmResponse = aiResult.response;
|
|
895
938
|
} else {
|
|
896
939
|
throw new Error('Invalid message configuration: no content source available');
|
|
897
940
|
}
|
|
@@ -908,25 +951,68 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
908
951
|
}
|
|
909
952
|
|
|
910
953
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
954
|
+
// sessionId is shared across the optional ReasoningPush and every
|
|
955
|
+
// ContentPush from this LLM round. Pin it to the task id when
|
|
956
|
+
// available (scheduled tasks) so retries reuse the same id;
|
|
957
|
+
// otherwise mint a UUID.
|
|
958
|
+
const sessionId = task.id != null
|
|
959
|
+
? `sess_task_${task.id}`
|
|
960
|
+
: `sess_${randomUUID()}`;
|
|
961
|
+
const source = decryptedPayload.messageType === 'instant' ? 'instant' : 'scheduled';
|
|
962
|
+
const messageSubtype = decryptedPayload.messageSubtype || 'chat';
|
|
963
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
964
|
+
const metadata = decryptedPayload.metadata || {};
|
|
965
|
+
|
|
966
|
+
// `messageId` format — deterministic when we have a task.id so a
|
|
967
|
+
// retry produces the same id for the same (task, sentence) pair
|
|
968
|
+
// (downstream dedupers can key on it). Falls back to a UUID for
|
|
969
|
+
// the legacy in-server instant path that has no row id.
|
|
970
|
+
const messageIdBase = task.id != null
|
|
971
|
+
? `msg_task_${task.id}`
|
|
972
|
+
: `msg_${randomUUID()}_instant`;
|
|
973
|
+
|
|
974
|
+
// ReasoningPush — auto-emitted before the content burst when the
|
|
975
|
+
// LLM response carried non-empty reasoning_content. `fixed` and
|
|
976
|
+
// explicit-userMessage paths produce no LLM response, so this
|
|
977
|
+
// block is naturally skipped for them (llmResponse stays null).
|
|
978
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
979
|
+
if (reasoning) {
|
|
980
|
+
const reasoningPush = buildReasoningPush({
|
|
981
|
+
messageType: decryptedPayload.messageType,
|
|
982
|
+
source,
|
|
983
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
984
|
+
sessionId,
|
|
985
|
+
reasoningContent: reasoning,
|
|
986
|
+
timestamp: new Date().toISOString(),
|
|
987
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
988
|
+
contactName: decryptedPayload.contactName,
|
|
989
|
+
avatarUrl,
|
|
990
|
+
messageSubtype,
|
|
991
|
+
metadata,
|
|
992
|
+
});
|
|
993
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
994
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
995
|
+
}
|
|
911
996
|
|
|
912
997
|
for (let i = 0; i < messages.length; i++) {
|
|
913
|
-
const
|
|
914
|
-
|
|
998
|
+
const contentPush = buildContentPush({
|
|
999
|
+
messageType: decryptedPayload.messageType,
|
|
1000
|
+
source,
|
|
1001
|
+
messageId: `${messageIdBase}_${i}`,
|
|
1002
|
+
sessionId,
|
|
915
1003
|
message: messages[i],
|
|
1004
|
+
timestamp: new Date().toISOString(),
|
|
1005
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
916
1006
|
contactName: decryptedPayload.contactName,
|
|
917
|
-
|
|
1007
|
+
avatarUrl,
|
|
1008
|
+
messageSubtype,
|
|
918
1009
|
messageIndex: i + 1,
|
|
919
1010
|
totalMessages: messages.length,
|
|
920
|
-
messageType: decryptedPayload.messageType,
|
|
921
|
-
messageSubtype: decryptedPayload.messageSubtype || 'chat',
|
|
922
1011
|
taskId: task.id || null,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
avatarUrl: decryptedPayload.avatarUrl || null,
|
|
926
|
-
metadata: decryptedPayload.metadata || {}
|
|
927
|
-
};
|
|
1012
|
+
metadata,
|
|
1013
|
+
});
|
|
928
1014
|
|
|
929
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(
|
|
1015
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
930
1016
|
|
|
931
1017
|
if (i < messages.length - 1) {
|
|
932
1018
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
@@ -1030,7 +1116,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1030
1116
|
|
|
1031
1117
|
/**
|
|
1032
1118
|
* Call an OpenAI-compatible API.
|
|
1119
|
+
*
|
|
1120
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
1121
|
+
* `content` string. Callers that only need the text can ignore
|
|
1122
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
1123
|
+
* read from `response.choices[0].message`.
|
|
1124
|
+
*
|
|
1033
1125
|
* @private
|
|
1126
|
+
* @param {Object} payload
|
|
1127
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
1034
1128
|
*/
|
|
1035
1129
|
async function _callAI(payload) {
|
|
1036
1130
|
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
@@ -1067,7 +1161,7 @@ async function _callAI(payload) {
|
|
|
1067
1161
|
throw new Error('AI API error: response missing choices[0].message.content');
|
|
1068
1162
|
}
|
|
1069
1163
|
|
|
1070
|
-
return content.trim();
|
|
1164
|
+
return { response: aiData, content: content.trim() };
|
|
1071
1165
|
}
|
|
1072
1166
|
|
|
1073
1167
|
/**
|
|
@@ -1125,20 +1219,6 @@ function buildAiRequestBody(payload) {
|
|
|
1125
1219
|
* to avoid an architectural dependency (server should not depend on the
|
|
1126
1220
|
* stateless worker package).
|
|
1127
1221
|
*
|
|
1128
|
-
* Rules (idempotent — running it twice equals running it once):
|
|
1129
|
-
* - Already ends with `/chat/completions` → leave as-is.
|
|
1130
|
-
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
1131
|
-
* - Path ends with a version segment like `/v1`,
|
|
1132
|
-
* `/v2`, … (with or without trailing slash) → append only
|
|
1133
|
-
* `/chat/completions`; never doubles `/v1` for
|
|
1134
|
-
* callers who already include it.
|
|
1135
|
-
* - Anything else (custom path that doesn't match the
|
|
1136
|
-
* OpenAI shape, e.g. `/v1/messages` for
|
|
1137
|
-
* Anthropic-style proxies) → leave as-is. The caller
|
|
1138
|
-
* knows their own routing.
|
|
1139
|
-
*
|
|
1140
|
-
* Query string is preserved verbatim.
|
|
1141
|
-
*
|
|
1142
1222
|
* @param {string} apiUrl
|
|
1143
1223
|
* @returns {string}
|
|
1144
1224
|
*/
|
|
@@ -1702,7 +1782,11 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1702
1782
|
if (Object.prototype.hasOwnProperty.call(updates, 'avatarUrl')) {
|
|
1703
1783
|
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1704
1784
|
if (avatarErr) {
|
|
1705
|
-
|
|
1785
|
+
// Soft-strip: drop the bad avatarUrl from the patch (keeps the
|
|
1786
|
+
// existing stored avatar untouched) and continue applying the rest
|
|
1787
|
+
// of the update. See standards §6.2.
|
|
1788
|
+
console.warn('[amsg-server] update-message avatarUrl 不合法,已忽略:', avatarErr);
|
|
1789
|
+
delete updates.avatarUrl;
|
|
1706
1790
|
}
|
|
1707
1791
|
}
|
|
1708
1792
|
|
package/dist/index.mjs
CHANGED
|
@@ -190,7 +190,8 @@ function validateScheduleMessagePayload(payload) {
|
|
|
190
190
|
}
|
|
191
191
|
const avatarErr = validateAvatarUrl(payload.avatarUrl);
|
|
192
192
|
if (avatarErr) {
|
|
193
|
-
|
|
193
|
+
console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
|
|
194
|
+
payload.avatarUrl = null;
|
|
194
195
|
}
|
|
195
196
|
if (payload.uuid && !isValidUUID(payload.uuid)) {
|
|
196
197
|
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)"] } };
|
|
@@ -482,6 +483,10 @@ function isUniqueViolation(error) {
|
|
|
482
483
|
|
|
483
484
|
// src/server/lib/message-processor.js
|
|
484
485
|
import { randomUUID } from "crypto";
|
|
486
|
+
import {
|
|
487
|
+
buildContentPush,
|
|
488
|
+
buildReasoningPush
|
|
489
|
+
} from "@rei-standard/amsg-shared";
|
|
485
490
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
486
491
|
function splitOnceByRegex(chunk, regex) {
|
|
487
492
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
@@ -502,6 +507,22 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
502
507
|
}
|
|
503
508
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
504
509
|
}
|
|
510
|
+
function readReasoningContent(llmResponse) {
|
|
511
|
+
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
512
|
+
const choices = (
|
|
513
|
+
/** @type {{ choices?: unknown }} */
|
|
514
|
+
llmResponse.choices
|
|
515
|
+
);
|
|
516
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
517
|
+
const message = (
|
|
518
|
+
/** @type {{ message?: { reasoning_content?: unknown } }} */
|
|
519
|
+
choices[0]?.message
|
|
520
|
+
);
|
|
521
|
+
const raw = message?.reasoning_content;
|
|
522
|
+
if (typeof raw !== "string") return null;
|
|
523
|
+
const trimmed = raw.trim();
|
|
524
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
525
|
+
}
|
|
505
526
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
506
527
|
try {
|
|
507
528
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -511,19 +532,24 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
511
532
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
512
533
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
513
534
|
let messageContent;
|
|
535
|
+
let llmResponse = null;
|
|
514
536
|
if (decryptedPayload.messageType === "fixed") {
|
|
515
537
|
messageContent = decryptedPayload.userMessage;
|
|
516
538
|
} else if (decryptedPayload.messageType === "instant") {
|
|
517
539
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
518
540
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
519
|
-
|
|
541
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
542
|
+
messageContent = aiResult.content;
|
|
543
|
+
llmResponse = aiResult.response;
|
|
520
544
|
} else if (decryptedPayload.userMessage) {
|
|
521
545
|
messageContent = decryptedPayload.userMessage;
|
|
522
546
|
} else {
|
|
523
547
|
throw new Error("Invalid instant message: no content source available");
|
|
524
548
|
}
|
|
525
549
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
526
|
-
|
|
550
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
551
|
+
messageContent = aiResult.content;
|
|
552
|
+
llmResponse = aiResult.response;
|
|
527
553
|
} else {
|
|
528
554
|
throw new Error("Invalid message configuration: no content source available");
|
|
529
555
|
}
|
|
@@ -532,23 +558,48 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
532
558
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
533
559
|
}
|
|
534
560
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
535
|
-
|
|
536
|
-
|
|
561
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
|
|
562
|
+
const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
|
|
563
|
+
const messageSubtype = decryptedPayload.messageSubtype || "chat";
|
|
564
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
565
|
+
const metadata = decryptedPayload.metadata || {};
|
|
566
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
|
|
567
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
568
|
+
if (reasoning) {
|
|
569
|
+
const reasoningPush = buildReasoningPush({
|
|
570
|
+
messageType: decryptedPayload.messageType,
|
|
571
|
+
source,
|
|
572
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
573
|
+
sessionId,
|
|
574
|
+
reasoningContent: reasoning,
|
|
575
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
537
576
|
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
577
|
+
contactName: decryptedPayload.contactName,
|
|
578
|
+
avatarUrl,
|
|
579
|
+
messageSubtype,
|
|
580
|
+
metadata
|
|
581
|
+
});
|
|
582
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
584
|
+
}
|
|
585
|
+
for (let i = 0; i < messages.length; i++) {
|
|
586
|
+
const contentPush = buildContentPush({
|
|
587
|
+
messageType: decryptedPayload.messageType,
|
|
588
|
+
source,
|
|
589
|
+
messageId: `${messageIdBase}_${i}`,
|
|
590
|
+
sessionId,
|
|
538
591
|
message: messages[i],
|
|
592
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
593
|
+
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
539
594
|
contactName: decryptedPayload.contactName,
|
|
540
|
-
|
|
595
|
+
avatarUrl,
|
|
596
|
+
messageSubtype,
|
|
541
597
|
messageIndex: i + 1,
|
|
542
598
|
totalMessages: messages.length,
|
|
543
|
-
messageType: decryptedPayload.messageType,
|
|
544
|
-
messageSubtype: decryptedPayload.messageSubtype || "chat",
|
|
545
599
|
taskId: task.id || null,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
metadata: decryptedPayload.metadata || {}
|
|
550
|
-
};
|
|
551
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(notificationPayload));
|
|
600
|
+
metadata
|
|
601
|
+
});
|
|
602
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
552
603
|
if (i < messages.length - 1) {
|
|
553
604
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
554
605
|
}
|
|
@@ -647,7 +698,7 @@ async function _callAI(payload) {
|
|
|
647
698
|
if (typeof content !== "string" || !content.trim()) {
|
|
648
699
|
throw new Error("AI API error: response missing choices[0].message.content");
|
|
649
700
|
}
|
|
650
|
-
return content.trim();
|
|
701
|
+
return { response: aiData, content: content.trim() };
|
|
651
702
|
}
|
|
652
703
|
function buildAiRequestBody(payload) {
|
|
653
704
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
@@ -1102,7 +1153,8 @@ function createUpdateMessageHandler(ctx) {
|
|
|
1102
1153
|
if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
|
|
1103
1154
|
const avatarErr = validateAvatarUrl(updates.avatarUrl);
|
|
1104
1155
|
if (avatarErr) {
|
|
1105
|
-
|
|
1156
|
+
console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
|
|
1157
|
+
delete updates.avatarUrl;
|
|
1106
1158
|
}
|
|
1107
1159
|
}
|
|
1108
1160
|
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters",
|
|
3
|
+
"version": "2.4.0-next.2",
|
|
4
|
+
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/Tosd0/ReiStandard",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"node": ">=20"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@rei-standard/amsg-shared": "0.1.0-next.4",
|
|
36
37
|
"web-push": "^3.6.7",
|
|
37
38
|
"@netlify/blobs": "^8.1.0"
|
|
38
39
|
},
|