@rei-standard/amsg-server 2.3.1 → 2.4.0-next.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 +43 -13
- package/dist/index.cjs +66 -16
- package/dist/index.d.cts +109 -33
- package/dist/index.d.ts +109 -33
- package/dist/index.mjs +64 -14
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rei-standard/amsg-server
|
|
2
2
|
|
|
3
|
-
`@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK
|
|
3
|
+
`@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK:Blob 租户配置、`tenantToken` / `cronToken` 鉴权、标准路由处理器。API 规范见 [API 技术规范](https://github.com/Tosd0/ReiStandard/blob/main/standards/active-messaging-api.md)。
|
|
4
4
|
|
|
5
5
|
## v2.0.1 变更摘要
|
|
6
6
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- 业务端点统一使用 `Authorization: Bearer <tenantToken>`
|
|
10
10
|
- `send-notifications` 支持 `cronToken`(Header 或 query token)
|
|
11
11
|
|
|
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
|
+
|
|
12
14
|
## 安装
|
|
13
15
|
|
|
14
16
|
```bash
|
|
@@ -52,7 +54,7 @@ const rei = await createReiServer({
|
|
|
52
54
|
|
|
53
55
|
## 关于 `messageType: 'instant'`
|
|
54
56
|
|
|
55
|
-
> **Note**:新代码的 instant 消息请用 [@rei-standard/amsg-instant](
|
|
57
|
+
> **Note**:新代码的 instant 消息请用 [@rei-standard/amsg-instant](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md),跳过本端点的"建任务 → 处理 → 删任务" DB 来回。本端点的 `instant` 分支为兼容保留,行为不变、不会有运行时警告。
|
|
56
58
|
|
|
57
59
|
## AI 接口 `apiUrl` 约束
|
|
58
60
|
|
|
@@ -130,17 +132,45 @@ AI 配置消息的提示词可以用两种形态之一,**互斥二选一**:
|
|
|
130
132
|
## 运行环境与要求
|
|
131
133
|
|
|
132
134
|
- Node.js `>=20`
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
135
|
+
- 必须装:`web-push`、`@netlify/blobs`、以及至少一个数据库驱动(`@neondatabase/serverless` 或 `pg`)
|
|
136
|
+
|
|
137
|
+
### 环境变量
|
|
138
|
+
|
|
139
|
+
必填:
|
|
140
|
+
|
|
141
|
+
- `VAPID_EMAIL` — VAPID 联系邮箱
|
|
142
|
+
- `NEXT_PUBLIC_VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` — VAPID 公私钥对,[vapidkeys.com](https://vapidkeys.com) 生成
|
|
143
|
+
- `TENANT_CONFIG_KEK` — 加密 Blob 里租户配置(含 db connection、masterKey)的 KEK
|
|
144
|
+
- `TENANT_TOKEN_SIGNING_KEY` — `tenantToken` / `cronToken` 的 HMAC 签名密钥
|
|
145
|
+
|
|
146
|
+
可选:
|
|
147
|
+
|
|
148
|
+
- `INIT_SECRET` — 配了之后 `POST /api/v1/init-tenant` 必须带 `X-Init-Secret` 头才能初始化租户
|
|
149
|
+
- `PUBLIC_BASE_URL` — 生产域名(如 `https://your-domain.com`),用来让 `init-tenant` 返回完整 `cronWebhookUrl`
|
|
150
|
+
- `VERCEL_PROTECTION_BYPASS` — Vercel 部署 + Preview Protection 时给 cron 走的 bypass key
|
|
151
|
+
|
|
152
|
+
`TENANT_CONFIG_KEK` / `TENANT_TOKEN_SIGNING_KEY` / `INIT_SECRET` 推荐:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
openssl rand -base64 32
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `.env` 模板
|
|
159
|
+
|
|
160
|
+
```dotenv
|
|
161
|
+
VAPID_EMAIL=youremail@example.com
|
|
162
|
+
NEXT_PUBLIC_VAPID_PUBLIC_KEY=YOUR-PUBLIC-KEY
|
|
163
|
+
VAPID_PRIVATE_KEY=YOUR-PRIVATE-KEY
|
|
164
|
+
TENANT_CONFIG_KEK=YOUR-KEK-SECRET
|
|
165
|
+
TENANT_TOKEN_SIGNING_KEY=YOUR-TOKEN-SIGNING-KEY
|
|
166
|
+
|
|
167
|
+
# 可选
|
|
168
|
+
INIT_SECRET=YOUR-INIT-SECRET
|
|
169
|
+
PUBLIC_BASE_URL=https://your-domain.com
|
|
170
|
+
VERCEL_PROTECTION_BYPASS=YOUR_BYPASS_KEY
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Vercel 部署配置可参考 [`examples/vercel.json.example`](https://github.com/Tosd0/ReiStandard/blob/main/examples/vercel.json.example)。
|
|
144
174
|
|
|
145
175
|
## 相关链接(绝对 URL)
|
|
146
176
|
|
package/dist/index.cjs
CHANGED
|
@@ -482,6 +482,10 @@ function isUniqueViolation(error) {
|
|
|
482
482
|
|
|
483
483
|
// src/server/lib/message-processor.js
|
|
484
484
|
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
var _amsgshared = require('@rei-standard/amsg-shared');
|
|
485
489
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
486
490
|
function splitOnceByRegex(chunk, regex) {
|
|
487
491
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
@@ -502,6 +506,22 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
502
506
|
}
|
|
503
507
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
504
508
|
}
|
|
509
|
+
function readReasoningContent(llmResponse) {
|
|
510
|
+
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
511
|
+
const choices = (
|
|
512
|
+
/** @type {{ choices?: unknown }} */
|
|
513
|
+
llmResponse.choices
|
|
514
|
+
);
|
|
515
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
516
|
+
const message = (
|
|
517
|
+
/** @type {{ message?: { reasoning_content?: unknown } }} */
|
|
518
|
+
_optionalChain([choices, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.message])
|
|
519
|
+
);
|
|
520
|
+
const raw = _optionalChain([message, 'optionalAccess', _4 => _4.reasoning_content]);
|
|
521
|
+
if (typeof raw !== "string") return null;
|
|
522
|
+
const trimmed = raw.trim();
|
|
523
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
524
|
+
}
|
|
505
525
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
506
526
|
try {
|
|
507
527
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -511,19 +531,24 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
511
531
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
512
532
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
513
533
|
let messageContent;
|
|
534
|
+
let llmResponse = null;
|
|
514
535
|
if (decryptedPayload.messageType === "fixed") {
|
|
515
536
|
messageContent = decryptedPayload.userMessage;
|
|
516
537
|
} else if (decryptedPayload.messageType === "instant") {
|
|
517
538
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
518
539
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
519
|
-
|
|
540
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
541
|
+
messageContent = aiResult.content;
|
|
542
|
+
llmResponse = aiResult.response;
|
|
520
543
|
} else if (decryptedPayload.userMessage) {
|
|
521
544
|
messageContent = decryptedPayload.userMessage;
|
|
522
545
|
} else {
|
|
523
546
|
throw new Error("Invalid instant message: no content source available");
|
|
524
547
|
}
|
|
525
548
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
526
|
-
|
|
549
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
550
|
+
messageContent = aiResult.content;
|
|
551
|
+
llmResponse = aiResult.response;
|
|
527
552
|
} else {
|
|
528
553
|
throw new Error("Invalid message configuration: no content source available");
|
|
529
554
|
}
|
|
@@ -532,23 +557,48 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
532
557
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
533
558
|
}
|
|
534
559
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
535
|
-
|
|
536
|
-
|
|
560
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${_crypto.randomUUID.call(void 0, )}`;
|
|
561
|
+
const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
|
|
562
|
+
const messageSubtype = decryptedPayload.messageSubtype || "chat";
|
|
563
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
564
|
+
const metadata = decryptedPayload.metadata || {};
|
|
565
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${_crypto.randomUUID.call(void 0, )}_instant`;
|
|
566
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
567
|
+
if (reasoning) {
|
|
568
|
+
const reasoningPush = _amsgshared.buildReasoningPush.call(void 0, {
|
|
569
|
+
messageType: decryptedPayload.messageType,
|
|
570
|
+
source,
|
|
571
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
572
|
+
sessionId,
|
|
573
|
+
reasoningContent: reasoning,
|
|
574
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
537
575
|
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
576
|
+
contactName: decryptedPayload.contactName,
|
|
577
|
+
avatarUrl,
|
|
578
|
+
messageSubtype,
|
|
579
|
+
metadata
|
|
580
|
+
});
|
|
581
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
582
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
583
|
+
}
|
|
584
|
+
for (let i = 0; i < messages.length; i++) {
|
|
585
|
+
const contentPush = _amsgshared.buildContentPush.call(void 0, {
|
|
586
|
+
messageType: decryptedPayload.messageType,
|
|
587
|
+
source,
|
|
588
|
+
messageId: `${messageIdBase}_${i}`,
|
|
589
|
+
sessionId,
|
|
538
590
|
message: messages[i],
|
|
591
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
592
|
+
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
539
593
|
contactName: decryptedPayload.contactName,
|
|
540
|
-
|
|
594
|
+
avatarUrl,
|
|
595
|
+
messageSubtype,
|
|
541
596
|
messageIndex: i + 1,
|
|
542
597
|
totalMessages: messages.length,
|
|
543
|
-
messageType: decryptedPayload.messageType,
|
|
544
|
-
messageSubtype: decryptedPayload.messageSubtype || "chat",
|
|
545
598
|
taskId: task.id || null,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
metadata: decryptedPayload.metadata || {}
|
|
550
|
-
};
|
|
551
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(notificationPayload));
|
|
599
|
+
metadata
|
|
600
|
+
});
|
|
601
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
552
602
|
if (i < messages.length - 1) {
|
|
553
603
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
554
604
|
}
|
|
@@ -643,11 +693,11 @@ async function _callAI(payload) {
|
|
|
643
693
|
);
|
|
644
694
|
}
|
|
645
695
|
const aiData = await aiResponse.json();
|
|
646
|
-
const content = _optionalChain([aiData, 'optionalAccess',
|
|
696
|
+
const content = _optionalChain([aiData, 'optionalAccess', _5 => _5.choices, 'optionalAccess', _6 => _6[0], 'optionalAccess', _7 => _7.message, 'optionalAccess', _8 => _8.content]);
|
|
647
697
|
if (typeof content !== "string" || !content.trim()) {
|
|
648
698
|
throw new Error("AI API error: response missing choices[0].message.content");
|
|
649
699
|
}
|
|
650
|
-
return content.trim();
|
|
700
|
+
return { response: aiData, content: content.trim() };
|
|
651
701
|
}
|
|
652
702
|
function buildAiRequestBody(payload) {
|
|
653
703
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
|
@@ -1326,7 +1376,7 @@ async function resolveBlobStore(namespace) {
|
|
|
1326
1376
|
}
|
|
1327
1377
|
try {
|
|
1328
1378
|
const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
|
|
1329
|
-
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access',
|
|
1379
|
+
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _9 => _9.default, 'optionalAccess', _10 => _10.getStore]);
|
|
1330
1380
|
if (typeof getStore === "function") {
|
|
1331
1381
|
return getStore({ name: namespace });
|
|
1332
1382
|
}
|
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
|
|
@@ -793,11 +794,23 @@ function isUniqueViolation(error) {
|
|
|
793
794
|
|
|
794
795
|
/**
|
|
795
796
|
* Message Processor (SDK version)
|
|
796
|
-
* ReiStandard
|
|
797
|
+
* ReiStandard amsg-server v2.4.0
|
|
798
|
+
*
|
|
799
|
+
* Handles single message content generation and Web Push delivery for
|
|
800
|
+
* scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
|
|
801
|
+
* via-server instant path (`messageType: 'instant'`).
|
|
797
802
|
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
803
|
+
* Push wire shape comes from `@rei-standard/amsg-shared`'s
|
|
804
|
+
* discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
|
|
805
|
+
* routes on `messageKind`. Server-driven pushes always carry
|
|
806
|
+
* `source: 'instant'` (for the legacy in-server instant) or
|
|
807
|
+
* `source: 'scheduled'` (for everything else).
|
|
808
|
+
*
|
|
809
|
+
* v2.4.0: when the LLM response carries non-empty
|
|
810
|
+
* `choices[0].message.reasoning_content`, the processor now emits a
|
|
811
|
+
* standalone `ReasoningPush` **before** the `ContentPush` burst.
|
|
812
|
+
* `messagesSent` in the return value continues to reflect the sentence
|
|
813
|
+
* count only (reasoning is an auxiliary push, not a sentence).
|
|
801
814
|
*/
|
|
802
815
|
|
|
803
816
|
|
|
@@ -850,6 +863,25 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
850
863
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
851
864
|
}
|
|
852
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
868
|
+
* string, or null when absent / empty. Mirrors
|
|
869
|
+
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
870
|
+
*
|
|
871
|
+
* @param {unknown} llmResponse
|
|
872
|
+
* @returns {string | null}
|
|
873
|
+
*/
|
|
874
|
+
function readReasoningContent(llmResponse) {
|
|
875
|
+
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
876
|
+
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
877
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
878
|
+
const message = /** @type {{ message?: { reasoning_content?: unknown } }} */ (choices[0])?.message;
|
|
879
|
+
const raw = message?.reasoning_content;
|
|
880
|
+
if (typeof raw !== 'string') return null;
|
|
881
|
+
const trimmed = raw.trim();
|
|
882
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
883
|
+
}
|
|
884
|
+
|
|
853
885
|
/**
|
|
854
886
|
* @typedef {Object} ProcessorContext
|
|
855
887
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -876,14 +908,19 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
876
908
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
877
909
|
|
|
878
910
|
let messageContent;
|
|
911
|
+
/** @type {unknown} */
|
|
912
|
+
let llmResponse = null;
|
|
879
913
|
|
|
880
914
|
if (decryptedPayload.messageType === 'fixed') {
|
|
881
915
|
messageContent = decryptedPayload.userMessage;
|
|
882
916
|
|
|
883
917
|
} else if (decryptedPayload.messageType === 'instant') {
|
|
884
|
-
const hasPrompt = !!decryptedPayload.completePrompt
|
|
918
|
+
const hasPrompt = !!decryptedPayload.completePrompt
|
|
919
|
+
|| (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
|
|
885
920
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
886
|
-
|
|
921
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
922
|
+
messageContent = aiResult.content;
|
|
923
|
+
llmResponse = aiResult.response;
|
|
887
924
|
} else if (decryptedPayload.userMessage) {
|
|
888
925
|
messageContent = decryptedPayload.userMessage;
|
|
889
926
|
} else {
|
|
@@ -891,7 +928,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
891
928
|
}
|
|
892
929
|
|
|
893
930
|
} else if (decryptedPayload.messageType === 'prompted' || decryptedPayload.messageType === 'auto') {
|
|
894
|
-
|
|
931
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
932
|
+
messageContent = aiResult.content;
|
|
933
|
+
llmResponse = aiResult.response;
|
|
895
934
|
} else {
|
|
896
935
|
throw new Error('Invalid message configuration: no content source available');
|
|
897
936
|
}
|
|
@@ -908,25 +947,68 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
908
947
|
}
|
|
909
948
|
|
|
910
949
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
950
|
+
// sessionId is shared across the optional ReasoningPush and every
|
|
951
|
+
// ContentPush from this LLM round. Pin it to the task id when
|
|
952
|
+
// available (scheduled tasks) so retries reuse the same id;
|
|
953
|
+
// otherwise mint a UUID.
|
|
954
|
+
const sessionId = task.id != null
|
|
955
|
+
? `sess_task_${task.id}`
|
|
956
|
+
: `sess_${randomUUID()}`;
|
|
957
|
+
const source = decryptedPayload.messageType === 'instant' ? 'instant' : 'scheduled';
|
|
958
|
+
const messageSubtype = decryptedPayload.messageSubtype || 'chat';
|
|
959
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
960
|
+
const metadata = decryptedPayload.metadata || {};
|
|
961
|
+
|
|
962
|
+
// `messageId` format — deterministic when we have a task.id so a
|
|
963
|
+
// retry produces the same id for the same (task, sentence) pair
|
|
964
|
+
// (downstream dedupers can key on it). Falls back to a UUID for
|
|
965
|
+
// the legacy in-server instant path that has no row id.
|
|
966
|
+
const messageIdBase = task.id != null
|
|
967
|
+
? `msg_task_${task.id}`
|
|
968
|
+
: `msg_${randomUUID()}_instant`;
|
|
969
|
+
|
|
970
|
+
// ReasoningPush — auto-emitted before the content burst when the
|
|
971
|
+
// LLM response carried non-empty reasoning_content. `fixed` and
|
|
972
|
+
// explicit-userMessage paths produce no LLM response, so this
|
|
973
|
+
// block is naturally skipped for them (llmResponse stays null).
|
|
974
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
975
|
+
if (reasoning) {
|
|
976
|
+
const reasoningPush = buildReasoningPush({
|
|
977
|
+
messageType: decryptedPayload.messageType,
|
|
978
|
+
source,
|
|
979
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
980
|
+
sessionId,
|
|
981
|
+
reasoningContent: reasoning,
|
|
982
|
+
timestamp: new Date().toISOString(),
|
|
983
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
984
|
+
contactName: decryptedPayload.contactName,
|
|
985
|
+
avatarUrl,
|
|
986
|
+
messageSubtype,
|
|
987
|
+
metadata,
|
|
988
|
+
});
|
|
989
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
990
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
991
|
+
}
|
|
911
992
|
|
|
912
993
|
for (let i = 0; i < messages.length; i++) {
|
|
913
|
-
const
|
|
914
|
-
|
|
994
|
+
const contentPush = buildContentPush({
|
|
995
|
+
messageType: decryptedPayload.messageType,
|
|
996
|
+
source,
|
|
997
|
+
messageId: `${messageIdBase}_${i}`,
|
|
998
|
+
sessionId,
|
|
915
999
|
message: messages[i],
|
|
1000
|
+
timestamp: new Date().toISOString(),
|
|
1001
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
916
1002
|
contactName: decryptedPayload.contactName,
|
|
917
|
-
|
|
1003
|
+
avatarUrl,
|
|
1004
|
+
messageSubtype,
|
|
918
1005
|
messageIndex: i + 1,
|
|
919
1006
|
totalMessages: messages.length,
|
|
920
|
-
messageType: decryptedPayload.messageType,
|
|
921
|
-
messageSubtype: decryptedPayload.messageSubtype || 'chat',
|
|
922
1007
|
taskId: task.id || null,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
avatarUrl: decryptedPayload.avatarUrl || null,
|
|
926
|
-
metadata: decryptedPayload.metadata || {}
|
|
927
|
-
};
|
|
1008
|
+
metadata,
|
|
1009
|
+
});
|
|
928
1010
|
|
|
929
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(
|
|
1011
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
930
1012
|
|
|
931
1013
|
if (i < messages.length - 1) {
|
|
932
1014
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
@@ -1030,7 +1112,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1030
1112
|
|
|
1031
1113
|
/**
|
|
1032
1114
|
* Call an OpenAI-compatible API.
|
|
1115
|
+
*
|
|
1116
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
1117
|
+
* `content` string. Callers that only need the text can ignore
|
|
1118
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
1119
|
+
* read from `response.choices[0].message`.
|
|
1120
|
+
*
|
|
1033
1121
|
* @private
|
|
1122
|
+
* @param {Object} payload
|
|
1123
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
1034
1124
|
*/
|
|
1035
1125
|
async function _callAI(payload) {
|
|
1036
1126
|
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
@@ -1067,7 +1157,7 @@ async function _callAI(payload) {
|
|
|
1067
1157
|
throw new Error('AI API error: response missing choices[0].message.content');
|
|
1068
1158
|
}
|
|
1069
1159
|
|
|
1070
|
-
return content.trim();
|
|
1160
|
+
return { response: aiData, content: content.trim() };
|
|
1071
1161
|
}
|
|
1072
1162
|
|
|
1073
1163
|
/**
|
|
@@ -1125,20 +1215,6 @@ function buildAiRequestBody(payload) {
|
|
|
1125
1215
|
* to avoid an architectural dependency (server should not depend on the
|
|
1126
1216
|
* stateless worker package).
|
|
1127
1217
|
*
|
|
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
1218
|
* @param {string} apiUrl
|
|
1143
1219
|
* @returns {string}
|
|
1144
1220
|
*/
|
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
|
|
@@ -793,11 +794,23 @@ function isUniqueViolation(error) {
|
|
|
793
794
|
|
|
794
795
|
/**
|
|
795
796
|
* Message Processor (SDK version)
|
|
796
|
-
* ReiStandard
|
|
797
|
+
* ReiStandard amsg-server v2.4.0
|
|
798
|
+
*
|
|
799
|
+
* Handles single message content generation and Web Push delivery for
|
|
800
|
+
* scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
|
|
801
|
+
* via-server instant path (`messageType: 'instant'`).
|
|
797
802
|
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
803
|
+
* Push wire shape comes from `@rei-standard/amsg-shared`'s
|
|
804
|
+
* discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
|
|
805
|
+
* routes on `messageKind`. Server-driven pushes always carry
|
|
806
|
+
* `source: 'instant'` (for the legacy in-server instant) or
|
|
807
|
+
* `source: 'scheduled'` (for everything else).
|
|
808
|
+
*
|
|
809
|
+
* v2.4.0: when the LLM response carries non-empty
|
|
810
|
+
* `choices[0].message.reasoning_content`, the processor now emits a
|
|
811
|
+
* standalone `ReasoningPush` **before** the `ContentPush` burst.
|
|
812
|
+
* `messagesSent` in the return value continues to reflect the sentence
|
|
813
|
+
* count only (reasoning is an auxiliary push, not a sentence).
|
|
801
814
|
*/
|
|
802
815
|
|
|
803
816
|
|
|
@@ -850,6 +863,25 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
850
863
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
851
864
|
}
|
|
852
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Read `choices[0].message.reasoning_content` as a non-empty trimmed
|
|
868
|
+
* string, or null when absent / empty. Mirrors
|
|
869
|
+
* `amsg-instant/src/message-processor.js#readReasoningContent`.
|
|
870
|
+
*
|
|
871
|
+
* @param {unknown} llmResponse
|
|
872
|
+
* @returns {string | null}
|
|
873
|
+
*/
|
|
874
|
+
function readReasoningContent(llmResponse) {
|
|
875
|
+
if (!llmResponse || typeof llmResponse !== 'object') return null;
|
|
876
|
+
const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
|
|
877
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
878
|
+
const message = /** @type {{ message?: { reasoning_content?: unknown } }} */ (choices[0])?.message;
|
|
879
|
+
const raw = message?.reasoning_content;
|
|
880
|
+
if (typeof raw !== 'string') return null;
|
|
881
|
+
const trimmed = raw.trim();
|
|
882
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
883
|
+
}
|
|
884
|
+
|
|
853
885
|
/**
|
|
854
886
|
* @typedef {Object} ProcessorContext
|
|
855
887
|
* @property {Object} webpush - The web-push module instance (already VAPID-configured).
|
|
@@ -876,14 +908,19 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
876
908
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
877
909
|
|
|
878
910
|
let messageContent;
|
|
911
|
+
/** @type {unknown} */
|
|
912
|
+
let llmResponse = null;
|
|
879
913
|
|
|
880
914
|
if (decryptedPayload.messageType === 'fixed') {
|
|
881
915
|
messageContent = decryptedPayload.userMessage;
|
|
882
916
|
|
|
883
917
|
} else if (decryptedPayload.messageType === 'instant') {
|
|
884
|
-
const hasPrompt = !!decryptedPayload.completePrompt
|
|
918
|
+
const hasPrompt = !!decryptedPayload.completePrompt
|
|
919
|
+
|| (Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0);
|
|
885
920
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
886
|
-
|
|
921
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
922
|
+
messageContent = aiResult.content;
|
|
923
|
+
llmResponse = aiResult.response;
|
|
887
924
|
} else if (decryptedPayload.userMessage) {
|
|
888
925
|
messageContent = decryptedPayload.userMessage;
|
|
889
926
|
} else {
|
|
@@ -891,7 +928,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
891
928
|
}
|
|
892
929
|
|
|
893
930
|
} else if (decryptedPayload.messageType === 'prompted' || decryptedPayload.messageType === 'auto') {
|
|
894
|
-
|
|
931
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
932
|
+
messageContent = aiResult.content;
|
|
933
|
+
llmResponse = aiResult.response;
|
|
895
934
|
} else {
|
|
896
935
|
throw new Error('Invalid message configuration: no content source available');
|
|
897
936
|
}
|
|
@@ -908,25 +947,68 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
908
947
|
}
|
|
909
948
|
|
|
910
949
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
950
|
+
// sessionId is shared across the optional ReasoningPush and every
|
|
951
|
+
// ContentPush from this LLM round. Pin it to the task id when
|
|
952
|
+
// available (scheduled tasks) so retries reuse the same id;
|
|
953
|
+
// otherwise mint a UUID.
|
|
954
|
+
const sessionId = task.id != null
|
|
955
|
+
? `sess_task_${task.id}`
|
|
956
|
+
: `sess_${randomUUID()}`;
|
|
957
|
+
const source = decryptedPayload.messageType === 'instant' ? 'instant' : 'scheduled';
|
|
958
|
+
const messageSubtype = decryptedPayload.messageSubtype || 'chat';
|
|
959
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
960
|
+
const metadata = decryptedPayload.metadata || {};
|
|
961
|
+
|
|
962
|
+
// `messageId` format — deterministic when we have a task.id so a
|
|
963
|
+
// retry produces the same id for the same (task, sentence) pair
|
|
964
|
+
// (downstream dedupers can key on it). Falls back to a UUID for
|
|
965
|
+
// the legacy in-server instant path that has no row id.
|
|
966
|
+
const messageIdBase = task.id != null
|
|
967
|
+
? `msg_task_${task.id}`
|
|
968
|
+
: `msg_${randomUUID()}_instant`;
|
|
969
|
+
|
|
970
|
+
// ReasoningPush — auto-emitted before the content burst when the
|
|
971
|
+
// LLM response carried non-empty reasoning_content. `fixed` and
|
|
972
|
+
// explicit-userMessage paths produce no LLM response, so this
|
|
973
|
+
// block is naturally skipped for them (llmResponse stays null).
|
|
974
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
975
|
+
if (reasoning) {
|
|
976
|
+
const reasoningPush = buildReasoningPush({
|
|
977
|
+
messageType: decryptedPayload.messageType,
|
|
978
|
+
source,
|
|
979
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
980
|
+
sessionId,
|
|
981
|
+
reasoningContent: reasoning,
|
|
982
|
+
timestamp: new Date().toISOString(),
|
|
983
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
984
|
+
contactName: decryptedPayload.contactName,
|
|
985
|
+
avatarUrl,
|
|
986
|
+
messageSubtype,
|
|
987
|
+
metadata,
|
|
988
|
+
});
|
|
989
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
990
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
991
|
+
}
|
|
911
992
|
|
|
912
993
|
for (let i = 0; i < messages.length; i++) {
|
|
913
|
-
const
|
|
914
|
-
|
|
994
|
+
const contentPush = buildContentPush({
|
|
995
|
+
messageType: decryptedPayload.messageType,
|
|
996
|
+
source,
|
|
997
|
+
messageId: `${messageIdBase}_${i}`,
|
|
998
|
+
sessionId,
|
|
915
999
|
message: messages[i],
|
|
1000
|
+
timestamp: new Date().toISOString(),
|
|
1001
|
+
title: `来自 ${decryptedPayload.contactName}`,
|
|
916
1002
|
contactName: decryptedPayload.contactName,
|
|
917
|
-
|
|
1003
|
+
avatarUrl,
|
|
1004
|
+
messageSubtype,
|
|
918
1005
|
messageIndex: i + 1,
|
|
919
1006
|
totalMessages: messages.length,
|
|
920
|
-
messageType: decryptedPayload.messageType,
|
|
921
|
-
messageSubtype: decryptedPayload.messageSubtype || 'chat',
|
|
922
1007
|
taskId: task.id || null,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
avatarUrl: decryptedPayload.avatarUrl || null,
|
|
926
|
-
metadata: decryptedPayload.metadata || {}
|
|
927
|
-
};
|
|
1008
|
+
metadata,
|
|
1009
|
+
});
|
|
928
1010
|
|
|
929
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(
|
|
1011
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
930
1012
|
|
|
931
1013
|
if (i < messages.length - 1) {
|
|
932
1014
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
@@ -1030,7 +1112,15 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
|
|
|
1030
1112
|
|
|
1031
1113
|
/**
|
|
1032
1114
|
* Call an OpenAI-compatible API.
|
|
1115
|
+
*
|
|
1116
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
1117
|
+
* `content` string. Callers that only need the text can ignore
|
|
1118
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
1119
|
+
* read from `response.choices[0].message`.
|
|
1120
|
+
*
|
|
1033
1121
|
* @private
|
|
1122
|
+
* @param {Object} payload
|
|
1123
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
1034
1124
|
*/
|
|
1035
1125
|
async function _callAI(payload) {
|
|
1036
1126
|
const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
|
|
@@ -1067,7 +1157,7 @@ async function _callAI(payload) {
|
|
|
1067
1157
|
throw new Error('AI API error: response missing choices[0].message.content');
|
|
1068
1158
|
}
|
|
1069
1159
|
|
|
1070
|
-
return content.trim();
|
|
1160
|
+
return { response: aiData, content: content.trim() };
|
|
1071
1161
|
}
|
|
1072
1162
|
|
|
1073
1163
|
/**
|
|
@@ -1125,20 +1215,6 @@ function buildAiRequestBody(payload) {
|
|
|
1125
1215
|
* to avoid an architectural dependency (server should not depend on the
|
|
1126
1216
|
* stateless worker package).
|
|
1127
1217
|
*
|
|
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
1218
|
* @param {string} apiUrl
|
|
1143
1219
|
* @returns {string}
|
|
1144
1220
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -482,6 +482,10 @@ function isUniqueViolation(error) {
|
|
|
482
482
|
|
|
483
483
|
// src/server/lib/message-processor.js
|
|
484
484
|
import { randomUUID } from "crypto";
|
|
485
|
+
import {
|
|
486
|
+
buildContentPush,
|
|
487
|
+
buildReasoningPush
|
|
488
|
+
} from "@rei-standard/amsg-shared";
|
|
485
489
|
var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
|
|
486
490
|
function splitOnceByRegex(chunk, regex) {
|
|
487
491
|
const out = chunk.split(regex).reduce((acc, part, i, arr) => {
|
|
@@ -502,6 +506,22 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
|
|
|
502
506
|
}
|
|
503
507
|
return chunks.length > 0 ? chunks : [messageContent];
|
|
504
508
|
}
|
|
509
|
+
function readReasoningContent(llmResponse) {
|
|
510
|
+
if (!llmResponse || typeof llmResponse !== "object") return null;
|
|
511
|
+
const choices = (
|
|
512
|
+
/** @type {{ choices?: unknown }} */
|
|
513
|
+
llmResponse.choices
|
|
514
|
+
);
|
|
515
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
516
|
+
const message = (
|
|
517
|
+
/** @type {{ message?: { reasoning_content?: unknown } }} */
|
|
518
|
+
choices[0]?.message
|
|
519
|
+
);
|
|
520
|
+
const raw = message?.reasoning_content;
|
|
521
|
+
if (typeof raw !== "string") return null;
|
|
522
|
+
const trimmed = raw.trim();
|
|
523
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
524
|
+
}
|
|
505
525
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
506
526
|
try {
|
|
507
527
|
const masterKey = providedMasterKey || ctx.masterKey;
|
|
@@ -511,19 +531,24 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
511
531
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
512
532
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
513
533
|
let messageContent;
|
|
534
|
+
let llmResponse = null;
|
|
514
535
|
if (decryptedPayload.messageType === "fixed") {
|
|
515
536
|
messageContent = decryptedPayload.userMessage;
|
|
516
537
|
} else if (decryptedPayload.messageType === "instant") {
|
|
517
538
|
const hasPrompt = !!decryptedPayload.completePrompt || Array.isArray(decryptedPayload.messages) && decryptedPayload.messages.length > 0;
|
|
518
539
|
if (hasPrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
|
|
519
|
-
|
|
540
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
541
|
+
messageContent = aiResult.content;
|
|
542
|
+
llmResponse = aiResult.response;
|
|
520
543
|
} else if (decryptedPayload.userMessage) {
|
|
521
544
|
messageContent = decryptedPayload.userMessage;
|
|
522
545
|
} else {
|
|
523
546
|
throw new Error("Invalid instant message: no content source available");
|
|
524
547
|
}
|
|
525
548
|
} else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
|
|
526
|
-
|
|
549
|
+
const aiResult = await _callAI(decryptedPayload);
|
|
550
|
+
messageContent = aiResult.content;
|
|
551
|
+
llmResponse = aiResult.response;
|
|
527
552
|
} else {
|
|
528
553
|
throw new Error("Invalid message configuration: no content source available");
|
|
529
554
|
}
|
|
@@ -532,23 +557,48 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
532
557
|
throw new Error("VAPID configuration missing - push notifications cannot be sent");
|
|
533
558
|
}
|
|
534
559
|
const pushSubscription = decryptedPayload.pushSubscription;
|
|
535
|
-
|
|
536
|
-
|
|
560
|
+
const sessionId = task.id != null ? `sess_task_${task.id}` : `sess_${randomUUID()}`;
|
|
561
|
+
const source = decryptedPayload.messageType === "instant" ? "instant" : "scheduled";
|
|
562
|
+
const messageSubtype = decryptedPayload.messageSubtype || "chat";
|
|
563
|
+
const avatarUrl = decryptedPayload.avatarUrl || null;
|
|
564
|
+
const metadata = decryptedPayload.metadata || {};
|
|
565
|
+
const messageIdBase = task.id != null ? `msg_task_${task.id}` : `msg_${randomUUID()}_instant`;
|
|
566
|
+
const reasoning = readReasoningContent(llmResponse);
|
|
567
|
+
if (reasoning) {
|
|
568
|
+
const reasoningPush = buildReasoningPush({
|
|
569
|
+
messageType: decryptedPayload.messageType,
|
|
570
|
+
source,
|
|
571
|
+
messageId: `${messageIdBase}_reasoning`,
|
|
572
|
+
sessionId,
|
|
573
|
+
reasoningContent: reasoning,
|
|
574
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
537
575
|
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
576
|
+
contactName: decryptedPayload.contactName,
|
|
577
|
+
avatarUrl,
|
|
578
|
+
messageSubtype,
|
|
579
|
+
metadata
|
|
580
|
+
});
|
|
581
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
|
|
582
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
583
|
+
}
|
|
584
|
+
for (let i = 0; i < messages.length; i++) {
|
|
585
|
+
const contentPush = buildContentPush({
|
|
586
|
+
messageType: decryptedPayload.messageType,
|
|
587
|
+
source,
|
|
588
|
+
messageId: `${messageIdBase}_${i}`,
|
|
589
|
+
sessionId,
|
|
538
590
|
message: messages[i],
|
|
591
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
592
|
+
title: `\u6765\u81EA ${decryptedPayload.contactName}`,
|
|
539
593
|
contactName: decryptedPayload.contactName,
|
|
540
|
-
|
|
594
|
+
avatarUrl,
|
|
595
|
+
messageSubtype,
|
|
541
596
|
messageIndex: i + 1,
|
|
542
597
|
totalMessages: messages.length,
|
|
543
|
-
messageType: decryptedPayload.messageType,
|
|
544
|
-
messageSubtype: decryptedPayload.messageSubtype || "chat",
|
|
545
598
|
taskId: task.id || null,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
metadata: decryptedPayload.metadata || {}
|
|
550
|
-
};
|
|
551
|
-
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(notificationPayload));
|
|
599
|
+
metadata
|
|
600
|
+
});
|
|
601
|
+
await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
|
|
552
602
|
if (i < messages.length - 1) {
|
|
553
603
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
554
604
|
}
|
|
@@ -647,7 +697,7 @@ async function _callAI(payload) {
|
|
|
647
697
|
if (typeof content !== "string" || !content.trim()) {
|
|
648
698
|
throw new Error("AI API error: response missing choices[0].message.content");
|
|
649
699
|
}
|
|
650
|
-
return content.trim();
|
|
700
|
+
return { response: aiData, content: content.trim() };
|
|
651
701
|
}
|
|
652
702
|
function buildAiRequestBody(payload) {
|
|
653
703
|
const llmMessages = Array.isArray(payload.messages) && payload.messages.length > 0 ? payload.messages : [{ role: "user", content: payload.completePrompt }];
|
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.0",
|
|
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.0",
|
|
36
37
|
"web-push": "^3.6.7",
|
|
37
38
|
"@netlify/blobs": "^8.1.0"
|
|
38
39
|
},
|