feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# @coze-arch/fetch-stream
fetch stream vanilla js
## Overview
This package is part of the Coze Studio monorepo and provides api & networking functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/fetch-stream": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/fetch-stream';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
### Exports
- `isFetchStreamErrorInfo,
fetchStream,
FetchStreamErrorCode,
type FetchSteamConfig,
type FetchStreamErrorInfo,`
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { FetchStreamErrorCode, type ValidateResult } from '../src/type';
describe('type definitions', () => {
describe('FetchStreamErrorCode', () => {
it('应该包含正确的错误码', () => {
expect(FetchStreamErrorCode.FetchException).toBe(10001);
expect(FetchStreamErrorCode.HttpChunkStreamingException).toBe(10002);
});
});
describe('ValidateResult', () => {
it('应该支持成功状态', () => {
const successResult: ValidateResult = { status: 'success' };
expect(successResult.status).toBe('success');
});
it('应该支持错误状态', () => {
const errorResult: ValidateResult = {
status: 'error',
error: new Error('Test error'),
};
expect(errorResult.status).toBe('error');
expect(errorResult.error).toBeInstanceOf(Error);
});
});
});

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
onStart,
validateChunk,
isFetchStreamErrorInfo,
getStreamingErrorInfo,
getFetchErrorInfo,
isAbortError,
} from '../src/utils';
import { FetchStreamErrorCode } from '../src/type';
describe('utils', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('onStart', () => {
it('应该调用 inputOnStart 函数', async () => {
const inputOnStart = vi.fn().mockResolvedValue(undefined);
const mockResponse = {
ok: true,
body: new ReadableStream(),
status: 200,
} as Response;
await onStart(mockResponse, inputOnStart);
expect(inputOnStart).toHaveBeenCalledWith(mockResponse);
});
it('当 response.ok 为 false 时应该抛出错误', async () => {
const mockResponse = {
ok: false,
body: new ReadableStream(),
status: 500,
} as Response;
await expect(onStart(mockResponse, undefined)).rejects.toThrow(
'Invalid Response, ResponseStatus: 500',
);
});
it('当 response.body 为 null 时应该抛出错误', async () => {
const mockResponse = {
ok: true,
body: null,
status: 200,
} as Response;
await expect(onStart(mockResponse, undefined)).rejects.toThrow(
'Invalid Response, ResponseStatus: 200',
);
});
it('当 inputOnStart 抛出错误时应该传播错误', async () => {
const inputOnStart = vi.fn().mockRejectedValue(new Error('Custom error'));
const mockResponse = {
ok: true,
body: new ReadableStream(),
status: 200,
} as Response;
await expect(onStart(mockResponse, inputOnStart)).rejects.toThrow(
'Custom error',
);
});
});
describe('validateChunk', () => {
it('应该成功验证正常的文本块', () => {
expect(() => validateChunk('normal text')).not.toThrow();
});
it('应该成功验证包含 code: 0 的 JSON', () => {
const chunk = JSON.stringify({ code: 0, msg: 'success' });
expect(() => validateChunk(chunk)).not.toThrow();
});
it('应该抛出包含非零 code 的 JSON 对象', () => {
const errorObj = { code: 400, msg: 'Bad Request' };
const chunk = JSON.stringify(errorObj);
expect(() => validateChunk(chunk)).toThrow();
});
it('应该成功处理无效的 JSON', () => {
expect(() => validateChunk('invalid json {')).not.toThrow();
});
it('应该成功处理非对象的 JSON', () => {
expect(() => validateChunk('"string"')).not.toThrow();
expect(() => validateChunk('123')).not.toThrow();
expect(() => validateChunk('true')).not.toThrow();
});
it('应该成功处理没有 code 字段的对象', () => {
const chunk = JSON.stringify({ msg: 'no code field' });
expect(() => validateChunk(chunk)).not.toThrow();
});
});
describe('isFetchStreamErrorInfo', () => {
it('应该识别有效的 FetchStreamErrorInfo', () => {
const errorInfo = { code: 400, msg: 'Error message' };
expect(isFetchStreamErrorInfo(errorInfo)).toBe(true);
});
it('应该拒绝缺少 code 字段的对象', () => {
const obj = { msg: 'Error message' };
expect(isFetchStreamErrorInfo(obj)).toBe(false);
});
it('应该拒绝缺少 msg 字段的对象', () => {
const obj = { code: 400 };
expect(isFetchStreamErrorInfo(obj)).toBe(false);
});
it('应该拒绝非对象值', () => {
expect(isFetchStreamErrorInfo(null)).toBe(false);
expect(isFetchStreamErrorInfo(undefined)).toBe(false);
expect(isFetchStreamErrorInfo('string')).toBe(false);
expect(isFetchStreamErrorInfo(123)).toBe(false);
});
});
describe('getStreamingErrorInfo', () => {
it('应该从 Error 对象中提取消息', () => {
const error = new Error('Test error message');
const result = getStreamingErrorInfo(error);
expect(result).toEqual({
msg: 'Test error message',
code: FetchStreamErrorCode.HttpChunkStreamingException,
error,
});
});
it('应该从 FetchStreamErrorInfo 对象中提取信息', () => {
const errorInfo = { code: 400, msg: 'Custom error' };
const result = getStreamingErrorInfo(errorInfo);
expect(result).toEqual({
msg: 'Custom error',
code: 400,
error: errorInfo,
});
});
it('应该处理未知错误类型', () => {
const error = 'string error';
const result = getStreamingErrorInfo(error);
expect(result).toEqual({
msg: 'An exception occurred during the process of dealing with HTTP chunked streaming response.',
code: FetchStreamErrorCode.HttpChunkStreamingException,
error,
});
});
});
describe('getFetchErrorInfo', () => {
it('应该从 Error 对象中提取消息', () => {
const error = new Error('Fetch failed');
const result = getFetchErrorInfo(error);
expect(result).toEqual({
msg: 'Fetch failed',
code: FetchStreamErrorCode.FetchException,
error,
});
});
it('应该处理非 Error 对象', () => {
const error = 'fetch error';
const result = getFetchErrorInfo(error);
expect(result).toEqual({
msg: 'An exception occurred during the fetch',
code: FetchStreamErrorCode.FetchException,
error,
});
});
});
describe('isAbortError', () => {
it('应该识别 AbortError', () => {
const abortError = new DOMException(
'The operation was aborted',
'AbortError',
);
expect(isAbortError(abortError)).toBe(true);
});
it('应该拒绝其他 DOMException', () => {
const otherError = new DOMException('Other error', 'OtherError');
expect(isAbortError(otherError)).toBe(false);
});
it('应该拒绝普通 Error', () => {
const error = new Error('Normal error');
expect(isAbortError(error)).toBe(false);
});
it('应该拒绝非错误对象', () => {
expect(isAbortError('string')).toBe(false);
expect(isAbortError(null)).toBe(false);
expect(isAbortError(undefined)).toBe(false);
});
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 0,
"incrementCoverage": 0
}
}

View File

@@ -0,0 +1,9 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'node',
rules: {
'no-console': 'error',
},
});

View File

@@ -0,0 +1,30 @@
{
"name": "@coze-arch/fetch-stream",
"version": "0.0.1",
"description": "fetch stream vanilla js",
"license": "Apache-2.0",
"author": "gaoyuanhan.duty@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@mattiasbuelens/web-streams-adapter": "~0.1.0",
"eventsource-parser": "^1.0.0",
"web-streams-polyfill": "~3.3.2"
},
"devDependencies": {
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/node": "^18",
"@vitest/coverage-v8": "~3.0.5",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
}
}

View File

@@ -0,0 +1,299 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable max-lines-per-function */
import {
createParser,
type ParseEvent,
type EventSourceParser,
} from 'eventsource-parser';
import {
getFetchErrorInfo,
getStreamingErrorInfo,
isAbortError,
onStart,
validateChunk,
} from './utils';
import { type FetchSteamConfig } from './type';
/** 发起流式消息拉取的请求 */
export async function fetchStream<Message = ParseEvent, DataClump = unknown>(
requestInfo: RequestInfo,
{
onStart: inputOnStart,
onError,
onAllSuccess,
onFetchStart,
onFetchSuccess,
onStartReadStream,
onMessage,
fetch: inputFetch,
dataClump,
signal,
streamParser,
totalFetchTimeout,
onTotalFetchTimeout,
betweenChunkTimeout,
onBetweenChunkTimeout,
validateMessage,
...rest
}: FetchSteamConfig<Message, DataClump>,
): Promise<void> {
const webStreamsPolyfill = await import(
/*webpackChunkName: "web-streams-polyfill"*/ 'web-streams-polyfill/ponyfill'
);
const { ReadableStream, WritableStream, TransformStream } =
webStreamsPolyfill as {
ReadableStream?: typeof globalThis.ReadableStream;
WritableStream: typeof globalThis.WritableStream;
TransformStream: typeof globalThis.TransformStream;
};
const { createReadableStreamWrapper } = await import(
/*webpackChunkName: "web-streams-polyfill"*/ '@mattiasbuelens/web-streams-adapter'
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const readableStreamWrapper = createReadableStreamWrapper(ReadableStream!);
return new Promise<void>(resolve => {
const decoder = new TextDecoder();
const fetch = inputFetch ?? window.fetch;
let totalFetchTimer: ReturnType<typeof setTimeout> | null = null;
let betweenChunkTimer: ReturnType<typeof setTimeout> | null = null;
/**
* clear 时机
* 所有异常退出
* create 函数 return
* readStream 结束
* abortSignal 触发
*/
const clearTotalFetchTimer = () => {
if (!totalFetchTimer) {
return;
}
clearTimeout(totalFetchTimer);
totalFetchTimer = null;
};
/**
* set 时机
* fetch 之间 set 一次, 只此一次
*/
const setTotalFetchTimer = () => {
if (totalFetchTimeout && onTotalFetchTimeout) {
totalFetchTimer = setTimeout(() => {
onTotalFetchTimeout(dataClump);
clearTotalFetchTimer();
}, totalFetchTimeout);
}
};
/**
* clear 时机
* readStream 异常退出
* readStream 结束
* 收到了新 chunk
* abortSignal 触发
*/
const clearBetweenChunkTimer = () => {
if (!betweenChunkTimer) {
return;
}
clearTimeout(betweenChunkTimer);
betweenChunkTimer = null;
};
/**
* set 时机
* readStream 之前 set 一次
* 每次收到 chunk 并执行了 clearBetweenChunkTimer 时 set 一次
*/
const setBetweenChunkTimer = () => {
if (betweenChunkTimeout && onBetweenChunkTimeout) {
betweenChunkTimer = setTimeout(() => {
onBetweenChunkTimeout(dataClump);
clearBetweenChunkTimer();
}, betweenChunkTimeout);
}
};
signal?.addEventListener('abort', () => {
// 此处 abort 后下方 readableStream 与 writableStream 都会停止
clearTotalFetchTimer();
clearBetweenChunkTimer();
resolve();
});
const fetchAndVerifyResponse = async () => {
try {
setTotalFetchTimer();
onFetchStart?.(dataClump);
const response = await fetch(requestInfo, {
signal,
...rest,
});
await onStart(response, inputOnStart);
onFetchSuccess?.(dataClump);
return response;
} catch (error) {
/**
* 这里会被 catch 的错误
* fetch 服务端返回异常
* js error例如被 onStart 抛出的
* fetch 过程中 signal 被 abort
*/
// 被 abort 不认为是异常,不调用 onError
if (isAbortError(error)) {
return;
}
clearTotalFetchTimer();
onError?.({
fetchStreamError: getFetchErrorInfo(error),
dataClump,
});
}
};
const readStream = async (
responseBody: globalThis.ReadableStream<Uint8Array>,
) => {
setBetweenChunkTimer();
let parser: EventSourceParser;
const streamTransformer = new TransformStream<ArrayBuffer, Message>({
start(controller) {
parser = createParser(parseEvent => {
if (!streamParser) {
controller.enqueue(parseEvent as Message);
return;
}
const terminateFn = controller.terminate;
const onParseErrorFn = controller.error;
const result = streamParser?.(parseEvent, {
terminate: terminateFn.bind(controller),
onParseError: onParseErrorFn.bind(controller),
});
if (result) {
controller.enqueue(result);
}
});
},
transform(chunk, controller) {
clearBetweenChunkTimer();
setBetweenChunkTimer();
const decodedChunk = decoder.decode(chunk, { stream: true });
try {
//
validateChunk(decodedChunk);
// 上方 start 会在 TransformStream 被构建的同时执行,所以此处执行时能取到 parser
parser.feed(decodedChunk);
} catch (chunkError) {
// 处理 validateChunk 抛出的业务错误
// 服务端不会流式返回业务错误,错误结构:{ msg: 'xxx', code: 123456 }
controller.error(chunkError);
}
},
});
const streamWriter = new WritableStream<Message>({
async write(chunk, controller) {
// 写消息异步化 避免回调中的错误 panic 管道流
await Promise.resolve();
const param = { message: chunk, dataClump };
const validateResult = validateMessage?.(param);
if (validateResult && validateResult.status === 'error') {
/**
* 会中断 WritableStream, 即使还有数据也会被中断, 不会再写了
*/
throw validateResult.error;
}
onMessage?.(param);
},
});
try {
onStartReadStream?.(dataClump);
await (
readableStreamWrapper(
responseBody,
) as unknown as ReadableStream<ArrayBuffer>
)
.pipeThrough(streamTransformer)
.pipeTo(streamWriter);
clearTotalFetchTimer();
clearBetweenChunkTimer();
onAllSuccess?.(dataClump);
resolve();
} catch (streamError) {
/**
* 这里会被 catch 的错误
* 流式返回中服务端异常
* js error
* 流式返回过程中被 signal 被 abort
* 上方 onParseErrorFn 被调用
*/
// 被 abort 不认为是异常,不调用 onError
if (isAbortError(streamError)) {
return;
}
clearTotalFetchTimer();
clearBetweenChunkTimer();
onError?.({
fetchStreamError: getStreamingErrorInfo(streamError),
dataClump,
});
}
};
async function create(): Promise<void> {
const response = await fetchAndVerifyResponse();
const body = response?.body;
// response 不合法与没有 body 的错误在上方 onStart 中处理过
if (!body) {
clearTotalFetchTimer();
return;
}
await readStream(body);
}
create();
});
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isFetchStreamErrorInfo } from './utils';
import {
FetchStreamErrorCode,
type FetchSteamConfig,
type FetchStreamErrorInfo,
} from './type';
import { fetchStream } from './fetch-stream';
export {
isFetchStreamErrorInfo,
fetchStream,
FetchStreamErrorCode,
type FetchSteamConfig,
type FetchStreamErrorInfo,
};

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ParseEvent } from 'eventsource-parser';
export enum FetchStreamErrorCode {
FetchException = 10001,
HttpChunkStreamingException = 10002,
}
export interface FetchStreamErrorInfo {
code: FetchStreamErrorCode | number;
msg: string;
}
export interface FetchStreamError extends FetchStreamErrorInfo {
error: unknown;
}
export type ValidateResult =
| {
status: 'success';
}
| {
status: 'error';
error: Error;
};
/**
* {@link RequestInfo} 与 {@link RequestInit} 是 Fetch 原有参数类型
*/
export interface FetchSteamConfig<Message = ParseEvent, DataClump = unknown>
extends RequestInit {
/**
* 当开始 fetch时调用
*/
onFetchStart?: (params?: DataClump) => void;
/**
* 当 fetch 返回 response 时调用此方法。使用这个方法来验证 Response 是否符合预期,当不符合预期时抛出错误
* 无论是否提供此方法,会自动校验 Response.ok 标志位与 Response.body 是否存在
*/
onStart?: (response: Response) => Promise<void>;
/**
* 当 fetch 成功返回 response 并且 onStart 成功后触发此回调
*/
onFetchSuccess?: (params?: DataClump) => void;
/**
* 开始读取 ReadableStream 时触发此回调。onFetchSuccess 后紧接着会触发这个回调
*/
onStartReadStream?: (params?: DataClump) => void;
/**
* 流式过程中解析服务端返回的 chunk 数据,返回值符合 {@link Message} 类型时,预期将在后续 {@link onMessage} 方法中响应
* 可在解析过程中进行中断或抛出错误,抛出错误同时会中断整个流式解析
* 如果不提供则由 onMessage 直接响应 chunk 数据
*/
streamParser?: (
parseEvent: ParseEvent,
method: {
/**
* 中止当前流式读取行为
*/
terminate: () => void;
/**
* @deprecated
* 抛出错误,同时中止当前流式读取行为,如果流中还有正常数据未被读取,也会被一起终止掉
*/
onParseError: (error: FetchStreamErrorInfo) => void;
},
) => Message | undefined;
/**
* 在 onMessage 回调之前执行。对业务错误的处理和抛出推荐在这个回调处理
*/
validateMessage?: (params: {
message: Message;
dataClump?: DataClump;
}) => ValidateResult;
/**
* 接收到服务端 Chunk 数据并经过 parse如果有如果过程中无异常则调用此方法
*/
onMessage?: (params: { message: Message; dataClump?: DataClump }) => void;
/**
* 当 fetchStream resolve 时调用此方法
*/
onAllSuccess?: (params?: DataClump) => void;
/**
* fetchStream 整个过程中出现任意错误会调用此方法,包括 fetch / 流式处理 chunk / response 非法等
* 不会自动重试
*/
onError?: (params: {
fetchStreamError: FetchStreamError;
dataClump?: DataClump;
}) => void;
/** Fetch 方法,默认为 window.fetch */
fetch?: typeof fetch;
/**
* {@link https://book-refactoring2.ifmicro.com/docs/ch3.html#310-%E6%95%B0%E6%8D%AE%E6%B3%A5%E5%9B%A2%EF%BC%88data-clumps%EF%BC%89}
* 如果你想为每个 fetchStream 维护一些业务数据、状态,推荐在此处传入抽象后的数据实例。它们会在每个回调函数中出现
*/
dataClump?: DataClump;
/**
* fetch stream 整个过程的超时时长, 单位: ms。缺省或者传入 0 代表不开启定时器
*/
totalFetchTimeout?: number;
/**
* 当设置了 totalFetchTimeout, 并且到期时触发此回调。除此外不会有其余副作用例如abort 请求。请调用方根据需要自行处理
*/
onTotalFetchTimeout?: (params?: DataClump) => void;
/**
* chunk 之间超时时长, 处理 stream 过程中, 从收到上一个 chunk 开始计时, 收到下一个 chunk 时清除定时并重新计时
* 缺省或者传入 0 代表不开启定时器, 单位: ms
*/
betweenChunkTimeout?: number;
/**
* 当设置了 chunkTimeout并且定时器到期时触发此回调。除此外不会有其余副作用例如abort 请求。请调用方根据需要自行处理
*/
onBetweenChunkTimeout?: (params?: DataClump) => void;
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type FetchSteamConfig,
FetchStreamErrorCode,
type FetchStreamErrorInfo,
type FetchStreamError,
} from './type';
export async function onStart(
response: Response,
inputOnStart: FetchSteamConfig<unknown, unknown>['onStart'],
): Promise<void> {
await inputOnStart?.(response);
if (!(response.ok && response.body)) {
throw new Error(`Invalid Response, ResponseStatus: ${response.status}`);
}
}
export function validateChunk(decodedChunk: string): void {
let json: unknown;
try {
json = JSON.parse(decodedChunk);
// eslint-disable-next-line @coze-arch/no-empty-catch, @coze-arch/use-error-in-catch -- 设计如此
} catch {
/**
* 此处捕获 JSON.parse 错误不做任何处理
* 正常流式返回 json 解析失败才是正常的
*/
}
if (
typeof json === 'object' &&
json !== null &&
'code' in json &&
json.code !== 0
) {
throw json;
}
}
export function isFetchStreamErrorInfo(
error: unknown,
): error is FetchStreamErrorInfo {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
'msg' in error
);
}
export function getStreamingErrorInfo(error: unknown): FetchStreamError {
let errorMsg =
'An exception occurred during the process of dealing with HTTP chunked streaming response.';
let errorCode = FetchStreamErrorCode.HttpChunkStreamingException;
if (error instanceof Error) {
errorMsg = error.message;
}
if (isFetchStreamErrorInfo(error)) {
errorMsg = error.msg;
errorCode = error.code;
}
return {
msg: errorMsg,
code: errorCode,
error,
};
}
export function getFetchErrorInfo(error: unknown): FetchStreamError {
const errorMsg = 'An exception occurred during the fetch';
const errorCode = FetchStreamErrorCode.FetchException;
return {
msg: error instanceof Error ? error.message : errorMsg,
code: errorCode,
error,
};
}
export function isAbortError(error: unknown): boolean {
return error instanceof DOMException && error.name === 'AbortError';
}

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"compilerOptions": {
"types": [],
"moduleResolution": "Node10",
"module": "CommonJS",
"lib": ["ES2022", "DOM"],
"strictNullChecks": true,
"noImplicitAny": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,21 @@
{
"extends": "@coze-arch/ts-config/tsconfig.node.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "__tests__/**/*.json", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"moduleResolution": "Node10",
"module": "CommonJS",
"lib": ["ES2022", "DOM"],
"strictNullChecks": true,
"noImplicitAny": true
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'node',
});