@baishuyun/chat-backend 0.0.4 → 0.0.7

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.
@@ -2,6 +2,7 @@ import { type LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
2
  import { JSONParser } from '@streamparser/json';
3
3
  import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
4
4
  import { getLastSubFormField } from './utils.js';
5
+ import { fillingChunkGuard as chunkGuard } from './utils.js';
5
6
  import { logger } from '../../../logger/index.js';
6
7
 
7
8
  export const createFieldsFillingResultTransformer = (enableJsonParser: boolean) => {
@@ -11,25 +12,12 @@ export const createFieldsFillingResultTransformer = (enableJsonParser: boolean)
11
12
  let parseCompleted: Promise<void>;
12
13
  let resolveParseCompleted: () => void;
13
14
 
14
- const chunkGuard = (chunk: LanguageModelV2StreamPart): boolean => {
15
- if (!enableJsonParser) {
16
- return false;
17
- }
18
-
19
- if (chunk.type !== 'text-delta' || !('delta' in chunk)) {
20
- return false;
21
- }
22
-
23
- return true;
24
- };
25
-
26
15
  const transformer = {
27
16
  flush: async (controller: TransformStreamDefaultController<any>) => {
28
17
  try {
29
18
  if (parser) {
30
- await parseCompleted; // 等待闭包中的完成标记
19
+ await parseCompleted;
31
20
  logger.debug('stop parser');
32
- // parser.end(); // 此时调用 end() 不会出现未完成 Token 异常
33
21
  }
34
22
  controller.terminate(); // 信号通知下游可读流已关闭
35
23
  } catch (e) {
@@ -47,6 +35,8 @@ export const createFieldsFillingResultTransformer = (enableJsonParser: boolean)
47
35
  return;
48
36
  }
49
37
 
38
+ const enqueueTextDelta = createTextInfoEnqueuer(controller);
39
+
50
40
  try {
51
41
  if (chunkGuard(chunk)) {
52
42
  if ('id' in chunk && 'delta' in chunk) {
@@ -61,7 +51,15 @@ export const createFieldsFillingResultTransformer = (enableJsonParser: boolean)
61
51
  controller.enqueue(chunk);
62
52
  }
63
53
  } catch (e) {
54
+ logger.debug('exception in transform while processing filling chunk');
64
55
  controller.error(e);
56
+ enqueueTextDelta('error', {
57
+ type: 'agent-error',
58
+ error: '解析异常,请刷新重试',
59
+ });
60
+
61
+ resolveParseCompleted?.();
62
+ controller.terminate(); // 信号通知下游可读流已关闭
65
63
  }
66
64
  },
67
65
  start: (controller: TransformStreamDefaultController<any>) => {
@@ -80,6 +78,17 @@ export const createFieldsFillingResultTransformer = (enableJsonParser: boolean)
80
78
  parser.onValue = (parsedInfo: any) => {
81
79
  const value = parsedInfo.value;
82
80
 
81
+ if (value.error && value.errorMessage === 'timeout') {
82
+ enqueueTextDelta(JSON.stringify(value), {
83
+ type: 'agent-error',
84
+ error: '操作超时,请刷新重试',
85
+ });
86
+
87
+ resolveParseCompleted?.();
88
+ controller.terminate(); // 信号通知下游可读流已关闭
89
+ return;
90
+ }
91
+
83
92
  const subFormField = getLastSubFormField(parsedInfo);
84
93
  if (subFormField) {
85
94
  enqueueTextDelta(JSON.stringify(value), {
@@ -106,17 +115,17 @@ export const createFieldsFillingResultTransformer = (enableJsonParser: boolean)
106
115
 
107
116
  let errorLogged = false;
108
117
  parser.onError = (err: any) => {
109
- // controller.enqueue({
110
- // type: "error",
111
- // error: "JsonWidgetStream: JSON Parsing Error:" + err.message,
112
- // });
113
118
  if (!errorLogged) {
114
119
  console.error('JsonWidgetStream: JSON Parsing Error:', err);
115
120
  errorLogged = true;
116
121
  }
117
122
 
118
- // close stream
119
- logger.error('JsonStream: closing stream due to parsing error');
123
+ enqueueTextDelta('error', {
124
+ type: 'agent-error',
125
+ error: '操作超时,请刷新重试',
126
+ });
127
+
128
+ resolveParseCompleted?.();
120
129
  controller.terminate(); // 信号通知下游可读流已关闭
121
130
  };
122
131
 
@@ -1,7 +1,9 @@
1
1
  import { type Context } from 'hono';
2
- import { convertToModelMessages, streamText } from 'ai';
2
+ import { convertToModelMessages, streamText, type UIMessage, generateId } from 'ai';
3
3
  import { logger } from '../../../logger/index.js';
4
4
  import { createFillingModel } from './model.js';
5
+ import { mode2part } from './utils.js';
6
+ import { batchFillForm } from './batch-fill.controller.js';
5
7
 
6
8
  /**
7
9
  * 表单填写
@@ -19,13 +21,29 @@ export const fillForm = async (c: Context) => {
19
21
  }
20
22
 
21
23
  const formStructure = requestBody.formStructure;
24
+ const mode = requestBody.mode;
25
+ const num = requestBody.count;
26
+ const messages: UIMessage[] = requestBody.messages;
27
+
28
+ // 如果是批量填写,调用批量填写接口
29
+ if (mode === 'batch') {
30
+ return batchFillForm(c);
31
+ }
32
+
22
33
  if (formStructure) {
23
- try {
24
- let lastMsg = requestBody.messages[requestBody.messages.length - 1];
34
+ const extraPart = mode2part(mode, num);
25
35
 
26
- lastMsg.parts.push({
27
- type: 'text',
28
- text: JSON.stringify(formStructure),
36
+ try {
37
+ messages.push({
38
+ id: generateId(),
39
+ role: 'user',
40
+ parts: [
41
+ {
42
+ type: 'text',
43
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
44
+ },
45
+ extraPart,
46
+ ],
29
47
  });
30
48
  } catch (e) {
31
49
  logger.error('Failed to append form structure');
@@ -34,12 +52,18 @@ export const fillForm = async (c: Context) => {
34
52
 
35
53
  const stream = streamText({
36
54
  model: createFillingModel(),
37
- messages: convertToModelMessages(requestBody.messages),
55
+ messages: convertToModelMessages(messages),
38
56
  includeRawChunks: true,
39
57
  headers: {
40
58
  'x-user-stage': requestBody.stage,
59
+ 'x-user-var': JSON.stringify({
60
+ mode: requestBody.mode,
61
+ }),
62
+ 'x-user-token': c.req.header('authorization') || '',
41
63
  },
42
64
  });
43
65
 
44
- return stream.toUIMessageStreamResponse();
66
+ return stream.toUIMessageStreamResponse({
67
+ originalMessages: messages, // 建议添加,便于消息 ID 管理
68
+ });
45
69
  };
@@ -2,13 +2,27 @@ import { createCoze } from '@baishuyun/coze-provider';
2
2
  import { createFieldsFillingResultTransformer } from './createFieldsFillingResultTransformStream.js';
3
3
 
4
4
  import config from 'config';
5
+ import { createBatchFillingResultTransformer } from './createBatchFillingTransformStream.js';
5
6
 
6
7
  export const createFillingModel = (enableParser: boolean = true) => {
7
8
  const aiFormFilling = createCoze({
8
9
  apiKey: config.get<string>('agent.form.fill.apiKey'),
9
10
  baseURL: config.get<string>('agent.form.fill.baseUrl'),
10
11
  botId: config.get<string>('agent.form.fill.botId'),
11
- extraStreamTransformers: [createFieldsFillingResultTransformer(enableParser)],
12
+ extraStreamTransformers: [() => createFieldsFillingResultTransformer(enableParser)],
13
+ });
14
+
15
+ const formFillingModel = aiFormFilling.chat('chat');
16
+
17
+ return formFillingModel;
18
+ };
19
+
20
+ export const createBatchFillingModel = (enableParser: boolean = true) => {
21
+ const aiFormFilling = createCoze({
22
+ apiKey: config.get<string>('agent.form.fill.apiKey'),
23
+ baseURL: config.get<string>('agent.form.fill.baseUrl'),
24
+ botId: config.get<string>('agent.form.fill.botId'),
25
+ extraStreamTransformers: [() => createBatchFillingResultTransformer(enableParser)],
12
26
  });
13
27
 
14
28
  const formFillingModel = aiFormFilling.chat('chat');
@@ -1,7 +1,9 @@
1
- import type { JSONObject } from "@ai-sdk/provider";
2
- import config from "config";
3
- import type { ParsedElementInfo } from "@streamparser/json/utils/types/parsedElementInfo.js";
4
- import { logger } from "../../../logger/index.js";
1
+ import type { JSONObject, LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
+ import { type FormFillingMode } from '@baishuyun/types';
3
+ import config from 'config';
4
+ import type { ParsedElementInfo } from '@streamparser/json/utils/types/parsedElementInfo.js';
5
+ import { logger } from '../../../logger/index.js';
6
+ import type { TextUIPart } from 'ai';
5
7
 
6
8
  export const isSubFormField = (parsedInfo: ParsedElementInfo) => {
7
9
  if (!parsedInfo || !parsedInfo.stack?.length) {
@@ -9,7 +11,7 @@ export const isSubFormField = (parsedInfo: ParsedElementInfo) => {
9
11
  }
10
12
 
11
13
  const lastStackItem = parsedInfo.stack[parsedInfo.stack.length - 1];
12
- return lastStackItem?.key === "value";
14
+ return lastStackItem?.key === 'value';
13
15
  };
14
16
 
15
17
  export const getLastStackItem = (parsedInfo: ParsedElementInfo) => {
@@ -26,28 +28,28 @@ export const getLastSubFormField = (parsedInfo: ParsedElementInfo) => {
26
28
  }
27
29
 
28
30
  const targetStackItem = parsedInfo.stack.findLast((s) => {
29
- return (s.value as JSONObject)?.fieldType === "subform";
31
+ return (s.value as JSONObject)?.fieldType === 'subform';
30
32
  });
31
33
 
32
34
  return targetStackItem;
33
35
  };
34
36
 
35
37
  export const parseImg = async (file: File) => {
36
- const isImg = file.type.startsWith("image/");
38
+ const isImg = file.type.startsWith('image/');
37
39
  if (!isImg) {
38
40
  return null;
39
41
  }
40
42
 
41
- const ocrApiKey = config.get<string>("agent.form.fill.ocrApiKey");
42
- const ocrEndpoint = "https://api.ocr.space/parse/image";
43
+ const ocrApiKey = config.get<string>('agent.form.fill.ocrApiKey');
44
+ const ocrEndpoint = 'https://api.ocr.space/parse/image';
43
45
 
44
46
  const formData = new FormData();
45
47
 
46
- formData.append("language", "chs");
47
- formData.append("file", file);
48
+ formData.append('language', 'chs');
49
+ formData.append('file', file);
48
50
 
49
51
  const response = await fetch(ocrEndpoint, {
50
- method: "POST",
52
+ method: 'POST',
51
53
  body: formData,
52
54
  headers: {
53
55
  // Add any auth headers
@@ -59,17 +61,51 @@ export const parseImg = async (file: File) => {
59
61
  console.log(result);
60
62
 
61
63
  if (result.IsErroredOnProcessing) {
62
- logger.error("image ocr parse error");
64
+ logger.error('image ocr parse error');
63
65
  return null;
64
66
  }
65
67
 
66
68
  if (!result.ParsedResults || !result.ParsedResults.length) {
67
- logger.debug("image ocr parse no result");
69
+ logger.debug('image ocr parse no result');
68
70
  return null;
69
71
  }
70
72
 
71
73
  // join all parsed text
72
74
  const parsedTexts = result.ParsedResults.map((pr: any) => pr.ParsedText);
73
75
 
74
- return parsedTexts.join("\n");
76
+ return parsedTexts.join('\n');
77
+ };
78
+
79
+ export const mode2part = (mode: FormFillingMode, count: number = 10): TextUIPart => {
80
+ const modeMap: Record<FormFillingMode, TextUIPart> = {
81
+ AI: {
82
+ type: 'text',
83
+ text: '智能填写当前表单数据',
84
+ },
85
+ batch: {
86
+ type: 'text',
87
+ text: `为表单批量填写示例数据`,
88
+ },
89
+ normal: {
90
+ type: 'text',
91
+ text: '解析输入作为当前表单的数据',
92
+ },
93
+ };
94
+
95
+ return modeMap[mode];
96
+ };
97
+
98
+ export const fillingChunkGuard = (
99
+ chunk: LanguageModelV2StreamPart,
100
+ enableGuard: boolean = true
101
+ ) => {
102
+ if (!enableGuard) {
103
+ return false;
104
+ }
105
+
106
+ if (chunk.type !== 'text-delta' || !('delta' in chunk)) {
107
+ return false;
108
+ }
109
+
110
+ return true;
75
111
  };