{"_id":"@bdky/ky-sse-hook","_rev":"3-18b2a07926fcf2e6bf998bf12789c1f2","name":"@bdky/ky-sse-hook","dist-tags":{"latest":"1.0.3"},"versions":{"1.0.1":{"name":"@bdky/ky-sse-hook","version":"1.0.1","author":{"name":"Baidu ACG FE Team","email":"chenfan02@baidu.com"},"license":"MIT","_id":"@bdky/ky-sse-hook@1.0.1","maintainers":[{"name":"zhangwx616","email":"bdwenxi@gmail.com"},{"name":"berwin0415","email":"hanbw@sina.cn"},{"name":"patrickli147","email":"patrick_li_147@foxmail.com"}],"contributors":[{"name":"FranckChen","email":"chenfan02@baidu.com"},{"name":"zhangwenxi","email":"zhangwenxi@baidu.com"}],"dist":{"shasum":"f79db249516492600997ff8a643bd46826e63176","tarball":"https://registry.npmjs.org/@bdky/ky-sse-hook/-/ky-sse-hook-1.0.1.tgz","fileCount":6,"integrity":"sha512-02/wU1XYVdFkiZNVv/Ib9zDVw3l4KAUOT3pJgEp29r+EEUJ5cz6iGjFOLBnKlXkjMA8+jLk9a2ZsjSNkCUDMgA==","signatures":[{"sig":"MEQCIFiIu/Os3WeQmoRkdlssomZz3ZZcXd0cOZY+gF5tiIh8AiAjQnMmruDvuJWrTB0p6prcb/0kmUGqcki6gerXfR5oJA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75291},"main":"./dist/index.cjs.js","module":"./dist/index.esm.js","exports":{".":{"types":"./dist/libs/ky-sse-hook/src/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"}},"gitHead":"46fdc6446f05002915f0a51ebb7ba4f85c55f3dc","scripts":{"test":"npx vitest run","build":"npx rslib build"},"typings":"./dist/libs/ky-sse-hook/src/index.d.ts","_npmUser":{"name":"zhangwx616","email":"bdwenxi@gmail.com"},"_npmVersion":"11.9.0","description":"ky afterResponse hook for processing SSE streams","directories":{},"_nodeVersion":"22.15.0","dependencies":{"eventsource-parser":"^3.0.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"ky":"^1.14.0","express":"^4.18.2","@types/express":"^4.17.17"},"peerDependencies":{"ky":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/ky-sse-hook_1.0.1_1773662310786_0.44236533466262107","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@bdky/ky-sse-hook","version":"1.0.2","keywords":["sse","server-sent-events","ky","ky-hook","streaming","eventsource","fetch"],"author":{"name":"Baidu ACG FE Team","email":"chenfan02@baidu.com"},"license":"MIT","_id":"@bdky/ky-sse-hook@1.0.2","maintainers":[{"name":"zhangwx616","email":"bdwenxi@gmail.com"},{"name":"berwin0415","email":"hanbw@sina.cn"},{"name":"patrickli147","email":"patrick_li_147@foxmail.com"}],"contributors":[{"name":"FranckChen","email":"chenfan02@baidu.com"},{"name":"zhangwenxi","email":"zhangwenxi@baidu.com"}],"dist":{"shasum":"53e11fcfa1c9a661d915f0e35e7eb701b5a5184c","tarball":"https://registry.npmjs.org/@bdky/ky-sse-hook/-/ky-sse-hook-1.0.2.tgz","fileCount":6,"integrity":"sha512-IHKDICe3tCycrhn098I8+cE2jlIcPOCkQ2fNH0GZeaIM07hCBVH98pp7TuDF8g3f/cx6OrhBP/CvlmSHFxqnJQ==","signatures":[{"sig":"MEQCIBrxNTKzosJrocZJn9sM7wMnTdaO7014Mcy1sn+exSPgAiBE9QKsniDDGbQ/hkGQscl7gU8oFkLz2ZVW/zAtgvb7LQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75454},"main":"./dist/index.cjs.js","module":"./dist/index.esm.js","exports":{".":{"types":"./dist/libs/ky-sse-hook/src/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"}},"gitHead":"9cd8a99c0b4a8e81e55663597f819ac1c78f13d6","scripts":{"test":"npx vitest run","build":"npx rslib build"},"typings":"./dist/libs/ky-sse-hook/src/index.d.ts","_npmUser":{"name":"zhangwx616","email":"bdwenxi@gmail.com"},"_npmVersion":"11.9.0","description":"ky afterResponse hook for processing SSE streams","directories":{},"_nodeVersion":"22.15.0","dependencies":{"eventsource-parser":"^3.0.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"ky":"^1.14.0","express":"^4.18.2","@types/express":"^4.17.17"},"peerDependencies":{"ky":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/ky-sse-hook_1.0.2_1774009176084_0.8106301035752654","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@bdky/ky-sse-hook","version":"1.0.3","description":"ky afterResponse hook for processing SSE streams","keywords":["sse","server-sent-events","ky","ky-hook","streaming","eventsource","fetch"],"license":"MIT","author":{"name":"Baidu ACG FE Team","email":"chenfan02@baidu.com"},"contributors":[{"name":"FranckChen","email":"chenfan02@baidu.com"},{"name":"zhangwenxi","email":"zhangwenxi@baidu.com"}],"main":"./dist/index.cjs.js","module":"./dist/index.esm.js","typings":"./dist/libs/ky-sse-hook/src/index.d.ts","exports":{".":{"types":"./dist/libs/ky-sse-hook/src/index.d.ts","import":"./dist/index.esm.js","require":"./dist/index.cjs.js"}},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"scripts":{"build":"npx rslib build","test":"npx vitest run"},"dependencies":{"eventsource-parser":"^3.0.0"},"peerDependencies":{"ky":">=1.0.0"},"devDependencies":{"@types/express":"^4.17.17","express":"^4.18.2","ky":"^1.14.0"},"gitHead":"3d476f70e95200f3b20aea47ba3fea528640b3e9","_id":"@bdky/ky-sse-hook@1.0.3","_nodeVersion":"22.15.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-UC6kP/TCOIh/N1HY2jJqJwSkaiechJACzlr4I3v2e4rka4mzFE9i++ncfDcPvdP371k0JzQizWcTCrEWCEfB9A==","shasum":"4dffa133970a3e30d9cf7ff42e5a0ad15c43311b","tarball":"https://registry.npmjs.org/@bdky/ky-sse-hook/-/ky-sse-hook-1.0.3.tgz","fileCount":6,"unpackedSize":75454,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDiI6CBjbxRe+RZfYwKbWNYHhBEYSeW9DAvOUV2XyBTYAIgMNcMk6Ubf672F51vDlu8iANFZun3IgqMeMgleEUK8bc="}]},"_npmUser":{"name":"zhangwx616","email":"bdwenxi@gmail.com"},"directories":{},"maintainers":[{"name":"zhangwx616","email":"bdwenxi@gmail.com"},{"name":"berwin0415","email":"hanbw@sina.cn"},{"name":"patrickli147","email":"patrick_li_147@foxmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ky-sse-hook_1.0.3_1784620286996_0.645182326753875"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-16T11:58:30.705Z","modified":"2026-07-21T07:51:27.250Z","1.0.1":"2026-03-16T11:58:30.913Z","1.0.2":"2026-03-20T12:19:36.221Z","1.0.3":"2026-07-21T07:51:27.121Z"},"author":{"name":"Baidu ACG FE Team","email":"chenfan02@baidu.com"},"license":"MIT","keywords":["sse","server-sent-events","ky","ky-hook","streaming","eventsource","fetch"],"description":"ky afterResponse hook for processing SSE streams","contributors":[{"name":"FranckChen","email":"chenfan02@baidu.com"},{"name":"zhangwenxi","email":"zhangwenxi@baidu.com"}],"maintainers":[{"name":"zhangwx616","email":"bdwenxi@gmail.com"},{"name":"berwin0415","email":"hanbw@sina.cn"},{"name":"patrickli147","email":"patrick_li_147@foxmail.com"}],"readme":"# @bdky/ky-sse-hook\n\n[![npm version](https://img.shields.io/npm/v/@bdky/ky-sse-hook)](https://www.npmjs.com/package/@bdky/ky-sse-hook)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@bdky/ky-sse-hook)](https://bundlephobia.com/package/@bdky/ky-sse-hook)\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n[English](./README.md) | 简体中文\n\n一个轻量级的 [ky](https://github.com/sindresorhus/ky) `afterResponse` 钩子，用于处理 **Server-Sent Events (SSE)** 流式响应。基于 [eventsource-parser](https://github.com/rexxars/eventsource-parser) 实现符合规范的 SSE 解析。\n\n适用于 AI 对话流式输出、实时数据推送，以及任何需要通过 ky 消费 SSE 响应的场景。\n\n## 特性\n\n- **无缝 ky 集成** — 直接插入 ky 的 `hooks.afterResponse`\n- **符合规范的 SSE 解析** — 基于 eventsource-parser v3\n- **回调驱动 API** — `onData` / `onCompleted` / `onAborted` / `onEvent` / `onMessage` / `onReconnectInterval`\n- **至多一次完成保证** — 内部守卫确保 `onCompleted` 至多触发一次\n- **感知中止操作** — 检测 `AbortController` 信号并路由到 `onAborted`\n- **完整 TypeScript 支持** — 内置 TypeScript 类型声明\n\n## 安装\n\n```bash\n# npm\nnpm install @bdky/ky-sse-hook\n\n# yarn\nyarn add @bdky/ky-sse-hook\n\n# pnpm\npnpm add @bdky/ky-sse-hook\n```\n\n> **前置依赖：** 需要 `ky >= 1.0.0`，如果尚未安装请单独安装。\n\n同时发布 ESM 和 CJS 格式，附带 TypeScript 类型声明。\n\n## 快速开始\n\n```typescript\nimport ky from 'ky';\nimport {createHook} from '@bdky/ky-sse-hook';\n\nconst hook = createHook({\n    onData(message) {\n        console.log('收到数据:', message);\n    },\n    onCompleted(error) {\n        if (error) {\n            console.error('流失败:', error);\n            return;\n        }\n        console.log('流结束');\n    }\n});\n\nawait ky.post('https://api.example.com/chat/stream', {\n    json: {prompt: 'Hello, world!'},\n    hooks: {afterResponse: [hook]},\n    timeout: false\n});\n```\n\n> **注意：** 使用 SSE 流时请设置 `timeout: false`。长时间运行的流会被 ky 的默认超时中断。\n\n## API\n\n### `createHook(options)`\n\n创建一个 ky `AfterResponseHook`，将响应体作为 SSE 流进行消费。\n\n```typescript\nimport type {AfterResponseHook} from 'ky';\n\nconst hook: AfterResponseHook = createHook(options);\n```\n\n**行为说明：**\n- 如果响应**非成功**（`response.ok === false`）或**没有 body**，hook 会立即返回，不消费流。\n- 响应体通过 `ReadableStream` 读取，以 UTF-8 解码后送入 SSE 解析器。\n- hook 会**等待**流完全消费后才返回，避免 ky 尝试读取已被消费的 body 导致冲突。\n\n### `CreateHookOptions`\n\n| 属性 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `onData` | `(message: string) => void` | 是 | 每个 data 行触发。当单条 SSE 消息包含多个 `data:` 字段时，按 `\\n` 分割后逐行调用 `onData`。 |\n| `onCompleted` | `(error?: Error) => void` | 否 | 流结束时调用一次。如果流因错误（非中止）终止，会传入 `error` 参数。保证至多触发一次。 |\n| `onAborted` | `() => void` | 否 | 通过 `AbortController` 中止请求时调用。中止时 `onCompleted` **不会**被调用。 |\n| `onEvent` | `(event: EventSourceMessage) => void` | 否 | 收到 `data` 字段非空的 SSE 事件时，传入完整的 `EventSourceMessage`。空 `data` 的事件会被静默跳过。在 `onData` 之前触发。 |\n| `onMessage` | `(event: EventSourceMessage) => void` | 否 | `onEvent` 的别名。两者同时提供时都会被调用。空 `data` 的事件同样被跳过。 |\n| `onReconnectInterval` | `(value: number) => void` | 否 | SSE 流包含 `retry:` 指令时调用，参数为重连间隔（毫秒）。 |\n\n#### `onData` 与 `onEvent` / `onMessage` 的区别\n\n- **`onData`** 接收每个 `data:` 行的原始字符串内容。如果单条 SSE 消息有多个 `data:` 字段，每行触发一次 `onData`。\n- **`onEvent`** / **`onMessage`** 接收完整的 [`EventSourceMessage`](https://github.com/rexxars/eventsource-parser) 对象，包含 `data`、`event`、`id` 字段。每条 SSE 消息触发一次，在 `onData` 之前。\n\n### `EventSourceMessage`\n\n从 `eventsource-parser` 重新导出。类型结构：\n\n```typescript\ninterface EventSourceMessage {\n    data: string;\n    event?: string;\n    id?: string;\n}\n```\n\n## 使用示例\n\n### AI 对话流式输出\n\n解析 JSON 编码的 SSE 数据并累积响应：\n\n```typescript\nimport ky from 'ky';\nimport {createHook} from '@bdky/ky-sse-hook';\n\ninterface ChatChunk {\n    answer: string;\n    is_end: boolean;\n}\n\nlet fullResponse = '';\n\nconst hook = createHook({\n    onData(data) {\n        try {\n            const chunk: ChatChunk = JSON.parse(data);\n            fullResponse += chunk.answer;\n            console.log('当前响应:', fullResponse);\n        }\n        catch {\n            console.error('解析失败:', data);\n        }\n    },\n    onCompleted(error) {\n        if (error) {\n            console.error('流错误:', error);\n            return;\n        }\n        console.log('最终响应:', fullResponse);\n    }\n});\n\nawait ky.post('https://api.example.com/chat/stream', {\n    json: {\n        messages: [\n            {role: 'user', content: '介绍一下量子计算'}\n        ]\n    },\n    hooks: {afterResponse: [hook]},\n    timeout: false\n});\n```\n\n### 中止请求\n\n使用 `AbortController` 取消流式请求：\n\n```typescript\nimport ky from 'ky';\nimport {createHook} from '@bdky/ky-sse-hook';\n\nconst controller = new AbortController();\n\nconst hook = createHook({\n    onData(data) {\n        console.log('数据块:', data);\n\n        // 收到第一个数据块后中止\n        controller.abort();\n    },\n    onAborted() {\n        console.log('请求已被用户中止');\n    },\n    onCompleted(error) {\n        // 中止时不会调用\n        if (error) {\n            console.error('错误:', error);\n            return;\n        }\n        console.log('完成');\n    }\n});\n\nawait ky.post('https://api.example.com/chat/stream', {\n    json: {prompt: '给我讲一个长故事'},\n    hooks: {afterResponse: [hook]},\n    signal: controller.signal,\n    timeout: false\n});\n```\n\n### 完整事件元数据\n\n获取每个 SSE 事件的完整 `EventSourceMessage`：\n\n```typescript\nimport ky from 'ky';\nimport {createHook} from '@bdky/ky-sse-hook';\n\nconst hook = createHook({\n    onData(data) {\n        console.log('数据:', data);\n    },\n    onEvent(event) {\n        console.log('事件类型:', event.event);\n        console.log('事件 ID:', event.id);\n        console.log('事件数据:', event.data);\n    },\n    onReconnectInterval(interval) {\n        console.log('服务端建议重连间隔:', interval, 'ms');\n    }\n});\n\nawait ky.post('https://api.example.com/events', {\n    hooks: {afterResponse: [hook]},\n    timeout: false\n});\n```\n\n### 错误处理\n\n同时处理流级别和 HTTP 级别的错误：\n\n```typescript\nimport ky, {HTTPError} from 'ky';\nimport {createHook} from '@bdky/ky-sse-hook';\n\nconst hook = createHook({\n    onData(data) {\n        console.log('数据:', data);\n    },\n    onCompleted(error) {\n        if (error) {\n            console.error('流读取失败:', error.message);\n            return;\n        }\n        console.log('流正常完成');\n    }\n});\n\ntry {\n    await ky.post('https://api.example.com/stream', {\n        json: {prompt: 'Hello'},\n        hooks: {afterResponse: [hook]},\n        timeout: false\n    });\n}\ncatch (error) {\n    // ky 对非 2xx 响应抛出 HTTPError\n    // hook 会跳过非成功响应，因此 HTTP 错误\n    // 在这里处理，而不是在 onCompleted 中\n    if (error instanceof HTTPError) {\n        console.error('HTTP 错误:', error.response.status);\n    }\n}\n```\n\n## 工作原理\n\n```\nky.post(url, { hooks: { afterResponse: [hook] } })\n        │\n        ▼\n  response.ok && response.body?\n        │ 否 → return（跳过）\n        │ 是\n        ▼\n  response.body.getReader()\n        │\n        ▼\n  TextDecoder (UTF-8)\n        │\n        ▼\n  eventsource-parser\n        │\n        ├─ onEvent → options.onEvent()\n        │            options.onMessage()\n        │            data.split('\\n') → options.onData()（逐行）\n        │\n        └─ onRetry → options.onReconnectInterval()\n        │\n        ▼\n  流结束?\n   ├─ 正常结束    → onCompleted()\n   ├─ 被中止      → onAborted()\n   └─ 发生错误    → onCompleted(error)\n```\n\n1. hook 接收 ky 响应，检查是否成功且有可读的 body。\n2. 通过 `ReadableStream` reader 分块消费 body。\n3. 每个数据块通过 `TextDecoder` 从 `Uint8Array` 解码为字符串。\n4. 解码后的文本送入 `eventsource-parser`，解析器发出结构化的 SSE 事件。\n5. 流正常结束时，调用 `onCompleted()`（不带参数）。\n6. 如果请求通过 `AbortController` 中止，则调用 `onAborted()`。\n7. 如果读取过程中发生意外错误，调用 `onCompleted(error)`。\n\n## 浏览器兼容性\n\n| 浏览器 | 最低版本 |\n|--------|----------|\n| Chrome | >= 74 |\n| Firefox | >= 90 |\n| Safari | >= 14.1 |\n| Edge | >= 79 |\n| iOS Safari | >= 14.1 |\n| Android Chrome | >= 74 |\n\n需要支持 `ReadableStream`、`TextDecoder` 和 `fetch` API。\n\n## 常见问题\n\n### 为什么 hook 不抛出异常？\n\n这是设计选择。流读取错误通过 `onCompleted(error)` 传递，而不是抛出异常。这使调用方能够以回调风格处理错误，与 API 的其余部分保持一致，同时避免流式场景中的未捕获 Promise rejection。\n\n### 支持 GET 请求吗？\n\n支持。hook 适用于**任何 HTTP 方法**（GET、POST、PUT 等），只要响应返回 SSE body 即可。它接入 ky 的 `afterResponse` 钩子，该钩子无论请求方法如何都会触发。\n\n### 为什么同时有 `onEvent` 和 `onMessage`？\n\n它们是便利别名。两者接收相同的 `EventSourceMessage`，每个 SSE 事件都会触发两者。你可以选择在代码中感觉更自然的名称。如果两者都提供，两者都会被调用。\n\n### 非 2xx 响应怎么处理？\n\nhook 检查 `response.ok`，如果响应不成功则**跳过**处理。非 2xx 错误由 ky 内置的错误处理机制（如 `HTTPError`）处理，你可以在 ky 调用外层用标准 `try/catch` 捕获。\n\n### 会自动重连吗？\n\n不会。此 hook 处理的是**单次** SSE 响应。如果服务端发送了 `retry:` 指令，该值会转发到 `onReconnectInterval`，但重连逻辑由使用方自行实现。\n\n### 为什么要设置 `timeout: false`？\n\nSSE 流是长时间运行的连接。ky 的默认超时（10 秒）会在流完成前中断请求。设置 `timeout: false` 以禁用流式请求的超时：\n\n```typescript\nawait ky.post(url, {\n    hooks: {afterResponse: [hook]},\n    timeout: false\n});\n```\n\n## 相关项目\n\n- [ky](https://github.com/sindresorhus/ky) — 优雅的 HTTP 请求库\n- [eventsource-parser](https://github.com/rexxars/eventsource-parser) — 流式 SSE 解析器\n- [@bdky/aaas-pilot-kit](https://www.npmjs.com/package/@bdky/aaas-pilot-kit) — 内部使用此 hook 的 AI Pilot SDK\n\n## 许可证\n\nMIT\n","readmeFilename":"README.zh-CN.md"}