@baishuyun/chat-backend 0.0.2 → 0.0.3
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 +17 -17
- package/CHANGELOG.md +17 -9
- package/README.md +8 -8
- package/config/default.ts +35 -35
- package/config/index.ts +14 -14
- package/package.json +4 -4
- package/src/app/main.ts +17 -17
- package/src/config/hono.config.ts +47 -47
- package/src/controllers/form/attachment-upload.controller.ts +57 -57
- package/src/controllers/form/build/build.controller.ts +40 -40
- package/src/controllers/form/build/model.ts +16 -16
- package/src/controllers/form/conversation/clear.controller.ts +31 -31
- package/src/controllers/form/fill/createFieldsFillingResultTransformStream.ts +138 -138
- package/src/controllers/form/fill/fill.controller.ts +45 -45
- package/src/controllers/form/fill/model.ts +17 -17
- package/src/controllers/form/fill/utils.ts +75 -75
- package/src/index.ts +2 -2
- package/src/logger/index.ts +54 -54
- package/src/logger/log-middleware.ts +25 -25
- package/src/routes/form/form.route.ts +23 -23
- package/tsconfig.json +10 -10
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
import { type LanguageModelV2StreamPart } from '@ai-sdk/provider';
|
|
2
|
-
import { JSONParser } from '@streamparser/json';
|
|
3
|
-
import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
|
|
4
|
-
import { getLastSubFormField } from './utils.js';
|
|
5
|
-
import { logger } from '../../../logger/index.js';
|
|
6
|
-
|
|
7
|
-
export const createFieldsFillingResultTransformer = (enableJsonParser: boolean) => {
|
|
8
|
-
let parser: JSONParser;
|
|
9
|
-
let id: string;
|
|
10
|
-
// 解析完成标记:用于等待解析器内部队列处理完毕
|
|
11
|
-
let parseCompleted: Promise<void>;
|
|
12
|
-
let resolveParseCompleted: () => void;
|
|
13
|
-
|
|
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
|
-
const transformer = {
|
|
27
|
-
flush: async (controller: TransformStreamDefaultController<any>) => {
|
|
28
|
-
try {
|
|
29
|
-
if (parser) {
|
|
30
|
-
await parseCompleted; // 等待闭包中的完成标记
|
|
31
|
-
logger.debug('stop parser');
|
|
32
|
-
// parser.end(); // 此时调用 end() 不会出现未完成 Token 异常
|
|
33
|
-
}
|
|
34
|
-
controller.terminate(); // 信号通知下游可读流已关闭
|
|
35
|
-
} catch (e) {
|
|
36
|
-
controller.error('stop error' + e);
|
|
37
|
-
// controller.terminate(); // 信号通知下游可读流已关闭
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
transform: (
|
|
42
|
-
chunk: LanguageModelV2StreamPart,
|
|
43
|
-
controller: TransformStreamDefaultController<LanguageModelV2StreamPart>
|
|
44
|
-
) => {
|
|
45
|
-
if (!parser) {
|
|
46
|
-
logger.warn('json parser not init');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
if (chunkGuard(chunk)) {
|
|
52
|
-
if ('id' in chunk && 'delta' in chunk) {
|
|
53
|
-
id = chunk.id;
|
|
54
|
-
|
|
55
|
-
const regExp = /`?`?`?/;
|
|
56
|
-
|
|
57
|
-
let delta = chunk.delta.replace(regExp, '');
|
|
58
|
-
parser.write(delta.replace('json', ''));
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
controller.enqueue(chunk);
|
|
62
|
-
}
|
|
63
|
-
} catch (e) {
|
|
64
|
-
controller.error(e);
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
start: (controller: TransformStreamDefaultController<any>) => {
|
|
68
|
-
parser = new JSONParser({
|
|
69
|
-
paths: ['$.*', '$.*.value.*.*'],
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
parseCompleted = new Promise((resolve) => {
|
|
73
|
-
resolveParseCompleted = resolve;
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const enqueueTextDelta = enableJsonParser
|
|
77
|
-
? createTextInfoEnqueuer(controller)
|
|
78
|
-
: (content: string) => {};
|
|
79
|
-
|
|
80
|
-
parser.onValue = (parsedInfo: any) => {
|
|
81
|
-
const value = parsedInfo.value;
|
|
82
|
-
|
|
83
|
-
const subFormField = getLastSubFormField(parsedInfo);
|
|
84
|
-
if (subFormField) {
|
|
85
|
-
enqueueTextDelta(JSON.stringify(value), {
|
|
86
|
-
type: 'mcp-fields-json',
|
|
87
|
-
field: subFormField.value,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (value.fieldType === 'subform') {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
enqueueTextDelta(
|
|
98
|
-
JSON.stringify(value),
|
|
99
|
-
{
|
|
100
|
-
type: 'mcp-fields-json',
|
|
101
|
-
field: value,
|
|
102
|
-
},
|
|
103
|
-
id
|
|
104
|
-
);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
let errorLogged = false;
|
|
108
|
-
parser.onError = (err: any) => {
|
|
109
|
-
// controller.enqueue({
|
|
110
|
-
// type: "error",
|
|
111
|
-
// error: "JsonWidgetStream: JSON Parsing Error:" + err.message,
|
|
112
|
-
// });
|
|
113
|
-
if (!errorLogged) {
|
|
114
|
-
console.error('JsonWidgetStream: JSON Parsing Error:', err);
|
|
115
|
-
errorLogged = true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// close stream
|
|
119
|
-
logger.error('JsonStream: closing stream due to parsing error');
|
|
120
|
-
controller.terminate(); // 信号通知下游可读流已关闭
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
parser.onEnd = () => {
|
|
124
|
-
enqueueTextDelta(
|
|
125
|
-
' ',
|
|
126
|
-
JSON.stringify({
|
|
127
|
-
appendConfirm: 'save-fields',
|
|
128
|
-
})
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
logger.debug('mark as end');
|
|
132
|
-
resolveParseCompleted?.();
|
|
133
|
-
};
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return new TransformStream<LanguageModelV2StreamPart, any>(transformer);
|
|
138
|
-
};
|
|
1
|
+
import { type LanguageModelV2StreamPart } from '@ai-sdk/provider';
|
|
2
|
+
import { JSONParser } from '@streamparser/json';
|
|
3
|
+
import { createTextInfoEnqueuer } from '@baishuyun/coze-provider';
|
|
4
|
+
import { getLastSubFormField } from './utils.js';
|
|
5
|
+
import { logger } from '../../../logger/index.js';
|
|
6
|
+
|
|
7
|
+
export const createFieldsFillingResultTransformer = (enableJsonParser: boolean) => {
|
|
8
|
+
let parser: JSONParser;
|
|
9
|
+
let id: string;
|
|
10
|
+
// 解析完成标记:用于等待解析器内部队列处理完毕
|
|
11
|
+
let parseCompleted: Promise<void>;
|
|
12
|
+
let resolveParseCompleted: () => void;
|
|
13
|
+
|
|
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
|
+
const transformer = {
|
|
27
|
+
flush: async (controller: TransformStreamDefaultController<any>) => {
|
|
28
|
+
try {
|
|
29
|
+
if (parser) {
|
|
30
|
+
await parseCompleted; // 等待闭包中的完成标记
|
|
31
|
+
logger.debug('stop parser');
|
|
32
|
+
// parser.end(); // 此时调用 end() 不会出现未完成 Token 异常
|
|
33
|
+
}
|
|
34
|
+
controller.terminate(); // 信号通知下游可读流已关闭
|
|
35
|
+
} catch (e) {
|
|
36
|
+
controller.error('stop error' + e);
|
|
37
|
+
// controller.terminate(); // 信号通知下游可读流已关闭
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
transform: (
|
|
42
|
+
chunk: LanguageModelV2StreamPart,
|
|
43
|
+
controller: TransformStreamDefaultController<LanguageModelV2StreamPart>
|
|
44
|
+
) => {
|
|
45
|
+
if (!parser) {
|
|
46
|
+
logger.warn('json parser not init');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (chunkGuard(chunk)) {
|
|
52
|
+
if ('id' in chunk && 'delta' in chunk) {
|
|
53
|
+
id = chunk.id;
|
|
54
|
+
|
|
55
|
+
const regExp = /`?`?`?/;
|
|
56
|
+
|
|
57
|
+
let delta = chunk.delta.replace(regExp, '');
|
|
58
|
+
parser.write(delta.replace('json', ''));
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
controller.enqueue(chunk);
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
controller.error(e);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
start: (controller: TransformStreamDefaultController<any>) => {
|
|
68
|
+
parser = new JSONParser({
|
|
69
|
+
paths: ['$.*', '$.*.value.*.*'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
parseCompleted = new Promise((resolve) => {
|
|
73
|
+
resolveParseCompleted = resolve;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const enqueueTextDelta = enableJsonParser
|
|
77
|
+
? createTextInfoEnqueuer(controller)
|
|
78
|
+
: (content: string) => {};
|
|
79
|
+
|
|
80
|
+
parser.onValue = (parsedInfo: any) => {
|
|
81
|
+
const value = parsedInfo.value;
|
|
82
|
+
|
|
83
|
+
const subFormField = getLastSubFormField(parsedInfo);
|
|
84
|
+
if (subFormField) {
|
|
85
|
+
enqueueTextDelta(JSON.stringify(value), {
|
|
86
|
+
type: 'mcp-fields-json',
|
|
87
|
+
field: subFormField.value,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value.fieldType === 'subform') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
enqueueTextDelta(
|
|
98
|
+
JSON.stringify(value),
|
|
99
|
+
{
|
|
100
|
+
type: 'mcp-fields-json',
|
|
101
|
+
field: value,
|
|
102
|
+
},
|
|
103
|
+
id
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let errorLogged = false;
|
|
108
|
+
parser.onError = (err: any) => {
|
|
109
|
+
// controller.enqueue({
|
|
110
|
+
// type: "error",
|
|
111
|
+
// error: "JsonWidgetStream: JSON Parsing Error:" + err.message,
|
|
112
|
+
// });
|
|
113
|
+
if (!errorLogged) {
|
|
114
|
+
console.error('JsonWidgetStream: JSON Parsing Error:', err);
|
|
115
|
+
errorLogged = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// close stream
|
|
119
|
+
logger.error('JsonStream: closing stream due to parsing error');
|
|
120
|
+
controller.terminate(); // 信号通知下游可读流已关闭
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
parser.onEnd = () => {
|
|
124
|
+
enqueueTextDelta(
|
|
125
|
+
' ',
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
appendConfirm: 'save-fields',
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
logger.debug('mark as end');
|
|
132
|
+
resolveParseCompleted?.();
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return new TransformStream<LanguageModelV2StreamPart, any>(transformer);
|
|
138
|
+
};
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { type Context } from 'hono';
|
|
2
|
-
import { convertToModelMessages, streamText } from 'ai';
|
|
3
|
-
import { logger } from '../../../logger/index.js';
|
|
4
|
-
import { createFillingModel } from './model.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 表单填写
|
|
8
|
-
* @param c
|
|
9
|
-
* @returns
|
|
10
|
-
*/
|
|
11
|
-
export const fillForm = async (c: Context) => {
|
|
12
|
-
let requestBody;
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const json = await c.req.json();
|
|
16
|
-
requestBody = json;
|
|
17
|
-
} catch (_) {
|
|
18
|
-
return c.json({ error: 'Invalid JSON' }, 400);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const formStructure = requestBody.formStructure;
|
|
22
|
-
if (formStructure) {
|
|
23
|
-
try {
|
|
24
|
-
let lastMsg = requestBody.messages[requestBody.messages.length - 1];
|
|
25
|
-
|
|
26
|
-
lastMsg.parts.push({
|
|
27
|
-
type: 'text',
|
|
28
|
-
text: JSON.stringify(formStructure),
|
|
29
|
-
});
|
|
30
|
-
} catch (e) {
|
|
31
|
-
logger.error('Failed to append form structure');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const stream = streamText({
|
|
36
|
-
model: createFillingModel(),
|
|
37
|
-
messages: convertToModelMessages(requestBody.messages),
|
|
38
|
-
includeRawChunks: true,
|
|
39
|
-
headers: {
|
|
40
|
-
'x-user-stage': requestBody.stage,
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return stream.toUIMessageStreamResponse();
|
|
45
|
-
};
|
|
1
|
+
import { type Context } from 'hono';
|
|
2
|
+
import { convertToModelMessages, streamText } from 'ai';
|
|
3
|
+
import { logger } from '../../../logger/index.js';
|
|
4
|
+
import { createFillingModel } from './model.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 表单填写
|
|
8
|
+
* @param c
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
export const fillForm = async (c: Context) => {
|
|
12
|
+
let requestBody;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const json = await c.req.json();
|
|
16
|
+
requestBody = json;
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return c.json({ error: 'Invalid JSON' }, 400);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const formStructure = requestBody.formStructure;
|
|
22
|
+
if (formStructure) {
|
|
23
|
+
try {
|
|
24
|
+
let lastMsg = requestBody.messages[requestBody.messages.length - 1];
|
|
25
|
+
|
|
26
|
+
lastMsg.parts.push({
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify(formStructure),
|
|
29
|
+
});
|
|
30
|
+
} catch (e) {
|
|
31
|
+
logger.error('Failed to append form structure');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const stream = streamText({
|
|
36
|
+
model: createFillingModel(),
|
|
37
|
+
messages: convertToModelMessages(requestBody.messages),
|
|
38
|
+
includeRawChunks: true,
|
|
39
|
+
headers: {
|
|
40
|
+
'x-user-stage': requestBody.stage,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return stream.toUIMessageStreamResponse();
|
|
45
|
+
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { createCoze } from '@baishuyun/coze-provider';
|
|
2
|
-
import { createFieldsFillingResultTransformer } from './createFieldsFillingResultTransformStream.js';
|
|
3
|
-
|
|
4
|
-
import config from 'config';
|
|
5
|
-
|
|
6
|
-
export const createFillingModel = (enableParser: boolean = true) => {
|
|
7
|
-
const aiFormFilling = createCoze({
|
|
8
|
-
apiKey: config.get<string>('agent.form.fill.apiKey'),
|
|
9
|
-
baseURL: config.get<string>('agent.form.fill.baseUrl'),
|
|
10
|
-
botId: config.get<string>('agent.form.fill.botId'),
|
|
11
|
-
extraStreamTransformers: [createFieldsFillingResultTransformer(enableParser)],
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const formFillingModel = aiFormFilling.chat('chat');
|
|
15
|
-
|
|
16
|
-
return formFillingModel;
|
|
17
|
-
};
|
|
1
|
+
import { createCoze } from '@baishuyun/coze-provider';
|
|
2
|
+
import { createFieldsFillingResultTransformer } from './createFieldsFillingResultTransformStream.js';
|
|
3
|
+
|
|
4
|
+
import config from 'config';
|
|
5
|
+
|
|
6
|
+
export const createFillingModel = (enableParser: boolean = true) => {
|
|
7
|
+
const aiFormFilling = createCoze({
|
|
8
|
+
apiKey: config.get<string>('agent.form.fill.apiKey'),
|
|
9
|
+
baseURL: config.get<string>('agent.form.fill.baseUrl'),
|
|
10
|
+
botId: config.get<string>('agent.form.fill.botId'),
|
|
11
|
+
extraStreamTransformers: [createFieldsFillingResultTransformer(enableParser)],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const formFillingModel = aiFormFilling.chat('chat');
|
|
15
|
+
|
|
16
|
+
return formFillingModel;
|
|
17
|
+
};
|
|
@@ -1,75 +1,75 @@
|
|
|
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";
|
|
5
|
-
|
|
6
|
-
export const isSubFormField = (parsedInfo: ParsedElementInfo) => {
|
|
7
|
-
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const lastStackItem = parsedInfo.stack[parsedInfo.stack.length - 1];
|
|
12
|
-
return lastStackItem?.key === "value";
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const getLastStackItem = (parsedInfo: ParsedElementInfo) => {
|
|
16
|
-
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return parsedInfo.stack[parsedInfo.stack.length - 1];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const getLastSubFormField = (parsedInfo: ParsedElementInfo) => {
|
|
24
|
-
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const targetStackItem = parsedInfo.stack.findLast((s) => {
|
|
29
|
-
return (s.value as JSONObject)?.fieldType === "subform";
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
return targetStackItem;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const parseImg = async (file: File) => {
|
|
36
|
-
const isImg = file.type.startsWith("image/");
|
|
37
|
-
if (!isImg) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ocrApiKey = config.get<string>("agent.form.fill.ocrApiKey");
|
|
42
|
-
const ocrEndpoint = "https://api.ocr.space/parse/image";
|
|
43
|
-
|
|
44
|
-
const formData = new FormData();
|
|
45
|
-
|
|
46
|
-
formData.append("language", "chs");
|
|
47
|
-
formData.append("file", file);
|
|
48
|
-
|
|
49
|
-
const response = await fetch(ocrEndpoint, {
|
|
50
|
-
method: "POST",
|
|
51
|
-
body: formData,
|
|
52
|
-
headers: {
|
|
53
|
-
// Add any auth headers
|
|
54
|
-
apiKey: ocrApiKey,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const result = await response.json();
|
|
59
|
-
console.log(result);
|
|
60
|
-
|
|
61
|
-
if (result.IsErroredOnProcessing) {
|
|
62
|
-
logger.error("image ocr parse error");
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!result.ParsedResults || !result.ParsedResults.length) {
|
|
67
|
-
logger.debug("image ocr parse no result");
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// join all parsed text
|
|
72
|
-
const parsedTexts = result.ParsedResults.map((pr: any) => pr.ParsedText);
|
|
73
|
-
|
|
74
|
-
return parsedTexts.join("\n");
|
|
75
|
-
};
|
|
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";
|
|
5
|
+
|
|
6
|
+
export const isSubFormField = (parsedInfo: ParsedElementInfo) => {
|
|
7
|
+
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lastStackItem = parsedInfo.stack[parsedInfo.stack.length - 1];
|
|
12
|
+
return lastStackItem?.key === "value";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getLastStackItem = (parsedInfo: ParsedElementInfo) => {
|
|
16
|
+
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parsedInfo.stack[parsedInfo.stack.length - 1];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const getLastSubFormField = (parsedInfo: ParsedElementInfo) => {
|
|
24
|
+
if (!parsedInfo || !parsedInfo.stack?.length) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const targetStackItem = parsedInfo.stack.findLast((s) => {
|
|
29
|
+
return (s.value as JSONObject)?.fieldType === "subform";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return targetStackItem;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const parseImg = async (file: File) => {
|
|
36
|
+
const isImg = file.type.startsWith("image/");
|
|
37
|
+
if (!isImg) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ocrApiKey = config.get<string>("agent.form.fill.ocrApiKey");
|
|
42
|
+
const ocrEndpoint = "https://api.ocr.space/parse/image";
|
|
43
|
+
|
|
44
|
+
const formData = new FormData();
|
|
45
|
+
|
|
46
|
+
formData.append("language", "chs");
|
|
47
|
+
formData.append("file", file);
|
|
48
|
+
|
|
49
|
+
const response = await fetch(ocrEndpoint, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: formData,
|
|
52
|
+
headers: {
|
|
53
|
+
// Add any auth headers
|
|
54
|
+
apiKey: ocrApiKey,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await response.json();
|
|
59
|
+
console.log(result);
|
|
60
|
+
|
|
61
|
+
if (result.IsErroredOnProcessing) {
|
|
62
|
+
logger.error("image ocr parse error");
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!result.ParsedResults || !result.ParsedResults.length) {
|
|
67
|
+
logger.debug("image ocr parse no result");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// join all parsed text
|
|
72
|
+
const parsedTexts = result.ParsedResults.map((pr: any) => pr.ParsedText);
|
|
73
|
+
|
|
74
|
+
return parsedTexts.join("\n");
|
|
75
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import "../config/index.js";
|
|
2
|
-
import("./app/main.js");
|
|
1
|
+
import "../config/index.js";
|
|
2
|
+
import("./app/main.js");
|
package/src/logger/index.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import pino from "pino";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import os from "os";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
8
|
-
const isProd = process.env.NODE_ENV === "production";
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
|
|
13
|
-
const logDir = path.join(__dirname, "logs");
|
|
14
|
-
if (isProd && !fs.existsSync(logDir)) {
|
|
15
|
-
fs.mkdirSync(logDir, { recursive: true }); // 递归创建目录
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// 3. 初始化 Pino 日志实例
|
|
19
|
-
const logger = pino(
|
|
20
|
-
{
|
|
21
|
-
level: isDev ? "debug" : "info",
|
|
22
|
-
base: isDev
|
|
23
|
-
? { pid: false, hostname: false }
|
|
24
|
-
: { pid: process.pid, hostname: os.hostname() },
|
|
25
|
-
timestamp: isProd,
|
|
26
|
-
transport: isDev
|
|
27
|
-
? {
|
|
28
|
-
target: "pino-pretty", // 依赖 pino-pretty
|
|
29
|
-
options: {
|
|
30
|
-
colorize: true, // 彩色输出(更易阅读)
|
|
31
|
-
translateTime: "SYS:yyyy-mm-dd HH:MM:ss", // 时间格式转换
|
|
32
|
-
ignore: "pid,hostname", // 忽略无用字段
|
|
33
|
-
singleLine: true, // 单行会输出(简化日志体积)
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
: {
|
|
37
|
-
// 生产环境使用 pino/file 实现日志轮转(官方推荐)
|
|
38
|
-
target: "pino/file",
|
|
39
|
-
options: {
|
|
40
|
-
destination: path.join(logDir, "app.log"), // 基础日志文件路径
|
|
41
|
-
mkdir: true, // 自动创建日志目录(无需手动判断,可选增强)
|
|
42
|
-
// 轮转配置
|
|
43
|
-
rotate: {
|
|
44
|
-
frequency: "daily", // 轮转频率:每日轮转(支持 hourly/weekly/monthly 等)
|
|
45
|
-
maxSize: "10m", // 单个日志文件最大体积(支持 k/m/g 单位)
|
|
46
|
-
maxFiles: 7, // 保留最近 7 个日志文件(自动删除更早的文件)
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
isProd ? pino.destination(path.join(logDir, "app.log")) : undefined,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
export { logger };
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
8
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const logDir = path.join(__dirname, "logs");
|
|
14
|
+
if (isProd && !fs.existsSync(logDir)) {
|
|
15
|
+
fs.mkdirSync(logDir, { recursive: true }); // 递归创建目录
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 3. 初始化 Pino 日志实例
|
|
19
|
+
const logger = pino(
|
|
20
|
+
{
|
|
21
|
+
level: isDev ? "debug" : "info",
|
|
22
|
+
base: isDev
|
|
23
|
+
? { pid: false, hostname: false }
|
|
24
|
+
: { pid: process.pid, hostname: os.hostname() },
|
|
25
|
+
timestamp: isProd,
|
|
26
|
+
transport: isDev
|
|
27
|
+
? {
|
|
28
|
+
target: "pino-pretty", // 依赖 pino-pretty
|
|
29
|
+
options: {
|
|
30
|
+
colorize: true, // 彩色输出(更易阅读)
|
|
31
|
+
translateTime: "SYS:yyyy-mm-dd HH:MM:ss", // 时间格式转换
|
|
32
|
+
ignore: "pid,hostname", // 忽略无用字段
|
|
33
|
+
singleLine: true, // 单行会输出(简化日志体积)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {
|
|
37
|
+
// 生产环境使用 pino/file 实现日志轮转(官方推荐)
|
|
38
|
+
target: "pino/file",
|
|
39
|
+
options: {
|
|
40
|
+
destination: path.join(logDir, "app.log"), // 基础日志文件路径
|
|
41
|
+
mkdir: true, // 自动创建日志目录(无需手动判断,可选增强)
|
|
42
|
+
// 轮转配置
|
|
43
|
+
rotate: {
|
|
44
|
+
frequency: "daily", // 轮转频率:每日轮转(支持 hourly/weekly/monthly 等)
|
|
45
|
+
maxSize: "10m", // 单个日志文件最大体积(支持 k/m/g 单位)
|
|
46
|
+
maxFiles: 7, // 保留最近 7 个日志文件(自动删除更早的文件)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
isProd ? pino.destination(path.join(logDir, "app.log")) : undefined,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export { logger };
|