@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.
@@ -1,11 +1,12 @@
1
- import config from "config";
2
- import { logger } from "../../../logger/index.js";
1
+ import {} from '@baishuyun/types';
2
+ import config from 'config';
3
+ import { logger } from '../../../logger/index.js';
3
4
  export const isSubFormField = (parsedInfo) => {
4
5
  if (!parsedInfo || !parsedInfo.stack?.length) {
5
6
  return false;
6
7
  }
7
8
  const lastStackItem = parsedInfo.stack[parsedInfo.stack.length - 1];
8
- return lastStackItem?.key === "value";
9
+ return lastStackItem?.key === 'value';
9
10
  };
10
11
  export const getLastStackItem = (parsedInfo) => {
11
12
  if (!parsedInfo || !parsedInfo.stack?.length) {
@@ -18,22 +19,22 @@ export const getLastSubFormField = (parsedInfo) => {
18
19
  return null;
19
20
  }
20
21
  const targetStackItem = parsedInfo.stack.findLast((s) => {
21
- return s.value?.fieldType === "subform";
22
+ return s.value?.fieldType === 'subform';
22
23
  });
23
24
  return targetStackItem;
24
25
  };
25
26
  export const parseImg = async (file) => {
26
- const isImg = file.type.startsWith("image/");
27
+ const isImg = file.type.startsWith('image/');
27
28
  if (!isImg) {
28
29
  return null;
29
30
  }
30
- const ocrApiKey = config.get("agent.form.fill.ocrApiKey");
31
- const ocrEndpoint = "https://api.ocr.space/parse/image";
31
+ const ocrApiKey = config.get('agent.form.fill.ocrApiKey');
32
+ const ocrEndpoint = 'https://api.ocr.space/parse/image';
32
33
  const formData = new FormData();
33
- formData.append("language", "chs");
34
- formData.append("file", file);
34
+ formData.append('language', 'chs');
35
+ formData.append('file', file);
35
36
  const response = await fetch(ocrEndpoint, {
36
- method: "POST",
37
+ method: 'POST',
37
38
  body: formData,
38
39
  headers: {
39
40
  // Add any auth headers
@@ -43,14 +44,40 @@ export const parseImg = async (file) => {
43
44
  const result = await response.json();
44
45
  console.log(result);
45
46
  if (result.IsErroredOnProcessing) {
46
- logger.error("image ocr parse error");
47
+ logger.error('image ocr parse error');
47
48
  return null;
48
49
  }
49
50
  if (!result.ParsedResults || !result.ParsedResults.length) {
50
- logger.debug("image ocr parse no result");
51
+ logger.debug('image ocr parse no result');
51
52
  return null;
52
53
  }
53
54
  // join all parsed text
54
55
  const parsedTexts = result.ParsedResults.map((pr) => pr.ParsedText);
55
- return parsedTexts.join("\n");
56
+ return parsedTexts.join('\n');
57
+ };
58
+ export const mode2part = (mode, count = 10) => {
59
+ const modeMap = {
60
+ AI: {
61
+ type: 'text',
62
+ text: '智能填写当前表单数据',
63
+ },
64
+ batch: {
65
+ type: 'text',
66
+ text: `为表单批量填写示例数据`,
67
+ },
68
+ normal: {
69
+ type: 'text',
70
+ text: '解析输入作为当前表单的数据',
71
+ },
72
+ };
73
+ return modeMap[mode];
74
+ };
75
+ export const fillingChunkGuard = (chunk, enableGuard = true) => {
76
+ if (!enableGuard) {
77
+ return false;
78
+ }
79
+ if (chunk.type !== 'text-delta' || !('delta' in chunk)) {
80
+ return false;
81
+ }
82
+ return true;
56
83
  };
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ apps: [
3
+ {
4
+ name: 'chat-sdk-backend', // 进程名称
5
+ script: './dist/src/index.js', // 启动入口文件
6
+ instances: 'max', // 启动多进程(利用多核CPU,可选)
7
+ autorestart: true, // 异常时自动重启(核心)
8
+ watch: false, // 生产环境关闭文件监听(避免容器内文件变动触发重启)
9
+ max_memory_restart: '2G', // 内存超过 1G 自动重启(防止内存泄漏)
10
+ env: {
11
+ // 开发环境变量
12
+ NODE_ENV: 'development',
13
+ },
14
+ env_production: {
15
+ // 生产环境变量
16
+ NODE_ENV: 'production',
17
+ },
18
+ log_date_format: 'YYYY-MM-DD HH:mm:ss', // 日志时间格式
19
+ error_file: './logs/err.log', // 错误日志路径
20
+ out_file: './logs/out.log', // 输出日志路径
21
+ },
22
+ ],
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/chat-backend",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
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.5",
26
- "@baishuyun/types": "1.0.5"
25
+ "@baishuyun/coze-provider": "0.0.7",
26
+ "@baishuyun/types": "1.0.7"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/config": "^3.3.5",
@@ -31,13 +31,16 @@
31
31
  "@types/node": "^20.11.17",
32
32
  "cross-env": "^10.1.0",
33
33
  "pino-pretty": "^13.1.3",
34
+ "pm2": "^6.0.14",
34
35
  "tsx": "^4.7.1",
35
36
  "typescript": "^5.8.3",
36
- "@baishuyun/typescript-config": "0.0.5"
37
+ "@baishuyun/typescript-config": "0.0.7"
37
38
  },
38
39
  "scripts": {
39
40
  "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
40
41
  "build": "tsc",
41
- "start": "cross-env NODE_ENV=production NODE_CONFIG_DIR=./dist/config node dist/src/index.js"
42
+ "start": "cross-env NODE_ENV=production NODE_CONFIG_DIR=./dist/config pm2-runtime ecosystem.config.cjs --env production",
43
+ "pm2start": "cross-env NODE_ENV=production NODE_CONFIG_DIR=./dist/config pm2 ecosystem.config.cjs --env production",
44
+ "pm2": "pm2"
42
45
  }
43
46
  }
package/src/app/main.ts CHANGED
@@ -9,9 +9,9 @@ import app from '../config/hono.config.js';
9
9
  import { createFormRouter } from '../routes/form/form.route.js';
10
10
 
11
11
  // 挂载子路由
12
- app.route('api/form', createFormRouter());
12
+ app.route('web/api/form', createFormRouter());
13
13
 
14
14
  // add test route
15
- app.get('/api/test', (c) => {
15
+ app.get('web/api/test', (c) => {
16
16
  return c.json({ message: 'Test route is working!' });
17
17
  });
@@ -1,9 +1,9 @@
1
- import { Hono } from "hono";
2
- import { serve } from "@hono/node-server";
3
- import config from "config";
4
- import { cors } from "hono/cors";
5
- import { logMiddleware } from "../logger/log-middleware.js";
6
- import { logger } from "../logger/index.js";
1
+ import { Hono } from 'hono';
2
+ import { serve } from '@hono/node-server';
3
+ import config from 'config';
4
+ import { cors } from 'hono/cors';
5
+ import { logMiddleware } from '../logger/log-middleware.js';
6
+ import { logger } from '../logger/index.js';
7
7
 
8
8
  /**
9
9
  * 公共配置层,跨域、日志等中间件配置
@@ -11,7 +11,7 @@ import { logger } from "../logger/index.js";
11
11
  const app = new Hono();
12
12
 
13
13
  // Enable CORS for all routes starting with /api/
14
- app.use("/api/*", cors());
14
+ app.use('web/api/*', cors());
15
15
 
16
16
  // Logging middleware
17
17
  app.use(logMiddleware);
@@ -26,22 +26,22 @@ app.onError((err, c) => {
26
26
  path: c.req.path,
27
27
  status: 500, // 默认 500 错误
28
28
  },
29
- "Request failed with unhandled error",
29
+ 'Request failed with unhandled error'
30
30
  ); // 错误描述
31
31
 
32
32
  // 返回统一的 JSON 错误响应
33
- return c.json({ message: "Internal Server Error" }, 500);
33
+ return c.json({ message: 'Internal Server Error' }, 500);
34
34
  });
35
35
 
36
36
  serve(
37
37
  {
38
38
  fetch: app.fetch,
39
- hostname: config.get<string>("app.host") || "",
40
- port: config.get<number>("app.port") || 3001,
39
+ hostname: config.get<string>('app.host') || '',
40
+ port: config.get<number>('app.port') || 3001,
41
41
  },
42
42
  (info) => {
43
43
  logger.info(`Server is running on http://localhost:${info.port}`);
44
- },
44
+ }
45
45
  );
46
46
 
47
47
  export default app;
@@ -25,14 +25,15 @@ export const buildForm = async (c: Context) => {
25
25
 
26
26
  const stream = streamText({
27
27
  model: createModel([
28
- createFieldsJsonTransformStream(isBuildStage),
29
- new SuggestionTransformStream(isBuildStage),
28
+ () => createFieldsJsonTransformStream(isBuildStage),
29
+ () => new SuggestionTransformStream(isBuildStage),
30
30
  ]),
31
31
  messages: convertToModelMessages(requestBody.messages),
32
32
  includeRawChunks: true,
33
33
  headers: {
34
34
  "x-user-stage": requestBody.stage,
35
35
  "x-user-id": Date.now().toString(),
36
+ "x-user-token": c.req.header("authorization") || "",
36
37
  },
37
38
  });
38
39
 
@@ -3,7 +3,7 @@ import { createCoze } from "@baishuyun/coze-provider";
3
3
  import config from "config";
4
4
 
5
5
  export const createModel = (
6
- extraStreamTransformers: TransformStream<LanguageModelV2StreamPart, any>[],
6
+ extraStreamTransformers: (() => TransformStream<LanguageModelV2StreamPart, any>)[]
7
7
  ) => {
8
8
  const coze = createCoze({
9
9
  apiKey: config.get<string>("agent.form.build.apiKey"),
@@ -0,0 +1,202 @@
1
+ import { type Context } from 'hono';
2
+ import {
3
+ convertToModelMessages,
4
+ streamText,
5
+ createUIMessageStream,
6
+ createUIMessageStreamResponse,
7
+ type UIMessage,
8
+ type TextUIPart,
9
+ generateId,
10
+ type ModelMessage,
11
+ } from 'ai';
12
+ import { logger } from '../../../logger/index.js';
13
+ import { createBatchFillingModel } from './model.js';
14
+ import { mode2part } from './utils.js';
15
+ import type { FormFillingMode } from '@baishuyun/types';
16
+
17
+ const getModelMessagesFromUserMessages = ({
18
+ continueMessageId,
19
+ currentMsg,
20
+ formStructure,
21
+ mode,
22
+ messages,
23
+ }: {
24
+ formStructure: Array<any>;
25
+ mode: FormFillingMode;
26
+ messages: UIMessage[];
27
+
28
+ continueMessageId?: string;
29
+ currentMsg?: string;
30
+ }): ModelMessage[] => {
31
+ // init fill
32
+ if (continueMessageId && currentMsg) {
33
+ return convertToModelMessages([
34
+ {
35
+ role: 'user',
36
+ parts: [
37
+ {
38
+ type: 'text',
39
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
40
+ },
41
+ mode2part(mode),
42
+ {
43
+ type: 'text',
44
+ text: currentMsg,
45
+ },
46
+ ],
47
+ },
48
+ ]);
49
+ }
50
+
51
+ let lastUserMsg: UIMessage | undefined = messages.findLast((m) => m.role === 'user') || {
52
+ id: generateId(),
53
+ role: 'user',
54
+ parts: [],
55
+ };
56
+
57
+ lastUserMsg.parts.push({
58
+ type: 'text',
59
+ text: `formStructure: ${JSON.stringify(formStructure)}`,
60
+ });
61
+
62
+ lastUserMsg.parts.push(mode2part(mode));
63
+
64
+ return convertToModelMessages([lastUserMsg]);
65
+ };
66
+
67
+ /**
68
+ * 表单填写
69
+ * @param c
70
+ * @returns
71
+ */
72
+ export const batchFillForm = async (c: Context) => {
73
+ let requestBody;
74
+
75
+ try {
76
+ const json = await c.req.json();
77
+ requestBody = json;
78
+ } catch (_) {
79
+ return c.json({ error: 'Invalid JSON' }, 400);
80
+ }
81
+
82
+ const formStructure = requestBody.formStructure;
83
+ const mode = requestBody.mode;
84
+ const continueMessageId = requestBody.continueMessageId; // 新增:继续生成的消息 ID
85
+ const userMessages: UIMessage[] = requestBody.messages;
86
+
87
+ // clear empty user text part message
88
+ const messages = userMessages.filter((msg) => {
89
+ if (msg.role !== 'user') {
90
+ return true;
91
+ }
92
+
93
+ const firstPart = msg.parts[0] as TextUIPart | undefined;
94
+ if (!firstPart) {
95
+ return false;
96
+ }
97
+
98
+ if (firstPart.type === 'text' && firstPart.text.trim() === '') {
99
+ return false;
100
+ }
101
+
102
+ return true;
103
+ });
104
+
105
+ // 如果是继续生成模式
106
+ if (continueMessageId) {
107
+ return handleContinueGeneration(c, {
108
+ messages,
109
+ formStructure,
110
+ continueMessageId,
111
+ msg: requestBody.msg,
112
+ mode,
113
+ stage: requestBody.stage,
114
+ });
115
+ }
116
+
117
+ const modelMessages = getModelMessagesFromUserMessages({
118
+ messages,
119
+ mode,
120
+ formStructure,
121
+ });
122
+
123
+ // 正常创建新流
124
+ const stream = streamText({
125
+ model: createBatchFillingModel(),
126
+ messages: modelMessages,
127
+ includeRawChunks: true,
128
+ headers: {
129
+ 'x-user-stage': requestBody.stage,
130
+ 'x-user-token': c.req.header('authorization') || '',
131
+ 'x-user-var': JSON.stringify({
132
+ mode: requestBody.mode,
133
+ }),
134
+ },
135
+ });
136
+
137
+ return stream.toUIMessageStreamResponse({
138
+ originalMessages: messages, // 建议添加,便于消息 ID 管理
139
+ });
140
+ };
141
+
142
+ /**
143
+ * 处理继续生成(扩展同一消息)
144
+ */
145
+ async function handleContinueGeneration(
146
+ c: Context,
147
+ {
148
+ messages,
149
+ continueMessageId,
150
+ mode,
151
+ msg,
152
+ formStructure,
153
+ stage,
154
+ }: {
155
+ messages: UIMessage[];
156
+ formStructure: Array<any>;
157
+ continueMessageId: string;
158
+ mode: FormFillingMode;
159
+ msg: string;
160
+ stage: string;
161
+ }
162
+ ) {
163
+ const stream = createUIMessageStream({
164
+ execute: async ({ writer }) => {
165
+ writer.write({
166
+ type: 'start',
167
+ messageId: continueMessageId,
168
+ });
169
+
170
+ // 构建模型消息
171
+ const modelMessages = getModelMessagesFromUserMessages({
172
+ messages,
173
+ continueMessageId,
174
+ currentMsg: msg,
175
+ mode,
176
+ formStructure,
177
+ });
178
+
179
+ logger.debug(`continue modelMessages ${JSON.stringify(modelMessages)}`);
180
+
181
+ const result = streamText({
182
+ model: createBatchFillingModel(),
183
+ messages: modelMessages,
184
+ includeRawChunks: true,
185
+ headers: {
186
+ 'x-user-stage': stage,
187
+ 'x-user-var': JSON.stringify({
188
+ mode,
189
+ }),
190
+ },
191
+ });
192
+
193
+ writer.merge(
194
+ result.toUIMessageStream({
195
+ sendStart: false,
196
+ })
197
+ );
198
+ },
199
+ });
200
+
201
+ return createUIMessageStreamResponse({ stream });
202
+ }
@@ -0,0 +1,156 @@
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 { fillingChunkGuard as chunkGuard } from './utils.js';
6
+ import { logger } from '../../../logger/index.js';
7
+
8
+ export const createBatchFillingResultTransformer = (enableJsonParser: boolean) => {
9
+ let parser: JSONParser;
10
+ let id: string;
11
+ // 解析完成标记:用于等待解析器内部队列处理完毕
12
+ let parseCompleted: Promise<void>;
13
+ let resolveParseCompleted: () => void;
14
+
15
+ const transformer = {
16
+ flush: async (controller: TransformStreamDefaultController<any>) => {
17
+ try {
18
+ if (parser) {
19
+ await parseCompleted;
20
+ logger.debug('stop parser');
21
+ }
22
+ controller.terminate(); // 信号通知下游可读流已关闭
23
+ } catch (e) {
24
+ controller.error('stop error' + e);
25
+ // controller.terminate(); // 信号通知下游可读流已关闭
26
+ }
27
+ },
28
+
29
+ transform: (
30
+ chunk: LanguageModelV2StreamPart,
31
+ controller: TransformStreamDefaultController<LanguageModelV2StreamPart>
32
+ ) => {
33
+ if (!parser) {
34
+ logger.warn('json parser not init');
35
+ return;
36
+ }
37
+
38
+ const enqueueTextDelta = createTextInfoEnqueuer(controller);
39
+
40
+ try {
41
+ if (chunkGuard(chunk)) {
42
+ if ('id' in chunk && 'delta' in chunk) {
43
+ id = chunk.id;
44
+
45
+ const regExp = /`?`?`?/;
46
+
47
+ let delta = chunk.delta.replace(regExp, '');
48
+ parser.write(delta.replace('json', ''));
49
+ }
50
+ } else {
51
+ // logger.debug(`bypass chunk in batch filling transformer: ${JSON.stringify(chunk)}`);
52
+ controller.enqueue(chunk);
53
+ }
54
+ } catch (e) {
55
+ logger.debug('exception in transform while processing filling chunk');
56
+ controller.error(e);
57
+ enqueueTextDelta('error', {
58
+ type: 'agent-error',
59
+ error: '解析异常,请刷新重试',
60
+ });
61
+
62
+ resolveParseCompleted?.();
63
+ controller.terminate(); // 信号通知下游可读流已关闭
64
+ }
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
+ if (value.error && value.errorMessage === 'timeout') {
84
+ enqueueTextDelta(JSON.stringify(value), {
85
+ type: 'agent-error',
86
+ error: '操作超时,请刷新重试',
87
+ });
88
+
89
+ resolveParseCompleted?.();
90
+ controller.terminate(); // 信号通知下游可读流已关闭
91
+ return;
92
+ }
93
+
94
+ const subFormField = getLastSubFormField(parsedInfo);
95
+ if (subFormField) {
96
+ // enqueueTextDelta(
97
+ // JSON.stringify(value),
98
+ // {
99
+ // type: 'mcp-fields-json',
100
+ // field: subFormField.value,
101
+ // },
102
+ // id,
103
+ // true
104
+ // );
105
+
106
+ return;
107
+ }
108
+
109
+ if (value.fieldType === 'subform') {
110
+ return;
111
+ }
112
+
113
+ // logger.debug(`id in onValue: ${id}`);
114
+ enqueueTextDelta(
115
+ `${JSON.stringify(value)},`,
116
+ {
117
+ type: 'mcp-fields-json',
118
+ field: value,
119
+ },
120
+ id,
121
+ true
122
+ );
123
+ };
124
+
125
+ let errorLogged = false;
126
+ parser.onError = (err: any) => {
127
+ if (!errorLogged) {
128
+ console.error('JsonWidgetStream: JSON Parsing Error:', err);
129
+ errorLogged = true;
130
+ }
131
+
132
+ enqueueTextDelta('error', {
133
+ type: 'agent-error',
134
+ error: '操作超时,请刷新重试',
135
+ });
136
+
137
+ resolveParseCompleted?.();
138
+ controller.terminate(); // 信号通知下游可读流已关闭
139
+ };
140
+
141
+ parser.onEnd = () => {
142
+ enqueueTextDelta(
143
+ ' ',
144
+ JSON.stringify({
145
+ appendConfirm: 'save-fields',
146
+ })
147
+ );
148
+
149
+ logger.debug('mark as end');
150
+ resolveParseCompleted?.();
151
+ };
152
+ },
153
+ };
154
+
155
+ return new TransformStream<LanguageModelV2StreamPart, any>(transformer);
156
+ };