@baishuyun/chat-backend 0.0.8 → 0.0.9

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/.env.example CHANGED
@@ -15,3 +15,6 @@ BUILD_BOT_ID=xxxxx
15
15
 
16
16
  # fill bot id
17
17
  FILL_BOT_ID=xxxxx
18
+
19
+ # query bot id
20
+ QUERY_BOT_ID=xxxxx
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @baishuyun/chat-backend
2
2
 
3
+ ## 0.0.9
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @baishuyun/coze-provider@0.0.9
9
+ - @baishuyun/types@1.0.9
10
+
3
11
  ## 0.0.8
4
12
 
5
13
  ### Patch Changes
package/config/default.ts CHANGED
@@ -1,34 +1,43 @@
1
- import { asyncConfig } from "config/async.js";
1
+ import { asyncConfig } from 'config/async.js';
2
2
  // load async configurations
3
3
  const fetchRemoteConfig = async () => {
4
4
  return {
5
5
  // mock remote config
6
- foo: "bar",
6
+ foo: 'bar',
7
7
  };
8
8
  };
9
9
 
10
10
  export default {
11
11
  app: {
12
12
  port: process.env.PORT || 3001,
13
- host: process.env.HOST || "localhost",
13
+ host: process.env.HOST || 'localhost',
14
14
  },
15
+
15
16
  agent: {
16
- host: process.env.AGENT_HOST || "47.99.202.157",
17
+ host: process.env.AGENT_HOST || '47.99.202.157',
17
18
 
18
19
  apiAuthKey: process.env.COZE_API_KEY,
19
20
 
20
21
  form: {
21
22
  build: {
22
- botId: process.env.BUILD_BOT_ID || "7579927677256073216",
23
+ botId: process.env.BUILD_BOT_ID || '7579927677256073216',
23
24
  baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
24
25
  apiKey: process.env.BOT_API_KEY, // load from env
25
26
  },
26
27
 
27
28
  fill: {
28
- botId: process.env.FILL_BOT_ID || "7586483957357608960",
29
+ botId: process.env.FILL_BOT_ID || '7586483957357608960',
30
+ baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
31
+ apiKey: process.env.BOT_API_KEY, // load from env
32
+ ocrApiKey: process.env.OCR_API_KEY || '', // load from env
33
+ },
34
+ },
35
+
36
+ report: {
37
+ query: {
38
+ botId: process.env.QUERY_BOT_ID || '7595888372090929152',
29
39
  baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
30
40
  apiKey: process.env.BOT_API_KEY, // load from env
31
- ocrApiKey: process.env.OCR_API_KEY || "", // load from env
32
41
  },
33
42
  },
34
43
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/chat-backend",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,8 +22,8 @@
22
22
  "parse-sse": "^0.1.0",
23
23
  "pino": "^10.1.0",
24
24
  "zod": "^4.1.13",
25
- "@baishuyun/coze-provider": "0.0.8",
26
- "@baishuyun/types": "1.0.8"
25
+ "@baishuyun/coze-provider": "0.0.9",
26
+ "@baishuyun/types": "1.0.9"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/config": "^3.3.5",
@@ -34,7 +34,7 @@
34
34
  "pm2": "^6.0.14",
35
35
  "tsx": "^4.7.1",
36
36
  "typescript": "^5.8.3",
37
- "@baishuyun/typescript-config": "0.0.8"
37
+ "@baishuyun/typescript-config": "0.0.9"
38
38
  },
39
39
  "scripts": {
40
40
  "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
package/src/app/main.ts CHANGED
@@ -7,9 +7,11 @@ import app from '../config/hono.config.js';
7
7
 
8
8
  // 子路由
9
9
  import { createFormRouter } from '../routes/form/form.route.js';
10
+ import { createReportRouter } from '../routes/report/report.route.js';
10
11
 
11
12
  // 挂载子路由
12
13
  app.route('web/api/form', createFormRouter());
14
+ app.route('web/api/report', createReportRouter());
13
15
 
14
16
  // add test route
15
17
  app.get('web/api/test', (c) => {
@@ -0,0 +1,121 @@
1
+ import { type LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
+ import { JSONParser } from '@streamparser/json';
3
+ import { type IQueryResult } from '@baishuyun/types';
4
+ import {
5
+ createJsonStreamTransformer,
6
+ type IParserCtx,
7
+ } from '../../../utils/createJsonStreamTransformer.js';
8
+ import { isArrayElement } from './utils.js';
9
+
10
+ export const createQueryResultTransformer = (): TransformStream<LanguageModelV2StreamPart, any> => {
11
+ return createJsonStreamTransformer<IQueryResult>({
12
+ createJSONParser: createJSONParser,
13
+ onParseValue: handleParsedValue,
14
+ onParseError(error) {
15
+ console.error('JSON parsing error in query transformer:', error);
16
+ return '数据解析异常,请重试';
17
+ },
18
+ onErrorChunk(chunk) {
19
+ console.log('Unsupported chunk encountered in query transformer:', chunk);
20
+ return '';
21
+ },
22
+ });
23
+ };
24
+
25
+ function createJSONParser(): JSONParser {
26
+ return new JSONParser({
27
+ stringBufferSize: undefined,
28
+ numberBufferSize: undefined,
29
+ separator: '', // 使用默认路径分隔符 '.'
30
+ paths: [
31
+ '$', // 根对象(可选,如果需要完整对象)
32
+ '$.title', // 报表名称
33
+ '$.userID', // 用户ID
34
+ '$.forms', // 表单ID
35
+ '$.formIds.*', // 表单ID数组中的每个元素
36
+ '$.fields.*', // fields数组中的每个对象
37
+ '$.xFields.*', // xFields数组中的每个对象
38
+ '$.metrics.*', // metrics数组中的每个对象
39
+ '$.formulas.*', // 公式字段数组中的每个对象
40
+ '$.filter.rel', // 过滤条件关系
41
+ '$.filter.conds.*', // 过滤条件数组中的每个对象
42
+ '$.limit', // 限制条数
43
+ ],
44
+ keepStack: true, // 保持栈信息以便获取父级上下文
45
+ });
46
+ }
47
+
48
+ function handleParsedValue(ctx: IParserCtx<IQueryResult>): void {
49
+ const { parsedInfo, parsedResult, currentChunkId, deltaChunkEnqueuer: enqueueTextDelta } = ctx;
50
+ const { key, value, stack, parent } = parsedInfo;
51
+
52
+ // 快速提取顶层字段
53
+ // @ts-ignore
54
+ if (key === 'title') parsedResult.title = value;
55
+ // @ts-ignore
56
+ if (key === 'userID') parsedResult.userID = value;
57
+ // @ts-ignore
58
+ if (key === 'limit') parsedResult.limit = value;
59
+ // @ts-ignore
60
+ if (key === 'forms') parsedResult.formIds = [value];
61
+
62
+ // 处理数组类型
63
+ // @ts-ignore
64
+ if (key === 'rel' && stack.length >= 1 && stack[stack.length - 1]?.key === 'filter') {
65
+ if (!parsedResult.filter) parsedResult.filter = { rel: 'and', conds: [] };
66
+ parsedResult.filter.rel = value as any;
67
+ }
68
+
69
+ if (isArrayElement('fields', parsedInfo)) {
70
+ if (!parsedResult.fields) {
71
+ parsedResult.fields = [];
72
+ }
73
+ // @ts-ignore
74
+ parsedResult.fields?.push(value);
75
+ }
76
+
77
+ if (isArrayElement('formulas', parsedInfo)) {
78
+ if (!parsedResult.formulas) {
79
+ parsedResult.formulas = [];
80
+ }
81
+ // @ts-ignore
82
+ parsedResult.formulas.push(value);
83
+ }
84
+
85
+ if (isArrayElement('conds', parsedInfo)) {
86
+ console.log('✅ Parsed filter cond object:', value);
87
+ if (!parsedResult.filter) {
88
+ parsedResult.filter = { rel: 'and', conds: [] };
89
+ }
90
+ // @ts-ignore
91
+ parsedResult.filter.conds.push(value);
92
+ }
93
+
94
+ if (isArrayElement('metrics', parsedInfo)) {
95
+ if (!parsedResult.metrics) {
96
+ parsedResult.metrics = [];
97
+ }
98
+ // @ts-ignore
99
+ parsedResult.metrics.push(value);
100
+ }
101
+
102
+ if (isArrayElement('xFields', parsedInfo)) {
103
+ if (!parsedResult.xFields) {
104
+ parsedResult.xFields = [];
105
+ }
106
+ // @ts-ignore
107
+ parsedResult.xFields.push(value);
108
+ }
109
+
110
+ const validResult = JSON.stringify(parsedResult);
111
+
112
+ enqueueTextDelta(
113
+ `${JSON.stringify(value)},`,
114
+ {
115
+ type: 'query-stream-parsed-info',
116
+ result: validResult,
117
+ },
118
+ currentChunkId,
119
+ true
120
+ );
121
+ }
@@ -0,0 +1,17 @@
1
+ import { createCoze } from '@baishuyun/coze-provider';
2
+
3
+ import config from 'config';
4
+ import { createQueryResultTransformer } from './createQueryTransformStream.js';
5
+
6
+ export const createReportQueryModel = () => {
7
+ const reportQuery = createCoze({
8
+ apiKey: config.get<string>('agent.report.query.apiKey'),
9
+ baseURL: config.get<string>('agent.report.query.baseUrl'),
10
+ botId: config.get<string>('agent.report.query.botId'),
11
+ extraStreamTransformers: [createQueryResultTransformer],
12
+ });
13
+
14
+ const reportQueryModel = reportQuery.chat('chat');
15
+
16
+ return reportQueryModel;
17
+ };
@@ -0,0 +1,50 @@
1
+ import { type Context } from 'hono';
2
+ import { convertToModelMessages, streamText, type UIMessage, generateId } from 'ai';
3
+ import { createReportQueryModel } from './model.js';
4
+ import { buildExtraMsgParts } from './utils.js';
5
+
6
+ export const queryReport = async (c: Context) => {
7
+ let requestBody;
8
+
9
+ try {
10
+ const json = await c.req.json();
11
+ requestBody = json;
12
+ } catch (_) {
13
+ return c.json({ error: 'Invalid JSON' }, 400);
14
+ }
15
+
16
+ const uid = c.req.header('X-User-Id') || '';
17
+ const messages: UIMessage[] = requestBody.messages;
18
+
19
+ const extraText = buildExtraMsgParts({
20
+ appId: requestBody.appId,
21
+ userId: uid,
22
+ textOnly: true,
23
+ }) as string;
24
+
25
+ const lastUserTextPartOfMessage = messages
26
+ .slice()
27
+ .reverse()
28
+ .find((msg) => msg.role === 'user')
29
+ ?.parts.findLast((part) => part.type === 'text') as { type: 'text'; text: string } | undefined;
30
+
31
+ if (lastUserTextPartOfMessage) {
32
+ lastUserTextPartOfMessage.text = `${extraText}${lastUserTextPartOfMessage.text}`;
33
+ }
34
+
35
+ const modelMessages = convertToModelMessages(messages);
36
+ console.log('Final messages for report query:', JSON.stringify(modelMessages, null, 2));
37
+
38
+ const stream = streamText({
39
+ model: createReportQueryModel(),
40
+ messages: modelMessages,
41
+ includeRawChunks: true,
42
+ headers: {
43
+ 'x-user-id': uid,
44
+ },
45
+ });
46
+
47
+ return stream.toUIMessageStreamResponse({
48
+ originalMessages: messages, // 建议添加,便于消息 ID 管理
49
+ });
50
+ };
@@ -0,0 +1,54 @@
1
+ import type { LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
+ import type { ParsedElementInfo } from '@streamparser/json/utils/types/parsedElementInfo.js';
3
+ import type { TextUIPart, UIMessagePart } from 'ai';
4
+
5
+ export const buildExtraMsgParts = ({
6
+ appId,
7
+ userId,
8
+ textOnly,
9
+ }: {
10
+ appId: string;
11
+ userId: string;
12
+ textOnly?: boolean;
13
+ }): Array<TextUIPart> | string => {
14
+ if (textOnly) {
15
+ return `appID为${appId};用户ID为${userId};`;
16
+ }
17
+
18
+ return [
19
+ {
20
+ type: 'text',
21
+ text: `appID为${appId};`,
22
+ },
23
+ {
24
+ type: 'text',
25
+ text: `用户ID为${userId};`,
26
+ },
27
+ ];
28
+ };
29
+
30
+ export const queryChunkGuard = (chunk: LanguageModelV2StreamPart) => {
31
+ if (chunk.type !== 'text-delta' || !('delta' in chunk)) {
32
+ return false;
33
+ }
34
+
35
+ return true;
36
+ };
37
+
38
+ // JSON Parser 处理数组元素
39
+ // 当解析 $.elementKey.* 时:
40
+ // - key 是数组索引(number)
41
+ // - parent 是数组 []
42
+ // - stack 的最后一个元素的 key 是 elementKey
43
+ export const isArrayElement = (elementKey: string, parsedInfo: ParsedElementInfo): boolean => {
44
+ const { key, value, stack, parent } = parsedInfo;
45
+ return (
46
+ typeof key === 'number' &&
47
+ Array.isArray(parent) &&
48
+ stack.length >= 1 &&
49
+ stack[stack.length - 1]?.key === elementKey &&
50
+ typeof value === 'object' &&
51
+ value !== null &&
52
+ !Array.isArray(value)
53
+ );
54
+ };
@@ -0,0 +1,11 @@
1
+ import { Hono } from 'hono';
2
+ import { queryReport } from '../../controllers/report/query/query.controller.js';
3
+
4
+ export const createReportRouter = () => {
5
+ const reportRouter = new Hono();
6
+
7
+ // 智能问数
8
+ reportRouter.post('/query', queryReport);
9
+
10
+ return reportRouter;
11
+ };
@@ -0,0 +1,240 @@
1
+ import { type LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
+ import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
3
+ import { JSONParser } from '@streamparser/json';
4
+ import type { ParsedElementInfo } from '@streamparser/json/utils/types/parsedElementInfo.js';
5
+ import { logger } from '../logger/index.js';
6
+
7
+ /** onParseValue 回调接收的上下文 */
8
+ export interface IParserCtx<T> {
9
+ /** 累积的解析结果,可直接修改 */
10
+ parsedResult: T;
11
+ /** 当前触发 onValue 的解析信息 */
12
+ parsedInfo: ParsedElementInfo;
13
+ /** 向下游推送 text-delta chunk 的工具函数 */
14
+ deltaChunkEnqueuer: ReturnType<typeof createTextInfoEnqueuer>;
15
+ /** 当前正在处理的 chunk id */
16
+ currentChunkId: string;
17
+ }
18
+
19
+ /** 创建 JSON 流转换器的选项 */
20
+ export interface JsonStreamTransformerOptions<T> {
21
+ /** 创建 JSONParser 实例(由使用者决定路径、分隔符等解析细节) */
22
+ createJSONParser: () => JSONParser;
23
+ /** 每次解析出值时的回调 */
24
+ onParseValue: (ctx: IParserCtx<T>) => void;
25
+ /** chunk 过滤,返回 true 表示需要解析,默认检查 text-delta 类型 */
26
+ chunkGuard?: (chunk: LanguageModelV2StreamPart) => boolean;
27
+ /** 收到 error 类型 chunk 时的回调(可选) */
28
+ onErrorChunk?: (chunk: LanguageModelV2StreamPart) => void;
29
+ /** JSON 解析出错时的回调,返回要展示的错误消息(可选) */
30
+ onParseError?: (error: any) => string;
31
+ }
32
+
33
+ /**
34
+ * 创建一个 TransformStream,将 AI 流式响应中的 JSON 片段解析为结构化数据。
35
+ *
36
+ * 使用者只需关心:
37
+ * 1. `createJSONParser` — 决定 JSON 解析的路径 / 选项
38
+ * 2. `onParseValue` — 处理每个解析出的值
39
+ * 3. 错误处理(均可选)
40
+ */
41
+ export const createJsonStreamTransformer = <T>(
42
+ options: JsonStreamTransformerOptions<T>
43
+ ): TransformStream<LanguageModelV2StreamPart, any> => {
44
+ const processor = new JsonStreamProcessor<T>(options);
45
+ return new TransformStream<LanguageModelV2StreamPart, any>({
46
+ start: (ctrl) => processor.start(ctrl),
47
+ transform: (chunk, ctrl) => processor.transform(chunk, ctrl),
48
+ flush: (ctrl) => processor.flush(ctrl),
49
+ });
50
+ };
51
+
52
+ const BACKTICK_RE = /`?`?`?/g;
53
+ const JSON_PREFIX = 'json';
54
+
55
+ const ERROR_MESSAGES = {
56
+ PARSE_ERROR: '解析异常,请刷新重试',
57
+ TIMEOUT: '操作超时,请刷新重试',
58
+ GENERAL_ERROR: '发生错误,请刷新重试',
59
+ } as const;
60
+
61
+ type Controller = TransformStreamDefaultController<any>;
62
+ type ChunkWithIdDelta = Extract<LanguageModelV2StreamPart, { id: string; delta: string }>;
63
+
64
+ class JsonStreamProcessor<T> {
65
+ // -- 配置 --
66
+ private readonly options: JsonStreamTransformerOptions<T>;
67
+
68
+ // -- 运行时状态 --
69
+ private parser: JSONParser | null = null;
70
+ private result: T = {} as T;
71
+ private currentChunkId = '';
72
+ private parseCompleted: Promise<void> | null = null;
73
+ private resolveParseCompleted: (() => void) | null = null;
74
+ private terminated = false;
75
+
76
+ constructor(options: JsonStreamTransformerOptions<T>) {
77
+ this.options = options;
78
+ }
79
+
80
+ // ===================== 生命周期 =====================
81
+
82
+ /** TransformStream start:创建 parser、绑定回调 */
83
+ start(controller: Controller): void {
84
+ this.parser = this.options.createJSONParser();
85
+ this.parseCompleted = this.createCompletionPromise();
86
+ this.bindParserCallbacks(controller);
87
+ }
88
+
89
+ /** TransformStream transform:逐 chunk 处理 */
90
+ transform(chunk: LanguageModelV2StreamPart, controller: Controller): void {
91
+ if (this.terminated || !this.parser) {
92
+ logger.warn('JSON parser not initialized or stream already terminated');
93
+ return;
94
+ }
95
+
96
+ try {
97
+ // 1) error chunk → 通知下游后终止
98
+ if (chunk.type === 'error') {
99
+ this.options.onErrorChunk?.(chunk);
100
+ this.enqueueError(controller, ERROR_MESSAGES.GENERAL_ERROR);
101
+ return;
102
+ }
103
+
104
+ // 2) 不需要解析的 chunk → 透传
105
+ const guard = this.options.chunkGuard ?? defaultChunkGuard;
106
+ if (!guard(chunk)) {
107
+ controller.enqueue(chunk);
108
+ return;
109
+ }
110
+
111
+ // 3) 提取 delta 写入 parser
112
+ if (isChunkWithDelta(chunk)) {
113
+ this.currentChunkId = chunk.id;
114
+ this.parser.write(stripCodeFence(chunk.delta));
115
+ }
116
+ } catch (error) {
117
+ this.enqueueError(controller, error);
118
+ }
119
+ }
120
+
121
+ /** TransformStream flush:结束 parser 并等待所有回调完成 */
122
+ async flush(controller: Controller): Promise<void> {
123
+ try {
124
+ if (this.parser) {
125
+ this.parser.end();
126
+ if (this.parseCompleted) await this.parseCompleted;
127
+ logger.debug('Parser stopped gracefully');
128
+ }
129
+ if (!this.terminated) controller.terminate();
130
+ } catch (error) {
131
+ controller.error(`Cleanup error: ${error}`);
132
+ }
133
+ }
134
+
135
+ // ===================== Parser 回调 =====================
136
+
137
+ private bindParserCallbacks(controller: Controller): void {
138
+ if (!this.parser) return;
139
+
140
+ const enqueue = createTextInfoEnqueuer(controller);
141
+
142
+ this.parser.onValue = (parsedInfo: ParsedElementInfo) => {
143
+ logger.debug('enter onValue');
144
+
145
+ // 超时错误特殊处理
146
+ if (isTimeoutError(parsedInfo.value)) {
147
+ if (!this.terminated) {
148
+ enqueue(JSON.stringify(parsedInfo.value), {
149
+ type: 'agent-error',
150
+ error: ERROR_MESSAGES.TIMEOUT,
151
+ });
152
+ this.terminateStream(controller);
153
+ }
154
+ return;
155
+ }
156
+
157
+ this.options.onParseValue({
158
+ parsedResult: this.result,
159
+ parsedInfo,
160
+ deltaChunkEnqueuer: enqueue,
161
+ currentChunkId: this.currentChunkId,
162
+ });
163
+ };
164
+
165
+ this.parser.onError = (err: any) => {
166
+ logger.debug('enter onError: ' + err);
167
+ if (!this.terminated) {
168
+ const msg = this.options.onParseError
169
+ ? this.options.onParseError(err)
170
+ : ERROR_MESSAGES.GENERAL_ERROR;
171
+ logger.debug(msg);
172
+ enqueue('error', { type: 'agent-error', error: msg });
173
+ this.terminateStream(controller);
174
+ }
175
+ };
176
+
177
+ this.parser.onEnd = () => {
178
+ logger.debug('Parsing completed');
179
+ enqueue(' ', JSON.stringify({ appendConfirm: 'save-fields' }));
180
+ this.completeParsing();
181
+ };
182
+ }
183
+
184
+ /** 创建可外部 resolve 的 Promise,用于 flush 等待 parser 回调结束 */
185
+ private createCompletionPromise(): Promise<void> {
186
+ return new Promise<void>((resolve) => {
187
+ this.resolveParseCompleted = () => {
188
+ resolve();
189
+ logger.debug('Parsing completion promise resolved');
190
+ };
191
+ });
192
+ }
193
+
194
+ /** 标记解析完成,重置状态 */
195
+ private completeParsing(): void {
196
+ this.resolveParseCompleted?.();
197
+ this.parser = null;
198
+ this.result = {} as T;
199
+ this.currentChunkId = '';
200
+ this.parseCompleted = null;
201
+ this.resolveParseCompleted = null;
202
+ }
203
+
204
+ /** 安全地终止流并清理状态 */
205
+ private terminateStream(controller: Controller): void {
206
+ this.completeParsing();
207
+ this.terminated = true;
208
+ controller.terminate();
209
+ }
210
+
211
+ /** 向下游推送 agent-error 并终止流 */
212
+ private enqueueError(controller: Controller, error: unknown): void {
213
+ if (this.terminated) return;
214
+ const enqueue = createTextInfoEnqueuer(controller);
215
+ const msg = typeof error === 'string' ? error : ERROR_MESSAGES.PARSE_ERROR;
216
+ logger.error('JsonStreamProcessor error: ' + error);
217
+ // 先 enqueue 错误信息让下游能收到,再 terminate
218
+ // 注意:不能调 controller.error(),否则流进入 errored 状态,enqueue 不再生效
219
+ enqueue('error', { type: 'agent-error', error: msg });
220
+ this.terminateStream(controller);
221
+ }
222
+ }
223
+
224
+ /** 默认 chunkGuard:只处理带 delta 的 text-delta chunk */
225
+ function defaultChunkGuard(chunk: LanguageModelV2StreamPart): boolean {
226
+ return chunk.type === 'text-delta' && 'delta' in chunk;
227
+ }
228
+
229
+ function isChunkWithDelta(chunk: LanguageModelV2StreamPart): chunk is ChunkWithIdDelta {
230
+ return 'id' in chunk && 'delta' in chunk;
231
+ }
232
+
233
+ /** 去掉 AI 返回的代码围栏标记 (```) 和 json 前缀 */
234
+ function stripCodeFence(delta: string): string {
235
+ return delta.replace(BACKTICK_RE, '').replace(JSON_PREFIX, '');
236
+ }
237
+
238
+ function isTimeoutError(value: any): boolean {
239
+ return value?.error === true && value?.errorMessage === 'timeout';
240
+ }