@rei-standard/amsg-shared 0.4.0-next.0 → 0.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/LICENSE +21 -0
- package/dist/index.cjs +517 -28
- package/dist/index.d.cts +13 -17
- package/dist/index.d.ts +13 -17
- package/dist/index.mjs +515 -28
- package/dist/llm-call.d.cts +113 -0
- package/dist/llm-call.d.ts +113 -0
- package/dist/llm-messages.d.cts +66 -0
- package/dist/llm-messages.d.ts +66 -0
- package/dist/protocol.d.cts +57 -0
- package/dist/protocol.d.ts +57 -0
- package/dist/webcrypto-utils.d.cts +42 -0
- package/dist/webcrypto-utils.d.ts +42 -0
- package/dist/webpush.d.cts +69 -0
- package/dist/webpush.d.ts +69 -0
- package/package.json +2 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible LLM HTTP 调用核心 — 单一事实来源。
|
|
3
|
+
*
|
|
4
|
+
* `@rei-standard/amsg-instant`(message-processor 的 callLlmRaw)与
|
|
5
|
+
* `@rei-standard/amsg-server`(lib/llm.js 的 callLlm)此前各写一份
|
|
6
|
+
* 「构造请求体 + fetch + 超时 + 解析响应 + trim」,并已出现漂移
|
|
7
|
+
* (stream 字段、messages 模式探测、超时可配性)。现在公共核心收敛到
|
|
8
|
+
* 这里,两侧差异通过 options 参数化,各包保留自己的导出名与错误码
|
|
9
|
+
* 包装:
|
|
10
|
+
*
|
|
11
|
+
* - `stream` — instant 传 `false`(一次性、非流式契约,字段显式
|
|
12
|
+
* 出现在请求体里);server 不传(字段缺省,行为与
|
|
13
|
+
* 之前逐字节一致)。
|
|
14
|
+
* - `forwardTools` — server 转发 payload.tools / payload.toolChoice
|
|
15
|
+
* (v2.6.0 起);instant 传 `false` 维持既有
|
|
16
|
+
* 「忽略 tools」行为。
|
|
17
|
+
* - `timeoutMs` — server 的 agentic 循环传剩余墙钟预算;instant 传
|
|
18
|
+
* 300000 维持现状。默认 300000。
|
|
19
|
+
*
|
|
20
|
+
* messages 模式探测统一为 `Array.isArray(payload.messages) &&
|
|
21
|
+
* payload.messages.length > 0`(server 语义):`messages: []` 回退
|
|
22
|
+
* completePrompt 模式,而不是把空数组原样发给上游。
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Call an OpenAI-compatible API.
|
|
26
|
+
*
|
|
27
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
28
|
+
* `content` string. Callers that only need the text can ignore
|
|
29
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
30
|
+
* read from `response.choices[0].message`.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} payload
|
|
33
|
+
* @param {{
|
|
34
|
+
* requireContent?: boolean,
|
|
35
|
+
* timeoutMs?: number,
|
|
36
|
+
* fetch?: typeof globalThis.fetch,
|
|
37
|
+
* stream?: boolean,
|
|
38
|
+
* forwardTools?: boolean,
|
|
39
|
+
* }} [options]
|
|
40
|
+
* requireContent defaults to true (legacy single-shot behavior:
|
|
41
|
+
* throw when the response carries no content). Tool rounds legitimately
|
|
42
|
+
* return no content (pure tool_calls), so agentic loops pass
|
|
43
|
+
* `{ requireContent: false }`.
|
|
44
|
+
* timeoutMs defaults to 300000 (the legacy per-call ceiling).
|
|
45
|
+
* fetch defaults to `globalThis.fetch` (resolved at call time so test
|
|
46
|
+
* stubs on the global still take effect).
|
|
47
|
+
* stream / forwardTools are forwarded to {@link buildLlmRequestBody}.
|
|
48
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
49
|
+
*/
|
|
50
|
+
export function callLlm(payload: any, options?: {
|
|
51
|
+
requireContent?: boolean;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
fetch?: typeof globalThis.fetch;
|
|
54
|
+
stream?: boolean;
|
|
55
|
+
forwardTools?: boolean;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
response: unknown;
|
|
58
|
+
content: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Build OpenAI-compatible request body.
|
|
62
|
+
*
|
|
63
|
+
* messages mode: forward the caller's OpenAI-style array verbatim — no
|
|
64
|
+
* auto role injection, no concatenation back to a single user message.
|
|
65
|
+
* Lets the upstream app preserve system / multi-turn context
|
|
66
|
+
* byte-for-byte. A missing / empty `payload.messages` falls back to
|
|
67
|
+
* wrapping `payload.completePrompt` into a single user message.
|
|
68
|
+
*
|
|
69
|
+
* `temperature`: only inject the 0.8 default for the legacy
|
|
70
|
+
* completePrompt path; messages mode forwards whatever the upstream app
|
|
71
|
+
* set (or nothing) so behavior matches their main chat path.
|
|
72
|
+
*
|
|
73
|
+
* `max_tokens` is optional:
|
|
74
|
+
* - include it only when payload.maxTokens is provided
|
|
75
|
+
* - omit it when payload.maxTokens is undefined / null
|
|
76
|
+
*
|
|
77
|
+
* `tools` / `tool_choice` are optional as well: forwarded only when
|
|
78
|
+
* `options.forwardTools` is not `false` and the caller passes a
|
|
79
|
+
* non-empty payload.tools. An empty array is treated as "no tools"
|
|
80
|
+
* because some OpenAI-compatible relays reject `tools: []`.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} payload
|
|
83
|
+
* @param {{ stream?: boolean, forwardTools?: boolean }} [options]
|
|
84
|
+
* stream — set to include an explicit `stream` field in the body
|
|
85
|
+
* (instant passes `false`: one-shot, non-streaming by contract);
|
|
86
|
+
* omit to leave the field out entirely (server behavior).
|
|
87
|
+
* @returns {Object}
|
|
88
|
+
*/
|
|
89
|
+
export function buildLlmRequestBody(payload: any, options?: {
|
|
90
|
+
stream?: boolean;
|
|
91
|
+
forwardTools?: boolean;
|
|
92
|
+
}): any;
|
|
93
|
+
/**
|
|
94
|
+
* Normalize the AI API URL for OpenAI-compatible chat endpoints.
|
|
95
|
+
*
|
|
96
|
+
* Rules (idempotent — running it twice is the same as running it once):
|
|
97
|
+
* - Already ends with `/chat/completions` → leave as-is.
|
|
98
|
+
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
99
|
+
* - Path ends with a version segment like `/v1`,
|
|
100
|
+
* `/v2`, … (with or without trailing slash) → append only `/chat/completions`
|
|
101
|
+
* (never doubles `/v1` for callers who already
|
|
102
|
+
* include it).
|
|
103
|
+
* - Anything else (custom path that doesn't match
|
|
104
|
+
* the OpenAI shape, e.g. `/v1/messages` for
|
|
105
|
+
* Anthropic-style proxies, or `/openai/api/foo`) → leave as-is. We don't
|
|
106
|
+
* guess — the caller knows their own routing.
|
|
107
|
+
*
|
|
108
|
+
* The query string is preserved verbatim.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} apiUrl
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function normalizeAiApiUrl(apiUrl: string): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible LLM HTTP 调用核心 — 单一事实来源。
|
|
3
|
+
*
|
|
4
|
+
* `@rei-standard/amsg-instant`(message-processor 的 callLlmRaw)与
|
|
5
|
+
* `@rei-standard/amsg-server`(lib/llm.js 的 callLlm)此前各写一份
|
|
6
|
+
* 「构造请求体 + fetch + 超时 + 解析响应 + trim」,并已出现漂移
|
|
7
|
+
* (stream 字段、messages 模式探测、超时可配性)。现在公共核心收敛到
|
|
8
|
+
* 这里,两侧差异通过 options 参数化,各包保留自己的导出名与错误码
|
|
9
|
+
* 包装:
|
|
10
|
+
*
|
|
11
|
+
* - `stream` — instant 传 `false`(一次性、非流式契约,字段显式
|
|
12
|
+
* 出现在请求体里);server 不传(字段缺省,行为与
|
|
13
|
+
* 之前逐字节一致)。
|
|
14
|
+
* - `forwardTools` — server 转发 payload.tools / payload.toolChoice
|
|
15
|
+
* (v2.6.0 起);instant 传 `false` 维持既有
|
|
16
|
+
* 「忽略 tools」行为。
|
|
17
|
+
* - `timeoutMs` — server 的 agentic 循环传剩余墙钟预算;instant 传
|
|
18
|
+
* 300000 维持现状。默认 300000。
|
|
19
|
+
*
|
|
20
|
+
* messages 模式探测统一为 `Array.isArray(payload.messages) &&
|
|
21
|
+
* payload.messages.length > 0`(server 语义):`messages: []` 回退
|
|
22
|
+
* completePrompt 模式,而不是把空数组原样发给上游。
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Call an OpenAI-compatible API.
|
|
26
|
+
*
|
|
27
|
+
* Returns the full response object alongside the extracted (trimmed)
|
|
28
|
+
* `content` string. Callers that only need the text can ignore
|
|
29
|
+
* `response`; callers that want `reasoning_content` / `tool_calls`
|
|
30
|
+
* read from `response.choices[0].message`.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} payload
|
|
33
|
+
* @param {{
|
|
34
|
+
* requireContent?: boolean,
|
|
35
|
+
* timeoutMs?: number,
|
|
36
|
+
* fetch?: typeof globalThis.fetch,
|
|
37
|
+
* stream?: boolean,
|
|
38
|
+
* forwardTools?: boolean,
|
|
39
|
+
* }} [options]
|
|
40
|
+
* requireContent defaults to true (legacy single-shot behavior:
|
|
41
|
+
* throw when the response carries no content). Tool rounds legitimately
|
|
42
|
+
* return no content (pure tool_calls), so agentic loops pass
|
|
43
|
+
* `{ requireContent: false }`.
|
|
44
|
+
* timeoutMs defaults to 300000 (the legacy per-call ceiling).
|
|
45
|
+
* fetch defaults to `globalThis.fetch` (resolved at call time so test
|
|
46
|
+
* stubs on the global still take effect).
|
|
47
|
+
* stream / forwardTools are forwarded to {@link buildLlmRequestBody}.
|
|
48
|
+
* @returns {Promise<{ response: unknown, content: string }>}
|
|
49
|
+
*/
|
|
50
|
+
export function callLlm(payload: any, options?: {
|
|
51
|
+
requireContent?: boolean;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
fetch?: typeof globalThis.fetch;
|
|
54
|
+
stream?: boolean;
|
|
55
|
+
forwardTools?: boolean;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
response: unknown;
|
|
58
|
+
content: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Build OpenAI-compatible request body.
|
|
62
|
+
*
|
|
63
|
+
* messages mode: forward the caller's OpenAI-style array verbatim — no
|
|
64
|
+
* auto role injection, no concatenation back to a single user message.
|
|
65
|
+
* Lets the upstream app preserve system / multi-turn context
|
|
66
|
+
* byte-for-byte. A missing / empty `payload.messages` falls back to
|
|
67
|
+
* wrapping `payload.completePrompt` into a single user message.
|
|
68
|
+
*
|
|
69
|
+
* `temperature`: only inject the 0.8 default for the legacy
|
|
70
|
+
* completePrompt path; messages mode forwards whatever the upstream app
|
|
71
|
+
* set (or nothing) so behavior matches their main chat path.
|
|
72
|
+
*
|
|
73
|
+
* `max_tokens` is optional:
|
|
74
|
+
* - include it only when payload.maxTokens is provided
|
|
75
|
+
* - omit it when payload.maxTokens is undefined / null
|
|
76
|
+
*
|
|
77
|
+
* `tools` / `tool_choice` are optional as well: forwarded only when
|
|
78
|
+
* `options.forwardTools` is not `false` and the caller passes a
|
|
79
|
+
* non-empty payload.tools. An empty array is treated as "no tools"
|
|
80
|
+
* because some OpenAI-compatible relays reject `tools: []`.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} payload
|
|
83
|
+
* @param {{ stream?: boolean, forwardTools?: boolean }} [options]
|
|
84
|
+
* stream — set to include an explicit `stream` field in the body
|
|
85
|
+
* (instant passes `false`: one-shot, non-streaming by contract);
|
|
86
|
+
* omit to leave the field out entirely (server behavior).
|
|
87
|
+
* @returns {Object}
|
|
88
|
+
*/
|
|
89
|
+
export function buildLlmRequestBody(payload: any, options?: {
|
|
90
|
+
stream?: boolean;
|
|
91
|
+
forwardTools?: boolean;
|
|
92
|
+
}): any;
|
|
93
|
+
/**
|
|
94
|
+
* Normalize the AI API URL for OpenAI-compatible chat endpoints.
|
|
95
|
+
*
|
|
96
|
+
* Rules (idempotent — running it twice is the same as running it once):
|
|
97
|
+
* - Already ends with `/chat/completions` → leave as-is.
|
|
98
|
+
* - Bare host (no path or just `/`) → append `/v1/chat/completions`.
|
|
99
|
+
* - Path ends with a version segment like `/v1`,
|
|
100
|
+
* `/v2`, … (with or without trailing slash) → append only `/chat/completions`
|
|
101
|
+
* (never doubles `/v1` for callers who already
|
|
102
|
+
* include it).
|
|
103
|
+
* - Anything else (custom path that doesn't match
|
|
104
|
+
* the OpenAI shape, e.g. `/v1/messages` for
|
|
105
|
+
* Anthropic-style proxies, or `/openai/api/foo`) → leave as-is. We don't
|
|
106
|
+
* guess — the caller knows their own routing.
|
|
107
|
+
*
|
|
108
|
+
* The query string is preserved verbatim.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} apiUrl
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function normalizeAiApiUrl(apiUrl: string): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} LlmMessagesShapeError
|
|
3
|
+
* @property {LlmMessagesErrorCode} code
|
|
4
|
+
* @property {number} [index] - 出错的 messages 下标(数组级错误时缺省)。
|
|
5
|
+
* @property {number} [toolCallIndex] - 出错的 tool_calls 下标(仅 TOOL_CALL_MALFORMED)。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Validate an OpenAI-style messages array. Pure(无副作用)。
|
|
9
|
+
*
|
|
10
|
+
* 接受的形状(与 OpenAI chat.completions 协议对齐):
|
|
11
|
+
* - system / user / assistant: content 为非空字符串或长度 ≥ 1 的数组
|
|
12
|
+
* (数组元素 schema 故意不校验 — 原样透传给上游 LLM)。
|
|
13
|
+
* - assistant 带非空 `tool_calls`: content 可为 null / 空串 / 缺省;
|
|
14
|
+
* 每个 tool_call 需满足 `{ id: string, function }` 的轻量形状
|
|
15
|
+
* (不严 — 上游 LLM API 会再校一遍)。
|
|
16
|
+
* - tool: content 允许空串(工具返回空结果合法,如 search 无命中),
|
|
17
|
+
* 但必须是字符串或数组;`tool_call_id` 必填(OpenAI 协议硬约束)。
|
|
18
|
+
*
|
|
19
|
+
* @param {unknown} messages
|
|
20
|
+
* @returns {LlmMessagesShapeError | null} Structured error, or null if valid.
|
|
21
|
+
*/
|
|
22
|
+
export function validateLlmMessagesShape(messages: unknown): LlmMessagesShapeError | null;
|
|
23
|
+
/**
|
|
24
|
+
* {@link validateLlmMessagesShape} 可能返回的稳定错误码。枚举固定 —
|
|
25
|
+
* 新增校验分支时必须新增 code,各消费包据此穷举映射错误文案。
|
|
26
|
+
*
|
|
27
|
+
* @typedef {'MESSAGES_NOT_ARRAY'
|
|
28
|
+
* | 'MESSAGE_NOT_OBJECT'
|
|
29
|
+
* | 'INVALID_ROLE'
|
|
30
|
+
* | 'TOOL_CALL_MALFORMED'
|
|
31
|
+
* | 'TOOL_CONTENT_INVALID'
|
|
32
|
+
* | 'TOOL_CALL_ID_MISSING'
|
|
33
|
+
* | 'CONTENT_EMPTY_STRING'
|
|
34
|
+
* | 'CONTENT_EMPTY_ARRAY'
|
|
35
|
+
* | 'CONTENT_INVALID_TYPE'} LlmMessagesErrorCode
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Runtime constant mirroring the {@link LlmMessagesErrorCode} type.
|
|
39
|
+
*/
|
|
40
|
+
export const LLM_MESSAGES_ERROR: Readonly<{
|
|
41
|
+
MESSAGES_NOT_ARRAY: "MESSAGES_NOT_ARRAY";
|
|
42
|
+
MESSAGE_NOT_OBJECT: "MESSAGE_NOT_OBJECT";
|
|
43
|
+
INVALID_ROLE: "INVALID_ROLE";
|
|
44
|
+
TOOL_CALL_MALFORMED: "TOOL_CALL_MALFORMED";
|
|
45
|
+
TOOL_CONTENT_INVALID: "TOOL_CONTENT_INVALID";
|
|
46
|
+
TOOL_CALL_ID_MISSING: "TOOL_CALL_ID_MISSING";
|
|
47
|
+
CONTENT_EMPTY_STRING: "CONTENT_EMPTY_STRING";
|
|
48
|
+
CONTENT_EMPTY_ARRAY: "CONTENT_EMPTY_ARRAY";
|
|
49
|
+
CONTENT_INVALID_TYPE: "CONTENT_INVALID_TYPE";
|
|
50
|
+
}>;
|
|
51
|
+
export type LlmMessagesShapeError = {
|
|
52
|
+
code: LlmMessagesErrorCode;
|
|
53
|
+
/**
|
|
54
|
+
* - 出错的 messages 下标(数组级错误时缺省)。
|
|
55
|
+
*/
|
|
56
|
+
index?: number;
|
|
57
|
+
/**
|
|
58
|
+
* - 出错的 tool_calls 下标(仅 TOOL_CALL_MALFORMED)。
|
|
59
|
+
*/
|
|
60
|
+
toolCallIndex?: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* {@link validateLlmMessagesShape} 可能返回的稳定错误码。枚举固定 —
|
|
64
|
+
* 新增校验分支时必须新增 code,各消费包据此穷举映射错误文案。
|
|
65
|
+
*/
|
|
66
|
+
export type LlmMessagesErrorCode = "MESSAGES_NOT_ARRAY" | "MESSAGE_NOT_OBJECT" | "INVALID_ROLE" | "TOOL_CALL_MALFORMED" | "TOOL_CONTENT_INVALID" | "TOOL_CALL_ID_MISSING" | "CONTENT_EMPTY_STRING" | "CONTENT_EMPTY_ARRAY" | "CONTENT_INVALID_TYPE";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} LlmMessagesShapeError
|
|
3
|
+
* @property {LlmMessagesErrorCode} code
|
|
4
|
+
* @property {number} [index] - 出错的 messages 下标(数组级错误时缺省)。
|
|
5
|
+
* @property {number} [toolCallIndex] - 出错的 tool_calls 下标(仅 TOOL_CALL_MALFORMED)。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Validate an OpenAI-style messages array. Pure(无副作用)。
|
|
9
|
+
*
|
|
10
|
+
* 接受的形状(与 OpenAI chat.completions 协议对齐):
|
|
11
|
+
* - system / user / assistant: content 为非空字符串或长度 ≥ 1 的数组
|
|
12
|
+
* (数组元素 schema 故意不校验 — 原样透传给上游 LLM)。
|
|
13
|
+
* - assistant 带非空 `tool_calls`: content 可为 null / 空串 / 缺省;
|
|
14
|
+
* 每个 tool_call 需满足 `{ id: string, function }` 的轻量形状
|
|
15
|
+
* (不严 — 上游 LLM API 会再校一遍)。
|
|
16
|
+
* - tool: content 允许空串(工具返回空结果合法,如 search 无命中),
|
|
17
|
+
* 但必须是字符串或数组;`tool_call_id` 必填(OpenAI 协议硬约束)。
|
|
18
|
+
*
|
|
19
|
+
* @param {unknown} messages
|
|
20
|
+
* @returns {LlmMessagesShapeError | null} Structured error, or null if valid.
|
|
21
|
+
*/
|
|
22
|
+
export function validateLlmMessagesShape(messages: unknown): LlmMessagesShapeError | null;
|
|
23
|
+
/**
|
|
24
|
+
* {@link validateLlmMessagesShape} 可能返回的稳定错误码。枚举固定 —
|
|
25
|
+
* 新增校验分支时必须新增 code,各消费包据此穷举映射错误文案。
|
|
26
|
+
*
|
|
27
|
+
* @typedef {'MESSAGES_NOT_ARRAY'
|
|
28
|
+
* | 'MESSAGE_NOT_OBJECT'
|
|
29
|
+
* | 'INVALID_ROLE'
|
|
30
|
+
* | 'TOOL_CALL_MALFORMED'
|
|
31
|
+
* | 'TOOL_CONTENT_INVALID'
|
|
32
|
+
* | 'TOOL_CALL_ID_MISSING'
|
|
33
|
+
* | 'CONTENT_EMPTY_STRING'
|
|
34
|
+
* | 'CONTENT_EMPTY_ARRAY'
|
|
35
|
+
* | 'CONTENT_INVALID_TYPE'} LlmMessagesErrorCode
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Runtime constant mirroring the {@link LlmMessagesErrorCode} type.
|
|
39
|
+
*/
|
|
40
|
+
export const LLM_MESSAGES_ERROR: Readonly<{
|
|
41
|
+
MESSAGES_NOT_ARRAY: "MESSAGES_NOT_ARRAY";
|
|
42
|
+
MESSAGE_NOT_OBJECT: "MESSAGE_NOT_OBJECT";
|
|
43
|
+
INVALID_ROLE: "INVALID_ROLE";
|
|
44
|
+
TOOL_CALL_MALFORMED: "TOOL_CALL_MALFORMED";
|
|
45
|
+
TOOL_CONTENT_INVALID: "TOOL_CONTENT_INVALID";
|
|
46
|
+
TOOL_CALL_ID_MISSING: "TOOL_CALL_ID_MISSING";
|
|
47
|
+
CONTENT_EMPTY_STRING: "CONTENT_EMPTY_STRING";
|
|
48
|
+
CONTENT_EMPTY_ARRAY: "CONTENT_EMPTY_ARRAY";
|
|
49
|
+
CONTENT_INVALID_TYPE: "CONTENT_INVALID_TYPE";
|
|
50
|
+
}>;
|
|
51
|
+
export type LlmMessagesShapeError = {
|
|
52
|
+
code: LlmMessagesErrorCode;
|
|
53
|
+
/**
|
|
54
|
+
* - 出错的 messages 下标(数组级错误时缺省)。
|
|
55
|
+
*/
|
|
56
|
+
index?: number;
|
|
57
|
+
/**
|
|
58
|
+
* - 出错的 tool_calls 下标(仅 TOOL_CALL_MALFORMED)。
|
|
59
|
+
*/
|
|
60
|
+
toolCallIndex?: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* {@link validateLlmMessagesShape} 可能返回的稳定错误码。枚举固定 —
|
|
64
|
+
* 新增校验分支时必须新增 code,各消费包据此穷举映射错误文案。
|
|
65
|
+
*/
|
|
66
|
+
export type LlmMessagesErrorCode = "MESSAGES_NOT_ARRAY" | "MESSAGE_NOT_OBJECT" | "INVALID_ROLE" | "TOOL_CALL_MALFORMED" | "TOOL_CONTENT_INVALID" | "TOOL_CALL_ID_MISSING" | "CONTENT_EMPTY_STRING" | "CONTENT_EMPTY_ARRAY" | "CONTENT_INVALID_TYPE";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-protocol constants — the strings both ends of a transport must
|
|
3
|
+
* agree on, byte-for-byte.
|
|
4
|
+
*
|
|
5
|
+
* 这是全生态唯一一份线协议常量:multipart transport 的 kind / encoding /
|
|
6
|
+
* version 与默认限额(instant 发送端与 sw 重组端共用),以及 SW ↔ 页面
|
|
7
|
+
* postMessage 信封的 type / event 常量(sw 广播端与页面订阅端共用)。
|
|
8
|
+
* 页面侧代码请从本包 import 这些常量,而不要从 `@rei-standard/amsg-sw`
|
|
9
|
+
* import —— sw 包的模块顶层带有 SW 运行时状态,在窗口环境里执行并不合适。
|
|
10
|
+
* 实现在独立模块 — shared 内部按主题拆文件,index 只负责聚合导出。
|
|
11
|
+
*/
|
|
12
|
+
/** Transport-level `messageKind` carried by every multipart chunk. */
|
|
13
|
+
export const MULTIPART_MESSAGE_KIND: "_multipart";
|
|
14
|
+
/** Chunk body encoding: JSON → UTF-8 bytes → base64url. */
|
|
15
|
+
export const MULTIPART_ENCODING: "json-utf8-base64url";
|
|
16
|
+
/**
|
|
17
|
+
* Multipart wire-format version. Producers stamp `multipart.version`
|
|
18
|
+
* with this; the SW drops any chunk whose version doesn't match.
|
|
19
|
+
*/
|
|
20
|
+
export const MULTIPART_VERSION: 1;
|
|
21
|
+
export const DEFAULT_MULTIPART_TTL_MS: 60000;
|
|
22
|
+
export const DEFAULT_MULTIPART_MAX_CHUNKS: 128;
|
|
23
|
+
export const DEFAULT_MULTIPART_MAX_TOTAL_BYTES: 256000;
|
|
24
|
+
/**
|
|
25
|
+
* Wire-level message type for SW → client postMessage envelopes.
|
|
26
|
+
* Clients filter on `e.data.type === 'REI_AMSG_PUSH'` before reading
|
|
27
|
+
* `e.data.event` (which is one of {@link REI_SW_EVENT}'s values).
|
|
28
|
+
*/
|
|
29
|
+
export const REI_AMSG_POSTMESSAGE_TYPE: "REI_AMSG_PUSH";
|
|
30
|
+
/**
|
|
31
|
+
* Per-kind event names dispatched to controlled clients. Each push the
|
|
32
|
+
* SW receives is mirrored to every window via
|
|
33
|
+
* `postMessage({ type: 'REI_AMSG_PUSH', event: <one of these>, payload })`.
|
|
34
|
+
*
|
|
35
|
+
* The mapping is keyed by `payload.messageKind`. Legacy payloads (and
|
|
36
|
+
* blob envelopes) without a `messageKind` field dispatch as
|
|
37
|
+
* {@link REI_SW_EVENT.UNKNOWN_RECEIVED} so apps can still handle 2.0.x
|
|
38
|
+
* producers during migration.
|
|
39
|
+
*/
|
|
40
|
+
export const REI_SW_EVENT: Readonly<{
|
|
41
|
+
CONTENT_RECEIVED: "rei-amsg-content-received";
|
|
42
|
+
REASONING_RECEIVED: "rei-amsg-reasoning-received";
|
|
43
|
+
TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received";
|
|
44
|
+
ERROR_RECEIVED: "rei-amsg-error-received";
|
|
45
|
+
MULTIPART_EXPIRED: "rei-amsg-multipart-expired";
|
|
46
|
+
UNKNOWN_RECEIVED: "rei-amsg-unknown-received";
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* 页面 → SW 方向的 message type(离线队列与业务投递管线)。
|
|
50
|
+
*/
|
|
51
|
+
export const REI_SW_MESSAGE_TYPE: Readonly<{
|
|
52
|
+
ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST";
|
|
53
|
+
DELIVER: "REI_AMSG_DELIVER";
|
|
54
|
+
FLUSH_QUEUE: "REI_FLUSH_QUEUE";
|
|
55
|
+
QUEUE_RESULT: "REI_QUEUE_RESULT";
|
|
56
|
+
}>;
|
|
57
|
+
export const REI_AMSG_DELIVER_MESSAGE_TYPE: "REI_AMSG_DELIVER";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-protocol constants — the strings both ends of a transport must
|
|
3
|
+
* agree on, byte-for-byte.
|
|
4
|
+
*
|
|
5
|
+
* 这是全生态唯一一份线协议常量:multipart transport 的 kind / encoding /
|
|
6
|
+
* version 与默认限额(instant 发送端与 sw 重组端共用),以及 SW ↔ 页面
|
|
7
|
+
* postMessage 信封的 type / event 常量(sw 广播端与页面订阅端共用)。
|
|
8
|
+
* 页面侧代码请从本包 import 这些常量,而不要从 `@rei-standard/amsg-sw`
|
|
9
|
+
* import —— sw 包的模块顶层带有 SW 运行时状态,在窗口环境里执行并不合适。
|
|
10
|
+
* 实现在独立模块 — shared 内部按主题拆文件,index 只负责聚合导出。
|
|
11
|
+
*/
|
|
12
|
+
/** Transport-level `messageKind` carried by every multipart chunk. */
|
|
13
|
+
export const MULTIPART_MESSAGE_KIND: "_multipart";
|
|
14
|
+
/** Chunk body encoding: JSON → UTF-8 bytes → base64url. */
|
|
15
|
+
export const MULTIPART_ENCODING: "json-utf8-base64url";
|
|
16
|
+
/**
|
|
17
|
+
* Multipart wire-format version. Producers stamp `multipart.version`
|
|
18
|
+
* with this; the SW drops any chunk whose version doesn't match.
|
|
19
|
+
*/
|
|
20
|
+
export const MULTIPART_VERSION: 1;
|
|
21
|
+
export const DEFAULT_MULTIPART_TTL_MS: 60000;
|
|
22
|
+
export const DEFAULT_MULTIPART_MAX_CHUNKS: 128;
|
|
23
|
+
export const DEFAULT_MULTIPART_MAX_TOTAL_BYTES: 256000;
|
|
24
|
+
/**
|
|
25
|
+
* Wire-level message type for SW → client postMessage envelopes.
|
|
26
|
+
* Clients filter on `e.data.type === 'REI_AMSG_PUSH'` before reading
|
|
27
|
+
* `e.data.event` (which is one of {@link REI_SW_EVENT}'s values).
|
|
28
|
+
*/
|
|
29
|
+
export const REI_AMSG_POSTMESSAGE_TYPE: "REI_AMSG_PUSH";
|
|
30
|
+
/**
|
|
31
|
+
* Per-kind event names dispatched to controlled clients. Each push the
|
|
32
|
+
* SW receives is mirrored to every window via
|
|
33
|
+
* `postMessage({ type: 'REI_AMSG_PUSH', event: <one of these>, payload })`.
|
|
34
|
+
*
|
|
35
|
+
* The mapping is keyed by `payload.messageKind`. Legacy payloads (and
|
|
36
|
+
* blob envelopes) without a `messageKind` field dispatch as
|
|
37
|
+
* {@link REI_SW_EVENT.UNKNOWN_RECEIVED} so apps can still handle 2.0.x
|
|
38
|
+
* producers during migration.
|
|
39
|
+
*/
|
|
40
|
+
export const REI_SW_EVENT: Readonly<{
|
|
41
|
+
CONTENT_RECEIVED: "rei-amsg-content-received";
|
|
42
|
+
REASONING_RECEIVED: "rei-amsg-reasoning-received";
|
|
43
|
+
TOOL_REQUEST_RECEIVED: "rei-amsg-tool-request-received";
|
|
44
|
+
ERROR_RECEIVED: "rei-amsg-error-received";
|
|
45
|
+
MULTIPART_EXPIRED: "rei-amsg-multipart-expired";
|
|
46
|
+
UNKNOWN_RECEIVED: "rei-amsg-unknown-received";
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* 页面 → SW 方向的 message type(离线队列与业务投递管线)。
|
|
50
|
+
*/
|
|
51
|
+
export const REI_SW_MESSAGE_TYPE: Readonly<{
|
|
52
|
+
ENQUEUE_REQUEST: "REI_ENQUEUE_REQUEST";
|
|
53
|
+
DELIVER: "REI_AMSG_DELIVER";
|
|
54
|
+
FLUSH_QUEUE: "REI_FLUSH_QUEUE";
|
|
55
|
+
QUEUE_RESULT: "REI_QUEUE_RESULT";
|
|
56
|
+
}>;
|
|
57
|
+
export const REI_AMSG_DELIVER_MESSAGE_TYPE: "REI_AMSG_DELIVER";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce ArrayBuffer | Uint8Array | view → Uint8Array (no copy when possible).
|
|
3
|
+
*/
|
|
4
|
+
export function toUint8(buf: any): Uint8Array<ArrayBufferLike>;
|
|
5
|
+
/**
|
|
6
|
+
* Concatenate Uint8Arrays into a single Uint8Array.
|
|
7
|
+
* @param {...(Uint8Array | ArrayBuffer | ArrayBufferView)} chunks
|
|
8
|
+
* @returns {Uint8Array}
|
|
9
|
+
*/
|
|
10
|
+
export function concatBytes(...chunks: (Uint8Array | ArrayBuffer | ArrayBufferView)[]): Uint8Array;
|
|
11
|
+
/** UTF-8 encode a string into a Uint8Array. */
|
|
12
|
+
export function utf8(str: any): Uint8Array<ArrayBuffer>;
|
|
13
|
+
/** UTF-8 decode a Uint8Array / ArrayBuffer into a string. */
|
|
14
|
+
export function utf8Decode(buf: any): string;
|
|
15
|
+
/** Encode bytes as standard base64 (with padding). */
|
|
16
|
+
export function bytesToBase64(buf: any): string;
|
|
17
|
+
/** Encode bytes as base64url (no padding). */
|
|
18
|
+
export function bytesToBase64Url(buf: any): string;
|
|
19
|
+
/**
|
|
20
|
+
* Decode base64url (with or without padding) → Uint8Array.
|
|
21
|
+
* @param {string} input
|
|
22
|
+
* @returns {Uint8Array}
|
|
23
|
+
*/
|
|
24
|
+
export function base64UrlToBytes(input: string): Uint8Array;
|
|
25
|
+
/** Encode a JSON-serializable value as base64url (UTF-8 JSON). */
|
|
26
|
+
export function jsonToBase64Url(value: any): string;
|
|
27
|
+
/** Encode bytes as lowercase hex. */
|
|
28
|
+
export function bytesToHex(buf: any): string;
|
|
29
|
+
/** Decode a hex string into a Uint8Array. */
|
|
30
|
+
export function hexToBytes(hex: any): Uint8Array<ArrayBuffer>;
|
|
31
|
+
/** HMAC-SHA-256 over `data` with `keyBytes`. Returns 32-byte Uint8Array. */
|
|
32
|
+
export function hmacSha256(keyBytes: any, data: any): Promise<Uint8Array<ArrayBuffer>>;
|
|
33
|
+
/**
|
|
34
|
+
* Constant-time byte comparison. Returns true iff `a` and `b` are equal-length
|
|
35
|
+
* sequences with the same bytes. Length is intentionally NOT secret — early
|
|
36
|
+
* length-check is fine and matches Node `timingSafeEqual`'s contract.
|
|
37
|
+
*/
|
|
38
|
+
export function timingSafeEqualBytes(a: any, b: any): boolean;
|
|
39
|
+
/** Cryptographically random bytes. */
|
|
40
|
+
export function randomBytes(n: any): Uint8Array<any>;
|
|
41
|
+
/** `crypto.randomUUID()`. The Node adapter polyfills `globalThis.crypto`. */
|
|
42
|
+
export function randomUUID(): `${string}-${string}-${string}-${string}-${string}`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce ArrayBuffer | Uint8Array | view → Uint8Array (no copy when possible).
|
|
3
|
+
*/
|
|
4
|
+
export function toUint8(buf: any): Uint8Array<ArrayBufferLike>;
|
|
5
|
+
/**
|
|
6
|
+
* Concatenate Uint8Arrays into a single Uint8Array.
|
|
7
|
+
* @param {...(Uint8Array | ArrayBuffer | ArrayBufferView)} chunks
|
|
8
|
+
* @returns {Uint8Array}
|
|
9
|
+
*/
|
|
10
|
+
export function concatBytes(...chunks: (Uint8Array | ArrayBuffer | ArrayBufferView)[]): Uint8Array;
|
|
11
|
+
/** UTF-8 encode a string into a Uint8Array. */
|
|
12
|
+
export function utf8(str: any): Uint8Array<ArrayBuffer>;
|
|
13
|
+
/** UTF-8 decode a Uint8Array / ArrayBuffer into a string. */
|
|
14
|
+
export function utf8Decode(buf: any): string;
|
|
15
|
+
/** Encode bytes as standard base64 (with padding). */
|
|
16
|
+
export function bytesToBase64(buf: any): string;
|
|
17
|
+
/** Encode bytes as base64url (no padding). */
|
|
18
|
+
export function bytesToBase64Url(buf: any): string;
|
|
19
|
+
/**
|
|
20
|
+
* Decode base64url (with or without padding) → Uint8Array.
|
|
21
|
+
* @param {string} input
|
|
22
|
+
* @returns {Uint8Array}
|
|
23
|
+
*/
|
|
24
|
+
export function base64UrlToBytes(input: string): Uint8Array;
|
|
25
|
+
/** Encode a JSON-serializable value as base64url (UTF-8 JSON). */
|
|
26
|
+
export function jsonToBase64Url(value: any): string;
|
|
27
|
+
/** Encode bytes as lowercase hex. */
|
|
28
|
+
export function bytesToHex(buf: any): string;
|
|
29
|
+
/** Decode a hex string into a Uint8Array. */
|
|
30
|
+
export function hexToBytes(hex: any): Uint8Array<ArrayBuffer>;
|
|
31
|
+
/** HMAC-SHA-256 over `data` with `keyBytes`. Returns 32-byte Uint8Array. */
|
|
32
|
+
export function hmacSha256(keyBytes: any, data: any): Promise<Uint8Array<ArrayBuffer>>;
|
|
33
|
+
/**
|
|
34
|
+
* Constant-time byte comparison. Returns true iff `a` and `b` are equal-length
|
|
35
|
+
* sequences with the same bytes. Length is intentionally NOT secret — early
|
|
36
|
+
* length-check is fine and matches Node `timingSafeEqual`'s contract.
|
|
37
|
+
*/
|
|
38
|
+
export function timingSafeEqualBytes(a: any, b: any): boolean;
|
|
39
|
+
/** Cryptographically random bytes. */
|
|
40
|
+
export function randomBytes(n: any): Uint8Array<any>;
|
|
41
|
+
/** `crypto.randomUUID()`. The Node adapter polyfills `globalThis.crypto`. */
|
|
42
|
+
export function randomUUID(): `${string}-${string}-${string}-${string}-${string}`;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a single Web Push notification.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} args
|
|
5
|
+
* @param {Object} args.subscription - Standard PushSubscription JSON.
|
|
6
|
+
* @param {string} args.subscription.endpoint
|
|
7
|
+
* @param {Object} args.subscription.keys
|
|
8
|
+
* @param {string} args.subscription.keys.p256dh - base64url, 65 B uncompressed P-256 point.
|
|
9
|
+
* @param {string} args.subscription.keys.auth - base64url, 16 B auth secret.
|
|
10
|
+
* @param {string} args.payload - Already-stringified JSON to deliver.
|
|
11
|
+
* @param {Object} args.vapid
|
|
12
|
+
* @param {string} args.vapid.email - VAPID `sub` (mailto: auto-prepended if missing).
|
|
13
|
+
* @param {string} args.vapid.publicKey - base64url, 65 B uncompressed P-256 point.
|
|
14
|
+
* @param {string} args.vapid.privateKey - base64url, 32 B scalar.
|
|
15
|
+
* @param {number} [args.ttl=60] - Push service TTL header, seconds.
|
|
16
|
+
* @param {typeof fetch} [args.fetch] - Override fetch impl (testing / proxy).
|
|
17
|
+
* @returns {Promise<{ statusCode: number, body: string, headers: Headers }>}
|
|
18
|
+
* @throws {Error} err.code = 'PUSH_SEND_FAILED' on push-service error.
|
|
19
|
+
*/
|
|
20
|
+
export function sendWebPush({ subscription, payload, vapid, ttl, fetch: fetchImpl }: {
|
|
21
|
+
subscription: {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
keys: {
|
|
24
|
+
p256dh: string;
|
|
25
|
+
auth: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
payload: string;
|
|
29
|
+
vapid: {
|
|
30
|
+
email: string;
|
|
31
|
+
publicKey: string;
|
|
32
|
+
privateKey: string;
|
|
33
|
+
};
|
|
34
|
+
ttl?: number;
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
statusCode: number;
|
|
38
|
+
body: string;
|
|
39
|
+
headers: Headers;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Build a VAPID `Authorization` JWT for a single push.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} args
|
|
45
|
+
* @param {string} args.audience - Origin of the push endpoint (e.g. https://fcm.googleapis.com).
|
|
46
|
+
* @param {string} args.subject - VAPID `sub` claim, typically `mailto:you@example.com`.
|
|
47
|
+
* @param {string} args.publicKey - base64url, 65 B uncompressed P-256 point.
|
|
48
|
+
* @param {string} args.privateKey - base64url, 32 B scalar.
|
|
49
|
+
* @returns {Promise<string>} compact JWS (three base64url segments).
|
|
50
|
+
*/
|
|
51
|
+
export function buildVapidJwt({ audience, subject, publicKey, privateKey }: {
|
|
52
|
+
audience: string;
|
|
53
|
+
subject: string;
|
|
54
|
+
publicKey: string;
|
|
55
|
+
privateKey: string;
|
|
56
|
+
}): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Verify a VAPID JWT signature. Exported for tests / advanced consumers.
|
|
59
|
+
* Returns the decoded payload if the signature and `exp` are valid.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} jwt
|
|
62
|
+
* @param {string} publicKey - VAPID public key (base64url, 65 B).
|
|
63
|
+
* @returns {Promise<{ aud: string, exp: number, sub: string }>}
|
|
64
|
+
*/
|
|
65
|
+
export function verifyVapidJwt(jwt: string, publicKey: string): Promise<{
|
|
66
|
+
aud: string;
|
|
67
|
+
exp: number;
|
|
68
|
+
sub: string;
|
|
69
|
+
}>;
|