@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.
- package/CHANGELOG.md +22 -0
- package/config/index.ts +8 -10
- package/dist/config/index.js +7 -10
- package/dist/src/app/main.js +2 -2
- package/dist/src/config/hono.config.js +11 -11
- package/dist/src/controllers/form/build/build.controller.js +3 -2
- package/dist/src/controllers/form/fill/batch-fill.controller.js +139 -0
- package/dist/src/controllers/form/fill/createBatchFillingTransformStream.js +124 -0
- package/dist/src/controllers/form/fill/createFieldsFillingResultTransformStream.js +24 -17
- package/dist/src/controllers/form/fill/fill.controller.js +29 -7
- package/dist/src/controllers/form/fill/model.js +12 -1
- package/dist/src/controllers/form/fill/utils.js +40 -13
- package/ecosystem.config.cjs +23 -0
- package/package.json +8 -5
- package/src/app/main.ts +2 -2
- package/src/config/hono.config.ts +12 -12
- package/src/controllers/form/build/build.controller.ts +3 -2
- package/src/controllers/form/build/model.ts +1 -1
- package/src/controllers/form/fill/batch-fill.controller.ts +202 -0
- package/src/controllers/form/fill/createBatchFillingTransformStream.ts +156 -0
- package/src/controllers/form/fill/createFieldsFillingResultTransformStream.ts +29 -20
- package/src/controllers/form/fill/fill.controller.ts +32 -8
- package/src/controllers/form/fill/model.ts +15 -1
- package/src/controllers/form/fill/utils.ts +51 -15
|
@@ -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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
24
|
-
let lastMsg = requestBody.messages[requestBody.messages.length - 1];
|
|
34
|
+
const extraPart = mode2part(mode, num);
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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>(
|
|
42
|
-
const ocrEndpoint =
|
|
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(
|
|
47
|
-
formData.append(
|
|
48
|
+
formData.append('language', 'chs');
|
|
49
|
+
formData.append('file', file);
|
|
48
50
|
|
|
49
51
|
const response = await fetch(ocrEndpoint, {
|
|
50
|
-
method:
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
};
|