Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/client-avatar-url-align.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/instant-gzip-request-body.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/server-vapid-subject-fix.md

This file was deleted.

9 changes: 0 additions & 9 deletions .changeset/shared-validation-vapid-reasoning.md

This file was deleted.

18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 20 additions & 9 deletions packages/rei-standard-amsg/client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog — @rei-standard/amsg-client

## 2.8.0

### Minor Changes

- 5c0e047: `avatarUrl` 本地预检改用 `@rei-standard/amsg-shared` 的统一校验,与 server / instant 对齐。现在非法(非 `data:`)URL —— 例如缺少协议的 `foo.com/a.png` —— 也会在客户端被 `console.warn` 并置空;此前 client 只检查 `data:` 与长度,会放行这类 URL(之后由服务端兜底置空)。软清空策略不变:装饰性字段不合法时只做清空,不会让整条请求失败。

### Patch Changes

- Updated dependencies [5c0e047]
- @rei-standard/amsg-shared@0.3.0

## 2.7.0 — `deliver()` 新增 `compressRequest` 请求体 gzip 压缩

给 `deliver()` 加一个**可选**的 `compressRequest`,把要发出去的请求体在上网线之前 gzip 压一下。中文 + 重复结构的 JSON 压缩比很高(实测 ~322KB 能压到 ~50KB),网线上字节小了,大 body 在慢/不稳的上行链路上就能在「发了没回应就杀」的超时之前传完。压的是**请求**,不是响应;上下文内容一字不动,只是传输层省字节。
Expand Down Expand Up @@ -63,11 +74,11 @@

### Migration

| 旧写法 | 新写法 |
| --- | --- |
| 旧写法 | 新写法 |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `try { await consumeInstantStream(p, '/instant', { onPayload }) } catch { fail() }` | `const r = await deliver(p, { delivery: { mode: 'observed', observed }, timeoutMs, onChunk: onPayload }); if (r.outcome !== 'delivered') ...` |
| `const r = await sendInstant(p); if (!r.success) fail()` | `const r = await deliver(p, { delivery: { mode: 'observed', observed }, timeoutMs }); if (r.outcome === 'send-failed') ...` |
| `sendInstant(p, '/instant', { authorization: 'Bearer ...' })` | `deliver(p, { delivery, timeoutMs, authorization: 'Bearer ...' })` |
| `const r = await sendInstant(p); if (!r.success) fail()` | `const r = await deliver(p, { delivery: { mode: 'observed', observed }, timeoutMs }); if (r.outcome === 'send-failed') ...` |
| `sendInstant(p, '/instant', { authorization: 'Bearer ...' })` | `deliver(p, { delivery, timeoutMs, authorization: 'Bearer ...' })` |

详见 README 的 `deliver()` 标准用法与「为什么需要 `deliver()`」段。

Expand Down Expand Up @@ -136,11 +147,11 @@ Self-review 时(仿 ultrareview 多角度分派)抓到的 correctness 修复
POST 到 amsg-instant 的 `/instant` 或 `/continue` 端点,按 SSE frame 解析 `event: payload` / `event: error` / `event: done`,分发到 `options.onPayload` 回调;可被 `options.signal` 中止。

```js
await client.consumeInstantStream(payload, '/instant', {
onPayload: async (p) => routeToIDB(p), // 必填
onError: (err) => log(err), // 可选;通知用,不抑制 throw
onDone: () => stopSpinner(), // 可选
signal: abortController.signal, // 可选
await client.consumeInstantStream(payload, "/instant", {
onPayload: async (p) => routeToIDB(p), // 必填
onError: (err) => log(err), // 可选;通知用,不抑制 throw
onDone: () => stopSpinner(), // 可选
signal: abortController.signal, // 可选
});
```

Expand Down
4 changes: 2 additions & 2 deletions packages/rei-standard-amsg/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rei-standard/amsg-client",
"version": "2.7.0",
"version": "2.8.0",
"description": "ReiStandard Active Messaging browser client SDK — also re-exports shared push types, builders, and guards from @rei-standard/amsg-shared",
"repository": {
"type": "git",
Expand Down Expand Up @@ -33,7 +33,7 @@
"node": ">=20"
},
"dependencies": {
"@rei-standard/amsg-shared": "^0.2.0"
"@rei-standard/amsg-shared": "^0.3.0"
},
"devDependencies": {
"tsup": "^8.0.0",
Expand Down
55 changes: 34 additions & 21 deletions packages/rei-standard-amsg/instant/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog — @rei-standard/amsg-instant

## 0.10.0

### Minor Changes

- f4812ce: 接收端支持 gzip 压缩的请求体。带 `X-Amsg-Request-Encoding: gzip` 头的请求会先 gunzip 再解析,不带这个头的请求按原样读取,行为不变。CORS 预检白名单里也加上了这个头。这样 `@rei-standard/amsg-client` 的 `deliver({ compressRequest })` 就能直接发到 `amsg-instant` 的 `/instant` / `/continue`,不用自己在后端解压。

### Patch Changes

- Updated dependencies [5c0e047]
- @rei-standard/amsg-shared@0.3.0

## 0.9.1 — SSE stream lifecycle owns LLM + push completion

- **Fix**: SSE 模式下 LLM 调用与每条 payload 的 Web Push backup / fallback 完整运行在 `ReadableStream.start()` 内——`start()` 先 await 所有 backup 推送,再 `controller.close()`。响应仍在产出期间 runtime 不会施加 wall-clock 上限,慢 LLM + 客户端中途断开(iOS Safari 杀掉后台 SSE socket、页面切走等)的组合下也能把这一轮消息送达。
Expand Down Expand Up @@ -191,12 +202,12 @@ Install with `npm install @rei-standard/amsg-instant@next`. Pre-release — brea

### Migration cheat sheet

| next.3 | next.4 |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| `return { decision: 'finish', pushPayload: { ... } }` | `return { decision: 'finish', pushPayloads: [{ ... }] }` |
| Request body `splitPattern: '([。!?!?]+)'` | Implement the split in your hook; return one push per segment |
| `pushPayload.splitPattern: null` (per-push disable from next.3) | Return `pushPayloads: [singleUnsplit]` |
| `reasoningSplitPattern` request field | Set `autoEmitReasoning: false`, build N reasoning pushes yourself with `buildReasoningPush(...)`, include them at the start of `pushPayloads` |
| next.3 | next.4 |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `return { decision: 'finish', pushPayload: { ... } }` | `return { decision: 'finish', pushPayloads: [{ ... }] }` |
| Request body `splitPattern: '([。!?!?]+)'` | Implement the split in your hook; return one push per segment |
| `pushPayload.splitPattern: null` (per-push disable from next.3) | Return `pushPayloads: [singleUnsplit]` |
| `reasoningSplitPattern` request field | Set `autoEmitReasoning: false`, build N reasoning pushes yourself with `buildReasoningPush(...)`, include them at the start of `pushPayloads` |

### Why breaking in pre-release

Expand Down Expand Up @@ -239,19 +250,20 @@ Coordinated with `@rei-standard/amsg-shared@0.1.0-next.2`. Install with `npm ins

- **`reasoningSplitPattern` / `errorSplitPattern` payload 字段** — 按 `messageKind` 独立的句号切配置:

| `messageKind` | 字段 | 默认 |
|----------------|---------------------------|---------------------|
| `content` | `splitPattern` | `/([。!?!?]+)/` (开) |
| `tool_request` | `splitPattern` | `/([。!?!?]+)/` (开) |
| `reasoning` | `reasoningSplitPattern` | **不切** |
| `error` | `errorSplitPattern` | **不切** |
| 自由 payload | — | 不切 |
| `messageKind` | 字段 | 默认 |
| -------------- | ----------------------- | ---------------------- |
| `content` | `splitPattern` | `/([。!?!?]+)/` (开) |
| `tool_request` | `splitPattern` | `/([。!?!?]+)/` (开) |
| `reasoning` | `reasoningSplitPattern` | **不切** |
| `error` | `errorSplitPattern` | **不切** |
| 自由 payload | — | 不切 |

四个 kind 共享的「禁用」语义:显式 `null` 或 `[]` 关闭切分。差别在 `undefined`(字段省略):`content` / `tool_request` 回落默认句号正则;`reasoning` / `error` 保持不切(这俩历史上就没切片 UX,默认 off 才符合预期)。

- **`reasoningChunkBytes` handler option(默认 2000,`null` 禁用)** — `ReasoningPush.reasoningContent` 的 UTF-8 字节上限。reasoning-heavy LLM(DeepSeek-R1 / GLM-4.5 / Qwen3-Thinking)经常输出 3-10 KB reasoning,超 Web Push ~2.6 KB 上限。next.2 内置 transparent 字节切分:超限时按 UTF-8 codepoint 边界切成 N 份,每片带 `chunkIndex` / `totalChunks`,SW 按这两个字段拼回完整字符串。**绝大多数 reasoning-heavy 部署不再需要 BlobStore。** `createInstantHandler` 构造期校验 `reasoningChunkBytes ∈ [500, maxInlineBytes - 600]`(600 B 余量给 push payload 元字段),不合法抛 `TypeError`。

- **两层 cascade(Layer 1 句切 → Layer 2 字节切)** — `reasoningSplitPattern` 先按句切成 M 段,每段单独量字节,超阈值的段再字节切成 N 块。最终 push 同时带两组索引:

- Layer 1:`messageIndex` 1..M / `totalMessages` M(M=1 时不写)
- Layer 2:`chunkIndex` 1..N / `totalChunks` N(N=1 时不写)

Expand Down Expand Up @@ -315,15 +327,15 @@ Coordinated minor across the whole amsg ecosystem. This release replaces the leg

### Migration from 0.7.x

| 0.7.x | 0.8.0 |
|------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| `buildInstantPushPayload({ message, index, total, contactName, ... })` | `buildContentPush({ messageType: 'instant', source: 'instant', messageId, sessionId, message, messageIndex, totalMessages, contactName, ... })` from `@rei-standard/amsg-instant` |
| Hook payload `{ type: 'tool-request', ... }` (free-form) | Either keep it free-form (still legal — `pushPayload: unknown`) or call `buildToolRequestPush({ ... })` for a typed envelope |
| SW dispatch by ad-hoc field sniffing on push payload | SW dispatch by `payload.messageKind` switch (consume the shared `AmsgPush` discriminated union) |
| `{ type: 'error', code: 'HOOK_THREW', message, sessionId, iteration }` | Auto-built — no caller-side change needed; the wire shape now uses `messageKind: 'error'` instead of `type: 'error'` |
| Hook fully owned every push (incl. reasoning, if you built one) | Framework auto-emits `ReasoningPush` before the hook runs. Set `autoEmitReasoning: false` on `createInstantHandler({...})` to restore total hook control. |
| 0.7.x | 0.8.0 |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `buildInstantPushPayload({ message, index, total, contactName, ... })` | `buildContentPush({ messageType: 'instant', source: 'instant', messageId, sessionId, message, messageIndex, totalMessages, contactName, ... })` from `@rei-standard/amsg-instant` |
| Hook payload `{ type: 'tool-request', ... }` (free-form) | Either keep it free-form (still legal — `pushPayload: unknown`) or call `buildToolRequestPush({ ... })` for a typed envelope |
| SW dispatch by ad-hoc field sniffing on push payload | SW dispatch by `payload.messageKind` switch (consume the shared `AmsgPush` discriminated union) |
| `{ type: 'error', code: 'HOOK_THREW', message, sessionId, iteration }` | Auto-built — no caller-side change needed; the wire shape now uses `messageKind: 'error'` instead of `type: 'error'` |
| Hook fully owned every push (incl. reasoning, if you built one) | Framework auto-emits `ReasoningPush` before the hook runs. Set `autoEmitReasoning: false` on `createInstantHandler({...})` to restore total hook control. |
| Hook returned `pushPayload` without a `sessionId` field | **Set `sessionId: ctx.sessionId`** in your hook's `pushPayload`. The framework does NOT auto-inject it (the `pushPayload: unknown` contract is preserved). Without this the SW can't pair your content push with the auto-emitted ReasoningPush. |
| Legacy path push failure aborted the whole burst | Reasoning-push failure is now best-effort (`reasoning_push_failed` event + continue). Content-push failures still abort, same as before. |
| Legacy path push failure aborted the whole burst | Reasoning-push failure is now best-effort (`reasoning_push_failed` event + continue). Content-push failures still abort, same as before. |

If you have a hook that builds its own pushPayload object, **set `sessionId: ctx.sessionId`** in it so the SW can pair your content push with the auto-emitted ReasoningPush.

Expand Down Expand Up @@ -423,6 +435,7 @@ If you have a hook that builds its own pushPayload object, **set `sessionId: ctx
- **CORS 内置**:handler 在入口处短路 `OPTIONS` 预检请求 → `204 No Content`,所有响应(含 200 / 4xx / 5xx)自动叠 `Access-Control-Allow-Origin / -Methods / -Headers` + `Access-Control-Max-Age: 86400`。浏览器跨域调用零配置 work。
- `options.cors?: { allowOrigin?: string }`:自定义允许来源,默认 `'*'`。配成具体来源时自动附 `Vary: Origin`,避免反向代理缓存把 CORS policy 串到错的站点。
- **`normalizeAiApiUrl(apiUrl)`** 智能补全 OpenAI 兼容路径,**幂等**(跑两次 = 跑一次):

- 裸 host(如 `https://api.openai.com`)→ 补 `/v1/chat/completions`
- 末尾是 `/v1` 或 `/v2` 等版本段 → 只补 `/chat/completions`,**不会重复加 v1**
- 已含 `/chat/completions` → 原样返回
Expand Down
Loading
Loading